diff --git a/.gitignore b/.gitignore index 460b0b95df..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,13 +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 @@ -220,4 +315,3 @@ Kopie von* src/SmartStoreNET.Packager.sln Log.txt -.vs/ diff --git a/README.md b/README.md index 6fbee6e54f..a7a83aee3a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-) -* **Website:** [http://www.smartstore.com/net](http://www.smartstore.com/net) +* **Website:** [http://www.smartstore.com/en/net](http://www.smartstore.com/en/net) * **Forum:** [http://community.smartstore.com](http://community.smartstore.com) * **Marketplace:** [http://community.smartstore.com/marketplace](http://community.smartstore.com/marketplace) * **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) @@ -54,18 +54,18 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC * and many more... ## Project Status -SmartStore.NET V2.2.1 has been released on May 15, 2015. The highlights are: - -* Overall performance increase -* Added multistore support for forums -* New option to display product thumbnails in instant search -* New mobile theme: _MobileLight_ (a light variant of the default mobile theme) -* Quantity unit management -* Limit country settings to stores -* Web API: Support for file upload and multipart mime -* More reliable mobile device detection -* Performance: product list rendering up to 10x (!) faster now -* Lots of bug fixes +SmartStore.NET V2.5.0 has been released on March 03, 2016. The highlights are: + + * New import/export framework (profiles, filters, mapping, projections, scheduling, deployment... just everything!) + * TaskScheduler: Rewritten from scratch to be suitable for Web Farms (including support for cron expressions) + * Payment and shipping methods by customer roles + * Restrict payment methods to countries + * Restrict payment methods to shipping methods + * Email attachment support for message templates + * Attach order invoice PDF automatically to order notification emails + * Overall performance increase + * Lots of bug fixes + ##Try it online diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index f9b7371083..f184eaa7eb 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -44,7 +44,7 @@ x86 $(BUILD_NUMBER) - 2.2.2 + 2.6.0 $(StageFolder) .$(Version) diff --git a/build.bat b/build.bat index 2cee9dc2b5..b58175809a 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,11 @@ FOR %%b in ( "%VS120COMNTOOLS%..\..\VC\vcvarsall.bat" "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" - "%ProgramFiles%\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 diff --git a/changelog.md b/changelog.md index 4f370dcf36..0dac88a9fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,87 @@ # Release Notes +## SmartStore.NET 2.6 + +### Highlights +* Major improvements in Importer & Exporter: better field mapping, higher performance, bug fixes etc. +* 'PayPal PLUS' payment plugin +* 'paydirekt' payment plugin +* 'Viveum' payment plugin +* 'BeezUP' export provider +* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure. + +### New Features +* #961 Fix "Open Redirection Vulnerability" +* #571 Option to display another checkbox on confirm page to let the customer accept that his email address can be handed over to a third party +* #870 Implement address import for customers (both billing & shipping address) +* #886 Add setting to hide manufacturer images on product detail page and to hide default image for manufacturers +* Import localized SEO names for product and categories +* #477 Implement option to specify the number of exported and imported pictures +* #859 Make checkout attributes suitable for multi-stores +* Product details: Select attribute and gift card values by query string parameters +* #950 make contact form comply with current German law + +### Improvements +* Major improvements in Importer: better field mapping, higher performance, bug fixes etc. +* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure. +* Localization & SEO: language switcher now takes language specific SEO slugs into account when building links +* Smarter import of plugin resource files with graceful fallbacks (de-DE > de > de-* > en-US > en > en-* > *) +* (Perf) Faster language resource file import +* Exports the product detail link including the attribute query string when exporting attribute combinations +* #918 Compare products: Display base price information +* Export email attachments needs to be stored in database because the temp file may not exist anymore when sending the email +* #913 Use HTML5 Input types (tel, email) +* Added paging to frontend order list +* Added paging to backend checkout attribute list +* #977 Show PAngV base/delivery amount also +* Updated LiveEngage LiveChat plugin + +### Bugfixes +* TaskScheduler could fail polling when primary store url is an external IP address +* Fixed ajax cache issue when saving payment or shipping restrictions. Internet Explorer showed the old data state (before storage). +* "The provider failed at the Execute method: Member 'CurrentValues' cannot be called for the entity of type 'Product'" when exporting product attribute combinations +* Bundles without selected attributes could throw an exception on product detail page +* GMC feed did not export the product type and Billiger did not export shop_cat (category path) +* The error message of a payment provider when capturing a payment was not displayed +* Adding new shipping method threw an exception +* Attribute Values: Assigning IsPreselected to more than one value causes an error +* BizImporter: fixed redirection bug when default file extension in .biz wasn't .html +* Fixed: Export deployment emails were always send manually +* Manually notifying gift card recipient threw an exception +* Loading shipping by weight grid failed under SQL CE +* #949 Import: ProcessSlugs does not process explicitly specified "SeName", if product name did not change in an update scenario +* Customer import: Creates customer role duplicates for inserted customers +* GMC feed does not generate the sale price if the sale price is set for a future date +* Mobile devices: Fixed "Uncaught Error: Syntax error, unrecognized expression: :nth-child" +* Download nor sample download were removable when editing product +* Copied product must not share sample download of source product. Could produce "The DELETE statement conflicted with the REFERENCE constraint 'FK_dbo.Product_dbo.Download_SampleDownloadId'". +* #921 Specification attribute options with single quotation marks are causing a Javascript error +* #971 Product is added to cart automatically if it has a non-required file upload attribute +* #973 Bundle item upload is nowhere linked +* Base price in product list ignored PriceDisplayType (catalog settings) and possibly displayed the wrong base price info +* Private messages: Fixes "No route in the route table matches the supplied values" +* Payone: Hash string incorrect for frontend API payments where the order has more than 9 products +* Export mail notification: Download link not working if SSL is enabled +* Discount rule has spent amount including sub total option can cause wrong discount calculation if the cart contains a product several times +* #986 File uploads possible through /content/filemanager/index.html + + ## SmartStore.NET 2.5 +### Highlights + * New import/export framework (profiles, filters, mapping, projections, scheduling, deployment... just everything!) + * TaskScheduler: Rewritten from scratch to be suitable for Web Farms (including support for cron expressions) + * Payment and shipping methods by customer roles + * Restrict payment methods to countries + * Restrict payment methods to shipping methods + * Email attachment support for message templates + * Attach order invoice PDF automatically to order notification emails + * Overall performance increase + * Lots of bug fixes + ### New Features -* #701 Implement new export framework +* New export and import framework +* Import of customer and category data * #141 Payment and shipping methods by customer roles * #67 Restrict payment methods to countries * #94 Restrict payment methods to shipping methods @@ -11,7 +89,9 @@ * #584 Email attachment support for message templates * Attach order invoice PDF automatically to order notification emails * #526 Min/Max amount option for which the payment method should be offered during checkout -* (Dev) New _SyncMapping_ service: enables easier entity synchronization with external systems +* (Dev) New _SyncMapping_ service: enables easier entity synchronization with external systems +* (Dev) #792 ViewEngine: Enable vbhtml views per configuration +* (Dev) Plugin developers can now render child actions into a dynamically created special tab called 'Plugins' * #718 ShopConnector: Import option for "Published" and "Disable buy\wishlist button" * #702 Facebook and Twitter external authentication suitable for multi-stores * New scheduled task: Clear e-mail queue @@ -28,6 +108,26 @@ * #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%. @@ -61,6 +161,19 @@ * 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 @@ -77,7 +190,17 @@ * #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 +* #829 Activity log: Searching by customer email out of function +* Product import: Store mappings were not applied when inserting new records +* Faulty permission handling in ajax grid actions (no message, infinite loading icon) +* Grouped products: Display order was not correct +* Deletion of a customer could delete all newsletter subscriptions +* PayPal: Fixed "The request was aborted: Could not create SSL/TLS secure channel." +* PayPal Express: Void and refund out of function ("The transaction id is not valid") +* Customer could not delete his avatar +* Facebook authentication: Email missing in verification +* Attribute with a product linkage throws exception if added to cart +* Number of products per product tag could be incorrect in a multi-store ## SmartStore.NET 2.2.2 diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 51e391ce23..8acdf39c24 100644 Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ diff --git a/src/AssemblySharedInfo.cs b/src/AssemblySharedInfo.cs index 0d0c359b93..79225de4c7 100644 --- a/src/AssemblySharedInfo.cs +++ b/src/AssemblySharedInfo.cs @@ -7,6 +7,6 @@ [assembly: AssemblyDescription("SmartStore.NET")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("SmartStore AG")] -[assembly: AssemblyCopyright("Copyright © SmartStore AG 2015")] +[assembly: AssemblyCopyright("Copyright © SmartStore AG 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index 0c8dfa2cfb..f74f0edf78 100644 --- a/src/AssemblyVersionInfo.cs +++ b/src/AssemblyVersionInfo.cs @@ -9,7 +9,7 @@ // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("2.2.0.0")] +[assembly: AssemblyVersion("2.6.0.0")] -[assembly: AssemblyFileVersion("2.2.0.0")] -[assembly: AssemblyInformationalVersion("2.2.2.0")] +[assembly: AssemblyFileVersion("2.6.0.0")] +[assembly: AssemblyInformationalVersion("2.6.0.0")] diff --git a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs index 66b12e67f8..789c363da0 100644 --- a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs @@ -99,7 +99,7 @@ public virtual void Collect(IEnumerable keys) } /// - /// Collect single key for combinde loading + /// Collect single key for combined loading /// /// Data key public virtual void Collect(int key) diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs index 9711e8e55d..068b84f008 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs @@ -96,7 +96,7 @@ public bool IsSequenceType { if (!_isSequenceType.HasValue) { - _isSequenceType = Property.PropertyType.IsSubClass(typeof(IEnumerable<>)); + _isSequenceType = Property.PropertyType != typeof(string) && Property.PropertyType.IsSubClass(typeof(IEnumerable<>)); } return _isSequenceType.Value; } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs index b265dbf8aa..1980693205 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs @@ -78,7 +78,7 @@ private static Func, object> CreateSequenceActivator(Type sequenc public override bool CanConvertFrom(Type type) { - return type == typeof(string); + return type == typeof(string) || typeof(IConvertible).IsAssignableFrom(type); } public override bool CanConvertTo(Type type) @@ -105,6 +105,15 @@ public override object ConvertFrom(CultureInfo culture, object value) 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); } @@ -137,7 +146,7 @@ protected virtual string[] GetStringArray(string input) { if (!String.IsNullOrEmpty(input)) { - var splitChar = ','; + var splitChar = '|'; if (input.IndexOf(splitChar) < 0) { @@ -145,9 +154,9 @@ protected virtual string[] GetStringArray(string input) { splitChar = ';'; } - else if (input.IndexOf('|') > -1) + else if (input.IndexOf(',') > -1) { - splitChar = '|'; + splitChar = ','; } } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs index a279a8c461..b7ed897169 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs @@ -31,7 +31,7 @@ public override object ConvertFrom(CultureInfo culture, object value) public override object ConvertTo(CultureInfo culture, string format, object value, Type to) { - return _converter.ConvertTo(value, to); + return _converter.ConvertTo(null, culture, value, to); } } } diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs index e1c4a7569a..ed034c1730 100644 --- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs @@ -10,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); diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index f021ce4069..12f9fa8587 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -33,6 +33,7 @@ public CatalogSettings() CompareProductsEnabled = true; FilterEnabled = true; MaxFilterItemsToDisplay = 4; + SortFilterResultsByMatches = true; SubCategoryDisplayType = SubCategoryDisplayType.AboveProductList; ProductSearchAutoCompleteEnabled = true; ShowProductImagesInSearchAutoComplete = true; @@ -41,6 +42,7 @@ public CatalogSettings() NumberOfBestsellersOnHomepage = 6; ShowManufacturersOnHomepage = true; ShowManufacturerPictures = false; + ShowManufacturerPicturesInProductDetail = true; SearchPageProductsPerPage = 6; ProductsAlsoPurchasedEnabled = true; ProductsAlsoPurchasedNumber = 6; @@ -175,7 +177,12 @@ public CatalogSettings() /// Gets or sets a value indicating whether all filter criterias should be expanded /// public bool ExpandAllFilterCriteria { get; set; } - + + /// + /// Gets or sets a value indicating whether filter results should be sorted by matches + /// + public bool SortFilterResultsByMatches { get; set; } + /// /// Gets or sets a value indicating whether and where to display a list of subcategories /// @@ -301,10 +308,30 @@ public CatalogSettings() /// public bool ShowManufacturerPictures { get; set; } - /// - /// Gets or sets a number of products per page on search products page - /// - public int SearchPageProductsPerPage { get; set; } + /// + /// Gets or sets a value indicating whether to hide manufacturer pictures in product detail + /// + public bool ShowManufacturerPicturesInProductDetail { get; set; } + + /// + /// Gets or sets a value indicating whether to hide maufacturer default pictures + /// + public bool HideManufacturerDefaultPictures { get; set; } + + /// + /// Gets or sets a value indicating whether to hide category default pictures + /// + public bool HideCategoryDefaultPictures { get; set; } + + /// + /// Gets or sets a value indicating whether to hide product default pictures + /// + public bool HideProductDefaultPictures { get; set; } + + /// + /// Gets or sets a number of products per page on search products page + /// + public int SearchPageProductsPerPage { get; set; } /// /// Gets or sets "List of products purchased by other customers who purchased the above" option is enable @@ -366,10 +393,15 @@ public CatalogSettings() public bool SuppressSkuSearch { get; set; } - /// - /// Gets or sets the available customer selectable default page size options - /// - public string DefaultPageSizeOptions { get; set; } + /// + /// Gets or sets a value indicating whether to search long description + /// + public bool SearchDescriptions { get; set; } + + /// + /// Gets or sets the available customer selectable default page size options + /// + public string DefaultPageSizeOptions { get; set; } /// /// Gets or sets the price display type for prices in product lists diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index 50fd39fe5a..999f66bad3 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs @@ -191,6 +191,7 @@ public string Sku /// Gets or sets the manufacturer part number /// [DataMember] + [Index] public string ManufacturerPartNumber { [DebuggerStepThrough] @@ -208,6 +209,7 @@ public string ManufacturerPartNumber /// Gets or sets the Global Trade Item Number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). /// [DataMember] + [Index] public string Gtin { [DebuggerStepThrough] 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/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs index 83380a63a3..e6d7f56f56 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs @@ -24,6 +24,7 @@ public CustomerSettings() NewsletterEnabled = true; OnlineCustomerMinutes = 20; StoreLastVisitedPage = true; + DisplayPrivacyAgreementOnContactUs = false; } /// @@ -151,7 +152,11 @@ public CustomerSettings() /// public bool StoreLastVisitedPage { get; set; } - + /// + /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms + /// + public bool DisplayPrivacyAgreementOnContactUs { get; set; } + #region Form fields /// @@ -246,8 +251,12 @@ public CustomerSettings() #endregion - // codehint: sm-add (no ui, only db edit) public string PrefillLoginUsername { get; set; } public string PrefillLoginPwd { get; set; } - } + + /// + /// Identifier of a customer role that new registered customers will be assigned to + /// + public int RegisterCustomerRoleId { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/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/DataExchange/DataExchangeEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs new file mode 100644 index 0000000000..7a3ab1f8ae --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs @@ -0,0 +1,26 @@ +namespace SmartStore.Core.Domain.DataExchange +{ + public delegate void ProgressValueSetter(int value, int maximum, string message); + + + /// + /// Data exchange abortion types + /// + public enum DataExchangeAbortion + { + /// + /// No abortion. Go on with processing. + /// + None = 0, + + /// + /// Break item processing but not the rest of the execution. Typically used for demo limitations. + /// + Soft, + + /// + /// Break processing immediately + /// + Hard + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs index 742e379df5..4b0bc6d497 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs @@ -7,11 +7,22 @@ public class DataExchangeSettings : ISettings public DataExchangeSettings() { MaxFileNameLength = 50; + ImageDownloadTimeout = 10; } /// /// The maximum length of file names (in characters) of files created by the export framework /// public int MaxFileNameLength { get; set; } + + /// + /// Relative path to a folder with images to be imported + /// + public string ImageImportFolder { get; set; } + + /// + /// The timeout for image download per entity in minutes + /// + public int ImageDownloadTimeout { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs index 3b8190d6a4..b9e323c670 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs @@ -21,14 +21,9 @@ public class ExportDeployment : BaseEntity, ICloneable public bool Enabled { get; set; } /// - /// Whether to deploy in a folder that can be reached from the internet + /// XML with information about the last deployment result /// - public bool IsPublic { get; set; } - - /// - /// Whether to create a zip archive with the content of the export - /// - public bool CreateZip { get; set; } + public string ResultInfo { get; set; } /// /// The deployment type identifier @@ -84,6 +79,11 @@ public ExportHttpTransmissionType HttpTransmissionType /// public string FileSystemPath { get; set; } + /// + /// Path of a subfolder + /// + public string SubFolder { get; set; } + /// /// Multiple email addresses can be separated by commas /// @@ -117,14 +117,13 @@ public ExportDeployment Clone() { Name = this.Name, Enabled = this.Enabled, - IsPublic = this.IsPublic, - CreateZip = this.CreateZip, DeploymentTypeId = this.DeploymentTypeId, Username = this.Username, Password = this.Password, Url = this.Url, HttpTransmissionTypeId = this.HttpTransmissionTypeId, FileSystemPath = this.FileSystemPath, + SubFolder = this.SubFolder, EmailAddresses = this.EmailAddresses, EmailSubject = this.EmailSubject, EmailAccountId = this.EmailAccountId, diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs index 88da59e9c3..6ea68ec73c 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -23,7 +23,8 @@ public enum ExportDeploymentType FileSystem = 0, Email, Http, - Ftp + Ftp, + PublicFolder } /// @@ -125,7 +126,12 @@ public enum ExportFeatures /// /// Whether to not automatically send a completion email /// - CanOmitCompletionMail = 1 << 12 + CanOmitCompletionMail = 1 << 12, + + /// + /// Whether to provide additional data of attribute combinations + /// + UsesAttributeCombination = 1 << 13 } /// @@ -137,25 +143,4 @@ public enum ExportOrderStatusChange Processing, Complete } - - /// - /// Export abortion types - /// - public enum ExportAbortion - { - /// - /// No abortion. Go on with processing. - /// - None = 0, - - /// - /// Break item processing but not the rest of the execution. Typically used for demo limitations. - /// - Soft, - - /// - /// Break processing immediately - /// - Hard - } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs index 7f22f55302..23d08fd03c 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Domain.Shipping; namespace SmartStore.Core.Domain.DataExchange { @@ -116,11 +112,64 @@ public class ExportFilter /// public int[] ShippingStatusIds { get; set; } + #endregion + + #region Customer + + /// + /// Filter by active or inactive customers + /// + public bool? IsActiveCustomer { get; set; } + + /// + /// Filter by tax exempt customers + /// + public bool? IsTaxExempt { get; set; } + /// /// Identifiers of customer roles /// public int[] CustomerRoleIds { get; set; } + /// + /// Filter by billing country identifiers + /// + public int[] BillingCountryIds { get; set; } + + /// + /// Filter by shipping country identifiers + /// + public int[] ShippingCountryIds { get; set; } + + /// + /// Filter by last activity date from + /// + public DateTime? LastActivityFrom { get; set; } + + /// + /// Filter by last activity date to + /// + public DateTime? LastActivityTo { get; set; } + + /// + /// Filter by at least spent amount + /// + public decimal? HasSpentAtLeastAmount { get; set; } + + /// + /// Filter by at least placed orders + /// + public int? HasPlacedAtLeastOrders { get; set; } + + #endregion + + #region Newsletter Subscription + + /// + /// Filter by active or inactive subscriber + /// + public bool? IsActiveSubscriber { get; set; } + #endregion } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs index b49d8d76d1..3161d1ea94 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs @@ -23,7 +23,7 @@ public ExportProfile() public string Name { get; set; } /// - /// The name of the folder (file system) + /// The root path of the export folder /// public string FolderName { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs index 472c94e114..493acda473 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs @@ -95,6 +95,11 @@ public ExportDescriptionMerging DescriptionMerging /// public string Brand { get; set; } + /// + /// Number of images per object to be exported + /// + public int? NumberOfPictures { get; set; } + /// /// Picture size /// diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs new file mode 100644 index 0000000000..94bbd69246 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs @@ -0,0 +1,28 @@ +using System; + +namespace SmartStore.Core.Domain.DataExchange +{ + /// + /// Supported entity types + /// + public enum ImportEntityType + { + Product = 0, + Category, + Customer, + NewsLetterSubscription + } + + public enum ImportFileType + { + CSV = 0, + XLSX + } + + [Flags] + public enum ImportModeFlags + { + Insert = 1, + Update = 2 + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs new file mode 100644 index 0000000000..1139496b4a --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs @@ -0,0 +1,113 @@ +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Tasks; + +namespace SmartStore.Core.Domain +{ + public class ImportProfile : BaseEntity + { + /// + /// The name of the profile + /// + public string Name { get; set; } + + /// + /// The name of the folder (file system) + /// + public string FolderName { get; set; } + + /// + /// The identifier of the file type + /// + public int FileTypeId { get; set; } + + /// + /// The file type + /// + public ImportFileType FileType + { + get + { + return (ImportFileType)FileTypeId; + } + set + { + FileTypeId = (int)value; + } + } + + /// + /// The identifier of the entity type + /// + public int EntityTypeId { get; set; } + + /// + /// The entity type + /// + public ImportEntityType EntityType + { + get + { + return (ImportEntityType)EntityTypeId; + } + set + { + EntityTypeId = (int)value; + } + } + + /// + /// Whether the profile is enabled + /// + public bool Enabled { get; set; } + + /// + /// Number of records to bypass + /// + public int Skip { get; set; } + + /// + /// Maximum number of records to return + /// + public int Take { get; set; } + + /// + /// Whether to only update existing data + /// + public bool UpdateOnly { get; set; } + + /// + /// Name of key fields to identify existing records during import + /// + public string KeyFieldNames { get; set; } + + /// + /// File type specific configuration + /// + public string FileTypeConfiguration { get; set; } + + /// + /// XML with extra data + /// + public string ExtraData { get; set; } + + /// + /// Mapping of import columns + /// + public string ColumnMapping { get; set; } + + /// + /// XML with information about the last import + /// + public string ResultInfo { get; set; } + + /// + /// The scheduling task identifier + /// + public int SchedulingTaskId { get; set; } + + /// + /// The scheduling task + /// + public virtual ScheduleTask ScheduleTask { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs index ffd8d2fc22..f0d0406fb4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs @@ -13,28 +13,28 @@ public partial class LocalizedProperty : BaseEntity /// Gets or sets the entity identifier /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 1)] + [Index("IX_LocalizedProperty_Compound", Order = 1)] public int EntityId { get; set; } /// /// Gets or sets the language identifier /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 4)] + [Index("IX_LocalizedProperty_Compound", Order = 4)] public int LanguageId { get; set; } /// /// Gets or sets the locale key group /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 3)] + [Index("IX_LocalizedProperty_Compound", Order = 3)] public string LocaleKeyGroup { get; set; } /// /// Gets or sets the locale key /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 2)] + [Index("IX_LocalizedProperty_Compound", Order = 2)] public string LocaleKey { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs index 6d1deff20f..b3072c0912 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs @@ -1,5 +1,4 @@ - -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Media { @@ -11,6 +10,7 @@ public MediaSettings() ProductThumbPictureSize = 100; ProductDetailsPictureSize = 300; ProductThumbPictureSizeOnProductDetailsPage = 70; + MessageProductThumbPictureSize = 70; AssociatedProductPictureSize = 125; BundledProductPictureSize = 70; CategoryThumbPictureSize = 125; @@ -18,7 +18,7 @@ public MediaSettings() CartThumbPictureSize = 80; CartThumbBundleItemPictureSize = 32; MiniCartThumbPictureSize = 32; - AutoCompleteSearchThumbPictureSize = 20; + VariantValueThumbPictureSize = 20; MaximumImageSize = 1280; DefaultPictureZoomEnabled = true; PictureZoomType = "window"; @@ -30,16 +30,17 @@ public MediaSettings() public int ProductThumbPictureSize { get; set; } public int ProductDetailsPictureSize { get; set; } public int ProductThumbPictureSizeOnProductDetailsPage { get; set; } - public int AssociatedProductPictureSize { get; set; } + public int MessageProductThumbPictureSize { get; set; } + public int AssociatedProductPictureSize { get; set; } public int BundledProductPictureSize { get; set; } - public int CategoryThumbPictureSize { get; set; } + public int CategoryThumbPictureSize { get; set; } public int ManufacturerThumbPictureSize { get; set; } public int CartThumbPictureSize { get; set; } public int CartThumbBundleItemPictureSize { get; set; } public int MiniCartThumbPictureSize { get; set; } - public int AutoCompleteSearchThumbPictureSize { get; set; } + public int VariantValueThumbPictureSize { get; set; } - public bool DefaultPictureZoomEnabled { get; set; } + public bool DefaultPictureZoomEnabled { get; set; } public string PictureZoomType { get; set; } public int MaximumImageSize { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/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/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/Seo/SeoSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs index f6ec324336..1f06b5aba6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs @@ -1,35 +1,80 @@ - +using System; using System.Collections.Generic; using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Seo { - public class SeoSettings : ISettings + public class SeoSettings : ISettings { public SeoSettings() { PageTitleSeparator = ". "; PageTitleSeoAdjustment = PageTitleSeoAdjustment.PagenameAfterStorename; - DefaultTitle = "Your store"; + DefaultTitle = "Shop"; DefaultMetaKeywords = ""; DefaultMetaDescription = ""; - AllowUnicodeCharsInUrls = true; - CanonicalHostNameRule = Seo.CanonicalHostNameRule.NoRule; - ReservedUrlRecordSlugs = new List { "admin", "install", "recentlyviewedproducts", "newproducts", "compareproducts", "clearcomparelist", "setproductreviewhelpfulness", "login", "register", "logout", "cart", "wishlist", "emailwishlist", "checkout", "contactus", "passwordrecovery", "subscribenewsletter", "blog", "boards", "inboxupdate", "sentupdate", "news", "sitemap", "sitemapseo", "search", "config", "api", "odata" }; - ExtraRobotsDisallows = new List { "/blog/tag/", "/blog/month/", "/producttags/" }; + AllowUnicodeCharsInUrls = false; + CanonicalHostNameRule = CanonicalHostNameRule.NoRule; LoadAllUrlAliasesOnStartup = true; + + ExtraRobotsDisallows = new List { "/blog/tag/", "/blog/month/", "/producttags/" }; + + ReservedUrlRecordSlugs = new List + { + "admin", + "install", + "recentlyviewedproducts", + "newproducts", + "compareproducts", + "clearcomparelist", + "setproductreviewhelpfulness", + "login", + "register", + "logout", + "cart", + "wishlist", + "emailwishlist", + "checkout", + "contactus", + "passwordrecovery", + "subscribenewsletter", + "blog", + "boards", + "inboxupdate", + "sentupdate", + "news", + "sitemap", + "sitemapseo", + "search", + "config", + "api", + "odata" + }; + + SeoNameCharConversion = string.Join(Environment.NewLine, new List + { + "ä;ae", + "ö;oe", + "ü;ue", + "Ä;Ae", + "Ö;Oe", + "Ü;Ue", + "ß;ss" + }); } - + public string PageTitleSeparator { get; set; } public PageTitleSeoAdjustment PageTitleSeoAdjustment { get; set; } public string DefaultTitle { get; set; } public string DefaultMetaKeywords { get; set; } public string DefaultMetaDescription { get; set; } + public string MetaRobotsContent { get; set; } - public bool ConvertNonWesternChars { get; set; } + public bool ConvertNonWesternChars { get; set; } public bool AllowUnicodeCharsInUrls { get; set; } + public string SeoNameCharConversion { get; set; } - public bool CanonicalUrlsEnabled { get; set; } + public bool CanonicalUrlsEnabled { get; set; } public CanonicalHostNameRule CanonicalHostNameRule { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs index 796e642109..054487610f 100644 --- a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs +++ b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs @@ -73,6 +73,8 @@ public SmtpClient ToSmtpClient() smtpClient.UseDefaultCredentials = this.UseDefaultCredentials; smtpClient.EnableSsl = this.EnableSsl; + smtpClient.Timeout = 10000; + if (this.UseDefaultCredentials) { smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials; diff --git a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs index 64297abe9d..a97d4862bf 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; +using System.Diagnostics; using System.Globalization; +using System.IO; using System.Runtime.Serialization.Formatters.Binary; -using SmartStore.Utilities; -using System.Diagnostics; using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; using SmartStore.ComponentModel; +using SmartStore.Utilities; namespace SmartStore { - public static class ConversionExtensions + public static class ConversionExtensions { #region Object @@ -121,7 +121,17 @@ public static int ToInt(this string value, int defaultValue = 0) 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 (CommonHelper.TryConvert(value, out result)) @@ -239,7 +249,11 @@ public static string AsString(this Stream stream, Encoding encoding) // convert stream to string string result; - stream.Position = 0; + + if (stream.CanSeek) + { + stream.Position = 0; + } using (StreamReader sr = new StreamReader(stream, encoding)) { diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs index ab2332c42e..948cfddc6a 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -129,6 +129,77 @@ public static ReadOnlyCollection AsReadOnly(this IEnumerable source) return new ReadOnlyCollection(source.ToArray()); } + /// + /// 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); + } + + /// + /// 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 #region Multimap diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index c83270e71f..033c5d4230 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -24,7 +24,7 @@ public static void Dump(this Exception exception) public static string ToAllMessages(this Exception exception) { var sb = new StringBuilder(); - + while (exception != null) { if (!sb.ToString().EmptyNull().Contains(exception.Message)) diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 346e3c193e..9131705215 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -490,16 +490,19 @@ public static string[] SplitSafe(this string value, string separator) /// true: success, false: failure [DebuggerStepThrough] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter) { + public static bool SplitToPair(this string value, out string 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; } @@ -661,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); diff --git a/src/Libraries/SmartStore.Core/IWebHelper.cs b/src/Libraries/SmartStore.Core/IWebHelper.cs index 03b4f128c8..2d089bd64d 100644 --- a/src/Libraries/SmartStore.Core/IWebHelper.cs +++ b/src/Libraries/SmartStore.Core/IWebHelper.cs @@ -13,11 +13,21 @@ public partial interface IWebHelper /// URL referrer string GetUrlReferrer(); - /// - /// Get context IP address - /// - /// URL referrer - string GetCurrentIpAddress(); + /// + /// Gets a unique client identifier + /// + /// A unique identifier + /// + /// The client identifier is a hashed combination of client ip address and user agent. + /// This method returns null if IP or user agent (or both) cannot be determined. + /// + string GetClientIdent(); + + /// + /// Get context IP address + /// + /// URL referrer + string GetCurrentIpAddress(); /// /// Gets this page name diff --git a/src/Libraries/SmartStore.Core/IWorkContext.cs b/src/Libraries/SmartStore.Core/IWorkContext.cs index d3a54cf702..e137ac98bd 100644 --- a/src/Libraries/SmartStore.Core/IWorkContext.cs +++ b/src/Libraries/SmartStore.Core/IWorkContext.cs @@ -59,10 +59,5 @@ public interface IWorkContext /// Gets or sets a value indicating whether we're in admin area /// bool IsAdmin { get; set; } - - ///// - ///// Gets a value indicating whether we're in the public shop - ///// - //bool IsPublic { get; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs index 7b1bbc9292..f39bbeedeb 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs @@ -95,7 +95,7 @@ public object ResolveUnregistered(Type type, ILifetimeScope scope = null) } } - throw new SmartException("No contructor was found that had all the dependencies satisfied."); + 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) diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs index d4c940ee36..ed9beed2ad 100644 --- a/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs +++ b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; namespace SmartStore.Core.Localization { - public static class LocalizationHelper + public static class LocalizationHelper { private readonly static HashSet _cultureCodes = new HashSet( @@ -18,5 +17,36 @@ public static bool IsValidCultureCode(string cultureCode) return _cultureCodes.Contains(cultureCode); } - } + public static string GetLanguageNativeName(string locale) + { + try + { + if (locale.HasValue()) + { + var info = CultureInfo.GetCultureInfoByIetfLanguageTag(locale); + if (info != null) + return info.NativeName; + } + } + catch { } + + return null; + } + + public static string GetCurrencySymbol(string locale) + { + try + { + if (locale.HasValue()) + { + var info = new RegionInfo(locale); + if (info != null) + return info.CurrencySymbol; + } + } + catch { } + + return null; + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs b/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs index 0ee497d1f8..3ff8526a96 100644 --- a/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs +++ b/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs @@ -74,20 +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 + /// + /// 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 - /// Activity log collection - IPagedList GetAllActivities(DateTime? createdOnFrom, - DateTime? createdOnTo, int? customerId, - int activityLogTypeId, int pageIndex, int pageSize, string email = null); + /// 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 diff --git a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs index f7c2633e40..fb8a10be35 100644 --- a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs +++ b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs @@ -9,45 +9,49 @@ public static class LoggingExtensions public static void Debug(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Debug, message, exception, customer); + FilteredLog(logger, LogLevel.Debug, message, null, exception, customer); } public static void Information(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Information, message, exception, customer); + FilteredLog(logger, LogLevel.Information, message, null, exception, customer); } public static void Warning(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Warning, message, exception, customer); + FilteredLog(logger, LogLevel.Warning, message, null, exception, customer); } public static void Error(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Error, message, exception, customer); + FilteredLog(logger, LogLevel.Error, message, null, exception, customer); } - public static void Fatal(this ILogger logger, string message, Exception exception = null, Customer customer = null) + public static void Error(this ILogger logger, string message, string fullMessage, Exception exception = null, Customer customer = null) + { + FilteredLog(logger, LogLevel.Error, message, fullMessage, exception, customer); + } + + public static void Fatal(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Fatal, message, exception, customer); + FilteredLog(logger, LogLevel.Fatal, message, null, exception, customer); } public static void Error(this ILogger logger, Exception exception, Customer customer = null) { - FilteredLog(logger, LogLevel.Error, exception.ToAllMessages(), exception, customer); + FilteredLog(logger, LogLevel.Error, exception.ToAllMessages(), null, exception, customer); } public static void ErrorsAll(this ILogger logger, Exception exception, Customer customer = null) { while (exception != null) { - FilteredLog(logger, LogLevel.Error, exception.Message, exception, customer); - + FilteredLog(logger, LogLevel.Error, exception.Message, exception.StackTrace, exception, customer); exception = exception.InnerException; } } - private static void FilteredLog(ILogger logger, LogLevel level, string message, Exception exception = null, Customer customer = null) + private static void FilteredLog(ILogger logger, LogLevel level, string message, string fullMessage, Exception exception = null, Customer customer = null) { // don't log thread abort exception if ((exception != null) && (exception is System.Threading.ThreadAbortException)) @@ -55,7 +59,11 @@ private static void FilteredLog(ILogger logger, LogLevel level, string message, if (logger.IsEnabled(level)) { - string fullMessage = exception == null ? string.Empty : exception.StackTrace; + if (exception != null && fullMessage.IsEmpty()) + { + fullMessage = "{0}\n{1}\n{2}".FormatCurrent(exception.Message, new String('-', 20), exception.StackTrace); + } + logger.InsertLog(level, message, fullMessage, customer); } } diff --git a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs index 7cb7b6e7de..593a39ddb0 100644 --- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.IO; using System.Text; -using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Customers; -using System.Diagnostics; +using SmartStore.Core.Domain.Logging; using SmartStore.Utilities; namespace SmartStore.Core.Logging @@ -12,6 +12,7 @@ namespace SmartStore.Core.Logging public class TraceLogger : DisposableObject, ILogger { private readonly TraceSource _traceSource; + private readonly StreamWriter _streamWriter; public TraceLogger() : this(CommonHelper.MapPath("~/App_Data/SmartStore.log")) { @@ -24,23 +25,37 @@ public TraceLogger(string fileName) _traceSource = new TraceSource("SmartStore"); _traceSource.Switch = new SourceSwitch("LogSwitch", "Error"); _traceSource.Listeners.Remove("Default"); - + var console = new ConsoleTraceListener(false); console.Filter = new EventTypeFilter(SourceLevels.All); console.Name = "console"; + _traceSource.Listeners.Add(console); + var textListener = new TextWriterTraceListener(fileName); textListener.Filter = new EventTypeFilter(SourceLevels.All); textListener.TraceOutputOptions = TraceOptions.DateTime; - _traceSource.Listeners.Add(console); - _traceSource.Listeners.Add(textListener); - - // Allow the trace source to send messages to - // listeners for all event types. Currently only + try + { + // force UTF-8 encoding (even if the text just contains ANSI characters) + var append = File.Exists(fileName); + _streamWriter = new StreamWriter(fileName, append, Encoding.UTF8); + + textListener.Writer = _streamWriter; + + _traceSource.Listeners.Add(textListener); + } + catch (IOException) + { + // file is locked by another process + } + + // Allow the trace source to send messages to + // listeners for all event types. Currently only // error messages or higher go to the listeners. - // Messages must get past the source switch to - // get to the listeners, regardless of the settings + // Messages must get past the source switch to + // get to the listeners, regardless of the settings // for the listeners. _traceSource.Switch.Level = SourceLevels.All; } @@ -133,6 +148,12 @@ protected override void OnDispose(bool disposing) { _traceSource.Flush(); _traceSource.Close(); + + if (_streamWriter != null) + { + _streamWriter.Close(); + _streamWriter.Dispose(); + } } } } diff --git a/src/Libraries/SmartStore.Core/RouteInfo.cs b/src/Libraries/SmartStore.Core/RouteInfo.cs index af8c5c7a18..b6856d1a0f 100644 --- a/src/Libraries/SmartStore.Core/RouteInfo.cs +++ b/src/Libraries/SmartStore.Core/RouteInfo.cs @@ -5,7 +5,6 @@ namespace SmartStore { - public class RouteInfo { public RouteInfo(RouteInfo cloneFrom) @@ -14,22 +13,35 @@ public RouteInfo(RouteInfo cloneFrom) Guard.ArgumentNotNull(() => cloneFrom); } - public RouteInfo(string action, string controller, object routeValues) + public RouteInfo(string action, object routeValues) + : this(action, null, routeValues) + { + } + + public RouteInfo(string action, string controller, object routeValues) : this(action, controller, new RouteValueDictionary(routeValues)) { - Guard.ArgumentNotNull(() => routeValues); } - public RouteInfo(string action, string controller, IDictionary routeValues) + public RouteInfo(string action, IDictionary routeValues) + : this(action, null, routeValues) + { + } + + public RouteInfo(string action, string controller, IDictionary routeValues) : this(action, controller, new RouteValueDictionary(routeValues)) { Guard.ArgumentNotNull(() => routeValues); } - public RouteInfo(string action, string controller, RouteValueDictionary routeValues) + public RouteInfo(string action, RouteValueDictionary routeValues) + : this(action, null, routeValues) + { + } + + public RouteInfo(string action, string controller, RouteValueDictionary routeValues) { Guard.ArgumentNotEmpty(() => action); - Guard.ArgumentNotEmpty(() => controller); Guard.ArgumentNotNull(() => routeValues); this.Action = action; @@ -56,5 +68,4 @@ public RouteValueDictionary RouteValues } } - } diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 96cdf8c400..a14afb2ba4 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -85,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 @@ -168,6 +168,9 @@ + + + @@ -178,6 +181,7 @@ + diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index 658a37fe4e..38750e3069 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -19,12 +19,14 @@ public static class SmartStoreVersion new Version("1.2.1"), // MC: had to be :-( new Version("2.0"), new Version("2.1"), - new Version("2.2") + new Version("2.2"), + new Version("2.5") }; - private const string HELP_BASEURL = "http://docs.smartstore.com/display/SMNET25/"; + //private const string HELP_BASEURL = "http://docs.smartstore.com/display/SMNET25/"; + private const string HELP_BASEURL = "http://docs.smartstore.com/display/"; - static SmartStoreVersion() + static SmartStoreVersion() { s_breakingChangesHistory.Reverse(); @@ -66,9 +68,18 @@ public static Version Version } } - public static string GenerateHelpUrl(string path) + public static string GenerateHelpUrl(string languageCode, string path) { - return HELP_BASEURL + path.Trim().TrimStart('/', '\\'); + return String.Concat( + HELP_BASEURL, + GetUserGuideSpaceKey(languageCode), + "/", + path.EmptyNull().Trim().TrimStart('/', '\\')); + } + + public static string GetUserGuideSpaceKey(string languageCode) + { + return "SMNET25"; } /// diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs index 2fdd530c40..d3fcef7874 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs @@ -31,14 +31,24 @@ public FileDownloadManager(HttpRequestBase httpRequest) /// The URL to download the file from (either a fully qualified URL or an app relative/absolute path) /// Specifies whether the FormsAuthenticationCookie should be sent /// Timeout in milliseconds - public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null) + /// Specifiers whether the file is located on the local server + public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null, bool isLocal = false) { Guard.ArgumentNotEmpty(() => url); url = WebHelper.GetAbsoluteUrl(url, _httpRequest); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = "SmartStore.NET"; + HttpWebRequest req; + + if (isLocal) + { + req = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); + } + else + { + req = WebRequest.CreateHttp(url); + req.UserAgent = "SmartStore.NET"; + } if (timeout.HasValue) { @@ -51,14 +61,32 @@ public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false } using (var resp = (HttpWebResponse)req.GetResponse()) + using (var stream = resp.GetResponseStream()) { - using (var stream = resp.GetResponseStream()) + if (resp.StatusCode == HttpStatusCode.OK) { - if (resp.StatusCode == HttpStatusCode.OK) + var data = stream.ToByteArray(); + if (data != null && data.Length != 0) { - var data = stream.ToByteArray(); - var cd = new ContentDisposition(resp.Headers["Content-Disposition"]); - var fileName = cd.FileName; + string fileName = null; + + var contentDisposition = resp.Headers["Content-Disposition"]; + if (contentDisposition.HasValue()) + { + var cd = new ContentDisposition(contentDisposition); + fileName = cd.FileName; + } + + if (fileName.IsEmpty()) + { + try + { + var uri = new Uri(url); + fileName = Path.GetFileName(uri.LocalPath); + } + catch { } + } + return new FileDownloadResponse(data, fileName, resp.ContentType); } } @@ -79,31 +107,40 @@ public async Task DownloadAsync(FileDownloadManagerContext context, IEnumerable< private async Task DownloadFiles(FileDownloadManagerContext context, IEnumerable items) { - var client = new HttpClient(); - - client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(); - client.DefaultRequestHeaders.CacheControl.NoCache = true; - client.DefaultRequestHeaders.Add("Connection", "Keep-alive"); + try + { + using (var client = new HttpClient()) + { + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(); + client.DefaultRequestHeaders.CacheControl.NoCache = true; + client.DefaultRequestHeaders.Add("Connection", "Keep-alive"); - if (context.Timeout.TotalMilliseconds > 0 && context.Timeout != Timeout.InfiniteTimeSpan) - client.Timeout = context.Timeout; + if (context.Timeout.TotalMilliseconds > 0 && context.Timeout != Timeout.InfiniteTimeSpan) + client.Timeout = context.Timeout; - IEnumerable downloadTasksQuery = - from item in items - select ProcessUrl(context, client, item); + IEnumerable downloadTasksQuery = + from item in items + select ProcessUrl(context, client, item); - // now execute the bunch - List downloadTasks = downloadTasksQuery.ToList(); + // now execute the bunch + List downloadTasks = downloadTasksQuery.ToList(); - while (downloadTasks.Count > 0) - { - // identify the first task that completes - Task firstFinishedTask = await Task.WhenAny(downloadTasks); + while (downloadTasks.Count > 0) + { + // identify the first task that completes + Task firstFinishedTask = await Task.WhenAny(downloadTasks); - // process only once - downloadTasks.Remove(firstFinishedTask); + // process only once + downloadTasks.Remove(firstFinishedTask); - await firstFinishedTask; + await firstFinishedTask; + } + } + } + catch (Exception exception) + { + if (context.Logger != null) + context.Logger.ErrorsAll(exception); } } @@ -111,7 +148,10 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli { try { - Task task = client.GetStreamAsync(item.Url); + //HttpResponseMessage response = await client.GetAsync(item.Url, HttpCompletionOption.ResponseHeadersRead); + //Task task = response.Content.ReadAsStreamAsync(); + + Task task = client.GetStreamAsync(item.Url); await task; int count; @@ -132,29 +172,31 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli item.Success = (!task.IsFaulted && !canceled); } - catch (Exception exc) + catch (Exception exception) { - item.Success = false; - item.ErrorMessage = exc.ToAllMessages(); - - var webExc = exc.InnerException as WebException; - if (webExc != null) - item.ExceptionStatus = webExc.Status; + try + { + item.Success = false; + item.ErrorMessage = exception.ToAllMessages(); - if (context.Logger != null) - context.Logger.Error(item.ToString(), exc); + var webExc = exception.InnerException as WebException; + if (webExc != null) + item.ExceptionStatus = webExc.Status; + + if (context.Logger != null) + context.Logger.Error(item.ToString(), exception); + } + catch { } } } - } + public class FileDownloadResponse { public FileDownloadResponse(byte[] data, string fileName, string contentType) { Guard.ArgumentNotNull(() => data); - Guard.ArgumentNotEmpty(() => fileName); - Guard.ArgumentNotEmpty(() => contentType); this.Data = data; this.FileName = fileName; @@ -182,12 +224,12 @@ public class FileDownloadManagerContext /// /// Optional logger to log errors /// - public TraceLogger Logger { get; set; } + public ILogger Logger { get; set; } /// /// Cancellation token /// - public CancellationTokenSource CancellationToken { get; set; } + public CancellationToken CancellationToken { get; set; } /// /// Timeout for the HTTP client diff --git a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs index 69b710c359..3fbb4f83f8 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs @@ -31,6 +31,28 @@ public static string TempDir(string subDirectory = null) return path; } + /// + /// Ensures that path is a valid root path + /// + /// Relative path + /// Valid root path + public static string ValidateRootPath(string path) + { + if (path.HasValue()) + { + path = path.Replace('\\', '/'); + + if (!path.StartsWith("~/")) + { + if (path.StartsWith("~")) + path = path.Substring(1); + + path = (path.StartsWith("/") ? "~" : "~/") + path; + } + } + return path; + } + /// /// Safe way to cleanup the temp directory. Should be called via scheduled task. /// @@ -114,8 +136,10 @@ public static bool Copy(string sourcePath, string destinationPath, bool overwrit /// Source directory /// Target directory /// Whether to override existing files - public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) + public static bool CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) { + var result = true; + foreach (FileInfo fi in source.GetFiles()) { try @@ -124,6 +148,7 @@ public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo } catch (Exception exc) { + result = false; exc.Dump(); } } @@ -137,9 +162,12 @@ public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo } catch (Exception exc) { + result = false; exc.Dump(); } } + + return result; } /// @@ -209,6 +237,30 @@ public static void ClearDirectory(string directoryPath, bool selfToo, List + /// Creates a non existing directory name + /// + /// Path of a directory + /// Default name for directory. null to use a guid. + /// Non existing directory name + public static string CreateNonExistingDirectoryName(string directoryPath, string defaultName) + { + if (defaultName.IsEmpty()) + defaultName = Guid.NewGuid().ToString(); + + if (directoryPath.IsEmpty() || !Directory.Exists(directoryPath)) + return defaultName; + + var newName = defaultName; + + for (int i = 1; i < 999999 && Directory.Exists(Path.Combine(directoryPath, newName)); ++i) + { + newName = defaultName + i.ToString(); + } + + return newName; + } + /// /// Safe way to count files in a directory /// diff --git a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs index f76b247354..3dec92a6d4 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs @@ -1,107 +1,109 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; namespace SmartStore.Utilities { - + public static class SeoHelper { + private static readonly string _okChars = "abcdefghijklmnopqrstuvwxyz1234567890 _-/"; + private static readonly object _lock = new object(); private static Dictionary _seoCharacterTable; - private static readonly object s_lock = new object(); + private static Dictionary _userSeoCharacterTable; /// - /// Get SE name + /// Get SEO friendly string /// - /// Name + /// String to be converted /// A value indicating whether non western chars should be converted /// A value indicating whether Unicode chars are allowed - /// Result + /// Raw data of semicolon separated char conversions + /// SEO friendly string [SuppressMessage("ReSharper", "PossibleNullReferenceException")] - public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeCharsInUrls) + public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeCharsInUrls, string charConversions = null) { if (String.IsNullOrEmpty(name)) return name; - string okChars = "abcdefghijklmnopqrstuvwxyz1234567890 _-/"; + name = name.Trim().ToLowerInvariant(); - if (convertNonWesternChars) + if (charConversions != null && _userSeoCharacterTable == null) { - if (_seoCharacterTable == null) - InitializeSeoCharacterTable(); + InitializeUserSeoCharacterTable(charConversions); + } + + if (convertNonWesternChars && _seoCharacterTable == null) + { + InitializeSeoCharacterTable(); } var sb = new StringBuilder(); foreach (char c in name) { - string c2 = c.ToString(); - if (convertNonWesternChars) + var c2 = c.ToString(); + + if (charConversions != null && _userSeoCharacterTable != null && _userSeoCharacterTable.ContainsKey(c2)) { - if (_seoCharacterTable.ContainsKey(c2)) - c2 = _seoCharacterTable[c2]; + sb.Append(_userSeoCharacterTable[c2]); + continue; } - switch (c2) + if (convertNonWesternChars && _seoCharacterTable.ContainsKey(c2)) { - case "ä": - sb.Append("ae"); - break; - case "ö": - sb.Append("oe"); - break; - case "ü": - sb.Append("ue"); - break; - case "ß": - sb.Append("ss"); - break; - case "Ä": - sb.Append("AE"); - break; - case "Ö": - sb.Append("OE"); - break; - case "Ü": - sb.Append("UE"); - break; + c2 = _seoCharacterTable[c2]; + } - case "é": - case "è": - case "ê": - sb.Append('e'); - break; - case "á": - case "à": - case "â": - sb.Append('a'); - break; - case "ú": - case "ù": - case "û": - sb.Append('u'); - break; - case "ó": - case "ò": - case "ô": - sb.Append('o'); - break; + if (_okChars.Contains(c2) || (allowUnicodeCharsInUrls && char.IsLetterOrDigit(c))) + { + sb.Append(c2); + } + } + + sb.Replace(" ", "-"); - default: - if (okChars.Contains(c2)) - sb.Append(c2); - break; - } // switch + var result = sb.ToString(); + + while (result.Contains("--")) + { + result = result.Replace("--", "-"); } - string name2 = sb.ToString(); - name2 = name2.Replace(" ", "-"); - while (name2.Contains("--")) - name2 = name2.Replace("--", "-"); - while (name2.Contains("__")) - name2 = name2.Replace("__", "_"); + while (result.Contains("__")) + { + result = result.Replace("__", "_"); + } + return result.Trim('/'); + } + + public static void ResetUserSeoCharacterTable() + { + if (_userSeoCharacterTable != null) + { + _userSeoCharacterTable.Clear(); + _userSeoCharacterTable = null; + } + } - return name2.Trim('/'); + private static void InitializeUserSeoCharacterTable(string charConversions) + { + lock (_lock) + { + if (_userSeoCharacterTable == null) + { + _userSeoCharacterTable = new Dictionary(); + + string strLeft, strRight; + + foreach (var conversion in charConversions.SplitSafe(Environment.NewLine)) + { + if (conversion.SplitToPair(out strLeft, out strRight, ";") && strLeft.HasValue() && !_userSeoCharacterTable.ContainsKey(strLeft)) + { + _userSeoCharacterTable.Add(strLeft, strRight); + } + } + } + } } /// @@ -115,7 +117,7 @@ public static string GetSeName(string name, bool convertNonWesternChars, bool al /// private static void InitializeSeoCharacterTable() { - lock (s_lock) + lock (_lock) { if (_seoCharacterTable == null) { diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index b5e9d594bc..edef7863b7 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -2,6 +2,7 @@ using System.Configuration; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Web; @@ -11,22 +12,29 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.Infrastructure; using SmartStore.Utilities; +using System.Net; +using System.Text; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Net.Sockets; namespace SmartStore.Core { - public partial class WebHelper : IWebHelper { + private static object s_lock = new object(); private static bool? s_optimizedCompilationsEnabled; private static AspNetHostingPermissionLevel? s_trustLevel; private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|bmp|html|htm|xml|pdf|doc|xls|rar|zip|ico|eot|svg|ttf|woff|otf|axd|ashx|less)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_htmlPathPattern = new Regex(@"(?<=(?:href|src)=(?:""|'))(?!https?://)(?[^(?:""|')]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex s_cssPathPattern = new Regex(@"url\('(?.+)'\)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); + private static ConcurrentDictionary s_safeLocalHostNames = new ConcurrentDictionary(); private readonly HttpContextBase _httpContext; private bool? _isCurrentConnectionSecured; private string _storeHost; private string _storeHostSsl; + private string _ipAddress; private bool? _appPathPossiblyAppended; private bool? _appPathPossiblyAppendedSsl; @@ -49,17 +57,71 @@ public virtual string GetUrlReferrer() return referrerUrl; } + public virtual string GetClientIdent() + { + var ipAddress = this.GetCurrentIpAddress(); + var userAgent = _httpContext.Request != null ? _httpContext.Request.UserAgent : string.Empty; + + if (ipAddress.HasValue() && userAgent.HasValue()) + { + return (ipAddress + userAgent).GetHashCode().ToString(); + } + + return null; + } + public virtual string GetCurrentIpAddress() { + if (_ipAddress != null) + { + return _ipAddress; + } + + if (_httpContext == null && _httpContext.Request == null) + { + return string.Empty; + } + + var vars = _httpContext.Request.ServerVariables; + + var keysToCheck = new string[] + { + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "REMOTE_ADDR", + "HTTP_CF_CONNECTING_IP" + }; + string result = null; - if (_httpContext != null && _httpContext.Request != null) - result = _httpContext.Request.UserHostAddress; + foreach (var key in keysToCheck) + { + var ipString = vars[key]; + if (ipString.HasValue()) + { + var arrStrings = ipString.Split(','); + // Take the last entry + ipString = arrStrings[arrStrings.Length - 1].Trim(); + + IPAddress address; + if (IPAddress.TryParse(ipString, out address)) + { + result = ipString; + break; + } + } + } if (result == "::1") + { result = "127.0.0.1"; + } - return result.EmptyNull(); + return (_ipAddress = result.EmptyNull()); } public virtual string GetThisPageUrl(bool includeQueryString) @@ -191,12 +253,12 @@ private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) if (_currentStore != null) { - var securityMode = _currentStore.GetSecurityMode(useSsl); + var securityMode = _currentStore.GetSecurityMode(); if (httpHost.IsEmpty()) { - //HTTP_HOST variable is not available. - //It's possible only when HttpContext is not available (for example, running in a schedule task) + // HTTP_HOST variable is not available. + // It's possible only when HttpContext is not available (for example, running in a schedule task) result = _currentStore.Url.EnsureEndsWith("/"); appPathPossiblyAppended = true; @@ -598,5 +660,177 @@ public static string GetAbsoluteUrl(string url, HttpRequestBase request) url = string.Format("{0}://{1}{2}", request.Url.Scheme, request.Url.Authority, url); return url; } - } + + public static string GetPublicIPAddress() + { + string result = string.Empty; + + try + { + using (var client = new WebClient()) + { + client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; + try + { + byte[] arr = client.DownloadData("http://checkip.amazonaws.com/"); + string response = Encoding.UTF8.GetString(arr); + result = response.Trim(); + } + catch { } + } + } + catch { } + + var checkers = new string[] + { + "https://ipinfo.io/ip", + "https://api.ipify.org", + "https://icanhazip.com", + "https://wtfismyip.com/text", + "http://bot.whatismyipaddress.com/" + }; + + if (string.IsNullOrEmpty(result)) + { + using (var client = new WebClient()) + { + foreach (var checker in checkers) + { + try + { + result = client.DownloadString(checker).Replace("\n", ""); + if (!string.IsNullOrEmpty(result)) + { + break; + } + } + catch { } + } + } + } + + if (string.IsNullOrEmpty(result)) + { + try + { + var url = "http://checkip.dyndns.org"; + var req = WebRequest.Create(url); + using (var resp = req.GetResponse()) + { + using (var sr = new StreamReader(resp.GetResponseStream())) + { + var response = sr.ReadToEnd().Trim(); + var a = response.Split(':'); + var a2 = a[1].Substring(1); + var a3 = a2.Split('<'); + result = a3[0]; + } + } + } + catch { } + } + + return result; + } + + public static HttpWebRequest CreateHttpRequestForSafeLocalCall(Uri requestUri) + { + Guard.ArgumentNotNull(() => requestUri); + + var safeHostName = GetSafeLocalHostName(requestUri); + + var uri = requestUri; + + if (!requestUri.Host.Equals(safeHostName, StringComparison.OrdinalIgnoreCase)) + { + var url = String.Format("{0}://{1}{2}", + requestUri.Scheme, + requestUri.IsDefaultPort ? safeHostName : safeHostName + ":" + requestUri.Port, + requestUri.PathAndQuery); + uri = new Uri(url); + } + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "SmartStore.NET {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); + + return request; + } + + private static string GetSafeLocalHostName(Uri requestUri) + { + return s_safeLocalHostNames.GetOrAdd(requestUri.Port, (port) => + { + // first try original host + if (TestHost(requestUri, requestUri.Host, 5000)) + { + return requestUri.Host; + } + + // try loopback + var hostName = Dns.GetHostName(); + var hosts = new List { "localhost", hostName, "127.0.0.1" }; + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // try local IP addresses + hosts.Clear(); + var ipAddresses = Dns.GetHostAddresses(hostName).Where(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.ToString()); + hosts.AddRange(ipAddresses); + + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // None of the hosts are callable. WTF? + return requestUri.Host; + }); + } + + private static bool TestHost(Uri originalUri, string host, int timeout) + { + var url = String.Format("{0}://{1}/taskscheduler/noop", + originalUri.Scheme, + originalUri.IsDefaultPort ? host : host + ":" + originalUri.Port); + var uri = new Uri(url); + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "SmartStore.NET"; + request.Timeout = timeout; + + HttpWebResponse response = null; + + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + return true; + } + } + catch + { + // try the next host + } + finally + { + if (response != null) + response.Dispose(); + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/XmlHelper.cs b/src/Libraries/SmartStore.Core/XmlHelper.cs index 6d9f60def4..3b3e3f2e75 100644 --- a/src/Libraries/SmartStore.Core/XmlHelper.cs +++ b/src/Libraries/SmartStore.Core/XmlHelper.cs @@ -169,14 +169,20 @@ public static object Deserialize(string xml, Type type) { Guard.NotNull(() => type); - if (xml.IsEmpty()) - return Activator.CreateInstance(type); - - using (var reader = new StringReader(xml)) + try { - var serializer = new XmlSerializer(type); - return serializer.Deserialize(reader); + if (xml.HasValue()) + { + using (var reader = new StringReader(xml)) + { + var serializer = new XmlSerializer(type); + return serializer.Deserialize(reader); + } + } } + catch { } + + return Activator.CreateInstance(type); } #endregion diff --git a/src/Libraries/SmartStore.Core/packages.config b/src/Libraries/SmartStore.Core/packages.config index 5765e490f2..61985f795a 100644 --- a/src/Libraries/SmartStore.Core/packages.config +++ b/src/Libraries/SmartStore.Core/packages.config @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/EfRepository.cs b/src/Libraries/SmartStore.Data/EfRepository.cs index 860551f723..5afd5e5d41 100644 --- a/src/Libraries/SmartStore.Data/EfRepository.cs +++ b/src/Libraries/SmartStore.Data/EfRepository.cs @@ -161,7 +161,7 @@ public virtual void UpdateRange(IEnumerable entities) private void SetEntityStateToModifiedIfApplicable(T entity) { var entry = InternalContext.Entry(entity); - if (entry.State == System.Data.Entity.EntityState.Detached || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) + if (entry.State < System.Data.Entity.EntityState.Added || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) { entry.State = System.Data.Entity.EntityState.Modified; } diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs index 0a57740d6e..2c49aa7d7a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs @@ -11,10 +11,12 @@ public ExportDeploymentMap() this.HasKey(x => x.Id); this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.ResultInfo).IsMaxLength(); this.Property(x => x.Username).HasMaxLength(400); this.Property(x => x.Password).HasMaxLength(400); this.Property(x => x.Url).HasMaxLength(4000); this.Property(x => x.FileSystemPath).HasMaxLength(400); + this.Property(x => x.SubFolder).HasMaxLength(400); this.Property(x => x.EmailAddresses).HasMaxLength(4000); this.Property(x => x.EmailSubject).HasMaxLength(400); diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs index 6e5e6758fe..adacc3c4c6 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs @@ -11,7 +11,7 @@ public ExportProfileMap() this.HasKey(x => x.Id); this.Property(x => x.Name).IsRequired().HasMaxLength(100); - this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(400); this.Property(x => x.SystemName).HasMaxLength(400); this.Property(x => x.ProviderSystemName).IsRequired().HasMaxLength(4000); this.Property(x => x.Filtering).IsMaxLength(); diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs new file mode 100644 index 0000000000..02515fd268 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs @@ -0,0 +1,30 @@ +using System.Data.Entity.ModelConfiguration; +using SmartStore.Core.Domain; + +namespace SmartStore.Data.Mapping.DataExchange +{ + public partial class ImportProfileMap : EntityTypeConfiguration + { + public ImportProfileMap() + { + this.ToTable("ImportProfile"); + this.HasKey(x => x.Id); + + this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); + this.Property(x => x.KeyFieldNames).HasMaxLength(1000); + this.Property(x => x.FileTypeConfiguration).IsMaxLength(); + this.Property(x => x.ExtraData).IsMaxLength(); + this.Property(x => x.ColumnMapping).IsMaxLength(); + this.Property(x => x.ResultInfo).IsMaxLength(); + + this.Ignore(x => x.FileType); + this.Ignore(x => x.EntityType); + + this.HasRequired(x => x.ScheduleTask) + .WithMany() + .HasForeignKey(x => x.SchedulingTaskId) + .WillCascadeOnDelete(false); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs b/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs index 3cf4f4c6b5..aa992d333a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs @@ -44,7 +44,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "{0} von {1} Zeilen verarbeitet."); builder.AddOrUpdate("Admin.Common.ImportFromExcel.QuickStats", - "{0} new, {1} updated - with {2} warning(s) und {3} error(s).", + "{0} new, {1} updated - with {2} warning(s) and {3} error(s).", "{0} neu, {1} aktualisiert - bei {2} Warnung(en) und {3} Fehler(n)."); builder.AddOrUpdate("Admin.Common.ImportFromExcel.ActiveSince", diff --git a/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs b/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs index c25739afec..dbc7b21d2b 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs @@ -40,12 +40,10 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Configuration.Category.Acl.AssignToSubCategoriesAndProducts.Hint", @"This function assigns the ACL configuration of this category to all subcategories and products included in this category.
- Please keep in mind you have to save changes in the ACL configuration
- before you can assign them to all subcategories and products.
- Attention: Please keep in mind that existing ACL records will be deleted", + Please keep in mind you have to save changes in the ACL configuration before you can assign them to all subcategories and products.
+ Attention: Please keep in mind that existing ACL records will be deleted.", @"Diese Funktion bernimmt die Zugriffsrecht-Konfiguration dieser Warengruppe fr alle Unterwarengruppen und Produkte.
- Bitte beachten Sie, dass die nderungen der Zugriffsrechte zunchst gespeichert werden mssen,
- bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
+ Bitte beachten Sie, dass die nderungen der Zugriffsrechte zunchst gespeichert werden mssen, bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
Vorsicht: Bitte beachten Sie, dass vorhandene Zugriffsrechte berschrieben bzw. gelscht werden."); builder.AddOrUpdate("Admin.Configuration.Category.Stores.AssignToSubCategoriesAndProducts", @@ -54,12 +52,10 @@ bevor diese f builder.AddOrUpdate("Admin.Configuration.Category.Stores.AssignToSubCategoriesAndProducts.Hint", @"This function assigns the store configuration of this category to all subcategories and products included in this category.
- Please keep in mind you have to save changes in the store configuration
- before you can assign them to all subcategories and products.
- Attention: Please keep in mind that existing store mappings will be deleted", + Please keep in mind you have to save changes in the store configuration before you can assign them to all subcategories and products.
+ Attention: Please keep in mind that existing store mappings will be deleted.", @"Diese Funktion bernimmt die Shop-Konfiguration dieser Warengruppe fr alle Unterwarengruppen und Produkte.
- Bitte beachten Sie, dass die nderungen an der Store-Konfiguration zunchst gespeichert werden mssen,
- bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
+ Bitte beachten Sie, dass die nderungen an der Store-Konfiguration zunchst gespeichert werden mssen, bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
Vorsicht: Bitte beachten Sie, dass vorhandene Store-Konfiguration berschrieben bzw. gelscht werden."); builder.AddOrUpdate("Admin.Configuration.Acl.NoRolesDefined", @@ -123,20 +119,20 @@ bevor diese f builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowManufacturersOnHomepage", "Show manufacturers on homepage", "Zeige Hersteller auf der Homepage", - "Specifies whether manufacturers will be displayed on the homepage.", - "Bestimmt ob Hersteller auf der Homepage angezeigt werden."); + "Specifies whether manufacturers are displayed on the homepage.", + "Legt fest, ob Hersteller auf der Homepage angezeigt werden."); builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowManufacturerPictures", "Show manufacturer pictures on homepage", "Zeige Herstellerbilder auf der Homepage", - "Specifies whether manufacturers will be displayed as images or textual links on the homepage.", - "Bestimmt ob Hersteller auf der Homepage als Bilder oder textuelle Links angezeigt werden."); + "Specifies whether manufacturers are displayed as images or textual links on the homepage.", + "Legt fest, ob Hersteller auf der Homepage als Bilder oder textuelle Links angezeigt werden."); builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberEnabled", "Customers can enter a customer number", "Kunden knnen Kundennummer hinterlegen", "Specifies whether customers can enter a customer number.", - "Bestimmt ob Kunden eine Kundennummer hinterlegen knnen."); + "Legt fest, ob Kunden eine Kundennummer hinterlegen knnen."); builder.AddOrUpdate("Account.Fields.CustomerNumber", "Customer number", @@ -145,8 +141,8 @@ bevor diese f builder.AddOrUpdate("Admin.Configuration.Settings.Tax.VatRequired", "Customers must enter a VATIN", "Kunden mssen eine Steuernummer angeben", - "Specifies whether Customers must enter a VAT identification number.", - "Bestimmt ob Kunden bei der Registrierung eine Steuernummer angeben mssen."); + "Specifies whether customers must enter a VAT identification number.", + "Legt fest, ob Kunden bei der Registrierung eine Steuernummer angeben mssen."); builder.AddOrUpdate("Account.Fields.Vat.Required", "Please enter your VATIN", diff --git a/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs index 300ceba25a..8c0dc62ad6 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs @@ -10,7 +10,7 @@ public partial class RemoveKeepAlive : DbMigration, ILocaleResourcesProvider, ID { public override void Up() { - if (HostingEnvironment.IsHosted) + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) { Sql("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Services.Common.KeepAliveTask, SmartStore.Services'"); } diff --git a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs index 64bb41f4d8..5203e70451 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs @@ -124,10 +124,10 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt den Export-Provider fest. Er ist fr die individuelle Formatierung der zu exportierenden Daten zustndig."); builder.AddOrUpdate("Admin.DataExchange.Export.EntityType", - "Entity", - "Entitt", - "The entity type the provider processes.", - "Der Entittstyp, den der Provider verarbeitet."); + "Object", + "Objekt", + "The object type the provider processes.", + "Der Objekttyp, den der Provider verarbeitet."); builder.AddOrUpdate("Admin.DataExchange.Export.Name", @@ -142,25 +142,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "The file type of the exported data.", "Der Dateityp der exportierten Daten."); - builder.AddOrUpdate("Admin.DataExchange.Export.LastExecution", - "Last execution", - "Letzte Ausfhrung", - "Information about the last execution of the export.", - "Informationen zur letzten Ausfhrung des Exports."); - builder.AddOrUpdate("Admin.DataExchange.Export.Offset", - "Skip", - "berspringen", - "Specifies the number of records to be skipped.", - "Legt die Anzahl der zu berspringenden Datenstze fest."); - - builder.AddOrUpdate("Admin.DataExchange.Export.Limit", - "Limit", - "Begrenzen", - "Specifies how many records to be loaded at maximum.", - "Legt die maximale Anzahl der zu exportierenden Datenstze fest."); - builder.AddOrUpdate("Admin.DataExchange.Export.BatchSize", "Batch size", "Stapelgre", @@ -482,9 +465,9 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAddresses", "Email addresses to", - "E-Mail-Addressen an", + "E-Mail Adressen an", "Specifies the email addresses where to send the data.", - "Legt die E-Mail Addressen fest, an die die Daten verschickt werden soll."); + "Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll."); builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailSubject", "Email subject", diff --git a/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs index b3499775ea..ce062a376e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs @@ -96,7 +96,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.ExtraRobotsDisallows", "Extra Disallows for robots.txt", "Extra Disallows fr robots.txt", - "Enter additional paths that should be included as Disallow entries in your robots.txt. Each entry has to entered in a new line.", + "Enter additional paths that should be included as Disallow entries in your robots.txt. Each entry has to be entered in a new line.", "Geben Sie hier zustzliche Pfade an, die als Disallow-Eintrge zur robots.txt hinzugefgt werden sollen. Jeder Eintrag muss in einer neuen Zeile erfolgen."); builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.DefaultSortOrderMode", diff --git a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs index cc8975e3cc..09f2dc6314 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs @@ -81,13 +81,13 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmailAddresses", "Email addresses to", - "E-Mail-Addressen an", + "E-Mail Adressen an", "Specifies the email addresses where to send the notification message.", - "Legt die E-Mail Addressen fest, an die die Benachrichtigung geschickt werden soll."); + "Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll."); builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmail.Subject", "Export of profile \"{0}\" has been finished", - "Export von Profile \"{0}\" ist abgeschlossen"); + "Export von Profil \"{0}\" ist abgeschlossen"); builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmail.Body", "This is an automatic notification of store \"{0}\" about a recent data export.", @@ -184,8 +184,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Das Exportprofil ist deaktiviert. Fr eine Exportvorschau muss das Exportprofil aktiviert sein."); builder.AddOrUpdate("Admin.DataExchange.Export.NoProfilesForProvider", - "There was no export profile of type \"{0}\" found. Create now a new export profile.", - "Es wurde kein Exportprofil vom Typ \"{0}\" gefunden. Jetzt ein neues Exportprofil anlegen."); + "There was no export profile of type {0} found. Create now a new export profile.", + "Es wurde kein Exportprofil vom Typ {0} gefunden. Jetzt ein neues Exportprofil anlegen."); builder.AddOrUpdate("Admin.DataExchange.Export.ProfileForProvider", "Export profile", diff --git a/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs index f37015afe4..5bab264538 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs @@ -40,7 +40,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Common.Example", "Example", "Beispiel"); builder.AddOrUpdate("Common.ShowAll", "Show all", "Alle anzeigen"); builder.AddOrUpdate("Admin.Common.Selected", "Selected", "Ausgewhlte"); - builder.AddOrUpdate("Admin.Common.Entity", "Entity", "Entitt"); + builder.AddOrUpdate("Admin.Common.Entity", "Object", "Objekt"); builder.AddOrUpdate("Admin.Common.Placeholder", "Placeholder", "Platzhalter"); @@ -75,16 +75,16 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt den SEO Namen fest."); builder.AddOrUpdate("Admin.System.SeNames.EntityId", - "Entity ID", - "ID der Entitt", - "Specifies the ID of the associated entity.", - "Legt die ID der zugehrigen Entitt fest."); + "Object ID", + "Objekt-ID", + "Specifies the ID of the associated object.", + "Legt die ID des zugehrigen Objektes fest."); builder.AddOrUpdate("Admin.System.SeNames.EntityName", - "Entity", - "Entitt", - "Specifies the name of the associated entity.", - "Legt den Namen der zugehrigen Entitt fest."); + "Object", + "Objekt", + "Specifies the name of the associated object.", + "Legt den Namen der zugehrigen Objektes fest."); builder.AddOrUpdate("Admin.System.SeNames.IsActive", "Is active", @@ -99,10 +99,10 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt die Sprache des SEO Namens fest."); builder.AddOrUpdate("Admin.System.SeNames.SlugsPerEntity", - "Names per entity", - "Namen pro Entitt", - "The number of SEO names per entity.", - "Die Anzahl der SEO Namen pro Entitt."); + "Names per object", + "Namen pro Objekt", + "The number of SEO names per object.", + "Die Anzahl der SEO Namen pro Objekt."); builder.AddOrUpdate("Admin.System.SeNames.ActiveSlugAlreadyExist", "Only one active SEO name should be set per language.", diff --git a/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs index 9857282fe6..ffbf880d57 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs @@ -64,8 +64,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Common.Of", "of", "von"); builder.AddOrUpdate("Admin.Common.NoTempFilesFound", - "No temporary files were found.", - "Es wurden keine temporren Dateien gefunden."); + "There are no temporary files.", + "Es sind keine temporren Dateien vorhanden."); builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.NewsLetterSubscription", "Newsletter Subscribers", 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 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.Designer.cs new file mode 100644 index 0000000000..f7e948a531 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ThirdPartyEmailHandOver : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ThirdPartyEmailHandOver)); + + string IMigrationMetadata.Id + { + get { return "201603121451066_ThirdPartyEmailHandOver"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs new file mode 100644 index 0000000000..9b5970f35a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs @@ -0,0 +1,235 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + using SmartStore.Core.Domain.Media; + using System.Linq; + + public partial class ThirdPartyEmailHandOver : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Order", "AcceptThirdPartyEmailHandOver", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.Order", "AcceptThirdPartyEmailHandOver"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + // Some users have disabled the "TransientMediaClearTask" due to a bug. + // When the task is enabled again, it would delete media files, which are marked as transient + // but are permanent actually. To avoid this, we have to set IsTransient to false. + var transientPictures = context.Set().Where(x => x.IsTransient == true).ToList(); + transientPictures.Each(x => x.IsTransient = false); + + var transientDownloads = context.Set().Where(x => x.IsTransient == true).ToList(); + transientDownloads.Each(x => x.IsTransient = false); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Common.Ignore", "Ignore", "Ignorieren"); + builder.AddOrUpdate("Common.My", "My", "Mein"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.None", "Do not show", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.Deactivated", "Show deactivated", "Deaktiviert anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.Activated", "Show activated", "Aktiviert anzeigen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOver", + "Consent for email transfer to third parties", + "Zustimmung zur E-Mail Weitergabe an Dritte", + "Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout.", + "Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen knnen und ob die Checkbox dafr standardmig aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel", + "Text for email transfer consent", + "Text fr E-Mail Weitergabe", + "Specifies the text to be displayed to the customer. Please choose a specific reason, e.g. 'I agree to the transfer and storage of my email address for TrustedShops buyer protection.'", + "Legt den Text fr die Zustimmung zur Weitergabe der E-Mail Adresse an Dritte fest. Whlen Sie bitte einen konkreten Grund fr die Weitergabe, z.B. 'Mit der bermittlung und Speicherung meiner E-Mail-Adresse zur Abwicklung des Kuferschutzes durch Trusted Shops bin ich einverstanden.'"); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel.Default", + "I agree to the transfer and storage of my email address by third parties.", + "Mit der bermittlung und Speicherung meiner E-Mail-Adresse durch dritte Parteien bin ich einverstanden."); + + builder.AddOrUpdate("Admin.Orders.Fields.AcceptThirdPartyEmailHandOver", + "Accepts transfer of email", + "Akzeptiert Weitergabe der E-Mail", + "Indicates whether the customer has agreed to a transfer of his email address to third parties.", + "Gibt an, ob der Kunde bei der Bestellung einer Weitergabe seiner E-Mail Adresse an Dritte zugestimmt hat oder nicht."); + + builder.AddOrUpdate("Admin.OrderNotice.OrderCaptureError", + "Unable to capture payment for order {0}.", + "Es ist ein Fehler bei der Zahlungsbuchung zu Auftrag {0} aufgetreten."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.None", + "None", "Keine"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowManufacturerPicturesInProductDetail", + "Show manufacturer pictures", + "Bilder von Herstellern anzeigen", + "Specifies whether to show manufacturer pictures on product detail page.", + "Legt fest, ob Herstellerbilder auf der Produktdetailseite angezeigt werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.HideManufacturerDefaultPictures", + "Hide default picture for manufacturers", + "Standardbild bei Herstellern ausblenden", + "Specifies whether to hide the default image for manufacturers. The default image is shown when no image is assigned to a manufacturer.", + "Legt fest, ob das Standardbild bei Herstellern ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Hersteller kein Bild zugeordnet ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Partition.Note", + "With the following settings you can partition the data to be exported. This includes
  • Skipping the first n records
  • The maximum number of records to be exported
  • The number of records per export file
  • Export data for each shop in a separate file
By default, all data of a store will be exported into one file.", + "Mit den folgenden Einstellungen lassen sich die zu exportierenden Daten aufteilen. Dazu zhlt
  • Das berspringen der ersten n Datenstze
  • Die maximale Anzahl zu exportierender Datenstze
  • Die Anzahl der Datenstze pro Exportdatei
  • Daten von jedem Shop in eine separate Datei exportieren
Standardmig werden alle Daten eines Shops in eine Datei exportiert."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Partition.Validate", + "Partitioning setting values must be greater than or equal to 0.", + "Einstellungen zur Aufteilung mssen grer oder gleich 0 sein."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsActiveSubscriber", + "Only active subscribers", + "Nur aktive Abonnenten", + "Filter by active or inactive newsletter subscribers.", + "Nach aktiven bzw. inaktiven Newsletter Abonnenten filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsActiveCustomer", + "Only active customers", + "Nur aktive Kunden", + "Filter by active or inactive customers.", + "Nach aktiven bzw. inaktiven Kunden filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsTaxExempt", + "Only tax exempt customers", + "Nur MwSt. befreite Kunden", + "Filter by tax exempt customers.", + "Nach MwSt. befreiten Kunden filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.BillingCountryIds", + "Billing countries", + "Rechnungslnder", + "Filter by billing countries.", + "Nach Rechnungslndern filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ShippingCountryIds", + "Shipping countries", + "Versandlnder", + "Filter by shipping countries.", + "Nach Versandlndern filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.LastActivityFrom", + "Last activity from", + "Zuletzt aktiv von", + "Filter by date of last store activity.", + "Nach dem Datum der letzten Shop Aktivitt filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.LastActivityTo", + "Last active until", + "Zuletzt aktiv bis", + "Filter by date of last store activity.", + "Nach dem Datum der letzten Shop-Aktivitt filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.HasSpentAtLeastAmount", + "Has spent amount x", + "Hat Betrag x ausgegeben", + "Filter by spent amount.", + "Nach dem insgesamt ausgegebenen Betrag filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.HasPlacedAtLeastOrders", + "Has placed x orders", + "Hat x Bestellungen", + "Filter by number of placed orders.", + "Nach der Anzahl der gettigten Bestellungen filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.SystemProfileNote", + "The following list contains system profiles, which are provided by plugins such as the Data Export Plugin. You can customize system profiles as desired, but cannot create new ones. These profiles also add additional action buttons. You will find these above data lists, such as the product or order list.", + "Die folgende Liste enthlt Systemprofile, die von Plugins wie bspw. dem Datenexporte Plugin bereitgestellt werden. Sie knnen Systemprofile nach Belieben anpassen, aber keine Neuen erstellen. Fr diese Profile stehen auerdem zustzliche Aktions-Buttons zur Verfgung. Sie finden diese ber den entsprechenden Listen, wie z.B. der Produkt- oder Auftragsliste."); + + builder.AddOrUpdate("Admin.DataExchange.AddNewProfile", + "New profile", + "Neues Profil"); + + builder.AddOrUpdate("Admin.DataExchange.Import.ProfileCreationNote", + "Please select the import object and upload an import file.", + "Whlen Sie bitte das zu importierende Objekt und laden Sie eine Importdatei hoch."); + + builder.Delete("Admin.DataExchange.Import.ProfileEntitySelectNote"); + + builder.AddOrUpdate("Admin.Configuration.Restriction.SaveBeforeEdit", + "You need to save before you can specify restrictions.", + "Sie mssen zunchst speichern, bevor Sie Einschrnkungen festlegen knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberMethod", + "Customer numbers", + "Kundennummern", + "Specifies whether to assign customer numbers and whether they should be created automatically.", + "Legt fest, ob Kundennummern vergeben werden und ob diese automatisch vergeben werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberVisibility", + "Customer number presentation", + "Darstellung der Kundennummer", + "Specifies the presentation and handling of the customer number to the customer.", + "Legt die Darstellung und Handhabung der Kundennummer gegenber dem Kunden fest."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.Disabled", "Disabled", "Deaktiviert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.Enabled", "Enabled", "Aktiviert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.AutomaticallySet", "Automatically assigned", "Automatisch vergeben"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.None", "Do not display", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.Display", "Display", "Anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.EditableIfEmpty", "Editable if empty", "Editierbar falls leer"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.Editable", "Always editable", "Immer editierbar"); + + builder.AddOrUpdate("Admin.Common.FileInUse", + "The file is in use and cannot be opened.", + "Die Datei ist in Benutzung und kann daher nicht geffnet werden."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.UserProfilesTitle", + "User profiles", + "Benutzerprofile"); + builder.AddOrUpdate("Admin.DataExchange.Export.SystemProfilesTitle", + "System profiles", + "Systemprofile"); + + builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmailAddresses", + "Email addresses to", + "E-Mail Adressen an", + "Specifies the email addresses where to send the notification message.", + "Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAddresses", + "Email addresses to", + "E-Mail Adressen an", + "Specifies the email addresses where to send the data.", + "Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll."); + + builder.AddOrUpdate("Admin.Common.FileNotFound", "File not found", "Datei nicht gefunden"); + + builder.AddOrUpdate("Admin.Common.EnterEmailAdress", + "Please enter an email address.", + "Bitte geben Sie eine E-Mail-Adresse ein."); + + builder.AddOrUpdate("Admin.Configuration.EmailAccounts.TestingEmail", + "Testing email functionality.", + "Test der E-Mail-Funktion."); + + builder.AddOrUpdate("Admin.Common.EmailSuccessfullySent", + "The email has been successfully sent.", + "Die E-Mail wurde erfolgreich versendet."); + + builder.AddOrUpdate("Admin.Common.SkipAndTakeGreaterThanOrEqualZero", + "Values for skip and limit must be greater than or equal to 0.", + "Werte fr berspringen und Begrenzen mssen grer oder gleich 0 sein."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.resx b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.resx new file mode 100644 index 0000000000..43d0084f04 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs new file mode 100644 index 0000000000..90e9ba6504 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class GtinMpnIndex : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(GtinMpnIndex)); + + string IMigrationMetadata.Id + { + get { return "201605020640016_GtinMpnIndex"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs new file mode 100644 index 0000000000..98f1936ecc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs @@ -0,0 +1,19 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class GtinMpnIndex : DbMigration + { + public override void Up() + { + CreateIndex("dbo.Product", "ManufacturerPartNumber"); + CreateIndex("dbo.Product", "Gtin"); + } + + public override void Down() + { + DropIndex("dbo.Product", new[] { "Gtin" }); + DropIndex("dbo.Product", new[] { "ManufacturerPartNumber" }); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx new file mode 100644 index 0000000000..15bcc90554 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.Designer.cs new file mode 100644 index 0000000000..1fa094db33 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class SwapColumnMappingValues : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(SwapColumnMappingValues)); + + string IMigrationMetadata.Id + { + get { return "201605061916117_SwapColumnMappingValues"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs new file mode 100644 index 0000000000..f16431d45b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs @@ -0,0 +1,105 @@ +namespace SmartStore.Data.Migrations +{ + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using Core.Domain; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using Setup; + + public partial class SwapColumnMappingValues : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + var importProfiles = context.Set().Where(x => x.ColumnMapping != null).ToList(); + + foreach (var profile in importProfiles) + { + var dic = new Dictionary>(); + var storeMapping = true; + + try + { + var json = JObject.Parse(profile.ColumnMapping); + + foreach (var kvp in json) + { + dynamic value = kvp.Value; + + var mappedName = (string)value.MappedName; + var property = (string)value.Property; + var defaultValue = (string)value.Default; + + if (mappedName.HasValue()) + { + // break migration because data is already migrated + storeMapping = false; + break; + } + else if (property.HasValue()) + { + if (!kvp.Key.IsCaseInsensitiveEqual(property) || defaultValue.HasValue()) + { + // swap value + dic.Add(property, new Dictionary + { + { "MappedName", kvp.Key }, + { "Default", defaultValue } + }); + } + else + { + // ignore because persisting not required anymore + } + } + else + { + // explicitly ignored property + dic.Add(property, new Dictionary + { + { "MappedName", property }, + { "Default", "[IGNOREPROPERTY]" } + }); + } + } + } + catch + { + storeMapping = true; + dic.Clear(); + } + + if (storeMapping) + { + if (dic.Any()) + profile.ColumnMapping = JsonConvert.SerializeObject(dic); + else + profile.ColumnMapping = null; + } + } + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete("Admin.DataExchange.ColumnMapping.Validate.MultipleMappedIgnored"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx new file mode 100644 index 0000000000..15bcc90554 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.Designer.cs new file mode 100644 index 0000000000..6cac518be6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ImportExtraData : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ImportExtraData)); + + string IMigrationMetadata.Id + { + get { return "201605111140288_ImportExtraData"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs new file mode 100644 index 0000000000..191ace97ba --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs @@ -0,0 +1,72 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Setup; + + public partial class ImportExtraData : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ImportProfile", "ExtraData", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.ImportProfile", "ExtraData"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.MigrateSettings(x => + { + x.Add("MediaSettings.VariantValueThumbPictureSize", "20"); + x.Delete("MediaSettings.AutoCompleteSearchThumbPictureSize"); + }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Import.NumberOfPictures", + "Number of pictures", + "Anzahl der Bilder", + "Specifies the number of images per object to be imported.", + "Legt die Anzahl der zu importierenden Bilder pro Objekt fest."); + + builder.Update("Admin.Configuration.Settings.Catalog.DefaultPageSizeOptions") + .Value("en", "Number of displayed products per page"); + + builder.AddOrUpdate("Admin.Validation.ValueGreaterZero", + "The value must be greater than 0.", + "Der Wert muss grer 0 sein."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.OrderListPageSize", + "Number of displayed orders per page", + "Anzahl der Auftrge pro Seite", + "Specifies the number of displayed orders per page.", + "Legt die Anzahl der dargestellten Auftrge pro Seite fest."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunningError", + "Error while running scheduled task \"{0}\"", + "Fehler beim Ausfhren der Aufgabe \"{0}\""); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.Cancellation", + "The scheduled task \"{0}\" has been canceled", + "Die geplante Aufgabe \"{0}\" wurde abgebrochen"); + + builder.AddOrUpdate("Admin.Common.HttpStatus", + "HTTP status {0} ({1}).", + "HTTP-Status {0} ({1})."); + + builder.AddOrUpdate("Admin.System.Warnings.SitemapReachable.MethodNotAllowed", + "The reachability of the sitemap could not be validated.", + "Die Erreichbarkeit der Sitemap konnte nicht berprft werden."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx new file mode 100644 index 0000000000..738509bbb4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.Designer.cs new file mode 100644 index 0000000000..d4625babee --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class CheckoutAttributeMultiStore : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CheckoutAttributeMultiStore)); + + string IMigrationMetadata.Id + { + get { return "201605191216116_CheckoutAttributeMultiStore"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs new file mode 100644 index 0000000000..1b6ba5a31f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs @@ -0,0 +1,17 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class CheckoutAttributeMultiStore : DbMigration + { + public override void Up() + { + AddColumn("dbo.CheckoutAttribute", "LimitedToStores", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.CheckoutAttribute", "LimitedToStores"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx new file mode 100644 index 0000000000..d743254b0e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.Designer.cs new file mode 100644 index 0000000000..e3a2d0014f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ExportRevision : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportRevision)); + + string IMigrationMetadata.Id + { + get { return "201605201911421_ExportRevision"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs new file mode 100644 index 0000000000..d168932260 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs @@ -0,0 +1,204 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Linq; + using Core.Domain; + using Core.Domain.DataExchange; + using Setup; + using Utilities; + + public partial class ExportRevision : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ExportDeployment", "ResultInfo", c => c.String()); + AddColumn("dbo.ExportDeployment", "SubFolder", c => c.String(maxLength: 400)); + AlterColumn("dbo.ExportProfile", "FolderName", c => c.String(nullable: false, maxLength: 400)); + DropColumn("dbo.ExportDeployment", "CreateZip"); + } + + public override void Down() + { + AddColumn("dbo.ExportDeployment", "CreateZip", c => c.Boolean(nullable: false)); + AlterColumn("dbo.ExportProfile", "FolderName", c => c.String(nullable: false, maxLength: 100)); + DropColumn("dbo.ExportDeployment", "SubFolder"); + DropColumn("dbo.ExportDeployment", "ResultInfo"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + // migrate folder name to folder path + var rootPath = "~/App_Data/ExportProfiles/"; + var exportProfiles = context.Set().ToList(); + + foreach (var profile in exportProfiles) + { + if (!profile.FolderName.EmptyNull().StartsWith(rootPath)) + { + profile.FolderName = rootPath + profile.FolderName; + } + } + + context.SaveChanges(); + + // migrate public file system deployment to new public deployment + if (context.ColumnExists("ExportDeployment", "IsPublic")) + { + var fileSystemDeploymentTypeId = (int)ExportDeploymentType.FileSystem; + var publicFolderDeploymentTypeId = (int)ExportDeploymentType.PublicFolder; + + context.ExecuteSqlCommand("Update [ExportDeployment] Set DeploymentTypeId = {0} Where DeploymentTypeId = {1} And IsPublic = 1", + true, null, publicFolderDeploymentTypeId, fileSystemDeploymentTypeId); + + context.ColumnDelete("ExportDeployment", "IsPublic"); + } + + var oldFileManagerPath = CommonHelper.MapPath("~/Content/filemanager"); + FileSystemHelper.ClearDirectory(oldFileManagerPath, true); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName", + "Folder path", + "Ordnerpfad", + "Specifies the relative path of the folder where to export the data.", + "Legt den relativen Pfad des Ordners fest, in den die Daten exportiert werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FileNamePattern.Validate", + "Please enter a valid pattern for file names. Example for file names: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%", + "Bitte ein gltiges Muster fr Dateinamen eingeben. Beispiel: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"); + + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName.Validate", + "Please enter a valid, relative folder path for the export data.", + "Bitte einen gltigen, relativen Ordnerpfad fr die zu exportierenden Daten eingeben."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.Http", "HTTP POST", "HTTP POST"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.PublicFolder", "Public folder", "ffentlicher Ordner"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.SubFolder", + "Name of subfolder", + "Name des Unterordners", + "Specifies the name of a subfolder where to publish the data.", + "Legt den Namen eines Unterordners fest, in den die Daten verffentlicht werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ZipUsageNote", + "If there are a large number of export files, it is recommended to use the option Create ZIP archive. This saves time and avoids problems, such as a full email mailbox.", + "Bei einer groen Anzahl an Exportdateien wird empfohlen die Option ZIP-Archiv erstellen zu benutzen. Das spart Zeit und vermeidet Probleme, wie z.B. ein volles E-Mail Postfach."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Cleanup", + "Clean up after successful deployment", + "Nach erfolgreicher Verffentlichung aufrumen", + "Specifies whether to delete unneeded files after all deployments succeeded.", + "Legt fest, ob nicht mehr bentigte Dateien gelscht werden sollen, nachdem alle Verffentlichungen erfolgreich waren."); + + builder.AddOrUpdate("Admin.Common.FtpStatus", + "FTP status {0} ({1}).", + "FTP-Status {0} ({1})."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.CopyFileFailed", + "At least one file could not be copied.", + "Mindestens eine Datei konnte nicht kopiert werden."); + + builder.AddOrUpdate("Admin.Common.LastRun", "Last run", "Letzte Ausfhrung"); + builder.AddOrUpdate("Admin.Common.SuccessfulOn", "Successful on", "Erfolgreich am"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Name", + "Name of profile", + "Name des Profils", + "Specifies the name of the publishing profile.", + "Legt den Namen des Verffentlichungsprofils fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ProfilesTitle", + "Publishing profiles", + "Verffentlichungsprofile"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.NoProfiles", + "There are no publishing profiles.", + "Es liegen keine Verffentlichungsprofile vor."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Note", + "Click New profile to add one or multiple publishing profiles to specify how to further proceed with the export files.", + "Legen Sie ber Neues Profil ein oder mehrere Verffentlichungsprofile an, um festzulegen wie mit den Exportdateien weiter zu verfahren ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.PublishingTarget", + "Publishing target", + "Verffentlichungsziel"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.DeploymentType", + "Publishing type", + "Art der Verffentlichung", + "Specifies the type of publishing.", + "Legt die Art Verffentlichung fest."); + + builder.AddOrUpdate("Common.Publishing", + "Publishing", + "Verffentlichung"); + + builder.AddOrUpdate("Common.NoFilesAvailable", + "There are no files available.", + "Es sind keine Dateien vorhanden."); + + builder.AddOrUpdate("Common.CopyToClipboard", "Copy to clipboard", "In die Zwischenablage kopieren"); + builder.AddOrUpdate("Common.CopyToClipboard.Succeeded", "Copied!", "Kopiert!"); + builder.AddOrUpdate("Common.CopyToClipboard.Failded", "Failed to copy.", "Kopieren ist fehlgeschlagen."); + + builder.AddOrUpdate("Products.NoBundledItems", + "No bundle items available", + "Keine Bundle-Bestandteile vorhanden"); + + builder.AddOrUpdate("Common.NoFileUploaded", + "There was no file uploaded.", + "Es wurde keine Datei hochgeladen."); + + builder.AddOrUpdate("Products.BasePriceInfo", + "Content: {0} {1} ({2} / {3} {1})", + "Inhalt: {0} {1} ({2} / {3} {1})"); + + builder.AddOrUpdate("Products.BasePriceInfo.LanguageInsensitive", + "{0} {1} ({2} / {3} {1})", + "{0} {1} ({2} / {3} {1})"); + + builder.AddOrUpdate("PrivateMessages.Disabled", + "Private messages are disabled.", + "Private Nachrichten sind deaktiviert."); + + builder.AddOrUpdate("Common.MethodNotSupportedForGuests", + "This function is not available for guests.", + "Diese Funktion steht fr Gste nicht zur Verfgung."); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement", + "Privacy consent", + "Einwilligungserklrung Datenschutz"); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement.MustBeAccepted", + "Please agree to the storage of your data.", + "Bitte stimmen Sie der Speicherung Ihrer Daten zu."); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement.DetailText", + "Yes I've read the privacy terms and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry.", + "Ja, ich habe die Datenschutzerklrung zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs", + "Get privacy consent for contact requests", + "Einwilligungserklrung im Kontaktformular fordern", + "Specifies whether a checkbox will be displayed on the contact page which requests the user to agree on storage of his data.", + "Bestimmt ob im Kontaktformular eine Checkbox angezeigt wird, die den Benutzer auffordert der Speicherung seiner Daten zuzustimmen."); + + + builder.Delete( + "Admin.DataExchange.Export.FolderAndFileName.Validate", + "Admin.DataExchange.Export.Deployment.IsPublic", + "Admin.DataExchange.Export.Deployment.CreateZip", + "Admin.Common.TemporaryFiles", + "Admin.Common.NoTempFilesFound"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx new file mode 100644 index 0000000000..a653f94065 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs new file mode 100644 index 0000000000..16b788408f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class FixExportDeploymentIsPublic : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(FixExportDeploymentIsPublic)); + + string IMigrationMetadata.Id + { + get { return "201607111548571_FixExportDeploymentIsPublic"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs new file mode 100644 index 0000000000..f48207c9fc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs @@ -0,0 +1,21 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using Core.Data; + + public partial class FixExportDeploymentIsPublic : DbMigration + { + public override void Up() + { + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + Sql("IF EXISTS(SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'ExportDeployment' AND c.name = 'IsPublic') ALTER TABLE [dbo].[ExportDeployment] DROP COLUMN [IsPublic];"); + } + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx new file mode 100644 index 0000000000..55e598ca59 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs new file mode 100644 index 0000000000..b397af928a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs @@ -0,0 +1,68 @@ +using System.Data.Entity; +using System.Linq; +using SmartStore.Core.Domain.Configuration; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Logging; + +namespace SmartStore.Data.Setup +{ + internal class ActivityLogTypeMigrator + { + private readonly SmartObjectContext _ctx; + private readonly DbSet _activityLogTypeRecords; + + public ActivityLogTypeMigrator(SmartObjectContext ctx) + { + Guard.ArgumentNotNull(() => ctx); + + _ctx = ctx; + _activityLogTypeRecords = _ctx.Set(); + } + + private Language GetDefaultAdminLanguage(DbSet settingRecords, DbSet languageRecords) + { + const string settingKey = "LocalizationSettings.DefaultAdminLanguageId"; + + var defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey && x.StoreId == 0); + + if (defaultAdminLanguageSetting == null) + defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey); + + if (defaultAdminLanguageSetting != null) + { + var defaultAdminLanguageId = defaultAdminLanguageSetting.Value.ToInt(); + if (defaultAdminLanguageId != 0) + { + var language = languageRecords.FirstOrDefault(x => x.Id == defaultAdminLanguageId); + if (language != null) + return language; + } + } + + return languageRecords.First(); + } + + public void AddActivityLogType(string systemKeyword, string enName, string deName) + { + Guard.ArgumentNotEmpty(() => systemKeyword); + Guard.ArgumentNotEmpty(() => enName); + Guard.ArgumentNotEmpty(() => deName); + + var record = _activityLogTypeRecords.FirstOrDefault(x => x.SystemKeyword == systemKeyword); + + if (record == null) + { + var language = GetDefaultAdminLanguage(_ctx.Set(), _ctx.Set()); + + _activityLogTypeRecords.Add(new ActivityLogType + { + Enabled = true, + SystemKeyword = systemKeyword, + Name = (language.UniqueSeoCode.IsCaseInsensitiveEqual("de") ? deName : enName) + }); + + _ctx.SaveChanges(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs index 9a1d698396..92d059bec8 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs @@ -35,7 +35,7 @@ public void Migrate(IEnumerable entries, bool updateTouched using (var scope = new DbContextScope(_ctx, autoDetectChanges: false)) { - var langMap = _languages.ToDictionary(x => x.UniqueSeoCode.EmptyNull().ToLower()); + var langMap = _languages.ToDictionarySafe(x => x.UniqueSeoCode.EmptyNull().ToLower()); var toDelete = new List(); var toUpdate = new List(); diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index 38e93f169a..c5c2745302 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -4048,7 +4048,7 @@ public IList MessageTemplates() { Name = "ReturnRequestStatusChanged.CustomerNotification", Subject = "%Store.Name%. Return request status was changed.", - Body = templateHeader + "

%Store.Name%



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

" + templateFooter, + Body = templateHeader + "

%Store.Name%



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

" + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 7e8cce3347..ff7fd39e2b 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -93,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 @@ -121,6 +121,7 @@ + @@ -371,6 +372,43 @@ 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 + + @@ -694,6 +732,33 @@ 201511271019577_ExportFramework3.cs + + 201512151526290_ImportFramework.cs + + + 201601262000441_ImportFramework1.cs + + + 201603121451066_ThirdPartyEmailHandOver.cs + + + 201605020640016_GtinMpnIndex.cs + + + 201605061916117_SwapColumnMappingValues.cs + + + 201605111140288_ImportExtraData.cs + + + 201605191216116_CheckoutAttributeMultiStore.cs + + + 201605201911421_ExportRevision.cs + + + 201607111548571_FixExportDeploymentIsPublic.cs + diff --git a/src/Libraries/SmartStore.Data/packages.config b/src/Libraries/SmartStore.Data/packages.config index b8d63c5067..b1c169951c 100644 --- a/src/Libraries/SmartStore.Data/packages.config +++ b/src/Libraries/SmartStore.Data/packages.config @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs index dcc6c9762c..35e853bbfc 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs @@ -17,7 +17,7 @@ public AuthorizeState(string returnUrl, OpenAuthenticationStatus openAuthenticat _returnUrl = returnUrl; AuthenticationStatus = openAuthenticationStatus; - //in way SEO friendly language URLs will be persisted + // in a way SEO friendly language URLs will be persisted if (AuthenticationStatus == OpenAuthenticationStatus.Authenticated) Result = new RedirectResult(!string.IsNullOrEmpty(_returnUrl) ? _returnUrl : "~/"); } diff --git a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs index b948930425..05cddc7f7b 100644 --- a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs @@ -12,7 +12,7 @@ namespace SmartStore.Services.Authentication ///
public partial class FormsAuthenticationService : IAuthenticationService { - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; private readonly ICustomerService _customerService; private readonly CustomerSettings _customerSettings; private readonly TimeSpan _expirationTimeSpan; @@ -33,7 +33,6 @@ public FormsAuthenticationService(HttpContextBase httpContext, ICustomerService this._expirationTimeSpan = FormsAuthentication.Timeout; } - public virtual void SignIn(Customer customer, bool createPersistentCookie) { var now = DateTime.UtcNow.ToLocalTime(); @@ -95,6 +94,21 @@ public virtual Customer GetAuthenticatedCustomer() if (customer != null && customer.Active && !customer.Deleted && customer.IsRegistered()) { + if (customer.LastLoginDateUtc == null) + { + try + { + // This is most probably the very first "login" after registering. Delete the + // ASP.NET anonymous id cookie so that a new guest account can be created + // upon signing out. + System.Web.Security.AnonymousIdentificationModule.ClearAnonymousIdentifier(); + } + finally + { + customer.LastLoginDateUtc = DateTime.UtcNow; + _customerService.UpdateCustomer(customer); + } + } _cachedCustomer = customer; } @@ -117,5 +131,5 @@ public virtual Customer GetAuthenticatedCustomerFromTicket(FormsAuthenticationTi return customer; } - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index a525580283..86d8e52b2a 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -154,7 +154,7 @@ public virtual void InheritAclIntoChildren(int categoryId, _categoryRepository.Update(subcategory); } - var existingAclRecords = _aclService.GetAclRecords(subcategory).ToDictionary(x => x.CustomerRoleId); + var existingAclRecords = _aclService.GetAclRecords(subcategory).ToDictionarySafe(x => x.CustomerRoleId); foreach (var customerRole in allCustomerRoles) { @@ -186,7 +186,7 @@ public virtual void InheritAclIntoChildren(int categoryId, _productRepository.Update(product); } - var existingAclRecords = _aclService.GetAclRecords(product).ToDictionary(x => x.CustomerRoleId); + var existingAclRecords = _aclService.GetAclRecords(product).ToDictionarySafe(x => x.CustomerRoleId); foreach (var customerRole in allCustomerRoles) { diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 71792bc582..f786bf9c78 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -84,8 +84,8 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli var utcNow = DateTime.UtcNow; // product download & sample download - int downloadId = product.DownloadId; - int? sampleDownloadId = product.SampleDownloadId; + int downloadId = 0; + int? sampleDownloadId = null; if (product.IsDownload) { @@ -518,6 +518,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli { var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, ShowHidden = true diff --git a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs index 18f9103883..24917da095 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs @@ -32,24 +32,26 @@ public partial interface IManufacturerService /// Manufacturer collection IList GetAllManufacturers(bool showHidden = false); - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// A value indicating whether to show hidden records - /// Manufacturer collection - IList GetAllManufacturers(string manufacturerName, bool showHidden = false); - - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Manufacturers - IPagedList GetAllManufacturers(string manufacturerName, - int pageIndex, int pageSize, bool showHidden = false); + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturer collection + IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false); + + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Page index + /// Page size + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturers + IPagedList GetAllManufacturers(string manufacturerName, + int pageIndex, int pageSize, int storeId = 0, bool showHidden = false); /// /// Gets a manufacturer diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs index 64a412d339..f8565562e5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs @@ -33,21 +33,20 @@ public partial interface IProductAttributeParser IEnumerable ParseProductVariantAttributeValues(string attributesXml); /// - /// Get list of localized product variant attribute values + /// Get list of product variant attribute values /// - /// XML formatted attributes + /// Map of combined attributes /// Product variant attributes - /// Language identifier - /// List of localized product variant attribute values - IList ParseProductVariantAttributeValues(string attributesXml, IEnumerable attributes, int languageId = 0); + /// List of product variant attribute values + IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes); - /// - /// Gets selected product variant attribute value - /// + /// + /// Gets selected product variant attribute value + /// /// XML formatted attributes - /// Product variant attribute identifier - /// Product variant attribute value - IList ParseValues(string attributesXml, int productVariantAttributeId); + /// Product variant attribute identifier + /// Product variant attribute value + IList ParseValues(string attributesXml, int productVariantAttributeId); /// /// Adds an attribute diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs index eb7dfa9fb9..0b1f7649d5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs @@ -108,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 /// @@ -330,8 +344,9 @@ public partial interface IProductService /// Get product pictures by product identifiers /// /// Product identifiers + /// Whether to only load the first picture for each product /// Product pictures - Multimap GetProductPicturesByProductIds(int[] productIds); + Multimap GetProductPicturesByProductIds(int[] productIds, bool onlyFirstPicture = false); /// /// Gets a product picture diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs new file mode 100644 index 0000000000..3f7315905b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Events; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Seo; +using SmartStore.Services.Stores; +using SmartStore.Utilities; + +namespace SmartStore.Services.Catalog.Importer +{ + public class CategoryImporter : EntityImporterBase + { + private readonly IRepository _categoryRepository; + private readonly IRepository _pictureRepository; + private readonly ICommonServices _services; + private readonly ICategoryTemplateService _categoryTemplateService; + private readonly IPictureService _pictureService; + private readonly ILocalizedEntityService _localizedEntityService; + private readonly FileDownloadManager _fileDownloadManager; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "FullName", x => x.FullName }, + { "Description", x => x.Description }, + { "BottomDescription", x => x.BottomDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle } + }; + + public CategoryImporter( + IRepository categoryRepository, + IRepository pictureRepository, + ICommonServices services, + ICategoryTemplateService categoryTemplateService, + IPictureService pictureService, + ILocalizedEntityService localizedEntityService, + FileDownloadManager fileDownloadManager) + { + _categoryRepository = categoryRepository; + _pictureRepository = pictureRepository; + _services = services; + _categoryTemplateService = categoryTemplateService; + _pictureService = pictureService; + _localizedEntityService = localizedEntityService; + _fileDownloadManager = fileDownloadManager; + } + + protected override void Import(ImportExecuteContext context) + { + var srcToDestId = new Dictionary(); + + var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); + + using (var scope = new DbContextScope(ctx: context.Services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _categoryRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + try + { + ProcessCategories(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories"); + } + + // reduce batch to saved (valid) products. + // No need to perform import operations on errored products. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // process slugs + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Category).Name); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } + + // process store mappings + if (segmenter.HasColumn("StoreIds")) + { + try + { + ProcessStoreMappings(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + } + } + + // localizations + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizedProperties"); + } + + // process pictures + if (srcToDestId.Any() && segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessPictures(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessPictures"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } + } + + // map parent id of inserted categories + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId") && !segmenter.IsIgnored("ParentCategoryId")) + { + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + _categoryRepository.Context.DetachAll(false); + + try + { + ProcessParentMappings(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + } + } + } + } + } + + protected virtual int ProcessPictures( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + Picture picture = null; + var equalPictureId = 0; + + foreach (var row in batch) + { + try + { + var srcId = row.GetDataValue("Id"); + var urlOrPath = row.GetDataValue("ImageUrl"); + + if (srcId != 0 && srcToDestId.ContainsKey(srcId) && urlOrPath.HasValue()) + { + var currentPictures = new List(); + var category = _categoryRepository.GetById(srcToDestId[srcId].DestinationId); + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + var image = CreateDownloadImage(urlOrPath, seoName, 1); + + if (category != null && image != null) + { + if (image.Url.HasValue() && !image.Success.HasValue) + { + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); + } + + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); + var pictureBinary = File.ReadAllBytes(image.Path); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + if (category.PictureId.HasValue && (picture = _pictureRepository.GetById(category.PictureId.Value)) != null) + currentPictures.Add(picture); + + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + if ((picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false)) != null) + { + category.PictureId = picture.Id; + + _categoryRepository.Update(category); + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls"); + } + } + } + else if (image.Url.HasValue()) + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls"); + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); + } + } + + var num = _categoryRepository.Context.SaveChanges(); + + return num; + } + + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + string[] localizedProperties) + { + if (localizedProperties.Length == 0) + { + return 0; + } + + bool shouldSave = false; + + foreach (var row in batch) + { + foreach (var prop in localizedProperties) + { + var lambda = _localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + _localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } + + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + return 0; + } + + protected virtual int ProcessParentMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + var rawParentId = row.GetDataValue("ParentCategoryId"); + var parentId = rawParentId.ToInt(-1); + + if (id != 0 && parentId != -1 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentId)) + { + // only touch hierarchical data if child and parent were inserted + if (srcToDestId[id].Inserted && srcToDestId[parentId].Inserted && srcToDestId[id].DestinationId != 0) + { + var category = _categoryRepository.GetById(srcToDestId[id].DestinationId); + if (category != null) + { + category.ParentCategoryId = srcToDestId[parentId].DestinationId; + + _categoryRepository.Update(category); + } + } + } + } + + var num = _categoryRepository.Context.SaveChanges(); + + return num; + } + + protected virtual int ProcessCategories( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _categoryRepository.AutoCommitEnabled = true; + + Category lastInserted = null; + Category lastUpdated = null; + var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; + + foreach (var row in batch) + { + Category category = null; + var id = row.GetDataValue("Id"); + var name = row.GetDataValue("Name"); + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Id": + if (id != 0) + category = _categoryRepository.GetById(id); + break; + case "Name": + if (name.HasValue()) + category = _categoryRepository.Table.FirstOrDefault(x => x.Name == name); + break; + } + + if (category != null) + break; + } + + if (category == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + // a Name is required with new categories + if (!row.Segmenter.HasColumn("Name")) + { + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new categories. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + category = new Category(); + } + + row.Initialize(category, name ?? category.Name); + + if (!row.IsNew && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.FullName); + row.SetProperty(context.Result, (x) => x.Description); + row.SetProperty(context.Result, (x) => x.BottomDescription); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.PageSize, 12); + row.SetProperty(context.Result, (x) => x.AllowCustomersToSelectPageSize, true); + row.SetProperty(context.Result, (x) => x.PageSizeOptions); + row.SetProperty(context.Result, (x) => x.PriceRanges); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HasDiscountsApplied); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.Alias); + row.SetProperty(context.Result, (x) => x.DefaultViewMode); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("CategoryTemplateViewPath", out tvp, row.IsTransient)) + { + category.CategoryTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + category.UpdatedOnUtc = UtcNow; + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportCategoryMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _categoryRepository.Insert(category); + lastInserted = category; + } + else + { + _categoryRepository.Update(category); + lastUpdated = category; + } + } + + // commit whole batch at once + var num = _categoryRepository.Context.SaveChanges(); + + // get new category ids + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + + if (id != 0 && srcToDestId.ContainsKey(id)) + srcToDestId[id].DestinationId = row.Entity.Id; + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Id", "Name" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Name", "Id" }; + } + } + + public class ImportCategoryMapping + { + public int DestinationId { get; set; } + public bool Inserted { get; set; } + } + } + +} diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs new file mode 100644 index 0000000000..6228f5b443 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -0,0 +1,738 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Events; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Utilities; + +namespace SmartStore.Services.Catalog.Importer +{ + public class ProductImporter : EntityImporterBase + { + private readonly IRepository _productPictureRepository; + private readonly IRepository _productManufacturerRepository; + private readonly IRepository _productCategoryRepository; + private readonly IRepository _productRepository; + private readonly ICommonServices _services; + private readonly ILocalizedEntityService _localizedEntityService; + private readonly IPictureService _pictureService; + private readonly IManufacturerService _manufacturerService; + private readonly ICategoryService _categoryService; + private readonly IProductService _productService; + private readonly IProductTemplateService _productTemplateService; + private readonly FileDownloadManager _fileDownloadManager; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "ShortDescription", x => x.ShortDescription }, + { "FullDescription", x => x.FullDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle }, + { "BundleTitleText", x => x.BundleTitleText } + }; + + public ProductImporter( + IRepository productPictureRepository, + IRepository productManufacturerRepository, + IRepository productCategoryRepository, + IRepository productRepository, + ICommonServices services, + ILocalizedEntityService localizedEntityService, + IPictureService pictureService, + IManufacturerService manufacturerService, + ICategoryService categoryService, + IProductService productService, + IProductTemplateService productTemplateService, + FileDownloadManager fileDownloadManager) + { + _productPictureRepository = productPictureRepository; + _productManufacturerRepository = productManufacturerRepository; + _productCategoryRepository = productCategoryRepository; + _productRepository = productRepository; + _services = services; + _localizedEntityService = localizedEntityService; + _pictureService = pictureService; + _manufacturerService = manufacturerService; + _categoryService = categoryService; + _productService = productService; + _productTemplateService = productTemplateService; + _fileDownloadManager = fileDownloadManager; + } + + protected override void Import(ImportExecuteContext context) + { + var srcToDestId = new Dictionary(); + + var templateViewPaths = _productTemplateService.GetAllProductTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); + + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _productRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + // =========================================================================== + // 1.) Import products + // =========================================================================== + try + { + ProcessProducts(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProducts"); + } + + // reduce batch to saved (valid) products. + // No need to perform import operations on errored products. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // =========================================================================== + // 2.) Import SEO Slugs + // IMPORTANT: Unlike with Products AutoCommitEnabled must be TRUE, + // as Slugs are going to be validated against existing ones in DB. + // =========================================================================== + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try + { + _productRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Product).Name); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + } + finally + { + _productRepository.Context.AutoDetectChangesEnabled = false; + } + } + + // =========================================================================== + // 3.) Import StoreMappings + // =========================================================================== + if (segmenter.HasColumn("StoreIds")) + { + try + { + ProcessStoreMappings(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + } + } + + // =========================================================================== + // 4.) Import Localizations + // =========================================================================== + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizations"); + } + + // =========================================================================== + // 5.) Import product category mappings + // =========================================================================== + if (segmenter.HasColumn("CategoryIds")) + { + try + { + ProcessProductCategories(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductCategories"); + } + } + + // =========================================================================== + // 6.) Import product manufacturer mappings + // =========================================================================== + if (segmenter.HasColumn("ManufacturerIds")) + { + try + { + ProcessProductManufacturers(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductManufacturers"); + } + } + + // =========================================================================== + // 7.) Import product picture mappings + // =========================================================================== + if (segmenter.HasColumn("ImageUrls")) + { + try + { + ProcessProductPictures(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductPictures"); + } + } + } + + // =========================================================================== + // 8.) Map parent id of inserted products + // =========================================================================== + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentGroupedProductId") && !segmenter.IsIgnored("ParentGroupedProductId")) + { + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + _productRepository.Context.DetachAll(false); + + try + { + ProcessProductMappings(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + } + } + } + } + } + + protected virtual int ProcessProducts( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + Product lastInserted = null; + Product lastUpdated = null; + var defaultTemplateId = templateViewPaths["ProductTemplate.Simple"]; + + foreach (var row in batch) + { + Product product = null; + var id = row.GetDataValue("Id"); + + foreach (var keyName in context.KeyFieldNames) + { + var keyValue = row.GetDataValue(keyName); + + if (keyValue.HasValue() || id > 0) + { + switch (keyName) + { + case "Id": + product = _productService.GetProductById(id); + break; + case "Sku": + product = _productService.GetProductBySku(keyValue); + break; + case "Gtin": + product = _productService.GetProductByGtin(keyValue); + break; + case "ManufacturerPartNumber": + product = _productService.GetProductByManufacturerPartNumber(keyValue); + break; + case "Name": + product = _productService.GetProductByName(keyValue); + break; + } + } + + if (product != null) + break; + } + + if (product == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + // a Name is required for new products. + if (!row.HasDataValue("Name")) + { + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + product = new Product(); + } + + var name = row.GetDataValue("Name"); + + row.Initialize(product, name ?? product.Name); + + if (!row.IsNew) + { + if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + } + + row.SetProperty(context.Result, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); + row.SetProperty(context.Result, (x) => x.VisibleIndividually, true); + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.ShortDescription); + row.SetProperty(context.Result, (x) => x.FullDescription); + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HomePageDisplayOrder); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.AllowCustomerReviews, true); + row.SetProperty(context.Result, (x) => x.ApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.NotApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.ApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.NotApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.Sku); + row.SetProperty(context.Result, (x) => x.ManufacturerPartNumber); + row.SetProperty(context.Result, (x) => x.Gtin); + row.SetProperty(context.Result, (x) => x.IsGiftCard); + row.SetProperty(context.Result, (x) => x.GiftCardTypeId); + row.SetProperty(context.Result, (x) => x.RequireOtherProducts); + row.SetProperty(context.Result, (x) => x.RequiredProductIds); // TODO: global scope + row.SetProperty(context.Result, (x) => x.AutomaticallyAddRequiredProducts); + row.SetProperty(context.Result, (x) => x.IsDownload); + row.SetProperty(context.Result, (x) => x.DownloadId); + row.SetProperty(context.Result, (x) => x.UnlimitedDownloads, true); + row.SetProperty(context.Result, (x) => x.MaxNumberOfDownloads, 10); + row.SetProperty(context.Result, (x) => x.DownloadExpirationDays); + row.SetProperty(context.Result, (x) => x.DownloadActivationTypeId, 1); + row.SetProperty(context.Result, (x) => x.HasSampleDownload); + row.SetProperty(context.Result, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); // TODO: global scope + row.SetProperty(context.Result, (x) => x.HasUserAgreement); + row.SetProperty(context.Result, (x) => x.UserAgreementText); + row.SetProperty(context.Result, (x) => x.IsRecurring); + row.SetProperty(context.Result, (x) => x.RecurringCycleLength, 100); + row.SetProperty(context.Result, (x) => x.RecurringCyclePeriodId); + row.SetProperty(context.Result, (x) => x.RecurringTotalCycles, 10); + row.SetProperty(context.Result, (x) => x.IsShipEnabled, true); + row.SetProperty(context.Result, (x) => x.IsFreeShipping); + row.SetProperty(context.Result, (x) => x.AdditionalShippingCharge); + row.SetProperty(context.Result, (x) => x.IsEsd); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.TaxCategoryId, 1); // TODO: global scope + row.SetProperty(context.Result, (x) => x.ManageInventoryMethodId); + row.SetProperty(context.Result, (x) => x.StockQuantity, 10000); + row.SetProperty(context.Result, (x) => x.DisplayStockAvailability); + row.SetProperty(context.Result, (x) => x.DisplayStockQuantity); + row.SetProperty(context.Result, (x) => x.MinStockQuantity); + row.SetProperty(context.Result, (x) => x.LowStockActivityId); + row.SetProperty(context.Result, (x) => x.NotifyAdminForQuantityBelow, 1); + row.SetProperty(context.Result, (x) => x.BackorderModeId); + row.SetProperty(context.Result, (x) => x.AllowBackInStockSubscriptions); + row.SetProperty(context.Result, (x) => x.OrderMinimumQuantity, 1); + row.SetProperty(context.Result, (x) => x.OrderMaximumQuantity, 10000); + row.SetProperty(context.Result, (x) => x.AllowedQuantities); + row.SetProperty(context.Result, (x) => x.DisableBuyButton); + row.SetProperty(context.Result, (x) => x.DisableWishlistButton); + row.SetProperty(context.Result, (x) => x.AvailableForPreOrder); + row.SetProperty(context.Result, (x) => x.CallForPrice); + row.SetProperty(context.Result, (x) => x.Price); + row.SetProperty(context.Result, (x) => x.OldPrice); + row.SetProperty(context.Result, (x) => x.ProductCost); + row.SetProperty(context.Result, (x) => x.SpecialPrice); + row.SetProperty(context.Result, (x) => x.SpecialPriceStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.SpecialPriceEndDateTimeUtc); + row.SetProperty(context.Result, (x) => x.CustomerEntersPrice); + row.SetProperty(context.Result, (x) => x.MinimumCustomerEnteredPrice); + row.SetProperty(context.Result, (x) => x.MaximumCustomerEnteredPrice, 1000); + // HasTierPrices... ignore as long as no tier prices are imported + // LowestAttributeCombinationPrice... ignore as long as no combinations are imported + row.SetProperty(context.Result, (x) => x.Weight); + row.SetProperty(context.Result, (x) => x.Length); + row.SetProperty(context.Result, (x) => x.Width); + row.SetProperty(context.Result, (x) => x.Height); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.DeliveryTimeId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.QuantityUnitId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.BasePriceEnabled); + row.SetProperty(context.Result, (x) => x.BasePriceMeasureUnit); + row.SetProperty(context.Result, (x) => x.BasePriceAmount); + row.SetProperty(context.Result, (x) => x.BasePriceBaseAmount); + row.SetProperty(context.Result, (x) => x.BundleTitleText); + row.SetProperty(context.Result, (x) => x.BundlePerItemShipping); + row.SetProperty(context.Result, (x) => x.BundlePerItemPricing); + row.SetProperty(context.Result, (x) => x.BundlePerItemShoppingCart); + row.SetProperty(context.Result, (x) => x.AvailableStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.AvailableEndDateTimeUtc); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("ProductTemplateViewPath", out tvp, row.IsTransient)) + { + product.ProductTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + product.UpdatedOnUtc = UtcNow; + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportProductMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _productRepository.Insert(product); + lastInserted = product; + } + else + { + _productRepository.Update(product); + lastUpdated = product; + } + } + + // commit whole batch at once + var num = _productRepository.Context.SaveChanges(); + + // get new product ids + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + + if (id != 0 && srcToDestId.ContainsKey(id)) + srcToDestId[id].DestinationId = row.Entity.Id; + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + protected virtual int ProcessProductMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + var parentGroupedProductId = row.GetDataValue("ParentGroupedProductId"); + + if (id != 0 && parentGroupedProductId != 0 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentGroupedProductId)) + { + // only touch relationship if child and parent were inserted + if (srcToDestId[id].Inserted && srcToDestId[parentGroupedProductId].Inserted && srcToDestId[id].DestinationId != 0) + { + var product = _productRepository.GetById(srcToDestId[id].DestinationId); + if (product != null) + { + product.ParentGroupedProductId = srcToDestId[parentGroupedProductId].DestinationId; + _productRepository.Update(product); + } + } + } + } + + var num = _productRepository.Context.SaveChanges(); + + return num; + } + + protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnumerable> batch) + { + // true, cause pictures must be saved and assigned an id prior adding a mapping. + _productPictureRepository.AutoCommitEnabled = true; + + ProductPicture lastInserted = null; + var equalPictureId = 0; + var numberOfPictures = (context.ExtraData.NumberOfPictures ?? int.MaxValue); + + foreach (var row in batch) + { + var imageUrls = row.GetDataValue>("ImageUrls"); + if (imageUrls.IsNullOrEmpty()) + continue; + + var imageNumber = 0; + var displayOrder = -1; + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + var imageFiles = new List(); + + // collect required image file infos + foreach (var urlOrPath in imageUrls) + { + var image = CreateDownloadImage(urlOrPath, seoName, ++imageNumber); + + if (image != null) + imageFiles.Add(image); + + if (imageFiles.Count >= numberOfPictures) + break; + } + + // download images + if (imageFiles.Any(x => x.Url.HasValue())) + { + // async downloading in batch processing is inefficient cause only the image processing benefits from async, + // not the record processing itself. a per record processing may speed up the import. + + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, imageFiles.Where(x => x.Url.HasValue() && !x.Success.HasValue))); + } + + // import images + foreach (var image in imageFiles.OrderBy(x => x.DisplayOrder)) + { + try + { + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); + var pictureBinary = File.ReadAllBytes(image.Path); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var currentProductPictures = _productPictureRepository.TableUntracked.Expand(x => x.Picture) + .Where(x => x.ProductId == row.Entity.Id) + .ToList(); + + var currentPictures = currentProductPictures + .Select(x => x.Picture) + .ToList(); + + if (displayOrder == -1) + { + displayOrder = (currentProductPictures.Any() ? currentProductPictures.Select(x => x.DisplayOrder).Max() : 0); + } + + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + // no equal picture found in sequence + var newPicture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false); + if (newPicture != null) + { + var mapping = new ProductPicture + { + ProductId = row.Entity.Id, + PictureId = newPicture.Id, + DisplayOrder = ++displayOrder + }; + + _productPictureRepository.Insert(mapping); + lastInserted = mapping; + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + } + else if (image.Url.HasValue()) + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.ToAllMessages(), row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + } + + protected virtual int ProcessProductManufacturers(ImportExecuteContext context, IEnumerable> batch) + { + _productManufacturerRepository.AutoCommitEnabled = false; + + ProductManufacturer lastInserted = null; + + foreach (var row in batch) + { + var manufacturerIds = row.GetDataValue>("ManufacturerIds"); + if (!manufacturerIds.IsNullOrEmpty()) + { + try + { + foreach (var id in manufacturerIds) + { + if (_productManufacturerRepository.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.ManufacturerId == id).FirstOrDefault() == null) + { + // ensure that manufacturer exists + var manufacturer = _manufacturerService.GetManufacturerById(id); + if (manufacturer != null) + { + var productManufacturer = new ProductManufacturer + { + ProductId = row.Entity.Id, + ManufacturerId = manufacturer.Id, + IsFeaturedProduct = false, + DisplayOrder = 1 + }; + _productManufacturerRepository.Insert(productManufacturer); + lastInserted = productManufacturer; + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.Message, row.GetRowInfo(), "ManufacturerIds"); + } + } + } + + // commit whole batch at once + var num = _productManufacturerRepository.Context.SaveChanges(); + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + _services.EventPublisher.EntityInserted(lastInserted); + + return num; + } + + protected virtual int ProcessProductCategories(ImportExecuteContext context, IEnumerable> batch) + { + _productCategoryRepository.AutoCommitEnabled = false; + + ProductCategory lastInserted = null; + + foreach (var row in batch) + { + var categoryIds = row.GetDataValue>("CategoryIds"); + if (!categoryIds.IsNullOrEmpty()) + { + try + { + foreach (var id in categoryIds) + { + if (_productCategoryRepository.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.CategoryId == id).FirstOrDefault() == null) + { + // ensure that category exists + var category = _categoryService.GetCategoryById(id); + if (category != null) + { + var productCategory = new ProductCategory + { + ProductId = row.Entity.Id, + CategoryId = category.Id, + IsFeaturedProduct = false, + DisplayOrder = 1 + }; + _productCategoryRepository.Insert(productCategory); + lastInserted = productCategory; + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.Message, row.GetRowInfo(), "CategoryIds"); + } + } + } + + // commit whole batch at once + var num = _productCategoryRepository.Context.SaveChanges(); + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + _services.EventPublisher.EntityInserted(lastInserted); + + return num; + } + + + private int? ZeroToNull(object value, CultureInfo culture) + { + int result; + if (CommonHelper.TryConvert(value, culture, out result) && result > 0) + { + return result; + } + + return (int?)null; + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Id", "Sku", "Gtin", "ManufacturerPartNumber", "Name" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Sku", "Gtin", "ManufacturerPartNumber" }; + } + } + + public class ImportProductMapping + { + public int DestinationId { get; set; } + public bool Inserted { get; set; } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs index e616f8cb2b..9128610551 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs @@ -98,22 +98,19 @@ public virtual IQueryable GetManufacturers(bool showHidden = false if (!showHidden) query = query.Where(m => m.Published); - if (!showHidden) + if (!QuerySettings.IgnoreMultiStore && storeId > 0) { - if (!QuerySettings.IgnoreMultiStore && storeId > 0) - { - query = from m in query - join sm in _storeMappingRepository.Table - on new { c1 = m.Id, c2 = "Manufacturer" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into m_sm - from sm in m_sm.DefaultIfEmpty() - where !m.LimitedToStores || storeId == sm.StoreId - select m; - - query = from m in query - group m by m.Id into mGroup - orderby mGroup.Key - select mGroup.FirstOrDefault(); - } + query = from m in query + join sm in _storeMappingRepository.Table + on new { c1 = m.Id, c2 = "Manufacturer" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into m_sm + from sm in m_sm.DefaultIfEmpty() + where !m.LimitedToStores || storeId == sm.StoreId + select m; + + query = from m in query + group m by m.Id into mGroup + orderby mGroup.Key + select mGroup.FirstOrDefault(); } return query; @@ -126,18 +123,19 @@ orderby mGroup.Key /// Manufacturer collection public virtual IList GetAllManufacturers(bool showHidden = false) { - return GetAllManufacturers(null, showHidden); + return GetAllManufacturers(null, 0, showHidden); } /// /// Gets all manufacturers /// /// Manufacturer name + /// Whether to filter result by store identifier /// A value indicating whether to show hidden records /// Manufacturer collection - public virtual IList GetAllManufacturers(string manufacturerName, bool showHidden = false) + public virtual IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false) { - var query = GetManufacturers(showHidden, _storeContext.CurrentStore.Id); + var query = GetManufacturers(showHidden, storeId); if (manufacturerName.HasValue()) query = query.Where(m => m.Name.Contains(manufacturerName)); @@ -148,19 +146,20 @@ public virtual IList GetAllManufacturers(string manufacturerName, var manufacturers = query.ToList(); return manufacturers; } - - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Manufacturers - public virtual IPagedList GetAllManufacturers(string manufacturerName, - int pageIndex, int pageSize, bool showHidden = false) + + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Page index + /// Page size + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturers + public virtual IPagedList GetAllManufacturers(string manufacturerName, + int pageIndex, int pageSize, int storeId = 0, bool showHidden = false) { - var manufacturers = GetAllManufacturers(manufacturerName, showHidden); + var manufacturers = GetAllManufacturers(manufacturerName, storeId, showHidden); return new PagedList(manufacturers, pageIndex, pageSize); } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 1ce3b347ce..ca45f495d7 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -251,6 +251,12 @@ protected virtual decimal GetPreselectedPrice(Product product, PriceCalculationC if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) { product.MergedDataValues = new Dictionary { { "Price", selectedCombination.Price.Value } }; + + if (selectedCombination.BasePriceAmount.HasValue) + product.MergedDataValues.Add("BasePriceAmount", selectedCombination.BasePriceAmount.Value); + + if (selectedCombination.BasePriceBaseAmount.HasValue) + product.MergedDataValues.Add("BasePriceBaseAmount", selectedCombination.BasePriceBaseAmount.Value); } } @@ -756,12 +762,14 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, { product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); - decimal attributesTotalPrice = decimal.Zero; + var attributesTotalPrice = decimal.Zero; - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); + 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); diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs index 23ee794f0b..cf4531f322 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs @@ -13,14 +13,13 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; -using SmartStore.Services.Localization; namespace SmartStore.Services.Catalog { - /// - /// Product attribute parser - /// - public partial class ProductAttributeParser : IProductAttributeParser + /// + /// Product attribute parser + /// + public partial class ProductAttributeParser : IProductAttributeParser { // 0 = ProductId, 1 = AttributeXml Hash private const string ATTRIBUTECOMBINATION_BY_ID_HASH = "SmartStore.parsedattributecombination.id-{0}-{1}"; @@ -194,21 +193,20 @@ where id.HasValue() return values; } - public IList ParseProductVariantAttributeValues(string attributesXml, IEnumerable attributes, int languageId = 0) + public virtual IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes) { - var values = new List(); + var result = new List(); - if (attributesXml.IsEmpty()) - return values; + if (attributeCombination == null || !attributeCombination.Any()) + return result; var allValueIds = new List(); - var combinedAttributes = DeserializeProductVariantAttributes(attributesXml); foreach (var pva in attributes.Where(x => x.ShouldHaveValues()).OrderBy(x => x.DisplayOrder)) { - if (combinedAttributes.ContainsKey(pva.Id)) + if (attributeCombination.ContainsKey(pva.Id)) { - var pvaValuesStr = combinedAttributes[pva.Id]; + var pvaValuesStr = attributeCombination[pva.Id]; var ids = pvaValuesStr.Where(x => x.HasValue()).Select(x => x.ToInt()); allValueIds.AddRange(ids); @@ -220,28 +218,24 @@ public IList ParseProductVariantAttributeValues(string attributesXml, IE foreach (var attribute in attributes) { var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == id); - if (attributeValue != null) + if (attributeValue != null && !result.Any(x => x.Id == attributeValue.Id)) { - var value = attributeValue.GetLocalized(x => x.Name, languageId, true, false); - - if (!values.Any(x => x.IsCaseInsensitiveEqual(value))) - values.Add(value); + result.Add(attributeValue); break; } } } - return values; + return result; } - - /// - /// Gets selected product variant attribute value - /// - /// Attributes - /// Product variant attribute identifier - /// Product variant attribute value - public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) + /// + /// Gets selected product variant attribute value + /// + /// Attributes + /// Product variant attribute identifier + /// Product variant attribute value + public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) { var selectedProductVariantAttributeValues = new List(); try @@ -374,15 +368,20 @@ public virtual ProductVariantAttributeCombination FindProductVariantAttributeCom public virtual List> DeserializeQueryData(string jsonData) { - if (jsonData.HasValue()) + try { - if (jsonData.StartsWith("[")) + if (jsonData.HasValue()) { - return JsonConvert.DeserializeObject>>(jsonData); - } + if (jsonData.StartsWith("[")) + { + return JsonConvert.DeserializeObject>>(jsonData); + } - return new List> { JsonConvert.DeserializeObject>(jsonData) }; + return new List> { JsonConvert.DeserializeObject>(jsonData) }; + } } + catch { } + return new List>(); } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index 85b55cfb4f..051fc841f2 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -14,7 +14,7 @@ namespace SmartStore.Services.Catalog { - public static class ProductExtensions + public static class ProductExtensions { public static ProductVariantAttributeCombination MergeWithCombination(this Product product, string selectedAttributes) { @@ -299,7 +299,7 @@ public static int[] ParseRequiredProductIds(this Product product) } /// - /// Gets the base price + /// Gets the base price info /// /// Product /// Localization service @@ -309,17 +309,23 @@ public static int[] ParseRequiredProductIds(this Product product) /// Price calculation service /// Target currency /// Price adjustment - /// Whether the result string should be language independent - /// The base price - public static string GetBasePriceInfo(this Product product, ILocalizationService localizationService, IPriceFormatter priceFormatter, - ICurrencyService currencyService, ITaxService taxService, IPriceCalculationService priceCalculationService, - Currency currency, decimal priceAdjustment = decimal.Zero, bool languageIndependent = false) + /// Whether the result string should be language insensitive + /// The base price info + public static string GetBasePriceInfo(this Product product, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, + ICurrencyService currencyService, + ITaxService taxService, + IPriceCalculationService priceCalculationService, + Currency currency, + decimal priceAdjustment = decimal.Zero, + bool languageInsensitive = false) { - if (product == null) - throw new ArgumentNullException("product"); - - if (localizationService == null && !languageIndependent) - throw new ArgumentNullException("localizationService"); + Guard.ArgumentNotNull(() => product); + Guard.ArgumentNotNull(() => currencyService); + Guard.ArgumentNotNull(() => taxService); + Guard.ArgumentNotNull(() => priceCalculationService); + Guard.ArgumentNotNull(() => currency); if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) { @@ -327,24 +333,58 @@ public static string GetBasePriceInfo(this Product product, ILocalizationService var taxrate = decimal.Zero; var currentPrice = priceCalculationService.GetFinalPrice(product, workContext.CurrentCustomer, true); - decimal price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); + var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); price = currencyService.ConvertFromPrimaryStoreCurrency(price, currency); - decimal basePriceValue = Convert.ToDecimal((price / product.BasePriceAmount) * product.BasePriceBaseAmount); + return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency, languageInsensitive); + } - string basePrice = priceFormatter.FormatPrice(basePriceValue, true, currency); - string unit = "{0} {1}".FormatWith(product.BasePriceBaseAmount, product.BasePriceMeasureUnit); + return ""; + } - if (languageIndependent) - { - return "{0} / {1}".FormatWith(basePrice, unit); - } + /// + /// Gets the base price info + /// + /// Product + /// The calculated product price + /// Localization service + /// Price formatter + /// Target currency + /// Whether the result string should be language insensitive + /// The base price info + public static string GetBasePriceInfo(this Product product, + decimal productPrice, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, + Currency currency, + bool languageInsensitive = false) + { + Guard.ArgumentNotNull(() => product); + Guard.ArgumentNotNull(() => localizationService); + Guard.ArgumentNotNull(() => priceFormatter); + Guard.ArgumentNotNull(() => currency); + + if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) + { + var value = Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); + var valueFormatted = priceFormatter.FormatPrice(value, true, currency); + var amountFormatted = Math.Round(product.BasePriceAmount.Value, 2).ToString("G29"); + + var infoTemplate = localizationService.GetResource(languageInsensitive ? "Products.BasePriceInfo.LanguageInsensitive" : "Products.BasePriceInfo"); + + var result = infoTemplate.FormatInvariant( + amountFormatted, + product.BasePriceMeasureUnit, + valueFormatted, + product.BasePriceBaseAmount + ); + + return result; + } - return localizationService.GetResource("Products.BasePriceInfo").FormatWith(basePrice, unit); - } return ""; - } + } public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) { diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index 88112f4064..6dc93b582d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -1149,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 /// @@ -1825,20 +1855,31 @@ orderby pp.DisplayOrder return productPictures; } - public virtual Multimap GetProductPicturesByProductIds(int[] productIds) + public virtual Multimap GetProductPicturesByProductIds(int[] productIds, bool onlyFirstPicture = false) { var query = from pp in _productPictureRepository.TableUntracked.Expand(x => x.Picture) where productIds.Contains(pp.ProductId) + orderby pp.ProductId, pp.DisplayOrder select pp; - var map = query - .OrderBy(x => x.ProductId) - .ThenBy(x => x.DisplayOrder) - .ToList() - .ToMultimap(x => x.ProductId, x => x); + if (onlyFirstPicture) + { + var map = query.GroupBy(x => x.ProductId, x => x) + .Select(x => x.FirstOrDefault()) + .ToList() + .ToMultimap(x => x.ProductId, x => x); - return map; + return map; + } + else + { + var map = query + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } } /// diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs index 53cad4b1a5..a80d073138 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs @@ -11,10 +11,10 @@ namespace SmartStore.Services.Catalog { - /// - /// Product tag service - /// - public partial class ProductTagService : IProductTagService + /// + /// Product tag service + /// + public partial class ProductTagService : IProductTagService { #region Constants @@ -36,6 +36,7 @@ public partial class ProductTagService : IProductTagService #region Fields private readonly IRepository _productTagRepository; + private readonly IRepository _storeMappingRepository; private readonly IDataProvider _dataProvider; private readonly IDbContext _dbContext; private readonly CommonSettings _commonSettings; @@ -55,7 +56,9 @@ public partial class ProductTagService : IProductTagService /// Common settings /// Cache manager /// Event published - public ProductTagService(IRepository productTagRepository, + public ProductTagService( + IRepository productTagRepository, + IRepository storeMappingRepository, IDataProvider dataProvider, IDbContext dbContext, CommonSettings commonSettings, @@ -63,14 +66,19 @@ public ProductTagService(IRepository productTagRepository, IEventPublisher eventPublisher) { _productTagRepository = productTagRepository; + _storeMappingRepository = storeMappingRepository; _dataProvider = dataProvider; _dbContext = dbContext; _commonSettings = commonSettings; _cacheManager = cacheManager; _eventPublisher = eventPublisher; - } - #endregion + QuerySettings = DbQuerySettings.Default; + } + + public DbQuerySettings QuerySettings { get; set; } + + #endregion #region Nested classes @@ -94,55 +102,38 @@ private Dictionary GetProductCount(int storeId) string key = string.Format(PRODUCTTAG_COUNT_KEY, storeId); return _cacheManager.Get(key, () => { + IEnumerable tagCount = null; if (_commonSettings.UseStoredProceduresIfSupported && _dataProvider.StoredProceduresSupported) { - //stored procedures are enabled and supported by the database. - //It's much faster than the LINQ implementation below + //stored procedures are enabled and supported by the database. It's much faster than the LINQ implementation below - #region Use stored procedure - - //prepare parameters var pStoreId = _dataProvider.GetParameter(); pStoreId.ParameterName = "StoreId"; pStoreId.Value = storeId; pStoreId.DbType = DbType.Int32; - - //invoke stored procedure - var result = _dbContext.SqlQuery( - "Exec ProductTagCountLoadAll @StoreId", - pStoreId); - - var dictionary = new Dictionary(); - foreach (var item in result) - dictionary.Add(item.ProductTagId, item.ProductCount); - return dictionary; - - #endregion + tagCount = _dbContext.SqlQuery("Exec ProductTagCountLoadAll @StoreId", pStoreId); } else { //stored procedures aren't supported. Use LINQ - #region Search products - var query = from pt in _productTagRepository.Table - select new - { - Id = pt.Id, - ProductCount = pt.Products - //published and not deleted product/variants - .Count(p => !p.Deleted && p.Published) - //UNDOEN filter by store identifier if specified ( > 0 ) - }; - - var dictionary = new Dictionary(); - foreach (var item in query) - dictionary.Add(item.Id, item.ProductCount); - return dictionary; - - #endregion + tagCount = _productTagRepository.Table + .Select(pt => new ProductTagWithCount + { + ProductTagId = pt.Id, + ProductCount = (storeId > 0 && !QuerySettings.IgnoreMultiStore) ? + (from p in pt.Products + join sm in _storeMappingRepository.Table on new { pid = p.Id, pname = "Product" } equals new { pid = sm.EntityId, pname = sm.EntityName } into psm + from sm in psm.DefaultIfEmpty() + where (!p.LimitedToStores || storeId == sm.StoreId) && !p.Deleted && p.Published + select p).Count() : + pt.Products.Count(p => !p.Deleted && p.Published) + }); } + + return tagCount.ToDictionary(x => x.ProductTagId, x => x.ProductCount); }); } @@ -265,10 +256,11 @@ public virtual void UpdateProductTag(ProductTag productTag) public virtual int GetProductCount(int productTagId, int storeId) { var dictionary = GetProductCount(storeId); + if (dictionary.ContainsKey(productTagId)) return dictionary[productTagId]; - else - return 0; + + return 0; } #endregion diff --git a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs index 9f108f69ed..1055aaa340 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Text; using SmartStore.Core.Domain.Common; @@ -7,41 +8,79 @@ namespace SmartStore.Services.Common { public static class AddressExtentions { - /// - /// Find an address - /// - /// Source - /// First name - /// Last name - /// Phone number - /// Email - /// Fax number - /// Company - /// Address 1 - /// Address 2 - /// City - /// State/province identifier - /// Zip postal code - /// Country identifier - /// Address - public static Address FindAddress(this List
source, - string firstName, string lastName, string phoneNumber, - string email, string faxNumber, string company, string address1, - string address2, string city, int? stateProvinceId, - string zipPostalCode, int? countryId) + /// + /// Find first occurrence of an address + /// + /// Addresses to be searched + /// Address to find + /// First occurrence of address + public static Address FindAddress(this ICollection
source, Address address) + { + return source.FindAddress( + address.FirstName, + address.LastName, + address.PhoneNumber, + address.Email, + address.FaxNumber, + address.Company, + address.Address1, + address.Address2, + address.City, + address.StateProvinceId, + address.ZipPostalCode, + address.CountryId + ); + } + + /// + /// Find an address + /// + /// Source + /// First name + /// Last name + /// Phone number + /// Email + /// Fax number + /// Company + /// Address 1 + /// Address 2 + /// City + /// State/province identifier + /// Zip postal code + /// Country identifier + /// Address + public static Address FindAddress( + this ICollection
source, + string firstName, + string lastName, + string phoneNumber, + string email, + string faxNumber, + string company, + string address1, + string address2, + string city, + int? stateProvinceId, + string zipPostalCode, + int? countryId) { - return source.Find((a) => ((String.IsNullOrEmpty(a.FirstName) && String.IsNullOrEmpty(firstName)) || a.FirstName == firstName) && - ((String.IsNullOrEmpty(a.LastName) && String.IsNullOrEmpty(lastName)) || a.LastName == lastName) && - ((String.IsNullOrEmpty(a.PhoneNumber) && String.IsNullOrEmpty(phoneNumber)) || a.PhoneNumber == phoneNumber) && - ((String.IsNullOrEmpty(a.Email) && String.IsNullOrEmpty(email)) || a.Email == email) && - ((String.IsNullOrEmpty(a.FaxNumber) && String.IsNullOrEmpty(faxNumber)) || a.FaxNumber == faxNumber) && - ((String.IsNullOrEmpty(a.Company) && String.IsNullOrEmpty(company)) || a.Company == company) && - ((String.IsNullOrEmpty(a.Address1) && String.IsNullOrEmpty(address1)) || a.Address1 == address1) && - ((String.IsNullOrEmpty(a.Address2) && String.IsNullOrEmpty(address2)) || a.Address2 == address2) && - ((String.IsNullOrEmpty(a.City) && String.IsNullOrEmpty(city)) || a.City == city) && - ((a.StateProvinceId.IsNullOrDefault() && stateProvinceId.IsNullOrDefault()) || a.StateProvinceId == stateProvinceId) && - ((String.IsNullOrEmpty(a.ZipPostalCode) && String.IsNullOrEmpty(zipPostalCode)) || a.ZipPostalCode == zipPostalCode) && - ((a.CountryId.IsNullOrDefault() && countryId.IsNullOrDefault()) || a.CountryId == countryId)); + Func addressMatcher = (x) => + { + return x.Email.IsCaseInsensitiveEqual(email) + && x.LastName.IsCaseInsensitiveEqual(lastName) + && x.FirstName.IsCaseInsensitiveEqual(firstName) + && x.Address1.IsCaseInsensitiveEqual(address1) + && x.Address2.IsCaseInsensitiveEqual(address2) + && x.Company.IsCaseInsensitiveEqual(company) + && x.ZipPostalCode.IsCaseInsensitiveEqual(zipPostalCode) + && x.City.IsCaseInsensitiveEqual(city) + && x.PhoneNumber.IsCaseInsensitiveEqual(phoneNumber) + && x.FaxNumber.IsCaseInsensitiveEqual(faxNumber) + && x.StateProvinceId == stateProvinceId + && x.CountryId == countryId; + }; + + return source.FirstOrDefault(addressMatcher); } /// Returns the full name of the address. diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs index 4c73f2e7ed..8f23424525 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs @@ -209,49 +209,54 @@ public virtual void SaveAttribute(BaseEntity entity, string key, TPro if (entity == null) throw new ArgumentNullException("entity"); - string keyGroup = entity.GetUnproxiedEntityType().Name; + SaveAttribute(entity.Id, key, entity.GetUnproxiedEntityType().Name, value, storeId); + } + + public virtual void SaveAttribute(int entityId, string key, string keyGroup, TPropType value, int storeId = 0) + { + Guard.ArgumentNotZero(entityId, "entityId"); - var props = GetAttributesForEntity(entity.Id, keyGroup) + var props = GetAttributesForEntity(entityId, keyGroup) .Where(x => x.StoreId == storeId) .ToList(); - var prop = props.FirstOrDefault(ga => - ga.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); // should be culture invariant + var prop = props.FirstOrDefault(ga => + ga.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); // should be culture invariant - string valueStr = value.Convert(); + string valueStr = value.Convert(); - if (prop != null) - { - if (string.IsNullOrWhiteSpace(valueStr)) - { - //delete - DeleteAttribute(prop); - } - else - { - //update - prop.Value = valueStr; - UpdateAttribute(prop); - } - } - else - { - if (!string.IsNullOrWhiteSpace(valueStr)) - { - //insert - prop = new GenericAttribute() - { - EntityId = entity.Id, - Key = key, - KeyGroup = keyGroup, - Value = valueStr, + if (prop != null) + { + if (string.IsNullOrWhiteSpace(valueStr)) + { + // delete + DeleteAttribute(prop); + } + else + { + // update + prop.Value = valueStr; + UpdateAttribute(prop); + } + } + else + { + if (!string.IsNullOrWhiteSpace(valueStr)) + { + // insert + prop = new GenericAttribute + { + EntityId = entityId, + Key = key, + KeyGroup = keyGroup, + Value = valueStr, StoreId = storeId - }; - InsertAttribute(prop); - } - } - } + }; + InsertAttribute(prop); + } + } + } - #endregion - } + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs index cb837a6f74..c584c4ad4c 100644 --- a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs @@ -69,5 +69,16 @@ public partial interface IGenericAttributeService /// Value /// Store identifier; pass 0 if this attribute will be available for all stores void SaveAttribute(BaseEntity entity, string key, TPropType value, int storeId = 0); - } + + /// + /// Save attribute value + /// + /// Property type + /// Entity identifier + /// The key + /// The key group + /// Property type + /// Store identifier; pass 0 if this attribute will be available for all stores + void SaveAttribute(int entityId, string key, string keyGroup, TPropType value, int storeId = 0); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs index 060c127d3f..a6b8e24305 100644 --- a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs @@ -205,7 +205,8 @@ public virtual bool IsBot { if (!_isBot.HasValue) { - _isBot = _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; + // empty useragent > bad bot! + _isBot = this.RawValue.IsEmpty() || _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; } return _isBot.Value; } diff --git a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs index 5a03d72ea6..aa688a4e96 100644 --- a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs +++ b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs @@ -10,6 +10,7 @@ using System.Linq.Expressions; using System.Reflection; using SmartStore.ComponentModel; +using System.Collections; namespace SmartStore.Services.Configuration { @@ -309,33 +310,46 @@ public virtual bool SettingExists(T settings, 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 && !fastProp.IsSequenceType) + if (setting == null) { - #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 + if (fastProp.IsSequenceType) + { + if ((fastProp.GetValue(settings) as IEnumerable) != null) + { + // Instance of IEnumerable<> was already created, most likely in the constructor of the settings concrete class. + // In this case we shouldn't let the EnumerableConverter create a new instance but keep this one. + continue; + } + } + else + { + #region Obsolete ('EnumerableConverter' can handle this case now) + //if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + //{ + // // convenience: don't return null for simple list types + // var listArg = prop.PropertyType.GetGenericArguments()[0]; + // object list = null; + + // if (listArg == typeof(int)) + // list = new List(); + // else if (listArg == typeof(decimal)) + // list = new List(); + // else if (listArg == typeof(string)) + // list = new List(); + + // if (list != null) + // { + // fastProp.SetValue(settings, list); + // } + //} + #endregion + + continue; + } - continue; } var converter = TypeConverterFactory.GetConverter(prop.PropertyType); diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs index 719f144ed5..afc1acdbe0 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs @@ -53,7 +53,7 @@ public static bool IsBackgroundTaskAccount(this Customer customer) } /// - /// Gets a value indicating whether customer a search engine + /// Gets a value indicating whether customer is a search engine /// /// Customer /// Result @@ -84,13 +84,13 @@ public static bool IsPdfConverter(this Customer customer) return result; } - /// - /// Gets a value indicating whether customer is administrator - /// - /// Customer - /// A value indicating whether we should look only in active customer roles - /// Result - public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) + /// + /// Gets a value indicating whether customer is administrator + /// + /// Customer + /// A value indicating whether we should look only in active customer roles + /// Result + public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) { return IsInCustomerRole(customer, SystemCustomerRoleNames.Administrators, onlyActiveCustomerRoles); } @@ -287,14 +287,15 @@ public static int CountProductsInCart(this Customer customer, ShoppingCartType c return count; } - public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null, bool orderById = false) + + public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null) { var rawItems = customer.ShoppingCartItems.Filter(cartType, storeId); - if (orderById) - rawItems = rawItems.OrderByDescending(x => x.Id); - - var organizedItems = rawItems.ToList().Organize(); + var organizedItems = rawItems + .OrderByDescending(x => x.Id) + .ToList() + .Organize(); return organizedItems.ToList(); } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs index 1615a2ce96..49fb4587aa 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs @@ -3,23 +3,22 @@ using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Events; -using SmartStore.Services.Localization; +using SmartStore.Core.Localization; using SmartStore.Services.Messages; using SmartStore.Services.Security; namespace SmartStore.Services.Customers { - /// - /// Customer registration service - /// - public partial class CustomerRegistrationService : ICustomerRegistrationService + /// + /// Customer registration service + /// + public partial class CustomerRegistrationService : ICustomerRegistrationService { #region Fields private readonly ICustomerService _customerService; private readonly IEncryptionService _encryptionService; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; - private readonly ILocalizationService _localizationService; private readonly RewardPointsSettings _rewardPointsSettings; private readonly CustomerSettings _customerSettings; private readonly IStoreContext _storeContext; @@ -35,31 +34,33 @@ public partial class CustomerRegistrationService : ICustomerRegistrationService public CustomerRegistrationService(ICustomerService customerService, IEncryptionService encryptionService, INewsLetterSubscriptionService newsLetterSubscriptionService, - ILocalizationService localizationService, RewardPointsSettings rewardPointsSettings, CustomerSettings customerSettings, IStoreContext storeContext, IEventPublisher eventPublisher) { this._customerService = customerService; this._encryptionService = encryptionService; this._newsLetterSubscriptionService = newsLetterSubscriptionService; - this._localizationService = localizationService; this._rewardPointsSettings = rewardPointsSettings; this._customerSettings = customerSettings; this._storeContext = storeContext; this._eventPublisher = eventPublisher; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + #endregion - /// - /// Validate customer - /// - /// Username or email - /// Password - /// Result - public virtual bool ValidateCustomer(string usernameOrEmail, string password) + public Localizer T { get; set; } + + #region Methods + + /// + /// Validate customer + /// + /// Username or email + /// Password + /// Result + public virtual bool ValidateCustomer(string usernameOrEmail, string password) { Customer customer = null; if (_customerSettings.UsernamesEnabled) @@ -112,48 +113,46 @@ public virtual bool ValidateCustomer(string usernameOrEmail, string password) /// Result public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationRequest request) { - if (request == null) - throw new ArgumentNullException("request"); - - if (request.Customer == null) - throw new ArgumentException("Can't load current customer"); + Guard.ArgumentNotNull(() => request); + Guard.ArgumentNotNull(() => request.Customer); var result = new CustomerRegistrationResult(); + if (request.Customer.IsSearchEngineAccount()) { - result.AddError("Search engine can't be registered"); + result.AddError(T("Account.Register.Errors.CannotRegisterSearchEngine")); return result; } if (request.Customer.IsBackgroundTaskAccount()) { - result.AddError("Background task account can't be registered"); + result.AddError(T("Account.Register.Errors.CannotRegisterTaskAccount")); return result; } if (request.Customer.IsRegistered()) { - result.AddError("Current customer is already registered"); + result.AddError(T("Account.Register.Errors.AlreadyRegistered")); return result; } if (String.IsNullOrEmpty(request.Email)) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.EmailIsNotProvided")); + result.AddError(T("Account.Register.Errors.EmailIsNotProvided")); return result; } if (!request.Email.IsEmail()) { - result.AddError(_localizationService.GetResource("Common.WrongEmail")); + result.AddError(T("Common.WrongEmail")); return result; } if (String.IsNullOrWhiteSpace(request.Password)) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.PasswordIsNotProvided")); + result.AddError(T("Account.Register.Errors.PasswordIsNotProvided")); return result; } if (_customerSettings.UsernamesEnabled) { if (String.IsNullOrEmpty(request.Username)) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.UsernameIsNotProvided")); + result.AddError(T("Account.Register.Errors.UsernameIsNotProvided")); return result; } } @@ -161,14 +160,14 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR //validate unique user if (_customerService.GetCustomerByEmail(request.Email) != null) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.EmailAlreadyExists")); + result.AddError(T("Account.Register.Errors.EmailAlreadyExists")); return result; } if (_customerSettings.UsernamesEnabled) { if (_customerService.GetCustomerByUsername(request.Username) != null) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.UsernameAlreadyExists")); + result.AddError(T("Account.Register.Errors.UsernameAlreadyExists")); return result; } } @@ -180,45 +179,52 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR switch (request.PasswordFormat) { - case PasswordFormat.Clear: - { - request.Customer.Password = request.Password; - } - break; - case PasswordFormat.Encrypted: - { - request.Customer.Password = _encryptionService.EncryptText(request.Password); - } - break; - case PasswordFormat.Hashed: - { - string saltKey = _encryptionService.CreateSaltKey(5); - request.Customer.PasswordSalt = saltKey; - request.Customer.Password = _encryptionService.CreatePasswordHash(request.Password, saltKey, _customerSettings.HashedPasswordFormat); - } - break; - default: - break; + case PasswordFormat.Clear: + request.Customer.Password = request.Password; + break; + case PasswordFormat.Encrypted: + request.Customer.Password = _encryptionService.EncryptText(request.Password); + break; + case PasswordFormat.Hashed: + string saltKey = _encryptionService.CreateSaltKey(5); + request.Customer.PasswordSalt = saltKey; + request.Customer.Password = _encryptionService.CreatePasswordHash(request.Password, saltKey, _customerSettings.HashedPasswordFormat); + break; } request.Customer.Active = request.IsApproved; - - //add to 'Registered' role - var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); - if (registeredRole == null) - throw new SmartException("'Registered' role could not be loaded"); + + if (_customerSettings.RegisterCustomerRoleId != 0) + { + var customerRole = _customerService.GetCustomerRoleById(_customerSettings.RegisterCustomerRoleId); + request.Customer.CustomerRoles.Add(customerRole); + } + + //add to 'Registered' role + var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); + if (registeredRole == null) + { + throw new SmartException(T("Admin.Customers.CustomerRoles.CannotFoundRole", "Registered")); + } + request.Customer.CustomerRoles.Add(registeredRole); + //remove from 'Guests' role var guestRole = request.Customer.CustomerRoles.FirstOrDefault(cr => cr.SystemName == SystemCustomerRoleNames.Guests); - if (guestRole != null) - request.Customer.CustomerRoles.Remove(guestRole); + if (guestRole != null) + { + request.Customer.CustomerRoles.Remove(guestRole); + } - //Add reward points for customer registration (if enabled) - if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForRegistration > 0) - request.Customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForRegistration, _localizationService.GetResource("RewardPoints.Message.RegisteredAsCustomer")); + //Add reward points for customer registration (if enabled) + if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForRegistration > 0) + { + request.Customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForRegistration, T("RewardPoints.Message.RegisteredAsCustomer")); + } _customerService.UpdateCustomer(request.Customer); - _eventPublisher.Publish(new CustomerRegisteredEvent{ Customer = request.Customer }); + _eventPublisher.Publish(new CustomerRegisteredEvent { Customer = request.Customer }); + return result; } @@ -235,19 +241,19 @@ public virtual PasswordChangeResult ChangePassword(ChangePasswordRequest request var result = new PasswordChangeResult(); if (String.IsNullOrWhiteSpace(request.Email)) { - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.EmailIsNotProvided")); + result.AddError(T("Account.ChangePassword.Errors.EmailIsNotProvided")); return result; } if (String.IsNullOrWhiteSpace(request.NewPassword)) { - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.PasswordIsNotProvided")); + result.AddError(T("Account.ChangePassword.Errors.PasswordIsNotProvided")); return result; } var customer = _customerService.GetCustomerByEmail(request.Email); if (customer == null) { - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.EmailNotFound")); + result.AddError(T("Account.ChangePassword.Errors.EmailNotFound")); return result; } @@ -272,7 +278,7 @@ public virtual PasswordChangeResult ChangePassword(ChangePasswordRequest request bool oldPasswordIsValid = oldPwd == customer.Password; if (!oldPasswordIsValid) - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.OldPasswordDoesntMatch")); + result.AddError(T("Account.ChangePassword.Errors.OldPasswordDoesntMatch")); if (oldPasswordIsValid) requestIsValid = true; @@ -327,14 +333,14 @@ public virtual void SetEmail(Customer customer, string newEmail) string oldEmail = customer.Email; if (!newEmail.IsEmail()) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.NewEmailIsNotValid")); + throw new SmartException(T("Account.EmailUsernameErrors.NewEmailIsNotValid")); if (newEmail.Length > 100) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.EmailTooLong")); + throw new SmartException(T("Account.EmailUsernameErrors.EmailTooLong")); var customer2 = _customerService.GetCustomerByEmail(newEmail); if (customer2 != null && customer.Id != customer2.Id) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.EmailAlreadyExists")); + throw new SmartException(T("Account.EmailUsernameErrors.EmailAlreadyExists")); customer.Email = newEmail; _customerService.UpdateCustomer(customer); @@ -370,11 +376,11 @@ public virtual void SetUsername(Customer customer, string newUsername) newUsername = newUsername.Trim(); if (newUsername.Length > 100) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.UsernameTooLong")); + throw new SmartException(T("Account.EmailUsernameErrors.UsernameTooLong")); var user2 = _customerService.GetCustomerByUsername(newUsername); if (user2 != null && customer.Id != user2.Id) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.UsernameAlreadyExists")); + throw new SmartException(T("Account.EmailUsernameErrors.UsernameAlreadyExists")); customer.Username = newUsername; _customerService.UpdateCustomer(customer); diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index 3537ba424f..d48fe10d6c 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Web; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; @@ -16,6 +17,7 @@ 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; @@ -26,9 +28,9 @@ namespace SmartStore.Services.Customers ///
public partial class CustomerService : ICustomerService { - #region Constants + #region Constants - private const string CUSTOMERROLES_ALL_KEY = "SmartStore.customerrole.all-{0}"; + private const string CUSTOMERROLES_ALL_KEY = "SmartStore.customerrole.all-{0}"; private const string CUSTOMERROLES_BY_SYSTEMNAME_KEY = "SmartStore.customerrole.systemname-{0}"; private const string CUSTOMERROLES_PATTERN_KEY = "SmartStore.customerrole."; @@ -41,31 +43,38 @@ public partial class CustomerService : ICustomerService private readonly IRepository _gaRepository; private readonly IRepository _rewardPointsHistoryRepository; private readonly IGenericAttributeService _genericAttributeService; - private readonly ICacheManager _cacheManager; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; + private readonly ICacheManager _cache; private readonly RewardPointsSettings _rewardPointsSettings; + private readonly HttpContextBase _httpContext; + private readonly IUserAgent _userAgent; - #endregion + #endregion - #region Ctor + #region Ctor - public CustomerService(ICacheManager cacheManager, + public CustomerService( IRepository customerRepository, IRepository customerRoleRepository, IRepository gaRepository, IRepository rewardPointsHistoryRepository, IGenericAttributeService genericAttributeService, - IEventPublisher eventPublisher, - 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; } @@ -291,6 +300,11 @@ where customerIds.Contains(c.Id) return sortedCustomers; } + public virtual IList GetSystemAccountCustomers() + { + return _customerRepository.Table.Where(x => x.IsSystemAccount).ToList(); + } + public virtual Customer GetCustomerByGuid(Guid customerGuid) { if (customerGuid == Guid.Empty) @@ -327,6 +341,7 @@ orderby c.Id where c.SystemName == systemName select c; var customer = query.FirstOrDefault(); + return customer; } @@ -344,27 +359,62 @@ orderby c.Id return customer; } - public virtual Customer InsertGuestCustomer() + public virtual Customer InsertGuestCustomer(Guid? customerGuid = null) { var customer = new Customer { - CustomerGuid = Guid.NewGuid(), + CustomerGuid = customerGuid ?? Guid.NewGuid(), Active = true, CreatedOnUtc = DateTime.UtcNow, LastActivityDateUtc = DateTime.UtcNow, }; - //add to 'Guests' role + // add to 'Guests' role var guestRole = GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); if (guestRole == null) throw new SmartException("'Guests' role could not be loaded"); customer.CustomerRoles.Add(guestRole); - _customerRepository.Insert(customer); + _customerRepository.Insert(customer); + + var clientIdent = _services.WebHelper.GetClientIdent(); + if (clientIdent.HasValue()) + { + //_genericAttributeService.SaveAttribute(customer, "ClientIdent", clientIdent); + } return customer; } + + public virtual Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60) + { + if (_httpContext.IsFakeContext() || _userAgent.IsBot || _userAgent.IsPdfConverter) + { + return null; + } + + clientIdent = clientIdent.NullEmpty() ?? _services.WebHelper.GetClientIdent(); + if (clientIdent.IsEmpty()) + { + return null; + } + + var dateFrom = DateTime.UtcNow.AddSeconds(maxAgeSeconds * -1); + + var query = from a in _gaRepository.TableUntracked + join c in _customerRepository.Table on a.EntityId equals c.Id into Customers + from c in Customers.DefaultIfEmpty() + where c.LastActivityDateUtc >= dateFrom + && c.Username == null + && c.Email == null + && a.KeyGroup == "Customer" + && a.Key == "ClientIdent" + && a.Value == clientIdent + select c; + + return query.FirstOrDefault(); + } public virtual void InsertCustomer(Customer customer) { @@ -373,8 +423,7 @@ public virtual void InsertCustomer(Customer customer) _customerRepository.Insert(customer); - //event notification - _eventPublisher.EntityInserted(customer); + _services.EventPublisher.EntityInserted(customer); } public virtual void UpdateCustomer(Customer customer) @@ -384,8 +433,7 @@ public virtual void UpdateCustomer(Customer customer) _customerRepository.Update(customer); - //event notification - _eventPublisher.EntityUpdated(customer); + _services.EventPublisher.EntityUpdated(customer); } public virtual void ResetCheckoutData(Customer customer, int storeId, @@ -564,10 +612,9 @@ public virtual void DeleteCustomerRole(CustomerRole customerRole) _customerRoleRepository.Delete(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityDeleted(customerRole); + _services.EventPublisher.EntityDeleted(customerRole); } public virtual CustomerRole GetCustomerRoleById(int customerRoleId) @@ -584,7 +631,7 @@ public virtual CustomerRole GetCustomerRoleBySystemName(string systemName) return null; string key = string.Format(CUSTOMERROLES_BY_SYSTEMNAME_KEY, systemName); - return _cacheManager.Get(key, () => + return _cache.Get(key, () => { var query = from cr in _customerRoleRepository.Table orderby cr.Id @@ -598,7 +645,7 @@ orderby cr.Id public virtual IList GetAllCustomerRoles(bool showHidden = false) { string key = string.Format(CUSTOMERROLES_ALL_KEY, showHidden); - return _cacheManager.Get(key, () => + return _cache.Get(key, () => { var query = from cr in _customerRoleRepository.Table orderby cr.Name @@ -616,10 +663,9 @@ public virtual void InsertCustomerRole(CustomerRole customerRole) _customerRoleRepository.Insert(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityInserted(customerRole); + _services.EventPublisher.EntityInserted(customerRole); } public virtual void UpdateCustomerRole(CustomerRole customerRole) @@ -629,10 +675,9 @@ public virtual void UpdateCustomerRole(CustomerRole customerRole) _customerRoleRepository.Update(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityUpdated(customerRole); + _services.EventPublisher.EntityUpdated(customerRole); } #endregion diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs index ec4b470074..57a2c6f8c9 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs @@ -88,12 +88,18 @@ IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, /// Customers IList GetCustomersByIds(int[] customerIds); - /// - /// Gets a customer by GUID - /// - /// Customer GUID - /// A customer - Customer GetCustomerByGuid(Guid customerGuid); + /// + /// Get system account customers + /// + /// System account customers + IList GetSystemAccountCustomers(); + + /// + /// Gets a customer by GUID + /// + /// Customer GUID + /// A customer + Customer GetCustomerByGuid(Guid customerGuid); /// /// Get customer by email @@ -119,14 +125,26 @@ IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, /// /// Insert a guest customer /// + /// The customer GUID. Pass null to create a random one. /// Customer - Customer InsertGuestCustomer(); + Customer InsertGuestCustomer(Guid? customerGuid = null); - /// - /// Insert a customer - /// - /// Customer - void InsertCustomer(Customer customer); + /// + /// Tries to find a guest/anonymous customer record by client ident. This method should be called when an + /// anonymous visitor rejects cookies and therefore cannot be identified automatically. + /// + /// + /// The client ident string, which is a hashed combination of client IP address and user agent. + /// Call to obtain an ident string, or pass null to let this method obtain it automatically. + /// The max age of the newly created guest customer record. The shorter, the better (default is 1 min.) + /// The identified customer or null + Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60); + + /// + /// Insert a customer + /// + /// Customer + void InsertCustomer(Customer customer); /// /// Updates the customer diff --git a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs new file mode 100644 index 0000000000..452f281fb8 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs @@ -0,0 +1,653 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Events; +using SmartStore.Services.Affiliates; +using SmartStore.Services.Common; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Directory; +using SmartStore.Services.Helpers; +using SmartStore.Services.Media; +using SmartStore.Services.Security; +using SmartStore.Utilities; + +namespace SmartStore.Services.Customers.Importer +{ + public class CustomerImporter : EntityImporterBase + { + private const string _attributeKeyGroup = "Customer"; + + private readonly IRepository _customerRepository; + private readonly IRepository _pictureRepository; + private readonly ICommonServices _services; + private readonly IGenericAttributeService _genericAttributeService; + private readonly ICustomerService _customerService; + private readonly IPictureService _pictureService; + private readonly IAffiliateService _affiliateService; + private readonly ICountryService _countryService; + private readonly IStateProvinceService _stateProvinceService; + private readonly FileDownloadManager _fileDownloadManager; + private readonly CustomerSettings _customerSettings; + private readonly DateTimeSettings _dateTimeSettings; + private readonly ForumSettings _forumSettings; + + public CustomerImporter( + IRepository customerRepository, + IRepository pictureRepository, + ICommonServices services, + IGenericAttributeService genericAttributeService, + ICustomerService customerService, + IPictureService pictureService, + IAffiliateService affiliateService, + ICountryService countryService, + IStateProvinceService stateProvinceService, + FileDownloadManager fileDownloadManager, + CustomerSettings customerSettings, + DateTimeSettings dateTimeSettings, + ForumSettings forumSettings) + { + _customerRepository = customerRepository; + _pictureRepository = pictureRepository; + _services = services; + _genericAttributeService = genericAttributeService; + _customerService = customerService; + _pictureService = pictureService; + _affiliateService = affiliateService; + _countryService = countryService; + _stateProvinceService = stateProvinceService; + _fileDownloadManager = fileDownloadManager; + _customerSettings = customerSettings; + _dateTimeSettings = dateTimeSettings; + _forumSettings = forumSettings; + } + + + protected override void Import(ImportExecuteContext context) + { + var customer = _services.WorkContext.CurrentCustomer; + var allowManagingCustomerRoles = _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomerRoles, customer); + + var allAffiliateIds = _affiliateService.GetAllAffiliates(true) + .Select(x => x.Id) + .ToList(); + + var allCountries = new Dictionary(); + foreach (var country in _countryService.GetAllCountries(true)) + { + if (!allCountries.ContainsKey(country.TwoLetterIsoCode)) + allCountries.Add(country.TwoLetterIsoCode, country.Id); + + if (!allCountries.ContainsKey(country.ThreeLetterIsoCode)) + allCountries.Add(country.ThreeLetterIsoCode, country.Id); + } + + var allStateProvinces = _stateProvinceService.GetAllStateProvinces(true) + .ToDictionarySafe(x => new Tuple(x.CountryId, x.Abbreviation), x => x.Id); + + var allCustomerNumbers = new HashSet( + _genericAttributeService.GetAttributes(SystemCustomerAttributeNames.CustomerNumber, _attributeKeyGroup).Select(x => x.Value), + StringComparer.OrdinalIgnoreCase); + + using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + _customerRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + // =========================================================================== + // Process customers + // =========================================================================== + try + { + ProcessCustomers(context, batch, allAffiliateIds); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCustomers"); + } + + // reduce batch to saved (valid) records. + // No need to perform import operations on errored records. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // =========================================================================== + // Process generic attributes + // =========================================================================== + try + { + ProcessGenericAttributes(context, batch, allCountries, allStateProvinces, allCustomerNumbers); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessGenericAttributes"); + } + + // =========================================================================== + // Process avatars + // =========================================================================== + if (_customerSettings.AllowCustomersToUploadAvatars) + { + try + { + ProcessAvatars(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAvatars"); + } + } + + // =========================================================================== + // Process addresses + // =========================================================================== + try + { + _services.DbContext.AutoDetectChangesEnabled = true; + ProcessAddresses(context, batch, allCountries, allStateProvinces); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAddresses"); + } + finally + { + _services.DbContext.AutoDetectChangesEnabled = false; + } + } + } + } + + protected virtual int ProcessCustomers( + ImportExecuteContext context, + IEnumerable> batch, + List allAffiliateIds) + { + _customerRepository.AutoCommitEnabled = true; + + Customer lastInserted = null; + Customer lastUpdated = null; + var currentCustomer = _services.WorkContext.CurrentCustomer; + + var guestRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); + var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); + var forumModeratorRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.ForumModerators); + + var customerQuery = _customerRepository.Table.Expand(x => x.Addresses); + + foreach (var row in batch) + { + Customer customer = null; + var id = row.GetDataValue("Id"); + var email = row.GetDataValue("Email"); + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Id": + if (id != 0) + { + customer = customerQuery.FirstOrDefault(x => x.Id == id); + } + break; + case "CustomerGuid": + var customerGuid = row.GetDataValue("CustomerGuid"); + if (customerGuid.HasValue()) + { + var guid = new Guid(customerGuid); + customer = customerQuery.FirstOrDefault(x => x.CustomerGuid == guid); + } + break; + case "Email": + if (email.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Email == email); + } + break; + case "Username": + var userName = row.GetDataValue("Username"); + if (userName.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Username == userName); + } + break; + } + + if (customer != null) + break; + } + + if (customer == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + customer = new Customer + { + CustomerGuid = new Guid(), + AffiliateId = 0, + Active = true + }; + } + else + { + _customerRepository.Context.LoadCollection(customer, (Customer x) => x.CustomerRoles); + } + + var isGuest = row.GetDataValue("IsGuest"); + var isRegistered = row.GetDataValue("IsRegistered"); + var isAdmin = row.GetDataValue("IsAdministrator"); + var isForumModerator = row.GetDataValue("IsForumModerator"); + var affiliateId = row.GetDataValue("AffiliateId"); + + row.Initialize(customer, email ?? id.ToString()); + + row.SetProperty(context.Result, (x) => x.CustomerGuid); + row.SetProperty(context.Result, (x) => x.Username); + row.SetProperty(context.Result, (x) => x.Email); + + if (email.HasValue() && currentCustomer.Email.IsCaseInsensitiveEqual(email)) + { + context.Result.AddInfo("Security. Ignored password of current customer (who started this import).", row.GetRowInfo(), "Password"); + } + else + { + row.SetProperty(context.Result, (x) => x.Password); + row.SetProperty(context.Result, (x) => x.PasswordFormatId); + row.SetProperty(context.Result, (x) => x.PasswordSalt); + } + + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.Active); + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + row.SetProperty(context.Result, (x) => x.LastActivityDateUtc, UtcNow); + + if (affiliateId > 0 && allAffiliateIds.Contains(affiliateId)) + { + customer.AffiliateId = affiliateId; + } + + if (isAdmin) + { + context.Result.AddInfo("Security. Ignored administrator role.", row.GetRowInfo(), "IsAdministrator"); + } + + UpsertRole(row, guestRole, isGuest); + UpsertRole(row, registeredRole, isRegistered); + UpsertRole(row, forumModeratorRole, isForumModerator); + + if (row.IsTransient) + { + _customerRepository.Insert(customer); + lastInserted = customer; + } + else + { + _customerRepository.Update(customer); + lastUpdated = customer; + } + } + + var num = _customerRepository.Context.SaveChanges(); + + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + protected virtual int ProcessAddresses( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + foreach (var row in batch) + { + ImportAddress("BillingAddress.", row, context, allCountries, allStateProvinces); + ImportAddress("ShippingAddress.", row, context, allCountries, allStateProvinces); + } + + return _services.DbContext.SaveChanges(); + } + + private void ImportAddress( + string fieldPrefix, + ImportRow row, + ImportExecuteContext context, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + // last name is mandatory for an address to be imported or updated + if (!row.HasDataValue(fieldPrefix + "LastName")) + return; + + var address = new Address + { + CreatedOnUtc = UtcNow + }; + + var childRow = new ImportRow
(row.Segmenter, row.DataRow, row.Position); + childRow.Initialize(address, row.EntityDisplayName); + + childRow.SetProperty(context.Result, fieldPrefix + "LastName", x => x.LastName); + childRow.SetProperty(context.Result, fieldPrefix + "FirstName", x => x.FirstName); + childRow.SetProperty(context.Result, fieldPrefix + "Email", x => x.Email); + childRow.SetProperty(context.Result, fieldPrefix + "Company", x => x.Company); + childRow.SetProperty(context.Result, fieldPrefix + "City", x => x.City); + childRow.SetProperty(context.Result, fieldPrefix + "Address1", x => x.Address1); + childRow.SetProperty(context.Result, fieldPrefix + "Address2", x => x.Address2); + childRow.SetProperty(context.Result, fieldPrefix + "ZipPostalCode", x => x.ZipPostalCode); + childRow.SetProperty(context.Result, fieldPrefix + "PhoneNumber", x => x.PhoneNumber); + childRow.SetProperty(context.Result, fieldPrefix + "FaxNumber", x => x.FaxNumber); + + childRow.SetProperty(context.Result, fieldPrefix + "CountryId", x => x.CountryId); + if (childRow.Entity.CountryId == null) + { + // try with country code + childRow.SetProperty(context.Result, fieldPrefix + "CountryCode", x => x.CountryId, converter: (val, ci) => CountryCodeToId(allCountries, val.ToString())); + } + + var countryId = childRow.Entity.CountryId; + + if (countryId.HasValue) + { + childRow.SetProperty(context.Result, fieldPrefix + "StateProvinceId", x => x.StateProvinceId); + if (childRow.Entity.StateProvinceId == null) + { + // try with state abbreviation + childRow.SetProperty(context.Result, fieldPrefix + "StateAbbreviation", x => x.StateProvinceId, converter: (val, ci) => StateAbbreviationToId(allStateProvinces, countryId, val.ToString())); + } + } + + if (!childRow.IsDirty) + { + // Not one single property could be set. Get out! + return; + } + + var appliedAddress = row.Entity.Addresses.FindAddress(address); + + if (appliedAddress == null) + { + appliedAddress = address; + row.Entity.Addresses.Add(appliedAddress); + } + + if (fieldPrefix == "BillingAddress.") + { + row.Entity.BillingAddress = appliedAddress; + } + else if (fieldPrefix == "ShippingAddress.") + { + row.Entity.ShippingAddress = appliedAddress; + } + + _customerRepository.Update(row.Entity); + } + + protected virtual int ProcessGenericAttributes( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces, + HashSet allCustomerNumbers) + { + foreach (var row in batch) + { + SaveAttribute(row, SystemCustomerAttributeNames.FirstName); + SaveAttribute(row, SystemCustomerAttributeNames.LastName); + + if (_dateTimeSettings.AllowCustomersToSetTimeZone) + SaveAttribute(row, SystemCustomerAttributeNames.TimeZoneId); + + if (_customerSettings.GenderEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Gender); + + if (_customerSettings.DateOfBirthEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.DateOfBirth); + + if (_customerSettings.CompanyEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Company); + + if (_customerSettings.StreetAddressEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress); + + if (_customerSettings.StreetAddress2Enabled) + SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress2); + + if (_customerSettings.ZipPostalCodeEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.ZipPostalCode); + + if (_customerSettings.CityEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.City); + + if (_customerSettings.CountryEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.CountryId); + + if (_customerSettings.CountryEnabled && _customerSettings.StateProvinceEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId); + + if (_customerSettings.PhoneEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Phone); + + if (_customerSettings.FaxEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Fax); + + if (_forumSettings.ForumsEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.ForumPostCount); + + if (_forumSettings.SignaturesEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Signature); + + var countryId = CountryCodeToId(allCountries, row.GetDataValue("CountryCode")); + var stateId = StateAbbreviationToId(allStateProvinces, countryId, row.GetDataValue("StateAbbreviation")); + + if (countryId.HasValue) + { + SaveAttribute(row, SystemCustomerAttributeNames.CountryId, countryId.Value); + } + + if (stateId.HasValue) + { + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId, stateId.Value); + } + + string customerNumber = null; + + if (_customerSettings.CustomerNumberMethod == CustomerNumberMethod.AutomaticallySet) + customerNumber = row.Entity.Id.ToString(); + else + customerNumber = row.GetDataValue("CustomerNumber"); + + if (customerNumber.IsEmpty() || !allCustomerNumbers.Contains(customerNumber)) + { + SaveAttribute(row, SystemCustomerAttributeNames.CustomerNumber, customerNumber); + + if (!customerNumber.IsEmpty()) + allCustomerNumbers.Add(customerNumber); + } + } + + return _services.DbContext.SaveChanges(); + } + + protected virtual int ProcessAvatars( + ImportExecuteContext context, + IEnumerable> batch) + { + foreach (var row in batch) + { + var urlOrPath = row.GetDataValue("AvatarPictureUrl"); + if (urlOrPath.IsEmpty()) + continue; + + Picture picture = null; + var equalPictureId = 0; + var currentPictures = new List(); + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + + var image = CreateDownloadImage(urlOrPath, seoName, 1); + if (image == null) + continue; + + if (image.Url.HasValue() && !image.Success.HasValue) + { + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); + } + + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); + var pictureBinary = File.ReadAllBytes(image.Path); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var currentPictureId = row.Entity.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId); + if (currentPictureId != 0 && (picture = _pictureRepository.GetById(currentPictureId)) != null) + { + currentPictures.Add(picture); + } + + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + if ((picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false)) != null) + { + _pictureRepository.Context.SaveChanges(); + SaveAttribute(row, SystemCustomerAttributeNames.AvatarPictureId, picture.Id); + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "AvatarPictureUrl"); + } + } + } + else + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "AvatarPictureUrl"); + } + } + + return _services.DbContext.SaveChanges(); + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Id", "CustomerGuid", "Email", "Username" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Email" }; + } + } + + private int? CountryCodeToId(Dictionary allCountries, string code) + { + int countryId; + if (code.HasValue() && allCountries.TryGetValue(code, out countryId) && countryId != 0) + { + return countryId; + } + + return null; + } + + private int? StateAbbreviationToId(Dictionary, int> allStateProvinces, int? countryId, string abbreviation) + { + if (countryId.HasValue && abbreviation.HasValue()) + { + var key = Tuple.Create(countryId.Value, abbreviation); + + int stateId; + if (allStateProvinces.TryGetValue(key, out stateId) && stateId != 0) + { + return stateId; + } + } + + return null; + } + + private void SaveAttribute(ImportRow row, string key) + { + SaveAttribute(row, key, row.GetDataValue(key)); + } + + private void SaveAttribute(ImportRow row, string key) + { + + SaveAttribute(row, key, row.GetDataValue(key)); + } + + private void SaveAttribute(ImportRow row, string key, TPropType value) + { + if (row.IsTransient) + return; + + if (row.IsNew || value != null) + { + _genericAttributeService.SaveAttribute(row.Entity.Id, key, _attributeKeyGroup, value); + } + } + + private void UpsertRole(ImportRow row, CustomerRole role, bool value) + { + if (role == null) + return; + + var hasRole = row.Entity.CustomerRoles.Any(x => x.SystemName == role.SystemName); + + if (value && !hasRole) + { + row.Entity.CustomerRoles.Add(role); + } + else if (!value && hasRole) + { + row.Entity.CustomerRoles.Remove(role); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ColumnMap.cs b/src/Libraries/SmartStore.Services/DataExchange/ColumnMap.cs deleted file mode 100644 index 5149e2c6d8..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ColumnMap.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SmartStore.Services.DataExchange -{ - public class ColumnMap - { - private readonly Dictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public IReadOnlyDictionary Mappings - { - get { return _map; } - } - - public void AddMapping(string sourceName, string mappedName) - { - AddMapping(sourceName, null, mappedName); - } - - public void AddMapping(string sourceName, string index, string mappedName) - { - Guard.ArgumentNotEmpty(() => sourceName); - Guard.ArgumentNotEmpty(() => mappedName); - - _map[CreateName(sourceName, index)] = mappedName; - } - - /// - /// Gets a mapped column name - /// - /// The name of the column to get a mapped name for. - /// The mapped column name OR - if the name is unmapped - the passed - public string GetMappedName(string sourceName) - { - string result; - if (_map.TryGetValue(sourceName, out result)) - { - return result; - } - - return sourceName; - } - - /// - /// Gets a mapped column name - /// - /// The name of the column to get a mapped name for. - /// The column index, e.g. a language code (de, en etc.) - /// The mapped column name OR - if the name is unmapped - the passed [] - public string GetMappedName(string sourceName, string index) - { - sourceName = CreateName(sourceName, index); - - string result; - if (_map.TryGetValue(sourceName, out result)) - { - return result; - } - - return sourceName; - } - - internal static string CreateName(string name, string index) - { - if (index.HasValue()) - { - name += String.Concat("[", index, "]"); - } - - return name; - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs index 30798f67e6..1680cf2d55 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs @@ -97,6 +97,14 @@ public static CsvConfiguration ExcelFriendlyConfiguration } } + /// + /// 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: #). /// @@ -119,7 +127,7 @@ public char Escape if (value == _escape) return; - if (new char[] { '\n', '\r', '\0' }.Contains(value)) + if (PresetCharacters.Contains(value)) { throw new SmartException("'{0}' is not a valid escape char.".FormatInvariant(value)); } @@ -145,7 +153,7 @@ public char Delimiter if (value == _delimiter) return; - if (new char[] { '\n', '\r', '\0' }.Contains(value)) + if (PresetCharacters.Contains(value)) { throw new SmartException("'{0}' is not a valid delimiter char.".FormatInvariant(value)); } @@ -172,7 +180,7 @@ public char Quote if (value == _quote) return; - if (new char[] { '\n', '\r', '\0' }.Contains(value)) + if (PresetCharacters.Contains(value)) { throw new SmartException("'{0}' is not a valid quote char.".FormatInvariant(value)); } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs index 9b70b63282..4a81792a19 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs @@ -32,6 +32,14 @@ public override object ConvertFrom(CultureInfo culture, object 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)) @@ -48,5 +56,10 @@ public override object ConvertTo(CultureInfo culture, string format, object valu 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/CsvWriter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs index 83eee20228..8bb9942052 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs @@ -51,7 +51,7 @@ public CsvConfiguration Configuration /// to complete writing of the current row. ///
/// The fields to write. - public virtual void WriteFields(string[] fields) + public virtual void WriteFields(IEnumerable fields) { Guard.ArgumentNotNull(() => fields); fields.Each(x => WriteField(x)); @@ -69,7 +69,7 @@ public virtual void WriteFields(string[] fields) ///
/// The fields to write. /// True to quote the fields, otherwise false. - public virtual void WriteFields(string[] fields, bool shouldQuote) + public virtual void WriteFields(IEnumerable fields, bool shouldQuote) { Guard.ArgumentNotNull(() => fields); fields.Each(x => WriteField(x, shouldQuote)); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FileSystemFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Deployment/FileSystemFilePublisher.cs deleted file mode 100644 index fb9750120f..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FileSystemFilePublisher.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.IO; -using System.Web; -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) - { - string folderDestination = null; - - if (deployment.IsPublic) - { - folderDestination = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); - } - else if (deployment.FileSystemPath.IsEmpty()) - { - return; - } - else if (deployment.FileSystemPath.StartsWith("/") || deployment.FileSystemPath.StartsWith("\\") || !Path.IsPathRooted(deployment.FileSystemPath)) - { - folderDestination = CommonHelper.MapPath(deployment.FileSystemPath); - } - else - { - folderDestination = deployment.FileSystemPath; - } - - if (!System.IO.Directory.Exists(folderDestination)) - { - System.IO.Directory.CreateDirectory(folderDestination); - } - - if (deployment.CreateZip) - { - var path = Path.Combine(folderDestination, deployment.Profile.FolderName + ".zip"); - - if (FileSystemHelper.Copy(context.ZipPath, path)) - context.Log.Information("Copied ZIP archive " + path); - } - else - { - FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(folderDestination)); - - context.Log.Information("Copied export data files to " + folderDestination); - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/IFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Deployment/IFilePublisher.cs deleted file mode 100644 index 38d79fbb8a..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/IFilePublisher.cs +++ /dev/null @@ -1,22 +0,0 @@ -using SmartStore.Core.Domain; -using SmartStore.Core.Logging; - -namespace SmartStore.Services.DataExchange.Export.Deployment -{ - public interface IFilePublisher - { - void Publish(ExportDeploymentContext context, ExportDeployment deployment); - } - - - public class ExportDeploymentContext - { - public TraceLogger Log { get; set; } - - public string[] DeploymentFiles { get; set; } - - public string FolderContent { get; set; } - - public string ZipPath { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs index d5eb02d5a9..d46879193c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs @@ -6,8 +6,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using OfficeOpenXml; namespace SmartStore.Services.DataExchange.Excel diff --git a/src/Libraries/SmartStore.Services/DataExchange/DataExportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs similarity index 86% rename from src/Libraries/SmartStore.Services/DataExchange/DataExportResult.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs index d7e0d9fa6a..c9d3fa26f2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/DataExportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs @@ -37,17 +37,11 @@ public bool Succeeded [XmlIgnore] public string FileFolder { get; set; } - /// - /// Suggested download file name - /// - [XmlIgnore] - public string DownloadFileName { get; set; } - [Serializable] public class ExportFileInfo { /// - /// Store identifier + /// Store identifier, can be 0. /// public int StoreId { get; set; } diff --git a/src/Libraries/SmartStore.Services/DataExchange/DataExportTask.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs similarity index 88% rename from src/Libraries/SmartStore.Services/DataExchange/DataExportTask.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs index c08be803c5..f32f06c625 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/DataExportTask.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs @@ -33,16 +33,11 @@ public void Execute(TaskExecutionContext ctx) // build export request var request = new DataExportRequest(profile, provider); - request.ProgressValueSetter = delegate(int val, int max, string msg) + request.ProgressValueSetter = delegate (int val, int max, string msg) { ctx.SetProgress(val, max, msg, true); }; - request.ProgressMessageSetter = delegate(string msg) - { - ctx.SetProgress(null, msg, true); - }; - if (ctx.Parameters.ContainsKey("SelectedIds")) { request.EntitiesToExport = ctx.Parameters["SelectedIds"] diff --git a/src/Libraries/SmartStore.Services/DataExchange/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs similarity index 74% rename from src/Libraries/SmartStore.Services/DataExchange/DataExporter.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 9193f38739..e6b182c270 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -30,6 +30,7 @@ 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; @@ -60,10 +61,11 @@ public partial class DataExporter : IDataExporter private readonly Lazy _productAttributeParser; private readonly Lazy _productAttributeService; private readonly Lazy _productTemplateService; - private readonly Lazy _productService; + private readonly Lazy _categoryTemplateService; + private readonly Lazy _productService; private readonly Lazy _orderService; private readonly Lazy _manufacturerService; - private readonly Lazy _customerService; + private readonly ICustomerService _customerService; private readonly Lazy _addressService; private readonly Lazy _countryService; private readonly Lazy _shipmentService; @@ -78,9 +80,10 @@ public partial class DataExporter : IDataExporter private readonly Lazy> _subscriptionRepository; private readonly Lazy> _orderRepository; - private readonly Lazy _dataExchangeSettings; private readonly Lazy _mediaSettings; private readonly Lazy _contactDataSettings; + private readonly Lazy _customerSettings; + private readonly Lazy _catalogSettings; public DataExporter( ICommonServices services, @@ -99,10 +102,11 @@ public DataExporter( Lazy productAttributeParser, Lazy productAttributeService, Lazy productTemplateService, + Lazy categoryTemplateService, Lazy productService, Lazy orderService, Lazy manufacturerService, - Lazy customerService, + ICustomerService customerService, Lazy addressService, Lazy countryService, Lazy shipmentService, @@ -115,9 +119,10 @@ public DataExporter( Lazy> customerRepository, Lazy> subscriptionRepository, Lazy> orderRepository, - Lazy dataExchangeSettings, Lazy mediaSettings, - Lazy contactDataSettings) + Lazy contactDataSettings, + Lazy customerSettings, + Lazy catalogSettings) { _services = services; _dbContext = dbContext; @@ -135,6 +140,7 @@ public DataExporter( _productAttributeParser = productAttributeParser; _productAttributeService = productAttributeService; _productTemplateService = productTemplateService; + _categoryTemplateService = categoryTemplateService; _productService = productService; _orderService = orderService; _manufacturerService = manufacturerService; @@ -153,9 +159,10 @@ public DataExporter( _subscriptionRepository = subscriptionRepository; _orderRepository = orderRepository; - _dataExchangeSettings = dataExchangeSettings; _mediaSettings = mediaSettings; _contactDataSettings = contactDataSettings; + _customerSettings = customerSettings; + _catalogSettings = catalogSettings; T = NullLocalizer.Instance; } @@ -178,9 +185,7 @@ private void SetProgress(DataExporterContext ctx, int loadedRecords) 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); } } @@ -193,12 +198,39 @@ private void SetProgress(DataExporterContext ctx, string message) { if (!ctx.IsPreview && message.HasValue()) { - ctx.Request.ProgressMessageSetter.Invoke(message); + 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 @@ -243,18 +275,15 @@ private void DetachAllEntitiesAndClear(DataExporterContext ctx) private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, int pageIndex = 0) { - var offset = ctx.Request.Profile.Offset + (pageIndex * PageSize); - - var limit = (ctx.IsPreview ? PageSize : ctx.Request.Profile.Limit); - - var recordsPerSegment = (ctx.IsPreview ? 0 : ctx.Request.Profile.BatchSize); - - var totalCount = ctx.Request.Profile.Offset + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; + 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.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetProducts(ctx, skip), entities => @@ -264,7 +293,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), x => _productService.Value.GetTierPricesByProductIds(x, (ctx.Projection.CurrencyId ?? 0) != 0 ? ctx.ContextCustomer : null, ctx.Store.Id), - x => _categoryService.Value.GetProductCategoriesByProductIds(x), + x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, true), x => _manufacturerService.Value.GetProductManufacturersByProductIds(x), x => _productService.Value.GetProductPicturesByProductIds(x), x => _productService.Value.GetProductTagsByProductIds(x), @@ -279,14 +308,14 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Order: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetOrders(ctx, skip), entities => { ctx.OrderExportContext = new OrderExportContext(entities, - x => _customerService.Value.GetCustomersByIds(x), - x => _customerService.Value.GetRewardPointsHistoriesByCustomerIds(x), + x => _customerService.GetCustomersByIds(x), + x => _customerService.GetRewardPointsHistoriesByCustomerIds(x), x => _addressService.Value.GetAddressByIds(x), x => _orderService.Value.GetOrderItemsByOrderIds(x), x => _shipmentService.Value.GetShipmentsByOrderIds(x) @@ -298,7 +327,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Manufacturer: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetManufacturers(ctx, skip), entities => @@ -314,7 +343,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Category: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetCategories(ctx, skip), entities => @@ -330,7 +359,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.Customer: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetCustomers(ctx, skip), entities => @@ -345,7 +374,7 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; case ExportEntityType.NewsLetterSubscription: - ctx.ExecuteContext.Segmenter = new ExportDataSegmenter + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter ( skip => GetNewsLetterSubscriptions(ctx, skip), null, @@ -355,17 +384,17 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in break; default: - ctx.ExecuteContext.Segmenter = null; + ctx.ExecuteContext.DataSegmenter = null; break; } - return ctx.ExecuteContext.Segmenter as IExportDataSegmenterProvider; + 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())); + throw new SmartException("Unknown export method {0}.".FormatInvariant(method.NaIfEmpty())); try { @@ -385,15 +414,16 @@ private bool CallProvider(DataExporterContext ctx, string streamId, string metho 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 " + path); - + ctx.Log.Information("Creating file {0}.".FormatInvariant(path)); ctx.ExecuteContext.DataStream.CopyTo(fileStream); } } @@ -401,8 +431,8 @@ private bool CallProvider(DataExporterContext ctx, string streamId, string metho } catch (Exception exception) { - ctx.ExecuteContext.Abort = ExportAbortion.Hard; - ctx.Log.Error("The provider failed at the {0} method: {1}".FormatInvariant(method, exception.ToAllMessages()), 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 @@ -412,61 +442,90 @@ private bool CallProvider(DataExporterContext ctx, string streamId, string metho 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 != ExportAbortion.Hard); + return (ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard); } - private void Deploy(DataExporterContext ctx, string zipPath) + private bool Deploy(DataExporterContext ctx, string zipPath) { - var allFiles = System.IO.Directory.GetFiles(ctx.FolderContent, "*.*", SearchOption.AllDirectories); + 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 - }; + ZipPath = zipPath, + CreateZipArchive = ctx.Request.Profile.CreateZipArchive + }; - foreach (var deployment in ctx.Request.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled)) + foreach (var deployment in deployments) { - if (deployment.CreateZip) - context.DeploymentFiles = new string[] { zipPath }; - else - context.DeploymentFiles = allFiles; + IFilePublisher publisher = null; - try + context.Result = new DataDeploymentResult { - IFilePublisher publisher = null; + LastExecutionUtc = DateTime.UtcNow + }; - if (deployment.DeploymentType == ExportDeploymentType.Email) - { - publisher = new EmailFilePublisher(_emailAccountService.Value, _queuedEmailService.Value); - } - else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) - { - publisher = new FileSystemFilePublisher(); - } - else if (deployment.DeploymentType == ExportDeploymentType.Ftp) - { - publisher = new FtpFilePublisher(); - } - else if (deployment.DeploymentType == ExportDeploymentType.Http) + try + { + switch (deployment.DeploymentType) { - publisher = new HttpFilePublisher(); + 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) @@ -476,7 +535,7 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) if (emailAccount == null) emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); - var downloadUrl = "{0}Admin/Export/DownloadExportFile/{1}?name=".FormatInvariant(_services.WebHelper.GetStoreLocation(false), ctx.Request.Profile.Id); + 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); @@ -488,21 +547,21 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) if (ctx.Result.LastError.HasValue()) { - body.AppendFormat("

{0}

", ctx.Result.LastError); + body.AppendFormat("

{0}

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

{1}

", downloadUrl + HttpUtility.UrlDecode(fileName), fileName); + body.AppendFormat("

{2}

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

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

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

"); } @@ -512,9 +571,6 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) 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.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)); @@ -526,7 +582,8 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) message.Body = body.ToString(); -#if DEBUG + _emailSender.Value.SendEmail(smtpContext, message); + //_queuedEmailService.Value.InsertQueuedEmail(new QueuedEmail //{ // From = emailAccount.Email, @@ -539,9 +596,6 @@ private void SendCompletionEmail(DataExporterContext ctx, string zipPath) // SendManually = true //}); //_dbContext.SaveChanges(); -#else - _emailSender.Value.SendEmail(smtpContext, message); -#endif } #endregion @@ -629,10 +683,10 @@ private List GetProducts(DataExporterContext ctx, int skip) { var associatedSearchContext = new ProductSearchContext { - OrderBy = ProductSortingEnum.CreatedOn, + OrderBy = ProductSortingEnum.Position, PageSize = int.MaxValue, StoreId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId), - VisibleIndividuallyOnly = false, + VisibleIndividuallyOnly = true, ParentGroupedProductId = product.Id }; @@ -675,7 +729,7 @@ private IQueryable GetOrderQuery(DataExporterContext ctx, int skip, int t null, null); - if (ctx.Request.EntitiesToExport.Count > 0) + if (ctx.Request.EntitiesToExport.Any()) query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); query = query.OrderByDescending(x => x.CreatedOnUtc); @@ -710,7 +764,7 @@ private IQueryable GetManufacturerQuery(DataExporterContext ctx, i var query = _manufacturerService.Value.GetManufacturers(showHidden, storeId); - if (ctx.Request.EntitiesToExport.Count > 0) + if (ctx.Request.EntitiesToExport.Any()) query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); query = query.OrderBy(x => x.DisplayOrder); @@ -740,7 +794,7 @@ private IQueryable GetCategoryQuery(DataExporterContext ctx, int skip, var query = _categoryService.Value.GetCategories(null, showHidden, null, true, storeId); - if (ctx.Request.EntitiesToExport.Count > 0) + if (ctx.Request.EntitiesToExport.Any()) query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); query = query @@ -775,7 +829,62 @@ private IQueryable GetCustomerQuery(DataExporterContext ctx, int skip, .Expand(x => x.CustomerRoles) .Where(x => !x.Deleted); - if (ctx.Request.EntitiesToExport.Count > 0) + 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); @@ -807,7 +916,22 @@ private IQueryable GetNewsLetterSubscriptionQuery(DataEx if (storeId > 0) query = query.Where(x => x.StoreId == storeId); - if (ctx.Request.EntitiesToExport.Count > 0) + 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 @@ -845,7 +969,7 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) ctx.ContextCurrency = _services.WorkContext.WorkingCurrency; if (ctx.Projection.CustomerId.HasValue) - ctx.ContextCustomer = _customerService.Value.GetCustomerById(ctx.Projection.CustomerId.Value); + ctx.ContextCustomer = _customerService.GetCustomerById(ctx.Projection.CustomerId.Value); else ctx.ContextCustomer = _services.WorkContext.CurrentCustomer; @@ -914,10 +1038,11 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) private void ExportCoreInner(DataExporterContext ctx, Store store) { - if (ctx.ExecuteContext.Abort != ExportAbortion.None) + if (ctx.ExecuteContext.Abort != DataExchangeAbortion.None) return; - int fileIndex = 0; + var fileIndex = 0; + var dataExchangeSettings = _services.Settings.LoadSetting(store.Id); ctx.Store = store; @@ -929,27 +1054,36 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) 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)); - logHead.AppendLine("Export provider:\t{0} ({1})".FormatInvariant(ctx.Request.Provider.Metadata.FriendlyName, ctx.Request.Profile.ProviderSystemName)); + 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\t"); + 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\t" + ctx.Request.Provider.Value.EntityType.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 storeInfo = (ctx.Request.Profile.PerStore ? "{0} (Id {1})".FormatInvariant(ctx.Store.Name, ctx.Store.Id) : "All stores"); - logHead.Append("Store:\t\t\t\t" + storeInfo); + 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; - ctx.ExecuteContext.MaxFileNameLength = _dataExchangeSettings.Value.MaxFileNameLength; - - ctx.ExecuteContext.HasPublicDeployment = ctx.Request.Profile.Deployments.Any(x => x.IsPublic && x.DeploymentType == ExportDeploymentType.FileSystem); - - ctx.ExecuteContext.PublicFolderPath = (ctx.ExecuteContext.HasPublicDeployment ? Path.Combine(HttpRuntime.AppDomainAppPath, PublicFolder) : null); + 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(".") : ""); @@ -958,15 +1092,15 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) { if (segmenter == null) { - throw new SmartException("Unsupported entity type '{0}'".FormatInvariant(ctx.Request.Provider.Value.EntityType.ToString())); + 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"); + ctx.Log.Information("There are no records to export."); } - while (ctx.ExecuteContext.Abort == ExportAbortion.None && segmenter.HasData) + while (ctx.ExecuteContext.Abort == DataExchangeAbortion.None && segmenter.HasData) { segmenter.RecordPerSegmentCount = 0; ctx.ExecuteContext.RecordsSucceeded = 0; @@ -979,17 +1113,13 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) ctx.ExecuteContext.FileName = resolvedPattern + fileExtension; path = Path.Combine(ctx.ExecuteContext.Folder, ctx.ExecuteContext.FileName); - - if (ctx.ExecuteContext.HasPublicDeployment) - ctx.ExecuteContext.PublicFileUrl = ctx.Store.Url.EnsureEndsWith("/") + PublicFolder.EnsureEndsWith("/") + ctx.ExecuteContext.FileName; } if (CallProvider(ctx, null, "Execute", path)) { - ctx.Log.Information("Provider reports {0} successful exported record(s)".FormatInvariant(ctx.ExecuteContext.RecordsSucceeded)); + ctx.Log.Information("Provider reports {0} successfully exported record(s).".FormatInvariant(ctx.ExecuteContext.RecordsSucceeded)); - // create info for deployment list in profile edit - if (ctx.IsFileBasedExport) + if (ctx.IsFileBasedExport && File.Exists(path)) { ctx.Result.Files.Add(new DataExportResult.ExportFileInfo { @@ -1002,28 +1132,27 @@ private void ExportCoreInner(DataExporterContext ctx, Store store) ctx.EntityIdsPerSegment.Clear(); if (ctx.ExecuteContext.IsMaxFailures) - ctx.Log.Warning("Export aborted. The maximum number of failures has been reached"); + 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"); + ctx.Log.Warning("Export aborted. A cancellation has been requested."); DetachAllEntitiesAndClear(ctx); } - if (ctx.ExecuteContext.Abort != ExportAbortion.Hard) + if (ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) { // always call OnExecuted - if (ctx.ExecuteContext.ExtraDataStreams.Count == 0) - ctx.ExecuteContext.ExtraDataStreams.Add(new ExportExtraStreams()); + if (ctx.ExecuteContext.ExtraDataUnits.Count == 0) + ctx.ExecuteContext.ExtraDataUnits.Add(new ExportDataUnit()); - ctx.ExecuteContext.ExtraDataStreams.ForEach(x => + 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.ExtraDataStreams.Clear(); + ctx.ExecuteContext.ExtraDataUnits.Clear(); } } } @@ -1044,20 +1173,21 @@ private void ExportCoreOuter(DataExporterContext ctx) { 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"); - } + 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); } - ctx.Log = logger; - ctx.ExecuteContext.Log = logger; - ctx.ProgressInfo = T("Admin.DataExchange.Export.ProgressInfo"); - if (ctx.Request.Profile.ProviderConfigData.HasValue()) { var configInfo = ctx.Request.Provider.Value.ConfigurationInfo; @@ -1073,7 +1203,8 @@ private void ExportCoreOuter(DataExporterContext ctx) { 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); + 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) { @@ -1106,20 +1237,27 @@ private void ExportCoreOuter(DataExporterContext ctx) stores.ForEach(x => ExportCoreInner(ctx, x)); } - if (!ctx.IsPreview && ctx.ExecuteContext.Abort != ExportAbortion.Hard) + if (!ctx.IsPreview && ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) { if (ctx.IsFileBasedExport) { - if (ctx.Request.Profile.CreateZipArchive || ctx.Request.Profile.Deployments.Any(x => x.Enabled && x.CreateZip)) + if (ctx.Request.Profile.CreateZipArchive) { - ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, true); + ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, false); } if (ctx.Request.Profile.Deployments.Any(x => x.Enabled)) { - SetProgress(ctx, T("Common.Deployment")); + SetProgress(ctx, T("Common.Publishing")); + + var allDeploymentsSucceeded = Deploy(ctx, zipPath); + + if (allDeploymentsSucceeded && ctx.Request.Profile.Cleanup) + { + logger.Information("Cleaning up export folder"); - Deploy(ctx, zipPath); + FileSystemHelper.ClearDirectory(ctx.FolderContent, false); + } } } @@ -1144,7 +1282,7 @@ private void ExportCoreOuter(DataExporterContext ctx) { if (!ctx.IsPreview && ctx.Request.Profile.Id != 0) { - ctx.Request.Profile.ResultInfo = XmlHelper.Serialize(ctx.Result); + ctx.Request.Profile.ResultInfo = XmlHelper.Serialize(ctx.Result); _exportProfileService.Value.UpdateExportProfile(ctx.Request.Profile); } @@ -1154,31 +1292,20 @@ private void ExportCoreOuter(DataExporterContext ctx) logger.ErrorsAll(exception); } - try - { - if (ctx.IsFileBasedExport && ctx.ExecuteContext.Abort != ExportAbortion.Hard && ctx.Request.Profile.Cleanup) - { - FileSystemHelper.ClearDirectory(ctx.FolderContent, false); - } - } - 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.Stores.Clear(); ctx.QuantityUnits.Clear(); ctx.DeliveryTimes.Clear(); ctx.CategoryPathes.Clear(); ctx.Categories.Clear(); + ctx.Stores.Clear(); ctx.Request.CustomData.Clear(); @@ -1193,11 +1320,11 @@ private void ExportCoreOuter(DataExporterContext ctx) } } - if (ctx.IsPreview || ctx.ExecuteContext.Abort == ExportAbortion.Hard) + if (ctx.IsPreview || ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard) return; // post process order entities - if (ctx.EntityIdsLoaded.Count > 0 && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) + if (ctx.EntityIdsLoaded.Any() && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) { using (var logger = new TraceLogger(logPath)) { @@ -1252,38 +1379,6 @@ public DataExportResult Export(DataExportRequest request, CancellationToken canc ExportCoreOuter(ctx); - if (ctx.Result != null && ctx.Result.Succeeded && ctx.Result.Files.Count > 0) - { - string prefix = null; - string suffix = null; - var extension = Path.GetExtension(ctx.Result.Files.First().FileName); - var provider = request.Provider.Value; - - if (provider.EntityType == ExportEntityType.Product) - prefix = T("Admin.Catalog.Products"); - else if (provider.EntityType == ExportEntityType.Order) - prefix = T("Admin.Orders"); - else if (provider.EntityType == ExportEntityType.Category) - prefix = T("Admin.Catalog.Categories"); - else if (provider.EntityType == ExportEntityType.Manufacturer) - prefix = T("Admin.Catalog.Manufacturers"); - else if (provider.EntityType == ExportEntityType.Customer) - prefix = T("Admin.Customers"); - else if (provider.EntityType == ExportEntityType.NewsLetterSubscription) - prefix = T("Admin.Promotions.NewsLetterSubscriptions"); - else - prefix = provider.EntityType.ToString(); - - var selectedEntityCount = (request.EntitiesToExport == null ? 0 : request.EntitiesToExport.Count); - - if (selectedEntityCount == 0) - suffix = T("Common.All"); - else - suffix = (selectedEntityCount == 1 ? request.EntitiesToExport.First().ToString() : T("Admin.Common.Selected").Text); - - ctx.Result.DownloadFileName = string.Concat(prefix, "-", suffix).ToLower().ToValidFileName() + extension; - } - cancellationToken.ThrowIfCancellationRequested(); return ctx.Result; @@ -1298,11 +1393,14 @@ public IList Preview(DataExportRequest request, int pageIndex, int? tot 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("Unsupported entity type '{0}'".FormatInvariant(ctx.Request.Provider.Value.EntityType.ToString())); + throw new SmartException(T("Admin.Common.UnsupportedEntityType", ctx.Request.Provider.Value.EntityType.ToString())); } while (segmenter.HasData) 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/Deployment/EmailFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs similarity index 74% rename from src/Libraries/SmartStore.Services/DataExchange/Deployment/EmailFilePublisher.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs index 1eb7f02081..298d476c41 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/EmailFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs @@ -4,7 +4,6 @@ using SmartStore.Core.Domain; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Email; -using SmartStore.Core.Infrastructure; using SmartStore.Core.IO; using SmartStore.Core.Logging; using SmartStore.Services.Messages; @@ -26,10 +25,7 @@ public EmailFilePublisher( public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) { - var emailAccountService = EngineContext.Current.Resolve(); - var queuedEmailService = EngineContext.Current.Resolve(); - - var emailAccount = emailAccountService.GetEmailAccountById(deployment.EmailAccountId); + var emailAccount = _emailAccountService.GetEmailAccountById(deployment.EmailAccountId); var smtpContext = new SmtpContext(emailAccount); var count = 0; @@ -39,26 +35,27 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de { From = emailAccount.Email, FromName = emailAccount.DisplayName, + SendManually = false, To = email, Subject = deployment.EmailSubject.NaIfEmpty(), CreatedOnUtc = DateTime.UtcNow, EmailAccountId = deployment.EmailAccountId }; - foreach (string path in context.DeploymentFiles) + foreach (var path in context.GetDeploymentFiles()) { - string name = Path.GetFileName(path); + var name = Path.GetFileName(path); queuedEmail.Attachments.Add(new QueuedEmailAttachment { - StorageLocation = EmailAttachmentStorageLocation.Path, - Path = path, + StorageLocation = EmailAttachmentStorageLocation.Blob, + Data = File.ReadAllBytes(path), Name = name, MimeType = MimeTypes.MapNameToMimeType(name) }); } - queuedEmailService.InsertQueuedEmail(queuedEmail); + _queuedEmailService.InsertQueuedEmail(queuedEmail); ++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/Deployment/FtpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs similarity index 82% rename from src/Libraries/SmartStore.Services/DataExchange/Deployment/FtpFilePublisher.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs index 2352d7250f..7a0954a1c7 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/FtpFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs @@ -16,7 +16,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de var url = deployment.Url; var buffLength = 32768; byte[] buff = new byte[buffLength]; - var deploymentFiles = context.DeploymentFiles.ToList(); + var deploymentFiles = context.GetDeploymentFiles().ToList(); var lastIndex = (deploymentFiles.Count - 1); if (!url.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase)) @@ -26,7 +26,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de { var fileUrl = url.EnsureEndsWith("/") + Path.GetFileName(path); - var request = (FtpWebRequest)System.Net.WebRequest.Create(fileUrl); + var request = (FtpWebRequest)WebRequest.Create(fileUrl); request.Method = WebRequestMethods.Ftp.UploadFile; request.KeepAlive = (deploymentFiles.IndexOf(path) != lastIndex); request.UseBinary = true; @@ -61,10 +61,9 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de } else { - var msg = "The FTP transfer might fail. {0} ({1}), {2}. File {3}".FormatInvariant( - response.StatusCode.ToString(), statusCode, response.StatusDescription.NaIfEmpty(), path); + context.Result.LastError = context.T("Admin.Common.FtpStatus", statusCode, response.StatusCode.ToString()); - context.Log.Error(msg); + context.Log.Error("The FTP transfer failed. FTP status {0} ({1}). File {3}".FormatInvariant(statusCode, response.StatusCode.ToString(), path)); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Deployment/HttpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs similarity index 73% rename from src/Libraries/SmartStore.Services/DataExchange/Deployment/HttpFilePublisher.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs index 48303bd200..4f0d9df8d8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Deployment/HttpFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs @@ -12,7 +12,7 @@ public class HttpFilePublisher : IFilePublisher { public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) { - var succeeded = 0; + var succeededFiles = 0; var url = deployment.Url; if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) @@ -20,7 +20,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de if (deployment.HttpTransmissionType == ExportHttpTransmissionType.MultipartFormDataPost) { - var count = 0; + var countFiles = 0; ICredentials credentials = null; if (deployment.Username.HasValue()) @@ -30,24 +30,26 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de using (var client = new HttpClient(handler)) using (var formData = new MultipartFormDataContent()) { - foreach (var path in context.DeploymentFiles) + foreach (var path in context.GetDeploymentFiles()) { byte[] fileData = File.ReadAllBytes(path); - formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++count), Path.GetFileName(path)); + formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++countFiles), Path.GetFileName(path)); } var response = client.PostAsync(url, formData).Result; if (response.IsSuccessStatusCode) { - succeeded = count; + 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. {0} ({1}). Response: {2}".FormatInvariant( - response.StatusCode.ToString(), (int)response.StatusCode, content.NaIfEmpty().Truncate(2000, "...")); + 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); } @@ -60,16 +62,16 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de if (deployment.Username.HasValue()) webClient.Credentials = new NetworkCredential(deployment.Username, deployment.Password); - foreach (var path in context.DeploymentFiles) + foreach (var path in context.GetDeploymentFiles()) { webClient.UploadFile(url, path); - ++succeeded; + ++succeededFiles; } } } - context.Log.Information("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeeded, deployment.HttpTransmissionType.ToString())); + 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/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs similarity index 80% rename from src/Libraries/SmartStore.Services/DataExchange/DynamicEntityHelper.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index a3bd172a02..1917084af2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -39,53 +39,50 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec string description = ""; // description merging - if (ctx.Projection.DescriptionMerging != ExportDescriptionMerging.None) + if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.None) { - if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty) - { - description = dynObject.FullDescription; + // 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) - { + if (description.IsEmpty()) 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, " "); - } + if (description.IsEmpty()) + description = dynObject.Name; } - else + 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()) @@ -107,7 +104,7 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec } // convert to plain text - if (ctx.Projection.DescriptionToPlainText) + if (description.HasValue() && ctx.Projection.DescriptionToPlainText) { //Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); //description = HttpUtility.HtmlDecode(reg.Replace(description, "")); @@ -139,15 +136,30 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec return price; } - private decimal CalculatePrice(DataExporterContext ctx, Product product, bool forAttributeCombination) + private decimal CalculatePrice( + DataExporterContext ctx, + Product product, + ProductVariantAttributeCombination combination, + IList attributeValues) { - decimal price = product.Price; + var price = product.Price; + var priceCalculationContext = ctx.ProductExportContext as PriceCalculationContext; - // price type - if (ctx.Projection.PriceType.HasValue && !forAttributeCombination) + if (combination != null) { - var priceCalculationContext = ctx.ProductExportContext as PriceCalculationContext; + // 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; @@ -166,7 +178,6 @@ private decimal CalculatePrice(DataExporterContext ctx, Product product, bool fo return ConvertPrice(ctx, product, price) ?? price; } - private List GetLocalized(DataExporterContext ctx, T entity, params Expression>[] keySelectors) where T : BaseEntity, ILocalizedEntity { @@ -310,6 +321,7 @@ private dynamic ToDynamic(DataExporterContext ctx, Customer customer) result._HasNewsletterSubscription = false; result._FullName = null; + result._AvatarPictureUrl = null; return result; } @@ -385,14 +397,14 @@ private dynamic ToDynamic(DataExporterContext ctx, Picture picture, int thumbPic 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); - var relativeUrl = _pictureService.Value.GetPictureUrl(picture); - result._FileName = relativeUrl.Substring(relativeUrl.LastIndexOf("/") + 1); - result._ThumbLocalPath = _pictureService.Value.GetThumbLocalPath(picture); return result; @@ -489,6 +501,11 @@ private dynamic ToDynamic(DataExporterContext ctx, Category category) 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, @@ -549,8 +566,8 @@ private dynamic ToDynamic( product.MergeWithCombination(combination); var languageId = (ctx.Projection.LanguageId ?? 0); - var productTemplate = ctx.ProductTemplates.FirstOrDefault(x => x.Key == product.ProductTemplateId); 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) @@ -564,34 +581,66 @@ private dynamic ToDynamic( 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.Price = CalculatePrice(ctx, product, combination != null); + 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 && ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) - { - var values = _productAttributeParser.Value.ParseProductVariantAttributeValues(combination.AttributesXml, productAttributes, ctx.Projection.LanguageId ?? 0); + if (combination == null) + dynObject._UniqueId = product.Id.ToString(); + else + dynObject._UniqueId = string.Concat(product.Id, "-", combination.Id); - dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", values), " "); - } + 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); - dynObject._ProductTemplateViewPath = (productTemplate.Value == null ? "" : productTemplate.Value.ViewPath); + if (ctx.ProductTemplates.ContainsKey(product.ProductTemplateId)) + dynObject._ProductTemplateViewPath = ctx.ProductTemplates[product.ProductTemplateId]; + else + dynObject._ProductTemplateViewPath = ""; - dynObject._DetailUrl = ctx.Store.Url.EnsureEndsWith("/") + (string)dynObject.SeName; + if (combination != null) + { + if (ctx.Supports(ExportFeatures.UsesAttributeCombination)) + { + dynObject._AttributeCombination = variantAttributes; + dynObject._AttributeCombinationValues = variantAttributeValues; + } - dynObject._CategoryName = null; + 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 && ctx.CategoryPathes.Count > 0) + if (ctx.Categories.Count > 0) { dynObject._CategoryPath = _categoryService.Value.GetCategoryPath( product, @@ -602,13 +651,8 @@ private dynamic ToDynamic( productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault() ); } - else - { - dynObject._CategoryPath = null; - } ToDeliveryTime(ctx, dynObject, product.DeliveryTimeId); - ToQuantityUnit(ctx, dynObject, product.QuantityUnitId); dynObject.ProductPictures = productPictures @@ -669,7 +713,7 @@ private dynamic ToDynamic( dynamic dyn = ToDynamic(ctx, x); var assignedPictures = new List(); - foreach (int pictureId in x.GetAssignedPictureIds()) + foreach (int pictureId in x.GetAssignedPictureIds().Take(numberOfPictures)) { var assignedPicture = productPictures.FirstOrDefault(y => y.PictureId == pictureId); if (assignedPicture != null && assignedPicture.Picture != null) @@ -769,9 +813,21 @@ private dynamic ToDynamic( if (ctx.Supports(ExportFeatures.CanIncludeMainPicture)) { if (productPictures != null && productPictures.Any()) - dynObject._MainPictureUrl = _pictureService.Value.GetPictureUrl(productPictures.First().Picture, ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); - else + { + 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()) @@ -817,20 +873,32 @@ private dynamic ToDynamic( if (ctx.Supports(ExportFeatures.UsesSpecialPrice)) { - dynObject._SpecialPrice = null; - dynObject._RegularPrice = null; // price if a special price would not exist + 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) + if (specialPrice.HasValue || dynObject._FutureSpecialPrice != null) { decimal tmpSpecialPrice = product.SpecialPrice.Value; product.SpecialPrice = null; - dynObject._RegularPrice = CalculatePrice(ctx, product, combination != null); + + dynObject._RegularPrice = CalculatePrice(ctx, product, combination, variantAttributeValues); + product.SpecialPrice = tmpSpecialPrice; } } @@ -871,6 +939,8 @@ private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) dynamic result = new DynamicEntity(orderItem); + orderItem.Product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser.Value); + result.Product = ToDynamic(ctx, orderItem.Product); return result; @@ -964,19 +1034,17 @@ private List Convert(DataExporterContext ctx, Product product) //var productValues = new Dictionary(); var dbContext = _dbContext as DbContext; - _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); - 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); } @@ -1005,10 +1073,13 @@ private List Convert(DataExporterContext ctx, Order order) { var result = new List(); - ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId ?? 0); + 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 addresses = ctx.OrderExportContext.Addresses.Load(ctx.IsPreview ? 0 : order.BillingAddressId); 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); @@ -1036,20 +1107,25 @@ private List Convert(DataExporterContext ctx, Order order) .PointsBalance; } - dynObject.BillingAddress = ToDynamic(ctx, addresses.FirstOrDefault(x => x.Id == order.BillingAddressId)); + if (ctx.OrderExportContext.Addresses.ContainsKey(order.BillingAddressId)) + { + dynObject.BillingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.BillingAddressId].FirstOrDefault()); + } - if (order.ShippingAddressId.HasValue) + if (order.ShippingAddressId.HasValue && ctx.OrderExportContext.Addresses.ContainsKey(order.ShippingAddressId.Value)) { - dynObject.ShippingAddress = ToDynamic(ctx, addresses.FirstOrDefault(x => x.Id == order.ShippingAddressId.Value)); + dynObject.ShippingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.ShippingAddressId.Value].FirstOrDefault()); } dynObject.OrderItems = orderItems .Select(e => { dynamic dyn = ToDynamic(ctx, e); - var productTemplate = ctx.ProductTemplates.FirstOrDefault(x => x.Key == e.Product.ProductTemplateId); - dyn.Product._ProductTemplateViewPath = (productTemplate.Value == null ? "" : productTemplate.Value.ViewPath); + 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); @@ -1088,9 +1164,10 @@ private List Convert(DataExporterContext ctx, Manufacturer manufacturer if (!ctx.IsPreview && manufacturer.PictureId.HasValue) { - var pictures = ctx.ManufacturerExportContext.Pictures.Load(manufacturer.PictureId.Value); + var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); + var pictures = ctx.ManufacturerExportContext.Pictures.Load(manufacturer.PictureId.Value).Take(numberOfPictures); - if (pictures.Count > 0) + if (pictures.Any()) dynObject.Picture = ToDynamic(ctx, pictures.First(), _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); } @@ -1120,16 +1197,17 @@ private List Convert(DataExporterContext ctx, Manufacturer manufacturer 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 pictures = ctx.CategoryExportContext.Pictures.Load(category.PictureId.Value); + var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); + var pictures = ctx.CategoryExportContext.Pictures.Load(category.PictureId.Value).Take(numberOfPictures); - if (pictures.Count > 0) + if (pictures.Any()) dynObject.Picture = ToDynamic(ctx, pictures.First(), _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); } @@ -1205,6 +1283,16 @@ private List Convert(DataExporterContext ctx, Customer customer) 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 diff --git a/src/Libraries/SmartStore.Services/DataExchange/Events/RowExportingEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs similarity index 50% rename from src/Libraries/SmartStore.Services/DataExchange/Events/RowExportingEvent.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs index 7459b9ee3f..0b96526e00 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Events/RowExportingEvent.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs @@ -4,13 +4,32 @@ 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 global the export definition. + // 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 IExportExecuteContext ExecuteContext { get; internal set; } + public dynamic Row + { + get; + internal set; + } + + public ExportEntityType EntityType + { + get; + internal set; + } + + public DataExportRequest ExportRequest + { + get; + internal set; + } + + public ExportExecuteContext ExecuteContext + { + get; + internal set; + } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportConfigurationInfo.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/ExportConfigurationInfo.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/ExportDataSegmenter.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs similarity index 67% rename from src/Libraries/SmartStore.Services/DataExchange/ExportExecuteContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs index 3bc8ed473e..d5f2860ff8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs @@ -7,132 +7,173 @@ namespace SmartStore.Services.DataExchange.Export { - public interface IExportExecuteContext + public class ExportExecuteContext { + private DataExportResult _result; + private CancellationToken _cancellation; + private DataExchangeAbortion _providerAbort; + + internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) + { + _result = result; + _cancellation = cancellation; + Folder = folder; + ExtraDataUnits = new List(); + CustomProperties = new Dictionary(); + } + /// /// Provides the data to be exported /// - IExportDataSegmenterConsumer Segmenter { get; } + public IExportDataSegmenterConsumer DataSegmenter { get; set; } /// /// The store context to be used for the export /// - dynamic Store { get; } + public dynamic Store { get; internal set; } /// /// The customer context to be used for the export /// - dynamic Customer { get; } + public dynamic Customer { get; internal set; } /// /// The currency context to be used for the export /// - dynamic Currency { get; } + public dynamic Currency { get; internal set; } /// /// The language context to be used for the export /// - dynamic Language { get; } + public dynamic Language { get; internal set; } /// /// Projection data /// - ExportProjection Projection { get; } + public ExportProjection Projection { get; internal set; } /// /// To log information into the export log file /// - ILogger Log { get; } + public ILogger Log { get; internal set; } /// /// Indicates whether and how to abort the export /// - ExportAbortion Abort { get; set; } + public DataExchangeAbortion Abort + { + get + { + if (_cancellation.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + + return _providerAbort; + } + set + { + _providerAbort = value; + } + } + public bool IsMaxFailures + { + get { return RecordsFailed > 11; } + } /// /// Identifier of current data stream. Can be null. /// - string DataStreamId { get; set; } + public string DataStreamId { get; set; } /// /// Stream used to write data to /// - Stream DataStream { get; } + public Stream DataStream { get; internal set; } /// - /// List with extra data streams required by provider + /// List with extra data units/streams required by provider /// - List ExtraDataStreams { get; set; } - + public List ExtraDataUnits { get; private set; } /// /// The maximum allowed file name length /// - int MaxFileNameLength { get; } + public int MaxFileNameLength { get; internal set; } /// /// The name of the current export file /// - string FileName { get; } + public string FileName { get; internal set; } /// /// The path of the export content folder /// - string Folder { get; } - + public string Folder { get; private set; } /// /// Whether the profile has a public deployment into "Exchange" folder /// - bool HasPublicDeployment { get; } + public bool HasPublicDeployment { get; internal set; } /// /// The local path to the public export folder "Exchange". null if the profile has no public deployment. /// - string PublicFolderPath { get; } - - /// - /// The public URL of the export file (accessible through the internet). null if the profile has no public deployment. - /// - string PublicFileUrl { get; } - + public string PublicFolderPath { get; internal set; } /// /// Provider specific configuration data /// - object ConfigurationData { get; } + public object ConfigurationData { get; internal set; } /// /// Use this dictionary for any custom data required along the export /// - Dictionary CustomProperties { get; set; } + public Dictionary CustomProperties { get; set; } /// /// Number of successful processed records /// - int RecordsSucceeded { get; set; } + public int RecordsSucceeded { get; set; } /// /// Number of failed records /// - int RecordsFailed { get; set; } + public int RecordsFailed { get; set; } /// /// Processes an exception that occurred while exporting a record /// - /// Exception - void RecordException(Exception exc, int entityId); + /// 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 - void SetProgressMessage(string message); + public void SetProgress(string message) + { + if (ProgressValueSetter != null && message.HasValue()) + { + try + { + ProgressValueSetter.Invoke(0, 0, message); + } + catch { } + } + } } - - public class ExportExtraStreams + public class ExportDataUnit { /// /// Your Id to identify this stream within a list of streams @@ -149,89 +190,4 @@ public class ExportExtraStreams /// public string FileName { get; set; } } - - - public class ExportExecuteContext : IExportExecuteContext - { - private DataExportResult _result; - private CancellationToken _cancellation; - private ExportAbortion _providerAbort; - - internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) - { - _result = result; - _cancellation = cancellation; - Folder = folder; - ExtraDataStreams = new List(); - CustomProperties = new Dictionary(); - } - - public IExportDataSegmenterConsumer Segmenter { get; set; } - - public dynamic Store { get; internal set; } - public dynamic Customer { get; internal set; } - public dynamic Currency { get; internal set; } - public dynamic Language { get; internal set; } - public ExportProjection Projection { get; internal set; } - - public ILogger Log { get; internal set; } - public ProgressMessageSetter ProgressMessageSetter { get; internal set; } - - public ExportAbortion Abort - { - get - { - if (_cancellation.IsCancellationRequested || IsMaxFailures) - return ExportAbortion.Hard; - - return _providerAbort; - } - set - { - _providerAbort = value; - } - } - - public bool IsMaxFailures - { - get { return RecordsFailed > 11; } - } - - public string DataStreamId { get; set; } - public Stream DataStream { get; internal set; } - public List ExtraDataStreams { get; set; } - - public int MaxFileNameLength { get; internal set; } - public string FileName { get; internal set; } - public string Folder { get; private set; } - - public bool HasPublicDeployment { get; internal set; } - public string PublicFolderPath { get; internal set; } - public string PublicFileUrl { get; internal set; } - - public object ConfigurationData { get; internal set; } - - public Dictionary CustomProperties { get; set; } - - public int RecordsSucceeded { get; set; } - public int RecordsFailed { get; set; } - - public void RecordException(Exception exc, int entityId) - { - ++RecordsFailed; - - Log.Error("Error while processing record with id {0}: {1}".FormatInvariant(entityId, exc.ToAllMessages()), exc); - - if (IsMaxFailures) - _result.LastError = exc.ToString(); - } - - public void SetProgressMessage(string message) - { - if (ProgressMessageSetter != null && message.HasValue()) - { - ProgressMessageSetter.Invoke(message); - } - } - } } 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/ExportFeaturesAttribute.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/ExportFeaturesAttribute.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs similarity index 87% rename from src/Libraries/SmartStore.Services/DataExchange/ExportProfileService.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index 3f2314afb1..1814ae56ae 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -45,8 +45,6 @@ public ExportProfileService( _localizationService = localizationService; } - #region Export profiles - public virtual ExportProfile InsertExportProfile( string providerSystemName, string name, @@ -58,9 +56,16 @@ public virtual ExportProfile InsertExportProfile( { 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; @@ -110,7 +115,8 @@ public virtual ExportProfile InsertExportProfile( RemoveCriticalCharacters = true, CriticalCharacters = "¼,½,¾", PriceType = PriceDisplayType.PreSelectedPrice, - NoGroupedProducts = (features.HasFlag(ExportFeatures.CanOmitGroupedProducts) ? true : false) + NoGroupedProducts = (features.HasFlag(ExportFeatures.CanOmitGroupedProducts) ? true : false), + DescriptionMerging = ExportDescriptionMerging.Description }; var filter = new ExportFilter @@ -138,10 +144,12 @@ public virtual ExportProfile InsertExportProfile( .Replace("/", "") .Replace("-", ""); - profile.FolderName = SeoHelper.GetSeName(cleanedSystemName, true, false) + 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 @@ -159,13 +167,15 @@ public virtual ExportProfile InsertExportProfile( { if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) { + var subFolder = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/" + DataExporter.PublicFolder), folderName); + profile.Deployments.Add(new ExportDeployment { ProfileId = profile.Id, Enabled = true, - IsPublic = true, - DeploymentType = ExportDeploymentType.FileSystem, - Name = profile.Name + DeploymentType = ExportDeploymentType.PublicFolder, + Name = profile.Name, + SubFolder = subFolder }); UpdateExportProfile(profile); @@ -212,6 +222,8 @@ 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); @@ -254,7 +266,6 @@ public virtual IQueryable GetExportProfiles(bool? enabled = null) query = query .OrderBy(x => x.IsSystemProfile) - .ThenBy(x => x.ProviderSystemName) .ThenBy(x => x.Name); return query; @@ -306,8 +317,8 @@ public virtual IEnumerable> LoadAllExportProviders(int { var allProviders = _providerManager.GetAllProviders(storeId) .Where(x => x.IsValid() && (showHidden || !x.Metadata.IsHidden)) - .OrderBy(x => x.Metadata.SystemName) - .ThenBy(x => x.Metadata.FriendlyName); + //.OrderBy(x => x.Metadata.SystemName) + .OrderBy(x => x.Metadata.FriendlyName); return allProviders; } @@ -331,6 +342,16 @@ public virtual ExportDeployment GetExportDeploymentById(int id) return deployment; } + public virtual void UpdateExportDeployment(ExportDeployment deployment) + { + if (deployment == null) + throw new ArgumentNullException("deployment"); + + _exportDeploymentRepository.Update(deployment); + + _eventPublisher.EntityUpdated(deployment); + } + public virtual void DeleteExportDeployment(ExportDeployment deployment) { if (deployment == null) @@ -340,7 +361,5 @@ public virtual void DeleteExportDeployment(ExportDeployment deployment) _eventPublisher.EntityDeleted(deployment); } - - #endregion } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProviderBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs similarity index 85% rename from src/Libraries/SmartStore.Services/DataExchange/ExportProviderBase.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs index 2a2d9618f1..c77638f60b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProviderBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs @@ -28,22 +28,22 @@ 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(IExportExecuteContext context); - - public void Execute(IExportExecuteContext context) - { - Export(context); - } + protected abstract void Export(ExportExecuteContext context); /// /// Called once per store when export execution ended /// /// Export execution context - public virtual void OnExecuted(IExportExecuteContext context) + public virtual void OnExecuted(ExportExecuteContext context) { } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs similarity index 93% rename from src/Libraries/SmartStore.Services/DataExchange/ExportXmlHelper.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index 425ac57265..68c8b78d55 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -49,6 +49,8 @@ public static XmlWriterSettings DefaultSettings } } + public ExportXmlExclude Exclude { get; set; } + public XmlWriter Writer { get { return _writer; } @@ -330,36 +332,41 @@ public void WriteCategory(dynamic category, string node) } _writer.Write("Id", entity.Id.ToString()); - _writer.Write("Name", (string)category.Name); - _writer.Write("FullName", (string)category.FullName); - _writer.Write("Description", (string)category.Description); - _writer.Write("BottomDescription", (string)category.BottomDescription); - _writer.Write("CategoryTemplateId", entity.CategoryTemplateId.ToString()); - _writer.Write("MetaKeywords", (string)category.MetaKeywords); - _writer.Write("MetaDescription", (string)category.MetaDescription); - _writer.Write("MetaTitle", (string)category.MetaTitle); - _writer.Write("SeName", (string)category.SeName); - _writer.Write("ParentCategoryId", 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"); + 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); - WriteLocalized(category); + WritePicture(category.Picture, "Picture"); + + WriteLocalized(category); + } if (node.HasValue()) { @@ -838,7 +845,9 @@ public void WriteCustomer(dynamic customer, string node) _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()); @@ -895,4 +904,15 @@ public void WriteCustomer(dynamic customer, string node) } } } + + + /// + /// Allows to exclude XML nodes from export + /// + [Flags] + public enum ExportXmlExclude + { + None = 0, + Category = 1 + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/IDataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs similarity index 77% rename from src/Libraries/SmartStore.Services/DataExchange/IDataExporter.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs index bfda711805..34b96faf24 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/IDataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs @@ -4,13 +4,11 @@ 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 delegate void ProgressValueSetter(int value, int maximum, string message); - public delegate void ProgressMessageSetter(string message); - public interface IDataExporter { DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken); @@ -24,7 +22,6 @@ public interface IDataExporter public class DataExportRequest { private readonly static ProgressValueSetter _voidProgressValueSetter = DataExportRequest.SetProgress; - private readonly static ProgressMessageSetter _voidProgressMessageSetter = DataExportRequest.SetProgress; public DataExportRequest(ExportProfile profile, Provider provider) { @@ -33,9 +30,7 @@ public DataExportRequest(ExportProfile profile, Provider provid Profile = profile; Provider = provider; - ProgressValueSetter = _voidProgressValueSetter; - ProgressMessageSetter = _voidProgressMessageSetter; EntitiesToExport = new List(); CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -45,10 +40,11 @@ public DataExportRequest(ExportProfile profile, Provider provid public Provider Provider { get; private set; } - public IList EntitiesToExport { get; set; } - public ProgressValueSetter ProgressValueSetter { get; set; } - public ProgressMessageSetter ProgressMessageSetter { get; set; } + + public bool HasPermission { get; set; } + + public IList EntitiesToExport { get; set; } public IDictionary CustomData { get; private set; } @@ -59,10 +55,5 @@ private static void SetProgress(int val, int max, string msg) { // do nothing } - - private static void SetProgress(string msg) - { - // do nothing - } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/IExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs similarity index 95% rename from src/Libraries/SmartStore.Services/DataExchange/IExportProfileService.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs index 7a61a36def..b0b0c8a6bd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/IExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs @@ -108,7 +108,13 @@ ExportProfile InsertExportProfile( ExportDeployment GetExportDeploymentById(int id); /// - /// Deleted a export deployment + /// Update an export deployment + /// + /// Export deployment + void UpdateExportDeployment(ExportDeployment deployment); + + /// + /// Deleted an export deployment /// /// Export deployment void DeleteExportDeployment(ExportDeployment deployment); diff --git a/src/Libraries/SmartStore.Services/DataExchange/IExportProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs similarity index 90% rename from src/Libraries/SmartStore.Services/DataExchange/IExportProvider.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs index e5248b20ba..60be2e11a9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/IExportProvider.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs @@ -24,12 +24,12 @@ public partial interface IExportProvider : IProvider, IUserEditable /// Export data to a file ///
/// Export execution context - void Execute(IExportExecuteContext context); + void Execute(ExportExecuteContext context); /// /// Called once per store when export execution ended /// /// Export execution context - void OnExecuted(IExportExecuteContext context); + void OnExecuted(ExportExecuteContext context); } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/CategoryExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/CategoryExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/CustomerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/CustomerExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs similarity index 88% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/DataExporterContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index 764c84e228..5c6c1315dd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -29,8 +29,13 @@ public DataExporterContext( Categories = new Dictionary(); CategoryPathes = new Dictionary(); + DeliveryTimes = new Dictionary(); + QuantityUnits = new Dictionary(); + Stores = new Dictionary(); + Languages = new Dictionary(); Countries = new Dictionary(); - ProductTemplates = new Dictionary(); + ProductTemplates = new Dictionary(); + CategoryTemplates = new Dictionary(); NewsletterSubscriptions = new HashSet(); RecordsPerStore = new Dictionary(); @@ -47,7 +52,7 @@ public DataExporterContext( if (!IsPreview) { - ExecuteContext.ProgressMessageSetter = Request.ProgressMessageSetter; + ExecuteContext.ProgressValueSetter = Request.ProgressValueSetter; } } @@ -105,7 +110,8 @@ public bool IsFileBasedExport public Dictionary Stores { get; set; } public Dictionary Languages { get; set; } public Dictionary Countries { get; set; } - public Dictionary ProductTemplates { get; set; } + public Dictionary ProductTemplates { get; set; } + public Dictionary CategoryTemplates { get; set; } public HashSet NewsletterSubscriptions { get; set; } // data loaded once per page diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/DynamicEntity.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/DynamicEntity.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/ManufacturerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/ManufacturerExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/OrderExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/OrderExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/Internal/ProductExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs similarity index 100% rename from src/Libraries/SmartStore.Services/DataExchange/Internal/ProductExportContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportExtensions.cs deleted file mode 100644 index 3d65d673c8..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportExtensions.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using SmartStore.Core.Domain; -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; - } - - /// - /// 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("~/App_Data/ExportProfiles/", 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) - { - return Path.Combine(profile.GetExportFolder(), profile.FolderName + ".zip"); - } - - /// - /// Gets existing export files for an export profile - /// - /// Export profile - /// List of file names - public static List GetExportFiles(this ExportProfile profile) - { - var exportFolder = profile.GetExportFolder(true); - - if (System.IO.Directory.Exists(exportFolder)) - { - return System.IO.Directory.EnumerateFiles(exportFolder, "*", SearchOption.TopDirectoryOnly) - .OrderBy(x => x) - .ToList(); - } - - return new List(); - } - - /// - /// 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; - } - } -} 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/ColumnMapConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs similarity index 70% rename from src/Libraries/SmartStore.Services/DataExchange/ColumnMapConverter.cs rename to src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs index 0fa97b26f0..c46b90b82a 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ColumnMapConverter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; using SmartStore.ComponentModel; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Import { public class ColumnMapConverter : TypeConverterBase { @@ -27,12 +27,12 @@ public override object ConvertFrom(CultureInfo culture, object value) { if (value is string) { - var dict = JsonConvert.DeserializeObject>((string)value); + var dict = JsonConvert.DeserializeObject>((string)value); var map = new ColumnMap(); foreach (var kvp in dict) { - map.AddMapping(kvp.Key, kvp.Value); + map.AddMapping(kvp.Key, null, kvp.Value.MappedName, kvp.Value.Default); } return map; @@ -41,6 +41,14 @@ public override object ConvertFrom(CultureInfo culture, object 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)) @@ -57,5 +65,10 @@ public override object ConvertTo(CultureInfo culture, string format, object valu 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/LightweightDataTable.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs index cf8a64a823..52be0a8dfa 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs @@ -4,11 +4,8 @@ using System.Data; using System.Data.Common; using System.Dynamic; -using System.Globalization; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Web; using SmartStore.Services.DataExchange.Csv; using SmartStore.Services.DataExchange.Excel; @@ -20,6 +17,7 @@ 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) { @@ -33,32 +31,71 @@ public LightweightDataTable(IList columns, IList data) _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++) { - _columnIndexes[columns[i].Name] = 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) { - return _columnIndexes.ContainsKey(name); + if (name.HasValue()) + { + return (_columnIndexes.ContainsKey(name) || _alternativeColumnIndexes.ContainsKey(name)); + } + + return false; } public int GetColumnIndex(string name) { - Guard.ArgumentNotEmpty(name, "name"); - int index; - if (!_columnIndexes.TryGetValue(name, out index)) + + if (name.HasValue()) { - return -1; + if (_columnIndexes.TryGetValue(name, out index)) + return index; + + if (_alternativeColumnIndexes.TryGetValue(name, out index)) + return index; } - return index; + return -1; } public IList Columns @@ -77,12 +114,25 @@ public IList 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) { - return FromPostedFile(file, new CsvConfiguration(), skip, take); + Guard.ArgumentNotNull(() => file); + + return FromFile(file.FileName, file.InputStream, file.ContentLength, new CsvConfiguration(), skip, take); } public static IDataTable FromPostedFile( @@ -92,41 +142,55 @@ public static IDataTable FromPostedFile( 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 (file.ContentLength == 0) + if (contentLength == 0) { - throw Error.Argument("file", "The posted file '{0}' does not contain any data.".FormatInvariant(file.FileName)); // TODO Loc + 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(file.FileName).ToLowerInvariant(); + var fileExt = System.IO.Path.GetExtension(fileName).ToLowerInvariant(); switch (fileExt) { case ".xlsx": - dataReader = new ExcelDataReader(file.InputStream, true); // TODO: let the user specify if excel file has headers + dataReader = new ExcelDataReader(stream, true); // TODO: let the user specify if excel file has headers break; default: - dataReader = new CsvDataReader(new StreamReader(file.InputStream), configuration); + dataReader = new CsvDataReader(new StreamReader(stream), configuration); break; } - var table = LightweightDataTable.FromDataReader(dataReader); + var table = LightweightDataTable.FromDataReader(dataReader, skip, take); if (table.Columns.Count == 0 || table.Rows.Count == 0) { - throw Error.InvalidOperation("file", "The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(file.FileName)); // TODO Loc + throw Error.InvalidOperation("The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(fileName)); } return table; } - catch + catch (Exception ex) { - throw; + throw ex; } finally { @@ -167,6 +231,8 @@ public static IDataTable FromDataReader( var fieldCount = reader.FieldCount; + take = Math.Min(take, int.MaxValue - skip); + int i = -1; while (reader.Read()) { 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/IImportManager.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IImportManager.cs deleted file mode 100644 index 5c26d54af8..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/IImportManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading; - -namespace SmartStore.Services.DataExchange.Import -{ - /// - /// Import manager interface - /// - public interface IImportManager - { - ImportResult ImportProducts( - IDataTable table, - 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 ImportProducts(this IImportManager manager, IDataTable table) - { - return manager.ImportProducts(table, CancellationToken.None); - } - } -} 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 index 4bcb355089..41129e24f8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs @@ -2,18 +2,17 @@ using System.Collections.Generic; using System.Data; using System.Globalization; -using System.IO; using System.Linq; using SmartStore.Core; namespace SmartStore.Services.DataExchange.Import { - public class ImportDataSegmenter where T : BaseEntity + public class ImportDataSegmenter { private const int BATCHSIZE = 100; private readonly IDataTable _table; - private ImportRow[] _currentBatch; + private object[] _currentBatch; private readonly IPageable _pageable; private bool _bof; private CultureInfo _culture; @@ -21,16 +20,17 @@ public class ImportDataSegmenter where T : BaseEntity private readonly IDictionary _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); - public ImportDataSegmenter(IDataTable table) + 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; - _columnMap = new ColumnMap(); } public CultureInfo Culture @@ -87,14 +87,67 @@ public int BatchSize get { return BATCHSIZE; } } - public bool HasDataColumn(string name) + /// + /// 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.GetMappedName(name)); + return _table.HasColumn(_columnMap.GetMapping(name, index).MappedName); } - public bool HasDataColumn(string name, string index) + /// + /// 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) { - return _table.HasColumn(_columnMap.GetMappedName(name, index)); + var mapping = _columnMap.GetMapping(columnName, index); + + return mapping.IgnoreProperty; } /// @@ -166,28 +219,25 @@ public bool ReadNextBatch() return false; } - public ImportRow[] CurrentBatch + public IEnumerable> GetCurrentBatch() where T : BaseEntity { - get + if (_currentBatch == null) { - 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++) { - 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++; - } + _currentBatch[i] = new ImportRow(this, _table.Rows[r], r); + i++; } - - return _currentBatch; } + + 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.Services/DataExchange/Import/ImportManager.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportManager.cs deleted file mode 100644 index 6e3632eea9..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportManager.cs +++ /dev/null @@ -1,770 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Data; -using SmartStore.Services.Catalog; -using SmartStore.Core.Events; -using SmartStore.Services.Localization; -using SmartStore.Services.Media; -using SmartStore.Services.Seo; -using SmartStore.Utilities; -using System.Text; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Domain.Media; -using SmartStore.Services.Stores; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core; -using System.Globalization; - -namespace SmartStore.Services.DataExchange.Import -{ - /// - /// 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 ImportProducts( - IDataTable table, - CancellationToken cancellationToken, - IProgress progress = null) - { - Guard.ArgumentNotNull(() => table); - - var result = new ImportResult(); - int saved = 0; - - if (progress != null) - progress.Report(new ImportProgressInfo { ElapsedTime = TimeSpan.Zero }); - - using (var scope = new DbContextScope(ctx: _rsProduct.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) - { - try { - var segmenter = new ImportDataSegmenter(table); - - segmenter.Culture = CultureInfo.CurrentUICulture; - - result.TotalRecords = segmenter.TotalRows; - - while (segmenter.ReadNextBatch() && !cancellationToken.IsCancellationRequested) - { - var batch = segmenter.CurrentBatch; - - // Perf: detach all entities - _rsProduct.Context.DetachAll(false); - - // Update progress for calling thread - if (progress != null) - { - progress.Report(new ImportProgressInfo - { - TotalRecords = result.TotalRecords, - TotalProcessed = segmenter.CurrentSegmentFirstRowIndex - 1, - NewRecords = result.NewRecords, - ModifiedRecords = result.ModifiedRecords, - ElapsedTime = DateTime.UtcNow - result.StartDateUtc, - TotalWarnings = result.Messages.Count(x => x.MessageType == ImportMessageType.Warning), - TotalErrors = result.Messages.Count(x => x.MessageType == ImportMessageType.Error), - }); - } - - // =========================================================================== - // 1.) Import products - // =========================================================================== - try - { - saved = ProcessProducts(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProducts"); - } - - // reduce batch to saved (valid) products. - // No need to perform import operations on errored products. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); - - // 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 (table.HasColumn("SeName") || batch.Any(x => x.IsNew || 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 (table.HasColumn("CategoryIds")) - { - try - { - ProcessProductCategories(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProductCategories"); - } - } - - // =========================================================================== - // 5.) Import product manufacturer mappings - // =========================================================================== - if (table.HasColumn("ManufacturerIds")) - { - try - { - ProcessProductManufacturers(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProductManufacturers"); - } - } - - - // =========================================================================== - // 6.) Import product picture mappings - // =========================================================================== - if (table.HasColumn("Picture1") || table.HasColumn("Picture2") || table.HasColumn("Picture3")) - { - try - { - ProcessProductPictures(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProductPictures"); - } - } - - } - } - catch (Exception ex) - { - result.AddError(ex, null, "ReadFile"); - } - } - - result.EndDateUtc = DateTime.UtcNow; - - if (cancellationToken.IsCancellationRequested) - { - result.Cancelled = true; - result.AddInfo("Import task was cancelled by user"); - } - - return result; - } - - private int ProcessProducts(ImportRow[] batch, ImportResult result) - { - _rsProduct.AutoCommitEnabled = true; - - Product lastInserted = null; - Product lastUpdated = null; - - foreach (var row in batch) - { - Product product = null; - - object key; - var dataRow = row.DataRow; - - // try get by int ID - if (dataRow.TryGetValue("Id", out key) && key.ToString().ToInt() > 0) - { - product = _productService.GetProductById(key.ToString().ToInt()); - } - - // try get by SKU - if (product == null && dataRow.TryGetValue("SKU", out key)) - { - product = _productService.GetProductBySku(key.ToString()); - } - - // try get by GTIN - if (product == null && dataRow.TryGetValue("Gtin", out key)) - { - product = _productService.GetProductByGtin(key.ToString()); - } - - if (product == null) - { - // a Name is required with new products. - if (!row.Segmenter.HasDataColumn("Name")) - { - result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); - continue; - } - product = new Product(); - } - - string name = row.GetDataValue("Name"); - - row.Initialize(product, name); - - if (!row.IsNew) - { - if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - { - // Perf: use this later for SeName updates. - row.NameChanged = true; - } - } - - row.SetProperty(result, product, (x) => x.Sku); - row.SetProperty(result, product, (x) => x.Gtin); - row.SetProperty(result, product, (x) => x.ManufacturerPartNumber); - row.SetProperty(result, product, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); - row.SetProperty(result, product, (x) => x.ParentGroupedProductId); - row.SetProperty(result, product, (x) => x.VisibleIndividually, true); - row.SetProperty(result, product, (x) => x.Name); - row.SetProperty(result, product, (x) => x.ShortDescription); - row.SetProperty(result, product, (x) => x.FullDescription); - row.SetProperty(result, product, (x) => x.ProductTemplateId); - row.SetProperty(result, product, (x) => x.ShowOnHomePage); - row.SetProperty(result, product, (x) => x.HomePageDisplayOrder); - row.SetProperty(result, product, (x) => x.MetaKeywords); - row.SetProperty(result, product, (x) => x.MetaDescription); - row.SetProperty(result, product, (x) => x.MetaTitle); - row.SetProperty(result, product, (x) => x.AllowCustomerReviews, true); - row.SetProperty(result, product, (x) => x.Published, true); - row.SetProperty(result, product, (x) => x.IsGiftCard); - row.SetProperty(result, product, (x) => x.GiftCardTypeId); - row.SetProperty(result, product, (x) => x.RequireOtherProducts); - row.SetProperty(result, product, (x) => x.RequiredProductIds); - row.SetProperty(result, product, (x) => x.AutomaticallyAddRequiredProducts); - row.SetProperty(result, product, (x) => x.IsDownload); - row.SetProperty(result, product, (x) => x.DownloadId); - row.SetProperty(result, product, (x) => x.UnlimitedDownloads, true); - row.SetProperty(result, product, (x) => x.MaxNumberOfDownloads, 10); - row.SetProperty(result, product, (x) => x.DownloadActivationTypeId, 1); - row.SetProperty(result, product, (x) => x.HasSampleDownload); - row.SetProperty(result, product, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); - row.SetProperty(result, product, (x) => x.HasUserAgreement); - row.SetProperty(result, product, (x) => x.UserAgreementText); - row.SetProperty(result, product, (x) => x.IsRecurring); - row.SetProperty(result, product, (x) => x.RecurringCycleLength, 100); - row.SetProperty(result, product, (x) => x.RecurringCyclePeriodId); - row.SetProperty(result, product, (x) => x.RecurringTotalCycles, 10); - row.SetProperty(result, product, (x) => x.IsShipEnabled, true); - row.SetProperty(result, product, (x) => x.IsFreeShipping); - row.SetProperty(result, product, (x) => x.AdditionalShippingCharge); - row.SetProperty(result, product, (x) => x.IsEsd); - row.SetProperty(result, product, (x) => x.IsTaxExempt); - row.SetProperty(result, product, (x) => x.TaxCategoryId, 1); - row.SetProperty(result, product, (x) => x.ManageInventoryMethodId); - row.SetProperty(result, product, (x) => x.StockQuantity, 10000); - row.SetProperty(result, product, (x) => x.DisplayStockAvailability); - row.SetProperty(result, product, (x) => x.DisplayStockQuantity); - row.SetProperty(result, product, (x) => x.MinStockQuantity); - row.SetProperty(result, product, (x) => x.LowStockActivityId); - row.SetProperty(result, product, (x) => x.NotifyAdminForQuantityBelow, 1); - row.SetProperty(result, product, (x) => x.BackorderModeId); - row.SetProperty(result, product, (x) => x.AllowBackInStockSubscriptions); - row.SetProperty(result, product, (x) => x.OrderMinimumQuantity, 1); - row.SetProperty(result, product, (x) => x.OrderMaximumQuantity, 10000); - row.SetProperty(result, product, (x) => x.AllowedQuantities); - row.SetProperty(result, product, (x) => x.DisableBuyButton); - row.SetProperty(result, product, (x) => x.DisableWishlistButton); - row.SetProperty(result, product, (x) => x.AvailableForPreOrder); - row.SetProperty(result, product, (x) => x.CallForPrice); - row.SetProperty(result, product, (x) => x.Price); - row.SetProperty(result, product, (x) => x.OldPrice); - row.SetProperty(result, product, (x) => x.ProductCost); - row.SetProperty(result, product, (x) => x.SpecialPrice); - row.SetProperty(result, product, (x) => x.SpecialPriceStartDateTimeUtc); - row.SetProperty(result, product, (x) => x.SpecialPriceEndDateTimeUtc); - 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); - row.SetProperty(result, product, (x) => x.AvailableEndDateTimeUtc); - row.SetProperty(result, product, (x) => x.LimitedToStores); - - var storeIds = row.GetDataValue>("StoreIds"); - if (storeIds != null && storeIds.Any()) - { - _storeMappingService.SaveStoreMappings(product, storeIds.ToArray()); - } - - row.SetProperty(result, product, (x) => x.CreatedOnUtc, DateTime.UtcNow); - - 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(ImportRow[] 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.Segmenter.HasDataColumn("SeName")) - { - try - { - string seName = row.GetDataValue("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(ImportRow[] 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.GetDataValue("Name", lang.UniqueSeoCode); - string localizedShortDescription = row.GetDataValue("ShortDescription", lang.UniqueSeoCode); - string localizedFullDescription = row.GetDataValue("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(ImportRow[] batch, ImportResult result) - { - _rsProductCategory.AutoCommitEnabled = false; - - ProductCategory lastInserted = null; - - foreach (var row in batch) - { - var categoryIds = row.GetDataValue>("CategoryIds"); - if (categoryIds != null && categoryIds.Any()) - { - try - { - foreach (var id in categoryIds) - { - 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(ImportRow[] batch, ImportResult result) - { - _rsProductManufacturer.AutoCommitEnabled = false; - - ProductManufacturer lastInserted = null; - - foreach (var row in batch) - { - var manufacturerIds = row.GetDataValue>("ManufacturerIds"); - if (manufacturerIds != null && manufacturerIds.Any()) - { - try - { - foreach (var id in manufacturerIds) - { - 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(ImportRow[] 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.GetDataValue("Picture1"), - row.GetDataValue("Picture2"), - row.GetDataValue("Picture3") - }; - - int i = 0; - try - { - for (i = 0; i < pictures.Length; i++) - { - var picture = pictures[i]; - - if (picture.IsEmpty() || !File.Exists(picture)) - continue; - - var currentPictures = _rsProductPicture.TableUntracked.Expand(x => x.Picture).Where(x => x.ProductId == row.Entity.Id).Select(x => x.Picture).ToList(); - var pictureBinary = _pictureService.FindEqualPicture(picture, currentPictures, out equalPictureId); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - // no equal picture found in sequence - var newPicture = _pictureService.InsertPicture(pictureBinary, "image/jpeg", _pictureService.GetPictureSeName(row.EntityDisplayName), true, false, false); - if (newPicture != null) - { - var mapping = new ProductPicture - { - ProductId = row.Entity.Id, - PictureId = newPicture.Id, - DisplayOrder = 1, - }; - _rsProductPicture.Insert(mapping); - lastInserted = mapping; - } - } - else - { - result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "Picture" + (i + 1).ToString()); - } - } - } - catch (Exception ex) - { - result.AddWarning(ex.Message, row.GetRowInfo(), "Picture" + (i + 1).ToString()); - } - - } - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _eventPublisher.EntityInserted(lastInserted); - } - - private int? ZeroToNull(object value, CultureInfo culture) - { - int result; - if (CommonHelper.TryConvert(value, culture, out result) && result > 0) - { - return result; - } - - return (int?)null; - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs index 245aabc173..1061e690e7 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs @@ -37,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/ImportModeFlags.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportModeFlags.cs deleted file mode 100644 index 1e85010067..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportModeFlags.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace SmartStore.Services.DataExchange.Import -{ - - [Flags] - public enum ImportModeFlags - { - Insert = 1, - Update = 2 - } - -} 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/ImportProgressInfo.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProgressInfo.cs deleted file mode 100644 index 19419845d0..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProgressInfo.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; - -namespace SmartStore.Services.DataExchange.Import -{ - - 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.Services/DataExchange/Import/ImportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs index 731a12861b..a8a29366f7 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Serialization; namespace SmartStore.Services.DataExchange.Import { - - public class ImportResult + public class ImportResult : ICloneable { - public ImportResult() { this.Messages = new List(); - this.StartDateUtc = DateTime.UtcNow; + Clear(); } public DateTime StartDateUtc @@ -32,6 +31,12 @@ public int TotalRecords set; } + public int SkippedRecords + { + get; + set; + } + public int NewRecords { get; @@ -55,6 +60,17 @@ public bool Cancelled 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); @@ -72,14 +88,6 @@ public ImportMessage AddError(string message, ImportRowInfo affectedRow = null, 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) { @@ -96,16 +104,29 @@ public ImportMessage AddError(Exception exception, int? affectedBatch = null, st msg = "[{0}] ".FormatCurrent(String.Join(", ", prefix)); } - msg += ex.Message; + msg += exception.ToAllMessages(); - return this.AddMessage(msg, ImportMessageType.Error); + 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) + 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; } @@ -121,11 +142,71 @@ 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 index 9ca5ab441b..1f213ee9e4 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Linq.Expressions; -using System.Text; using SmartStore.ComponentModel; using SmartStore.Core; @@ -11,17 +8,20 @@ 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 ImportDataSegmenter _segmenter; private readonly IDataRow _row; - public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) + public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) { _segmenter = parent; _row = row; @@ -45,6 +45,27 @@ private void CheckInitialized() } } + 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; } @@ -55,12 +76,12 @@ public bool IsNew get { return _isNew; } } - public IDataRow DataRow + public bool IsDirty { - get { return _row; } + get { return _isDirty; } } - public ImportDataSegmenter Segmenter + public ImportDataSegmenter Segmenter { get { return _segmenter; } } @@ -70,6 +91,11 @@ public T Entity get { return _entity; } } + public IDataRow DataRow + { + get { return _row; } + } + public string EntityDisplayName { get { return _entityDisplayName; } @@ -86,41 +112,147 @@ public int Position get { return _position; } } - public TProp GetDataValue(string columnName) + /// + /// 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) { - return GetDataValue(columnName, null); + 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; } - public TProp GetDataValue(string columnName, string index) + /// + /// 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; - if (_row.TryGetValue(_segmenter.ColumnMap.GetMappedName(columnName, index), out 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) { - return value.Convert(_segmenter.Culture); + value = rawValue.ToString().IsCaseInsensitiveEqual(ExplicitNull) + ? default(TProp) + : rawValue.Convert(_segmenter.Culture); + return true; } - return default(TProp); + 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, - T target, Expression> prop, TProp defaultValue = default(TProp), Func converter = null) { - // TBD: (MC) do not check for perf reason? + 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 { - var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); - object value; - if (_row.TryGetValue(_segmenter.ColumnMap.GetMappedName(propName), out 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; @@ -128,34 +260,52 @@ public bool SetProperty( { 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.ToString().ToUpper().Equals("NULL") - ? default(TProp) - : value.Convert(_segmenter.Culture); + converted = value.Convert(_segmenter.Culture); } + var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); fastProp.SetValue(target, converted); - return true; + isPropertySet = true; } else { - // source does not contain field data or is empty... - if (IsTransient && defaultValue != null) + // source field value does not exist or is null/empty + if (IsNew) { - // ...but the entity is new. In this case - // set the default value if given. - fastProp.SetValue(target, defaultValue); - return true; + // 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 ex) + catch (Exception exception) { - result.AddWarning("Conversion failed: " + ex.Message, this.GetRowInfo(), propName); + result.AddWarning("Conversion failed: " + exception.Message, this.GetRowInfo(), propName); } - return false; + if (isPropertySet && !_isDirty) + { + _isDirty = true; + } + + return isPropertySet; } public ImportRowInfo GetRowInfo() 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/Directory/IStateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs index 95f0c91c2a..e62ff53527 100644 --- a/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Directory @@ -14,12 +15,19 @@ public partial interface IStateProvinceService /// The state/province void DeleteStateProvince(StateProvince stateProvince); - /// - /// Gets a state/province - /// - /// The state/province identifier - /// State/province - StateProvince GetStateProvinceById(int stateProvinceId); + /// + /// Get all states/provinces + /// + /// A value indicating whether to show hidden records + /// + IQueryable GetAllStateProvinces(bool showHidden = false); + + /// + /// Gets a state/province + /// + /// The state/province identifier + /// State/province + StateProvince GetStateProvinceById(int stateProvinceId); /// /// Gets a state/province diff --git a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs index 27ddf6f5a6..b631dca9e9 100644 --- a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs @@ -8,10 +8,10 @@ namespace SmartStore.Services.Directory { - /// - /// State province service - /// - public partial class StateProvinceService : IStateProvinceService + /// + /// State province service + /// + public partial class StateProvinceService : IStateProvinceService { #region Constants private const string STATEPROVINCES_ALL_KEY = "SmartStore.stateprovince.all-{0}"; @@ -46,6 +46,7 @@ public StateProvinceService(ICacheManager cacheManager, #endregion #region Methods + /// /// Deletes a state/province /// @@ -63,12 +64,22 @@ public virtual void DeleteStateProvince(StateProvince stateProvince) _eventPublisher.EntityDeleted(stateProvince); } - /// - /// Gets a state/province - /// - /// The state/province identifier - /// State/province - public virtual StateProvince GetStateProvinceById(int stateProvinceId) + public virtual IQueryable GetAllStateProvinces(bool showHidden = false) + { + var query = _stateProvinceRepository.Table; + + if (!showHidden) + query = query.Where(x => x.Published); + + return query; + } + + /// + /// Gets a state/province + /// + /// The state/province identifier + /// State/province + public virtual StateProvince GetStateProvinceById(int stateProvinceId) { if (stateProvinceId == 0) return null; diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs index 9b75b86e8b..ffa0fa2615 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs @@ -12,6 +12,7 @@ using SmartStore.Services.Customers; using SmartStore.Services.Common; using SmartStore.Services.Configuration; +using SmartStore.Services.Orders; namespace SmartStore.Services.Discounts { @@ -317,18 +318,18 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string } //check date range - DateTime now = DateTime.UtcNow; - int storeId = _storeContext.CurrentStore.Id; + var now = DateTime.UtcNow; + var store = _storeContext.CurrentStore; if (discount.StartDateUtc.HasValue) { - DateTime startDate = DateTime.SpecifyKind(discount.StartDateUtc.Value, DateTimeKind.Utc); + var startDate = DateTime.SpecifyKind(discount.StartDateUtc.Value, DateTimeKind.Utc); if (startDate.CompareTo(now) > 0) return false; } if (discount.EndDateUtc.HasValue) { - DateTime endDate = DateTime.SpecifyKind(discount.EndDateUtc.Value, DateTimeKind.Utc); + var endDate = DateTime.SpecifyKind(discount.EndDateUtc.Value, DateTimeKind.Utc); if (endDate.CompareTo(now) < 0) return false; } @@ -336,33 +337,37 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string if (!CheckDiscountLimitations(discount, customer)) return false; - // discount requirements - var requirements = discount.DiscountRequirements; + // better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. + if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) + { + var cart = customer.ShoppingCartItems + .Filter(ShoppingCartType.ShoppingCart, store.Id) + .ToList(); + + if (cart.Any(x => x.Product.IsGiftCard)) + return false; + } + + // discount requirements + var requirements = discount.DiscountRequirements; foreach (var req in requirements) { - var requirementRule = LoadDiscountRequirementRuleBySystemName(req.DiscountRequirementRuleSystemName, storeId); + var requirementRule = LoadDiscountRequirementRuleBySystemName(req.DiscountRequirementRuleSystemName, store.Id); if (requirementRule == null) continue; - var request = new CheckDiscountRequirementRequest() + var request = new CheckDiscountRequirementRequest { DiscountRequirement = req, Customer = customer, - Store = _storeContext.CurrentStore + Store = store }; - if (!requirementRule.Value.CheckRequirement(request)) + + // TODO: cache result... CheckRequirement is very often called + if (!requirementRule.Value.CheckRequirement(request)) return false; } - // better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. - if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) - { - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, storeId); - - if (cart.Any(x => x.Item.Product.IsGiftCard)) - return false; - } - return true; } diff --git a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs index a2e53aa8da..216894b2d2 100644 --- a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs +++ b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs @@ -13,6 +13,7 @@ namespace SmartStore { public static class NameValueCollectionExtensions { + // TODO: find place to make it public static private static string AttributeFormatedName(int productAttributeId, int attributeId, int productId = 0, int bundleItemId = 0) { if (productId == 0) @@ -25,34 +26,57 @@ public static void AddProductAttribute(this NameValueCollection collection, int { if (productAttributeId != 0 && attributeId != 0 && valueId != 0) { - string name = AttributeFormatedName(productAttributeId, attributeId, productId, bundleItemId); + var name = AttributeFormatedName(productAttributeId, attributeId, productId, bundleItemId); collection.Add(name, valueId.ToString()); } } /// - /// Converts attribute query data + /// Get selected attributes from query string /// - /// Name value collection + /// Name value collection with selected attributes + /// Query string parameters /// Attribute query data items with following structure: /// Product.Id, ProductAttribute.Id, Product_ProductAttribute_Mapping.Id, ProductVariantAttributeValue.Id, [BundleItem.Id] /// Product identifier to filter - public static void ConvertAttributeQueryData(this NameValueCollection collection, List> queryData, int productId = 0) + public static void GetSelectedAttributes(this NameValueCollection collection, NameValueCollection queryString, List> attributes, int productId = 0) { - if (collection == null || queryData == null || queryData.Count <= 0) - return; + Guard.NotNull(() => collection); - var items = queryData.Where(i => i.Count > 3); + // ambiguous parameters: let other query string parameters win over the json formatted attributes parameter + if (queryString != null && queryString.Count > 0) + { + var items = queryString.AllKeys + .Where(x => x.EmptyNull().StartsWith("product_attribute_")) + .SelectMany(queryString.GetValues, (k, v) => new { key = k.EmptyNull(), value = v.TrimSafe() }); - if (productId != 0) - items = items.Where(i => i[0] == productId); + foreach (var item in items) + { + var ids = item.key.Replace("product_attribute_", "").SplitSafe("_"); + if (ids.Count() > 3) + { + if (productId == 0 || (productId != 0 && productId == ids[0].ToInt())) + { + collection.Add(item.key, item.value); + } + } + } + } - foreach (var item in items) + if (attributes != null && attributes.Count > 0) { - var name = AttributeFormatedName(item[1], item[2], item[0], item.Count > 4 ? item[4] : 0); + var items = attributes.Where(i => i.Count > 3); + + if (productId != 0) + items = items.Where(i => i[0] == productId); - collection.Add(name, item[3].ToString()); + foreach (var item in items) + { + var name = AttributeFormatedName(item[1], item[2], item[0], item.Count > 4 ? item[4] : 0); + + collection.Add(name, item[3].ToString()); + } } } @@ -88,7 +112,7 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec var ctrlAttributes = collection[controlId]; if (ctrlAttributes.HasValue()) { - int selectedAttributeId = int.Parse(ctrlAttributes); + var selectedAttributeId = ctrlAttributes.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } @@ -102,7 +126,7 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec { foreach (var item in cblAttributes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - int selectedAttributeId = int.Parse(item); + var selectedAttributeId = item.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } diff --git a/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs b/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs index b567453f1b..6bbd9756f9 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs @@ -38,6 +38,8 @@ public class FilterCriteria : IComparable // Metadata public int MatchCount { get; set; } + public int DisplayOrder { get; set; } + public int DisplayOrderValues { get; set; } public bool IsInactive { get; set; } public string NameLocalized { get; set; } public string ValueLocalized { get; set; } diff --git a/src/Libraries/SmartStore.Services/Filter/FilterService.cs b/src/Libraries/SmartStore.Services/Filter/FilterService.cs index 6187db1d44..507b1f2fed 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterService.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterService.cs @@ -11,7 +11,6 @@ using SmartStore.Core.Infrastructure; using SmartStore.Services.Catalog; using SmartStore.Services.Localization; -using SmartStore.Utilities; namespace SmartStore.Services.Filter { @@ -254,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(); @@ -282,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 @@ -303,14 +314,28 @@ from a in attributes Value = g.FirstOrDefault().Name, ID = g.Key.Id, PId = g.FirstOrDefault().SpecificationAttribute.Id, - MatchCount = g.Count() + MatchCount = g.Count(), + DisplayOrder = g.FirstOrDefault().SpecificationAttribute.DisplayOrder, + DisplayOrderValues = g.FirstOrDefault().DisplayOrder }; + if (_catalogSettings.SortFilterResultsByMatches) + { + criterias = grouped + .OrderBy(a => a.DisplayOrder) + .ThenByDescending(a => a.MatchCount) + .ThenBy(a => a.DisplayOrderValues) + .ToList(); + } + else + { + criterias = grouped + .OrderBy(a => a.DisplayOrder) + .ThenBy(a => a.DisplayOrderValues) + .ToList(); + } - var lst = grouped.OrderByDescending(a => a.MatchCount).ToList(); - int languageId = _services.WorkContext.WorkingLanguage.Id; - - lst.ForEach(c => + criterias.ForEach(c => { c.Entity = ShortcutSpecAttribute; c.IsInactive = true; @@ -322,7 +347,21 @@ from a in attributes c.ValueLocalized = _localizedEntityService.GetLocalizedValue(languageId, c.ID.Value, "SpecificationAttributeOption", "Name"); }); - return lst; + return criterias; + } + + private void AddChildCategoryIds(List result, int categoryId) + { + var ids = _categoryService.GetAllCategoriesByParentCategoryId(categoryId).Select(x => x.Id); + + foreach (var id in ids) + { + if (!result.Contains(id)) + { + result.Add(id); + AddChildCategoryIds(result, id); + } + } } public virtual List Deserialize(string jsonData) @@ -364,9 +403,7 @@ 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; diff --git a/src/Libraries/SmartStore.Services/ICommonServices.cs b/src/Libraries/SmartStore.Services/ICommonServices.cs index ee16a33432..f9b3043bf0 100644 --- a/src/Libraries/SmartStore.Services/ICommonServices.cs +++ b/src/Libraries/SmartStore.Services/ICommonServices.cs @@ -86,4 +86,37 @@ 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 9f1cc99290..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.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Plugins; -using SmartStore.Services.DataExchange.Import; 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 8bbd78cf9b..a55b8ab84f 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs @@ -1,18 +1,18 @@ 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 System.Xml; using SmartStore.Utilities; -using SmartStore.Services.DataExchange.Import; -using SmartStore.ComponentModel; namespace SmartStore.Services.Localization { - public static class LocalizationExtentions + public static class LocalizationExtentions { /// /// Get localized property of an entity diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs index 21cf03447f..c6ac869c27 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs @@ -1,29 +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 SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Events; 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.Services.DataExchange.Import; +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}"; @@ -173,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; } @@ -271,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) { @@ -416,13 +419,183 @@ public virtual string ExportResourcesToXml(Language language) return stringWriter.ToString(); } + public virtual void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null) + { + var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.OriginalAssemblyFile.Directory.FullName, "Localization")); + + if (!directory.Exists) + return; + + if (targetList == null && updateTouchedResources) + { + DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + } + + var unprocessedLanguages = new List(); + + var defaultLanguageId = _languageService.GetDefaultLanguageId(); + var languages = filterLanguages ?? _languageService.GetAllLanguages(true); + + string code = null; + foreach (var language in languages) + { + code = ImportPluginResourcesForLanguage( + language, + null, + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + false); + + if (code == null) + { + unprocessedLanguages.Add(language); + } + } + + if (filterLanguages == null && unprocessedLanguages.Count > 0) + { + // There were unprocessed languages (no corresponding resource file could be found). + // In order for GetResource() to be able to gracefully fallback to the default language's resources, + // we need to import resources for the current default language.... + var processedLanguages = languages.Except(unprocessedLanguages).ToList(); + if (!processedLanguages.Any(x => x.Id == defaultLanguageId)) + { + // ...but only if no resource file could be mapped to the default language before, + // namely because in this case the following operation would be redundant. + var defaultLanguage = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId()); + if (defaultLanguage != null) + { + ImportPluginResourcesForLanguage( + defaultLanguage, + "en-us", + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + true); + } + } + } + } + /// - /// Import language resources from XML file + /// Resolves a resource file for the specified language and processes the import /// /// Language - /// XML document - /// Prefix for resource key name - public virtual void ImportResourcesFromXml( + /// The culture code of the processed resource file + private string ImportPluginResourcesForLanguage( + Language language, + string fileCode, + DirectoryInfo directory, + string resourceRootKey, + IList targetList, + bool updateTouchedResources, + bool canFallBackToAnyResourceFile) + { + var fileNamePattern = "resources.{0}.xml"; + + var codeCandidates = GetResourceFileCodeCandidates( + fileCode ?? language.LanguageCulture, + directory, + canFallBackToAnyResourceFile); + + string path = null; + string code = null; + + foreach (var candidate in codeCandidates) + { + var pathCandidate = Path.Combine(directory.FullName, fileNamePattern.FormatInvariant(candidate)); + if (File.Exists(pathCandidate)) + { + code = candidate; + path = pathCandidate; + break; + } + } + + if (code != null) + { + var doc = new XmlDocument(); + + doc.Load(path); + doc = FlattenResourceFile(doc); + + if (targetList == null) + { + ImportResourcesFromXml(language, doc, resourceRootKey, true, updateTouchedResources: updateTouchedResources); + } + else + { + var nodes = doc.SelectNodes(@"//Language/LocaleResource"); + foreach (XmlNode node in nodes) + { + var valueNode = node.SelectSingleNode("Value"); + var res = new LocaleStringResource() + { + ResourceName = node.Attributes["Name"].InnerText.Trim(), + ResourceValue = (valueNode == null ? "" : valueNode.InnerText), + LanguageId = language.Id, + IsFromPlugin = true + }; + + if (res.ResourceName.HasValue()) + { + targetList.Add(res); + } + } + } + } + + return code; + } + + private IEnumerable GetResourceFileCodeCandidates(string code, DirectoryInfo directory, bool canFallBackToAnyResourceFile) + { + // exact match (de-DE) + yield return code; + + // neutral culture (de) + var ci = CultureInfo.GetCultureInfo(code); + if (ci.Parent != null && !ci.IsNeutralCulture) + { + code = ci.Parent.Name; + yield return code; + } + + var rgFileName = new Regex("^resources.(.+?).xml$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // any other region with same language (de-*) + foreach (var fi in directory.EnumerateFiles("resources.{0}-*.xml".FormatInvariant(code), SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + + if (canFallBackToAnyResourceFile) + { + foreach (var fi in directory.EnumerateFiles("resources.*.xml", SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + } + } + + public virtual int ImportResourcesFromXml( Language language, XmlDocument xmlDocument, string rootKey = null, @@ -430,15 +603,18 @@ public virtual void ImportResourcesFromXml( ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, bool updateTouchedResources = false) { - using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false, forceNoTracking: true, hooksEnabled: false)) { var toAdd = new List(); var toUpdate = new List(); var nodes = xmlDocument.SelectNodes(@"//Language/LocaleResource"); + var resources = language.LocaleStringResources.ToDictionarySafe(x => x.ResourceName, StringComparer.OrdinalIgnoreCase); + + LocaleStringResource resource; + foreach (var xel in nodes.Cast()) { - string name = xel.GetAttribute("Name").TrimSafe(); string value = ""; var valueNode = xel.SelectSingleNode("Value"); @@ -454,18 +630,23 @@ public virtual void ImportResourcesFromXml( name = "{0}.{1}".FormatWith(rootKey, name); } + resource = null; + // do not use "Insert"/"Update" methods because they clear cache // let's bulk insert - var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (resource != null) + //var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (resources.TryGetValue(name, out resource)) { if (mode.HasFlag(ImportModeFlags.Update)) { if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) { - resource.ResourceValue = value; - resource.IsTouched = null; - toUpdate.Add(resource); + if (value != resource.ResourceValue) + { + resource.ResourceValue = value; + resource.IsTouched = null; + toUpdate.Add(resource); + } } } } @@ -474,7 +655,7 @@ public virtual void ImportResourcesFromXml( if (mode.HasFlag(ImportModeFlags.Insert)) { toAdd.Add( - new LocaleStringResource() + new LocaleStringResource { LanguageId = language.Id, ResourceName = name, @@ -485,90 +666,25 @@ public virtual void ImportResourcesFromXml( } } - _lsrRepository.AutoCommitEnabled = true; - _lsrRepository.InsertRange(toAdd, 500); - toAdd.Clear(); + //_lsrRepository.AutoCommitEnabled = true; - _lsrRepository.AutoCommitEnabled = null; - toUpdate.Each(x => + if (toAdd.Any() || toUpdate.Any()) { - _lsrRepository.Update(x); - }); - - _lsrRepository.Context.SaveChanges(); - toUpdate.Clear(); + _lsrRepository.InsertRange(toAdd); + toAdd.Clear(); - //clear cache - _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - } - } - - /// - /// Import plugin resources from xml files in plugin's localization directory. - /// - /// Descriptor of the plugin - /// Load them into list rather than into database - /// Specifies whether user touched resources should also be updated - /// Import only files for particular languages - public virtual void ImportPluginResourcesFromXml(PluginDescriptor pluginDescriptor, - List forceToList = null, bool updateTouchedResources = true, IList filterLanguages = null) - { - string pluginDir = pluginDescriptor.OriginalAssemblyFile.Directory.FullName; - string localizationDir = Path.Combine(pluginDir, "Localization"); - - if (!System.IO.Directory.Exists(localizationDir)) - return; - - if (forceToList == null && updateTouchedResources) - DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + _lsrRepository.UpdateRange(toUpdate); + toUpdate.Clear(); - var languages = _languageService.GetAllLanguages(true); - var doc = new XmlDocument(); + int num = _lsrRepository.Context.SaveChanges(); - foreach (var filePath in System.IO.Directory.EnumerateFiles(localizationDir, "*.xml")) - { - Match match = Regex.Match(Path.GetFileName(filePath), Regex.Escape("resources.") + "(.*?)" + Regex.Escape(".xml")); - string languageCode = match.Groups[1].Value; + // clear cache + _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - Language language = languages.Where(l => l.LanguageCulture.IsCaseInsensitiveEqual(languageCode)).FirstOrDefault(); - if (language != null) - { - language = _languageService.GetLanguageById(language.Id); + return num; } - if (languageCode.HasValue() && language != null) - { - if (filterLanguages != null && !filterLanguages.Any(x => x.Id == language.Id)) - { - continue; - } - - doc.Load(filePath); - doc = FlattenResourceFile(doc); - - if (forceToList == null) - { - ImportResourcesFromXml(language, doc, pluginDescriptor.ResourceRootKey, true, updateTouchedResources: updateTouchedResources); - } - else - { - var nodes = doc.SelectNodes(@"//Language/LocaleResource"); - foreach (XmlNode node in nodes) - { - var valueNode = node.SelectSingleNode("Value"); - var res = new LocaleStringResource() - { - ResourceName = node.Attributes["Name"].InnerText.Trim(), - ResourceValue = (valueNode == null ? "" : valueNode.InnerText), - LanguageId = language.Id, - IsFromPlugin = true - }; - - if (res.ResourceName.HasValue()) - forceToList.Add(res); - } - } - } + return 0; } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index a0b2c2989f..f765e0d580 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; +using System.Collections.Concurrent; namespace SmartStore.Services.Localization { @@ -15,126 +16,76 @@ namespace SmartStore.Services.Localization /// public partial class LocalizedEntityService : ILocalizedEntityService { - #region Constants - - private const string LOCALIZEDPROPERTY_KEY = "SmartStore.localizedproperty.value-{0}-{1}-{2}-{3}"; private const string LOCALIZEDPROPERTY_ALL_KEY = "SmartStore.localizedproperty.all"; - private const string LOCALIZEDPROPERTY_PATTERN_KEY = "SmartStore.localizedproperty."; - - #endregion - - #region Fields private readonly IRepository _localizedPropertyRepository; private readonly ICacheManager _cacheManager; private readonly LocalizationSettings _localizationSettings; - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Cache manager - /// Localized property repository - public LocalizedEntityService(ICacheManager cacheManager, IRepository localizedPropertyRepository, LocalizationSettings localizationSettings) + public LocalizedEntityService( + ICacheManager cacheManager, + IRepository localizedPropertyRepository, + LocalizationSettings localizationSettings) { this._cacheManager = cacheManager; this._localizedPropertyRepository = localizedPropertyRepository; this._localizationSettings = localizationSettings; } - #endregion - - #region Methods - - /// - /// Deletes a localized property - /// - /// Localized property - public virtual void DeleteLocalizedProperty(LocalizedProperty localizedProperty) - { - if (localizedProperty == null) - throw new ArgumentNullException("localizedProperty"); - - _localizedPropertyRepository.Delete(localizedProperty); - - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); - } - - /// - /// Gets a localized property - /// - /// Localized property identifier - /// Localized property - public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId) - { - if (localizedPropertyId == 0) - return null; - - var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); - return localizedProperty; - } - - /// - /// Find localized value - /// - /// Language identifier - /// Entity identifier - /// Locale key group - /// Locale key - /// Found localized value - public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) - { - string val = null; - - if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) + protected virtual ConcurrentDictionary GetAllProperties() + { + var result = _cacheManager.Get(LOCALIZEDPROPERTY_ALL_KEY, () => { - var allValues = _cacheManager.Get(LOCALIZEDPROPERTY_ALL_KEY, () => + if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) { - var result = _localizedPropertyRepository.TableUntracked.ToDictionary( + var props = _localizedPropertyRepository.TableUntracked.ToDictionarySafe( x => GenerateKey(x.LanguageId, x.LocaleKeyGroup, x.LocaleKey, x.EntityId), - x => x.LocaleValue); - return result; - }); + x => x.LocaleValue.EmptyNull()); + return new ConcurrentDictionary(props); + } + else + { + return new ConcurrentDictionary(); + } + }); - string key = GenerateKey(languageId, localeKeyGroup, localeKey, entityId); + return result; + } - if (!allValues.TryGetValue(key, out val)) + public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + var props = GetAllProperties(); + string key = GenerateKey(languageId, localeKeyGroup, localeKey, entityId); + string val = null; + + if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) + { + if (!props.TryGetValue(key, out val)) { return string.Empty; } } else { - string cacheKey = string.Format(LOCALIZEDPROPERTY_KEY, languageId, entityId, localeKeyGroup, localeKey); - val = _cacheManager.Get(cacheKey, () => - { + val = props.GetOrAdd(key, k => { var query = from lp in _localizedPropertyRepository.TableUntracked - where lp.EntityId == entityId && - lp.LocaleKey == localeKey && - lp.LocaleKeyGroup == localeKeyGroup && - lp.LanguageId == languageId + where + lp.EntityId == entityId && + lp.LocaleKey == localeKey && + lp.LocaleKeyGroup == localeKeyGroup && + lp.LanguageId == languageId select lp.LocaleValue; - + return query.FirstOrDefault().EmptyNull(); }); } return val; - } + } - /// - /// Gets localized properties - /// - /// Entity identifier - /// Locale key group - /// Localized properties public virtual IList GetLocalizedProperties(int entityId, string localeKeyGroup) { - if (string.IsNullOrEmpty(localeKeyGroup)) + if (localeKeyGroup.IsEmpty()) return new List(); var query = from lp in _localizedPropertyRepository.Table @@ -142,49 +93,92 @@ orderby lp.Id where lp.EntityId == entityId && lp.LocaleKeyGroup == localeKeyGroup select lp; + var props = query.ToList(); return props; } - /// - /// Inserts a localized property - /// - /// Localized property - public virtual void InsertLocalizedProperty(LocalizedProperty localizedProperty) - { - if (localizedProperty == null) - throw new ArgumentNullException("localizedProperty"); + protected virtual LocalizedProperty GetLocalizedProperty(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + var query = from lp in _localizedPropertyRepository.Table + where + lp.EntityId == entityId && + lp.LocaleKey == localeKey && + lp.LocaleKeyGroup == localeKeyGroup && + lp.LanguageId == languageId + select lp; + + return query.FirstOrDefault(); + } - _localizedPropertyRepository.Insert(localizedProperty); + public virtual void InsertLocalizedProperty(LocalizedProperty localizedProperty) + { + Guard.ArgumentNotNull(() => localizedProperty); - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); + try + { + // db + _localizedPropertyRepository.Insert(localizedProperty); + + // cache + var key = GenerateKey(localizedProperty); + var val = localizedProperty.LocaleValue.EmptyNull(); + GetAllProperties().AddOrUpdate( + key, + val, + (k, v) => val); + } + catch { } } - /// - /// Updates the localized property - /// - /// Localized property public virtual void UpdateLocalizedProperty(LocalizedProperty localizedProperty) { - if (localizedProperty == null) - throw new ArgumentNullException("localizedProperty"); + Guard.ArgumentNotNull(() => localizedProperty); - _localizedPropertyRepository.Update(localizedProperty); + try + { + // db + _localizedPropertyRepository.Update(localizedProperty); + + // cache + var key = GenerateKey(localizedProperty); + var val = localizedProperty.LocaleValue.EmptyNull(); + GetAllProperties().AddOrUpdate( + key, + val, + (k, v) => val); + } + catch { } + } - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); - } + public virtual void DeleteLocalizedProperty(LocalizedProperty localizedProperty) + { + Guard.ArgumentNotNull(() => localizedProperty); + + try + { + // cache + var key = GenerateKey(localizedProperty); + string val = null; + GetAllProperties().TryRemove(key, out val); + + // db + _localizedPropertyRepository.Delete(localizedProperty); + } + catch { } + } + + public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId) + { + if (localizedPropertyId == 0) + return null; - /// - /// Save localized value - /// - /// Type - /// Entity - /// Key selector - /// Locale value - /// Language ID - public virtual void SaveLocalizedValue(T entity, + var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); + return localizedProperty; + } + + public virtual void SaveLocalizedValue( + T entity, Expression> keySelector, string localeValue, int languageId) where T : BaseEntity, ILocalizedEntity @@ -192,16 +186,14 @@ public virtual void SaveLocalizedValue(T entity, SaveLocalizedValue(entity, keySelector, localeValue, languageId); } - public virtual void SaveLocalizedValue(T entity, + public virtual void SaveLocalizedValue( + T entity, Expression> keySelector, TPropType localeValue, int languageId) where T : BaseEntity, ILocalizedEntity { - if (entity == null) - throw new ArgumentNullException("entity"); - - if (languageId == 0) - throw new ArgumentOutOfRangeException("languageId", "Language ID should not be 0"); + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotZero(languageId, "languageId"); var member = keySelector.Body as MemberExpression; if (member == null) @@ -222,32 +214,33 @@ public virtual void SaveLocalizedValue(T entity, string localeKeyGroup = typeof(T).Name; string localeKey = propInfo.Name; - var props = GetLocalizedProperties(entity.Id, localeKeyGroup); - var prop = props.FirstOrDefault(lp => lp.LanguageId == languageId && - lp.LocaleKey.Equals(localeKey, StringComparison.InvariantCultureIgnoreCase)); //should be culture invariant + var prop = GetLocalizedProperty(languageId, entity.Id, localeKeyGroup, localeKey); string localeValueStr = localeValue.Convert(); if (prop != null) { - if (string.IsNullOrWhiteSpace(localeValueStr)) + if (localeValueStr.IsEmpty()) { - //delete + // delete DeleteLocalizedProperty(prop); } else { - //update - prop.LocaleValue = localeValueStr; - UpdateLocalizedProperty(prop); + // update + if (prop.LocaleValue != localeValueStr) + { + prop.LocaleValue = localeValueStr; + UpdateLocalizedProperty(prop); + } } } else { - if (!string.IsNullOrWhiteSpace(localeValueStr)) + if (localeValueStr.HasValue()) { - //insert - prop = new LocalizedProperty() + // insert + prop = new LocalizedProperty { EntityId = entity.Id, LanguageId = languageId, @@ -260,11 +253,14 @@ public virtual void SaveLocalizedValue(T entity, } } + private string GenerateKey(LocalizedProperty prop) + { + return GenerateKey(prop.LanguageId, prop.LocaleKeyGroup, prop.LocaleKey, prop.EntityId); + } + private string GenerateKey(int languageId, string localeKeyGroup, string localeKey, int entityId) { return "{0}.{1}.{2}.{3}".FormatInvariant(languageId, localeKeyGroup, localeKey, entityId); } - - #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs b/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs index 08d4a77083..61ba8f21ad 100644 --- a/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs +++ b/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs @@ -213,19 +213,33 @@ public virtual void DeleteActivity(ActivityLog activityLog) /// 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) + 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()) + 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 _customerRepository.Table on al.CustomerId equals c.Id - where c.Email == email + join c in queryCustomers on al.CustomerId equals c.Id select al; } diff --git a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs index fb152d8a1f..8387de3969 100644 --- a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs +++ b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs @@ -119,9 +119,9 @@ public virtual IPagedList GetAllLogs(DateTime? fromUtc, DateTime? toUtc, st var query = _logRepository.Table; if (fromUtc.HasValue) - query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc); + query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc || fromUtc.Value <= l.UpdatedOnUtc); if (toUtc.HasValue) - query = query.Where(l => toUtc.Value >= l.CreatedOnUtc); + query = query.Where(l => toUtc.Value >= l.CreatedOnUtc || toUtc.Value >= l.UpdatedOnUtc); if (logLevel.HasValue) { int logLevelId = (int)logLevel.Value; @@ -129,7 +129,8 @@ public virtual IPagedList GetAllLogs(DateTime? fromUtc, DateTime? toUtc, st } if (!String.IsNullOrEmpty(message)) query = query.Where(l => l.ShortMessage.Contains(message) || l.FullMessage.Contains(message)); - query = query.OrderByDescending(l => l.CreatedOnUtc); + + query = query.OrderByDescending(l => l.UpdatedOnUtc).ThenByDescending(l => l.CreatedOnUtc); if (minFrequency > 0) query = query.Where(l => l.Frequency >= minFrequency); diff --git a/src/Libraries/SmartStore.Services/Media/IPictureService.cs b/src/Libraries/SmartStore.Services/Media/IPictureService.cs index 85bc1741b5..ce9f231349 100644 --- a/src/Libraries/SmartStore.Services/Media/IPictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/IPictureService.cs @@ -5,10 +5,10 @@ namespace SmartStore.Services.Media { - /// - /// Picture service interface - /// - public partial interface IPictureService + /// + /// Picture service interface + /// + public partial interface IPictureService { /// /// Validates input picture dimensions and prevents that the image size exceeds global max size @@ -22,20 +22,19 @@ public partial interface IPictureService /// Finds an equal picture by comparing the binary buffer /// /// The picture to find a duplicate for - /// The sequence of product pictures to seek within for duplicates + /// The sequence of pictures to seek within for duplicates /// Id of equal picture if any /// The picture binary for path when no picture equals in the sequence, null otherwise. - byte[] FindEqualPicture(string path, IEnumerable productPictures, out int equalPictureId); + byte[] FindEqualPicture(string path, IEnumerable pictures, out int equalPictureId); /// /// Finds an equal picture by comparing the binary buffer /// /// Binary picture data - /// The sequence of product pictures to seek within for duplicates + /// The sequence of pictures to seek within for duplicates /// Id of equal picture if any /// The picture binary for path when no picture equals in the sequence, null otherwise. - byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable productPictures, out int equalPictureId); - + byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable pictures, out int equalPictureId); /// /// Gets the loaded picture binary depending on picture storage settings diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index 95c58fb845..563993234f 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -154,17 +154,21 @@ public virtual byte[] ValidatePicture(byte[] pictureBinary) } } - public byte[] FindEqualPicture(string path, IEnumerable productPictures, out int equalPictureId) + #endregion + + #region Methods + + public byte[] FindEqualPicture(string path, IEnumerable pictures, out int equalPictureId) { - return FindEqualPicture(File.ReadAllBytes(path), productPictures, out equalPictureId); + return FindEqualPicture(File.ReadAllBytes(path), pictures, out equalPictureId); } - public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable productPictures, out int equalPictureId) + public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable pictures, out int equalPictureId) { equalPictureId = 0; try { - foreach (var picture in productPictures) + foreach (var picture in pictures) { var otherPictureBinary = LoadPictureBinary(picture); @@ -187,11 +191,7 @@ public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable produc } } - #endregion - - #region Methods - - public virtual string GetPictureSeName(string name) + public virtual string GetPictureSeName(string name) { return SeoHelper.GetSeName(name, true, false); } diff --git a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs index 4392ca231a..b67c04fbe5 100644 --- a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs +++ b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs @@ -43,7 +43,7 @@ public void Execute(TaskExecutionContext ctx) { using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) { - var pictures = _pictureRepository.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); + var pictures = _pictureRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); foreach (var picture in pictures) { _pictureService.DeletePicture(picture); diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs index 7265b4faa4..3cf3bd7563 100644 --- a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs @@ -3,6 +3,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; @@ -11,13 +12,13 @@ namespace SmartStore.Services.Messages { - public partial interface IMessageTokenProvider + public partial interface IMessageTokenProvider { void AddStoreTokens(IList tokens, Store store); - void AddOrderTokens(IList tokens, Order order, int languageId); + void AddOrderTokens(IList tokens, Order order, Language language); - void AddShipmentTokens(IList tokens, Shipment shipment, int languageId); + void AddShipmentTokens(IList tokens, Shipment shipment, Language language); void AddOrderNoteTokens(IList tokens, OrderNote orderNote); @@ -37,9 +38,9 @@ public partial interface IMessageTokenProvider void AddNewsCommentTokens(IList tokens, NewsComment newsComment); - void AddProductTokens(IList tokens, Product product, int languageId); + void AddProductTokens(IList tokens, Product product, Language language); - void AddForumTokens(IList tokens, Forum forum, int languageId); + void AddForumTokens(IList tokens, Forum forum, Language language); void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, int? friendlyForumTopicPageIndex = null, int? appendedPostIdentifierAnchor = null); @@ -54,13 +55,10 @@ void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, string[] GetListOfAllowedTokens(); - //codehint: sm-add begin void AddBankConnectionTokens(IList tokens); void AddCompanyTokens(IList tokens); void AddContactDataTokens(IList tokens); - //codehint: sm-add end - } } diff --git a/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs index 34c4b8a7c1..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.Services.DataExchange.Import; namespace SmartStore.Services.Messages { - public partial interface INewsLetterSubscriptionService - { - - /// - /// Executes a bulk import of subscriber data from a CSV source - /// - /// The input file stream - /// The import result - ImportResult ImportSubscribers(Stream stream); - + public partial interface INewsLetterSubscriptionService + { /// /// Inserts a newsletter subscription /// @@ -37,12 +27,21 @@ public partial interface INewsLetterSubscriptionService /// if set to true [publish subscription events]. void DeleteNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true); - /// - /// Gets a newsletter subscription by newsletter subscription identifier - /// - /// The newsletter subscription identifier - /// NewsLetter subscription - NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId); + /// + /// Adds or deletes a newsletter subscription + /// + /// true add subscription, false delete + /// Email address + /// Store identifier + /// true added subscription, false removed subscription, null did nothing + bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId); + + /// + /// Gets a newsletter subscription by newsletter subscription identifier + /// + /// The newsletter subscription identifier + /// NewsLetter subscription + NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId); /// /// Gets a newsletter subscription by newsletter subscription GUID @@ -70,4 +69,4 @@ public partial interface INewsLetterSubscriptionService /// NewsLetterSubscription entity list IPagedList GetAllNewsLetterSubscriptions(string email, int pageIndex, int pageSize, bool showHidden = false, int storeId = 0); } -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs new file mode 100644 index 0000000000..979051d1d6 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs @@ -0,0 +1,154 @@ +using System; +using System.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Messages; +using SmartStore.Services.DataExchange.Import; + +namespace SmartStore.Services.Messages.Importer +{ + public class NewsLetterSubscriptionImporter : IEntityImporter + { + private readonly ICommonServices _services; + private readonly IRepository _subscriptionRepository; + + public NewsLetterSubscriptionImporter( + ICommonServices services, + IRepository subscriptionRepository) + { + _services = services; + _subscriptionRepository = subscriptionRepository; + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Email" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Email" }; + } + } + + public void Execute(ImportExecuteContext context) + { + var utcNow = DateTime.UtcNow; + var currentStoreId = _services.StoreContext.CurrentStore.Id; + + using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + var segmenter = context.DataSegmenter; + + context.Result.TotalRecords = segmenter.TotalRows; + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _subscriptionRepository.Context.DetachEntities(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + foreach (var row in batch) + { + try + { + var active = true; + var email = row.GetDataValue("Email"); + var storeId = row.GetDataValue("StoreId"); + + if (storeId == 0) + { + storeId = currentStoreId; + } + + if (row.HasDataValue("Active") && row.TryGetDataValue("Active", out active)) + { + } + else + { + active = true; // default + } + + if (email.IsEmpty()) + { + context.Result.AddWarning("Skipped empty email address", row.GetRowInfo(), "Email"); + continue; + } + + if (email.Length > 255) + { + context.Result.AddWarning("Skipped email address '{0}'. It exceeds the maximum allowed length of 255".FormatInvariant(email), row.GetRowInfo(), "Email"); + continue; + } + + if (!email.IsEmail()) + { + context.Result.AddWarning("Skipped invalid email address '{0}'".FormatInvariant(email), row.GetRowInfo(), "Email"); + continue; + } + + NewsLetterSubscription subscription = null; + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Email": + subscription = _subscriptionRepository.Table + .OrderBy(x => x.Id) + .FirstOrDefault(x => x.Email == email && x.StoreId == storeId); + break; + } + + if (subscription != null) + break; + } + + if (subscription == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + subscription = new NewsLetterSubscription + { + Active = active, + CreatedOnUtc = utcNow, + Email = email, + NewsLetterSubscriptionGuid = Guid.NewGuid(), + StoreId = storeId + }; + + _subscriptionRepository.Insert(subscription); + context.Result.NewRecords++; + } + else + { + subscription.Active = active; + + _subscriptionRepository.Update(subscription); + context.Result.ModifiedRecords++; + } + } + catch (Exception exception) + { + context.Result.AddError(exception.ToAllMessages(), row.GetRowInfo()); + } + } // for + + _subscriptionRepository.Context.SaveChanges(); + } // while + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs index b1e50412da..d0ff917259 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs @@ -13,6 +13,8 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; @@ -71,12 +73,14 @@ public partial class MessageTokenProvider : IMessageTokenProvider private readonly IUrlRecordService _urlRecordService; private readonly IStoreService _storeService; private readonly IGenericAttributeService _attrService; + private readonly IPictureService _pictureService; + private readonly MediaSettings _mediaSettings; - #endregion + #endregion - #region Ctor + #region Ctor - public MessageTokenProvider(ILanguageService languageService, + public MessageTokenProvider(ILanguageService languageService, ILocalizationService localizationService, IDateTimeHelper dateTimeHelper, IEmailAccountService emailAccountService, IPriceFormatter priceFormatter, ICurrencyService currencyService, IWebHelper webHelper, @@ -91,7 +95,9 @@ public MessageTokenProvider(ILanguageService languageService, ContactDataSettings contactDataSettings, ITopicService topicService, IDeliveryTimeService deliveryTimeService, IQuantityUnitService quantityUnitService, IUrlRecordService urlRecordService, IStoreService storeService, - IGenericAttributeService attrService) + IGenericAttributeService attrService, + IPictureService pictureService, + MediaSettings mediaSettings) { this._languageService = languageService; this._localizationService = localizationService; @@ -122,33 +128,87 @@ public MessageTokenProvider(ILanguageService languageService, this._urlRecordService = urlRecordService; this._storeService = storeService; this._attrService = attrService; + this._pictureService = pictureService; + this._mediaSettings = mediaSettings; } - #endregion + #endregion - #region Utilities + #region Utilities - /// - /// Convert a collection to a HTML table - /// - /// Order - /// Language identifier - /// HTML table of products - protected virtual string ProductListToHtmlTable(Order order, int languageId) - { - var result = ""; + protected virtual Picture GetPictureFor(Product product, string attributesXml) + { + Picture picture = null; + + if (attributesXml.HasValue()) + { + var combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, attributesXml); + + if (combination != null) + { + var picturesIds = combination.GetAssignedPictureIds(); + if (picturesIds != null && picturesIds.Length > 0) + picture = _pictureService.GetPictureById(picturesIds[0]); + } + } + + if (picture == null) + { + picture = _pictureService.GetPicturesByProductId(product.Id, 1).FirstOrDefault(); + } + + if (picture == null && !product.VisibleIndividually && product.ParentGroupedProductId > 0) + { + picture = _pictureService.GetPicturesByProductId(product.ParentGroupedProductId, 1).FirstOrDefault(); + } + + return picture; + } + + protected virtual string ProductPictureToHtml(Picture picture, Language language, string productName, string productUrl, string storeLocation) + { + if (picture != null && _mediaSettings.MessageProductThumbPictureSize > 0) + { + var imageUrl = _pictureService.GetPictureUrl(picture, _mediaSettings.MessageProductThumbPictureSize, false, storeLocation); + if (imageUrl.HasValue()) + { + var title = _localizationService.GetResource("Media.Product.ImageLinkTitleFormat", language.Id).FormatInvariant(productName); + var alternate = _localizationService.GetResource("Media.Product.ImageAlternateTextFormat", language.Id).FormatInvariant(productName); - var language = _languageService.GetLanguageById(languageId); + var polaroid = "padding: 3px; background-color: #fff; border: 1px solid #ccc; border: 1px solid rgba(0,0,0,.2);"; + var style = "max-width: {0}px; max-height: {0}px; {1}".FormatInvariant(_mediaSettings.MessageProductThumbPictureSize, polaroid); + var image = "\"{1}\"".FormatInvariant(imageUrl, alternate, title, style); + + if (productUrl.IsEmpty()) + return image; + + return "{1}".FormatInvariant(productUrl, image); + } + } + return ""; + } + + /// + /// Convert a collection to a HTML table + /// + /// Order + /// Language identifier + /// HTML table of products + protected virtual string ProductListToHtmlTable(Order order, Language language) + { var sb = new StringBuilder(); - sb.AppendLine(""); + var storeLocation = _webHelper.GetStoreLocation(false); + + sb.AppendLine("
"); #region Products - sb.AppendLine(string.Format("", _templatesSettings.Color1)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Price", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Total", languageId))); + + sb.AppendLine(string.Format("", _templatesSettings.Color1)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Price", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Total", language.Id))); sb.AppendLine(""); var table = order.OrderItems.ToList(); @@ -161,11 +221,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) DeliveryTime deliveryTime = null; - // merging attribute combination data required? - if (_catalogSettings.ShowProductSku || (_shoppingCartSettings.ShowDeliveryTimes && product.IsShipEnabled)) - { - product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser); - } + product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser); if (_shoppingCartSettings.ShowDeliveryTimes && product.IsShipEnabled) { @@ -173,16 +229,29 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) } sb.AppendLine(string.Format("", _templatesSettings.Color2)); - //product name - string productName = product.GetLocalized(x => x.Name, languageId); + + var productName = product.GetLocalized(x => x.Name, language.Id); + var productUrl = _productAttributeParser.GetProductUrlWithAttributes(orderItem.AttributesXml, product.Id, product.GetSeName()); + + sb.AppendLine(""); @@ -237,9 +306,9 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine(string.Format("", unitPriceStr)); var quantityUnit = _quantityUnitService.GetQuantityUnitById(product.QuantityUnitId); + sb.AppendLine(string.Format("", - orderItem.Quantity, - quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name))); + orderItem.Quantity, quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name))); string priceStr = string.Empty; switch (order.CustomerTaxDisplayType) @@ -261,6 +330,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine(""); } + #endregion #region Checkout Attributes @@ -285,6 +355,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) string cusTaxTotal = string.Empty; string cusDiscount = string.Empty; string cusTotal = string.Empty; + //subtotal, shipping, payment method fee switch (order.CustomerTaxDisplayType) { @@ -383,41 +454,34 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); cusTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); - - - //subtotal - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.SubTotal", languageId), cusSubTotal)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotal", languageId), cusSubTotal)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotal", language.Id), cusSubTotal)); //discount (applied to order subtotal) if (dislaySubTotalDiscount) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.SubTotalDiscount", languageId), cusSubTotalDiscount)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotalDiscount", languageId), cusSubTotalDiscount)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotalDiscount", language.Id), cusSubTotalDiscount)); } //shipping if (dislayShipping) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.Shipping", languageId), cusShipTotal)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Shipping", languageId), cusShipTotal)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Shipping", language.Id), cusShipTotal)); } //payment method fee if (displayPaymentMethodFee) { - string paymentMethodFeeTitle = _localizationService.GetResource("Messages.Order.PaymentMethodAdditionalFee", languageId); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); - sb.AppendLine(string.Format("", paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); + string paymentMethodFeeTitle = _localizationService.GetResource("Messages.Order.PaymentMethodAdditionalFee", language.Id); + + sb.AppendLine(string.Format("", paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); } //tax if (displayTax) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.Tax", languageId), cusTaxTotal)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Tax", languageId), cusTaxTotal)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Tax", language.Id), cusTaxTotal)); } if (displayTaxRates) { @@ -425,45 +489,50 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) { string taxRate = String.Format(_localizationService.GetResource("Messages.Order.TaxRateLine"), _priceFormatter.FormatTaxRate(item.Key)); string taxValue = _priceFormatter.FormatPrice(item.Value, true, order.CustomerCurrencyCode, false, language); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, taxRate, taxValue)); - sb.AppendLine(string.Format("", taxRate, taxValue)); + + sb.AppendLine(string.Format("", taxRate, taxValue)); } } //discount if (dislayDiscount) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.TotalDiscount", languageId), cusDiscount)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.TotalDiscount", languageId), cusDiscount)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.TotalDiscount", language.Id), cusDiscount)); } //gift cards var gcuhC = order.GiftCardUsageHistory; foreach (var gcuh in gcuhC) { - string giftCardText = String.Format(_localizationService.GetResource("Messages.Order.GiftCardInfo", languageId), HttpUtility.HtmlEncode(gcuh.GiftCard.GiftCardCouponCode)); + string giftCardText = String.Format(_localizationService.GetResource("Messages.Order.GiftCardInfo", language.Id), HttpUtility.HtmlEncode(gcuh.GiftCard.GiftCardCouponCode)); string giftCardAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(gcuh.UsedValue, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, giftCardText, giftCardAmount)); - sb.AppendLine(string.Format("", giftCardText, giftCardAmount)); + + var remaining = _currencyService.ConvertCurrency(gcuh.GiftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); + var remainingFormatted = _priceFormatter.FormatPrice(remaining, true, false); + var remainingText = _localizationService.GetResource("ShoppingCart.Totals.GiftCardInfo.Remaining", language.Id).FormatInvariant(remainingFormatted); + + sb.AppendLine(string.Format("", + giftCardText, remainingText, giftCardAmount)); } //reward points if (order.RedeemedRewardPointsEntry != null) { - string rpTitle = string.Format(_localizationService.GetResource("Messages.Order.RewardPoints", languageId), -order.RedeemedRewardPointsEntry.Points); + string rpTitle = string.Format(_localizationService.GetResource("Messages.Order.RewardPoints", language.Id), -order.RedeemedRewardPointsEntry.Points); string rpAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(order.RedeemedRewardPointsEntry.UsedAmount, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, rpTitle, rpAmount)); - sb.AppendLine(string.Format("", rpTitle, rpAmount)); + + sb.AppendLine(string.Format("", rpTitle, rpAmount)); } //total - sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.OrderTotal", languageId), cusTotal)); - #endregion + sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.OrderTotal", language.Id), cusTotal)); + + #endregion sb.AppendLine("
{0}{0}{0}{0}
{0}{0}{0}{0}
"); + + if (_mediaSettings.MessageProductThumbPictureSize > 0) + { + var pictureHtml = ProductPictureToHtml(GetPictureFor(product, orderItem.AttributesXml), language, productName, productUrl, storeLocation); + if (pictureHtml.HasValue()) + { + sb.AppendLine("
{0}
".FormatInvariant(pictureHtml)); + } + } + + sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); - sb.AppendLine("
" + HttpUtility.HtmlEncode(productName)); - //add download link - if (_downloadService.IsDownloadAllowed(orderItem)) + //add download link + if (_downloadService.IsDownloadAllowed(orderItem)) { //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - string downloadUrl = string.Format("{0}download/getdownload/{1}", _webHelper.GetStoreLocation(false), orderItem.OrderItemGuid); - string downloadLink = string.Format("{1}", downloadUrl, _localizationService.GetResource("Messages.Order.Product(s).Download", languageId)); + string downloadUrl = string.Format("{0}download/getdownload/{1}", storeLocation, orderItem.OrderItemGuid); + string downloadLink = string.Format("{1}", downloadUrl, _localizationService.GetResource("Messages.Order.Product(s).Download", language.Id)); sb.AppendLine("  ("); sb.AppendLine(downloadLink); sb.AppendLine(")"); @@ -195,7 +264,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine("
"); sb.AppendLine("
"); - sb.AppendLine("" + _localizationService.GetResource("Products.DeliveryTime") + ""); + sb.AppendLine("" + _localizationService.GetResource("Products.DeliveryTime", language.Id) + ""); sb.AppendLine(""); sb.AppendLine("" + deliveryTimeName + ""); sb.AppendLine("
"); @@ -213,7 +282,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) if (!String.IsNullOrEmpty(product.Sku)) { sb.AppendLine("
"); - sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", languageId), HttpUtility.HtmlEncode(product.Sku))); + sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", language.Id), HttpUtility.HtmlEncode(product.Sku))); } } sb.AppendLine("
{0}{0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0} {1}
 {1} {2}
 {0} {1}
 {0}
{1}
{2}
 {1} {2}
 {0} {1}
 {0} {1}
 {1}{2}
 {1}{2}
"); - result = sb.ToString(); - return result; - } + + return sb.ToString(); + } /// /// Convert a collection to a HTML table @@ -471,17 +540,17 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) /// Shipment /// Language identifier /// HTML table of products - protected virtual string ProductListToHtmlTable(Shipment shipment, int languageId) + protected virtual string ProductListToHtmlTable(Shipment shipment, Language language) { - var result = ""; - var sb = new StringBuilder(); + sb.AppendLine(""); #region Products + sb.AppendLine(string.Format("", _templatesSettings.Color1)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", languageId))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", language.Id))); sb.AppendLine(""); var table = shipment.ShipmentItems.ToList(); @@ -497,12 +566,15 @@ protected virtual string ProductListToHtmlTable(Shipment shipment, int languageI continue; sb.AppendLine(string.Format("", _templatesSettings.Color2)); - //product name - string productName = product.GetLocalized(x => x.Name, languageId); - sb.AppendLine(""); @@ -524,12 +596,13 @@ protected virtual string ProductListToHtmlTable(Shipment shipment, int languageI sb.AppendLine(""); } + #endregion sb.AppendLine("
{0}{0}{0}{0}
" + HttpUtility.HtmlEncode(productName)); - //attributes - if (!String.IsNullOrEmpty(orderItem.AttributeDescription)) + var productName = product.GetLocalized(x => x.Name, language.Id); + var productUrl = _productAttributeParser.GetProductUrlWithAttributes(orderItem.AttributesXml, product.Id, product.GetSeName()); + + sb.AppendLine(""); + sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); + + //attributes + if (!String.IsNullOrEmpty(orderItem.AttributeDescription)) { sb.AppendLine("
"); sb.AppendLine(orderItem.AttributeDescription); @@ -515,7 +587,7 @@ protected virtual string ProductListToHtmlTable(Shipment shipment, int languageI if (!String.IsNullOrEmpty(product.Sku)) { sb.AppendLine("
"); - sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", languageId), HttpUtility.HtmlEncode(product.Sku))); + sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", language.Id), HttpUtility.HtmlEncode(product.Sku))); } } sb.AppendLine("
"); - result = sb.ToString(); - return result; - } + + return sb.ToString(); + } protected virtual string TopicToHtml(string systemName, int languageId) { @@ -722,7 +795,7 @@ public virtual void AddContactDataTokens(IList tokens) tokens.Add(new Token("Contact.ContactEmailAddress", _contactDataSettings.ContactEmailAddress)); } - public virtual void AddOrderTokens(IList tokens, Order order, int languageId) + public virtual void AddOrderTokens(IList tokens, Order order, Language language) { tokens.Add(new Token("Order.ID", order.Id.ToString())); tokens.Add(new Token("Order.OrderNumber", order.GetOrderNumber())); @@ -757,15 +830,22 @@ public virtual void AddOrderTokens(IList tokens, Order order, int languag tokens.Add(new Token("Order.ShippingZipPostalCode", order.ShippingAddress != null ? order.ShippingAddress.ZipPostalCode : "")); tokens.Add(new Token("Order.ShippingCountry", order.ShippingAddress != null && order.ShippingAddress.Country != null ? order.ShippingAddress.Country.GetLocalized(x => x.Name) : "")); + string paymentMethodName = null; var paymentMethod = _providerManager.GetProvider(order.PaymentMethodSystemName); - var paymentMethodName = paymentMethod != null ? GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName) : order.PaymentMethodSystemName; - - tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); + if (paymentMethod != null) + { + paymentMethodName = GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName); + } + if (paymentMethodName.IsEmpty()) + { + paymentMethodName = order.PaymentMethodSystemName; + } + + tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); tokens.Add(new Token("Order.VatNumber", order.VatNumber)); - tokens.Add(new Token("Order.Product(s)", ProductListToHtmlTable(order, languageId), true)); + tokens.Add(new Token("Order.Product(s)", ProductListToHtmlTable(order, language), true)); tokens.Add(new Token("Order.CustomerComment", order.CustomerOrderComment, true)); - var language = _languageService.GetLanguageById(languageId); if (language != null && !String.IsNullOrEmpty(language.LanguageCulture)) { DateTime createdOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, TimeZoneInfo.Utc, _dateTimeHelper.GetCustomerTimeZone(order.Customer)); @@ -779,8 +859,8 @@ public virtual void AddOrderTokens(IList tokens, Order order, int languag // TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) tokens.Add(new Token("Order.OrderURLForCustomer", string.Format("{0}order/details/{1}", _webHelper.GetStoreLocation(false), order.Id), true)); - tokens.Add(new Token("Order.Disclaimer", TopicToHtml("Disclaimer", languageId), true)); - tokens.Add(new Token("Order.ConditionsOfUse", TopicToHtml("ConditionsOfUse", languageId), true)); + tokens.Add(new Token("Order.Disclaimer", TopicToHtml("Disclaimer", language.Id), true)); + tokens.Add(new Token("Order.ConditionsOfUse", TopicToHtml("ConditionsOfUse", language.Id), true)); //event notification _eventPublisher.EntityTokensAdded(order, tokens); @@ -803,11 +883,11 @@ private string GetLocalizedValue(ProviderMetadata metadata, string propertyName, return result; } - public virtual void AddShipmentTokens(IList tokens, Shipment shipment, int languageId) + public virtual void AddShipmentTokens(IList tokens, Shipment shipment, Language language) { tokens.Add(new Token("Shipment.ShipmentNumber", shipment.Id.ToString())); tokens.Add(new Token("Shipment.TrackingNumber", shipment.TrackingNumber)); - tokens.Add(new Token("Shipment.Product(s)", ProductListToHtmlTable(shipment, languageId), true)); + tokens.Add(new Token("Shipment.Product(s)", ProductListToHtmlTable(shipment, language), true)); tokens.Add(new Token("Shipment.URLForCustomer", string.Format("{0}order/shipmentdetails/{1}", _webHelper.GetStoreLocation(false), shipment.Id), true)); //event notification @@ -847,14 +927,27 @@ public virtual void AddReturnRequestTokens(IList tokens, ReturnRequest re public virtual void AddGiftCardTokens(IList tokens, GiftCard giftCard) { - tokens.Add(new Token("GiftCard.SenderName", giftCard.SenderName)); + var order = (giftCard.PurchasedWithOrderItem != null ? giftCard.PurchasedWithOrderItem.Order : null); + + if (order != null) + { + var remainingAmount = _currencyService.ConvertCurrency(giftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); + + tokens.Add(new Token("GiftCard.RemainingAmount", _priceFormatter.FormatPrice(remainingAmount, true, false))); + } + else + { + tokens.Add(new Token("GiftCard.RemainingAmount", "")); + } + + tokens.Add(new Token("GiftCard.SenderName", giftCard.SenderName)); tokens.Add(new Token("GiftCard.SenderEmail", giftCard.SenderEmail)); tokens.Add(new Token("GiftCard.RecipientName", giftCard.RecipientName)); tokens.Add(new Token("GiftCard.RecipientEmail", giftCard.RecipientEmail)); tokens.Add(new Token("GiftCard.Amount", _priceFormatter.FormatPrice(giftCard.Amount, true, false))); - tokens.Add(new Token("GiftCard.CouponCode", giftCard.GiftCardCouponCode)); + tokens.Add(new Token("GiftCard.CouponCode", giftCard.GiftCardCouponCode)); - var giftCardMesage = !String.IsNullOrWhiteSpace(giftCard.Message) ? + var giftCardMesage = !String.IsNullOrWhiteSpace(giftCard.Message) ? HtmlUtils.FormatText(giftCard.Message, false, true, false, false, false, false) : ""; tokens.Add(new Token("GiftCard.Message", giftCardMesage, true)); @@ -929,20 +1022,36 @@ public virtual void AddNewsCommentTokens(IList tokens, NewsComment newsCo _eventPublisher.EntityTokensAdded(newsComment, tokens); } - public virtual void AddProductTokens(IList tokens, Product product, int languageId) + public virtual void AddProductTokens(IList tokens, Product product, Language language) { + // TODO: add a method for getting URL (use routing because it handles all SEO friendly URLs) + var storeLocation = _webHelper.GetStoreLocation(false); + var productUrl = storeLocation + product.GetSeName(); + var productName = product.GetLocalized(x => x.Name, language.Id); + tokens.Add(new Token("Product.ID", product.Id.ToString())); tokens.Add(new Token("Product.Sku", product.Sku)); - tokens.Add(new Token("Product.Name", product.GetLocalized(x => x.Name, languageId))); - tokens.Add(new Token("Product.ShortDescription", product.GetLocalized(x => x.ShortDescription, languageId), true)); + tokens.Add(new Token("Product.Name", productName)); + tokens.Add(new Token("Product.ShortDescription", product.GetLocalized(x => x.ShortDescription, language.Id), true)); tokens.Add(new Token("Product.StockQuantity", product.StockQuantity.ToString())); - - // TODO: add a method for getting URL (use routing because it handles all SEO friendly URLs) - var productUrl = string.Format("{0}{1}", _webHelper.GetStoreLocation(false), product.GetSeName()); tokens.Add(new Token("Product.ProductURLForCustomer", productUrl, true)); - //event notification - _eventPublisher.EntityTokensAdded(product, tokens); + var currency = _workContext.WorkingCurrency; + + var additionalShippingCharge = _currencyService.ConvertFromPrimaryStoreCurrency(product.AdditionalShippingCharge, currency); + var additionalShippingChargeFormatted = _priceFormatter.FormatPrice(additionalShippingCharge, false, currency.CurrencyCode, false, language); + + tokens.Add(new Token("Product.AdditionalShippingCharge", additionalShippingChargeFormatted)); + + if (_mediaSettings.MessageProductThumbPictureSize > 0) + { + var pictureHtml = ProductPictureToHtml(GetPictureFor(product, null), language, productName, productUrl, storeLocation); + + tokens.Add(new Token("Product.Thumbnail", pictureHtml, true)); + } + + //event notification + _eventPublisher.EntityTokensAdded(product, tokens); } public virtual void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, @@ -972,12 +1081,12 @@ public virtual void AddForumPostTokens(IList tokens, ForumPost forumPost) _eventPublisher.EntityTokensAdded(forumPost, tokens); } - public virtual void AddForumTokens(IList tokens, Forum forum, int languageId) + public virtual void AddForumTokens(IList tokens, Forum forum, Language language) { //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - var forumUrl = string.Format("{0}boards/forum/{1}/{2}", _webHelper.GetStoreLocation(false), forum.Id, forum.GetSeName(languageId)); + var forumUrl = string.Format("{0}boards/forum/{1}/{2}", _webHelper.GetStoreLocation(false), forum.Id, forum.GetSeName(language.Id)); tokens.Add(new Token("Forums.ForumURL", forumUrl, true)); - tokens.Add(new Token("Forums.ForumName", forum.GetLocalized(x => x.Name, languageId))); + tokens.Add(new Token("Forums.ForumName", forum.GetLocalized(x => x.Name, language.Id))); //event notification _eventPublisher.EntityTokensAdded(forum, tokens); @@ -1075,7 +1184,9 @@ public virtual string[] GetListOfAllowedTokens() "%Product.ShortDescription%", "%Product.ProductURLForCustomer%", "%Product.StockQuantity%", - "%RecurringPayment.ID%", + "%Product.AdditionalShippingCharge%", + "%Product.Thumbnail%", + "%RecurringPayment.ID%", "%Shipment.ShipmentNumber%", "%Shipment.TrackingNumber%", "%Shipment.Product(s)%", @@ -1092,8 +1203,9 @@ public virtual string[] GetListOfAllowedTokens() "%GiftCard.SenderEmail%", "%GiftCard.RecipientName%", "%GiftCard.RecipientEmail%", - "%GiftCard.Amount%", - "%GiftCard.CouponCode%", + "%GiftCard.Amount%", + "%GiftCard.RemainingAmount%", + "%GiftCard.CouponCode%", "%GiftCard.Message%", "%Customer.Email%", "%Customer.Username%", diff --git a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs index f07fc77139..dde1d076c9 100644 --- a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs @@ -1,148 +1,25 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Events; -using SmartStore.Services.Stores; -using SmartStore.Services.DataExchange.Import; namespace SmartStore.Services.Messages { - - public class NewsLetterSubscriptionService : INewsLetterSubscriptionService + public class NewsLetterSubscriptionService : INewsLetterSubscriptionService { private readonly IEventPublisher _eventPublisher; private readonly IDbContext _context; private readonly IRepository _subscriptionRepository; - private readonly IStoreService _storeService; public NewsLetterSubscriptionService(IDbContext context, IRepository subscriptionRepository, - IEventPublisher eventPublisher, - IStoreService storeService) + IEventPublisher eventPublisher) { _context = context; _subscriptionRepository = subscriptionRepository; _eventPublisher = eventPublisher; - _storeService = storeService; - } - - public ImportResult ImportSubscribers(Stream stream) - { - Guard.ArgumentNotNull(() => stream); - - var result = new ImportResult(); - var toAdd = new List(); - var toUpdate = new List(); - - using (var scope = new DbContextScope(ctx: _context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) - { - using (var reader = new StreamReader(stream)) - { - while (!reader.EndOfStream) - { - string line = reader.ReadLine(); - if (line.IsEmpty()) - { - continue; - } - string[] tmp = line.Split(','); - - var email = ""; - bool isActive = true; - int storeId = 0; - - // parse - if (tmp.Length == 1) - { - // "email" only - email = tmp[0].Trim(); - } - else if (tmp.Length == 2) - { - // "email" and "active" fields specified - email = tmp[0].Trim(); - isActive = Boolean.Parse(tmp[1].Trim()); - } - else if (tmp.Length == 3) - { - email = tmp[0].Trim(); - isActive = Boolean.Parse(tmp[1].Trim()); - storeId = int.Parse(tmp[2].Trim()); - } - else - { - throw new SmartException("Wrong file format (expected comma separated entries 'Email' and optionally 'IsActive')"); - } - - result.TotalRecords++; - - if (email.Length > 255) - { - result.AddWarning("The emal address '{0}' exceeds the maximun allowed length of 255.".FormatInvariant(email)); - continue; - } - - if (!email.IsEmail()) - { - result.AddWarning("'{0}' is not a valid email address.".FormatInvariant(email)); - continue; - } - - if (storeId == 0) - { - storeId = _storeService.GetAllStores().First().Id; - } - - // import - var subscription = (from nls in _subscriptionRepository.Table - where nls.Email == email && nls.StoreId == storeId - orderby nls.Id - select nls).FirstOrDefault(); - - if (subscription != null) - { - subscription.Active = isActive; - - toUpdate.Add(subscription); - result.ModifiedRecords++; - } - else - { - subscription = new NewsLetterSubscription - { - Active = isActive, - CreatedOnUtc = DateTime.UtcNow, - Email = email, - NewsLetterSubscriptionGuid = Guid.NewGuid(), - StoreId = storeId - }; - - toAdd.Add(subscription); - result.NewRecords++; - } - } - } - - // insert new subscribers - _subscriptionRepository.AutoCommitEnabled = true; - _subscriptionRepository.InsertRange(toAdd, 500); - toAdd.Clear(); - - // update modified subscribers - _subscriptionRepository.AutoCommitEnabled = null; - toUpdate.Each(x => - { - _subscriptionRepository.Update(x); - }); - _subscriptionRepository.Context.SaveChanges(); - toUpdate.Clear(); - } - - return result; } /// @@ -236,7 +113,8 @@ public void UpdateNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr /// if set to true [publish subscription events]. public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true) { - if (newsLetterSubscription == null) throw new ArgumentNullException("newsLetterSubscription"); + if (newsLetterSubscription == null) + throw new ArgumentNullException("newsLetterSubscription"); _subscriptionRepository.Delete(newsLetterSubscription); @@ -247,12 +125,52 @@ public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLett _eventPublisher.EntityDeleted(newsLetterSubscription); } - /// - /// Gets a newsletter subscription by newsletter subscription identifier - /// - /// The newsletter subscription identifier - /// NewsLetter subscription - public virtual NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId) + public virtual bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId) + { + bool? result = null; + + if (email.IsEmail()) + { + var newsletter = GetNewsLetterSubscriptionByEmail(email, storeId); + if (newsletter != null) + { + if (add) + { + newsletter.Active = true; + UpdateNewsLetterSubscription(newsletter); + result = true; + } + else + { + DeleteNewsLetterSubscription(newsletter); + result = false; + } + } + else + { + if (add) + { + InsertNewsLetterSubscription(new NewsLetterSubscription + { + NewsLetterSubscriptionGuid = Guid.NewGuid(), + Email = email, + Active = true, + CreatedOnUtc = DateTime.UtcNow, + StoreId = storeId + }); + result = true; + } + } + } + return result; + } + + /// + /// Gets a newsletter subscription by newsletter subscription identifier + /// + /// The newsletter subscription identifier + /// NewsLetter subscription + public virtual NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId) { if (newsLetterSubscriptionId == 0) return null; diff --git a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs index 64adb4aa26..1f2746245e 100644 --- a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs +++ b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs @@ -8,12 +8,14 @@ using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; +using SmartStore.Core.Localization; using SmartStore.Services.Customers; using SmartStore.Services.Localization; using SmartStore.Services.Media; @@ -71,13 +73,17 @@ public WorkflowMessageService( this._workContext = workContext; this._httpRequest = httpRequest; this._downloadServioce = downloadServioce; - } - #endregion + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion - #region Utilities + #region Utilities - protected int SendNotification( + protected int SendNotification( MessageTemplate messageTemplate, EmailAccount emailAccount, int languageId, @@ -232,7 +238,7 @@ private string GetDisplayNameForCustomer(Customer customer) return name ?? customer.Username.EmptyNull(); } - protected int EnsureLanguageIsActive(int languageId, int storeId) + protected Language EnsureLanguageIsActive(int languageId, int storeId) { //load language by specified ID var language = _languageService.GetLanguageById(languageId); @@ -249,8 +255,9 @@ protected int EnsureLanguageIsActive(int languageId, int storeId) } if (language == null) - throw new Exception("No active language could be loaded"); - return language.Id; + throw new SmartException(T("Common.Error.NoActiveLanguage")); + + return language; } #endregion @@ -271,7 +278,7 @@ public virtual int SendCustomerRegisteredNotificationMessage(Customer customer, throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewCustomer.Notification", store.Id); if (messageTemplate == null) @@ -285,17 +292,14 @@ public virtual int SendCustomerRegisteredNotificationMessage(Customer customer, //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -310,7 +314,7 @@ public virtual int SendCustomerWelcomeMessage(Customer customer, int languageId) throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.WelcomeMessage", store.Id); if (messageTemplate == null) @@ -324,12 +328,11 @@ public virtual int SendCustomerWelcomeMessage(Customer customer, int languageId) //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -344,7 +347,7 @@ public virtual int SendCustomerEmailValidationMessage(Customer customer, int lan throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.EmailValidationMessage", store.Id); if (messageTemplate == null) @@ -358,12 +361,11 @@ public virtual int SendCustomerEmailValidationMessage(Customer customer, int lan //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -378,7 +380,7 @@ public virtual int SendCustomerPasswordRecoveryMessage(Customer customer, int la throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.PasswordRecovery", store.Id); if (messageTemplate == null) @@ -392,12 +394,11 @@ public virtual int SendCustomerPasswordRecoveryMessage(Customer customer, int la //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -416,7 +417,7 @@ public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int langua throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderPlaced.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -425,13 +426,13 @@ public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int langua //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; @@ -443,10 +444,7 @@ public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int langua replyToName += ", " + order.BillingAddress.Company; } - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyToEmail, replyToName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyToEmail, replyToName); } /// @@ -461,7 +459,7 @@ public virtual int SendOrderPlacedCustomerNotification(Order order, int language throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderPlaced.CustomerNotification", store.Id); if (messageTemplate == null) @@ -470,7 +468,7 @@ public virtual int SendOrderPlacedCustomerNotification(Order order, int language //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); _messageTokenProvider.AddCompanyTokens(tokens); @@ -480,12 +478,11 @@ public virtual int SendOrderPlacedCustomerNotification(Order order, int language // event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -501,10 +498,10 @@ public virtual int SendShipmentSentCustomerNotification(Shipment shipment, int l var order = shipment.Order; if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("ShipmentSent.CustomerNotification", store.Id); if (messageTemplate == null) @@ -513,19 +510,18 @@ public virtual int SendShipmentSentCustomerNotification(Shipment shipment, int l //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, languageId); - _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, languageId); + _messageTokenProvider.AddShipmentTokens(tokens, shipment, language); + _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, language); _messageTokenProvider.AddCustomerTokens(tokens, shipment.Order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -541,10 +537,10 @@ public virtual int SendShipmentDeliveredCustomerNotification(Shipment shipment, var order = shipment.Order; if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("ShipmentDelivered.CustomerNotification", store.Id); if (messageTemplate == null) @@ -553,19 +549,18 @@ public virtual int SendShipmentDeliveredCustomerNotification(Shipment shipment, //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, languageId); - _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, languageId); + _messageTokenProvider.AddShipmentTokens(tokens, shipment, language); + _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, language); _messageTokenProvider.AddCustomerTokens(tokens, shipment.Order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -580,7 +575,7 @@ public virtual int SendOrderCompletedCustomerNotification(Order order, int langu throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderCompleted.CustomerNotification", store.Id); if (messageTemplate == null) @@ -589,7 +584,7 @@ public virtual int SendOrderCompletedCustomerNotification(Order order, int langu //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); _messageTokenProvider.AddCompanyTokens(tokens); @@ -599,12 +594,11 @@ public virtual int SendOrderCompletedCustomerNotification(Order order, int langu //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -619,7 +613,7 @@ public virtual int SendOrderCancelledCustomerNotification(Order order, int langu throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderCancelled.CustomerNotification", store.Id); if (messageTemplate == null) @@ -628,18 +622,17 @@ public virtual int SendOrderCancelledCustomerNotification(Order order, int langu //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -656,7 +649,7 @@ public virtual int SendNewOrderNoteAddedCustomerNotification(OrderNote orderNote var order = orderNote.Order; var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.NewOrderNote", store.Id); if (messageTemplate == null) @@ -666,18 +659,17 @@ public virtual int SendNewOrderNoteAddedCustomerNotification(OrderNote orderNote var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddOrderNoteTokens(tokens, orderNote); - _messageTokenProvider.AddOrderTokens(tokens, orderNote.Order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, orderNote.Order, language); _messageTokenProvider.AddCustomerTokens(tokens, orderNote.Order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -692,7 +684,7 @@ public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(Recurring throw new ArgumentNullException("recurringPayment"); var store = _storeService.GetStoreById(recurringPayment.InitialOrder.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("RecurringPaymentCancelled.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -701,20 +693,18 @@ public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(Recurring //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, recurringPayment.InitialOrder, languageId); + _messageTokenProvider.AddOrderTokens(tokens, recurringPayment.InitialOrder, language); _messageTokenProvider.AddCustomerTokens(tokens, recurringPayment.InitialOrder.Customer); _messageTokenProvider.AddRecurringPaymentTokens(tokens, recurringPayment); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -727,14 +717,13 @@ public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(Recurring /// Newsletter subscription /// Language identifier /// Queued email identifier - public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - int languageId) + public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, int languageId) { if (subscription == null) throw new ArgumentNullException("subscription"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.ActivationMessage", store.Id); if (messageTemplate == null) @@ -748,12 +737,11 @@ public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscri //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = subscription.Email; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -762,14 +750,13 @@ public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscri /// Newsletter subscription /// Language identifier /// Queued email identifier - public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - int languageId) + public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, int languageId) { if (subscription == null) throw new ArgumentNullException("subscription"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.DeactivationMessage", store.Id); if (messageTemplate == null) @@ -782,12 +769,11 @@ public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubsc //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = subscription.Email; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -814,7 +800,7 @@ public virtual int SendProductEmailAFriendMessage(Customer customer, int languag throw new ArgumentNullException("product"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Service.EmailAFriend", store.Id); if (messageTemplate == null) @@ -824,19 +810,19 @@ public virtual int SendProductEmailAFriendMessage(Customer customer, int languag var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); + tokens.Add(new Token("EmailAFriend.PersonalMessage", personalMessage, true)); tokens.Add(new Token("EmailAFriend.Email", customerEmail)); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = friendsEmail; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } public virtual int SendProductQuestionMessage(Customer customer, int languageId, Product product, @@ -852,7 +838,7 @@ public virtual int SendProductQuestionMessage(Customer customer, int languageId, throw new ArgumentNullException("product"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Product.AskQuestion", store.Id); if (messageTemplate == null) @@ -861,7 +847,7 @@ public virtual int SendProductQuestionMessage(Customer customer, int languageId, var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); tokens.Add(new Token("ProductQuestion.Message", question, true)); tokens.Add(new Token("ProductQuestion.SenderEmail", senderEmail)); @@ -871,11 +857,11 @@ public virtual int SendProductQuestionMessage(Customer customer, int languageId, //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName, senderEmail, senderName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, senderEmail, senderName); } /// @@ -894,7 +880,7 @@ public virtual int SendWishlistEmailAFriendMessage(Customer customer, int langua throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Wishlist.EmailAFriend", store.Id); if (messageTemplate == null) @@ -910,12 +896,11 @@ public virtual int SendWishlistEmailAFriendMessage(Customer customer, int langua //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = friendsEmail; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -935,7 +920,7 @@ public virtual int SendNewReturnRequestStoreOwnerNotification(ReturnRequest retu throw new ArgumentNullException("returnRequest"); var store = _storeService.GetStoreById(orderItem.Order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewReturnRequest.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -950,17 +935,14 @@ public virtual int SendNewReturnRequestStoreOwnerNotification(ReturnRequest retu //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(returnRequest.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -976,7 +958,7 @@ public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequ throw new ArgumentNullException("returnRequest"); var store = _storeService.GetStoreById(orderItem.Order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("ReturnRequestStatusChanged.CustomerNotification", store.Id); if (messageTemplate == null) @@ -991,14 +973,14 @@ public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequ //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = returnRequest.Customer.FindEmail(); var toName = returnRequest.Customer.GetFullName(); if (toEmail.IsEmpty()) return 0; - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -1013,8 +995,7 @@ public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequ /// Forum /// Message language identifier /// Queued email identifier - public int SendNewForumTopicMessage(Customer customer, - ForumTopic forumTopic, Forum forum, int languageId) + public int SendNewForumTopicMessage(Customer customer, ForumTopic forumTopic, Forum forum, int languageId) { if (customer == null) { @@ -1022,6 +1003,7 @@ public int SendNewForumTopicMessage(Customer customer, } var store = _storeContext.CurrentStore; + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Forums.NewForumTopic", store.Id); if (messageTemplate == null) @@ -1034,16 +1016,16 @@ public int SendNewForumTopicMessage(Customer customer, _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); _messageTokenProvider.AddForumTopicTokens(tokens, forumTopic); - _messageTokenProvider.AddForumTokens(tokens, forumTopic.Forum, languageId); + _messageTokenProvider.AddForumTokens(tokens, forumTopic.Forum, language); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -1056,9 +1038,7 @@ public int SendNewForumTopicMessage(Customer customer, /// Friendly (starts with 1) forum topic page to use for URL generation /// Message language identifier /// Queued email identifier - public int SendNewForumPostMessage(Customer customer, - ForumPost forumPost, ForumTopic forumTopic, - Forum forum, int friendlyForumTopicPageIndex, int languageId) + public int SendNewForumPostMessage(Customer customer, ForumPost forumPost, ForumTopic forumTopic, Forum forum, int friendlyForumTopicPageIndex, int languageId) { if (customer == null) { @@ -1066,6 +1046,7 @@ public int SendNewForumPostMessage(Customer customer, } var store = _storeContext.CurrentStore; + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Forums.NewForumPost", store.Id); if (messageTemplate == null) @@ -1079,16 +1060,16 @@ public int SendNewForumPostMessage(Customer customer, _messageTokenProvider.AddForumPostTokens(tokens, forumPost); _messageTokenProvider.AddCustomerTokens(tokens, customer); _messageTokenProvider.AddForumTopicTokens(tokens, forumPost.ForumTopic, friendlyForumTopicPageIndex, forumPost.Id); - _messageTokenProvider.AddForumTokens(tokens, forumPost.ForumTopic.Forum, languageId); + _messageTokenProvider.AddForumTokens(tokens, forumPost.ForumTopic.Forum, language); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -1163,7 +1144,8 @@ public virtual int SendGenericMessage(string messageTemplateName, Action @@ -1231,14 +1212,13 @@ public virtual int SendGiftCardNotification(GiftCard giftCard, int languageId) /// Product review /// Message language identifier /// Queued email identifier - public virtual int SendProductReviewNotificationMessage(ProductReview productReview, - int languageId) + public virtual int SendProductReviewNotificationMessage(ProductReview productReview, int languageId) { if (productReview == null) throw new ArgumentNullException("productReview"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Product.ProductReview", store.Id); if (messageTemplate == null) @@ -1252,17 +1232,14 @@ public virtual int SendProductReviewNotificationMessage(ProductReview productRev //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(productReview.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1277,7 +1254,7 @@ public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int throw new ArgumentNullException("product"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("QuantityBelow.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -1285,17 +1262,16 @@ public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -1306,14 +1282,13 @@ public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int /// Received VAT address /// Message language identifier /// Queued email identifier - public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, - string vatName, string vatAddress, int languageId) + public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, string vatName, string vatAddress, int languageId) { if (customer == null) throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewVATSubmitted.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -1329,17 +1304,14 @@ public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1354,7 +1326,7 @@ public virtual int SendBlogCommentNotificationMessage(BlogComment blogComment, i throw new ArgumentNullException("blogComment"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Blog.BlogComment", store.Id); if (messageTemplate == null) @@ -1368,17 +1340,14 @@ public virtual int SendBlogCommentNotificationMessage(BlogComment blogComment, i //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(blogComment.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1393,7 +1362,7 @@ public virtual int SendNewsCommentNotificationMessage(NewsComment newsComment, i throw new ArgumentNullException("newsComment"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("News.NewsComment", store.Id); if (messageTemplate == null) @@ -1407,17 +1376,14 @@ public virtual int SendNewsCommentNotificationMessage(NewsComment newsComment, i //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as sender/reply address var replyTo = GetReplyToEmail(newsComment.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1432,7 +1398,7 @@ public virtual int SendBackInStockNotification(BackInStockSubscription subscript throw new ArgumentNullException("subscription"); var store = _storeService.GetStoreById(subscription.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.BackInStock", store.Id); if (messageTemplate == null) @@ -1447,13 +1413,12 @@ public virtual int SendBackInStockNotification(BackInStockSubscription subscript //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var customer = subscription.Customer; var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion diff --git a/src/Libraries/SmartStore.Services/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/IOrderService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs index 039beeef57..9fca02b17f 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs @@ -164,17 +164,25 @@ IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, /// Payment method system name /// Order Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName); - - #endregion - #region Orders items - - /// - /// Gets an order item - /// - /// Order item identifier - /// Order item - OrderItem GetOrderItemById(int orderItemId); + /// + /// Shortcut to add an order + /// + /// Order + /// Order note + /// Whether to display the note to the customer + void AddOrderNote(Order order, string note, bool displayToCustomer = false); + + #endregion + + #region Orders items + + /// + /// Gets an order item + /// + /// Order item identifier + /// Order item + OrderItem GetOrderItemById(int orderItemId); /// /// Gets an order item diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index 7c0e0f4bf6..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; + } - #region Utilities + public Localizer T { get; set; } + + #endregion + + #region Utilities private decimal Round(decimal value) { return (_shoppingCartSettings.RoundPricesDuringCalculation ? Math.Round(value, 2) : value); } - private string TNote(string resKey) - { - return _localizationService.GetResource("Admin.OrderNotice." + resKey); - } + private void ProcessErrors(Order order, IList errors, string messageKey) + { + if (errors.Any()) + { + var msg = string.Concat(T(messageKey, order.GetOrderNumber()), " ", string.Join(" ", errors)); + + _orderService.AddOrderNote(order, msg); + _logger.InsertLog(LogLevel.Error, msg, msg); + } + } /// /// Award reward points @@ -229,18 +242,15 @@ protected void AwardRewardPoints(Order order, decimal? amount = null) if (order.RewardPointsWereAdded) return; - // Truncate increases the risk of inaccuracy of rounding - //int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); - - // why are points awarded for OrderTotal? wouldn't be OrderSubtotalInclTax better? - - int points = (int)Math.Round((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + // Trucate same as Floor for positive amounts + int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); if (points == 0) return; //add reward points - order.Customer.AddRewardPointsHistoryEntry(points, string.Format(_localizationService.GetResource("RewardPoints.Message.EarnedForOrder"), order.GetOrderNumber())); + order.Customer.AddRewardPointsHistoryEntry(points, T("RewardPoints.Message.EarnedForOrder", order.GetOrderNumber())); order.RewardPointsWereAdded = true; + _orderService.UpdateOrder(order); } @@ -277,7 +287,7 @@ protected void ReduceRewardPoints(Order order, decimal? amount = null) return; //reduce reward points - order.Customer.AddRewardPointsHistoryEntry(-points, string.Format(_localizationService.GetResource("RewardPoints.Message.ReducedForOrder"), order.GetOrderNumber())); + order.Customer.AddRewardPointsHistoryEntry(-points, T("RewardPoints.Message.ReducedForOrder", order.GetOrderNumber())); if (!order.RewardPointsRemaining.HasValue) order.RewardPointsRemaining = (int)Math.Round(order.OrderTotal / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); @@ -295,34 +305,42 @@ protected void ReduceRewardPoints(Order order, decimal? amount = null) protected void SetActivatedValueForPurchasedGiftCards(Order order, bool activate) { var giftCards = _giftCardService.GetAllGiftCards(order.Id, null, null, !activate); + foreach (var gc in giftCards) { if (activate) { //activate - bool isRecipientNotified = gc.IsRecipientNotified; + var isRecipientNotified = gc.IsRecipientNotified; + if (gc.GiftCardType == GiftCardType.Virtual) { //send email for virtual gift card - if (!String.IsNullOrEmpty(gc.RecipientEmail) && - !String.IsNullOrEmpty(gc.SenderEmail)) + if (!String.IsNullOrEmpty(gc.RecipientEmail) && !String.IsNullOrEmpty(gc.SenderEmail)) { var customerLang = _languageService.GetLanguageById(order.CustomerLanguageId); if (customerLang == null) customerLang = _languageService.GetAllLanguages().FirstOrDefault(); - int queuedEmailId = _workflowMessageService.SendGiftCardNotification(gc, customerLang.Id); - if (queuedEmailId > 0) - isRecipientNotified = true; + + var queuedEmailId = _workflowMessageService.SendGiftCardNotification(gc, customerLang.Id); + + if (queuedEmailId > 0) + { + isRecipientNotified = true; + } } } + gc.IsGiftCardActivated = true; gc.IsRecipientNotified = isRecipientNotified; + _giftCardService.UpdateGiftCard(gc); } else { //deactivate gc.IsGiftCardActivated = false; + _giftCardService.UpdateGiftCard(gc); } } @@ -347,15 +365,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) order.OrderStatusId = (int)os; _orderService.UpdateOrder(order); - //order notes, notifications - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("OrderStatusChanged"), os.GetLocalizedEnum(_localizationService)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderStatusChanged", os.GetLocalizedEnum(_localizationService))); if (prevOrderStatus != OrderStatus.Complete && os == OrderStatus.Complete && notifyCustomer) { @@ -363,13 +373,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) int orderCompletedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCompletedCustomerNotification(order, order.CustomerLanguageId); if (orderCompletedCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("CustomerCompletedEmailQueued"), orderCompletedCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", orderCompletedCustomerNotificationQueuedEmailId)); } } @@ -379,13 +383,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) int orderCancelledCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCancelledCustomerNotification(order, order.CustomerLanguageId); if (orderCancelledCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("CustomerCancelledEmailQueued"), orderCancelledCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", orderCancelledCustomerNotificationQueuedEmailId)); } } @@ -400,15 +398,13 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) } //gift cards activation - if (_orderSettings.GiftCards_Activated_OrderStatusId > 0 && - _orderSettings.GiftCards_Activated_OrderStatusId == (int)order.OrderStatus) + if (_orderSettings.GiftCards_Activated_OrderStatusId > 0 && _orderSettings.GiftCards_Activated_OrderStatusId == (int)order.OrderStatus) { SetActivatedValueForPurchasedGiftCards(order, true); } //gift cards deactivation - if (_orderSettings.GiftCards_Deactivated_OrderStatusId > 0 && - _orderSettings.GiftCards_Deactivated_OrderStatusId == (int)order.OrderStatus) + if (_orderSettings.GiftCards_Deactivated_OrderStatusId > 0 && _orderSettings.GiftCards_Deactivated_OrderStatusId == (int)order.OrderStatus) { SetActivatedValueForPurchasedGiftCards(order, false); } @@ -468,9 +464,11 @@ public void CheckOrderStatus(Order order) /// /// Process payment request /// Place order result - public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentRequest, Dictionary extraData) + public virtual PlaceOrderResult PlaceOrder( + ProcessPaymentRequest processPaymentRequest, + Dictionary extraData) { - //think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method + // think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method if (processPaymentRequest == null) throw new ArgumentNullException("processPaymentRequest"); @@ -484,28 +482,30 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { #region Order details (customer, totals) - //Recurring orders. Load initial order - Order initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); + // Recurring orders. Load initial order + var initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); if (processPaymentRequest.IsRecurringPayment) { if (initialOrder == null) - throw new ArgumentException("Initial order is not set for recurring payment"); + throw new ArgumentException(T("Order.InitialOrderDoesNotExistForRecurringPayment")); processPaymentRequest.PaymentMethodSystemName = initialOrder.PaymentMethodSystemName; } - //customer + // customer var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); if (customer == null) - throw new ArgumentException("Customer is not set"); + throw new ArgumentException(T("Customer.DoesNotExist")); - //affilites - int affiliateId = 0; + // affilites + var affiliateId = 0; var affiliate = _affiliateService.GetAffiliateById(customer.AffiliateId); if (affiliate != null && affiliate.Active && !affiliate.Deleted) + { affiliateId = affiliate.Id; + } - //customer currency + // customer currency string customerCurrencyCode = ""; decimal customerCurrencyRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) @@ -524,87 +524,77 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR customerCurrencyRate = initialOrder.CurrencyRate; } - //customer language + // customer language Language customerLanguage = null; if (!processPaymentRequest.IsRecurringPayment) { - customerLanguage = _languageService.GetLanguageById(customer.GetAttribute( - SystemCustomerAttributeNames.LanguageId, processPaymentRequest.StoreId)); + customerLanguage = _languageService.GetLanguageById(customer.GetAttribute(SystemCustomerAttributeNames.LanguageId, processPaymentRequest.StoreId)); } else { customerLanguage = _languageService.GetLanguageById(initialOrder.CustomerLanguageId); } - if (customerLanguage == null || !customerLanguage.Published) - customerLanguage = _workContext.WorkingLanguage; - //check whether customer is guest + if (customerLanguage == null || !customerLanguage.Published) + { + customerLanguage = _workContext.WorkingLanguage; + } + + // check whether customer is guest if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) - throw new SmartException("Anonymous checkout is not allowed"); + throw new SmartException(T("Checkout.AnonymousNotAllowed")); var storeId = _storeContext.CurrentStore.Id; - //load and validate customer shopping cart + // load and validate customer shopping cart IList cart = null; if (!processPaymentRequest.IsRecurringPayment) { //load shopping cart - cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); + if (processPaymentRequest.ShoppingCartItems.Count > 0) + cart = processPaymentRequest.ShoppingCartItems; + else + cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); if (cart.Count == 0) - throw new SmartException("Cart is empty"); + throw new SmartException(T("ShoppingCart.CartIsEmpty")); - //validate the entire shopping cart - var warnings = _shoppingCartService.GetShoppingCartWarnings(cart, - customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); + // validate the entire shopping cart + var warnings = _shoppingCartService.GetShoppingCartWarnings(cart, customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); if (warnings.Count > 0) - { - var warningsSb = new StringBuilder(); - foreach (string warning in warnings) - { - warningsSb.Append(warning); - warningsSb.Append(";"); - } - throw new SmartException(warningsSb.ToString()); - } + throw new SmartException(string.Join(" ", warnings)); - //validate individual cart items + // validate individual cart items foreach (var sci in cart) { var sciWarnings = _shoppingCartService.GetShoppingCartItemWarnings(customer, sci.Item.ShoppingCartType, sci.Item.Product, processPaymentRequest.StoreId, sci.Item.AttributesXml, sci.Item.CustomerEnteredPrice, sci.Item.Quantity, false, childItems: sci.ChildItems); + if (sciWarnings.Count > 0) - { - var warningsSb = new StringBuilder(); - foreach (string warning in sciWarnings) - { - warningsSb.Append(warning); - warningsSb.Append(";"); - } - throw new SmartException(warningsSb.ToString()); - } + throw new SmartException(string.Join(" ", sciWarnings)); } } - //min totals validation + // min totals validation if (!processPaymentRequest.IsRecurringPayment) { - bool minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); + var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); if (!minOrderSubtotalAmountOk) { - decimal minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); - throw new SmartException(string.Format(_localizationService.GetResource("Checkout.MinOrderSubtotalAmount"), _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); + var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); + throw new SmartException(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); } - bool minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); + + var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); if (!minOrderTotalAmountOk) { - decimal minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); - throw new SmartException(string.Format(_localizationService.GetResource("Checkout.MinOrderTotalAmount"), _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); + var minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); + throw new SmartException(T("Checkout.MinOrderTotalAmount", _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); } } - //tax display type + // tax display type var customerTaxDisplayType = TaxDisplayType.IncludingTax; if (!processPaymentRequest.IsRecurringPayment) { @@ -615,7 +605,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR customerTaxDisplayType = initialOrder.CustomerTaxDisplayType; } - //checkout attributes + // checkout attributes string checkoutAttributeDescription, checkoutAttributesXml; if (!processPaymentRequest.IsRecurringPayment) { @@ -628,15 +618,15 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR checkoutAttributeDescription = initialOrder.CheckoutAttributeDescription; } - //applied discount (used to store discount usage history) + // applied discount (used to store discount usage history) var appliedDiscounts = new List(); - //sub total + // sub total decimal orderSubTotalInclTax, orderSubTotalExclTax; decimal orderSubTotalDiscountInclTax = 0, orderSubTotalDiscountExclTax = 0; if (!processPaymentRequest.IsRecurringPayment) { - //sub total (incl tax) + // sub total (incl tax) decimal orderSubTotalDiscountAmount1 = decimal.Zero; Discount orderSubTotalAppliedDiscount1 = null; decimal subTotalWithoutDiscountBase1 = decimal.Zero; @@ -648,11 +638,11 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR orderSubTotalInclTax = subTotalWithoutDiscountBase1; orderSubTotalDiscountInclTax = orderSubTotalDiscountAmount1; - //discount history + // discount history if (orderSubTotalAppliedDiscount1 != null && !appliedDiscounts.Any(x => x.Id == orderSubTotalAppliedDiscount1.Id)) appliedDiscounts.Add(orderSubTotalAppliedDiscount1); - //sub total (excl tax) + // sub total (excl tax) decimal orderSubTotalDiscountAmount2 = decimal.Zero; Discount orderSubTotalAppliedDiscount2 = null; decimal subTotalWithoutDiscountBase2 = decimal.Zero; @@ -671,7 +661,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } - //shipping info + // shipping info bool shoppingCartRequiresShipping = false; if (!processPaymentRequest.IsRecurringPayment) { @@ -687,7 +677,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { if (!processPaymentRequest.IsRecurringPayment) { - var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, processPaymentRequest.StoreId); + var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, processPaymentRequest.StoreId); if (shippingOption != null) { shippingMethodName = shippingOption.Name; @@ -701,7 +691,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //shipping total + // shipping total decimal? orderShippingTotalInclTax, orderShippingTotalExclTax = null; decimal orderShippingTaxRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) @@ -710,7 +700,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out shippingTotalDiscount); orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) - throw new SmartException("Shipping total couldn't be calculated"); + throw new SmartException(T("Order.CannotCalculateShippingTotal")); if (shippingTotalDiscount != null && !appliedDiscounts.Any(x => x.Id == shippingTotalDiscount.Id)) appliedDiscounts.Add(shippingTotalDiscount); @@ -749,7 +739,9 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR //VAT number var customerVatStatus = (VatNumberStatus)customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId); if (_taxSettings.EuVatEnabled && customerVatStatus == VatNumberStatus.Valid) + { vatNumber = customer.GetAttribute(SystemCustomerAttributeNames.VatNumber); + } //tax rates foreach (var kvp in taxRatesDictionary) @@ -777,14 +769,16 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { Discount orderAppliedDiscount = null; orderTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart, - out orderDiscountAmount, out orderAppliedDiscount, out appliedGiftCards, - out redeemedRewardPoints, out redeemedRewardPointsAmount); + out orderDiscountAmount, out orderAppliedDiscount, out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount); + if (!orderTotal.HasValue) - throw new SmartException("Order total couldn't be calculated"); + throw new SmartException(T("Order.CannotCalculateOrderTotal")); - //discount history - if (orderAppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == orderAppliedDiscount.Id)) - appliedDiscounts.Add(orderAppliedDiscount); + //discount history + if (orderAppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == orderAppliedDiscount.Id)) + { + appliedDiscounts.Add(orderAppliedDiscount); + } } else { @@ -803,7 +797,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!preProcessPaymentResult.Success) { result.Errors.AddRange(preProcessPaymentResult.Errors); - result.Errors.Add(_localizationService.GetResource("Common.Error.PreProcessPayment")); + result.Errors.Add(T("Common.Error.PreProcessPayment")); return result; } @@ -811,23 +805,23 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!processPaymentRequest.IsRecurringPayment) { if (customer.BillingAddress == null) - throw new SmartException("Billing address is not provided"); + throw new SmartException(T("Order.BillingAddressMissing")); if (!customer.BillingAddress.Email.IsEmail()) - throw new SmartException("Email is not valid"); + throw new SmartException(T("Common.Error.InvalidEmail")); billingAddress = (Address)customer.BillingAddress.Clone(); } else { if (initialOrder.BillingAddress == null) - throw new SmartException("Billing address is not available"); + throw new SmartException(T("Order.BillingAddressMissing")); billingAddress = (Address)initialOrder.BillingAddress.Clone(); } if (billingAddress.Country != null && !billingAddress.Country.AllowsBilling) - throw new SmartException(string.Format("Country '{0}' is not allowed for billing", billingAddress.Country.Name)); + throw new SmartException(T("Order.CountryNotAllowedForBilling", billingAddress.Country.Name)); Address shippingAddress = null; if (shoppingCartRequiresShipping) @@ -835,64 +829,68 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!processPaymentRequest.IsRecurringPayment) { if (customer.ShippingAddress == null) - throw new SmartException("Shipping address is not provided"); + throw new SmartException(T("Order.ShippingAddressMissing")); if (!customer.ShippingAddress.Email.IsEmail()) - throw new SmartException("Email is not valid"); + throw new SmartException(T("Common.Error.InvalidEmail")); shippingAddress = (Address)customer.ShippingAddress.Clone(); } else { if (initialOrder.ShippingAddress == null) - throw new SmartException("Shipping address is not available"); + throw new SmartException(T("Order.ShippingAddressMissing")); shippingAddress = (Address)initialOrder.ShippingAddress.Clone(); } if (shippingAddress.Country != null && !shippingAddress.Country.AllowsShipping) - throw new SmartException(string.Format("Country '{0}' is not allowed for shipping", shippingAddress.Country.Name)); + throw new SmartException(T("Order.CountryNotAllowedForShipping", shippingAddress.Country.Name)); } #endregion #region Payment workflow - //skip payment workflow if order total equals zero - bool skipPaymentWorkflow = false; - if (orderTotal.Value == decimal.Zero) - skipPaymentWorkflow = true; + // skip payment workflow if order total equals zero + var skipPaymentWorkflow = false; + if (orderTotal.Value == decimal.Zero) + { + skipPaymentWorkflow = true; + } - //payment workflow + // payment workflow Provider paymentMethod = null; if (!skipPaymentWorkflow) { paymentMethod = _paymentService.LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); - //ensure that payment method is active + // ensure that payment method is active if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) - throw new SmartException("Payment method is not active"); + throw new SmartException(T("Payment.MethodNotAvailable")); } else { processPaymentRequest.PaymentMethodSystemName = ""; } - //recurring or standard shopping cart? - bool isRecurringShoppingCart = false; + // recurring or standard shopping cart? + var isRecurringShoppingCart = false; if (!processPaymentRequest.IsRecurringPayment) { isRecurringShoppingCart = cart.IsRecurring(); if (isRecurringShoppingCart) { - int recurringCycleLength = 0; + var recurringCycleLength = 0; + var recurringTotalCycles = 0; RecurringProductCyclePeriod recurringCyclePeriod; - int recurringTotalCycles = 0; string recurringCyclesError = cart.GetRecurringCycleInfo(_localizationService, out recurringCycleLength, out recurringCyclePeriod, out recurringTotalCycles); + if (!string.IsNullOrEmpty(recurringCyclesError)) throw new SmartException(recurringCyclesError); + processPaymentRequest.RecurringCycleLength = recurringCycleLength; processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod; processPaymentRequest.RecurringTotalCycles = recurringTotalCycles; @@ -903,31 +901,31 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR isRecurringShoppingCart = true; } - //process payment + // process payment ProcessPaymentResult processPaymentResult = null; - if (!skipPaymentWorkflow) + if (!skipPaymentWorkflow && !processPaymentRequest.IsMultiOrder) { if (!processPaymentRequest.IsRecurringPayment) { if (isRecurringShoppingCart) { - //recurring cart + // recurring cart var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { case RecurringPaymentType.NotSupported: - throw new SmartException("Recurring payments are not supported by selected payment method"); + throw new SmartException(T("Payment.RecurringPaymentNotSupported")); case RecurringPaymentType.Manual: case RecurringPaymentType.Automatic: processPaymentResult = _paymentService.ProcessRecurringPayment(processPaymentRequest); break; default: - throw new SmartException("Not supported recurring payment type"); + throw new SmartException(T("Payment.RecurringPaymentTypeUnknown")); } } else { - //standard cart + // standard cart processPaymentResult = _paymentService.ProcessPayment(processPaymentRequest); } } @@ -935,12 +933,13 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { if (isRecurringShoppingCart) { - //Old credit card info + // Old credit card info processPaymentRequest.CreditCardType = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardType) : ""; processPaymentRequest.CreditCardName = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardName) : ""; processPaymentRequest.CreditCardNumber = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardNumber) : ""; - //MaskedCreditCardNumber + // MaskedCreditCardNumber processPaymentRequest.CreditCardCvv2 = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardCvv2) : ""; + try { processPaymentRequest.CreditCardExpireMonth = initialOrder.AllowStoringCreditCardNumber ? Convert.ToInt32(_encryptionService.DecryptText(initialOrder.CardExpirationMonth)) : 0; @@ -952,7 +951,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR switch (recurringPaymentType) { case RecurringPaymentType.NotSupported: - throw new SmartException("Recurring payments are not supported by selected payment method"); + throw new SmartException(T("Payment.RecurringPaymentNotSupported")); case RecurringPaymentType.Manual: processPaymentResult = _paymentService.ProcessRecurringPayment(processPaymentRequest); break; @@ -961,26 +960,25 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR processPaymentResult = new ProcessPaymentResult(); break; default: - throw new SmartException("Not supported recurring payment type"); - } + throw new SmartException(T("Payment.RecurringPaymentTypeUnknown")); + } } else { - throw new SmartException("No recurring products"); + throw new SmartException(T("Order.NoRecurringProducts")); } } } else { - //payment is not required - if (processPaymentResult == null) - processPaymentResult = new ProcessPaymentResult(); + // payment is not required + if (processPaymentResult == null) + { + processPaymentResult = new ProcessPaymentResult(); + } processPaymentResult.NewPaymentStatus = PaymentStatus.Paid; } - if (processPaymentResult == null) - throw new SmartException("processPaymentResult is not available"); - #endregion if (processPaymentResult.Success) @@ -992,10 +990,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR #region Save order details var shippingStatus = ShippingStatus.NotYetShipped; - if (!shoppingCartRequiresShipping) - shippingStatus = ShippingStatus.ShippingNotRequired; + if (!shoppingCartRequiresShipping) + { + shippingStatus = ShippingStatus.ShippingNotRequired; + } - var order = new Order() + var order = new Order { StoreId = processPaymentRequest.StoreId, OrderGuid = processPaymentRequest.OrderGuid, @@ -1060,6 +1060,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR UpdatedOnUtc = utcNow, CustomerOrderComment = extraData.ContainsKey("CustomerComment") ? extraData["CustomerComment"] : "" }; + + if (extraData.ContainsKey("AcceptThirdPartyEmailHandOver") && _shoppingCartSettings.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None) + { + order.AcceptThirdPartyEmailHandOver = extraData["AcceptThirdPartyEmailHandOver"].ToBool(); + } + _orderService.InsertOrder(order); result.PlacedOrder = order; @@ -1086,12 +1092,14 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR decimal discountAmount = _priceCalculationService.GetDiscountAmount(sc, out scDiscount); decimal discountAmountInclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, true, customer, out taxRate); decimal discountAmountExclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, false, customer, out taxRate); - + if (scDiscount != null && !appliedDiscounts.Any(x => x.Id == scDiscount.Id)) - appliedDiscounts.Add(scDiscount); + { + appliedDiscounts.Add(scDiscount); + } //attributes - string attributeDescription = _productAttributeFormatter.FormatAttributes(sc.Item.Product, sc.Item.AttributesXml, customer); + var attributeDescription = _productAttributeFormatter.FormatAttributes(sc.Item.Product, sc.Item.AttributesXml, customer); var itemWeight = _shippingService.GetShoppingCartItemWeight(sc); @@ -1124,10 +1132,10 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR foreach (var childItem in sc.ChildItems) { - decimal bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); + var bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); - string attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, - renderPrices: false, allowHyperlinks: false); + var attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, + renderPrices: false, allowHyperlinks: true); childItem.BundleItemData.ToOrderData(listBundleData, bundleItemSubTotal, childItem.Item.AttributesXml, attributesInfo); } @@ -1141,11 +1149,10 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR //gift cards if (sc.Item.Product.IsGiftCard) { - string giftCardRecipientName, giftCardRecipientEmail, - giftCardSenderName, giftCardSenderEmail, giftCardMessage; + string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; + _productAttributeParser.GetGiftCardAttribute(sc.Item.AttributesXml, - out giftCardRecipientName, out giftCardRecipientEmail, - out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); + out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); for (int i = 0; i < sc.Item.Quantity; i++) { @@ -1168,12 +1175,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //inventory _productService.AdjustInventory(sc, true); } //clear shopping cart - cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); + if (!processPaymentRequest.IsMultiOrder) + cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); } else { @@ -1182,7 +1189,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR foreach (var orderItem in initialOrderItems) { //save item - var newOrderItem = new OrderItem() + var newOrderItem = new OrderItem { OrderItemGuid = Guid.NewGuid(), Order = order, @@ -1213,12 +1220,11 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; _productAttributeParser.GetGiftCardAttribute(orderItem.AttributesXml, - out giftCardRecipientName, out giftCardRecipientEmail, - out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); + out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); for (int i = 0; i < orderItem.Quantity; i++) { - var gc = new GiftCard() + var gc = new GiftCard { GiftCardType = orderItem.Product.GiftCardType, PurchasedWithOrderItem = newOrderItem, @@ -1237,7 +1243,6 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //inventory _productService.AdjustInventory(orderItem, true, orderItem.Quantity); } } @@ -1247,7 +1252,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { foreach (var discount in appliedDiscounts) { - var duh = new DiscountUsageHistory() + var duh = new DiscountUsageHistory { Discount = discount, Order = order, @@ -1262,8 +1267,8 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { foreach (var agc in appliedGiftCards) { - decimal amountUsed = agc.AmountCanBeUsed; - var gcuh = new GiftCardUsageHistory() + var amountUsed = agc.AmountCanBeUsed; + var gcuh = new GiftCardUsageHistory { GiftCard = agc.GiftCard, UsedWithOrder = order, @@ -1279,9 +1284,10 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (redeemedRewardPointsAmount > decimal.Zero) { customer.AddRewardPointsHistoryEntry(-redeemedRewardPoints, - string.Format(_localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId), order.GetOrderNumber()), + _localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId).FormatInvariant(order.GetOrderNumber()), order, - redeemedRewardPointsAmount); + redeemedRewardPointsAmount); + _customerService.UpdateCustomer(customer); } @@ -1289,7 +1295,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!processPaymentRequest.IsRecurringPayment && isRecurringShoppingCart) { //create recurring payment (the first payment) - var rp = new RecurringPayment() + var rp = new RecurringPayment { CycleLength = processPaymentRequest.RecurringCycleLength, CyclePeriod = processPaymentRequest.RecurringCyclePeriod, @@ -1301,7 +1307,6 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR }; _orderService.InsertRecurringPayment(rp); - var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { @@ -1333,50 +1338,34 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - #endregion + #endregion - #region Notifications, notes and attributes - - //notes, messages - order.OrderNotes.Add(new OrderNote - { - Note = TNote("OrderPlaced"), - DisplayToCustomer = false, - CreatedOnUtc = utcNow - }); - _orderService.UpdateOrder(order); + #region Notifications, notes and attributes + + // notes, messages + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); //send email notifications int orderPlacedStoreOwnerNotificationQueuedEmailId = _workflowMessageService.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); if (orderPlacedStoreOwnerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("MerchantEmailQueued"), orderPlacedStoreOwnerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = utcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.MerchantEmailQueued", orderPlacedStoreOwnerNotificationQueuedEmailId)); } int orderPlacedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderPlacedCustomerNotification(order, order.CustomerLanguageId); if (orderPlacedCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("CustomerEmailQueued"), orderPlacedCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = utcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", orderPlacedCustomerNotificationQueuedEmailId)); } - //check order status + // check order status CheckOrderStatus(order); - //reset checkout data - if (!processPaymentRequest.IsRecurringPayment) + //reset checkout data + if (!processPaymentRequest.IsRecurringPayment && !processPaymentRequest.IsMultiOrder) + { _customerService.ResetCheckoutData(customer, processPaymentRequest.StoreId, clearCouponCodes: true, clearCheckoutAttributes: true); + } // check for generic attributes to be inserted automatically foreach (var customProperty in processPaymentRequest.CustomProperties.Where(x => x.Key.HasValue() && x.Value.AutoCreateGenericAttribute)) @@ -1387,15 +1376,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR //uncomment this line to support transactions //scope.Complete(); - //raise event + // raise event _eventPublisher.PublishOrderPlaced(order); if (!processPaymentRequest.IsRecurringPayment) { - _customerActivityService.InsertActivity( - "PublicStore.PlaceOrder", - _localizationService.GetResource("ActivityLog.PublicStore.PlaceOrder"), - order.GetOrderNumber()); + _customerActivityService.InsertActivity("PublicStore.PlaceOrder", T("ActivityLog.PublicStore.PlaceOrder", order.GetOrderNumber())); } //raise event @@ -1403,13 +1389,37 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { _eventPublisher.PublishOrderPaid(order); } - #endregion - } - } + + #endregion + + #region Newsletter subscription + + if (extraData.ContainsKey("SubscribeToNewsLetter") && _shoppingCartSettings.NewsLetterSubscription != CheckoutNewsLetterSubscription.None) + { + var addSubscription = extraData["SubscribeToNewsLetter"].ToBool(); + + bool? nsResult = _newsLetterSubscriptionService.AddNewsLetterSubscriptionFor(addSubscription, customer.Email, order.StoreId); + + if (nsResult.HasValue) + { + if (nsResult.Value) + _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionAdded")); + else + _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionRemoved")); + } + } + + #endregion + } + } else { - foreach (var paymentError in processPaymentResult.Errors) - result.AddError(paymentError); + result.AddError(T("Payment.PayingFailed")); + + foreach (var paymentError in processPaymentResult.Errors) + { + result.AddError(paymentError); + } } } catch (Exception exc) @@ -1452,14 +1462,8 @@ public virtual void DeleteOrder(Order order) _productService.AdjustInventory(orderItem, false, orderItem.Quantity); } - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderDeleted"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + //add a note + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderDeleted")); //now delete an order _orderService.DeleteOrder(order); @@ -1474,25 +1478,25 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen { if (recurringPayment == null) throw new ArgumentNullException("recurringPayment"); + try { if (!recurringPayment.IsActive) - throw new SmartException("Recurring payment is not active"); + throw new SmartException(T("Payment.RecurringPaymentNotActive")); var initialOrder = recurringPayment.InitialOrder; if (initialOrder == null) - throw new SmartException("Initial order could not be loaded"); + throw new SmartException(T("Order.InitialOrderDoesNotExistForRecurringPayment")); var customer = initialOrder.Customer; if (customer == null) - throw new SmartException("Customer could not be loaded"); + throw new SmartException(T("Customer.DoesNotExist")); var nextPaymentDate = recurringPayment.NextPaymentDate; if (!nextPaymentDate.HasValue) - throw new SmartException("Next payment date could not be calculated"); + throw new SmartException(T("Payment.CannotCalculateNextPaymentDate")); - //payment info - var paymentInfo = new ProcessPaymentRequest() + var paymentInfo = new ProcessPaymentRequest { StoreId = initialOrder.StoreId, CustomerId = customer.Id, @@ -1506,35 +1510,30 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen //place a new order var result = this.PlaceOrder(paymentInfo, new Dictionary()); + if (result.Success) { if (result.PlacedOrder == null) - throw new SmartException("Placed order could not be loaded"); + throw new SmartException(T("Order.NotFound", "".NaIfEmpty())); - var rph = new RecurringPaymentHistory() + var rph = new RecurringPaymentHistory { RecurringPayment = recurringPayment, CreatedOnUtc = DateTime.UtcNow, - OrderId = result.PlacedOrder.Id, + OrderId = result.PlacedOrder.Id }; + recurringPayment.RecurringPaymentHistory.Add(rph); _orderService.UpdateRecurringPayment(recurringPayment); } - else - { - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - throw new SmartException(error); + else if (result.Errors.Count > 0) + { + throw new SmartException(string.Join(" ", result.Errors)); } } - catch (Exception exc) + catch (Exception exception) { - _logger.Error(string.Format("Error while processing recurring order. {0}", exc.Message), exc); + _logger.ErrorsAll(exception); throw; } } @@ -1550,69 +1549,42 @@ public virtual IList CancelRecurringPayment(RecurringPayment recurringPa var initialOrder = recurringPayment.InitialOrder; if (initialOrder == null) - return new List() { "Initial order could not be loaded" }; - + return new List { T("Order.InitialOrderDoesNotExistForRecurringPayment") }; var request = new CancelRecurringPaymentRequest(); CancelRecurringPaymentResult result = null; + try { request.Order = initialOrder; - result = _paymentService.CancelRecurringPayment(request); + + result = _paymentService.CancelRecurringPayment(request); + if (result.Success) { //update recurring payment recurringPayment.IsActive = false; _orderService.UpdateRecurringPayment(recurringPayment); - - //add a note - initialOrder.OrderNotes.Add(new OrderNote() - { - Note = TNote("RecurringPaymentCancelled"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(initialOrder); + _orderService.AddOrderNote(initialOrder, T("Admin.OrderNotice.RecurringPaymentCancelled")); //notify a store owner - _workflowMessageService - .SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, - _localizationSettings.DefaultAdminLanguageId); + _workflowMessageService.SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, _localizationSettings.DefaultAdminLanguageId); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new CancelRecurringPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); + if (result == null) + { + result = new CancelRecurringPaymentResult(); + } + + result.AddError(exception.ToAllMessages()); } + ProcessErrors(initialOrder, result.Errors, "Admin.OrderNotice.RecurringPaymentCancellationError"); - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - initialOrder.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("RecurringPaymentCancellationError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(initialOrder); - - //log it - string logError = string.Format("Error cancelling recurring payment. Order #{0}. Error: {1}", initialOrder.Id, error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + return result.Errors; } /// @@ -1666,10 +1638,10 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) var order = _orderService.GetOrderById(shipment.OrderId); if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); if (shipment.ShippedDateUtc.HasValue) - throw new Exception("This shipment is already shipped"); + throw new SmartException(T("Shipment.AlreadyShipped")); shipment.ShippedDateUtc = DateTime.UtcNow; _shipmentService.UpdateShipment(shipment); @@ -1679,30 +1651,18 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) order.ShippingStatusId = (int)ShippingStatus.PartiallyShipped; else order.ShippingStatusId = (int)ShippingStatus.Shipped; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("ShipmentSent"), shipment.Id), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentSent", shipment.Id)); + if (notifyCustomer) { //notify customer int queuedEmailId = _workflowMessageService.SendShipmentSentCustomerNotification(shipment, order.CustomerLanguageId); if (queuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("CustomerShippedEmailQueued"), queuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", queuedEmailId)); } } @@ -1722,40 +1682,30 @@ public virtual void Deliver(Shipment shipment, bool notifyCustomer) var order = shipment.Order; if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); - if (shipment.DeliveryDateUtc.HasValue) - throw new Exception("This shipment is already delivered"); + if (shipment.DeliveryDateUtc.HasValue) + throw new SmartException(T("Shipment.AlreadyDelivered")); shipment.DeliveryDateUtc = DateTime.UtcNow; _shipmentService.UpdateShipment(shipment); - if (!order.HasItemsToAddToShipment() && !order.HasItemsToShip() && !order.HasItemsToDeliver()) - order.ShippingStatusId = (int)ShippingStatus.Delivered; - _orderService.UpdateOrder(order); + if (!order.HasItemsToAddToShipment() && !order.HasItemsToShip() && !order.HasItemsToDeliver()) + { + order.ShippingStatusId = (int)ShippingStatus.Delivered; + } - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("ShipmentDelivered"), shipment.Id), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentDelivered", shipment.Id)); + if (notifyCustomer) { //send email notification int queuedEmailId = _workflowMessageService.SendShipmentDeliveredCustomerNotification(shipment, order.CustomerLanguageId); if (queuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("CustomerDeliveredEmailQueued"), queuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", queuedEmailId)); } } @@ -1792,19 +1742,12 @@ public virtual void CancelOrder(Order order, bool notifyCustomer) throw new ArgumentNullException("order"); if (!CanCancelOrder(order)) - throw new SmartException("Cannot do cancel for order."); + throw new SmartException(T("Order.CannotCancel")); //Cancel order SetOrderStatus(order, OrderStatus.Cancelled, notifyCustomer); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderCancelled"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCancelled")); //cancel recurring payments var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); @@ -1915,14 +1858,7 @@ public virtual void MarkAsAuthorized(Order order) order.PaymentStatusId = (int)PaymentStatus.Authorized; _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderMarkedAsAuthorized"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsAuthorized")); //check order status CheckOrderStatus(order); @@ -1949,7 +1885,7 @@ public virtual bool CanCompleteOrder(Order order) public virtual void CompleteOrder(Order order) { if (!CanCompleteOrder(order)) - throw new SmartException("You can't mark this order as completed"); + throw new SmartException(T("Order.CannotMarkCompleted")); if (CanMarkOrderAsPaid(order)) { @@ -1977,12 +1913,10 @@ public virtual bool CanCapture(Order order) if (order == null) throw new ArgumentNullException("order"); - if (order.OrderStatus == OrderStatus.Cancelled || - order.OrderStatus == OrderStatus.Pending) + if (order.OrderStatus == OrderStatus.Cancelled || order.OrderStatus == OrderStatus.Pending) return false; - if (order.PaymentStatus == PaymentStatus.Authorized && - _paymentService.SupportCapture(order.PaymentMethodSystemName)) + if (order.PaymentStatus == PaymentStatus.Authorized && _paymentService.SupportCapture(order.PaymentMethodSystemName)) return true; return false; @@ -1999,7 +1933,7 @@ public virtual IList Capture(Order order) throw new ArgumentNullException("order"); if (!CanCapture(order)) - throw new SmartException("Cannot do capture for order."); + throw new SmartException(T("Order.CannotCapture")); var request = new CapturePaymentRequest(); CapturePaymentResult result = null; @@ -2012,24 +1946,20 @@ public virtual IList Capture(Order order) if (result.Success) { var paidDate = order.PaidDateUtc; - if (result.NewPaymentStatus == PaymentStatus.Paid) - paidDate = DateTime.UtcNow; + if (result.NewPaymentStatus == PaymentStatus.Paid) + { + paidDate = DateTime.UtcNow; + } order.CaptureTransactionId = result.CaptureTransactionId; order.CaptureTransactionResult = result.CaptureTransactionResult; order.PaymentStatus = result.NewPaymentStatus; order.PaidDateUtc = paidDate; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderCaptured"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCaptured")); + CheckOrderStatus(order); //raise event @@ -2039,38 +1969,19 @@ public virtual IList Capture(Order order) } } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new CapturePaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new CapturePaymentResult(); + } + result.AddError(exception.ToAllMessages()); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format("Unable to capture order. {0}", error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderCaptureError"); - //log it - string logError = string.Format(TNote("OrderCaptureError"), order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + return result.Errors; } /// @@ -2104,20 +2015,14 @@ public virtual void MarkOrderAsPaid(Order order) throw new ArgumentNullException("order"); if (!CanMarkOrderAsPaid(order)) - throw new SmartException("You can't mark this order as paid"); + throw new SmartException(T("Order.CannotMarkPaid")); order.PaymentStatusId = (int)PaymentStatus.Paid; order.PaidDateUtc = DateTime.UtcNow; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote - { - Note = TNote("OrderMarkedAsPaid"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.UpdateOrder(order); + + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPaid")); CheckOrderStatus(order); @@ -2165,7 +2070,7 @@ public virtual IList Refund(Order order) throw new ArgumentNullException("order"); if (!CanRefund(order)) - throw new SmartException("Cannot do refund for order."); + throw new SmartException(T("Order.CannotRefund")); var request = new RefundPaymentRequest(); RefundPaymentResult result = null; @@ -2174,7 +2079,9 @@ public virtual IList Refund(Order order) request.Order = order; request.AmountToRefund = order.OrderTotal; request.IsPartialRefund = false; - result = _paymentService.Refund(request); + + result = _paymentService.Refund(request); + if (result.Success) { //total amount refunded @@ -2183,53 +2090,29 @@ public virtual IList Refund(Order order) //update order info order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = result.NewPaymentStatus; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderRefunded"), _priceFormatter.FormatPrice(request.AmountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderRefunded", _priceFormatter.FormatPrice(request.AmountToRefund, true, false))); + //check order status CheckOrderStatus(order); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new RefundPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new RefundPaymentResult(); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; + result.AddError(exception.ToAllMessages()); } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderRefundError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - //log it - string logError = string.Format("Error refunding order '{0}'. Error: {1}", order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderRefundError"); + + return result.Errors; } /// @@ -2265,7 +2148,7 @@ public virtual void RefundOffline(Order order) throw new ArgumentNullException("order"); if (!CanRefundOffline(order)) - throw new SmartException("You can't refund this order"); + throw new SmartException(T("Order.CannotRefund")); //amout to refund decimal amountToRefund = order.OrderTotal; @@ -2276,17 +2159,11 @@ public virtual void RefundOffline(Order order) //update order info order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = PaymentStatus.Refunded; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderMarkedAsRefunded"), _priceFormatter.FormatPrice(amountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + //check order status CheckOrderStatus(order); } @@ -2336,10 +2213,11 @@ public virtual IList PartiallyRefund(Order order, decimal amountToRefund throw new ArgumentNullException("order"); if (!CanPartiallyRefund(order, amountToRefund)) - throw new SmartException("Cannot do partial refund for order."); + throw new SmartException(T("Order.CannotPartialRefund")); var request = new RefundPaymentRequest(); RefundPaymentResult result = null; + try { request.Order = order; @@ -2356,53 +2234,28 @@ public virtual IList PartiallyRefund(Order order, decimal amountToRefund //update order info order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = result.NewPaymentStatus; - _orderService.UpdateOrder(order); - - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderPartiallyRefunded"), _priceFormatter.FormatPrice(amountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + //check order status CheckOrderStatus(order); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new RefundPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new RefundPaymentResult(); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderPartiallyRefundError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + result.AddError(exception.ToAllMessages()); + } - //log it - string logError = string.Format("Error refunding order '{0}'. Error: {1}", order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderPartiallyRefundError"); + + return result.Errors; } /// @@ -2448,7 +2301,7 @@ public virtual void PartiallyRefundOffline(Order order, decimal amountToRefund) throw new ArgumentNullException("order"); if (!CanPartiallyRefundOffline(order, amountToRefund)) - throw new SmartException("You can't partially refund (offline) this order"); + throw new SmartException(T("Order.CannotPartialRefund")); //total amount refunded decimal totalAmountRefunded = order.RefundedAmount + amountToRefund; @@ -2457,17 +2310,11 @@ public virtual void PartiallyRefundOffline(Order order, decimal amountToRefund) order.RefundedAmount = totalAmountRefunded; //if (order.OrderTotal == totalAmountRefunded), then set order.PaymentStatus = PaymentStatus.Refunded; order.PaymentStatus = PaymentStatus.PartiallyRefunded; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderMarkedAsPartiallyRefunded"), _priceFormatter.FormatPrice(amountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + //check order status CheckOrderStatus(order); } @@ -2509,10 +2356,11 @@ public virtual IList Void(Order order) throw new ArgumentNullException("order"); if (!CanVoid(order)) - throw new SmartException("Cannot do void for order."); + throw new SmartException(T("Order.CannotVoid")); var request = new VoidPaymentRequest(); VoidPaymentResult result = null; + try { request.Order = order; @@ -2522,51 +2370,27 @@ public virtual IList Void(Order order) { //update order info order.PaymentStatus = result.NewPaymentStatus; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderVoided"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderVoided")); + //check order status CheckOrderStatus(order); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new VoidPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new VoidPaymentResult(); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderVoidError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + result.AddError(exception.ToAllMessages()); + } + + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderVoidError"); - //log it - string logError = string.Format("Error voiding order '{0}'. Error: {1}", order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } return result.Errors; } @@ -2603,20 +2427,14 @@ public virtual void VoidOffline(Order order) throw new ArgumentNullException("order"); if (!CanVoidOffline(order)) - throw new SmartException("You can't void this order"); + throw new SmartException(T("Order.CannotVoid")); - order.PaymentStatusId = (int)PaymentStatus.Voided; - _orderService.UpdateOrder(order); + order.PaymentStatusId = (int)PaymentStatus.Voided; - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderMarkedAsVoided"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsVoided")); + //check orer status CheckOrderStatus(order); } @@ -2634,7 +2452,7 @@ public virtual void ReOrder(Order order) foreach (var orderItem in order.OrderItems) { - bool isBundle = (orderItem.Product.ProductType == ProductType.BundledProduct); + var isBundle = (orderItem.Product.ProductType == ProductType.BundledProduct); var addToCartContext = new AddToCartContext(); @@ -2673,7 +2491,8 @@ public virtual bool IsReturnRequestAllowed(Order order) if (order.OrderStatus != OrderStatus.Complete) return false; - bool numberOfDaysReturnRequestAvailableValid = false; + var numberOfDaysReturnRequestAvailableValid = false; + if (_orderSettings.NumberOfDaysReturnRequestAvailable == 0) { numberOfDaysReturnRequestAvailableValid = true; @@ -2700,14 +2519,13 @@ public virtual bool ValidateMinOrderSubtotalAmount(IList 0 && _orderSettings.MinOrderSubtotalAmount > decimal.Zero) { - //subtotal decimal orderSubTotalDiscountAmountBase = decimal.Zero; Discount orderSubTotalAppliedDiscount = null; decimal subTotalWithoutDiscountBase = decimal.Zero; decimal subTotalWithDiscountBase = decimal.Zero; + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - out orderSubTotalDiscountAmountBase, out orderSubTotalAppliedDiscount, - out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); + out orderSubTotalDiscountAmountBase, out orderSubTotalAppliedDiscount, out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); if (subTotalWithoutDiscountBase < _orderSettings.MinOrderSubtotalAmount) return false; @@ -2729,6 +2547,7 @@ public virtual bool ValidateMinOrderTotalAmount(IList if (cart.Count > 0 && _orderSettings.MinOrderTotalAmount > decimal.Zero) { decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); + if (shoppingCartTotalBase.HasValue && shoppingCartTotalBase.Value < _orderSettings.MinOrderTotalAmount) return false; } @@ -2771,6 +2590,7 @@ public virtual Shipment AddShipment(Order order, string trackingNumber, Dictiona { if (!totalWeight.HasValue) totalWeight = 0; + totalWeight += orderItemTotalWeight.Value; } @@ -2800,6 +2620,7 @@ public virtual Shipment AddShipment(Order order, string trackingNumber, Dictiona if (shipment != null && shipment.ShipmentItems.Count > 0) { shipment.TotalWeight = totalWeight; + _shipmentService.InsertShipment(shipment); return shipment; diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index c54392d8c4..7ed45bb61a 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -13,10 +13,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderService : IOrderService + /// + /// Order service + /// + public partial class OrderService : IOrderService { #region Fields @@ -392,8 +392,7 @@ public virtual void DeleteOrderNote(OrderNote orderNote) /// Authorization transaction ID /// Payment method system name /// Order - public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, - string paymentMethodSystemName) + public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName) { var query = _orderRepository.Table; if (!String.IsNullOrWhiteSpace(authorizationTransactionId)) @@ -406,17 +405,32 @@ public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string var order = query.FirstOrDefault(); return order; } - - #endregion - - #region Order items - /// - /// Gets an Order item - /// - /// Order item identifier - /// Order item - public virtual OrderItem GetOrderItemById(int orderItemId) + public virtual void AddOrderNote(Order order, string note, bool displayToCustomer = false) + { + if (order != null && note.HasValue()) + { + order.OrderNotes.Add(new OrderNote + { + Note = note, + DisplayToCustomer = displayToCustomer, + CreatedOnUtc = DateTime.UtcNow + }); + + UpdateOrder(order); + } + } + + #endregion + + #region Order items + + /// + /// Gets an Order item + /// + /// Order item identifier + /// Order item + public virtual OrderItem GetOrderItemById(int orderItemId) { if (orderItemId == 0) return null; diff --git a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs index 3e48061380..e154dd519d 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -19,10 +20,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderTotalCalculationService : IOrderTotalCalculationService + /// + /// Order service + /// + public partial class OrderTotalCalculationService : IOrderTotalCalculationService { #region Fields @@ -98,20 +99,24 @@ public OrderTotalCalculationService(IWorkContext workContext, this._shippingSettings = shippingSettings; this._shoppingCartSettings = shoppingCartSettings; this._catalogSettings = catalogSettings; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + public Localizer T { get; set; } - /// - /// Gets shopping cart subtotal - /// - /// Cart - /// Applied discount amount - /// Applied discount - /// Sub total (without discount) - /// Sub total (with discount) + #endregion + + #region Methods + + /// + /// Gets shopping cart subtotal + /// + /// Cart + /// Applied discount amount + /// Applied discount + /// Sub total (without discount) + /// Sub total (with discount) public virtual void GetShoppingCartSubTotal(IList cart, out decimal discountAmount, out Discount appliedDiscount, out decimal subTotalWithoutDiscount, out decimal subTotalWithDiscount) @@ -613,7 +618,7 @@ public virtual decimal AdjustShippingRate(decimal shippingRate, IList cart, out So decimal taxRate = decimal.Zero; var provider = _providerManager.GetProvider(paymentMethodSystemName); - decimal paymentMethodAdditionalFee = provider.GetAdditionalHandlingFee(cart, _shoppingCartSettings.RoundPricesDuringCalculation); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); + + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } decimal paymentMethodAdditionalFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer, out taxRate); decimal paymentMethodAdditionalFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, true, customer, out taxRate); @@ -859,9 +869,9 @@ public virtual decimal GetTaxTotal(IList cart, out So int redeemedRewardPoints = 0; decimal redeemedRewardPointsAmount = decimal.Zero; List appliedGiftCards = null; + return GetShoppingCartTotal(cart, out discountAmount, out appliedDiscount, - out appliedGiftCards, - out redeemedRewardPoints, out redeemedRewardPointsAmount, ignoreRewardPonts, usePaymentMethodAdditionalFee); + out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount, ignoreRewardPonts, usePaymentMethodAdditionalFee); } /// @@ -899,9 +909,7 @@ public virtual decimal GetTaxTotal(IList cart, out So decimal subTotalWithoutDiscountBase = decimal.Zero; decimal subTotalWithDiscountBase = decimal.Zero; - GetShoppingCartSubTotal(cart, false, - out orderSubTotalDiscountAmount, out orderSubTotalAppliedDiscount, - out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); + GetShoppingCartSubTotal(cart, false, out orderSubTotalDiscountAmount, out orderSubTotalAppliedDiscount, out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); //subtotal with discount subtotalBase = subTotalWithDiscountBase; @@ -914,9 +922,14 @@ public virtual decimal GetTaxTotal(IList cart, out So if (usePaymentMethodAdditionalFee && !String.IsNullOrEmpty(paymentMethodSystemName)) { var provider = _providerManager.GetProvider(paymentMethodSystemName); - decimal paymentMethodAdditionalFee = provider.GetAdditionalHandlingFee(cart, _shoppingCartSettings.RoundPricesDuringCalculation); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } + + paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); } //tax @@ -1124,8 +1137,14 @@ public virtual int ConvertAmountToRewardPoints(decimal amount) if (amount <= 0) return 0; - if (_rewardPointsSettings.ExchangeRate > 0) - result = (int)Math.Ceiling(amount / _rewardPointsSettings.ExchangeRate); + if (_rewardPointsSettings.ExchangeRate > 0) + { + if (_rewardPointsSettings.RoundDownRewardPoints) + result = (int)Math.Floor(amount / _rewardPointsSettings.ExchangeRate); + else + result = (int)Math.Ceiling(amount / _rewardPointsSettings.ExchangeRate); + } + return result; } diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index ceec6e4ee5..27f0331be4 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -8,6 +8,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Events; +using SmartStore.Core.Localization; using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; @@ -19,10 +20,10 @@ namespace SmartStore.Services.Orders { - /// - /// Shopping cart service - /// - public partial class ShoppingCartService : IShoppingCartService + /// + /// Shopping cart service + /// + public partial class ShoppingCartService : IShoppingCartService { #region Fields @@ -112,20 +113,24 @@ public ShoppingCartService( this._genericAttributeService = genericAttributeService; this._downloadService = downloadService; this._catalogSettings = catalogSettings; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + public Localizer T { get; set; } - /// - /// Delete shopping cart item - /// - /// Shopping cart item - /// A value indicating whether to reset checkout data - /// A value indicating whether to ensure that only active checkout attributes are attached to the current customer + #endregion + + #region Methods + + /// + /// Delete shopping cart item + /// + /// Shopping cart item + /// A value indicating whether to reset checkout data + /// A value indicating whether to ensure that only active checkout attributes are attached to the current customer /// A value indicating whether to delete child cart items - public virtual void DeleteShoppingCartItem(ShoppingCartItem shoppingCartItem, bool resetCheckoutData = true, + public virtual void DeleteShoppingCartItem(ShoppingCartItem shoppingCartItem, bool resetCheckoutData = true, bool ensureOnlyActiveCheckoutAttributes = false, bool deleteChildCartItems = true) { if (shoppingCartItem == null) @@ -271,17 +276,17 @@ public virtual IList GetRequiredProductWarnings(Customer customer, //don't display specific errors from 'addToCartWarnings' variable //display only generic error - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.RequiredProductWarning"), rp.GetLocalized(x => x.Name))); + warnings.Add(T("ShoppingCart.RequiredProductWarning", rp.GetLocalized(x => x.Name))); } } else { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.RequiredProductWarning"), rp.GetLocalized(x => x.Name))); + warnings.Add(T("ShoppingCart.RequiredProductWarning", rp.GetLocalized(x => x.Name))); } } else { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.RequiredProductWarning"), rp.GetLocalized(x => x.Name))); + warnings.Add(T("ShoppingCart.RequiredProductWarning", rp.GetLocalized(x => x.Name))); } } } @@ -314,57 +319,57 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart //deleted? if (product.Deleted) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductDeleted")); + warnings.Add(T("ShoppingCart.ProductDeleted")); return warnings; } // check if the product type is available for order if (product.ProductType == ProductType.GroupedProduct) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductNotAvailableForOrder")); + warnings.Add(T("ShoppingCart.ProductNotAvailableForOrder")); } // validate bundle if (product.ProductType == ProductType.BundledProduct) { if (product.BundlePerItemPricing && customerEnteredPrice != decimal.Zero) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.NoCustomerEnteredPrice")); + warnings.Add(T("ShoppingCart.Bundle.NoCustomerEnteredPrice")); } //published? if (!product.Published) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); + warnings.Add(T("ShoppingCart.ProductUnpublished")); } //ACL if (!_aclService.Authorize(product, customer)) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); + warnings.Add(T("ShoppingCart.ProductUnpublished")); } //Store mapping if (!_storeMappingService.Authorize(product, _storeContext.CurrentStore.Id)) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); + warnings.Add(T("ShoppingCart.ProductUnpublished")); } //disabled "add to cart" button if (shoppingCartType == ShoppingCartType.ShoppingCart && product.DisableBuyButton) { - warnings.Add(_localizationService.GetResource("ShoppingCart.BuyingDisabled")); + warnings.Add(T("ShoppingCart.BuyingDisabled")); } //disabled "add to wishlist" button if (shoppingCartType == ShoppingCartType.Wishlist && product.DisableWishlistButton) { - warnings.Add(_localizationService.GetResource("ShoppingCart.WishlistDisabled")); + warnings.Add(T("ShoppingCart.WishlistDisabled")); } //call for price if (shoppingCartType == ShoppingCartType.ShoppingCart && product.CallForPrice) { - warnings.Add(_localizationService.GetResource("Products.CallForPrice")); + warnings.Add(T("Products.CallForPrice")); } //customer entered price @@ -373,11 +378,13 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart if (customerEnteredPrice < product.MinimumCustomerEnteredPrice || customerEnteredPrice > product.MaximumCustomerEnteredPrice) { - decimal minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _workContext.WorkingCurrency); - decimal maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _workContext.WorkingCurrency); - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.CustomerEnteredPrice.RangeError"), - _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, true, false), - _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, true, false))); + var minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _workContext.WorkingCurrency); + var maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _workContext.WorkingCurrency); + + warnings.Add(T("ShoppingCart.CustomerEnteredPrice.RangeError", + _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, true, false), + _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, true, false)) + ); } } @@ -385,18 +392,20 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart var hasQtyWarnings = false; if (quantity < product.OrderMinimumQuantity) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MinimumQuantity"), product.OrderMinimumQuantity)); + warnings.Add(T("ShoppingCart.MinimumQuantity", product.OrderMinimumQuantity)); hasQtyWarnings = true; } + if (quantity > product.OrderMaximumQuantity) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MaximumQuantity"), product.OrderMaximumQuantity)); + warnings.Add(T("ShoppingCart.MaximumQuantity", product.OrderMaximumQuantity)); hasQtyWarnings = true; } + var allowedQuantities = product.ParseAllowedQuatities(); if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(quantity)) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.AllowedQuantities"), string.Join(", ", allowedQuantities))); + warnings.Add(T("ShoppingCart.AllowedQuantities", string.Join(", ", allowedQuantities))); } var validateOutOfStock = shoppingCartType == ShoppingCartType.ShoppingCart || !_shoppingCartSettings.AllowOutOfStockItemsToBeAddedToWishlist; @@ -414,11 +423,12 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart { if (product.StockQuantity < quantity) { - int maximumQuantityCanBeAdded = product.StockQuantity; + var maximumQuantityCanBeAdded = product.StockQuantity; + if (maximumQuantityCanBeAdded <= 0) - warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); + warnings.Add(T("ShoppingCart.OutOfStock")); else - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded)); + warnings.Add(T("ShoppingCart.QuantityExceedsStock", maximumQuantityCanBeAdded)); } } } @@ -434,9 +444,9 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart int maximumQuantityCanBeAdded = combination.StockQuantity; if (maximumQuantityCanBeAdded <= 0) - warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); + warnings.Add(T("ShoppingCart.OutOfStock")); else - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded)); + warnings.Add(T("ShoppingCart.QuantityExceedsStock", maximumQuantityCanBeAdded)); } } } @@ -447,14 +457,14 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart } //availability dates - bool availableStartDateError = false; + var availableStartDateError = false; if (product.AvailableStartDateTimeUtc.HasValue) { DateTime now = DateTime.UtcNow; DateTime availableStartDateTime = DateTime.SpecifyKind(product.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc); if (availableStartDateTime.CompareTo(now) > 0) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); availableStartDateError = true; } } @@ -464,7 +474,7 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart DateTime availableEndDateTime = DateTime.SpecifyKind(product.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc); if (availableEndDateTime.CompareTo(now) < 0) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); } } return warnings; @@ -497,7 +507,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings( if (pv1 == null || pv1.Id != product.Id) { - warnings.Add(_localizationService.GetResource("ShoppingCart.AttributeError")); + warnings.Add(T("ShoppingCart.AttributeError")); return warnings; } } @@ -533,8 +543,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings( if (!found) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.SelectAttribute"), - pva2.TextPrompt.HasValue() ? pva2.TextPrompt : pva2.ProductAttribute.GetLocalized(a => a.Name))); + warnings.Add(T("ShoppingCart.SelectAttribute", pva2.TextPrompt.HasValue() ? pva2.TextPrompt : pva2.ProductAttribute.GetLocalized(a => a.Name))); } } } @@ -549,7 +558,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings( if (combination != null && !combination.IsActive) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); } } @@ -568,18 +577,16 @@ public virtual IList GetShoppingCartItemAttributeWarnings( foreach (var linkageWarning in linkageWarnings) { - string msg = _localizationService.GetResource("ShoppingCart.ProductLinkageAttributeWarning").FormatWith( + warnings.Add(T("ShoppingCart.ProductLinkageAttributeWarning", pvaValue.ProductVariantAttribute.ProductAttribute.GetLocalized(a => a.Name), pvaValue.GetLocalized(a => a.Name), - linkageWarning); - - warnings.Add(msg); + linkageWarning) + ); } } else { - string msg = _localizationService.GetResource("ShoppingCart.ProductLinkageProductNotLoading").FormatWith(pvaValue.LinkedProductId); - warnings.Add(msg); + warnings.Add(T("ShoppingCart.ProductLinkageProductNotLoading", pvaValue.LinkedProductId)); } } } @@ -668,27 +675,34 @@ public virtual IList GetShoppingCartItemGiftCardWarnings(ShoppingCartTyp string giftCardMessage = string.Empty; _productAttributeParser.GetGiftCardAttribute(selectedAttributes, - out giftCardRecipientName, out giftCardRecipientEmail, - out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); + out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); - if (String.IsNullOrEmpty(giftCardRecipientName)) - warnings.Add(_localizationService.GetResource("ShoppingCart.RecipientNameError")); + if (String.IsNullOrEmpty(giftCardRecipientName)) + { + warnings.Add(T("ShoppingCart.RecipientNameError")); + } if (product.GiftCardType == GiftCardType.Virtual) { - //validate for virtual gift cards only + //validate for virtual gift cards only if (String.IsNullOrEmpty(giftCardRecipientEmail) || !giftCardRecipientEmail.IsEmail()) - warnings.Add(_localizationService.GetResource("ShoppingCart.RecipientEmailError")); + { + warnings.Add(T("ShoppingCart.RecipientEmailError")); + } } - if (String.IsNullOrEmpty(giftCardSenderName)) - warnings.Add(_localizationService.GetResource("ShoppingCart.SenderNameError")); + if (String.IsNullOrEmpty(giftCardSenderName)) + { + warnings.Add(T("ShoppingCart.SenderNameError")); + } if (product.GiftCardType == GiftCardType.Virtual) { - //validate for virtual gift cards only + //validate for virtual gift cards only if (String.IsNullOrEmpty(giftCardSenderEmail) || !giftCardSenderEmail.IsEmail()) - warnings.Add(_localizationService.GetResource("ShoppingCart.SenderEmailError")); + { + warnings.Add(T("ShoppingCart.SenderEmailError")); + } } } @@ -707,19 +721,27 @@ public virtual IList GetBundleItemWarnings(ProductBundleItem bundleItem) if (bundleItem != null) { - string name = bundleItem.GetLocalizedName(); + var name = bundleItem.GetLocalizedName(); if (!bundleItem.Published) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.BundleItemUnpublished").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.BundleItemUnpublished", name)); + } if (bundleItem.ProductId == 0 || bundleItem.BundleProductId == 0 || bundleItem.Product == null || bundleItem.BundleProduct == null) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.MissingProduct").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.MissingProduct", name)); + } if (bundleItem.Quantity <= 0) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.Quantity").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.Quantity", name)); + } if (bundleItem.Product.IsDownload || bundleItem.Product.IsRecurring) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.ProductResrictions").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.ProductResrictions", name)); + } } return warnings; @@ -819,7 +841,7 @@ public virtual IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList a.TextPrompt))) warnings.Add(ca2.GetLocalized(a => a.TextPrompt)); else - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.SelectAttribute"), ca2.GetLocalized(a => a.Name))); + warnings.Add(T("ShoppingCart.SelectAttribute", ca2.GetLocalized(a => a.Name))); } } } @@ -927,7 +955,7 @@ public virtual OrganizedShoppingCartItem FindShoppingCartItemInTheCart(IList AddToCart(Customer customer, Product product, Shoppi if (cartType == ShoppingCartType.ShoppingCart && !_permissionService.Authorize(StandardPermissionProvider.EnableShoppingCart, customer)) { - warnings.Add("Shopping cart is disabled"); + warnings.Add(T("ShoppingCart.IsDisabled")); return warnings; } if (cartType == ShoppingCartType.Wishlist && !_permissionService.Authorize(StandardPermissionProvider.EnableWishlist, customer)) { - warnings.Add("Wishlist is disabled"); + warnings.Add(T("Wishlist.IsDisabled")); return warnings; } if (quantity <= 0) { - warnings.Add(_localizationService.GetResource("ShoppingCart.QuantityShouldPositive")); + warnings.Add(T("ShoppingCart.QuantityShouldPositive")); return warnings; } //if (parentItemId.HasValue && (parentItemId.Value == 0 || bundleItem == null || bundleItem.Id == 0)) //{ - // warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.BundleItemNotFound").FormatWith(bundleItem.GetLocalizedName())); + // warnings.Add(T("ShoppingCart.Bundle.BundleItemNotFound", bundleItem.GetLocalizedName())); // return warnings; //} @@ -1066,17 +1098,17 @@ public virtual List AddToCart(Customer customer, Product product, Shoppi //maximum items validation if (cartType == ShoppingCartType.ShoppingCart && cart.Count >= _shoppingCartSettings.MaximumShoppingCartItems) { - warnings.Add(_localizationService.GetResource("ShoppingCart.MaximumShoppingCartItems")); + warnings.Add(T("ShoppingCart.MaximumShoppingCartItems")); return warnings; } else if (cartType == ShoppingCartType.Wishlist && cart.Count >= _shoppingCartSettings.MaximumWishlistItems) { - warnings.Add(_localizationService.GetResource("ShoppingCart.MaximumWishlistItems")); + warnings.Add(T("ShoppingCart.MaximumWishlistItems")); return warnings; } var now = DateTime.UtcNow; - var cartItem = new ShoppingCartItem() + var cartItem = new ShoppingCartItem { ShoppingCartType = cartType, StoreId = storeId, @@ -1090,8 +1122,9 @@ public virtual List AddToCart(Customer customer, Product product, Shoppi }; if (bundleItem != null) + { cartItem.BundleItemId = bundleItem.Id; - + } if (ctx == null) { @@ -1137,7 +1170,7 @@ public virtual void AddToCart(AddToCartContext ctx) _downloadService, _catalogSettings, null, ctx.Warnings, true, ctx.BundleItemId); if (ctx.Product.ProductType == ProductType.BundledProduct && ctx.Attributes.HasValue()) - ctx.Warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.NoAttributes")); + ctx.Warnings.Add(T("ShoppingCart.Bundle.NoAttributes")); if (ctx.Product.IsGiftCard) ctx.Attributes = ctx.AttributeForm.AddGiftCardAttribute(ctx.Attributes, ctx.Product.Id, _productAttributeParser, ctx.BundleItemId); diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs index 3ba3d4c3d3..d9cd258259 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs @@ -9,7 +9,7 @@ namespace SmartStore.Services.Payments { - public static class PaymentExtentions + public static class PaymentExtentions { /// /// Is payment method active? @@ -34,25 +34,6 @@ public static bool IsPaymentMethodActive(this Provider paymentMe return paymentSettings.ActivePaymentMethodSystemNames.Contains(paymentMethod.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); } - /// - /// Gets an additional handling fee of a payment method - /// - /// Shoping cart - /// Whether to round the fee - /// Additional handling fee - public static decimal GetAdditionalHandlingFee(this Provider paymentMethod, IList cart, bool round) - { - var result = decimal.Zero; - if (paymentMethod != null) - { - result = paymentMethod.Value.GetAdditionalHandlingFee(cart); - - if (round) - result = Math.Round(result, 2); - } - return result; - } - /// /// Calculate payment method fee /// diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs index 496ed410ac..0bce57bb5e 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Domain.Payments; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; namespace SmartStore.Services.Payments @@ -59,21 +60,25 @@ public PaymentService( _providerManager = providerManager; _services = services; _typeFinder = typeFinder; - } - #endregion + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion #region Methods /// - /// Load active payment methods - /// + /// Load active payment methods + /// /// Filter payment methods by customer and apply payment method restrictions; null to load all records /// Filter payment methods by cart amount; null to load all records /// Filter payment methods by store identifier; pass 0 to load all records /// Filter payment methods by payment method types /// Provide a fallback payment method if none is active - /// Payment methods + /// Payment methods public virtual IEnumerable> LoadActivePaymentMethods( Customer customer = null, IList cart = null, @@ -128,7 +133,7 @@ public virtual IEnumerable> LoadActivePaymentMethods( else { if (DataSettings.DatabaseIsInstalled()) - throw Error.Application("At least one payment method provider is required to be active."); + throw new SmartException(T("Payment.OneActiveMethodProviderRequired")); } } @@ -253,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); } @@ -282,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); } } @@ -300,7 +308,7 @@ public virtual void PostProcessPayment(PostProcessPaymentRequest postProcessPaym { var paymentMethod = LoadPaymentMethodBySystemName(postProcessPaymentRequest.Order.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); paymentMethod.Value.PostProcessPayment(postProcessPaymentRequest); } @@ -349,8 +357,14 @@ public virtual bool CanRePostProcessPayment(Order order) public virtual decimal GetAdditionalHandlingFee(IList cart, string paymentMethodSystemName) { var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); + var paymentMethodAdditionalFee = (paymentMethod != null ? paymentMethod.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - return paymentMethod.GetAdditionalHandlingFee(cart, _shoppingCartSettings.RoundPricesDuringCalculation); + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } + + return paymentMethodAdditionalFee; } @@ -377,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 { @@ -386,7 +400,7 @@ public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePayment catch (NotSupportedException) { var result = new CapturePaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoCaptureSupport")); + result.AddError(T("Common.Payment.NoCaptureSupport")); return result; } catch @@ -432,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 { @@ -441,7 +455,7 @@ public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequ catch (NotSupportedException) { var result = new RefundPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoRefundSupport")); + result.AddError(T("Common.Payment.NoRefundSupport")); return result; } catch @@ -474,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 { @@ -483,7 +497,7 @@ public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) catch (NotSupportedException) { var result = new VoidPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoVoidSupport")); + result.AddError(T("Common.Payment.NoVoidSupport")); return result; } catch @@ -504,6 +518,7 @@ public virtual RecurringPaymentType GetRecurringPaymentType(string paymentMethod var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return RecurringPaymentType.NotSupported; + return paymentMethod.Value.RecurringPaymentType; } @@ -526,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 { @@ -535,7 +550,7 @@ public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReques catch (NotSupportedException) { var result = new ProcessPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoRecurringPaymentSupport")); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); return result; } catch @@ -557,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 { @@ -566,7 +581,7 @@ public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurri catch (NotSupportedException) { var result = new CancelRecurringPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoRecurringPaymentSupport")); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); return result; } catch @@ -587,6 +602,7 @@ public virtual PaymentMethodType GetPaymentMethodType(string paymentMethodSystem var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return PaymentMethodType.Unknown; + return paymentMethod.Value.PaymentMethodType; } 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/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 3ea2815466..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; } diff --git a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs index 886196b240..4f1032077a 100644 --- a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs +++ b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs @@ -50,6 +50,7 @@ public partial class StandardPermissionProvider : IPermissionProvider public static readonly PermissionRecord UploadPictures = new PermissionRecord { Name = "Admin area. Upload Pictures", SystemName = "UploadPictures", Category = "Configuration" }; public static readonly PermissionRecord ManageScheduleTasks = new PermissionRecord { Name = "Admin area. Manage Schedule Tasks", SystemName = "ManageScheduleTasks", Category = "Configuration" }; public static readonly PermissionRecord ManageExports = new PermissionRecord { Name = "Admin area. Manage Exports", SystemName = "ManageExports", Category = "Configuration" }; + public static readonly PermissionRecord ManageImports = new PermissionRecord { Name = "Admin area. Manage Imports", SystemName = "ManageImports", Category = "Configuration" }; public static readonly PermissionRecord ManageUrlRecords = new PermissionRecord { Name = "Admin area. Manage Url Records", SystemName = "ManageUrlRecords", Category = "Configuration" }; //public store permissions @@ -105,6 +106,7 @@ public virtual IEnumerable GetPermissions() UploadPictures, ManageScheduleTasks, ManageExports, + ManageImports, ManageUrlRecords, DisplayPrices, EnableShoppingCart, @@ -164,6 +166,7 @@ public virtual IEnumerable GetDefaultPermissions() UploadPictures, ManageScheduleTasks, ManageExports, + ManageImports, ManageUrlRecords, DisplayPrices, EnableShoppingCart, diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index e6cc541ac4..29c29d13ef 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -243,7 +243,7 @@ public static string ValidateSeName(this T entity, } } - // validate and alter sename if it could be interpreted as SEO code + // validate and alter SeName if it could be interpreted as SEO code if (LocalizationHelper.IsValidCultureCode(seName)) { if (seName.Length == 2) @@ -304,7 +304,8 @@ public static string GetSeName(string name, SeoSettings seoSettings) return SeoHelper.GetSeName( name, seoSettings == null ? false : seoSettings.ConvertNonWesternChars, - seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls); + seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls, + seoSettings == null ? null : seoSettings.SeoNameCharConversion); } #endregion diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index 337cf5bd98..38fe4a7bda 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -68,7 +68,7 @@ public virtual IList GetUrlRecordsByIds(int[] urlRecordIds) if (urlRecordIds == null || urlRecordIds.Length == 0) return new List(); - var urlRecords = _urlRecordRepository + var urlRecords = _urlRecordRepository.Table .Where(x => urlRecordIds.Contains(x.Id)) .ToList(); @@ -165,9 +165,9 @@ where x.IsActive orderby x.Id descending select x; - var result = query.ToDictionary( - x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), // Key - x => x.Slug, // Value + var result = query.ToDictionarySafe( + x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), + x => x.Slug, StringComparer.OrdinalIgnoreCase); return result; diff --git a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs index 981e64523f..4f6db7fa9c 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs @@ -9,12 +9,12 @@ 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 @@ -32,7 +32,6 @@ public partial class ShippingService : IShippingService private readonly IProductService _productService; private readonly ICheckoutAttributeParser _checkoutAttributeParser; private readonly IGenericAttributeService _genericAttributeService; - private readonly ILocalizationService _localizationService; private readonly ShippingSettings _shippingSettings; private readonly IEventPublisher _eventPublisher; private readonly ShoppingCartSettings _shoppingCartSettings; @@ -67,7 +66,6 @@ public ShippingService( IProductService productService, ICheckoutAttributeParser checkoutAttributeParser, IGenericAttributeService genericAttributeService, - ILocalizationService localizationService, ShippingSettings shippingSettings, IEventPublisher eventPublisher, ShoppingCartSettings shoppingCartSettings, @@ -81,26 +79,29 @@ public ShippingService( this._productService = productService; this._checkoutAttributeParser = checkoutAttributeParser; this._genericAttributeService = genericAttributeService; - this._localizationService = localizationService; this._shippingSettings = shippingSettings; 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); @@ -126,7 +127,7 @@ public virtual IEnumerable> LoadActiveS else { if (DataSettings.DatabaseIsInstalled()) - throw Error.Application("At least one shipping method provider is required to be active."); + throw new SmartException(T("Shipping.OneActiveMethodProviderRequired")); } } @@ -316,11 +317,11 @@ public virtual decimal GetShoppingCartTotalWeight(IList(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); @@ -331,6 +332,7 @@ public virtual decimal GetShoppingCartTotalWeight(IList 0 && result.Errors.Count > 0) result.Errors.Clear(); } - - //no shipping options loaded - if (result.ShippingOptions.Count == 0 && result.Errors.Count == 0) - result.Errors.Add(_localizationService.GetResource("Checkout.ShippingOptionCouldNotBeLoaded")); + + //no shipping options loaded + if (result.ShippingOptions.Count == 0 && result.Errors.Count == 0) + { + result.Errors.Add(T("Checkout.ShippingOptionCouldNotBeLoaded")); + } return result; } diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 9276e94dc0..1edfe58738 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -104,9 +104,9 @@ ..\..\packages\ncrontab.2.0.0\lib\net20\NCrontab.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -166,62 +166,77 @@ Properties\AssemblyVersionInfo.cs + + - - + + + + + - - - - - + + + + + - - + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + - - - + + + + @@ -350,7 +365,6 @@ - @@ -369,7 +383,6 @@ - diff --git a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs index da282801ef..c2ccb86f17 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs @@ -141,12 +141,12 @@ public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where { if (selectedStoreIds != null && selectedStoreIds.Contains(store.Id)) { - if (existingStoreMappings.Where(sm => sm.StoreId == store.Id).Count() == 0) + if (!existingStoreMappings.Any(x => x.StoreId == store.Id)) InsertStoreMapping(entity, store.Id); } else { - var storeMappingToDelete = existingStoreMappings.Where(sm => sm.StoreId == store.Id).FirstOrDefault(); + var storeMappingToDelete = existingStoreMappings.FirstOrDefault(x => x.StoreId == store.Id); if (storeMappingToDelete != null) DeleteStoreMapping(storeMappingToDelete); } @@ -185,7 +185,7 @@ public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseE int entityId = entity.Id; string entityName = typeof(T).Name; - var storeMapping = new StoreMapping() + var storeMapping = new StoreMapping { EntityId = entityId, EntityName = entityName, @@ -219,7 +219,7 @@ public virtual void UpdateStoreMapping(StoreMapping storeMapping) public virtual int[] GetStoresIdsWithAccess(T entity) where T : BaseEntity, IStoreMappingSupported { if (entity == null) - throw new ArgumentNullException("entity"); + return new int[0]; int entityId = entity.Id; string entityName = typeof(T).Name; diff --git a/src/Libraries/SmartStore.Services/Stores/StoreService.cs b/src/Libraries/SmartStore.Services/Stores/StoreService.cs index 16daad6591..a773079402 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -177,6 +177,8 @@ public virtual bool IsStoreDataValid(Store store) { case "www.yourstore.com": case "yourstore.com": + case "www.mystore.com": + case "mystore.com": case "www.mein-shop.de": case "mein-shop.de": return false; diff --git a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs index 25243b0ad5..c35b4b7128 100644 --- a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs +++ b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs @@ -59,14 +59,16 @@ public static string GetFriendlyDescription(string expression) Use24HourTimeFormat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.AMDesignator.IsEmpty() }; - try - { - return ExpressionDescriptor.GetDescription(expression, options); - } - catch + if (expression.HasValue()) { - return null; + try + { + return ExpressionDescriptor.GetDescription(expression, options); + } + catch { } } + + return "?"; } } diff --git a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs index e5b6db97de..3329fbf3a7 100644 --- a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs +++ b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs @@ -17,24 +17,35 @@ namespace SmartStore.Services.Tasks public class InitializeSchedulerFilter : IAuthorizationFilter { private readonly static object s_lock = new object(); + private static int s_errCount; private static bool s_initializing = false; public void OnAuthorization(AuthorizationContext filterContext) { - lock (s_lock) + if (filterContext == null || filterContext.HttpContext == null) + return; + + var request = filterContext.HttpContext.Request; + if (request == null) + return; + + if (filterContext.IsChildAction) + return; + + lock (s_lock) { if (!s_initializing) { s_initializing = true; - var logger = EngineContext.Current.Resolve(); + ILogger logger = EngineContext.Current.Resolve(); + ITaskScheduler taskScheduler = EngineContext.Current.Resolve(); try { var taskService = EngineContext.Current.Resolve(); var storeService = EngineContext.Current.Resolve(); var eventPublisher = EngineContext.Current.Resolve(); - var taskScheduler = EngineContext.Current.Resolve(); var tasks = taskService.GetAllTasks(true); taskService.CalculateFutureSchedules(tasks, true /* isAppStart */); @@ -59,11 +70,23 @@ public void OnAuthorization(AuthorizationContext filterContext) } catch (Exception ex) { + s_errCount++; + s_initializing = false; logger.Error("Error while initializing Task Scheduler", ex); } finally { - GlobalFilters.Filters.Remove(this); + var tooManyFailures = s_errCount >= 10; + + if (tooManyFailures || (taskScheduler != null && taskScheduler.IsActive)) + { + GlobalFilters.Filters.Remove(this); + } + + if (tooManyFailures && logger != null) + { + logger.Warning("Stopped trying to initialize the Task Scheduler: too many failed attempts in succession (10+). Maybe uncommenting the setting 'sm:TaskSchedulerBaseUrl' in web.config solves the problem?"); + } } } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index 8efba4376f..9b0f1aaad5 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -57,10 +57,17 @@ public T ResolveNamed(string name) where T : class /// 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) { - float fraction = (float)value / (float)Math.Max(maximum, 1f); - int percentage = (int)Math.Round(fraction * 100f, 0); + 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); + SetProgress(Math.Min(Math.Max(percentage, 0), 100), message, immediately); + } } /// diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs index 18408a15c0..8fd93c2956 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -1,21 +1,22 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using Autofac; +using SmartStore.Core; using SmartStore.Core.Async; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services.Customers; -using SmartStore.Core; -using SmartStore.Core.Domain.Customers; -using System.Diagnostics; -using System.Collections.Generic; namespace SmartStore.Services.Tasks { - public class TaskExecutor : ITaskExecutor + public class TaskExecutor : ITaskExecutor { private readonly IScheduleTaskService _scheduledTaskService; private readonly IDbContext _dbContext; @@ -24,6 +25,8 @@ public class TaskExecutor : ITaskExecutor private readonly Func _taskResolver; private readonly IComponentContext _componentContext; + public const string CurrentCustomerIdParamName = "CurrentCustomerId"; + public TaskExecutor( IScheduleTaskService scheduledTaskService, IDbContext dbContext, @@ -40,11 +43,13 @@ public TaskExecutor( this._taskResolver = taskResolver; Logger = NullLogger.Instance; - } + T = NullLocalizer.Instance; + } public ILogger Logger { get; set; } + public Localizer T { get; set; } - public void Execute( + public void Execute( ScheduleTask task, IDictionary taskParameters = null, bool throwOnError = false) @@ -82,8 +87,20 @@ public void Execute( try { - // set background task system customer as current customer - var customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); + Customer customer = null; + + // try virtualize current customer (which is necessary when user manually executes a task) + if (taskParameters != null && taskParameters.ContainsKey(CurrentCustomerIdParamName)) + { + customer = _customerService.GetCustomerById(taskParameters[CurrentCustomerIdParamName].ToInt()); + } + + if (customer == null) + { + // no virtualization: set background task system customer as current customer + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); + } + _workContext.CurrentCustomer = customer; // create task instance @@ -112,12 +129,17 @@ public void Execute( instance.Execute(ctx); } - catch (Exception ex) + catch (Exception exception) { faulted = true; - canceled = ex is OperationCanceledException; - Logger.Error(string.Format("Error while running scheduled task '{0}'. {1}", task.Name, ex.Message), ex); - lastError = ex.Message.Truncate(995, "..."); + canceled = exception is OperationCanceledException; + lastError = exception.Message.Truncate(995, "..."); + + if (canceled) + Logger.Warning(T("Admin.System.ScheduleTasks.Cancellation", task.Name), exception); + else + Logger.Error(string.Concat(T("Admin.System.ScheduleTasks.RunningError", task.Name), ": ", exception.Message), exception); + if (throwOnError) { throw; diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs index 22cecb7046..0e055029a3 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs @@ -11,10 +11,12 @@ 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; @@ -23,7 +25,6 @@ public class DefaultTaskScheduler : DisposableObject, ITaskScheduler, IRegistere private System.Timers.Timer _timer; private bool _shuttingDown; private int _errCount; - private readonly ConcurrentDictionary _authTokens = new ConcurrentDictionary(); public DefaultTaskScheduler() { @@ -48,7 +49,7 @@ public string BaseUrl { CheckUrl(value); _baseUrl = value.TrimEnd('/', '\\'); - } + } } public void Start() @@ -93,13 +94,35 @@ public CancellationTokenSource GetCancelTokenSourceFor(int scheduleTaskId) return cts; } + private string CreateAuthToken() + { + string authToken = Guid.NewGuid().ToString(); + + var cacheManager = EngineContext.Current.Resolve("static"); + cacheManager.Set(GenerateAuthTokenCacheKey(authToken), true, 1); + + return authToken; + } + + private string GenerateAuthTokenCacheKey(string authToken) + { + return "Scheduler.AuthToken." + authToken; + } + public bool VerifyAuthToken(string authToken) { if (authToken.IsEmpty()) return false; - bool val = false; - return _authTokens.TryRemove(authToken, out val); + var cacheManager = EngineContext.Current.Resolve("static"); + var cacheKey = GenerateAuthTokenCacheKey(authToken); + if (cacheManager.Contains(cacheKey)) + { + cacheManager.Remove(cacheKey); + return true; + } + + return false; } public void RunSingleTask(int scheduleTaskId, IDictionary taskParameters = null) @@ -108,12 +131,12 @@ public void RunSingleTask(int scheduleTaskId, IDictionary taskPa if (taskParameters != null && taskParameters.Any()) { - var qs = new QueryString(); + var qs = new QueryString(); taskParameters.Each(x => qs.Add(x.Key, x.Value)); query = qs.ToString(); } - CallEndpoint("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query)); + CallEndpoint(new Uri("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query))); } private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) @@ -131,7 +154,7 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) _intervalFixed = true; } - CallEndpoint(_baseUrl + "/Sweep"); + CallEndpoint(new Uri(_baseUrl + "/Sweep")); } } finally @@ -140,26 +163,25 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) } } - protected internal virtual void CallEndpoint(string url) + protected internal virtual void CallEndpoint(Uri uri) { if (_shuttingDown) return; - - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = "SmartStore.NET"; - req.Method = "POST"; + + var req = WebHelper.CreateHttpRequestForSafeLocalCall(uri); + req.Method = "POST"; req.ContentType = "text/plain"; req.ContentLength = 0; + req.Timeout = 10000; // 10 sec. - string authToken = Guid.NewGuid().ToString(); - _authTokens.TryAdd(authToken, true); + string authToken = CreateAuthToken(); req.Headers.Add("X-AUTH-TOKEN", authToken); - req.GetResponseAsync().ContinueWith(t => - { + req.GetResponseAsync().ContinueWith(t => + { if (t.IsFaulted) { - HandleException(t.Exception, url); + HandleException(t.Exception, uri); _errCount++; if (_errCount >= 10) { @@ -174,34 +196,48 @@ protected internal virtual void CallEndpoint(string url) else { _errCount = 0; - t.Result.Dispose(); + var response = t.Result; + + //using (var logger = new TraceLogger()) + //{ + // logger.Debug("TaskScheduler Sweep called successfully: {0}".FormatCurrent(response.GetResponseStream().AsString())); + //} + + response.Dispose(); } - }); - } + }); + } - private void HandleException(AggregateException exception, string url) + private void HandleException(AggregateException exception, Uri uri) { using (var logger = new TraceLogger()) { - string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(url); + string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(uri.OriginalString); var wex = exception.InnerExceptions.OfType().FirstOrDefault(); if (wex == null) { - logger.Error(msg, exception); + logger.Error(msg, exception.InnerException); + } + else if (wex.Response == null) + { + logger.Error(msg, wex); } else { using (var response = wex.Response as HttpWebResponse) { - msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + if (response != null) + { + msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + } logger.Error(msg); } } } } - private void CheckUrl(string url) + private void CheckUrl(string url) { if (!url.IsWebUrl()) { diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index 7f5b29df78..d078b22719 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -13,7 +13,7 @@ - + diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 383de5f866..8237c4a7ad 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Pay with Amazon SystemName: SmartStore.AmazonPay -Version: 2.2.0.3 +Version: 2.6.0 Group: Payment -MinAppVersion: 2.2.0 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.AmazonPay.dll diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml index 63b8f6b811..fbe79d7119 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml @@ -11,11 +11,11 @@ Registrieren Sie sich zunächst bei Amazon Payments.

So richten Sie "Bezahlen mit Amazon" ein:

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

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

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

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

      How to set up "Pay with Amazon":

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

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

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

      • Option 1: Pay with Amazon: Pay now with the payment and shipping information from your Amazon account.
      • Option 2: Already Amazon customer? Pay now with the payment and shipping information from your Amazon account.
      • @@ -131,7 +131,7 @@ Text suggestions:
          Updating the payment status - Determines the method used to update the payment status. + Specifies the method used to update the payment status. IPN (instant payment notification) requires valid SSL certificate to be installed on this server. Pay attention that the SSL certificate must be issued by a trusted Certificate Authority, self-signed certificates are not permittted. @@ -152,7 +152,7 @@ Text suggestions:
            Payment action - Determines when to debit the customer account. + Specifies when to debit the customer account. Immediately debit @@ -167,7 +167,7 @@ Text suggestions:
              Apply customer data - Determines whether and when the Amazon email address and telephone number of the customer should be saved. + Specifies whether and when the Amazon email address and telephone number of the customer should be saved. Only if empty @@ -179,7 +179,7 @@ Text suggestions:
                Show button in mini shopping cart - Determines to show the "Pay with Amazon" button in the mini shopping cart too. + Specifies to show the "Pay with Amazon" button in the mini shopping cart too. Width of address widget @@ -218,36 +218,36 @@ Text suggestions:
                  Additional fee percentage - Determines whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. + Specifies whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. Create order notes - Determines that order notes should be created in context of the data exchange with Amazon Payments. + Specifies that order notes should be created in context of the data exchange with Amazon Payments. Frequency (in minutes) - Determines how often status of the different object shall be polled from Amazon Payments servers. + Specifies how often status of the different object shall be polled from Amazon Payments servers. Maximal order age (in days) - Determines that only orders which are not older than x days to be included in payment data updates. + Specifies that only orders which are not older than x days to be included in payment data updates. Inform about a refusal of an authorization - Determines to create order notes in case of a declination of an Amazon payment, which are visible for customers too. In addition the customer is informed by email about the case. + Specifies to create order notes in case of a declination of an Amazon payment, which are visible for customers too. In addition the customer is informed by email about the case. Append error message - Determines to append the error message to the order note and email. + Specifies to append the error message to the order note and email. \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/web.config b/src/Plugins/SmartStore.AmazonPay/web.config index 0e57dde0c1..3fd5b09a23 100644 --- a/src/Plugins/SmartStore.AmazonPay/web.config +++ b/src/Plugins/SmartStore.AmazonPay/web.config @@ -1,131 +1,131 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + 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/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/Views/SmsClickatell/Configure.cshtml b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml index adee89c127..03b16cb275 100644 --- a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml +++ b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml @@ -1,8 +1,8 @@ -@{ - Layout = ""; -} +@using SmartStore.Web.Framework; @model SmartStore.Clickatell.Models.SmsClickatellModel -@using SmartStore.Web.Framework; +@{ + Layout = ""; +}
@@ -69,8 +69,8 @@ -
-

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

+
+
@T("Plugins.Sms.Clickatell.SendTest.Hint")
diff --git a/src/Plugins/SmartStore.Clickatell/web.config b/src/Plugins/SmartStore.Clickatell/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.Clickatell/web.config +++ b/src/Plugins/SmartStore.Clickatell/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs index 91f0a57419..4e4cb6d96d 100644 --- a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs @@ -1,12 +1,5 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Web.Mvc; -using System.Reflection; using Autofac; -using Autofac.Core; using Autofac.Integration.Mvc; -using SmartStore.Core.Plugins; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; @@ -16,13 +9,13 @@ namespace SmartStore.DevTools { - public class DependencyRegistrar : IDependencyRegistrar + public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { builder.RegisterType().As().InstancePerRequest(); - if (isActiveModule) + if (isActiveModule && DataSettings.DatabaseIsInstalled()) { // intercept ALL public store controller actions builder.RegisterType().AsActionFilterFor(); diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index c2fd9f3fd6..b169d8b16a 100644 --- a/src/Plugins/SmartStore.DevTools/Description.txt +++ b/src/Plugins/SmartStore.DevTools/Description.txt @@ -1,8 +1,8 @@ FriendlyName: SmartStore.NET Developer Tools (MiniProfiler and other goodies) SystemName: SmartStore.DevTools Group: Developer -Version: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.DevTools.dll ResourceRootKey: Plugins.Developer.DevTools \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs index e1c5693558..c2e585d0eb 100644 --- a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs +++ b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs @@ -1,5 +1,6 @@ using System; using System.Web; +using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using StackExchange.Profiling; @@ -43,6 +44,11 @@ private static bool ShouldProfile(HttpApplication app) if (app.Context == null || app.Context.Request == null) return false; + if (!DataSettings.DatabaseIsInstalled()) + { + return false; + } + var url = app.Context.Request.AppRelativeCurrentExecutionFilePath; if (url.StartsWith("~/admin", StringComparison.InvariantCultureIgnoreCase) || url.StartsWith("~/mini-profiler", StringComparison.InvariantCultureIgnoreCase) || url.StartsWith("~/bundles", StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/Plugins/SmartStore.DevTools/Web.config b/src/Plugins/SmartStore.DevTools/Web.config index e1ac213bd8..b076d239ac 100644 --- a/src/Plugins/SmartStore.DevTools/Web.config +++ b/src/Plugins/SmartStore.DevTools/Web.config @@ -1,135 +1,135 @@ - + -
+
- - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + 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/SmartStore.DiscountRules.csproj b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj index 813caac6a0..dc162de8da 100644 --- a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj +++ b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj @@ -85,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 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 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.DiscountRules/web.config +++ b/src/Plugins/SmartStore.DiscountRules/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs index abc3a709bd..5a99884991 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs @@ -138,10 +138,10 @@ private ActionResult LoginInternal(string returnUrl, bool verifyResponse) if (result.Result != null) return result.Result; - return HttpContext.Request.IsAuthenticated ? - new RedirectResult(!string.IsNullOrEmpty(returnUrl) ? returnUrl : "~/") : + return HttpContext.Request.IsAuthenticated ? + RedirectToReferrer(returnUrl, "~/") : new RedirectResult(Url.LogOn(returnUrl)); - } + } public ActionResult Login(string returnUrl) { diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs new file mode 100644 index 0000000000..5f0960101b --- /dev/null +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Data; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Web; +using System.Web.Script.Serialization; +using DotNetOpenAuth.AspNet; +using DotNetOpenAuth.AspNet.Clients; +using Newtonsoft.Json; + +namespace SmartStore.FacebookAuth.Core +{ + /// + /// + internal class FacebookOAuth2Client : OAuth2Client + { + /// + /// The authorization endpoint. + /// + private const string AuthorizationEndpoint = "https://www.facebook.com/v2.8/dialog/oauth"; + /// + /// The token endpoint. + /// + private const string TokenEndpoint = "https://graph.facebook.com/v2.8/oauth/access_token"; + /// + /// The user info endpoint. + /// + private const string UserInfoEndpoint = "https://graph.facebook.com/v2.8/me"; + /// + /// The app id. + /// + private readonly string _appId; + /// + /// The app secret. + /// + private readonly string _appSecret; + + /// + /// The requested scopes. + /// + private readonly string[] _requestedScopes; + + + /// + /// Creates a new Facebook OAuth2 client, requesting the default "email" scope. + /// + /// The Facebook App Id + /// The Facebook App Secret + public FacebookOAuth2Client(string appId, string appSecret) + : this(appId, appSecret, new[] { "email" }) { } + + /// + /// Creates a new Facebook OAuth2 client. + /// + /// The Facebook App Id + /// The Facebook App Secret + /// One or more requested scopes, passed without the base URI. + public FacebookOAuth2Client(string appId, string appSecret, params string[] requestedScopes) + : base("facebook") + { + if (string.IsNullOrWhiteSpace(appId)) + throw new ArgumentNullException("appId"); + + if (string.IsNullOrWhiteSpace(appSecret)) + throw new ArgumentNullException("appSecret"); + + if (requestedScopes == null) + throw new ArgumentNullException("requestedScopes"); + + if (requestedScopes.Length == 0) + throw new ArgumentException("One or more scopes must be requested.", "requestedScopes"); + + _appId = appId; + _appSecret = appSecret; + _requestedScopes = requestedScopes; + } + + public override void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; + context.Response.Redirect(redirectUrl, endResponse: true); + } + + public new AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + throw new NoNullAllowedException(); + } + + public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl) + { + string code = context.Request.QueryString["code"]; + if (string.IsNullOrEmpty(code)) + { + return AuthenticationResult.Failed; + } + + string accessToken = this.QueryAccessToken(returnPageUrl, code); + if (accessToken == null) + { + return AuthenticationResult.Failed; + } + + IDictionary userData = this.GetUserData(accessToken); + if (userData == null) + { + return AuthenticationResult.Failed; + } + + string id = userData["id"]; + string name; + + // Some oAuth providers do not return value for the 'username' attribute. + // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' + if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) + { + name = id; + } + + // add the access token to the user data dictionary just in case page developers want to use it + userData["accesstoken"] = accessToken; + + return new AuthenticationResult( + isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData); + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + var state = string.IsNullOrEmpty(returnUrl.Query) ? string.Empty : returnUrl.Query.Substring(1); + + return BuildUri(AuthorizationEndpoint, new NameValueCollection + { + { "client_id", _appId }, + { "scope", string.Join(" ", _requestedScopes) }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + { "state", state }, + }); + } + + protected override IDictionary GetUserData(string accessToken) + { + var uri = BuildUri(UserInfoEndpoint, new NameValueCollection { { "access_token", accessToken } }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + { + if (stream == null) + return null; + + using (var textReader = new StreamReader(stream)) + { + var json = textReader.ReadToEnd(); + var extraData = JsonConvert.DeserializeObject>(json); + var data = extraData.ToDictionary(x => x.Key, x => x.Value.ToString()); + + data.Add("picture", string.Format("https://graph.facebook.com/{0}/picture", data["id"])); + + return data; + } + } + } + + public string QueryAccessTokenByCode(Uri returnUrl, string authorizationCode) + { + return this.QueryAccessToken(returnUrl, authorizationCode); + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + var uri = BuildUri(TokenEndpoint, new NameValueCollection + { + { "code", authorizationCode }, + { "client_id", _appId }, + { "client_secret", _appSecret }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + string accessToken = null; + HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse(); + + // handle response from FB + // this will not be a url with params like the first request to get the 'code' + Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); + + using (StreamReader sr = new StreamReader(response.GetResponseStream(), rEncoding)) + { + var serializer = new JavaScriptSerializer(); + var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); + var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); + + Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); + accessToken = desirializedJsonObject["access_token"].ToString(); + } + return accessToken; + } + + private static Uri BuildUri(string baseUri, NameValueCollection queryParameters) + { + var keyValuePairs = queryParameters.AllKeys.Select(k => HttpUtility.UrlEncode(k) + "=" + HttpUtility.UrlEncode(queryParameters[k])); + var qs = String.Join("&", keyValuePairs); + + var builder = new UriBuilder(baseUri) { Query = qs }; + return builder.Uri; + } + + /// + /// Facebook works best when return data be packed into a "state" parameter. + /// This should be called before verifying the request, so that the url is rewritten to support this. + /// + public static void RewriteRequest() + { + var ctx = HttpContext.Current; + + var stateString = HttpUtility.UrlDecode(ctx.Request.QueryString["state"]); + if (stateString == null || !stateString.Contains("__provider__=facebook")) + return; + + var q = HttpUtility.ParseQueryString(stateString); + q.Add(ctx.Request.QueryString); + q.Remove("state"); + + ctx.RewritePath(ctx.Request.Path + "?" + q); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs index 7bb6234362..378599e6d5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs @@ -2,19 +2,22 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Web; using System.Web.Mvc; using DotNetOpenAuth.AspNet; using DotNetOpenAuth.AspNet.Clients; +using Newtonsoft.Json.Linq; using SmartStore.Core.Domain.Customers; using SmartStore.Services; using SmartStore.Services.Authentication.External; namespace SmartStore.FacebookAuth.Core { - public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer + public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer { #region Fields @@ -24,7 +27,7 @@ public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer private readonly HttpContextBase _httpContext; private readonly ICommonServices _services; - private FacebookClient _facebookApplication; + private FacebookOAuth2Client _facebookApplication; #endregion @@ -47,7 +50,7 @@ public FacebookProviderAuthorizer(IExternalAuthorizer authorizer, #region Utilities - private FacebookClient FacebookApplication + private FacebookOAuth2Client FacebookApplication { get { @@ -55,7 +58,7 @@ private FacebookClient FacebookApplication { var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - _facebookApplication = new FacebookClient(settings.ClientKeyIdentifier, settings.ClientSecret); + _facebookApplication = new FacebookOAuth2Client(settings.ClientKeyIdentifier, settings.ClientSecret); } return _facebookApplication; @@ -90,18 +93,47 @@ private AuthorizeState VerifyAuthentication(string returnUrl) } var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); - var error = authResult.Error != null ? authResult.Error.Message : "Unknown error"; - state.AddError(error); - return state; + + state.AddError(authResult.Error != null + ? authResult.Error.Message + : _services.Localization.GetResource("Admin.Common.UnknownError")); + + return state; } + private string GetEmailFromFacebook(string accessToken) + { + var result = ""; + var webRequest = WebRequest.Create("https://graph.facebook.com/me?fields=email&access_token=" + EscapeUriDataStringRfc3986(accessToken)); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + using (var reader = new StreamReader(stream)) + { + var strResponse = reader.ReadToEnd(); + var info = JObject.Parse(strResponse); + + if (info["email"] != null) + { + result = info["email"].ToString(); + } + } + return result; + } + private void ParseClaims(AuthenticationResult authenticationResult, OAuthAuthenticationParameters parameters) { var claims = new UserClaims(); claims.Contact = new ContactClaims(); - + if (authenticationResult.ExtraData.ContainsKey("username")) + { claims.Contact.Email = authenticationResult.ExtraData["username"]; + } + else + { + claims.Contact.Email = GetEmailFromFacebook(authenticationResult.ExtraData["accesstoken"]); + } claims.Name = new NameClaims(); diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index f7cc05d671..d60a7a363b 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Description.txt +++ b/src/Plugins/SmartStore.FacebookAuth/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Facebook SystemName: SmartStore.FacebookAuth Group: Security -Version: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0.1 +MinAppVersion: 2.5.0 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll ResourceRootKey: Plugins.ExternalAuth.Facebook \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 734ef16423..85992eb451 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -121,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 @@ -179,6 +180,7 @@ Properties\AssemblyVersionInfo.cs + diff --git a/src/Plugins/SmartStore.FacebookAuth/packages.config b/src/Plugins/SmartStore.FacebookAuth/packages.config index 7d02692d69..2cd39833bf 100644 --- a/src/Plugins/SmartStore.FacebookAuth/packages.config +++ b/src/Plugins/SmartStore.FacebookAuth/packages.config @@ -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 6b304e839a..68523a15bc 100644 --- a/src/Plugins/SmartStore.FacebookAuth/web.config +++ b/src/Plugins/SmartStore.FacebookAuth/web.config @@ -1,120 +1,120 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index 2e32945c39..59bf21b018 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt +++ b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Analytics SystemName: SmartStore.GoogleAnalytics Group: Analytics -Version: 2.2.2.2 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll ResourceRootKey: Plugins.Widgets.GoogleAnalytics diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml index 627698df96..d7f6055f92 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml @@ -5,13 +5,14 @@ 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 2ac97f8895..d54877d501 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.en-us.xml @@ -5,14 +5,14 @@ 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/web.config b/src/Plugins/SmartStore.GoogleAnalytics/web.config index 32347fc005..c0f82db177 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/web.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/web.config @@ -1,115 +1,115 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs index b6c3684093..2dbc226f57 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs @@ -7,17 +7,13 @@ public class AdminMenu : AdminMenuProvider { protected override void BuildMenuCore(TreeNode pluginsNode) { - var root = pluginsNode.SelectNode(x => x.Value.Id == "promotion-feeds"); - if (root == null) - return; - var menuItem = new MenuItem().ToBuilder() .Text("Google Merchant Center") .ResKey("Plugins.FriendlyName.SmartStore.GoogleMerchantCenter") .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) .ToItem(); - root.Append(menuItem); + pluginsNode.Prepend(menuItem); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs index 74b368fe98..6c37461c87 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs @@ -36,21 +36,31 @@ public ActionResult ProductEditTab(int productId) var culture = CultureInfo.InvariantCulture; var model = new GoogleProductModel { ProductId = productId }; var entity = _googleFeedService.GetGoogleProductRecord(productId); + string notSpecified = T("Common.Unspecified"); if (entity != null) { model.Taxonomy = entity.Taxonomy; model.Gender = entity.Gender; model.AgeGroup = entity.AgeGroup; + model.IsAdult = entity.IsAdult; model.Color = entity.Color; model.Size = entity.Size; model.Material = entity.Material; model.Pattern = entity.Pattern; - model.Exporting = entity.Export; + model.Export2 = entity.Export; + model.Multipack2 = entity.Multipack; + model.IsBundle = entity.IsBundle; + model.EnergyEfficiencyClass = entity.EnergyEfficiencyClass; + model.CustomLabel0 = entity.CustomLabel0; + model.CustomLabel1 = entity.CustomLabel1; + model.CustomLabel2 = entity.CustomLabel2; + model.CustomLabel3 = entity.CustomLabel3; + model.CustomLabel4 = entity.CustomLabel4; } else { - model.Exporting = true; + model.Export2 = true; } ViewBag.DefaultCategory = ""; @@ -58,8 +68,13 @@ public ActionResult ProductEditTab(int productId) ViewBag.DefaultSize = ""; ViewBag.DefaultMaterial = ""; ViewBag.DefaultPattern = ""; - ViewBag.DefaultGender = T("Common.Auto"); - ViewBag.DefaultAgeGroup = T("Common.Auto"); + ViewBag.DefaultGender = notSpecified; + ViewBag.DefaultAgeGroup = notSpecified; + ViewBag.DefaultIsAdult = ""; + ViewBag.DefaultMultipack2 = ""; + ViewBag.DefaultIsBundle = ""; + ViewBag.DefaultEnergyEfficiencyClass = notSpecified; + ViewBag.DefaultCustomLabel = ""; // we do not have export profile context here, so we simply use the first profile var profile = _exportService.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName).FirstOrDefault(); @@ -99,6 +114,11 @@ public ActionResult ProductEditTab(int productId) new SelectListItem { Value = "kids", Text = T("Plugins.Feed.Froogle.AgeGroupKids") }, }; + ViewBag.AvailableEnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text + .SplitSafe(",") + .Select(x => new SelectListItem { Value = x, Text = x }) + .ToList(); + var result = PartialView(model); result.ViewData.TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "CustomProperties[GMC]" }; return result; @@ -114,8 +134,9 @@ public ActionResult Configure() { var model = new FeedGoogleMerchantCenterModel(); - model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); model.GridPageSize = _adminAreaSettings.GridPageSize; + model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); + model.EnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text.SplitSafe(","); return View(model); } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs index 5b4e0817ab..98aaec1aa4 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs @@ -9,6 +9,14 @@ public GoogleProductRecordMap() { this.ToTable("GoogleProduct"); this.HasKey(x => x.Id); - } + + this.Property(x => x.EnergyEfficiencyClass).HasMaxLength(50); + + this.Property(x => x.CustomLabel0).HasMaxLength(100); + this.Property(x => x.CustomLabel1).HasMaxLength(100); + this.Property(x => x.CustomLabel2).HasMaxLength(100); + this.Property(x => x.CustomLabel3).HasMaxLength(100); + this.Property(x => x.CustomLabel4).HasMaxLength(100); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.Designer.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.Designer.cs new file mode 100644 index 0000000000..652bba012c --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.GoogleMerchantCenter.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class IsBundle : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(IsBundle)); + + string IMigrationMetadata.Id + { + get { return "201601061649324_IsBundle"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs new file mode 100644 index 0000000000..bc8af7c35e --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs @@ -0,0 +1,34 @@ +namespace SmartStore.GoogleMerchantCenter.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class IsBundle : DbMigration + { + public override void Up() + { + AddColumn("dbo.GoogleProduct", "Multipack", c => c.Int(nullable: false)); + AddColumn("dbo.GoogleProduct", "IsBundle", c => c.Boolean()); + AddColumn("dbo.GoogleProduct", "IsAdult", c => c.Boolean()); + AddColumn("dbo.GoogleProduct", "EnergyEfficiencyClass", c => c.String(maxLength: 50)); + AddColumn("dbo.GoogleProduct", "CustomLabel0", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel1", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel2", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel3", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel4", c => c.String(maxLength: 100)); + } + + public override void Down() + { + DropColumn("dbo.GoogleProduct", "CustomLabel4"); + DropColumn("dbo.GoogleProduct", "CustomLabel3"); + DropColumn("dbo.GoogleProduct", "CustomLabel2"); + DropColumn("dbo.GoogleProduct", "CustomLabel1"); + DropColumn("dbo.GoogleProduct", "CustomLabel0"); + DropColumn("dbo.GoogleProduct", "EnergyEfficiencyClass"); + DropColumn("dbo.GoogleProduct", "IsAdult"); + DropColumn("dbo.GoogleProduct", "IsBundle"); + DropColumn("dbo.GoogleProduct", "Multipack"); + } + } +} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.resx b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.resx new file mode 100644 index 0000000000..ebf95a7c54 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + H4sIAAAAAAAEAM2a227jNhCG7wv0HQRdtUDW8iEF2sDeReIki6BxHETJ3tPS2FGXIlWSCuy+Wi/6SH2Fjs6iJNuSHQe9s0fkNwdS1K+x//37n/GXtU+NNxDS42xiDnp90wDmcNdjq4kZquWnX80vn3/8YXzj+mvjWzZuFI3DmUxOzFelggvLks4r+ET2fM8RXPKl6jnct4jLrWG//5s1GFiACBNZhjF+CpnyfIi/4NcpZw4EKiR0xl2gMrXjFTumGg/EBxkQByam7ROhbMUF9L5yvqIwA+G8EqamwBSI3jVRxDQuqUcwNhvo0jQIY1wRhZFfvEiwleBsZQdoIPR5EwCOWxIqIc3oohjeNrn+MErOKiZmKCeUivsdgYNRWi2rOv2gmpt5NbGeN1h3tYmyjms6MZMaPgruho56AocL1zSqfi+mVERzWlQfp3ms10A9M/bMPcu317A37PV7/TNjGlIVCpgwCJUg9Mx4DBfUc36HzTP/DmzCQkrL6WGCeE0zoAnDCECozRMs06TvMEdLn2dVJ+bTSnOSGtwxNRqaxgM6JwsK+e4p1SvO8iswEESB+0gU5sciBsT1r3mv+EoLt9/lbswzWXPG/U1GwZ2Pt7VpzMj6HthKvU5M/Ggat94a3MySkl+Yh6cATlIi3OsIM3VBnNzN5Qq+Ch4GJ3c05ZSfPh3b+wtO7mSGG1DgQXdyR+kmP7mfOwV+vAuKu+N0vuQzD/GMzT1dcU6BsM534lRAdBDM2YtyMhY+qOAZH4KdYS+B+36wm3XAhTo2vxke1B4+n78fd2DdyauQuRRq4eybduliAB1n3eDpvNrcLJee46Hk2UwpkXLHlvql/x4nS/xkvScLoP0dvgb9d3Y2+Ehnw490NvpIZ+fv7GxsFYKsLtNQFitUUyCatNp88Qc4KhoCa9Ug2VDppqpNpp71JBMXNqjtQhDvhiK8REw3KbvmlPLgCzFvJWo+U/3WFtk/npEgwOqWXgNSi2En7wDTT3Z3KewnDMuRDYo4jzb3hAqOrKByNZIGLtx6QqroPWNBovWdun5t2I6l2rIMmeMdq1EVtsXiZJOjz6m4aPGa1LiYFSdF8W+xHj5OjksDedzNrxA1SvwuRygRDbIa5Vbos23SfNfsklAuQ0rm9qxCLZdRhbU9KZPDZU5ma08p1G6ZU1jbk1I5W8akpvaMRKyWEYmlPaFQomVKYe2w6pnU1NY8M7bnaFJS24TlCx14hVzUaIW5w5ppilFbOu1Ke6IuG8tE/Up7YqYdy6zM1mFfFNpR2xiFucsKZAJSX4DM2oWUakodlBo71KhZZWolax7SYa9oolLbK9qVg4iDrcTBgcThVuLwQOJoK3F0IPF8K/G8iTi2Kg/J6qPaqj2rK+2nqg7YpaiqQ3LvubKqKKhxqmb2d1dr8iYZYhpYqjfPjaSNvZF4PCYCwv6TTiluWlUMmBHmLUGqpENnDvuDYaUd+/9pjVpSurR7f/TDO45eVOC9PcWOL9u1JmPs5cgWI3sjkcwUP/lk/XOZ1r2NeBSq2io8Cqa1A48ilVt+R4Gqbb2jYJXW3VGshvbccbxqC27hdd+lTe23SPKo92q/HQzT22+H5FZrvR1yG1cbb5VA2i2U1oQ7gLCzIZdtolpLruOt3NB/a0TH3ZyD2YMTsocnZI9OyD4/jt2tVVZv03TufrVtfiUKCc+ABccMk/C1gQf3x+qqbWyVfzkfX4P0VgUi+h2dgRPJoQKajbljS54tDuZdjigbUj1ZQBE82MilUN6SOAovOyBl3Pf8RmgY3bL+Atw7Ng9VEKpLKcFfUK1VMrZ2+4+bgHrM43kQfZPvkQKG6UVn85xdhR5187hv64fiNkS0k1LVhVHZKlJfq01OeuCsJSgt3zUEkcxh6hn8gCJMzplN3uCQ2F4k3MOKOJtMfG+H7F8Ivezja4+sBPFlyijmR/8GsaK/g3z+D2Zr4jJAIgAA + + + dbo + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index 043c9f3476..6f343b6b69 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Merchant Center (GMC) feed SystemName: SmartStore.GoogleMerchantCenter Group: Marketing -Version: 2.2.0.4 -MinAppVersion: 2.2.0 +Version: 2.6.0.1 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs index cca0f73827..7064e84880 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs @@ -29,5 +29,16 @@ public GoogleProductRecord() public DateTime UpdatedOnUtc { get; set; } public bool Export { get; set; } - } + + public int Multipack { get; set; } + public bool? IsBundle { get; set; } + public bool? IsAdult { get; set; } + public string EnergyEfficiencyClass { get; set; } + + public string CustomLabel0 { get; set; } + public string CustomLabel1 { get; set; } + public string CustomLabel2 { get; set; } + public string CustomLabel3 { get; set; } + public string CustomLabel4 { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs index 3673c52c20..10b5732247 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs @@ -10,8 +10,7 @@ namespace SmartStore.GoogleMerchantCenter { public class Events : IConsumer, - IConsumer/*, - IConsumer*/ + IConsumer { private readonly IGoogleFeedService _googleService; @@ -25,38 +24,15 @@ 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(); - } - } - - //public void HandleEvent(RowExportingEvent eventMessage) - //{ - // if (eventMessage.EntityType != ExportEntityType.Product) - // return; - - // var row = eventMessage.Row; - // var product = eventMessage.Row.Entity as Product; - // if (product == null) - // return; - - // var gmc = _googleService.GetGoogleProductRecord(product.Id); - // if (gmc == null) - // return; - - // row["_GMC_AgeGroup"] = gmc.AgeGroup; - // row["_GMC_Color"] = gmc.Color; - // row["_GMC_Gender"] = gmc.Gender; - // row["_GMC_Size"] = gmc.Size; - // row["_GMC_Taxonomy"] = gmc.Taxonomy; - // row["_GMC_Material"] = gmc.Material; - // row["_GMC_Pattern"] = gmc.Pattern; - //} + 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) { @@ -88,8 +64,17 @@ public void HandleEvent(ModelBoundEvent eventMessage) entity.Taxonomy = model.Taxonomy; entity.Material = model.Material; entity.Pattern = model.Pattern; - entity.Export = model.Exporting; + entity.Export = model.Export2; entity.UpdatedOnUtc = utcNow; + entity.Multipack = model.Multipack2 ?? 0; + entity.IsBundle = model.IsBundle; + entity.IsAdult = model.IsAdult; + entity.EnergyEfficiencyClass = model.EnergyEfficiencyClass; + entity.CustomLabel0 = model.CustomLabel0; + entity.CustomLabel1 = model.CustomLabel1; + entity.CustomLabel2 = model.CustomLabel2; + entity.CustomLabel3 = model.CustomLabel3; + entity.CustomLabel4 = model.CustomLabel4; entity.IsTouched = entity.IsTouched(); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs index c140111c1e..5a8dd0168d 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs @@ -9,7 +9,7 @@ public static string XEditableLink(this HtmlHelper hlp, string fieldName, string { string displayText = null; - if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Exporting") + if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Export2" || fieldName == "IsBundle" || fieldName == "IsAdult") displayText = "<#= {0}Localize #>".FormatInvariant(fieldName); else displayText = "<#= {0} #>".FormatInvariant(fieldName); @@ -22,12 +22,15 @@ public static string XEditableLink(this HtmlHelper hlp, string fieldName, string return skeleton.FormatInvariant(fieldName, fieldName.ToLower(), type, displayText); } - public static bool IsTouched(this GoogleProductRecord product) + public static bool IsTouched(this GoogleProductRecord p) { - if (product != null) + if (p != null) { - return product.Taxonomy.HasValue() || product.Gender.HasValue() || product.AgeGroup.HasValue() || product.Color.HasValue() || - product.Size.HasValue() || product.Material.HasValue() || product.Pattern.HasValue() || product.ItemGroupId.HasValue() || !product.Export; + return + p.Taxonomy.HasValue() || p.Gender.HasValue() || p.AgeGroup.HasValue() || p.Color.HasValue() || + p.Size.HasValue() || p.Material.HasValue() || p.Pattern.HasValue() || p.ItemGroupId.HasValue() || + !p.Export || p.Multipack != 0 || p.IsBundle.HasValue || p.IsAdult.HasValue || p.EnergyEfficiencyClass.HasValue() || + p.CustomLabel0.HasValue() || p.CustomLabel1.HasValue() || p.CustomLabel2.HasValue() || p.CustomLabel3.HasValue() || p.CustomLabel4.HasValue(); } return false; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml index e4aa14c576..17b90e856c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml @@ -37,24 +37,9 @@ Google-Kategorie - - Geschlecht - - - Altersgruppe - - - Farbe - - - Größe - - - Material - - - Muster - + + Legt die dem Artikel entsprechende Google-Kategorie fest. Zwingend erforderlich. + Zusätzliche Bilder @@ -148,12 +133,6 @@ Muster oder grafisches Druckdesign eines Produktes. Angabe ist z.B. bei Bekleidung sinnvoll. - - Nur online zu kaufen - - - Legt fest, ob Produkte nur online erworben werden können. Aktivieren Sie diese Option nicht, falls Produkte auch in Ihrem Ladengeschäft erhältlich sind. - Leer @@ -196,4 +175,61 @@ Legt fest, ob der Grundpreis eines Produktes exportiert werden soll. + + Multipack + + + Anzahl identischer Produkte in einem händlerdefinierten Multipack. Muss größer 1 sein. + + + Bundle + + + Händlerdefiniertes Produktpaket bestehend aus einem Haupt- und mehreren Zubehörartikel bzw. Add-ons. + + + Nicht jugendfrei + + + Produkt, welches nur für erwachsene Nutzer bestimmt ist. + + + Energieeffizienz + + + Die Energieeffizienzklasse des Produktes gemäß EU-Richtlinie 2010/30/EU. Mögliche Werte sind G, F, E, D, C, B, A, A+, A++, A+++. + + + A+++,A++,A+,A,B,C,D,E,F,G + + + Label 0 + + + Benutzerdefiniertes Label 0. Dient der individuellen Gruppierung von Produkten. + + + Label 1 + + + Benutzerdefiniertes Label 1. Dient der individuellen Gruppierung von Produkten. + + + Label 2 + + + Benutzerdefiniertes Label 2. Dient der individuellen Gruppierung von Produkten. + + + Label 3 + + + Benutzerdefiniertes Label 3. Dient der individuellen Gruppierung von Produkten. + + + Label 4 + + + Benutzerdefiniertes Label 4. Dient der individuellen Gruppierung von Produkten. + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml index a80721be22..3b75a8c306 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml @@ -37,24 +37,9 @@ Google Category - - Gender - - - Age group - - - Color - - - Size - - - Material - - - Pattern - + + Specifies the Google category corresponding to the product. Mandatory. + Additional images @@ -148,12 +133,6 @@ The pattern or graphic print featured on a product. Usefull for clothes for instance. - - Only online available - - - Check the box if products can only be purchased online. - Empty @@ -196,4 +175,61 @@ Activate this option if you want to export the base price of the product. + + Multipack + + + Number of identical products in a merchant defined multipack. Must be greater than 1. + + + Bundle + + + Merchant defined product package consisting of one main and several accessories or add-ons. + + + Adult + + + Product, which is intended only for adult users. + + + Energy efficiency + + + The energy efficiency class of the product in accordance with EU directive 2010/30/EU. Possible values are G, F, E, D, C, B, A, A +, A++, A+++. + + + A+++,A++,A+,A,B,C,D,E,F,G + + + Label 0 + + + Custom label 0 serves the individual grouping of products. + + + Label 1 + + + Custom label 1 serves the individual grouping of products. + + + Label 2 + + + Custom label 2 serves the individual grouping of products. + + + Label 3 + + + Custom label 3 serves the individual grouping of products. + + + Label 4 + + + Custom label 4 serves the individual grouping of products. + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs index 0ec41770fe..36c5f02799 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text; using Newtonsoft.Json; using SmartStore.Core.Domain.Catalog; using SmartStore.Web.Framework; @@ -18,10 +19,12 @@ public string AvailableGoogleCategoriesAsJson { if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) return JsonConvert.SerializeObject(AvailableGoogleCategories); - return ""; + return "[ ]"; } } + public string[] EnergyEfficiencyClasses { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] public string SearchProductName { get; set; } @@ -29,7 +32,6 @@ public string AvailableGoogleCategoriesAsJson public string SearchIsTouched { get; set; } } - public class GoogleProductModel : ModelBase { public int TotalCount { get; set; } @@ -74,35 +76,66 @@ public string ProductTypeLabelHint [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.GoogleCategory")] public string Taxonomy { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Gender")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] public string Gender { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.AgeGroup")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] public string AgeGroup { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Color")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] public string Color { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Size")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] public string Size { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Material")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] public string Material { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Pattern")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] public string Pattern { get; set; } [SmartResourceDisplayName("Common.Export")] public int Export { get; set; } [SmartResourceDisplayName("Common.Export")] - public bool Exporting + public bool Export2 { get { return Export != 0; } set { Export = (value ? 1 : 0); } } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] + public int Multipack { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] + public int? Multipack2 + { + get { return Multipack > 0 ? Multipack : (int?)null; } + set { Multipack = (value ?? 0); } + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.IsBundle")] + public bool? IsBundle { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.IsAdult")] + public bool? IsAdult { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.EnergyEfficiencyClass")] + public string EnergyEfficiencyClass { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel0")] + public string CustomLabel0 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel1")] + public string CustomLabel1 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel2")] + public string CustomLabel2 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel3")] + public string CustomLabel3 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel4")] + public string CustomLabel4 { get; set; } + public string GenderLocalize { get; set; } public string AgeGroupLocalize { get; set; } - public string ExportingLocalize { get; set; } + public string Export2Localize { get; set; } + public string IsBundleLocalize { get; set; } + public string IsAdultLocalize { get; set; } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs index 6b96944375..69d81d50dd 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs @@ -14,7 +14,6 @@ public class ProfileConfigurationModel public ProfileConfigurationModel() { Condition = "new"; - OnlineOnly = true; AdditionalImages = true; SpecialPrice = true; } @@ -66,9 +65,6 @@ public string AvailableGoogleCategoriesAsJson [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] public string Pattern { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.OnlineOnly")] - public bool OnlineOnly { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExpirationDays")] public int ExpirationDays { get; set; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index d1b98968bb..3edd31e361 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -153,10 +153,11 @@ public override string FileExtension get { return "XML"; } } - protected override void Export(IExportExecuteContext context) + protected override void Export(ExportExecuteContext context) { dynamic currency = context.Currency; string measureWeightSystemKey = ""; + var dateFormat = "yyyy-MM-ddTHH:mmZ"; var measureWeight = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); @@ -176,16 +177,16 @@ protected override void Export(IExportExecuteContext context) writer.WriteElementString("link", "http://base.google.com/base/"); writer.WriteElementString("description", "Information about products"); - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) + while (context.Abort == DataExchangeAbortion.None && context.DataSegmenter.ReadNextSegment()) { - var segment = context.Segmenter.CurrentSegment; + var segment = context.DataSegmenter.CurrentSegment; int[] productIds = segment.Select(x => (int)((dynamic)x).Id).ToArray(); var googleProducts = _googleFeedService.GetGoogleProductRecords(productIds); foreach (dynamic product in segment) { - if (context.Abort != ExportAbortion.None) + if (context.Abort != DataExchangeAbortion.None) break; Product entity = product.Entity; @@ -201,14 +202,18 @@ protected override void Export(IExportExecuteContext context) string category = (gmc == null ? null : gmc.Taxonomy); string productType = product._CategoryPath; string mainImageUrl = product._MainPictureUrl; - var specialPrice = product._SpecialPrice as decimal?; var price = (decimal)product.Price; + var uniqueId = (string)product._UniqueId; string brand = product._Brand; string gtin = product.Gtin; string mpn = product.ManufacturerPartNumber; string condition = "new"; string availability = "in stock"; + var specialPrice = product._FutureSpecialPrice as decimal?; + if (!specialPrice.HasValue) + specialPrice = product._SpecialPrice; + if (category.IsEmpty()) category = config.DefaultGoogleCategory; @@ -243,7 +248,7 @@ protected override void Export(IExportExecuteContext context) } } - writer.WriteElementString("g", "id", _googleNamespace, entity.Id.ToString()); + writer.WriteElementString("g", "id", _googleNamespace, uniqueId); writer.WriteStartElement("title"); writer.WriteCData(((string)product.Name).Truncate(70).RemoveInvalidXmlChars()); @@ -287,14 +292,24 @@ protected override void Export(IExportExecuteContext context) writer.WriteElementString("g", "condition", _googleNamespace, condition); writer.WriteElementString("g", "availability", _googleNamespace, availability); - if (config.SpecialPrice && specialPrice.HasValue && entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) + if (availability == "preorder" && entity.AvailableStartDateTimeUtc.HasValue && entity.AvailableStartDateTimeUtc.Value > DateTime.UtcNow) { - var dateFormat = "yyyy-MM-ddTHH:mmZ"; - var specialPriceDate = "{0}/{1}".FormatInvariant( - entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); + var availabilityDate = entity.AvailableStartDateTimeUtc.Value.ToString(dateFormat); - writer.WriteElementString("g", "sale_price", _googleNamespace, price.FormatInvariant() + " " + (string)currency.CurrencyCode); - writer.WriteElementString("g", "sale_price_effective_date", _googleNamespace, specialPriceDate); + 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; } @@ -321,7 +336,6 @@ protected override void Export(IExportExecuteContext context) writer.WriteCData("pattern", gmc != null && gmc.Pattern.HasValue() ? gmc.Pattern : config.Pattern, "g", _googleNamespace); writer.WriteCData("item_group_id", gmc != null && gmc.ItemGroupId.HasValue() ? gmc.ItemGroupId : "", "g", _googleNamespace); - writer.WriteElementString("g", "online_only", _googleNamespace, config.OnlineOnly ? "y" : "n"); writer.WriteElementString("g", "identifier_exists", _googleNamespace, gtin.HasValue() || brand.HasValue() || mpn.HasValue() ? "TRUE" : "FALSE"); if (config.ExpirationDays > 0) @@ -360,11 +374,56 @@ protected override void Export(IExportExecuteContext context) } } + if (gmc != null && gmc.Multipack > 1) + { + writer.WriteElementString("g", "multipack", _googleNamespace, gmc.Multipack.ToString()); + } + + if (gmc != null && gmc.IsBundle.HasValue) + { + writer.WriteElementString("g", "is_bundle", _googleNamespace, gmc.IsBundle.Value ? "TRUE" : "FALSE"); + } + + if (gmc != null && gmc.IsAdult.HasValue) + { + writer.WriteElementString("g", "adult", _googleNamespace, gmc.IsAdult.Value ? "TRUE" : "FALSE"); + } + + if (gmc != null && gmc.EnergyEfficiencyClass.HasValue()) + { + writer.WriteElementString("g", "energy_efficiency_class", _googleNamespace, gmc.EnergyEfficiencyClass); + } + + if (gmc != null && gmc.CustomLabel0.HasValue()) + { + writer.WriteElementString("g", "custom_label_0", _googleNamespace, gmc.CustomLabel0); + } + + if (gmc != null && gmc.CustomLabel1.HasValue()) + { + writer.WriteElementString("g", "custom_label_1", _googleNamespace, gmc.CustomLabel1); + } + + if (gmc != null && gmc.CustomLabel2.HasValue()) + { + writer.WriteElementString("g", "custom_label_2", _googleNamespace, gmc.CustomLabel2); + } + + if (gmc != null && gmc.CustomLabel3.HasValue()) + { + writer.WriteElementString("g", "custom_label_3", _googleNamespace, gmc.CustomLabel3); + } + + if (gmc != null && gmc.CustomLabel4.HasValue()) + { + writer.WriteElementString("g", "custom_label_4", _googleNamespace, gmc.CustomLabel4); + } + ++context.RecordsSucceeded; } - catch (Exception exc) + catch (Exception exception) { - context.RecordException(exc, entity.Id); + context.RecordException(exception, entity.Id); } writer.WriteEndElement(); // item diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs index 48d574215e..b5fab3af04 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs @@ -125,9 +125,36 @@ public void Upsert(int pk, string name, string value) case "Pattern": product.Pattern = value; break; - case "Exporting": + case "Export2": product.Export = value.ToBool(true); break; + case "Multipack2": + product.Multipack = value.ToInt(); + break; + case "IsBundle": + product.IsBundle = (value.IsEmpty() ? (bool?)null : value.ToBool()); + break; + case "IsAdult": + product.IsAdult = (value.IsEmpty() ? (bool?)null : value.ToBool()); + break; + case "EnergyEfficiencyClass": + product.EnergyEfficiencyClass = value; + break; + case "CustomLabel0": + product.CustomLabel0 = value; + break; + case "CustomLabel1": + product.CustomLabel1 = value; + break; + case "CustomLabel2": + product.CustomLabel2 = value; + break; + case "CustomLabel3": + product.CustomLabel3 = value; + break; + case "CustomLabel4": + product.CustomLabel4 = value; + break; } product.UpdatedOnUtc = utcNow; @@ -156,7 +183,8 @@ public GridModel GetGridModel(GridCommand command, string se string yes = T("Admin.Common.Yes"); string no = T("Admin.Common.No"); - // there's no way to share a context instance across repositories which makes GoogleProductObjectContext pretty useless here. + // there's no way to share a context instance across repositories in EF. + // so we have to fallback to pure SQL here to get the data paged and filtered. var whereClause = new StringBuilder("(NOT ([t2].[Deleted] = 1)) AND ([t2].[VisibleIndividually] = 1)"); @@ -181,11 +209,11 @@ public GridModel GetGridModel(GridCommand command, string se { // fastest possible paged data query sql = - "SELECT [TotalCount], [t3].[Id], [t3].[Name], [t3].[SKU], [t3].[ProductTypeId], [t3].[value] AS [Taxonomy], [t3].[value2] AS [Gender], [t3].[value3] AS [AgeGroup], [t3].[value4] AS [Color], [t3].[value5] AS [Size], [t3].[value6] AS [Material], [t3].[value7] AS [Pattern], [t3].[value8] AS [Export]" + + "SELECT [TotalCount], [t3].[Id], [t3].[Name], [t3].[SKU], [t3].[ProductTypeId], [t3].[value] AS [Taxonomy], [t3].[value2] AS [Gender], [t3].[value3] AS [AgeGroup], [t3].[value4] AS [Color], [t3].[value5] AS [Size], [t3].[value6] AS [Material], [t3].[value7] AS [Pattern], [t3].[value8] AS [Export], [t3].[value9] AS [Multipack], [t3].[value10] AS [IsBundle], [t3].[value11] AS [IsAdult], [t3].[value12] AS [EnergyEfficiencyClass], [t3].[value13] AS [CustomLabel0], [t3].[value14] AS [CustomLabel1], [t3].[value15] AS [CustomLabel2], [t3].[value16] AS [CustomLabel3], [t3].[value17] AS [CustomLabel4]" + " FROM (" + - " SELECT COUNT(id) OVER() [TotalCount], ROW_NUMBER() OVER (ORDER BY [t2].[Name]) AS [ROW_NUMBER], [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value], [t2].[value2], [t2].[value3], [t2].[value4], [t2].[value5], [t2].[value6], [t2].[value7], [t2].[value8]" + + " SELECT COUNT(id) OVER() [TotalCount], ROW_NUMBER() OVER (ORDER BY [t2].[Name]) AS [ROW_NUMBER], [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value], [t2].[value2], [t2].[value3], [t2].[value4], [t2].[value5], [t2].[value6], [t2].[value7], [t2].[value8], [t2].[value9], [t2].[value10], [t2].[value11], [t2].[value12], [t2].[value13], [t2].[value14], [t2].[value15], [t2].[value16], [t2].[value17]" + " FROM (" + - " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched]" + + " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], COALESCE([t1].[Multipack],0) AS [value9], [t1].[IsBundle] AS [value10], [t1].[IsAdult] AS [value11], [t1].[EnergyEfficiencyClass] AS [value12], [t1].[CustomLabel0] AS [value13], [t1].[CustomLabel1] AS [value14], [t1].[CustomLabel2] AS [value15], [t1].[CustomLabel3] AS [value16], [t1].[CustomLabel4] AS [value17], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched]" + " FROM [Product] AS [t0]" + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + " ) AS [t2]" + @@ -198,9 +226,9 @@ public GridModel GetGridModel(GridCommand command, string se { // OFFSET... FETCH NEXT requires SQL Server 2012 or SQL CE 4 sql = - "SELECT [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value] AS [Taxonomy], [t2].[value2] AS [Gender], [t2].[value3] AS [AgeGroup], [t2].[value4] AS [Color], [t2].[value5] AS [Size], [t2].[value6] AS [Material], [t2].[value7] AS [Pattern], [t2].[value8] AS [Export]" + + "SELECT [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value] AS [Taxonomy], [t2].[value2] AS [Gender], [t2].[value3] AS [AgeGroup], [t2].[value4] AS [Color], [t2].[value5] AS [Size], [t2].[value6] AS [Material], [t2].[value7] AS [Pattern], [t2].[value8] AS [Export], [t2].[value9] AS [Multipack], [t2].[value10] AS [IsBundle], [t2].[value11] AS [IsAdult], [t2].[value12] AS [EnergyEfficiencyClass], [t2].[value13] AS [CustomLabel0], [t2].[value14] AS [CustomLabel1], [t2].[value15] AS [CustomLabel2], [t2].[value16] AS [CustomLabel3], [t2].[value17] AS [CustomLabel4]" + " FROM (" + - " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched] AS [IsTouched]" + + " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], COALESCE([t1].[Multipack],0) AS [value9], [t1].[IsBundle] AS [value10], [t1].[IsAdult] AS [value11], [t1].[EnergyEfficiencyClass] AS [value12], [t1].[CustomLabel0] AS [value13], [t1].[CustomLabel1] AS [value14], [t1].[CustomLabel2] AS [value15], [t1].[CustomLabel3] AS [value16], [t1].[CustomLabel4] AS [value17], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched] AS [IsTouched]" + " FROM [Product] AS [t0]" + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + " ) AS [t2]" + @@ -232,7 +260,17 @@ public GridModel GetGridModel(GridCommand command, string se if (x.AgeGroup.HasValue()) x.AgeGroupLocalize = T("Plugins.Feed.Froogle.AgeGroup" + textInfo.ToTitleCase(x.AgeGroup)); - x.ExportingLocalize = (x.Export == 0 ? no : yes); + x.Export2Localize = (x.Export == 0 ? no : yes); + + if (x.IsBundle.HasValue) + x.IsBundleLocalize = (x.IsBundle.Value ? yes : no); + else + x.IsBundleLocalize = null; + + if (x.IsAdult.HasValue) + x.IsAdultLocalize = (x.IsAdult.Value ? yes : no); + else + x.IsAdultLocalize = null; }); model.Data = data; diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index aa2b7dca53..f743627997 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -102,8 +102,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 @@ -169,6 +170,10 @@ 201504211854125_IsActive.cs + + + 201601061649324_IsBundle.cs + @@ -218,6 +223,9 @@ 201504211854125_IsActive.cs + + 201601061649324_IsBundle.cs + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml index 6312648112..d91ec16571 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml @@ -3,7 +3,6 @@ @using SmartStore.GoogleMerchantCenter.Models; @using SmartStore.GoogleMerchantCenter.Providers; @using SmartStore.Web.Framework; -@using SmartStore.Web.Framework.Plugins; @using Telerik.Web.Mvc.UI; @using SmartStore.Web.Framework.UI; @@ -53,7 +52,8 @@ @Html.SmartLabelFor(m => m.SearchIsTouched) - @Html.DropDownList("SearchIsTouched", new List { + @Html.DropDownList("SearchIsTouched", new List + { new SelectListItem { Text = T("Common.Unspecified"), Value = "" }, new SelectListItem { Text = T("Plugins.Feed.Froogle.SearchIsTouched.Touched"), Value = "touched" }, new SelectListItem { Text = T("Plugins.Feed.Froogle.SearchIsTouched.Untouched"), Value = "untouched" } @@ -82,25 +82,56 @@ }) .Columns(c => { - c.Bound(x => x.ProductId).ReadOnly().Visible(false); + c.Bound(x => x.ProductId) + .ReadOnly() + .Visible(false); c.Bound(x => x.Name) - .ReadOnly().Visible(true).Width(420) + .ReadOnly().Visible(true) .Template(x => @Html.LabeledProductName(x.ProductId, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) .ClientTemplate(@Html.LabeledProductName("ProductId", "Name")); - c.Bound(x => x.SKU).ReadOnly().Visible(true); - c.Bound(x => x.Exporting).ClientTemplate(Html.XEditableLink("Exporting", "select2")); - c.Bound(x => x.Taxonomy).ClientTemplate(Html.XEditableLink("Taxonomy", "typeahead")); - c.Bound(x => x.Gender).ClientTemplate(Html.XEditableLink("Gender", "select2")).Width(100); - c.Bound(x => x.AgeGroup).ClientTemplate(Html.XEditableLink("AgeGroup", "select2")).Width(100); - c.Bound(x => x.Color).ClientTemplate(Html.XEditableLink("Color", "text")); - c.Bound(x => x.Size).ClientTemplate(Html.XEditableLink("Size", "text")); - c.Bound(x => x.Material).ClientTemplate(Html.XEditableLink("Material", "text")); - c.Bound(x => x.Pattern).ClientTemplate(Html.XEditableLink("Pattern", "text")); + c.Bound(x => x.SKU) + .ReadOnly() + .Visible(true); + c.Bound(x => x.Export2) + .ClientTemplate(Html.XEditableLink("Export2", "select2")); + c.Bound(x => x.Taxonomy) + .ClientTemplate(Html.XEditableLink("Taxonomy", "typeahead")); + c.Bound(x => x.Gender) + .ClientTemplate(Html.XEditableLink("Gender", "select2")); + c.Bound(x => x.AgeGroup) + .ClientTemplate(Html.XEditableLink("AgeGroup", "select2")); + c.Bound(x => x.IsAdult) + .ClientTemplate(Html.XEditableLink("IsAdult", "select2")); + c.Bound(x => x.Color) + .ClientTemplate(Html.XEditableLink("Color", "text")); + c.Bound(x => x.Size) + .ClientTemplate(Html.XEditableLink("Size", "text")); + c.Bound(x => x.Material) + .ClientTemplate(Html.XEditableLink("Material", "text")); + c.Bound(x => x.Pattern) + .ClientTemplate(Html.XEditableLink("Pattern", "text")); + c.Bound(x => x.Multipack2) + .ClientTemplate(Html.XEditableLink("Multipack2", "text")); + c.Bound(x => x.IsBundle) + .ClientTemplate(Html.XEditableLink("IsBundle", "select2")); + c.Bound(x => x.EnergyEfficiencyClass) + .ClientTemplate(Html.XEditableLink("EnergyEfficiencyClass", "select2")); + c.Bound(x => x.CustomLabel0) + .ClientTemplate(Html.XEditableLink("CustomLabel0", "text")); + c.Bound(x => x.CustomLabel1) + .ClientTemplate(Html.XEditableLink("CustomLabel1", "text")); + c.Bound(x => x.CustomLabel2) + .ClientTemplate(Html.XEditableLink("CustomLabel2", "text")); + c.Bound(x => x.CustomLabel3) + .ClientTemplate(Html.XEditableLink("CustomLabel3", "text")); + c.Bound(x => x.CustomLabel4) + .ClientTemplate(Html.XEditableLink("CustomLabel4", "text")); }) .ClientEvents(e => { e.OnDataBound("OnGridDataBound"); e.OnDataBinding("OnGridDataBinding"); + e.OnError("OnGridError"); }) .DataBinding(dataBinding => { @@ -127,6 +158,11 @@ return false; }); + function OnGridError(e) { + e.preventDefault(); + alert(e.XMLHttpRequest.responseText); + } + function OnGridDataBinding(e) { e.data = { searchProductName: $('#SearchProductName').val(), @@ -135,28 +171,32 @@ } function OnGridDataBound(e) { - var grid = $('#gmc-products-grid'), - options = { - emptytext: '@T("Plugins.Feed.Froogle.EmptyGridCell")', - url: '@Url.Action("GoogleProductEdit", "FeedGoogleMerchantCenter", new { Namespaces = "SmartStore.GoogleMerchantCenter.Controllers", area = GoogleMerchantCenterFeedPlugin.SystemName })' - }, - select2Options = { - placeholder: '@T("Plugins.Feed.Froogle.EmptyGridCell")', - minimumResultsForSearch: 20, - allowClear: true - }; + var grid = $('#gmc-products-grid'); + + var emptyCell = '@T("Plugins.Feed.Froogle.EmptyGridCell")'; + + var options = { + emptytext: emptyCell, + url: '@Url.Action("GoogleProductEdit", "FeedGoogleMerchantCenter", new { Namespaces = "SmartStore.GoogleMerchantCenter.Controllers", area = GoogleMerchantCenterFeedPlugin.SystemName })' + }; + var select2Options = { + placeholder: emptyCell, + minimumResultsForSearch: 20, + allowClear: true + }; + var boolSource = [ + { id: 'true', text: '@T("Admin.Common.Yes")' }, + { id: 'false', text: '@T("Admin.Common.No")' } + ]; - grid.find('a.edit-link-exporting').editable($.extend(options, { + grid.find('a.edit-link-export2').editable($.extend({}, options, { select2: { minimumResultsForSearch: 20 }, - source: [ - { id: 'true', text: '@T("Admin.Common.Yes")' }, - { id: 'false', text: '@T("Admin.Common.No")' } - ] + source: boolSource })); - grid.find('a.edit-link-gender').editable($.extend(options, { + grid.find('a.edit-link-gender').editable($.extend({}, options, { select2: select2Options, source: [ { id: 'male', text: '@T("Plugins.Feed.Froogle.GenderMale")' }, @@ -165,7 +205,7 @@ ] })); - grid.find('a.edit-link-agegroup').editable($.extend(options, { + grid.find('a.edit-link-agegroup').editable($.extend({}, options, { select2: select2Options, source: [ { id: 'adult', text: '@T("Plugins.Feed.Froogle.AgeGroupAdult").Text' }, @@ -173,6 +213,11 @@ ] })); + grid.find('a.edit-link-isadult').editable($.extend({}, options, { + select2: select2Options, + source: boolSource + })); + grid.find('a.edit-link-color').editable(options); grid.find('a.edit-link-size').editable(options); @@ -181,7 +226,46 @@ grid.find('a.edit-link-pattern').editable(options); - grid.find('a.edit-link-taxonomy').editable($.extend(options, { + grid.find('a.edit-link-multipack2').editable($.extend({}, options, { + validate: function(value) { + if (value.length !== 0) { + var regex = /^[0-9]+$/; + if (!regex.test(value)) { + return '@T("Admin.System.Warnings.DigitsOnly")'; + } + } + return null; + } + })); + + grid.find('a.edit-link-isbundle').editable($.extend({}, options, { + select2: select2Options, + source: boolSource + })); + + grid.find('a.edit-link-energyefficiencyclass').editable($.extend({}, options, { + select2: select2Options, + source: [ + @if (Model.EnergyEfficiencyClasses != null && Model.EnergyEfficiencyClasses.Length > 0) + { + foreach (var str in Model.EnergyEfficiencyClasses.Select(x => x.Replace("'", "\""))) + { + { id: '@str', text: '@str' }, + } + } + ] + })); + + grid.find('a.edit-link-customlabel0').editable(options); + grid.find('a.edit-link-customlabel1').editable(options); + grid.find('a.edit-link-customlabel2').editable(options); + grid.find('a.edit-link-customlabel3').editable(options); + grid.find('a.edit-link-customlabel4').editable(options); + + grid.find('a.edit-link-taxonomy').editable($.extend({}, options, { + select2: { + minimumResultsForSearch: 20 + }, source: @Html.Raw(Model.AvailableGoogleCategoriesAsJson) })); } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml index fa3273da09..c20ef9acd7 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml @@ -6,24 +6,24 @@ + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml index 70868da9e5..5846c1ed43 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml @@ -14,7 +14,7 @@
@@ -24,6 +24,14 @@ @using (Html.BeginForm()) { + + + + + + + + + +
+ @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
@Html.SmartLabelFor(model => model.UseSandbox) @@ -106,6 +114,15 @@ @Html.ValidationMessageFor(model => model.EnableIpn)
+ @Html.SmartLabelFor(model => model.IpnChangesPaymentStatus) + + @Html.SettingEditorFor(model => model.IpnChangesPaymentStatus) + @Html.ValidationMessageFor(model => model.IpnChangesPaymentStatus) +
@Html.SmartLabelFor(model => model.IpnUrl) diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml index c5b73f2d11..ee77251801 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml @@ -5,7 +5,7 @@
- PayPal + PayPal
@T("Plugins.Payments.PayPalStandard.Fields.RedirectionTip") diff --git a/src/Plugins/SmartStore.PayPal/changelog.md b/src/Plugins/SmartStore.PayPal/changelog.md index 80afe49fdd..e074479a1d 100644 --- a/src/Plugins/SmartStore.PayPal/changelog.md +++ b/src/Plugins/SmartStore.PayPal/changelog.md @@ -1,7 +1,42 @@ -#Release Notes# +#Release Notes + +##Paypal 2.6.0.5 +###Bugfixes +* PayPal PLUS: Fixes "Cannot perform runtime binding on a null reference" when rendering the payment wall. + +##Paypal 2.6.0.4 +###Bugfixes +* PayPal PLUS: Excluding tax issue. Fixes "Transaction amount details (subtotal, tax, shipping) must add up to specified amount total". + +##Paypal 2.6.0.3 +###Bugfixes +* PayPal PLUS: Integration review through PayPal +* PayPal PLUS: Generic attribute caching problem. Fixes "Item amount must add up to specified amount subtotal (or total if amount details not specified)". + +##PayPal 2.6.0.1 +###Improvements +* Added PayPal partner attribution Id as request header + +##Paypal 2.5.0.2 +###New Features +* PayPal PLUS payment provider + +##Paypal 2.5.0.1 +###Bugfixes +* PayPal Standard: The order amount transmitted to PayPal was wrong if gift cards or reward points were applied + +##Paypal 2.2.0.4 +###New Features +* Option for API security protocol +* Option to display express checkout button in mini shopping cart +* Support for partial refunds +* Option whether IPD may change the payment status of an order +###Bugfixes +* "The request was aborted: Could not create SSL/TLS secure channel." See https://devblog.paypal.com/upcoming-security-changes-notice/ +* PayPal Express: Void and refund out of function ("The transaction id is not valid") ##Paypal 2.2.0.3 -### New Features +###New Features * Option to add order note when order total validation fails ##PayPal 2.2.0.2 @@ -9,7 +44,7 @@ * Redirecting to payment provider performed by core instead of plugin ##Paypal 2.2.0.1 -### New Features +###New Features * Supports order list label for new incoming IPNs ##Paypal 1.22 diff --git a/src/Plugins/SmartStore.PayPal/packages.config b/src/Plugins/SmartStore.PayPal/packages.config index d1ef764942..eb91eb3011 100644 --- a/src/Plugins/SmartStore.PayPal/packages.config +++ b/src/Plugins/SmartStore.PayPal/packages.config @@ -7,4 +7,5 @@ + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/web.config b/src/Plugins/SmartStore.PayPal/web.config index d9badc0f86..f6daa0fb4b 100644 --- a/src/Plugins/SmartStore.PayPal/web.config +++ b/src/Plugins/SmartStore.PayPal/web.config @@ -66,7 +66,7 @@ - + @@ -94,7 +94,7 @@ - + diff --git a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs index 1bf1f643b1..5c5e03f8ab 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs @@ -43,7 +43,7 @@ public ActionResult Configure() var shippingMethods = _shippingService.GetAllShippingMethods(); if (shippingMethods.Count == 0) { - return Content("No shipping methods can be loaded"); + return Content(T("Admin.Configuration.Shipping.Methods.NoMethodsLoaded")); } var model = new ByTotalListModel(); @@ -100,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; @@ -112,6 +113,7 @@ public ActionResult RateUpdate(ByTotalModel model, GridCommand command) shippingByTotalRecord.ShippingChargePercentage = model.ShippingChargePercentage; shippingByTotalRecord.BaseCharge = model.BaseCharge; shippingByTotalRecord.MaxCharge = model.MaxCharge; + _shippingByTotalService.UpdateShippingByTotalRecord(shippingByTotalRecord); return RatesList(command); diff --git a/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs b/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs index f0e510f3b7..353bafe59d 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs @@ -29,7 +29,7 @@ public ActionResult Configure() { var shippingMethods = _shippingService.GetAllShippingMethods(); if (shippingMethods.Count == 0) - return Content("No shipping methods can be loaded"); + return Content(T("Admin.Configuration.Shipping.Methods.NoMethodsLoaded")); var tmp = new List(); foreach (var shippingMethod in shippingMethods) diff --git a/src/Plugins/SmartStore.Shipping/Description.txt b/src/Plugins/SmartStore.Shipping/Description.txt index 533761599a..f241173e84 100644 --- a/src/Plugins/SmartStore.Shipping/Description.txt +++ b/src/Plugins/SmartStore.Shipping/Description.txt @@ -2,8 +2,8 @@ Description: Provides shipping methods for fixed rate shipping and computation based on weight. SystemName: SmartStore.Shipping Group: Shipping -Version: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.Shipping.dll ResourceRootKey: Plugins.SmartStore.Shipping \ No newline at end of file diff --git a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs index 882a576f15..82ec989cf4 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs @@ -1,20 +1,20 @@ using System; -using System.Data.Entity.Migrations; using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; using SmartStore.Core.Plugins; -using SmartStore.Shipping.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; -using SmartStore.Core.Logging; using SmartStore.Services.Shipping; using SmartStore.Services.Shipping.Tracking; +using SmartStore.Shipping.Services; namespace SmartStore.Shipping { - [SystemName("Shipping.ByTotal")] + [SystemName("Shipping.ByTotal")] [FriendlyName("Shipping by total")] [DisplayOrder(1)] public class ByTotalProvider : IShippingRateComputationMethod, IConfigurable @@ -28,7 +28,6 @@ public class ByTotalProvider : IShippingRateComputationMethod, IConfigurable private readonly ISettingService _settingService; private readonly ILocalizationService _localizationService; - /// /// Ctor /// @@ -56,14 +55,18 @@ public ByTotalProvider(IShippingService shippingService, this._logger = logger; this._settingService = settingService; this._localizationService = localizationService; - } - #region Properties + T = NullLocalizer.Instance; + } - /// - /// Gets a shipping rate computation method type - /// - public ShippingRateComputationMethodType ShippingRateComputationMethodType + #region Properties + + public Localizer T { get; set; } + + /// + /// Gets a shipping rate computation method type + /// + public ShippingRateComputationMethodType ShippingRateComputationMethodType { get { @@ -158,7 +161,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) { - response.AddError("No shipment items"); + response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } diff --git a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs index a4ae31c94e..cc303cee6e 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Web.Routing; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; @@ -10,27 +11,29 @@ namespace SmartStore.Shipping { - /// - /// Fixed rate shipping computation provider - /// - [SystemName("Shipping.FixedRate")] + /// + /// Fixed rate shipping computation provider + /// + [SystemName("Shipping.FixedRate")] [FriendlyName("Fixed Rate Shipping")] [DisplayOrder(0)] public class FixedRateProvider : IShippingRateComputationMethod, IConfigurable { private readonly ISettingService _settingService; private readonly IShippingService _shippingService; - private readonly ILocalizationService _localizationService; public FixedRateProvider(ISettingService settingService, - IShippingService shippingService, ILocalizationService localizationService) + IShippingService shippingService) { this._settingService = settingService; this._shippingService = shippingService; - _localizationService = localizationService; - } - - private decimal GetRate(int shippingMethodId) + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + private decimal GetRate(int shippingMethodId) { string key = string.Format("ShippingRateComputationMethod.FixedRate.Rate.ShippingMethodId{0}", shippingMethodId); decimal rate = this._settingService.GetSettingByKey(key); @@ -51,7 +54,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) { - response.AddError("No shipment items"); + response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } diff --git a/src/Plugins/SmartStore.Shipping/web.config b/src/Plugins/SmartStore.Shipping/web.config index 32347fc005..c0f82db177 100644 --- a/src/Plugins/SmartStore.Shipping/web.config +++ b/src/Plugins/SmartStore.Shipping/web.config @@ -1,115 +1,115 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs index 0c59828d2a..a147a1855d 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs @@ -3,15 +3,16 @@ using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; -using SmartStore.ShippingByWeight.Data; -using SmartStore.ShippingByWeight.Data.Migrations; -using SmartStore.ShippingByWeight.Services; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Localization; using SmartStore.Services.Shipping; using SmartStore.Services.Shipping.Tracking; +using SmartStore.ShippingByWeight.Data; +using SmartStore.ShippingByWeight.Data.Migrations; +using SmartStore.ShippingByWeight.Services; namespace SmartStore.ShippingByWeight { @@ -32,6 +33,7 @@ public class ByWeightShippingComputationMethod : BasePlugin, IShippingRateComput #endregion #region Ctor + public ByWeightShippingComputationMethod(IShippingService shippingService, IStoreContext storeContext, IShippingByWeightService shippingByWeightService, @@ -51,12 +53,17 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, this._localizationService = localizationService; this._priceFormatter = priceFormatter; this._services = services; - } - #endregion - #region Utilities + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion - private decimal? GetRate(decimal subTotal, decimal weight, int shippingMethodId, int storeId, int countryId, string zip) + #region Utilities + + private decimal? GetRate(decimal subTotal, decimal weight, int shippingMethodId, int storeId, int countryId, string zip) { decimal? shippingTotal = null; @@ -109,41 +116,41 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, /// /// Gets available shipping options /// - /// A request for getting shipping options + /// A request for getting shipping options /// Represents a response of getting shipping rate options - public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest getShippingOptionRequest) + public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest request) { - if (getShippingOptionRequest == null) + if (request == null) throw new ArgumentNullException("getShippingOptionRequest"); var response = new GetShippingOptionResponse(); - if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) + if (request.Items == null || request.Items.Count == 0) { - response.AddError("No shipment items"); + response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } - - int storeId = _storeContext.CurrentStore.Id; + + int storeId = request.StoreId > 0 ? request.StoreId : _storeContext.CurrentStore.Id; decimal subTotal = decimal.Zero; int countryId = 0; string zip = null; - if (getShippingOptionRequest.ShippingAddress != null) + if (request.ShippingAddress != null) { - countryId = getShippingOptionRequest.ShippingAddress.CountryId ?? 0; - zip = getShippingOptionRequest.ShippingAddress.ZipPostalCode; + countryId = request.ShippingAddress.CountryId ?? 0; + zip = request.ShippingAddress.ZipPostalCode; } - foreach (var shoppingCartItem in getShippingOptionRequest.Items) + foreach (var shoppingCartItem in request.Items) { if (shoppingCartItem.Item.IsFreeShipping || !shoppingCartItem.Item.IsShipEnabled) continue; subTotal += _priceCalculationService.GetSubTotal(shoppingCartItem, true); } - decimal weight = _shippingService.GetShoppingCartTotalWeight(getShippingOptionRequest.Items); + decimal weight = _shippingService.GetShoppingCartTotalWeight(request.Items); - var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest); + var shippingMethods = _shippingService.GetAllShippingMethods(request); foreach (var shippingMethod in shippingMethods) { var record = _shippingByWeightService.FindRecord(shippingMethod.Id, storeId, countryId, weight, zip); diff --git a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs index aaa92a955f..383d77dfa2 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs @@ -50,8 +50,10 @@ public ShippingByWeightController( public ActionResult Configure() { var shippingMethods = _shippingService.GetAllShippingMethods(); - if (shippingMethods.Count == 0) - return Content("No shipping methods can be loaded"); + if (shippingMethods.Count == 0) + { + return Content(T("Admin.Configuration.Shipping.Methods.NoMethodsLoaded")); + } var model = new ShippingByWeightListModel(); var countries = _countryService.GetAllCountries(true); diff --git a/src/Plugins/SmartStore.ShippingByWeight/Description.txt b/src/Plugins/SmartStore.ShippingByWeight/Description.txt index 65f72e227d..218a277c2b 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Description.txt +++ b/src/Plugins/SmartStore.ShippingByWeight/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Shipping by weight SystemName: SmartStore.ShippingByWeight Group: Shipping -Version: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.ShippingByWeight.dll ResourceRootKey: Plugins.Shipping.ByWeight \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs index 06d9b7ff55..f60212f80c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs @@ -48,7 +48,7 @@ public virtual IQueryable GetShippingByWeightRecords() { var query = from x in _sbwRepository.Table - orderby x.StoreId, x.CountryId, x.ShippingMethodId, x.From, x.Zip + orderby x.StoreId, x.CountryId, x.ShippingMethodId, x.From select x; return query; @@ -83,7 +83,7 @@ public virtual IList GetShippingByWeightModels(int pageIn var shippingMethod = _shippingService.GetShippingMethodById(x.ShippingMethodId); var country = _countryService.GetCountryById(x.CountryId); - var model = new ShippingByWeightModel() + var model = new ShippingByWeightModel { Id = x.Id, StoreId = x.StoreId, diff --git a/src/Plugins/SmartStore.ShippingByWeight/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/web.config b/src/Plugins/SmartStore.ShippingByWeight/web.config index 32347fc005..c0f82db177 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/web.config +++ b/src/Plugins/SmartStore.ShippingByWeight/web.config @@ -1,115 +1,115 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs b/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs index 53bc5628e7..ca43d31f14 100644 --- a/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs +++ b/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs @@ -70,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 : "*"; @@ -99,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/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/SmartStore.Tax.csproj b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj index 692427666d..3f4f325f97 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -96,8 +96,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml b/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml index 5b8d09d71d..f3b670b3b2 100644 --- a/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml +++ b/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml @@ -1,10 +1,11 @@ -@{ - Layout = ""; -} -@model SmartStore.Tax.Models.ByRegionTaxRateListModel -@using SmartStore.Web.Framework; +@using SmartStore.Web.Framework; @using Telerik.Web.Mvc.UI; @using System.Linq; +@model SmartStore.Tax.Models.ByRegionTaxRateListModel +@{ + Layout = ""; +} +
@@ -46,8 +47,8 @@
-

-

+ +

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

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

-
- +{ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
- @Html.SmartLabelFor(model => model.AddCountryId) - - @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) - @Html.ValidationMessageFor(model => model.AddCountryId) -
- @Html.SmartLabelFor(model => model.AddStateProvinceId) - - @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates) - @Html.ValidationMessageFor(model => model.AddStateProvinceId) -
- @Html.SmartLabelFor(model => model.AddZip) - - @Html.EditorFor(model => model.AddZip) - @Html.ValidationMessageFor(model => model.AddZip) -
- @Html.SmartLabelFor(model => model.AddTaxCategoryId) - - @Html.DropDownListFor(model => model.AddTaxCategoryId, Model.AvailableTaxCategories) - @Html.ValidationMessageFor(model => model.AddTaxCategoryId) -
- @Html.SmartLabelFor(model => model.AddPercentage) - - @Html.EditorFor(model => model.AddPercentage) - @Html.ValidationMessageFor(model => model.AddPercentage) -
-   - - -
+
+
@T("Plugins.Tax.CountryStateZip.AddRecord.Hint")
+
+
+ @Html.SmartLabelFor(model => model.AddCountryId) + + @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) + @Html.ValidationMessageFor(model => model.AddCountryId) +
+ @Html.SmartLabelFor(model => model.AddStateProvinceId) + + @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates) + @Html.ValidationMessageFor(model => model.AddStateProvinceId) +
+ @Html.SmartLabelFor(model => model.AddZip) + + @Html.EditorFor(model => model.AddZip) + @Html.ValidationMessageFor(model => model.AddZip) +
+ @Html.SmartLabelFor(model => model.AddTaxCategoryId) + + @Html.DropDownListFor(model => model.AddTaxCategoryId, Model.AvailableTaxCategories) + @Html.ValidationMessageFor(model => model.AddTaxCategoryId) +
+ @Html.SmartLabelFor(model => model.AddPercentage) + + @Html.EditorFor(model => model.AddPercentage) + @Html.ValidationMessageFor(model => model.AddPercentage) +
+   + + +
} \ No newline at end of file diff --git a/src/Plugins/SmartStore.Tax/packages.config b/src/Plugins/SmartStore.Tax/packages.config index cc784b42f4..7f794ac209 100644 --- a/src/Plugins/SmartStore.Tax/packages.config +++ b/src/Plugins/SmartStore.Tax/packages.config @@ -6,5 +6,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Tax/web.config b/src/Plugins/SmartStore.Tax/web.config index 3e1b370f5d..ba87d0f098 100644 --- a/src/Plugins/SmartStore.Tax/web.config +++ b/src/Plugins/SmartStore.Tax/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs index 2d4e879f22..d2b92900b9 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs @@ -1,16 +1,24 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using System.Web.Http; using SmartStore.Core; +using SmartStore.Core.Domain; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; using SmartStore.Services.Catalog; +using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Media; using SmartStore.Utilities; +using SmartStore.Utilities.Threading; using SmartStore.Web.Framework.WebApi; using SmartStore.Web.Framework.WebApi.OData; using SmartStore.Web.Framework.WebApi.Security; @@ -18,29 +26,56 @@ namespace SmartStore.WebApi.Controllers.Api { + /// public class UploadsController : ApiController { + private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + private readonly Lazy _productService; private readonly Lazy _pictureService; + private readonly Lazy _importProfileService; private readonly Lazy _storeContext; private readonly Lazy _mediaSettings; public UploadsController( Lazy productService, Lazy pictureService, + Lazy importProfileService, Lazy storeContext, Lazy mediaSettings) { _productService = productService; _pictureService = pictureService; + _importProfileService = importProfileService; _storeContext = storeContext; _mediaSettings = mediaSettings; } - /// + #region Utilities + + private StringContent CloneHeaderContent(string path, MultipartFileData origin) + { + var content = new StringContent(path); + + ContentDispositionHeaderValue disposition; + ContentDispositionHeaderValue.TryParse(origin.Headers.ContentDisposition.ToString(), out disposition); + + content.Headers.ContentDisposition = disposition; + + content.Headers.ContentDisposition.Name = origin.Headers.ContentDisposition.Name.ToUnquoted(); + content.Headers.ContentDisposition.FileName = Path.GetFileName(path); + + content.Headers.ContentType.MediaType = MimeTypes.MapNameToMimeType(path); + + return content; + } + + #endregion + + [HttpPost] [WebApiAuthenticate(Permission = "ManageCatalog")] [WebApiQueryable(PagingOptional = true)] - public async Task> PostProductImages() + public async Task> ProductImages() { if (!Request.Content.IsMimeMultipartContent()) { @@ -56,10 +91,10 @@ public async Task> PostProductImages() { await Request.Content.ReadAsMultipartAsync(provider); } - catch (Exception exc) + catch (Exception exception) { provider.DeleteLocalFiles(); - throw this.ExceptionInternalServerError(exc); + throw this.ExceptionInternalServerError(exception); } // find product entity @@ -82,7 +117,7 @@ public async Task> PostProductImages() if (entity == null) { provider.DeleteLocalFiles(); - throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatWith(identifier.NaIfEmpty())); + throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatInvariant(identifier.NaIfEmpty())); } // process images @@ -97,15 +132,7 @@ public async Task> PostProductImages() foreach (var file in provider.FileData) { - var image = new UploadImage - { - FileName = file.Headers.ContentDisposition.FileName.ToUnquoted(), - Name = file.Headers.ContentDisposition.Name.ToUnquoted(), - ContentDisposition = file.Headers.ContentDisposition.Parameters - }; - - if (file.Headers.ContentType != null) - image.MediaType = file.Headers.ContentType.MediaType.ToUnquoted(); + var image = new UploadImage(file.Headers); if (image.FileName.IsEmpty()) image.FileName = entity.Name; @@ -115,7 +142,6 @@ public async Task> PostProductImages() if (pictureBinary != null && pictureBinary.Length > 0) { pictureBinary = _pictureService.Value.ValidatePicture(pictureBinary); - pictureBinary = _pictureService.Value.FindEqualPicture(pictureBinary, pictures, out equalPictureId); if (pictureBinary != null) @@ -157,5 +183,121 @@ public async Task> PostProductImages() provider.DeleteLocalFiles(); return result.AsQueryable(); } + + [HttpPost] + [WebApiAuthenticate(Permission = "ManageImports")] + [WebApiQueryable(PagingOptional = true)] + public async Task> ImportFiles() + { + if (!Request.Content.IsMimeMultipartContent()) + { + throw this.ExceptionUnsupportedMediaType(); + } + + ImportProfile profile = null; + string identifier = null; + var tempDir = FileSystemHelper.TempDir(Guid.NewGuid().ToString()); + var provider = new MultipartFormDataStreamProvider(tempDir); + + try + { + await Request.Content.ReadAsMultipartAsync(provider); + } + catch (Exception exception) + { + FileSystemHelper.ClearDirectory(tempDir, true); + throw this.ExceptionInternalServerError(exception); + } + + // find import profile + if (provider.FormData.AllKeys.Contains("Id")) + { + identifier = provider.FormData.GetValues("Id").FirstOrDefault(); + profile = _importProfileService.Value.GetImportProfileById(identifier.ToInt()); + } + else if (provider.FormData.AllKeys.Contains("Name")) + { + identifier = provider.FormData.GetValues("Name").FirstOrDefault(); + profile = _importProfileService.Value.GetImportProfileByName(identifier); + } + + if (profile == null) + { + FileSystemHelper.ClearDirectory(tempDir, true); + throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatInvariant(identifier.NaIfEmpty())); + } + + var deleteExisting = false; + var result = new List(); + var unzippedFiles = new List(); + var importFolder = profile.GetImportFolder(true, true); + var csvTypes = new string[] { ".csv", ".txt", ".tab" }; + + if (provider.FormData.AllKeys.Contains("deleteExisting")) + { + var strDeleteExisting = provider.FormData.GetValues("deleteExisting").FirstOrDefault(); + deleteExisting = (strDeleteExisting.HasValue() && strDeleteExisting.ToBool()); + } + + // unzip files + foreach (var file in provider.FileData) + { + var import = new UploadImportFile(file.Headers); + + if (import.FileExtension.IsCaseInsensitiveEqual(".zip")) + { + var subDir = Path.Combine(tempDir, Guid.NewGuid().ToString()); + ZipFile.ExtractToDirectory(file.LocalFileName, subDir); + FileSystemHelper.Delete(file.LocalFileName); + + foreach (var unzippedFile in Directory.GetFiles(subDir, "*.*")) + { + var content = CloneHeaderContent(unzippedFile, file); + unzippedFiles.Add(new MultipartFileData(content.Headers, unzippedFile)); + } + } + else + { + unzippedFiles.Add(new MultipartFileData(file.Headers, file.LocalFileName)); + } + } + + // copy files to import folder + if (unzippedFiles.Any()) + { + using (_rwLock.GetWriteLock()) + { + if (deleteExisting) + { + FileSystemHelper.ClearDirectory(importFolder, false); + } + + foreach (var file in unzippedFiles) + { + var import = new UploadImportFile(file.Headers); + var destPath = Path.Combine(importFolder, import.FileName); + + import.Exists = File.Exists(destPath); + + switch (profile.FileType) + { + case ImportFileType.XLSX: + import.IsSupportedByProfile = import.FileExtension.IsCaseInsensitiveEqual(".xlsx"); + break; + case ImportFileType.CSV: + import.IsSupportedByProfile = csvTypes.Contains(import.FileExtension, StringComparer.OrdinalIgnoreCase); + break; + } + + import.Inserted = FileSystemHelper.Copy(file.LocalFileName, destPath); + + result.Add(import); + } + } + } + + FileSystemHelper.ClearDirectory(tempDir, true); + return result.AsQueryable(); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs index 84d3f2fc09..df8dfd389d 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs @@ -236,6 +236,7 @@ public IQueryable GetProductBundleItems(int key) { var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, Query = this.GetExpandedEntitySet(requiredProperties), ParentGroupedProductId = entity.Id, PageSize = int.MaxValue, diff --git a/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs b/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs index 808387b270..4fb254bbff 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs @@ -97,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 085a7c5a09..646ac2fd0d 100644 --- a/src/Plugins/SmartStore.WebApi/Description.txt +++ b/src/Plugins/SmartStore.WebApi/Description.txt @@ -1,8 +1,8 @@ FriendlyName: SmartStore.NET Web Api SystemName: SmartStore.WebApi -Version: 2.2.0.4 +Version: 2.6.0 Group: Api -MinAppVersion: 2.2.0 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.WebApi.dll diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml index f34a2ceb2e..650e50d1ab 100644 --- a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml @@ -30,10 +30,10 @@ Aus Sicherheitsgründen darf die Zeit, zu der die Anfrage an die API gesendet wurde, nicht zu weit von der aktuellen Server-Zeit abweichen. Mit dieser Einstellung legen Sie dieses Zeitfenster in Minuten fest. - Unauthorisierte Zugriffe speichern + Unautorisierte Zugriffe speichern - Jeder unauthorisierte Zugriff wird in der Ereignisliste gespeichert. + Jeder unautorisierte Zugriff wird in der Ereignisliste gespeichert. Keine Zeitstempelprüfung @@ -41,6 +41,12 @@ Legt fest, ob geprüft werden soll, ob der Zeitstempel der letzten Anfrage kleiner gleich der der aktuellen ist. Verhindert das Auftreten von HmacResult.TimestampOlderThanLastRequest. + + Authentifizierung ohne MD5-Hash erlauben + + + Legt fest, ob Authentifizierungen ohne MD5 Inhalts-Hash erlaubt sind. + Öffentlicher Schlüssel diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml index c3a2bca64a..7aafa97635 100644 --- a/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml @@ -40,6 +40,12 @@ Specifies whether to validate that the timestamp of the previous request is less or equal than the one of the current. Prevents the occurrence of HmacResult.TimestampOlderThanLastRequest. + + + Allow authentification without MD5 hash + + + Specifies whether to allow authentifications without MD5 content hash. Public key diff --git a/src/Plugins/SmartStore.WebApi/Models/Api/UploadFileBase.cs b/src/Plugins/SmartStore.WebApi/Models/Api/UploadFileBase.cs new file mode 100644 index 0000000000..d0884fa6a3 --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Models/Api/UploadFileBase.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http.Headers; +using System.Runtime.Serialization; + +namespace SmartStore.WebApi.Models.Api +{ + [DataContract] + public abstract partial class UploadFileBase + { + public UploadFileBase() + { + } + + public UploadFileBase(HttpContentHeaders headers) + { + Name = headers.ContentDisposition.Name.ToUnquoted(); + FileName = headers.ContentDisposition.FileName.ToUnquoted(); + ContentDisposition = headers.ContentDisposition.Parameters; + + if (headers.ContentType != null) + { + MediaType = headers.ContentType.MediaType.ToUnquoted(); + } + + if (FileName.HasValue()) + { + FileExtension = Path.GetExtension(FileName); + } + } + + /// + /// Unquoted name attribute of content-disposition multipart header + /// + [DataMember] + public string Name { get; set; } + + /// + /// Unquoted filename attribute of content-disposition multipart header + /// + [DataMember] + public string FileName { get; set; } + + /// + /// Extension of FileName + /// + [DataMember] + public string FileExtension { get; set; } + + /// + /// Media (mime) type of content-type multipart header + /// + [DataMember] + public string MediaType { get; set; } + + /// + /// Indicates whether the uploaded file already exist + /// + [DataMember] + public bool Exists { get; set; } + + /// + /// Indicates whether the uploaded file has been inserted + /// + [DataMember] + public bool Inserted { get; set; } + + /// + /// Raw custom parameters of the content-disposition multipart header + /// + [DataMember] + public ICollection ContentDisposition { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs index 5ad5933fc9..cca701246b 100644 --- a/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs +++ b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs @@ -1,42 +1,19 @@ -using System.Collections.Generic; -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Runtime.Serialization; using SmartStore.Core.Domain.Media; namespace SmartStore.WebApi.Models.Api { [DataContract] - public partial class UploadImage + public partial class UploadImage : UploadFileBase { - /// - /// Unquoted name attribute of content-disposition multipart header - /// - [DataMember] - public string Name { get; set; } - - /// - /// Unquoted filename attribute of content-disposition multipart header - /// - [DataMember] - public string FileName { get; set; } - - /// - /// Media (mime) type of content-type multipart header - /// - [DataMember] - public string MediaType { get; set; } + public UploadImage() + { + } - /// - /// Indicates whether the uploaded image already exist and therefore has been skipped - /// - [DataMember] - public bool Exists { get; set; } - - /// - /// Indicates whether the uploaded image has been inserted - /// - [DataMember] - public bool Inserted { get; set; } + public UploadImage(HttpContentHeaders headers) : base(headers) + { + } /// /// Url of the default size image @@ -56,12 +33,6 @@ public partial class UploadImage [DataMember] public string FullSizeImageUrl { get; set; } - /// - /// Raw custom parameters of the content-disposition multipart header - /// - [DataMember] - public ICollection ContentDisposition { get; set; } - /// /// The picture entity. Can be null. /// diff --git a/src/Plugins/SmartStore.WebApi/Models/Api/UploadImportFile.cs b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImportFile.cs new file mode 100644 index 0000000000..ac43524577 --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImportFile.cs @@ -0,0 +1,23 @@ +using System.Net.Http.Headers; +using System.Runtime.Serialization; + +namespace SmartStore.WebApi.Models.Api +{ + [DataContract] + public partial class UploadImportFile : UploadFileBase + { + public UploadImportFile() + { + } + + public UploadImportFile(HttpContentHeaders headers) : base(headers) + { + } + + /// + /// Whether the file type is supported by the import profile + /// + [DataMember] + public bool IsSupportedByProfile { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs b/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs index e5c3e39e0f..23882c149e 100644 --- a/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs +++ b/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs @@ -18,7 +18,10 @@ public class WebApiConfigModel : ModelBase [SmartResourceDisplayName("Plugins.Api.WebApi.NoRequestTimestampValidation")] public bool NoRequestTimestampValidation { get; set; } - [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] + [SmartResourceDisplayName("Plugins.Api.WebApi.AllowEmptyMd5Hash")] + public bool AllowEmptyMd5Hash { get; set; } + + [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] public bool LogUnauthorized { get; set; } public int GridPageSize { get; set; } @@ -29,12 +32,14 @@ public void Copy(WebApiSettings settings, bool fromSettings) { ValidMinutePeriod = settings.ValidMinutePeriod; NoRequestTimestampValidation = settings.NoRequestTimestampValidation; + AllowEmptyMd5Hash = settings.AllowEmptyMd5Hash; LogUnauthorized = settings.LogUnauthorized; } else { settings.ValidMinutePeriod = ValidMinutePeriod; settings.NoRequestTimestampValidation = NoRequestTimestampValidation; + settings.AllowEmptyMd5Hash = AllowEmptyMd5Hash; settings.LogUnauthorized = LogUnauthorized; } } diff --git a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj index ffd8a4f776..73e315200d 100644 --- a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj +++ b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj @@ -107,12 +107,14 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + False @@ -229,7 +231,9 @@ + + diff --git a/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml b/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml index 9e21e21993..9c6dbd98ef 100644 --- a/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml +++ b/src/Plugins/SmartStore.WebApi/Views/WebApi/Configure.cshtml @@ -107,6 +107,15 @@ @Html.ValidationMessageFor(model => model.NoRequestTimestampValidation) + + + @Html.SmartLabelFor(model => model.AllowEmptyMd5Hash) + + + @Html.EditorFor(model => model.AllowEmptyMd5Hash) + @Html.ValidationMessageFor(model => model.AllowEmptyMd5Hash) + + @Html.SmartLabelFor(model => model.LogUnauthorized) diff --git a/src/Plugins/SmartStore.WebApi/WebApiPlugin.cs b/src/Plugins/SmartStore.WebApi/WebApiPlugin.cs index b0c7c61515..43d717c4c1 100644 --- a/src/Plugins/SmartStore.WebApi/WebApiPlugin.cs +++ b/src/Plugins/SmartStore.WebApi/WebApiPlugin.cs @@ -59,9 +59,6 @@ public override void Uninstall() _permissionService.UninstallPermissions(new WebApiPermissionProvider()); - _localizationService.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); - _localizationService.DeleteLocaleStringResources("Plugins.FriendlyName.Api.WebApi", false); - base.Uninstall(); } diff --git a/src/Plugins/SmartStore.WebApi/changelog.md b/src/Plugins/SmartStore.WebApi/changelog.md index 23143ca0fc..a2f7a528c8 100644 --- a/src/Plugins/SmartStore.WebApi/changelog.md +++ b/src/Plugins/SmartStore.WebApi/changelog.md @@ -1,5 +1,13 @@ #Release Notes +##Web Api 2.5.0.1 +###New Features +* Option to allow authentification without MD5 content hash + +##Web Api 2.2.0.5 +###New Features +* Bridge to import framework: uploading import files to import profile directory + ##Web Api 2.2.0.4 ###New Features * Added OData endpoint for shipment items diff --git a/src/Plugins/SmartStore.WebApi/packages.config b/src/Plugins/SmartStore.WebApi/packages.config index 4b574c3ff9..9e4fbc38a8 100644 --- a/src/Plugins/SmartStore.WebApi/packages.config +++ b/src/Plugins/SmartStore.WebApi/packages.config @@ -16,6 +16,6 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/web.config b/src/Plugins/SmartStore.WebApi/web.config index 71013386a2..95ed427e0a 100644 --- a/src/Plugins/SmartStore.WebApi/web.config +++ b/src/Plugins/SmartStore.WebApi/web.config @@ -50,7 +50,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs index 3b6cf51813..29fdb90d52 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Web; using System.Web.Mvc; using SmartStore.Core.Localization; using SmartStore.Core.Logging; @@ -92,17 +93,89 @@ protected virtual void NotifyError(Exception exception, bool durable = true, boo LogException(exception); } - Services.Notifier.Error(exception.ToAllMessages(), durable); + Services.Notifier.Error(HttpUtility.HtmlEncode(exception.ToAllMessages()), durable); } - protected virtual ActionResult RedirectToReferrer() + /// + /// Pushes an error message to the notification queue that the access to a resource has been denied + /// + /// A value indicating whether a message should be persisted for the next request + /// A value indicating whether the message should be logged + protected virtual void NotifyAccessDenied(bool durable = true, bool log = true) { - if (Request.UrlReferrer != null && Request.UrlReferrer.ToString().HasValue()) + var message = T("Admin.AccessDenied.Description"); + + if (log) { - return Redirect(Request.UrlReferrer.ToString()); + Logger.Error(message, null, Services.WorkContext.CurrentCustomer); } - return RedirectToRoute("HomePage"); + Services.Notifier.Error(message, durable); + } + + protected ActionResult RedirectToReferrer() + { + return RedirectToReferrer(null, () => RedirectToRoute("HomePage")); + } + + protected ActionResult RedirectToReferrer(string referrer) + { + return RedirectToReferrer(referrer, () => RedirectToRoute("HomePage")); + } + + protected ActionResult RedirectToReferrer(string referrer, string fallbackUrl) + { + // addressing "Open Redirection Vulnerability" (prevent cross-domain redirects / phishing) + if (fallbackUrl.HasValue() && !Url.IsLocalUrl(fallbackUrl)) + { + fallbackUrl = null; + } + + return RedirectToReferrer( + referrer, + fallbackUrl.HasValue() ? () => Redirect(fallbackUrl) : (Func)null); + } + + protected virtual ActionResult RedirectToReferrer(string referrer, Func fallbackResult) + { + bool skipLocalCheck = false; + + if (referrer.IsEmpty() && Request.UrlReferrer != null && Request.UrlReferrer.ToString().HasValue()) + { + referrer = Request.UrlReferrer.ToString(); + if (referrer.HasValue()) + { + var domain1 = (new Uri(referrer)).GetLeftPart(UriPartial.Authority); + var domain2 = this.Request.Url.GetLeftPart(UriPartial.Authority); + if (domain1.IsCaseInsensitiveEqual(domain2)) + { + // always allow fully qualified urls from local host + skipLocalCheck = true; + } + else + { + referrer = null; + } + } + } + + // addressing "Open Redirection Vulnerability" (prevent cross-domain redirects / phishing) + if (referrer.HasValue() && !skipLocalCheck && !Url.IsLocalUrl(referrer)) + { + referrer = null; + } + + if (referrer.HasValue()) + { + return Redirect(referrer); + } + + if (fallbackResult != null) + { + return fallbackResult(); + } + + return HttpNotFound(); } protected virtual ActionResult RedirectToHomePageWithError(string reason, bool durable = true) diff --git a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs index eb2f5b2b1e..73a2c9bd9a 100644 --- a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs +++ b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs @@ -67,17 +67,19 @@ using SmartStore.Services.Themes; using SmartStore.Services.Topics; using SmartStore.Utilities; -using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Bundling; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Plugins; +using SmartStore.Web.Framework.Routing; using SmartStore.Web.Framework.Theming; using SmartStore.Web.Framework.UI; using SmartStore.Web.Framework.WebApi; using SmartStore.Web.Framework.WebApi.Configuration; using Module = Autofac.Module; -using SmartStore.Web.Framework.Bundling; -using SmartStore.Web.Framework.Filters; -using SmartStore.Web.Framework.Routing; +using SmartStore.Services.Catalog.Importer; +using SmartStore.Services.Customers.Importer; +using SmartStore.Services.Messages.Importer; namespace SmartStore.Web.Framework { @@ -105,7 +107,8 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b builder.RegisterModule(new PackagingModule()); builder.RegisterModule(new ProvidersModule(typeFinder, pluginFinder)); builder.RegisterModule(new TasksModule(typeFinder)); - } + builder.RegisterModule(new DataExchangeModule(typeFinder)); + } public int Order { @@ -241,13 +244,8 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - - builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -1083,9 +1081,44 @@ protected override void Load(ContainerBuilder builder) }); } - } + public class DataExchangeModule : Module + { + private readonly ITypeFinder _typeFinder; + + public DataExchangeModule(ITypeFinder typeFinder) + { + _typeFinder = typeFinder; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + + if (!DataSettings.DatabaseIsInstalled()) + return; + + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + + // IEntityImporter implementations + builder.RegisterType().Keyed(ImportEntityType.Product).InstancePerRequest(); + builder.RegisterType().Keyed(ImportEntityType.Category).InstancePerRequest(); + builder.RegisterType().Keyed(ImportEntityType.Customer).InstancePerRequest(); + builder.RegisterType().Keyed(ImportEntityType.NewsLetterSubscription).InstancePerRequest(); + + // Register resolving delegate + builder.Register>(c => + { + var cc = c.Resolve(); + return keyed => cc.ResolveKeyed(keyed); + }); + } + + } + #endregion #region Sources diff --git a/src/Presentation/SmartStore.Web.Framework/Events/TabStripCreated.cs b/src/Presentation/SmartStore.Web.Framework/Events/TabStripCreated.cs index 3b4b4e6893..d86a2d5c9c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Events/TabStripCreated.cs +++ b/src/Presentation/SmartStore.Web.Framework/Events/TabStripCreated.cs @@ -1,14 +1,20 @@ using System; using System.Web.Mvc; using SmartStore.Web.Framework.UI; +using System.Collections; +using System.Collections.Generic; +using SmartStore.Core.Infrastructure; +using SmartStore.Web.Framework.Localization; namespace SmartStore.Web.Framework.Events { /// - /// Admin tabstrip created event + /// Tabstrip created event /// public class TabStripCreated { + private IList _widgets; + public TabStripCreated(TabFactory itemFactory, string tabStripName, HtmlHelper html, object model = null) { this.TabStripName = tabStripName; @@ -21,5 +27,37 @@ public TabStripCreated(TabFactory itemFactory, string tabStripName, HtmlHelper h public HtmlHelper Html { get; private set; } public object Model { get; private set; } public TabFactory ItemFactory { get; private set; } + + /// + /// Renders a child action into a dynamically created special tab called 'Plugins' + /// + /// + /// Should only be called for admin tabstrips + public void AddWidget(RouteInfo routeInfo) + { + Guard.ArgumentNotNull(() => routeInfo); + + if (_widgets == null) + { + _widgets = new List(); + Html.ViewContext.ViewData["Tab.{0}.Widgets".FormatInvariant(this.TabStripName)] = _widgets; + + CreateWidgetsTab(); + } + + _widgets.Add(routeInfo); + } + + private Tab CreateWidgetsTab() + { + return ItemFactory.Add() + .Text(EngineContext.Current.Resolve().Get("Admin.Plugins")) + .Name("tab-special-plugin-widgets") + .Icon("fa fa-puzzle-piece fa-lg fa-fw") + .LinkHtmlAttributes(new { data_tab_name = "PLUGIN_WIDGETS" }) + .Content("TabWidgets", "Widget", new { area = "", model = this.Model, viewDataKey = "Tab.{0}.Widgets".FormatInvariant(this.TabStripName) }) + .Ajax(false) + .Item; + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index b279a5b083..b56f183331 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -1,29 +1,29 @@ using System; -using System.Web; -using System.Threading; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Threading; +using System.Web; using System.Web.Mvc; -using System.Web.Routing; using System.Web.Mvc.Html; +using System.Web.Routing; using System.Web.WebPages; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Infrastructure; using SmartStore.Services.Localization; -using SmartStore.Web.Framework.Localization; -using SmartStore.Web.Framework.UI; -using SmartStore.Web.Framework.Settings; using SmartStore.Utilities; -using SmartStore.Core.Domain.Catalog; +using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Framework.Settings; +using SmartStore.Web.Framework.UI; namespace SmartStore.Web.Framework { - public enum InputEditorType + public enum InputEditorType { TextBox, Password, Hidden, @@ -447,8 +447,12 @@ public static MvcHtmlString ControlGroupFor( { return html.HiddenFor(expression); } - - var sb = new StringBuilder("
"); + + string inputHtml = ""; + var htmlAttributes = new RouteValueDictionary(); + var dataTypeName = ModelMetadata.FromLambdaExpression(expression, html.ViewData).DataTypeName.EmptyNull(); + + var sb = new StringBuilder("
"); if (editorType != InputEditorType.Checkbox) { @@ -458,14 +462,23 @@ public static MvcHtmlString ControlGroupFor( } sb.AppendLine("
"); - string inputHtml = ""; - object attrs = null; + if (!required && (editorType == InputEditorType.TextBox || editorType == InputEditorType.Password)) { - attrs = new { placeholder = "Optional" /* TODO: Loc */ }; + htmlAttributes.Add("placeholder", EngineContext.Current.Resolve().GetResource("Common.Optional")); } - //var x = ModelMetadata.FromLambdaExpression(expression, html.ViewData).DisplayName; - switch (editorType) + + switch (dataTypeName) + { + case "EmailAddress": + htmlAttributes.Add("type", "email"); + break; + case "PhoneNumber": + htmlAttributes.Add("type", "tel"); + break; + } + + switch (editorType) { case InputEditorType.Checkbox: inputHtml = string.Format("", @@ -473,12 +486,13 @@ public static MvcHtmlString ControlGroupFor( ModelMetadata.FromLambdaExpression(expression, html.ViewData).DisplayName); // TBD: ist das OK so? break; case InputEditorType.Password: - inputHtml = html.PasswordFor(expression, attrs).ToString(); + inputHtml = html.PasswordFor(expression, htmlAttributes).ToString(); break; default: - inputHtml = html.TextBoxFor(expression, attrs).ToString(); + inputHtml = html.TextBoxFor(expression, htmlAttributes).ToString(); break; } + sb.AppendLine(inputHtml); sb.AppendLine(html.ValidationMessageFor(expression).ToString()); if (helpHint.HasValue()) @@ -612,54 +626,125 @@ public static MvcHtmlString CollapsedText(this HtmlHelper helper, string text) return MvcHtmlString.Create(result); } - public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string fileExtension, bool renderExtensionText) + public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string fileExtension, bool renderLabel = false) + { + return IconForFileExtension(helper, fileExtension, null, renderLabel); + } + + public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string fileExtension, string extraCssClasses = null, bool renderLabel = false) { - string result = ""; + Guard.ArgumentNotNull(() => helper); + Guard.ArgumentNotEmpty(() => fileExtension); - if (fileExtension != null && fileExtension.StartsWith(".")) - { - fileExtension = fileExtension.Substring(1); - } + var icon = "file-o"; + var ext = fileExtension; - if (fileExtension.IsCaseInsensitiveEqual("xml")) - { - result = ""; - } - else if (fileExtension.IsCaseInsensitiveEqual("xls") || fileExtension.IsCaseInsensitiveEqual("xlsx")) - { - result = ""; - } - else if (fileExtension.IsCaseInsensitiveEqual("pdf")) - { - result = ""; - } - else if (fileExtension.IsCaseInsensitiveEqual("zip")) - { - result = ""; - } - else if (fileExtension.IsCaseInsensitiveEqual("txt") || fileExtension.IsCaseInsensitiveEqual("csv")) + if (ext != null && ext.StartsWith(".")) { - result = ""; + ext = ext.Substring(1); } - else if (fileExtension.IsCaseInsensitiveEqual("doc")) - { - result = ""; - } - else if (fileExtension.IsCaseInsensitiveEqual("jpg") || fileExtension.IsCaseInsensitiveEqual("png") || fileExtension.IsCaseInsensitiveEqual("gif")) + + if (ext.HasValue()) { - result = ""; + switch (ext.ToLowerInvariant()) + { + case "pdf": + icon = "file-pdf-o"; + break; + case "doc": + case "docx": + case "docm": + case "odt": + case "dot": + case "dotx": + case "dotm": + icon = "file-word-o"; + break; + case "xls": + case "xlsx": + case "xlsm": + case "xlsb": + case "ods": + icon = "file-excel-o"; + break; + case "csv": + case "tab": + icon = "table"; + break; + case "ppt": + case "pptx": + case "pptm": + case "ppsx": + case "odp": + case "potx": + case "pot": + case "potm": + case "pps": + case "ppsm": + icon = "file-powerpoint-o"; + break; + case "zip": + case "rar": + case "7z": + icon = "file-archive-o"; + break; + case "png": + case "jpg": + case "jpeg": + case "bmp": + case "psd": + icon = "file-image-o"; + break; + case "mp3": + case "wav": + case "ogg": + case "wma": + icon = "file-audio-o"; + break; + case "mp4": + case "mkv": + case "wmv": + case "avi": + case "asf": + case "mpg": + case "mpeg": + icon = "file-video-o"; + break; + case "txt": + icon = "file-text-o"; + break; + case "exe": + icon = "gear"; + break; + case "xml": + case "html": + case "htm": + icon = "file-code-o"; + break; + } } - if (renderExtensionText) + var label = ext.NaIfEmpty().ToUpper(); + + var result = "".FormatInvariant( + icon, + extraCssClasses.HasValue() ? " " + extraCssClasses : "", + label); + + if (renderLabel) { - if (fileExtension.IsEmpty()) + if (ext.IsEmpty()) + { result = "{0}".FormatInvariant("".NaIfEmpty()); + } else - result = result + "{0}"; + { + result = result + "{0}".FormatInvariant(label); + } } - return MvcHtmlString.Create(result.FormatInvariant(fileExtension.NaIfEmpty().ToUpper())); + return MvcHtmlString.Create(result); } - } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs index fce317402c..11b8f1eeb0 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs @@ -8,7 +8,6 @@ namespace SmartStore.Web.Framework.Filters { public class StoreIpAddressAttribute : ActionFilterAttribute { - public Lazy WebHelper { get; set; } public Lazy WorkContext { get; set; } public Lazy CustomerService { get; set; } @@ -21,17 +20,15 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) if (filterContext == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) return; - //don't apply filter to child methods if (filterContext.IsChildAction) return; - //only GET requests if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return; var webHelper = this.WebHelper.Value; - //update IP address + // Update IP address string currentIpAddress = webHelper.GetCurrentIpAddress(); if (!String.IsNullOrEmpty(currentIpAddress)) { diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs b/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs index 9e0551bc26..f96004f6f3 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SmartStore.Core.Localization; +using SmartStore.Core.Localization; using SmartStore.Services.Localization; namespace SmartStore.Web.Framework.Localization @@ -12,20 +9,30 @@ public class Text : IText public Text(ILocalizationService localizationService) { - this._localizationService = localizationService; + _localizationService = localizationService; } public LocalizedString Get(string key, params object[] args) { - var value = _localizationService.GetResource(key); - if (string.IsNullOrEmpty(value)) + try { - return new LocalizedString(key); + var value = _localizationService.GetResource(key); + + if (string.IsNullOrEmpty(value)) + { + return new LocalizedString(key); + } + + if (args == null || args.Length == 0) + { + return new LocalizedString(value); + } + + return new LocalizedString(string.Format(value, args), key, args); } - return - new LocalizedString((args == null || args.Length == 0) - ? value - : string.Format(value, args), key, args); + catch { } + + return new LocalizedString(key); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs index e93252bf47..9d2de00ebf 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/ProviderManager.cs @@ -108,21 +108,13 @@ protected virtual void SetUserData(ProviderMetadata metadata) var displayOrder = _pluginMediator.GetUserDisplayOrder(metadata); var name = _pluginMediator.GetSetting(metadata, "FriendlyName"); var description = _pluginMediator.GetSetting(metadata, "Description"); + metadata.FriendlyName = name; + metadata.Description = description; if (displayOrder.HasValue) { metadata.DisplayOrder = displayOrder.Value; } - - if (name != null) - { - metadata.FriendlyName = name; - } - - if (description != null) - { - metadata.Description = description; - } } private bool IsActiveForStore(PluginDescriptor plugin, int storeId) diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs index 2b3d2f2bd5..f683d550e1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs @@ -57,7 +57,7 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) if (securityMode == HttpSecurityMode.SharedSsl) { // Don't attempt to redirect when shared SSL is being used and current request is secured. - // Redirecting from https://ssl.bla.com to https://www.ssl.bla.com will most probably fail. + // Redirecting from http://ssl.bla.com to https://www.ssl.bla.com will most probably fail. return; } } diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs index 0c73aa1f3a..80a2d035e8 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs @@ -19,6 +19,7 @@ public StoreDependingSettingHelper(ViewDataDictionary viewData) } public static string ViewDataKey { get { return "StoreDependingSettingData"; } } + public StoreDependingSettingData Data { get @@ -38,16 +39,19 @@ private bool IsOverrideChecked(string settingKey, FormCollection form) } return false; } + public bool IsOverrideChecked(object settings, string name, FormCollection form) { var key = settings.GetType().Name + "." + name; return IsOverrideChecked(key, form); } + public void AddOverrideKey(object settings, string name) { var key = settings.GetType().Name + "." + name; Data.OverrideSettingKeys.Add(key); } + public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootSettingClass = null) { _viewData[ViewDataKey] = new StoreDependingSettingData() @@ -57,112 +61,60 @@ public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootS }; } - public void GetOverrideKeys(object settings, object model, int storeId, ISettingService settingService, bool isRootModel = true) - { - if (storeId <= 0) - return; // single store mode -> there are no overrides + public void GetOverrideKeys(object settings, object model, int storeId, ISettingService settingService, bool isRootModel = true, ILocalizedModelLocal localized = null, int? index = null) + { + if (storeId <= 0) + return; // single store mode -> there are no overrides - var data = Data; - if (data == null) - data = new StoreDependingSettingData(); + var data = Data; + if (data == null) + data = new StoreDependingSettingData(); - var settingName = settings.GetType().Name; - var properties = settings.GetType().GetProperties(); + var settingName = settings.GetType().Name; + var properties = settings.GetType().GetProperties(); + var localizedEntityService = EngineContext.Current.Resolve(); var modelType = model.GetType(); - foreach (var prop in properties) - { - var name = prop.Name; - var modelProperty = modelType.GetProperty(name); - - if (modelProperty == null) - continue; // setting is not configurable or missing or whatever... however we don't need the override info - - var key = settingName + "." + name; - var setting = settingService.GetSettingByKey(key, storeId: storeId); - - if (setting != null) - data.OverrideSettingKeys.Add(key); - } - - if (isRootModel) - { - data.ActiveStoreScopeConfiguration = storeId; - data.RootSettingClass = settingName; - - _viewData[ViewDataKey] = data; - } - } - - // DRY? - public void GetOverrideKeysLocalized(object settings, object model, int storeId, ISettingService settingService, bool isRootModel = true, ILocalizedModelLocal localized = null, int? index = null) - { - if (storeId <= 0) - return; // single store mode -> there are no overrides - - var data = Data; - if (data == null) - data = new StoreDependingSettingData(); - - var settingName = settings.GetType().Name; - var properties = localized.GetType().GetProperties(); - var localizedEntityService = EngineContext.Current.Resolve(); - - var modelType = model.GetType(); - - foreach (var prop in properties) - { - var name = prop.Name; + foreach (var prop in properties) + { + var name = prop.Name; var modelProperty = modelType.GetProperty(name); - if (modelProperty == null) - continue; // setting is not configurable or missing or whatever... however we don't need the override info - - var key = "Locales[" + index.ToString() + "]." + name; + if (modelProperty == null) + continue; // setting is not configurable or missing or whatever... however we don't need the override info - var resultStr = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); + var key = String.Empty; + var setting = String.Empty; - if (!String.IsNullOrEmpty(resultStr)) - data.OverrideSettingKeys.Add(key); - } + if (localized == null) + { + key = settingName + "." + name; + setting = settingService.GetSettingByKey(key, storeId: storeId); + } + else + { + key = "Locales[" + index.ToString() + "]." + name; + setting = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); + } - if (isRootModel) - { - data.ActiveStoreScopeConfiguration = storeId; - data.RootSettingClass = settingName; + if (!String.IsNullOrEmpty(setting)) + data.OverrideSettingKeys.Add(key); + } - _viewData[ViewDataKey] = data; - } - } + if (isRootModel) + { + data.ActiveStoreScopeConfiguration = storeId; + data.RootSettingClass = settingName; - public void UpdateSettings(object settings, FormCollection form, int storeId, ISettingService settingService) - { - var settingName = settings.GetType().Name; - var properties = FastProperty.GetProperties(settings.GetType()).Values; - - foreach (var prop in properties) - { - var name = prop.Name; - var key = settingName + "." + name; - - if (storeId == 0 || IsOverrideChecked(key, form)) - { - dynamic value = prop.GetValue(settings); - settingService.SetSetting(key, value == null ? "" : value, storeId, false); - } - else if (storeId > 0) - { - settingService.DeleteSetting(key, storeId); - } - } - } + _viewData[ViewDataKey] = data; + } + } - // DRY? - public void UpdateLocalizedSettings(object settings, FormCollection form, int storeId, ISettingService settingService, ILocalizedModelLocal localized) + public void UpdateSettings(object settings, FormCollection form, int storeId, ISettingService settingService, ILocalizedModelLocal localized = null) { var settingName = settings.GetType().Name; - var properties = FastProperty.GetProperties(localized.GetType()).Values; + var properties = FastProperty.GetProperties(localized == null ? settings.GetType() : localized.GetType()).Values; foreach (var prop in properties) { @@ -171,7 +123,7 @@ public void UpdateLocalizedSettings(object settings, FormCollection form, int st if (storeId == 0 || IsOverrideChecked(key, form)) { - dynamic value = prop.GetValue(settings); + dynamic value = prop.GetValue(localized == null ? settings : localized); settingService.SetSetting(key, value == null ? "" : value, storeId, false); } else if (storeId > 0) diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index 05afb411d8..b37e9baf66 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -122,12 +122,9 @@ 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\recaptcha.1.0.5.0\lib\.NetFramework 4.0\Recaptcha.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -140,6 +137,7 @@ ..\..\packages\Microsoft.AspNet.WebApi.Client.5.1.2\lib\net45\System.Net.Http.Formatting.dll + False diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs index d9464f2c6a..bb5a86dd51 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/AdminThemedAttribute.cs @@ -22,8 +22,8 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) // add extra view location formats to all view results (even the partial ones) filterContext.RouteData.DataTokens["ExtraAreaViewLocations"] = new string[] { - "~/Administration/Views/{1}/{0}.cshtml", - "~/Administration/Views/Shared/{0}.cshtml" + "~/Administration/Views/{1}/{0}", + "~/Administration/Views/Shared/{0}" }; } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs index 87e9caafdf..8004a1aec5 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableRazorViewEngine.cs @@ -14,41 +14,34 @@ public ThemeableRazorViewEngine() var areaBasePaths = areaBasePathsSetting.Split(',').Select(x => x.Trim().EnsureEndsWith("/")).ToArray(); // 0: view, 1: controller, 2: area - var areaFormats = new string[] { "{2}/Views/{1}/{0}.cshtml", "{2}/Views/Shared/{0}.cshtml" }; - var areaViewLocationFormats = areaBasePaths.SelectMany(x => areaFormats.Select(f => x + f)); + var areaFormats = new string[] { "{2}/Views/{1}/{0}", "{2}/Views/Shared/{0}" }; + var areaViewLocationFormats = ExpandLocationFormats(areaBasePaths.SelectMany(x => areaFormats.Select(f => x + f))); AreaViewLocationFormats = areaViewLocationFormats.ToArray(); AreaMasterLocationFormats = areaViewLocationFormats.ToArray(); AreaPartialViewLocationFormats = areaViewLocationFormats.ToArray(); - // 0: view, 1: controller, 2: theme - ViewLocationFormats = new[] + // 0: view, 1: controller, 2: theme + var locationFormats = ExpandLocationFormats(new[] { - "~/Themes/{2}/Views/{1}/{0}.cshtml", - "~/Views/{1}/{0}.cshtml", - "~/Themes/{2}/Views/Shared/{0}.cshtml", - "~/Views/Shared/{0}.cshtml" - }; + "~/Themes/{2}/Views/{1}/{0}", + "~/Views/{1}/{0}", + "~/Themes/{2}/Views/Shared/{0}", + "~/Views/Shared/{0}" + }); - // 0: view, 1: controller, 2: theme - MasterLocationFormats = new[] - { - "~/Themes/{2}/Views/{1}/{0}.cshtml", - "~/Views/{1}/{0}.cshtml", - "~/Themes/{2}/Views/Shared/{0}.cshtml", - "~/Views/Shared/{0}.cshtml" - }; + ViewLocationFormats = locationFormats.ToArray(); + MasterLocationFormats = locationFormats.ToArray(); + PartialViewLocationFormats = locationFormats.ToArray(); - // 0: view, 1: controller, 2: theme - PartialViewLocationFormats = new[] + if (EnableVbViews) { - "~/Themes/{2}/Views/{1}/{0}.cshtml", - "~/Views/{1}/{0}.cshtml", - "~/Themes/{2}/Views/Shared/{0}.cshtml", - "~/Views/Shared/{0}.cshtml" - }; - - FileExtensions = new[] { "cshtml" }; + FileExtensions = new[] { "cshtml", "vbhtml" }; + } + else + { + FileExtensions = new[] { "cshtml" }; + } } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs index 79d7508ad8..cf07e30c28 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs @@ -19,7 +19,8 @@ public abstract class ThemeableVirtualPathProviderViewEngine : BuildManagerViewE private static readonly string[] _emptyLocations = new string[0]; private static bool? _enableLocalizedViews; - private readonly string _cacheKeyType = typeof(ThemeableRazorViewEngine).Name; + private static bool? _enableVbViews; + private readonly string _cacheKeyType = typeof(ThemeableRazorViewEngine).Name; private readonly string _cacheKeyEntry = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}"; #endregion @@ -121,7 +122,24 @@ public static bool EnableLocalizedViews } } - protected virtual string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string theme, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) + public static bool EnableVbViews + { + get + { + if (!_enableVbViews.HasValue) + { + _enableVbViews = CommonHelper.GetAppSetting("sm:EnableVbViews", false); + } + + return _enableVbViews.Value; + } + set + { + _enableVbViews = value; + } + } + + protected virtual string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string theme, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) { searchedLocations = _emptyLocations; @@ -146,11 +164,11 @@ protected virtual string GetPath(ControllerContext controllerContext, string[] l if (isAdminArea) { // the admin area cannot fallback to itself. Prepend to list. - extraAreaViewLocations.Reverse().Each(x => newLocations.Insert(0, x)); + ExpandLocationFormats(extraAreaViewLocations).Reverse().Each(x => newLocations.Insert(0, x)); } else { - newLocations.AddRange(extraAreaViewLocations); + newLocations.AddRange(ExpandLocationFormats(extraAreaViewLocations)); } areaLocations = newLocations.ToArray(); @@ -317,6 +335,21 @@ internal static string AppendDisplayModeToCacheKey(string cacheKey, string displ return cacheKey + displayMode + ":"; } + protected virtual IEnumerable ExpandLocationFormats(IEnumerable formats) + { + // appends razor view file extensions to location formats + Guard.ArgumentNotNull(() => formats); + + foreach (var format in formats) + { + yield return format + ".cshtml"; + if (EnableVbViews) + { + yield return format + ".vbhtml"; + } + } + } + protected virtual List GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats) { List allLocations = new List(); diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs index b4a4268f49..b6fade194d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs @@ -322,9 +322,9 @@ public string GetThemeVariable(string varname, string defaultValue = "") return defaultValue; } - public static string GenerateHelpUrl(string path) + public string GenerateHelpUrl(string path) { - return SmartStoreVersion.GenerateHelpUrl(path); + return SmartStoreVersion.GenerateHelpUrl(WorkContext.WorkingLanguage.UniqueSeoCode, path); } private void EnsureThemeContextInitialized() diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Captcha/CaptchaValidatorAttribute.cs b/src/Presentation/SmartStore.Web.Framework/UI/Captcha/CaptchaValidatorAttribute.cs index 94459d3067..050b694202 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Captcha/CaptchaValidatorAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Captcha/CaptchaValidatorAttribute.cs @@ -1,44 +1,82 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Web; using System.Web.Mvc; -using SmartStore.Core.Infrastructure; +using SmartStore.Core.Logging; +using SmartStore.Services.Localization; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI.Captcha { - public class CaptchaValidatorAttribute : ActionFilterAttribute + public class CaptchaValidatorAttribute : ActionFilterAttribute { - private const string CHALLENGE_FIELD_KEY = "recaptcha_challenge_field"; - private const string RESPONSE_FIELD_KEY = "recaptcha_response_field"; - public Lazy CaptchaSettings { get; set; } + public Lazy Logger { get; set; } + public Lazy LocalizationService { get; set; } - public override void OnActionExecuting(ActionExecutingContext filterContext) + public override void OnActionExecuting(ActionExecutingContext filterContext) { - bool valid = false; - var captchaChallengeValue = filterContext.HttpContext.Request.Form[CHALLENGE_FIELD_KEY]; - var captchaResponseValue = filterContext.HttpContext.Request.Form[RESPONSE_FIELD_KEY]; - if (!string.IsNullOrEmpty(captchaChallengeValue) && !string.IsNullOrEmpty(captchaResponseValue)) - { + var valid = false; + + try + { var captchaSettings = CaptchaSettings.Value; - if (captchaSettings.Enabled) - { - //validate captcha - var captchaValidtor = new Recaptcha.RecaptchaValidator - { - PrivateKey = captchaSettings.ReCaptchaPrivateKey, - RemoteIP = filterContext.HttpContext.Request.UserHostAddress, - Challenge = captchaChallengeValue, - Response = captchaResponseValue - }; - - var recaptchaResponse = captchaValidtor.Validate(); - valid = recaptchaResponse.IsValid; - } - } - - //this will push the result value into a parameter in our Action - filterContext.ActionParameters["captchaValid"] = valid; + var verifyUrl = CommonHelper.GetAppSetting("g:RecaptchaVerifyUrl"); + var recaptchaResponse = filterContext.HttpContext.Request.Form["g-recaptcha-response"]; + + var url = "{0}?secret={1}&response={2}".FormatInvariant( + verifyUrl, + HttpUtility.UrlEncode(captchaSettings.ReCaptchaPrivateKey), + HttpUtility.UrlEncode(recaptchaResponse) + ); + + using (var client = new WebClient()) + { + var jsonResponse = client.DownloadString(url); + using (var memoryStream = new MemoryStream(Encoding.Unicode.GetBytes(jsonResponse))) + { + var serializer = new DataContractJsonSerializer(typeof(GoogleRecaptchaApiResponse)); + var result = serializer.ReadObject(memoryStream) as GoogleRecaptchaApiResponse; + + if (result == null) + { + Logger.Value.Error(LocalizationService.Value.GetResource("Common.CaptchaUnableToVerify")); + } + else + { + if (result.ErrorCodes == null) + { + valid = result.Success; + } + } + } + } + } + catch (Exception exception) + { + Logger.Value.ErrorsAll(exception); + } + + // this will push the result value into a parameter in our Action + filterContext.ActionParameters["captchaValid"] = valid; base.OnActionExecuting(filterContext); } } + + + [DataContract] + public class GoogleRecaptchaApiResponse + { + [DataMember(Name = "success")] + public bool Success { get; set; } + + [DataMember(Name = "error-codes")] + public List ErrorCodes { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Captcha/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Captcha/HtmlExtensions.cs index 4fc9e585bc..77d7b1cb7a 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Captcha/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Captcha/HtmlExtensions.cs @@ -1,28 +1,30 @@ -using System.IO; +using System.Text; using System.Web.Mvc; -using System.Web.UI; using SmartStore.Core.Infrastructure; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI.Captcha { - public static class HtmlExtensions + public static class HtmlExtensions { public static string GenerateCaptcha(this HtmlHelper helper) { + var sb = new StringBuilder(); var captchaSettings = EngineContext.Current.Resolve(); - var captchaControl = new Recaptcha.RecaptchaControl - { - ID = "recaptcha", - Theme = "blackglass", - PublicKey = captchaSettings.ReCaptchaPublicKey, - PrivateKey = captchaSettings.ReCaptchaPrivateKey - }; + var widgetUrl = CommonHelper.GetAppSetting("g:RecaptchaWidgetUrl"); + var elementId = "GoogleRecaptchaWidget"; - var htmlWriter = new HtmlTextWriter(new StringWriter()); + sb.AppendLine(""); + sb.AppendLine("
".FormatInvariant(elementId)); + sb.AppendLine("".FormatInvariant(widgetUrl)); - captchaControl.RenderControl(htmlWriter); - - return htmlWriter.InnerWriter.ToString(); + return sb.ToString(); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs index eb76938217..1ca2fb9a77 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs @@ -67,6 +67,7 @@ private set private void EnrichRenderer(ComponentRenderer renderer) { renderer.Component = this.Component; + renderer.HtmlHelper = this.HtmlHelper; renderer.ViewContext = this.HtmlHelper.ViewContext; renderer.ViewData = this.HtmlHelper.ViewData; } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs index 388a76111c..b16b8e703c 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs @@ -28,6 +28,12 @@ protected internal TComponent Component set; } + protected internal HtmlHelper HtmlHelper + { + get; + internal set; + } + protected internal ViewContext ViewContext { get; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs index 7b86cad330..a6dca6e4eb 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs @@ -159,7 +159,6 @@ public override string ToString() public abstract class NavigationItemWithContent : NavigationItem, IContentContainer, IHideObjectMembers { - public NavigationItemWithContent() { this.ContentHtmlAttributes = new RouteValueDictionary(); @@ -170,7 +169,6 @@ public NavigationItemWithContent() public IDictionary ContentHtmlAttributes { get; private set; } public HelperResult Content { get; set; } - } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs index 0d8823ff1c..a0b6ab86ea 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Web.Mvc; +using System.Web.Mvc.Html; using System.Web.Routing; using System.Web.WebPages; using SmartStore.Core; @@ -185,9 +186,18 @@ public abstract class NavigationItemtWithContentBuilder : Navig where TBuilder : NavigationItemtWithContentBuilder { - public NavigationItemtWithContentBuilder(TItem item) + public NavigationItemtWithContentBuilder(TItem item, HtmlHelper htmlHelper) : base(item) { + Guard.ArgumentNotNull(() => htmlHelper); + + HtmlHelper = htmlHelper; + } + + protected HtmlHelper HtmlHelper + { + get; + private set; } /// @@ -226,6 +236,34 @@ public TBuilder Content(HelperResult value) return (this as TBuilder); } + /// + /// Renders child action as content + /// + /// Action name + /// Controller name + /// Route values + /// builder instance + public TBuilder Content(string action, string controller, object routeValues) + { + return Content(action, controller, new RouteValueDictionary(routeValues)); + } + + /// + /// Renders child action as content + /// + /// Action name + /// Controller name + /// Route values + /// builder instance + public TBuilder Content(string action, string controller, RouteValueDictionary routeValues) + { + return this.Content(x => new HelperResult(writer => + { + var value = this.HtmlHelper.Action(action, controller, routeValues); + writer.Write(value); + })); + } + public TBuilder ContentHtmlAttributes(object attributes) { return this.ContentHtmlAttributes(CommonHelper.ObjectToDictionary(attributes)); diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs index 55fe80ab38..513639d3f2 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabBuilder.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Mvc; -using SmartStore.Utilities; -using System.Web.WebPages; +using System.Web.Mvc; namespace SmartStore.Web.Framework.UI { @@ -12,8 +6,8 @@ namespace SmartStore.Web.Framework.UI public class TabBuilder : NavigationItemtWithContentBuilder { - public TabBuilder(Tab item) - : base(item) + public TabBuilder(Tab item, HtmlHelper htmlHelper) + : base(item, htmlHelper) { } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs index a1d0f65af0..99f631bcc0 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabFactory.cs @@ -5,25 +5,25 @@ using System.Web.Mvc; namespace SmartStore.Web.Framework.UI -{ - +{ public class TabFactory : IHideObjectMembers { private readonly IList _items; + private readonly HtmlHelper _htmlHelper; - public TabFactory(IList items) + public TabFactory(IList items, HtmlHelper htmlHelper) { + Guard.ArgumentNotNull(() => htmlHelper); + _items = items; + _htmlHelper = htmlHelper; } public virtual TabBuilder Add() { var item = new Tab(); _items.Add(item); - return new TabBuilder(item); + return new TabBuilder(item, _htmlHelper); } - - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs index c744510dae..bcc330ce39 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs @@ -30,7 +30,7 @@ public TabStrip() this.SmartTabSelection = true; } - public IList Items + public List Items { get; private set; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs index 1442c6fcea..14b7458c83 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs @@ -1,15 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Mvc; namespace SmartStore.Web.Framework.UI -{ - +{ public class TabStripBuilder : ComponentBuilder { - public TabStripBuilder(TabStrip Component, HtmlHelper htmlHelper) : base(Component, htmlHelper) { @@ -17,7 +12,7 @@ public TabStripBuilder(TabStrip Component, HtmlHelper htmlHelper) public TabStripBuilder Items(Action addAction) { - TabFactory factory = new TabFactory(base.Component.Items); + var factory = new TabFactory(base.Component.Items, this.HtmlHelper); addAction(factory); return this; } @@ -75,7 +70,5 @@ public TabStripBuilder OnAjaxComplete(string value) base.Component.OnAjaxComplete = value; return this; } - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs index 7168949f97..4c235ab3c1 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs @@ -5,15 +5,14 @@ using System.Text; using System.Web.UI; using System.Web.Mvc; +using System.Web.Mvc.Html; using System.Web.Routing; using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Framework.UI -{ - +{ public class TabStripRenderer : ComponentRenderer { - public TabStripRenderer() { } @@ -22,7 +21,10 @@ public TabStripRenderer() protected override void WriteHtmlCore(HtmlTextWriter writer) { var tab = base.Component; - var hasContent = tab.Items.Any(x => x.Content != null || x.Ajax); + + MoveSpecialTabToEnd(tab.Items); + + var hasContent = tab.Items.Any(x => x.Content != null || x.Ajax); var isTabbable = tab.Position != TabsPosition.Top; var urlHelper = new UrlHelper(this.ViewContext.RequestContext); @@ -129,8 +131,19 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) } } - // returns a query selector - private string TrySelectRememberedTab() + private void MoveSpecialTabToEnd(List tabs) + { + var idx = tabs.FindIndex(x => x.Name == "tab-special-plugin-widgets"); + if (idx > -1 && idx < (tabs.Count - 1)) + { + var tab = tabs[idx]; + tabs.RemoveAt(idx); + tabs.Add(tab); + } + } + + // returns a query selector + private string TrySelectRememberedTab() { var tab = this.Component; @@ -358,7 +371,7 @@ protected virtual void RenderItemContent(HtmlTextWriter writer, Tab item, int in writer.AddAttribute("id", BuildItemId(item, index)); writer.RenderBeginTag("div"); { - if (item.Content != null) + if (item.Content != null) { writer.WriteLine(item.Content.ToHtmlString()); } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/IPageAssetsBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/IPageAssetsBuilder.cs index 94aced15ef..55b81e8353 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/IPageAssetsBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/IPageAssetsBuilder.cs @@ -17,7 +17,7 @@ public partial interface IPageAssetsBuilder void AddCssFileParts(ResourceLocation location, IEnumerable parts, bool excludeFromBundling = false, bool append = false); void AddLinkPart(string rel, string href, RouteValueDictionary htmlAttributes); - string GenerateTitle(bool addDefaultTitle); + string GenerateTitle(bool addDefaultTitle); string GenerateMetaDescription(); string GenerateMetaKeywords(); string GenerateCanonicalUrls(); @@ -26,7 +26,8 @@ public partial interface IPageAssetsBuilder string GenerateScripts(UrlHelper urlHelper, ResourceLocation location, bool? enableBundling = null); string GenerateCssFiles(UrlHelper urlHelper, ResourceLocation location, bool? enableBundling = null); string GenerateLinkRels(); - } + string GenerateMetaRobots(); + } public static class PageAssetsBuilderExtensions { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/LayoutExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/LayoutExtensions.cs index 3995a230ef..0b9e447a4f 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/LayoutExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/LayoutExtensions.cs @@ -64,11 +64,20 @@ public static MvcHtmlString SmartMetaKeywords(this HtmlHelper html, params strin html.AppendMetaKeywordParts(parts); return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateMetaKeywords())); } - #endregion + #endregion + + #region MetaMisc + + public static MvcHtmlString SmartMetaRobots(this HtmlHelper html) + { + var pageAssetsBuilder = EngineContext.Current.Resolve(); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateMetaRobots()); + } + #endregion - #region ScriptParts - public static void AddScriptParts(this HtmlHelper html, params string[] parts) + #region ScriptParts + public static void AddScriptParts(this HtmlHelper html, params string[] parts) { AddScriptParts(html, ResourceLocation.Foot, false, parts); } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/PageAssetsBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/PageAssetsBuilder.cs index 4256ce6aea..616362cf84 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/PageAssetsBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/PageAssetsBuilder.cs @@ -406,7 +406,16 @@ public string GenerateLinkRels() return sb.ToString(); } - public class WebAssetDescriptor + public string GenerateMetaRobots() + { + if (_seoSettings.MetaRobotsContent.HasValue()) + { + return "".FormatInvariant(_seoSettings.MetaRobotsContent); + } + return null; + } + + public class WebAssetDescriptor { public bool ExcludeFromBundling { get; set; } public string Part { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs index 6f8c74b211..7d349f7e4a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs @@ -16,10 +16,13 @@ public void Validate(T model, ModelStateDictionary modelState) if (!result.IsValid) { - int i = 0; foreach (var error in result.Errors) { - modelState.AddModelError(error.PropertyName + (++i).ToString(), error.ErrorMessage); + try + { + modelState.AddModelError(error.PropertyName, error.ErrorMessage); + } + catch { } } } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs index d68ad50cef..f0b5de4270 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Caching/WebApiCachingControllingData.cs @@ -39,6 +39,7 @@ public static WebApiControllingCacheData Data() { ValidMinutePeriod = settings.ValidMinutePeriod, NoRequestTimestampValidation = settings.NoRequestTimestampValidation, + AllowEmptyMd5Hash = settings.AllowEmptyMd5Hash, LogUnauthorized = settings.LogUnauthorized, ApiUnavailable = (plugin == null || !plugin.Installed), PluginVersion = (plugin == null ? "1.0" : plugin.Version.ToString()) @@ -57,6 +58,7 @@ public partial class WebApiControllingCacheData public bool ApiUnavailable { get; set; } public int ValidMinutePeriod { get; set; } public bool NoRequestTimestampValidation { get; set; } + public bool AllowEmptyMd5Hash { get; set; } public bool LogUnauthorized { get; set; } public string PluginVersion { get; set; } @@ -64,7 +66,7 @@ public string Version { get { - return "{0} {1}".FormatWith(WebApiGlobal.MaxApiVersion, PluginVersion); + return "{0} {1}".FormatInvariant(WebApiGlobal.MaxApiVersion, PluginVersion); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs index 3a0e10857e..db9be1cdb6 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs @@ -167,7 +167,21 @@ protected virtual HmacResult IsAuthenticated(HttpActionContext actionContext, Da string signatureProvider = _hmac.CreateSignature(apiUser.SecretKey, messageRepresentation); if (signatureProvider != signatureConsumer) - return HmacResult.InvalidSignature; + { + if (cacheControllingData.AllowEmptyMd5Hash) + { + messageRepresentation = _hmac.CreateMessageRepresentation(context, null, headTimestamp); + + signatureProvider = _hmac.CreateSignature(apiUser.SecretKey, messageRepresentation); + + if (signatureProvider != signatureConsumer) + return HmacResult.InvalidSignature; + } + else + { + return HmacResult.InvalidSignature; + } + } customer = GetCustomer(apiUser.CustomerId); if (customer == null) diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs index 993c7624e9..425e19a5fa 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs @@ -10,10 +10,11 @@ public static class WebApiGlobal public static int DefaultTimePeriodMinutes { get { return 15; } } public static string RouteNameDefaultApi { get { return "WebApi.Default"; } } public static string RouteNameDefaultOdata { get { return "WebApi.OData.Default"; } } + public static string RouteNameUploads { get { return "WebApi.Uploads"; } } public static string MostRecentOdataPath { get { return "odata/v1"; } } public static string PluginSystemName { get { return "SmartStore.WebApi"; } } - /// see http://tools.ietf.org/html/rfc6648 + /// public static class Header { private static string Prefix { get { return "SmartStore-Net-Api-"; } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs index 2d7a98ecda..000f1549ea 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Data.Entity; using System.Data.Entity.Core.Metadata.Edm; using System.Data.Entity.Infrastructure; using System.Linq; @@ -8,15 +10,13 @@ using System.Web.Http; using System.Web.Http.OData; using System.Web.Http.OData.Routing; -using System.Data.Entity; +using Autofac; +using SmartStore.ComponentModel; using SmartStore.Core; using SmartStore.Core.Data; +using SmartStore.Services; using SmartStore.Services.Directory; using SmartStore.Services.Localization; -using System.Collections.Generic; -using SmartStore.Services; -using Autofac; -using SmartStore.ComponentModel; namespace SmartStore.Web.Framework.WebApi { @@ -338,7 +338,20 @@ protected internal virtual SingleResult GetRelatedEntity( public override HttpResponseMessage Post(TEntity entity) { - return Request.CreateResponse(HttpStatusCode.OK, CreateEntity(entity)); + var response = Request.CreateResponse(HttpStatusCode.OK, CreateEntity(entity)); + + try + { + var entityUrl = Url.ODataLink( + new EntitySetPathSegment(entity.GetType().Name.EnsureEndsWith("s")), + new KeyValuePathSegment(entity.Id.ToString()) + ); + + response.Headers.Location = new Uri(entityUrl); + } + catch { } + + return response; } protected override TEntity CreateEntity(TEntity entity) diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs index 1f2207d94f..392e14d13f 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiSettings.cs @@ -7,5 +7,6 @@ public class WebApiSettings : ISettings public int ValidMinutePeriod { get; set; } public bool LogUnauthorized { get; set; } public bool NoRequestTimestampValidation { get; set; } + public bool AllowEmptyMd5Hash { get; set; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiStartupTask.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiStartupTask.cs index 0cb709e658..680844f150 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiStartupTask.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiStartupTask.cs @@ -10,8 +10,8 @@ using SmartStore.Web.Framework.WebApi.OData; namespace SmartStore.Web.Framework.WebApi -{ - public class WebApiStartupTask : IStartupTask +{ + public class WebApiStartupTask : IStartupTask { public void Execute() @@ -52,6 +52,12 @@ public void Execute() try { + if (!config.Routes.ContainsKey(WebApiGlobal.RouteNameUploads)) + { + config.Routes.MapHttpRoute(WebApiGlobal.RouteNameUploads, "api/{version}/Uploads/{action}/{id}", + new { version = "v1", controller = "Uploads", action = "Index", id = RouteParameter.Optional }); + } + if (!config.Routes.ContainsKey(WebApiGlobal.RouteNameDefaultApi)) { config.Routes.MapHttpRoute(WebApiGlobal.RouteNameDefaultApi, "api/{version}/{controller}/{id}", diff --git a/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs b/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs index 298f30bc62..5651615faf 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net; using System.Web; using SmartStore.Core; using SmartStore.Core.Caching; @@ -24,9 +25,7 @@ namespace SmartStore.Web.Framework /// public partial class WebWorkContext : IWorkContext { - private const string CustomerCookieName = "smartstore.customer"; - - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; private readonly ICustomerService _customerService; private readonly IStoreContext _storeContext; private readonly IAuthenticationService _authenticationService; @@ -76,44 +75,6 @@ public WebWorkContext(Func cacheManager, this._userAgent = userAgent; } - protected HttpCookie GetCustomerCookie() - { - if (_httpContext == null || _httpContext.Request == null) - return null; - - return _httpContext.Request.Cookies[CustomerCookieName]; - } - - protected void SetCustomerCookie(Guid customerGuid) - { - if (_httpContext != null && _httpContext.Response != null) - { - var cookie = new HttpCookie(CustomerCookieName); - cookie.HttpOnly = true; - cookie.Value = customerGuid.ToString(); - if (customerGuid == Guid.Empty) - { - cookie.Expires = DateTime.Now.AddMonths(-1); - } - else - { - int cookieExpires = 24 * 365; //TODO make configurable - cookie.Expires = DateTime.Now.AddHours(cookieExpires); - } - - try - { - if (_httpContext.Response.Cookies[CustomerCookieName] != null) - _httpContext.Response.Cookies.Remove(CustomerCookieName); - } - catch (Exception) { } - - _httpContext.Response.Cookies.Add(cookie); - } - } - - //public Lazy TaxService { get; set; } - /// /// Gets or sets the current customer /// @@ -126,38 +87,16 @@ public Customer CurrentCustomer Customer customer = null; - // check whether request is made by a background task - // in this case return built-in customer record for background task - if (_httpContext == null || _httpContext.IsFakeContext()) - { - customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); - } - - // check whether request is made by a search engine - // in this case return built-in customer record for search engines - if (customer == null || customer.Deleted || !customer.Active) - { - if (_userAgent.IsBot) - { - customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); - } - } - - // check whether request is made by the PDF converter - // in this case return built-in customer record for the converter - if (customer == null || customer.Deleted || !customer.Active) + // Is system account? + if (TryGetSystemAccount(out customer)) { - if (_userAgent.IsPdfConverter) - { - customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.PdfConverter); - } + // Get out quickly. Bots tend to overstress the shop. + _cachedCustomer = customer; + return customer; } - // registered user? - if (customer == null || customer.Deleted || !customer.Active) - { - customer = _authenticationService.GetAuthenticatedCustomer(); - } + // registered user? + customer = _authenticationService.GetAuthenticatedCustomer(); // impersonate user if required (currently used for 'phone order' support) if (customer != null && !customer.Deleted && customer.Active) @@ -168,59 +107,93 @@ public Customer CurrentCustomer var impersonatedCustomer = _customerService.GetCustomerById(impersonatedCustomerId.Value); if (impersonatedCustomer != null && !impersonatedCustomer.Deleted && impersonatedCustomer.Active) { - //set impersonated customer + // set impersonated customer _originalCustomerIfImpersonated = customer; customer = impersonatedCustomer; } } } - // load guest customer + // Load guest customer if (customer == null || customer.Deleted || !customer.Active) { - var customerCookie = GetCustomerCookie(); - if (customerCookie != null && !String.IsNullOrEmpty(customerCookie.Value)) - { - Guid customerGuid; - if (Guid.TryParse(customerCookie.Value, out customerGuid)) - { - var customerByCookie = _customerService.GetCustomerByGuid(customerGuid); - if (customerByCookie != null && !customerByCookie.IsRegistered()) - customer = customerByCookie; - } - } + customer = GetGuestCustomer(); } - // create guest if not exists - if (customer == null || customer.Deleted || !customer.Active) - { - customer = _customerService.InsertGuestCustomer(); - } - - - // validation - if (!customer.Deleted && customer.Active) - { - SetCustomerCookie(customer.CustomerGuid); - _cachedCustomer = customer; - } - - return _cachedCustomer; + _cachedCustomer = customer; + return _cachedCustomer; } set { - if (!value.IsSystemAccount) - { - SetCustomerCookie(value.CustomerGuid); - } _cachedCustomer = value; } } - /// - /// Gets or sets the original customer (in case the current one is impersonated) - /// - public Customer OriginalCustomerIfImpersonated + protected bool TryGetSystemAccount(out Customer customer) + { + // Never check whether customer is deleted/inactive in this method. + // System accounts should neither be deletable nor activatable, they are mandatory. + + customer = null; + + // check whether request is made by a background task + // in this case return built-in customer record for background task + if (_httpContext == null || _httpContext.IsFakeContext()) + { + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); + } + + // check whether request is made by a search engine + // in this case return built-in customer record for search engines + if (customer == null && _userAgent.IsBot) + { + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.SearchEngine); + } + + // check whether request is made by the PDF converter + // in this case return built-in customer record for the converter + if (customer == null && _userAgent.IsPdfConverter) + { + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.PdfConverter); + } + + return customer != null; + } + + protected virtual Customer GetGuestCustomer() + { + Customer customer = null; + Guid customerGuid = Guid.Empty; + + var anonymousId = _httpContext.Request.AnonymousID; + + if (anonymousId != null && anonymousId.HasValue()) + { + Guid.TryParse(anonymousId, out customerGuid); + } + + if (customerGuid == Guid.Empty) + { + _httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + _httpContext.Response.End(); + } + + // Try to load an existing record... + customer = _customerService.GetCustomerByGuid(customerGuid); + + if (customer == null || customer.Deleted || !customer.Active || customer.IsRegistered()) + { + // ...but no record yet. Create one. + customer = _customerService.InsertGuestCustomer(customerGuid); + } + + return customer; + } + + /// + /// Gets or sets the original customer (in case the current one is impersonated) + /// + public Customer OriginalCustomerIfImpersonated { get { @@ -243,11 +216,8 @@ public Language WorkingLanguage if (this.CurrentCustomer != null) { - customerLangId = this.CurrentCustomer.GetAttribute( - SystemCustomerAttributeNames.LanguageId, - _attrService, - _storeContext.CurrentStore.Id); - } + customerLangId = this.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.LanguageId, _attrService, storeId); + } #region Get language from URL (if possible) @@ -331,12 +301,8 @@ public Language WorkingLanguage private void SetCustomerLanguage(int languageId, int storeId) { - _attrService.SaveAttribute( - this.CurrentCustomer, - SystemCustomerAttributeNames.LanguageId, - languageId, - storeId); - } + _attrService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.LanguageId, languageId, storeId); + } /// /// Get or set current user working currency @@ -365,8 +331,8 @@ public Currency WorkingCurrency if (customer != null && !customer.IsSearchEngineAccount()) { // search engines should always crawl by primary store currency - var customerCurrencyId = customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, _attrService, _storeContext.CurrentStore.Id); - if (customerCurrencyId.GetValueOrDefault() > 0) + var customerCurrencyId = customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, _attrService, _storeContext.CurrentStore.Id); + if (customerCurrencyId.GetValueOrDefault() > 0) { if (storeCurrenciesMap.TryGetValue(customerCurrencyId.Value, out currency)) { @@ -428,7 +394,7 @@ public Currency WorkingCurrency { int? id = value != null ? value.Id : (int?)null; _attrService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.CurrencyId, id, _storeContext.CurrentStore.Id); - _cachedCurrency = null; + _cachedCurrency = null; } } @@ -455,10 +421,8 @@ public TaxDisplayType TaxDisplayType if (!_taxSettings.AllowCustomersToSelectTaxDisplayType) return; - _attrService.SaveAttribute(this.CurrentCustomer, - SystemCustomerAttributeNames.TaxDisplayTypeId, - (int)value, _storeContext.CurrentStore.Id); - } + _attrService.SaveAttribute(this.CurrentCustomer, SystemCustomerAttributeNames.TaxDisplayTypeId, (int)value, _storeContext.CurrentStore.Id); + } } public TaxDisplayType GetTaxDisplayTypeFor(Customer customer, int storeId) @@ -472,8 +436,9 @@ public TaxDisplayType GetTaxDisplayTypeFor(Customer customer, int storeId) if (_taxSettings.AllowCustomersToSelectTaxDisplayType && customer != null) { - taxDisplayType = customer.GetAttribute(SystemCustomerAttributeNames.TaxDisplayTypeId, storeId); - } + taxDisplayType = customer.GetAttribute(SystemCustomerAttributeNames.TaxDisplayTypeId, _attrService, storeId); + + } if (!taxDisplayType.HasValue && _taxSettings.EuVatEnabled) { @@ -538,6 +503,5 @@ public string GetDefaultLanguageSeoCode(int storeId = 0) { return _languageService.GetDefaultLanguageSeoCode(storeId); } - } } diff --git a/src/Presentation/SmartStore.Web.Framework/app.config b/src/Presentation/SmartStore.Web.Framework/app.config index 5d4773d412..751632b38f 100644 --- a/src/Presentation/SmartStore.Web.Framework/app.config +++ b/src/Presentation/SmartStore.Web.Framework/app.config @@ -24,7 +24,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/packages.config b/src/Presentation/SmartStore.Web.Framework/packages.config index d105ec9f25..cbc00e31fa 100644 --- a/src/Presentation/SmartStore.Web.Framework/packages.config +++ b/src/Presentation/SmartStore.Web.Framework/packages.config @@ -24,8 +24,7 @@ - - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/500.html b/src/Presentation/SmartStore.Web/500.html index f9eaa260f1..741ebbcab5 100644 --- a/src/Presentation/SmartStore.Web/500.html +++ b/src/Presentation/SmartStore.Web/500.html @@ -41,7 +41,7 @@

- We apologize, an error ocurred while handling your request, this is not a problem with your computer or internet connection. + We apologize, an error occurred while handling your request, this is not a problem with your computer or internet connection. The details have been sent to our support team and we will investigate the issue very soon.

diff --git a/src/Presentation/SmartStore.Web/Administration/Content/admin.less b/src/Presentation/SmartStore.Web/Administration/Content/admin.less index 620ce8206f..5483f5971f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/admin.less +++ b/src/Presentation/SmartStore.Web/Administration/Content/admin.less @@ -42,6 +42,11 @@ a:hover { background-color: @dropdownLinkBackgroundHover; } +.alert { + padding-top: 14px; + padding-bottom: 14px; +} + /* Page -------------------------------------------------------------- */ @@ -266,16 +271,6 @@ header { /* SM additions -------------------------------------------------------------- */ -.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; -} - #ajax-busy { position: fixed; opacity: 0; @@ -395,26 +390,33 @@ td.adminData > input[type="checkbox"] { .admin-config-group { .title, - .small-title { + .small-title, + .head { font-family: @headingsFontFamily; font-weight: @headingsFontWeight; color: inherit; border-bottom: 1px solid #ddd; } - .title { + .sub-title { + font-family: @baseFontFamily; + font-size: @baseFontSize; + font-weight: normal; + } + .title, + .head { font-size: @fontSizeLarge; + padding: 12px 0; + } + .head { + margin-bottom: 12px; + } + .title { margin: 12px 0; - padding: 8px 0; } .small-title { margin: 2px 0; padding: 2px 0; } - .sub-title { - font-family: @baseFontFamily; - font-size: @baseFontSize; - font-weight: normal; - } } @@ -425,6 +427,21 @@ td.adminData > input[type="checkbox"] { min-width: 314px; /* ensures more clean look & feel */ } +.select2-image-item { + img { + margin-bottom: 3px; + } + span:not(.select2-icon) { + padding-left: 5px; + } + .icon-container { + float: left; + display: inline-block; + width: 16px; + min-width: 16px; + max-width: 16px; + } +} /* select2 with large, templated items -------------------------------------------------------------- */ @@ -582,11 +599,6 @@ body .t-content.t-state-active { padding: 5px; } -.modal-large { - width: 800px !important; - margin: -250px 0 0 -400px !important; -} - /* HTML editor (tiny mce) @@ -809,6 +821,16 @@ td.adminTitle > .ctl-label { } } } +tr.adminGroup { + > td { + padding-top: 18px; + } + + label { + padding-bottom: 3px; + text-decoration: underline; + } +} td.adminTitle:hover a.hint { .opacity(100); @@ -850,13 +872,14 @@ td.adminData .t-combobox { width: 306px; } -tr.adminSeparator hr { - background-color: #D3D3D3; - border: medium none; - color: #D3D3D3; +tr.adminSeparator hr, +td.adminSeparator hr { + border: medium none; + background-color: #ddd; + color: #ddd; height: 1px; - margin-top: 10px; - margin-bottom: 10px; + margin-top: 12px; + margin-bottom: 12px; padding: 0; } @@ -879,15 +902,25 @@ tr.adminSeparator hr { } .admin-table { + thead th { + text-transform: uppercase; + } + th, td { + padding: 12px; + } .disabled { .muted(); } - .centered { - text-align: center; - } .progress-info { min-width: 260px; } + select, input { + margin-bottom: 0; + } + .control-large { + width: 500px; + min-width: 500px; + } } /* SERVER CONTROLS */ @@ -1508,20 +1541,6 @@ table.payment-method-features td { text-decoration: none; } -#product-edit h3, -.caption-container h3 { - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; - font-size: 18px; - line-height: 36px; - color: #333; -} -#product-edit h3.bordered, -.caption-container h3.bordered { - border: 0; - border-bottom: 1px solid #e5e5e5; -} - /* Download editor -------------------------------------------------------------- */ @@ -1559,3 +1578,90 @@ table.payment-method-features td { display: inline-block; margin-left: 10px; } + + + + +/* Data exchange +-------------------------------------------------------------- */ +.profile-list { + margin-bottom: 10px; + border-bottom: 1px solid #ddd; +} + +.info-profile td { + padding-top: 20px; +} + +/* Import column mapping +-------------------------------------------------------------- */ +@media (max-width: 1280px) { + #ImportColumnMappings .right-label, + .column-mapping .mapping-button-label, + .mapping-list-item .right-label { + display: none; + } +} + +.column-mapping .right-label, +.mapping-list-item .right-label { + float: right; + font-size: 0.9em; +} +.mapping-list-item { + .ellipsis(); +} + +.column-mapping { + .column-one, .column-two { + width: 42%; + } + + .select-column, .select-property, .input-default { + width: 98%; + } + + th, td, .mapping-delete, .mapping-add, .mapping-apply, .mapping-cancel { + white-space: nowrap; + padding-left: 8px; + padding-right: 8px; + } + + .select2-container .select2-choice span { + margin-right: 12px; + white-space: nowrap; + } + + .mapping-edit { + white-space: nowrap; + + .select-column, .select-property { + width: 84%; + } + .select2-container { + float: left; + display: inline-block; + min-width: 220px; + margin-right: 4px; + } + } + + .mapping-item { + .item-inner { + white-space: nowrap; + cursor: pointer; + padding: 7px 0; + + .property-icon { + display: inline-block; + margin-right: 0; + } + .left-label { + display: inline-block; + } + .right-label { + margin-right: 20px; + } + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/LICENSE.txt b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/LICENSE.txt similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/LICENSE.txt rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/LICENSE.txt diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json new file mode 100644 index 0000000000..c08220bdfe --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json @@ -0,0 +1,33 @@ +{ + "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": "../../../Admin/RoxyFileManager/ProcessRequest?a=DIRLIST", + "CREATEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=CREATEDIR", + "DELETEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=DELETEDIR", + "MOVEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=MOVEDIR", + "COPYDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=COPYDIR", + "RENAMEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=RENAMEDIR", + "FILESLIST": "../../../Admin/RoxyFileManager/ProcessRequest?a=FILESLIST", + "UPLOAD": "../../../Admin/RoxyFileManager/ProcessRequest?a=UPLOAD", + "DOWNLOAD": "../../../Admin/RoxyFileManager/ProcessRequest?a=DOWNLOAD", + "DOWNLOADDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=DOWNLOADDIR", + "DELETEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=DELETEFILE", + "MOVEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=MOVEFILE", + "COPYFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=COPYFILE", + "RENAMEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=RENAMEFILE", + "GENERATETHUMB": "../../../Admin/RoxyFileManager/ProcessRequest?a=GENERATETHUMB", + "DEFAULTVIEW": "list", + "FORBIDDEN_UPLOADS": "zip js jsp jsb mhtml mht xhtml xht php phtml php3 php4 php5 phps shtml jhtml pl sh py cgi exe scr dll msi vbs bat com pif cmd vxd cpl htpasswd htaccess", + "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/filemanager/css/images/animated-overlay.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/animated-overlay.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/animated-overlay.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/animated-overlay.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_222222_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_222222_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_454545_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_454545_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_888888_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_888888_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/main.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/main.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/css/main.min.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/css/main.min.css rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-paste.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-paste.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-rename.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/action-folder-rename.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_down.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_down.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_up.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/arrow_up.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/blank.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/blank.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/blank.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/blank.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/copy.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/copy.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/cut.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/cut.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/dir-minus.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/dir-minus.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/dir-plus.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/dir-plus.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-add.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-add.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-delete.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-delete.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-download.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-download.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-duplicate.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-duplicate.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/file-upload.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-upload.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/file-upload.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-upload.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_3gp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_3gp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_7z.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_7z.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ace.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ace.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ai.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ai.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aiff.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_aiff.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_amr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_amr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_asx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bin.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bin.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bup.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_bup.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cab.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cab.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cbr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cbr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cda.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cda.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdl.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdl.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_cdr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_chm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_chm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_divx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_divx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dll.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dll.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dmg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dmg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_doc.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_doc.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dvf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dvf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dwg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_dwg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eml.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eml.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_eps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_exe.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_exe.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_fla.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_fla.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_flv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_flv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_gz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_hqx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_hqx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_htm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_htm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_html.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_html.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ifo.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ifo.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_indd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_indd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_iso.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_iso.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_jpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_lnk.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_lnk.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_log.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_log.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4a.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4a.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4b.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4b.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4p.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4p.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4v.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_m4v.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mdb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mdb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mid.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mid.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mov.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mov.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp2.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp2.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp4.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mp4.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_msi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_msi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mswmm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_mswmm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ogg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ogg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pdf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pdf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_png.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_png.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_psd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_psd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pst.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pst.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ptb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ptb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pub.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_pub.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbw.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qbw.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qxd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_qxd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ram.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ram.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rmvb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rmvb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rtf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_rtf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sea.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sea.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ses.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ses.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sit.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sit.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sitx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_sitx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_swf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_swf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tgz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tgz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_thm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_thm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_tmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_torrent.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_torrent.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ttf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_ttf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_txt.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_txt.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vob.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_vob.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wav.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wav.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wma.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wma.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wmv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wmv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_wps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xls.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xls.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xpi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_xpi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_zip.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/file_extension_zip.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/unknown.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/big/unknown.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_3gp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_3gp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_7z.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_7z.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ace.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ace.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ai.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ai.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aiff.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_aiff.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_amr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_amr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_asx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bin.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bin.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bup.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_bup.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cab.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cab.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cbr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cbr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cda.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cda.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdl.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdl.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_cdr.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_chm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_chm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dat.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_divx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_divx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dll.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dll.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dmg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dmg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_doc.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_doc.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dvf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dvf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dwg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_dwg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eml.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eml.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_eps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_exe.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_exe.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_fla.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_fla.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_flv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_flv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_gz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_hqx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_hqx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_htm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_htm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_html.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_html.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ifo.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ifo.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_indd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_indd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_iso.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_iso.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_jpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_lnk.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_lnk.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_log.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_log.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4a.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4a.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4b.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4b.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4p.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4p.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4v.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_m4v.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mdb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mdb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mid.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mid.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mov.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mov.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp2.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp2.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp4.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mp4.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpeg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mpg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_msi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_msi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mswmm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_mswmm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ogg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ogg.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pdf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pdf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_png.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_png.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_psd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_psd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pst.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pst.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ptb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ptb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pub.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_pub.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbw.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qbw.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qxd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_qxd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ram.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ram.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rar.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rmvb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rmvb.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rtf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_rtf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sea.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sea.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ses.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ses.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sit.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sit.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sitx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_sitx.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ss.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_swf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_swf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tgz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tgz.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_thm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_thm.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tif.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_tmp.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_torrent.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_torrent.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ttf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_ttf.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_txt.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_txt.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vcd.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vob.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_vob.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wav.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wav.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wma.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wma.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wmv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wmv.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_wps.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xls.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xls.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xpi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_xpi.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_zip.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/file_extension_zip.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/unknown.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/filetypes/unknown.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/find.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/find.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-add.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-add.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-delete.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-delete.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-download.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-download.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-green.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-green.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-green.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-green.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder-opened.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-opened.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder-opened.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-opened.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/folder.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/folder.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/loading-dir.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading-dir.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/loading-dir.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading-dir.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/loading.gif b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading.gif similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/loading.gif rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/loading.gif diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/paste.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/paste.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/preview.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/preview.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/rename.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/rename.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/search.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/search.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/select.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/select.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/upload-big.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/upload-big.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/upload-big.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/upload-big.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/view-list.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/view-list.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/images/view-tile.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/images/view-tile.png rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/index.html b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/index.html rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/custom.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/custom.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/directory.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/directory.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/file.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/file.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/fileman-modified.zip b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/fileman-modified.zip similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/fileman-modified.zip rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/fileman-modified.zip diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/filetypes.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/filetypes.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-1.10.2.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-1.10.2.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-dateFormat.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-dateFormat.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-dateFormat.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-dateFormat.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-ui-1.10.4.custom.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/main.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/main.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/main.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.min.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/main.min.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/main.min.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/mini-main.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/mini-main.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/mini-main.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/mini-main.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/tiny_mce_popup.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/tiny_mce_popup.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/tiny_mce_popup.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/tiny_mce_popup.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/js/utils.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/js/utils.js rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/utils.js diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/bg.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/bg.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/bg.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/bg.json diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/de.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/de.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/de.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/de.json diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/en.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/en.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/en.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/en.json diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/lang/fr.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/fr.json similarity index 100% rename from src/Presentation/SmartStore.Web/Content/filemanager/lang/fr.json rename to src/Presentation/SmartStore.Web/Administration/Content/filemanager/lang/fr.json diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ajax-progress-2.gif b/src/Presentation/SmartStore.Web/Administration/Content/images/ajax-progress-2.gif deleted file mode 100644 index 2ed9a034bd..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ajax-progress-2.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-catalog-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-catalog-white.png deleted file mode 100644 index d12bfff9be..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-catalog-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-catalog.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-catalog.png deleted file mode 100644 index 5e3a11babf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-catalog.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-chevron.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-chevron.png deleted file mode 100644 index 4168ab3209..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-chevron.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-cms-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-cms-white.png deleted file mode 100644 index 5bfb96f67b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-cms-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-cms.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-cms.png deleted file mode 100644 index fe052b3879..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-cms.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-customers-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-customers-white.png deleted file mode 100644 index f2b8e2608c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-customers-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-customers.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-customers.png deleted file mode 100644 index faad9b156e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-customers.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-dashboard-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-dashboard-white.png deleted file mode 100644 index eee29a0dd6..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-dashboard-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-dashboard.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-dashboard.png deleted file mode 100644 index 3388b811aa..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-dashboard.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-default-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-default-white.png deleted file mode 100644 index ea4f248b44..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-default-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-default.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-default.png deleted file mode 100644 index 365847d12f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-default.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-help-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-help-white.png deleted file mode 100644 index d7fa0626c1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-help-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-help.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-help.png deleted file mode 100644 index 2acb084bc1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-help.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-marketing-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-marketing-white.png deleted file mode 100644 index 9d3ef59faf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-marketing-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-marketing.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-marketing.png deleted file mode 100644 index 82649f18dc..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-marketing.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-sales-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-sales-white.png deleted file mode 100644 index 25013b3977..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-sales-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-sales.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-sales.png deleted file mode 100644 index fb6ffda399..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-sales.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-settings-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-settings-white.png deleted file mode 100644 index 76e61cb9ed..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-settings-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-settings.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-settings.png deleted file mode 100644 index de9bbd1376..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-settings.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-system-white.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-system-white.png deleted file mode 100644 index 1fc35c2bc5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-system-white.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-system.png b/src/Presentation/SmartStore.Web/Administration/Content/images/ap-system.png deleted file mode 100644 index 204b19571d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/ap-system.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/images/preloader-32.gif b/src/Presentation/SmartStore.Web/Administration/Content/images/preloader-32.gif deleted file mode 100644 index f4144d0f19..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/images/preloader-32.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/theme.less b/src/Presentation/SmartStore.Web/Administration/Content/theme.less index 2d3a4fbece..a816128c79 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/theme.less +++ b/src/Presentation/SmartStore.Web/Administration/Content/theme.less @@ -64,6 +64,7 @@ // (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/Administration/Content/variables.less b/src/Presentation/SmartStore.Web/Administration/Content/variables.less index 1de0d20a5e..741c1de2bc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/variables.less +++ b/src/Presentation/SmartStore.Web/Administration/Content/variables.less @@ -71,7 +71,7 @@ // ------------------------- // Based on 14px font-size and 20px line-height -@fontSizeLarge: @baseFontSize * 1.25; // ~18px +@fontSizeLarge: @baseFontSize * 1.4; // ~18px @fontSizeSmall: @baseFontSize * 0.85; // ~12px @fontSizeMini: @baseFontSize * 0.75; // ~11px diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ActivityLogController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ActivityLogController.cs index 479b394285..e6c49b910e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ActivityLogController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ActivityLogController.cs @@ -5,6 +5,7 @@ using SmartStore.Admin.Models.Logging; using SmartStore.Core.Domain.Common; using SmartStore.Core.Logging; +using SmartStore.Services.Customers; using SmartStore.Services.Helpers; using SmartStore.Services.Security; using SmartStore.Web.Framework.Controllers; @@ -20,7 +21,8 @@ public partial class ActivityLogController : AdminControllerBase #region Fields private readonly ICustomerActivityService _customerActivityService; - private readonly IDateTimeHelper _dateTimeHelper; + private readonly ICustomerService _customerService; + private readonly IDateTimeHelper _dateTimeHelper; private readonly IPermissionService _permissionService; private readonly AdminAreaSettings _adminAreaSettings; @@ -30,11 +32,13 @@ public partial class ActivityLogController : AdminControllerBase public ActivityLogController( ICustomerActivityService customerActivityService, - IDateTimeHelper dateTimeHelper, + ICustomerService customerService, + IDateTimeHelper dateTimeHelper, IPermissionService permissionService, AdminAreaSettings adminAreaSettings) { this._customerActivityService = customerActivityService; + this._customerService = customerService; this._dateTimeHelper = dateTimeHelper; this._permissionService = permissionService; this._adminAreaSettings = adminAreaSettings; @@ -132,17 +136,34 @@ public JsonResult ListActivityLogs(GridCommand command, ActivityLogSearchModel m DateTime? endDateValue = (model.CreatedOnTo == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnTo.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); - var activityLog = _customerActivityService.GetAllActivities(startDateValue, endDateValue,null, model.ActivityLogTypeId, - command.Page - 1, command.PageSize, model.CustomerEmail); + var activityLog = _customerActivityService.GetAllActivities(startDateValue, endDateValue, null, model.ActivityLogTypeId, + command.Page - 1, command.PageSize, model.CustomerEmail, model.CustomerSystemAccount); - var gridModel = new GridModel + var systemAccountCustomers = _customerService.GetSystemAccountCustomers(); + + var gridModel = new GridModel { Data = activityLog.Select(x => { var m = x.ToModel(); + var systemCustomer = systemAccountCustomers.FirstOrDefault(y => y.Id == x.CustomerId); + m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + m.IsSystemAccount = (systemCustomer != null); + + if (systemCustomer != null) + { + if (systemCustomer.IsSearchEngineAccount()) + m.SystemAccountName = T("Admin.System.SystemCustomerNames.SearchEngine"); + else if (systemCustomer.IsBackgroundTaskAccount()) + m.SystemAccountName = T("Admin.System.SystemCustomerNames.BackgroundTask"); + else if (systemCustomer.IsPdfConverter()) + m.SystemAccountName = T("Admin.System.SystemCustomerNames.PdfConverter"); + else + m.SystemAccountName = "".NaIfEmpty(); + } + return m; - }), Total = activityLog.TotalCount }; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/AffiliateController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/AffiliateController.cs index e98628b496..cf1e2b3c87 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/AffiliateController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/AffiliateController.cs @@ -24,7 +24,7 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public partial class AffiliateController : AdminControllerBase { #region Fields @@ -156,23 +156,31 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageAffiliates)) - return AccessDeniedView(); + var model = new GridModel(); - var affiliates = _affiliateService.GetAllAffiliates(true); - var gridModel = new GridModel - { - Data = affiliates.PagedForCommand(command).Select(x => - { - var m = new AffiliateModel(); - PrepareAffiliateModel(m, x, false); - return m; - }), - Total = affiliates.Count, - }; - return new JsonResult + if (_permissionService.Authorize(StandardPermissionProvider.ManageAffiliates)) + { + var affiliates = _affiliateService.GetAllAffiliates(true); + + model.Data = affiliates.PagedForCommand(command).Select(x => + { + var m = new AffiliateModel(); + PrepareAffiliateModel(m, x, false); + return m; + }); + + model.Total = affiliates.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + + return new JsonResult { - Data = gridModel + Data = model }; } @@ -287,31 +295,34 @@ public ActionResult Delete(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult AffiliatedOrderList(int affiliateId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageAffiliates)) - return AccessDeniedView(); + var model = new GridModel(); - var affiliate = _affiliateService.GetAffiliateById(affiliateId); - if (affiliate == null) - throw new ArgumentException("No affiliate found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageAffiliates)) + { + var affiliate = _affiliateService.GetAffiliateById(affiliateId); + var orders = _orderService.GetAllOrders(affiliate.Id, command.Page - 1, command.PageSize); + + model.Data = orders.Select(order => + { + var orderModel = new AffiliateModel.AffiliatedOrderModel(); + orderModel.Id = order.Id; + orderModel.OrderStatus = order.OrderStatus.GetLocalizedEnum(_localizationService, _workContext); + orderModel.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_localizationService, _workContext); + orderModel.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_localizationService, _workContext); + orderModel.OrderTotal = _priceFormatter.FormatPrice(order.OrderTotal, true, false); + orderModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc); + return orderModel; + }); + model.Total = orders.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); - var orders = _orderService.GetAllOrders(affiliate.Id, command.Page - 1, command.PageSize); - var model = new GridModel - { - Data = orders.Select(order => - { - var orderModel = new AffiliateModel.AffiliatedOrderModel(); - orderModel.Id = order.Id; - orderModel.OrderStatus = order.OrderStatus.GetLocalizedEnum(_localizationService, _workContext); - orderModel.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_localizationService, _workContext); - orderModel.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_localizationService, _workContext); - orderModel.OrderTotal = _priceFormatter.FormatPrice(order.OrderTotal, true, false); - orderModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc); - return orderModel; - }), - Total = orders.TotalCount - }; + NotifyAccessDenied(); + } - return new JsonResult + return new JsonResult { Data = model }; @@ -320,19 +331,16 @@ public ActionResult AffiliatedOrderList(int affiliateId, GridCommand command) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult AffiliatedCustomerList(int affiliateId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageAffiliates)) - return AccessDeniedView(); + var model = new GridModel(); - var affiliate = _affiliateService.GetAffiliateById(affiliateId); - if (affiliate == null) - throw new ArgumentException("No affiliate found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageAffiliates)) + { + var affiliate = _affiliateService.GetAffiliateById(affiliateId); + var customers = _customerService.GetAllCustomers(affiliate.Id, command.Page - 1, command.PageSize); - var customers = _customerService.GetAllCustomers(affiliate.Id, command.Page - 1, command.PageSize); - var model = new GridModel - { - Data = customers.Select(customer => - { - var customerModel = new AffiliateModel.AffiliatedCustomerModel + model.Data = customers.Select(customer => + { + var customerModel = new AffiliateModel.AffiliatedCustomerModel { Id = customer.Id, Email = customer.Email, @@ -340,16 +348,23 @@ public ActionResult AffiliatedCustomerList(int affiliateId, GridCommand command) FullName = customer.GetFullName() }; - return customerModel; - }), - Total = customers.TotalCount - }; + return customerModel; + }); + model.Total = customers.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { Data = model }; } + #endregion } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/BlogController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/BlogController.cs index 98d4a868c4..f0883628b1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/BlogController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/BlogController.cs @@ -105,29 +105,37 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageBlog)) - return AccessDeniedView(); + var model = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageBlog)) + { + var blogPosts = _blogService.GetAllBlogPosts(0, 0, null, null, command.Page - 1, command.PageSize, true); + + model.Data = blogPosts.Select(x => + { + var m = x.ToModel(); + if (x.StartDateUtc.HasValue) + m.StartDate = _dateTimeHelper.ConvertToUserTime(x.StartDateUtc.Value, DateTimeKind.Utc); + if (x.EndDateUtc.HasValue) + m.EndDate = _dateTimeHelper.ConvertToUserTime(x.EndDateUtc.Value, DateTimeKind.Utc); + m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + m.LanguageName = x.Language.Name; + m.Comments = x.ApprovedCommentCount + x.NotApprovedCommentCount; + return m; + }); + + model.Total = blogPosts.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var blogPosts = _blogService.GetAllBlogPosts(0, 0, null, null, command.Page - 1, command.PageSize, true); - var gridModel = new GridModel - { - Data = blogPosts.Select(x => - { - var m = x.ToModel(); - if (x.StartDateUtc.HasValue) - m.StartDate = _dateTimeHelper.ConvertToUserTime(x.StartDateUtc.Value, DateTimeKind.Utc); - if (x.EndDateUtc.HasValue) - m.EndDate = _dateTimeHelper.ConvertToUserTime(x.EndDateUtc.Value, DateTimeKind.Utc); - m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - m.LanguageName = x.Language.Name; - m.Comments = x.ApprovedCommentCount + x.NotApprovedCommentCount; - return m; - }), - Total = blogPosts.TotalCount - }; return new JsonResult { - Data = gridModel + Data = model }; } @@ -269,71 +277,76 @@ public ActionResult Comments(int? filterByBlogPostId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Comments(int? filterByBlogPostId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageBlog)) - return AccessDeniedView(); + var model = new GridModel(); - IList comments; - if (filterByBlogPostId.HasValue) - { - //filter comments by blog - var blogPost = _blogService.GetBlogPostById(filterByBlogPostId.Value); - comments = blogPost.BlogComments.OrderBy(bc => bc.CreatedOnUtc).ToList(); - } - else - { - //load all blog comments - comments = _customerContentService.GetAllCustomerContent(0, null); - } + if (_permissionService.Authorize(StandardPermissionProvider.ManageBlog)) + { + IList comments; + if (filterByBlogPostId.HasValue) + { + //filter comments by blog + var blogPost = _blogService.GetBlogPostById(filterByBlogPostId.Value); + comments = blogPost.BlogComments.OrderBy(bc => bc.CreatedOnUtc).ToList(); + } + else + { + //load all blog comments + comments = _customerContentService.GetAllCustomerContent(0, null); + } - var gridModel = new GridModel - { - Data = comments.PagedForCommand(command).Select(blogComment => - { - var commentModel = new BlogCommentModel(); + model.Data = comments.PagedForCommand(command).Select(blogComment => + { + var commentModel = new BlogCommentModel(); var customer = _customerService.GetCustomerById(blogComment.CustomerId); - commentModel.Id = blogComment.Id; - commentModel.BlogPostId = blogComment.BlogPostId; - commentModel.BlogPostTitle = blogComment.BlogPost.Title; - commentModel.CustomerId = blogComment.CustomerId; - commentModel.IpAddress = blogComment.IpAddress; - commentModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(blogComment.CreatedOnUtc, DateTimeKind.Utc); - commentModel.Comment = Core.Html.HtmlUtils.FormatText(blogComment.CommentText, false, true, false, false, false, false); + commentModel.Id = blogComment.Id; + commentModel.BlogPostId = blogComment.BlogPostId; + commentModel.BlogPostTitle = blogComment.BlogPost.Title; + commentModel.CustomerId = blogComment.CustomerId; + commentModel.IpAddress = blogComment.IpAddress; + commentModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(blogComment.CreatedOnUtc, DateTimeKind.Utc); + commentModel.Comment = Core.Html.HtmlUtils.FormatText(blogComment.CommentText, false, true, false, false, false, false); if (customer == null) commentModel.CustomerName = "".NaIfEmpty(); else commentModel.CustomerName = customer.GetFullName(); - return commentModel; - }), - Total = comments.Count, - }; + return commentModel; + }); + + model.Total = comments.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { - Data = gridModel + Data = model }; } [GridAction(EnableCustomBinding = true)] public ActionResult CommentDelete(int? filterByBlogPostId, int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageBlog)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageBlog)) + { + var comment = _customerContentService.GetCustomerContentById(id) as BlogComment; - var comment = _customerContentService.GetCustomerContentById(id) as BlogComment; - if (comment == null) - throw new ArgumentException("No comment found with the specified id"); + var blogPost = comment.BlogPost; + _customerContentService.DeleteCustomerContent(comment); - var blogPost = comment.BlogPost; - _customerContentService.DeleteCustomerContent(comment); - //update totals - _blogService.UpdateCommentTotals(blogPost); + //update totals + _blogService.UpdateCommentTotals(blogPost); + } return Comments(filterByBlogPostId, command); } - #endregion } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CampaignController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CampaignController.cs index 7b3b290fd9..77a2ca4909 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CampaignController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CampaignController.cs @@ -88,24 +88,31 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCampaigns)) - return AccessDeniedView(); + var model = new GridModel(); - var campaigns = _campaignService.GetAllCampaigns(); - var gridModel = new GridModel - { - Data = campaigns.Select(x => - { - var model = x.ToModel(); - model.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - return model; - }), - Total = campaigns.Count - }; + if (_permissionService.Authorize(StandardPermissionProvider.ManageCampaigns)) + { + var campaigns = _campaignService.GetAllCampaigns(); + + model.Data = campaigns.Select(x => + { + var m = x.ToModel(); + m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + return m; + }); + + model.Total = campaigns.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { - Data = gridModel + Data = model }; } @@ -204,7 +211,7 @@ public ActionResult SendTestEmail(CampaignModel model) { var emailAccount = _emailAccountService.GetDefaultEmailAccount(); if (emailAccount == null) - throw new SmartException("Email account could not be loaded"); + throw new SmartException(T("Common.Error.NoEmailAccount")); var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(model.TestEmail); if (subscription != null) @@ -220,7 +227,8 @@ public ActionResult SendTestEmail(CampaignModel model) _campaignService.SendCampaign(campaign, emailAccount, model.TestEmail); } - NotifySuccess(_localizationService.GetResource("Admin.Promotions.Campaigns.TestEmailSentToCustomers"), false); + NotifySuccess(T("Admin.Promotions.Campaigns.TestEmailSentToCustomers"), false); + return View(model); } catch (Exception exc) @@ -249,7 +257,7 @@ public ActionResult SendMassEmail(CampaignModel model) { var emailAccount = _emailAccountService.GetDefaultEmailAccount(); if (emailAccount == null) - throw new SmartException("Email account could not be loaded"); + throw new SmartException(T("Common.Error.NoEmailAccount")); var subscriptions = _newsLetterSubscriptionService.GetAllNewsLetterSubscriptions(null, 0 , int.MaxValue, false); var totalEmailsSent = _campaignService.SendCampaign(campaign, emailAccount, subscriptions); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs index c8b714dfab..df70f6c9c6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CategoryController.cs @@ -21,13 +21,12 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Stores; -using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; -using Telerik.Web.Mvc; -using Telerik.Web.Mvc.UI; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; +using Telerik.Web.Mvc; +using Telerik.Web.Mvc.UI; namespace SmartStore.Admin.Controllers { @@ -162,6 +161,8 @@ protected void PrepareCategoryModel(CategoryModel model, Category category, bool if (model == null) throw new ArgumentNullException("model"); + model.GridPageSize = _adminAreaSettings.GridPageSize; + var discounts = _discountService.GetAllDiscounts(DiscountType.AssignedToCategories, null, true); model.AvailableDiscounts = discounts.ToList(); @@ -273,7 +274,6 @@ public ActionResult List() GridPageSize = _adminAreaSettings.GridPageSize }; - model.AvailableStores.Add(new SelectListItem { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); foreach (var store in allStores) { model.AvailableStores.Add(new SelectListItem { Text = store.Name, Value = store.Id.ToString() }); @@ -285,24 +285,30 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, CategoryListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var categories = _categoryService.GetAllCategories(model.SearchCategoryName, command.Page - 1, command.PageSize, true, model.SearchAlias, true, false, model.SearchStoreId); - var mappedCategories = categories.ToDictionary(x => x.Id); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var categories = _categoryService.GetAllCategories(model.SearchCategoryName, command.Page - 1, command.PageSize, true, model.SearchAlias, true, false, model.SearchStoreId); + var mappedCategories = categories.ToDictionary(x => x.Id); - var gridModel = new GridModel - { - Data = categories.Select(x => - { - var categoryModel = x.ToModel(); - categoryModel.Breadcrumb = x.GetCategoryBreadCrumb(_categoryService, mappedCategories); - return categoryModel; - }), - Total = categories.TotalCount - }; + gridModel.Data = categories.Select(x => + { + var categoryModel = x.ToModel(); + categoryModel.Breadcrumb = x.GetCategoryBreadCrumb(_categoryService, mappedCategories); + return categoryModel; + }); - return new JsonResult + gridModel.Total = categories.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + + return new JsonResult { Data = gridModel }; @@ -370,7 +376,6 @@ public ActionResult Tree() var allStores = _storeService.GetAllStores(); var model = new CategoryTreeModel(); - model.AvailableStores.Add(new SelectListItem { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); foreach (var store in allStores) { model.AvailableStores.Add(new SelectListItem { Text = store.Name, Value = store.Id.ToString() }); @@ -491,17 +496,16 @@ public ActionResult Create() return AccessDeniedView(); var model = new CategoryModel(); - //parent categories - //locales - AddLocales(_languageService, model.Locales); - //templates + + AddLocales(_languageService, model.Locales); + PrepareTemplatesModel(model); PrepareCategoryModel(model, null, true); - //ACL - PrepareAclModel(model, null, false); - //Stores + + PrepareAclModel(model, null, false); + PrepareStoresMappingModel(model, null, false); - //default values + model.PageSize = 12; model.Published = true; @@ -595,16 +599,18 @@ public ActionResult Edit(int id) return RedirectToAction("List"); var model = category.ToModel(); - //parent categories - if (model.ParentCategoryId.HasValue) + + //parent categories + if (model.ParentCategoryId.HasValue) { var parentCategory = _categoryService.GetCategoryById(model.ParentCategoryId.Value); + if (parentCategory != null && !parentCategory.Deleted) model.ParentCategoryBreadcrumb = parentCategory.GetCategoryBreadCrumb(_categoryService); else model.ParentCategoryId = 0; } - //locales + AddLocales(_languageService, model.Locales, (locale, languageId) => { locale.Name = category.GetLocalized(x => x.Name, languageId, false, false); @@ -616,12 +622,12 @@ public ActionResult Edit(int id) locale.MetaTitle = category.GetLocalized(x => x.MetaTitle, languageId, false, false); locale.SeName = category.GetSeName(languageId, false, false); }); - //templates + PrepareTemplatesModel(model); PrepareCategoryModel(model, category, false); - //ACL + PrepareAclModel(model, category, false); - //Stores + PrepareStoresMappingModel(model, category, false); return View(model); @@ -758,17 +764,16 @@ public ActionResult Delete(int id, string deleteType) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductList(GridCommand command, int categoryId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var productCategories = _categoryService.GetProductCategoriesByCategoryId(categoryId, command.Page - 1, command.PageSize, true); + var model = new GridModel(); - var products = _productService.GetProductsByIds(productCategories.Select(x => x.ProductId).ToArray()); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productCategories = _categoryService.GetProductCategoriesByCategoryId(categoryId, command.Page - 1, command.PageSize, true); - var model = new GridModel - { - Data = productCategories.Select(x => - { + var products = _productService.GetProductsByIds(productCategories.Select(x => x.ProductId).ToArray()); + + model.Data = productCategories.Select(x => + { var productModel = new CategoryModel.CategoryProductModel { Id = x.Id, @@ -788,13 +793,20 @@ public ActionResult ProductList(GridCommand command, int categoryId) productModel.ProductTypeLabelHint = product.ProductTypeLabelHint; productModel.Published = product.Published; } - + return productModel; - }), - Total = productCategories.TotalCount - }; + }); + + model.Total = productCategories.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - return new JsonResult + return new JsonResult { Data = model }; @@ -803,16 +815,15 @@ public ActionResult ProductList(GridCommand command, int categoryId) [GridAction(EnableCustomBinding = true)] public ActionResult ProductUpdate(GridCommand command, CategoryModel.CategoryProductModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productCategory = _categoryService.GetProductCategoryById(model.Id); - var productCategory = _categoryService.GetProductCategoryById(model.Id); - if (productCategory == null) - throw new ArgumentException("No product category mapping found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + productCategory.IsFeaturedProduct = model.IsFeaturedProduct; + productCategory.DisplayOrder = model.DisplayOrder1; - productCategory.IsFeaturedProduct = model.IsFeaturedProduct; - productCategory.DisplayOrder = model.DisplayOrder1; - _categoryService.UpdateProductCategory(productCategory); + _categoryService.UpdateProductCategory(productCategory); + } return ProductList(command, productCategory.CategoryId); } @@ -820,138 +831,56 @@ public ActionResult ProductUpdate(GridCommand command, CategoryModel.CategoryPro [GridAction(EnableCustomBinding = true)] public ActionResult ProductDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productCategory = _categoryService.GetProductCategoryById(id); + var categoryId = productCategory.CategoryId; - var productCategory = _categoryService.GetProductCategoryById(id); - if (productCategory == null) - throw new ArgumentException("No product category mapping found with the specified id"); - - var categoryId = productCategory.CategoryId; - _categoryService.DeleteProductCategory(productCategory); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _categoryService.DeleteProductCategory(productCategory); + } return ProductList(command, categoryId); } - public ActionResult ProductAddPopup(int categoryId) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - - var ctx = new ProductSearchContext(); - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageSize = _adminAreaSettings.GridPageSize; - ctx.ShowHidden = true; - - var products = _productService.SearchProducts(ctx); + [HttpPost] + public ActionResult ProductAdd(int categoryId, string selectedProductIds) + { + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productIds = selectedProductIds.SplitSafe(",").Select(x => x.ToInt()).ToArray(); + var products = _productService.GetProductsByIds(productIds); + ProductCategory productCategory = null; + var maxDisplayOrder = -1; - var model = new CategoryModel.AddCategoryProductModel(); - model.Products = new GridModel - { - Data = products.Select(x => + foreach (var product in products) { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }), - Total = products.TotalCount - }; - // categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult ProductAddPopupList(GridCommand command, CategoryModel.AddCategoryProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var gridModel = new GridModel(); - var ctx = new ProductSearchContext(); - - if (model.SearchCategoryId > 0) - ctx.CategoryIds.Add(model.SearchCategoryId); + var existingProductCategories = _categoryService.GetProductCategoriesByCategoryId(categoryId, 0, int.MaxValue, true); - ctx.ManufacturerId = model.SearchManufacturerId; - ctx.Keywords = model.SearchProductName; - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageIndex = command.Page - 1; - ctx.PageSize = command.PageSize; - ctx.ShowHidden = true; - ctx.ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null; - - var products = _productService.SearchProducts(ctx); - gridModel.Data = products.Select(x => + if (existingProductCategories.FindProductCategory(product.Id, categoryId) == null) + { + if (maxDisplayOrder == -1 && (productCategory = existingProductCategories.OrderByDescending(x => x.DisplayOrder).FirstOrDefault()) != null) + { + maxDisplayOrder = productCategory.DisplayOrder; + } + + _categoryService.InsertProductCategory(new ProductCategory + { + CategoryId = categoryId, + ProductId = product.Id, + IsFeaturedProduct = false, + DisplayOrder = ++maxDisplayOrder + }); + } + } + } + else { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }); - - gridModel.Total = products.TotalCount; - return new JsonResult - { - Data = gridModel - }; - } - - [HttpPost] - [FormValueRequired("save")] - public ActionResult ProductAddPopup(string btnId, string formId, CategoryModel.AddCategoryProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (model.SelectedProductIds != null) - { - foreach (int id in model.SelectedProductIds) - { - var product = _productService.GetProductById(id); - if (product != null) - { - var existingProductCategories = _categoryService.GetProductCategoriesByCategoryId(model.CategoryId, 0, int.MaxValue, true); - if (existingProductCategories.FindProductCategory(id, model.CategoryId) == null) - { - _categoryService.InsertProductCategory( - new ProductCategory() - { - CategoryId = model.CategoryId, - ProductId = id, - IsFeaturedProduct = false, - DisplayOrder = 1 - }); - } - } - } - } + NotifyAccessDenied(); + } - ViewBag.RefreshPage = true; - ViewBag.btnId = btnId; - ViewBag.formId = formId; - model.Products = new GridModel(); - return View(model); - } + return new EmptyResult(); + } - #endregion - } + #endregion + } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs index 78170d9674..51db07dad9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CheckoutAttributeController.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Web.Mvc; using SmartStore.Admin.Models.Orders; +using SmartStore.Core; +using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Logging; @@ -10,7 +12,9 @@ using SmartStore.Services.Localization; using SmartStore.Services.Orders; using SmartStore.Services.Security; +using SmartStore.Services.Stores; using SmartStore.Services.Tax; +using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; @@ -18,41 +22,49 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class CheckoutAttributeController : AdminControllerBase { - #region Fields + #region Fields - private readonly ICheckoutAttributeService _checkoutAttributeService; + private readonly ICommonServices _services; + private readonly ICheckoutAttributeService _checkoutAttributeService; private readonly ILanguageService _languageService; private readonly ILocalizedEntityService _localizedEntityService; private readonly ITaxCategoryService _taxCategoryService; private readonly IMeasureService _measureService; private readonly MeasureSettings _measureSettings; private readonly ICustomerActivityService _customerActivityService; - private readonly ICommonServices _services; + private readonly IStoreMappingService _storeMappingService; - #endregion + private readonly AdminAreaSettings _adminAreaSettings; + + #endregion - #region Constructors + #region Constructors - public CheckoutAttributeController(ICheckoutAttributeService checkoutAttributeService, + public CheckoutAttributeController( + ICommonServices services, + ICheckoutAttributeService checkoutAttributeService, ILanguageService languageService, ILocalizedEntityService localizedEntityService, ITaxCategoryService taxCategoryService, ICustomerActivityService customerActivityService, IMeasureService measureService, MeasureSettings measureSettings, - ICommonServices services) + IStoreMappingService storeMappingService, + AdminAreaSettings adminAreaSettings) { - this._checkoutAttributeService = checkoutAttributeService; - this._languageService = languageService; - this._localizedEntityService = localizedEntityService; - this._taxCategoryService = taxCategoryService; - this._customerActivityService = customerActivityService; - this._measureService = measureService; - this._measureSettings = measureSettings; - this._services = services; + _services = services; + _checkoutAttributeService = checkoutAttributeService; + _languageService = languageService; + _localizedEntityService = localizedEntityService; + _taxCategoryService = taxCategoryService; + _customerActivityService = customerActivityService; + _measureService = measureService; + _measureSettings = measureSettings; + _storeMappingService = storeMappingService; + _adminAreaSettings = adminAreaSettings; } #endregion @@ -94,17 +106,32 @@ private void PrepareCheckoutAttributeModel(CheckoutAttributeModel model, Checkou if (model == null) throw new ArgumentNullException("model"); - //tax categories var taxCategories = _taxCategoryService.GetAllTaxCategories(); - foreach (var tc in taxCategories) - model.AvailableTaxCategories.Add(new SelectListItem() { Text = tc.Name, Value = tc.Id.ToString(), Selected = checkoutAttribute != null && !excludeProperties && tc.Id == checkoutAttribute.TaxCategoryId }); - } + + foreach (var tc in taxCategories) + { + model.AvailableTaxCategories.Add(new SelectListItem + { + Text = tc.Name, + Value = tc.Id.ToString(), + Selected = (checkoutAttribute != null && !excludeProperties && tc.Id == checkoutAttribute.TaxCategoryId) + }); + } + + model.AvailableStores = _services.StoreService.GetAllStores() + .Select(s => s.ToModel()) + .ToList(); + + if (!excludeProperties) + { + model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(checkoutAttribute); + } + } #endregion #region Checkout attributes - //list public ActionResult Index() { return RedirectToAction("List"); @@ -115,29 +142,45 @@ public ActionResult List() if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) return AccessDeniedView(); - return View(); + var model = new CheckoutAttributeListModel + { + GridPageSize = _adminAreaSettings.GridPageSize + }; + + model.AvailableStores = _services.StoreService.GetAllStores().ToSelectListItems(); + + return View(model); } [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(true); - var gridModel = new GridModel + var model = new GridModel(); + + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var query = _checkoutAttributeService.GetCheckoutAttributes(0, true); + var pagedList = new PagedList(query, command.Page - 1, command.PageSize); + + model.Data = pagedList.Select(x => + { + var caModel = x.ToModel(); + caModel.AttributeControlTypeName = x.AttributeControlType.GetLocalizedEnum(_services.Localization, _services.WorkContext); + return caModel; + }); + + model.Total = pagedList.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + + return new JsonResult { - Data = checkoutAttributes.Select(x => - { - var caModel = x.ToModel(); - caModel.AttributeControlTypeName = x.AttributeControlType.GetLocalizedEnum(_services.Localization, _services.WorkContext); - return caModel; - }), - Total = checkoutAttributes.Count() - }; - return new JsonResult - { - Data = gridModel + Data = model }; } @@ -166,10 +209,13 @@ public ActionResult Create(CheckoutAttributeModel model, bool continueEditing) { var checkoutAttribute = model.ToEntity(); _checkoutAttributeService.InsertCheckoutAttribute(checkoutAttribute); + UpdateAttributeLocales(checkoutAttribute, model); - //activity log - _customerActivityService.InsertActivity("AddNewCheckoutAttribute", _services.Localization.GetResource("ActivityLog.AddNewCheckoutAttribute"), checkoutAttribute.Name); + _storeMappingService.SaveStoreMappings(checkoutAttribute, model.SelectedStoreIds); + + //activity log + _customerActivityService.InsertActivity("AddNewCheckoutAttribute", _services.Localization.GetResource("ActivityLog.AddNewCheckoutAttribute"), checkoutAttribute.Name); NotifySuccess(_services.Localization.GetResource("Admin.Catalog.Attributes.CheckoutAttributes.Added")); return continueEditing ? RedirectToAction("Edit", new { id = checkoutAttribute.Id }) : RedirectToAction("List"); @@ -188,7 +234,6 @@ public ActionResult Edit(int id) var checkoutAttribute = _checkoutAttributeService.GetCheckoutAttributeById(id); if (checkoutAttribute == null) - //No checkout attribute found with the specified id return RedirectToAction("List"); var model = checkoutAttribute.ToModel(); @@ -211,7 +256,6 @@ public ActionResult Edit(CheckoutAttributeModel model, bool continueEditing) var checkoutAttribute = _checkoutAttributeService.GetCheckoutAttributeById(model.Id); if (checkoutAttribute == null) - //No checkout attribute found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) @@ -221,8 +265,10 @@ public ActionResult Edit(CheckoutAttributeModel model, bool continueEditing) UpdateAttributeLocales(checkoutAttribute, model); - //activity log - _customerActivityService.InsertActivity("EditCheckoutAttribute", _services.Localization.GetResource("ActivityLog.EditCheckoutAttribute"), checkoutAttribute.Name); + _storeMappingService.SaveStoreMappings(checkoutAttribute, model.SelectedStoreIds); + + //activity log + _customerActivityService.InsertActivity("EditCheckoutAttribute", _services.Localization.GetResource("ActivityLog.EditCheckoutAttribute"), checkoutAttribute.Name); NotifySuccess(_services.Localization.GetResource("Admin.Catalog.Attributes.CheckoutAttributes.Updated")); return continueEditing ? RedirectToAction("Edit", checkoutAttribute.Id) : RedirectToAction("List"); @@ -254,31 +300,29 @@ public ActionResult DeleteConfirmed(int id) #region Checkout attribute values - //list [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ValueList(int checkoutAttributeId, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var values = _checkoutAttributeService.GetCheckoutAttributeValues(checkoutAttributeId); - var gridModel = new GridModel - { - Data = values.Select(x => - { - var model = x.ToModel(); - //locales - //AddLocales(_languageService, model.Locales, (locale, languageId) => - //{ - // locale.Name = x.GetLocalized(y => y.Name, languageId, false, false); - //}); - return model; - }), - Total = values.Count() - }; - return new JsonResult + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var values = _checkoutAttributeService.GetCheckoutAttributeValues(checkoutAttributeId); + + model.Data = values.Select(x => x.ToModel()); + + model.Total = values.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + + return new JsonResult { - Data = gridModel + Data = model }; } @@ -306,7 +350,6 @@ public ActionResult ValueCreatePopup(string btnId, string formId, CheckoutAttrib var checkoutAttribute = _checkoutAttributeService.GetCheckoutAttributeById(model.CheckoutAttributeId); if (checkoutAttribute == null) - //No checkout attribute found with the specified id return RedirectToAction("List"); model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; @@ -337,7 +380,6 @@ public ActionResult ValueEditPopup(int id) var cav = _checkoutAttributeService.GetCheckoutAttributeValueById(id); if (cav == null) - //No checkout attribute value found with the specified id return RedirectToAction("List"); var model = cav.ToModel(); @@ -361,7 +403,6 @@ public ActionResult ValueEditPopup(string btnId, string formId, CheckoutAttribut var cav = _checkoutAttributeService.GetCheckoutAttributeValueById(model.Id); if (cav == null) - //No checkout attribute value found with the specified id return RedirectToAction("List"); model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; @@ -384,22 +425,19 @@ public ActionResult ValueEditPopup(string btnId, string formId, CheckoutAttribut return View(model); } - //delete [GridAction(EnableCustomBinding = true)] public ActionResult ValueDelete(int valueId, int checkoutAttributeId, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var cav = _checkoutAttributeService.GetCheckoutAttributeValueById(valueId); - var cav = _checkoutAttributeService.GetCheckoutAttributeValueById(valueId); - if (cav == null) - throw new ArgumentException("No checkout attribute value found with the specified id"); - _checkoutAttributeService.DeleteCheckoutAttributeValue(cav); + _checkoutAttributeService.DeleteCheckoutAttributeValue(cav); + } return ValueList(checkoutAttributeId, command); } - #endregion } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs index d7f4b817d3..4bebf35ee1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CommonController.cs @@ -28,6 +28,7 @@ using SmartStore.Services; using SmartStore.Services.Common; using SmartStore.Services.Customers; +using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Directory; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; @@ -65,7 +66,8 @@ public class CommonController : AdminControllerBase private readonly Lazy _securitySettings; private readonly Lazy _menuPublisher; private readonly Lazy _pluginFinder; - private readonly IGenericAttributeService _genericAttributeService; + private readonly Lazy _importProfileService; + private readonly IGenericAttributeService _genericAttributeService; private readonly ICommonServices _services; private readonly Func _cache; @@ -91,7 +93,8 @@ public CommonController( Lazy securitySettings, Lazy menuPublisher, Lazy pluginFinder, - IGenericAttributeService genericAttributeService, + Lazy importProfileService, + IGenericAttributeService genericAttributeService, ICommonServices services, Func cache) { @@ -110,6 +113,7 @@ public CommonController( this._securitySettings = securitySettings; this._menuPublisher = menuPublisher; this._pluginFinder = pluginFinder; + this._importProfileService = importProfileService; this._genericAttributeService = genericAttributeService; this._services = services; this._cache = cache; @@ -515,36 +519,47 @@ public ActionResult Warnings() } // sitemap reachability - var sitemapReachable = false; + string sitemapUrl = null; try { - var sitemapUrl = Url.RouteUrl("SitemapSEO", (object)null, _securitySettings.Value.ForceSslForAllPages ? "https" : "http"); - var request = (HttpWebRequest)WebRequest.Create(sitemapUrl); + sitemapUrl = WebHelper.GetAbsoluteUrl(Url.RouteUrl("SitemapSEO"), this.Request); + var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(sitemapUrl)); request.Method = "HEAD"; request.Timeout = 15000; using (var response = (HttpWebResponse)request.GetResponse()) { - sitemapReachable = (response.StatusCode == HttpStatusCode.OK); + var status = response.StatusCode; + var warningModel = new SystemWarningModel(); + warningModel.Level = (status == HttpStatusCode.OK ? SystemWarningLevel.Pass : SystemWarningLevel.Warning); + + switch (status) + { + case HttpStatusCode.OK: + warningModel.Text = T("Admin.System.Warnings.SitemapReachable.OK"); + break; + default: + if (status == HttpStatusCode.MethodNotAllowed) + warningModel.Text = T("Admin.System.Warnings.SitemapReachable.MethodNotAllowed"); + else + warningModel.Text = T("Admin.System.Warnings.SitemapReachable.Wrong"); + + warningModel.Text = string.Concat(warningModel.Text, " ", T("Admin.Common.HttpStatus", (int)status, status.ToString())); + break; + } + + model.Add(warningModel); } } - catch (WebException) { } - - if (sitemapReachable) - { - model.Add(new SystemWarningModel - { - Level = SystemWarningLevel.Pass, - Text = _localizationService.GetResource("Admin.System.Warnings.SitemapReachable.OK") - }); - } - else + catch (WebException exception) { model.Add(new SystemWarningModel { Level = SystemWarningLevel.Warning, - Text = _localizationService.GetResource("Admin.System.Warnings.SitemapReachable.Wrong") + Text = T("Admin.System.Warnings.SitemapReachable.Wrong") }); + + Logger.Warning(sitemapUrl.IsEmpty() ? "SitemapSEO" : sitemapUrl, exception); } //primary exchange rate currency @@ -920,7 +935,23 @@ public ActionResult MaintenanceDeleteFiles(MaintenanceModel model) } } - return View(model); + // clear unreferenced profile folders + var importProfileFolders = _importProfileService.Value.GetImportProfiles() + .Select(x => x.FolderName) + .ToList(); + + var infoImportProfiles = new DirectoryInfo(CommonHelper.MapPath("~/App_Data/ImportProfiles")); + + foreach (var infoSubFolder in infoImportProfiles.GetDirectories()) + { + if (!importProfileFolders.Contains(infoSubFolder.Name)) + { + FileSystemHelper.ClearDirectory(infoSubFolder.FullName, true); + ++model.DeleteExportedFiles.NumberOfDeletedFolders; + } + } + + return View(model); } [HttpPost, ActionName("Maintenance"), ValidateInput(false)] @@ -937,11 +968,11 @@ public ActionResult MaintenanceExecuteSql(MaintenanceModel model) { dbContext.ExecuteSqlThroughSmo(model.SqlQuery); - NotifySuccess("The sql command was executed successfully."); + NotifySuccess(T("Admin.System.Maintenance.SqlQuery.Succeeded")); } - catch (Exception ex) + catch (Exception exception) { - NotifyError("Error executing sql command: {0}".FormatCurrentUI(ex.Message)); + NotifyError(exception); } } @@ -1014,35 +1045,43 @@ public ActionResult GenericAttributes(string entityName, int entityId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult GenericAttributesSelect(string entityName, int entityId, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) - return AccessDeniedView(); + var model = new GridModel(); var storeId = _services.StoreContext.CurrentStore.Id; - ViewBag.StoreId = storeId; + ViewBag.StoreId = storeId; - var model = new List(); - if (entityName.HasValue() && entityId > 0) - { - var attributes = _genericAttributeService.GetAttributesForEntity(entityId, entityName); - var query = from attr in attributes - where attr.StoreId == storeId || attr.StoreId == 0 - select new GenericAttributeModel - { - Id = attr.Id, - EntityId = attr.EntityId, - EntityName = attr.KeyGroup, - Key = attr.Key, - Value = attr.Value - }; - model.AddRange(query); - } + if (_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) + { + if (entityName.HasValue() && entityId > 0) + { + var attributes = _genericAttributeService.GetAttributesForEntity(entityId, entityName); + model.Data = attributes + .Where(x => x.StoreId == storeId || x.StoreId == 0) + .Select(x => new GenericAttributeModel + { + Id = x.Id, + EntityId = x.EntityId, + EntityName = x.KeyGroup, + Key = x.Key, + Value = x.Value + }) + .ToList(); + + model.Total = model.Data.Count(); + } + else + { + model.Data = Enumerable.Empty(); + } + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var result = new GridModel - { - Data = model, - Total = model.Count - }; return new JsonResult { Data = model @@ -1052,38 +1091,36 @@ public ActionResult GenericAttributesSelect(string entityName, int entityId, Gri [GridAction(EnableCustomBinding = true)] public ActionResult GenericAttributeAdd(GenericAttributeModel model, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) - return AccessDeniedView(); - - model.Key = model.Key.TrimSafe(); - model.Value = model.Value.TrimSafe(); + if (_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) + { + model.Key = model.Key.TrimSafe(); + model.Value = model.Value.TrimSafe(); - if (!ModelState.IsValid) - { - // display the first model error - var modelStateErrorMessages = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrorMessages.FirstOrDefault()); - } + if (!ModelState.IsValid) + { + var modelStateErrorMessages = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrorMessages.FirstOrDefault()); + } - var storeId = _services.StoreContext.CurrentStore.Id; + var storeId = _services.StoreContext.CurrentStore.Id; - var attr = _genericAttributeService.GetAttribute(model.EntityName, model.EntityId, model.Key, storeId); - if (attr == null) - { - var ga = new GenericAttribute - { - StoreId = storeId, - KeyGroup = model.EntityName, - EntityId = model.EntityId, - Key = model.Key, - Value = model.Value - }; - _genericAttributeService.InsertAttribute(ga); - } - else - { - return Content(string.Format(_localizationService.GetResource("Admin.Common.GenericAttributes.NameAlreadyExists"), model.Key)); - } + var attr = _genericAttributeService.GetAttribute(model.EntityName, model.EntityId, model.Key, storeId); + if (attr == null) + { + _genericAttributeService.InsertAttribute(new GenericAttribute + { + StoreId = storeId, + KeyGroup = model.EntityName, + EntityId = model.EntityId, + Key = model.Key, + Value = model.Value + }); + } + else + { + return Content(T("Admin.Common.GenericAttributes.NameAlreadyExists", model.Key)); + } + } return GenericAttributesSelect(model.EntityName, model.EntityId, command); } @@ -1091,38 +1128,38 @@ public ActionResult GenericAttributeAdd(GenericAttributeModel model, GridCommand [GridAction(EnableCustomBinding = true)] public ActionResult GenericAttributeUpdate(GenericAttributeModel model, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) + { + model.Key = model.Key.TrimSafe(); + model.Value = model.Value.TrimSafe(); - model.Key = model.Key.TrimSafe(); - model.Value = model.Value.TrimSafe(); + if (!ModelState.IsValid) + { + var modelStateErrorMessages = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrorMessages.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - // display the first model error - var modelStateErrorMessages = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrorMessages.FirstOrDefault()); - } + var storeId = _services.StoreContext.CurrentStore.Id; - var storeId = _services.StoreContext.CurrentStore.Id; + var attr = _genericAttributeService.GetAttributeById(model.Id); + // if the key changed, ensure it isn't being used by another attribute + if (!attr.Key.IsCaseInsensitiveEqual(model.Key)) + { + var attr2 = _genericAttributeService.GetAttributesForEntity(model.EntityId, model.EntityName) + .Where(x => x.StoreId == storeId && x.Key.Equals(model.Key, StringComparison.InvariantCultureIgnoreCase)) + .FirstOrDefault(); - var attr = _genericAttributeService.GetAttributeById(model.Id); - // if the key changed, ensure it isn't being used by another attribute - if (!attr.Key.IsCaseInsensitiveEqual(model.Key)) - { - var attr2 = _genericAttributeService.GetAttributesForEntity(model.EntityId, model.EntityName) - .Where(x => x.StoreId == storeId && x.Key.Equals(model.Key, StringComparison.InvariantCultureIgnoreCase)) - .FirstOrDefault(); - if (attr2 != null && attr2.Id != attr.Id) - { - return Content(string.Format(_localizationService.GetResource("Admin.Common.GenericAttributes.NameAlreadyExists"), model.Key)); - } - } + if (attr2 != null && attr2.Id != attr.Id) + { + return Content(T("Admin.Common.GenericAttributes.NameAlreadyExists", model.Key)); + } + } - attr.Key = model.Key; - attr.Value = model.Value; + attr.Key = model.Key; + attr.Value = model.Value; - _genericAttributeService.UpdateAttribute(attr); + _genericAttributeService.UpdateAttribute(attr); + } return GenericAttributesSelect(model.EntityName, model.EntityId, command); } @@ -1130,17 +1167,12 @@ public ActionResult GenericAttributeUpdate(GenericAttributeModel model, GridComm [GridAction(EnableCustomBinding = true)] public ActionResult GenericAttributeDelete(int id, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) - return AccessDeniedView(); - var attr = _genericAttributeService.GetAttributeById(id); - if (attr == null) - { - throw new System.Web.HttpException(404, "No resource found with the specified id"); - } - - _genericAttributeService.DeleteAttribute(attr); + if (_services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel)) + { + _genericAttributeService.DeleteAttribute(attr); + } return GenericAttributesSelect(attr.KeyGroup, attr.EntityId, command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs index 18b11af66b..73192b52db 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ContentSliderController.cs @@ -2,22 +2,22 @@ using System.Linq; using System.Web.Mvc; using SmartStore.Admin.Models.ContentSlider; +using SmartStore.Core; using SmartStore.Core.Domain.Cms; +using SmartStore.Core.Domain.Localization; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; -using SmartStore.Services.Security; -using SmartStore.Web.Framework.Controllers; using SmartStore.Services.Media; -using SmartStore.Core.Domain.Localization; +using SmartStore.Services.Security; using SmartStore.Services.Stores; -using SmartStore.Core; +using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class ContentSliderController : AdminControllerBase { #region Fields @@ -67,12 +67,9 @@ private ContentSliderSettingsModel PrepareContentSliderSettingsModel(ContentSlid var allStores = _storeService.GetAllStores(); var model = _contentSliderSettings.ToModel(); - model.StoreCount = allStores.Count; - - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); foreach (var s in allStores) { - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + model.AvailableStores.Add(new SelectListItem { Text = s.Name, Value = s.Id.ToString() }); } foreach (var slide in model.Slides) @@ -237,16 +234,11 @@ public ActionResult EditSlide(ContentSliderSlideModel model, bool continueEditin model.LanguageName = lang.Name; } - model.PictureUrl = _pictureService.GetPictureUrl(model.PictureId); + model.PictureUrl = _pictureService.GetPictureUrl(model.PictureId); - //delete an old picture (if deleted or updated) - int prevPictureId = _contentSliderSettings.Slides[index].PictureId; - if (prevPictureId > 0 && prevPictureId != model.PictureId) - { - var prevPicture = _pictureService.GetPictureById(prevPictureId); - if (prevPicture != null) - _pictureService.DeletePicture(prevPicture); - } + // delete an old picture (if deleted or updated) + int prevPictureId = _contentSliderSettings.Slides[index].PictureId; + MediaHelper.UpdatePictureTransientState(prevPictureId, model.PictureId, true); _contentSliderSettings.Slides[index] = model.ToEntity(); _settingService.SaveSetting(_contentSliderSettings); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CountryController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CountryController.cs index 17300117c2..3a4d573b43 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CountryController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CountryController.cs @@ -3,8 +3,8 @@ using System.Linq; using System.Web.Mvc; using SmartStore.Admin.Models.Directory; -using SmartStore.Core; using SmartStore.Core.Domain.Directory; +using SmartStore.Services; using SmartStore.Services.Common; using SmartStore.Services.Directory; using SmartStore.Services.Localization; @@ -24,37 +24,31 @@ public class CountryController : AdminControllerBase private readonly ICountryService _countryService; private readonly IStateProvinceService _stateProvinceService; - private readonly ILocalizationService _localizationService; private readonly IAddressService _addressService; - private readonly IPermissionService _permissionService; private readonly ILocalizedEntityService _localizedEntityService; private readonly ILanguageService _languageService; - private readonly IStoreService _storeService; private readonly IStoreMappingService _storeMappingService; + private readonly ICommonServices _services; - #endregion + #endregion #region Constructors - public CountryController(ICountryService countryService, + public CountryController(ICountryService countryService, IStateProvinceService stateProvinceService, - ILocalizationService localizationService, IAddressService addressService, - IPermissionService permissionService, ILocalizedEntityService localizedEntityService, ILanguageService languageService, - IStoreService storeService, - IStoreMappingService storeMappingService) + IStoreMappingService storeMappingService, + ICommonServices services) { _countryService = countryService; _stateProvinceService = stateProvinceService; - _localizationService = localizationService; _addressService = addressService; - _permissionService = permissionService; _localizedEntityService = localizedEntityService; _languageService = languageService; - _storeService = storeService; _storeMappingService = storeMappingService; + _services = services; } #endregion  @@ -66,10 +60,7 @@ private void UpdateLocales(Country country, CountryModel model) { foreach (var localized in model.Locales) { - _localizedEntityService.SaveLocalizedValue(country, - x => x.Name, - localized.Name, - localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(country, x => x.Name, localized.Name, localized.LanguageId); } } @@ -78,10 +69,7 @@ private void UpdateLocales(StateProvince stateProvince, StateProvinceModel model { foreach (var localized in model.Locales) { - _localizedEntityService.SaveLocalizedValue(stateProvince, - x => x.Name, - localized.Name, - localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(stateProvince, x => x.Name, localized.Name, localized.LanguageId); } } @@ -91,19 +79,14 @@ private void PrepareCountryModel(CountryModel model, Country country, bool exclu if (model == null) throw new ArgumentNullException("model"); - var allStores = _storeService.GetAllStores(); + var allStores = _services.StoreService.GetAllStores(); model.AvailableStores = allStores.Select(s => s.ToModel()).ToList(); if (!excludeProperties) { - if (country != null) - model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(country); - else - model.SelectedStoreIds = new int[0]; + model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(country); } - - ViewBag.StoreCount = allStores.Count; } #endregion @@ -117,40 +100,85 @@ public ActionResult Index() public ActionResult List() { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); - var countries = _countryService.GetAllCountries(true); - var model = new GridModel - { - Data = countries.Select(x => x.ToModel()), - Total = countries.Count - }; - return View(model); + var allStores = _services.StoreService.GetAllStores(); + + var model = new CountryListModel + { + StoreCount = allStores.Count + }; + + return View(model); } [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult CountryList(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) - return AccessDeniedView(); + var model = new GridModel(); - var countries = _countryService.GetAllCountries(true); - var model = new GridModel - { - Data = countries.Select(x => x.ToModel()), - Total = countries.Count - }; + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) + { + var countries = _countryService.GetAllCountries(true); + + model.Data = countries.Select(x => x.ToModel()); + model.Total = countries.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { Data = model }; } - - public ActionResult Create() + + [GridAction(EnableCustomBinding = true)] + public ActionResult CountryUpdate(CountryModel model, GridCommand command) + { + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } + + var country = _countryService.GetCountryById(model.Id); + + country = model.ToEntity(country); + _countryService.UpdateCountry(country); + } + + return CountryList(command); + } + + [GridAction(EnableCustomBinding = true)] + public ActionResult CountryDelete(int id, GridCommand command) + { + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) + { + if (_addressService.GetAddressTotalByCountryId(id) > 0) + { + return Content(T("Admin.Configuration.Countries.CannotDeleteDueToAssociatedAddresses")); + } + + var country = _countryService.GetCountryById(id); + + _countryService.DeleteCountry(country); + } + + return CountryList(command); + } + + public ActionResult Create() { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var model = new CountryModel(); @@ -169,7 +197,7 @@ public ActionResult Create() [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Create(CountryModel model, bool continueEditing) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); if (ModelState.IsValid) @@ -181,7 +209,7 @@ public ActionResult Create(CountryModel model, bool continueEditing) _storeMappingService.SaveStoreMappings(country, model.SelectedStoreIds); - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Countries.Added")); + NotifySuccess(T("Admin.Configuration.Countries.Added")); return continueEditing ? RedirectToAction("Edit", new { id = country.Id }) : RedirectToAction("List"); } @@ -193,7 +221,7 @@ public ActionResult Create(CountryModel model, bool continueEditing) public ActionResult Edit(int id) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var country = _countryService.GetCountryById(id); @@ -215,7 +243,7 @@ public ActionResult Edit(int id) [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Edit(CountryModel model, bool continueEditing) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var country = _countryService.GetCountryById(model.Id); @@ -231,7 +259,7 @@ public ActionResult Edit(CountryModel model, bool continueEditing) _storeMappingService.SaveStoreMappings(country, model.SelectedStoreIds); - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Countries.Updated")); + NotifySuccess(T("Admin.Configuration.Countries.Updated")); return continueEditing ? RedirectToAction("Edit", new { id = country.Id }) : RedirectToAction("List"); } @@ -244,7 +272,7 @@ public ActionResult Edit(CountryModel model, bool continueEditing) [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var country = _countryService.GetCountryById(id); @@ -254,11 +282,12 @@ public ActionResult DeleteConfirmed(int id) try { if (_addressService.GetAddressTotalByCountryId(country.Id) > 0) - throw new SmartException("The country can't be deleted. It has associated addresses"); + throw new SmartException(T("Admin.Configuration.Countries.CannotDeleteDueToAssociatedAddresses")); _countryService.DeleteCountry(country); - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Countries.Deleted")); + NotifySuccess(T("Admin.Configuration.Countries.Deleted")); + return RedirectToAction("List"); } catch (Exception exc) @@ -268,7 +297,6 @@ public ActionResult DeleteConfirmed(int id) } } - #endregion #region States / provinces @@ -276,41 +304,49 @@ public ActionResult DeleteConfirmed(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult States(int countryId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) - return AccessDeniedView(); + var model = new GridModel(); - var states = _stateProvinceService.GetStateProvincesByCountryId(countryId, true) - .Select(x => x.ToModel()); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) + { + var states = _stateProvinceService.GetStateProvincesByCountryId(countryId, true) + .Select(x => x.ToModel()); + + model.Data = states; + model.Total = states.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var model = new GridModel - { - Data = states, - Total = states.Count() - }; return new JsonResult { Data = model }; } - //create public ActionResult StateCreatePopup(int countryId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); - var model = new StateProvinceModel(); - model.CountryId = countryId; - //locales + var model = new StateProvinceModel + { + CountryId = countryId + }; + AddLocales(_languageService, model.Locales); + return View(model); } [HttpPost] public ActionResult StateCreatePopup(string btnId, string formId, StateProvinceModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var country = _countryService.GetCountryById(model.CountryId); @@ -327,6 +363,7 @@ public ActionResult StateCreatePopup(string btnId, string formId, StateProvinceM ViewBag.RefreshPage = true; ViewBag.btnId = btnId; ViewBag.formId = formId; + return View(model); } @@ -337,16 +374,15 @@ public ActionResult StateCreatePopup(string btnId, string formId, StateProvinceM //edit public ActionResult StateEditPopup(int id) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var sp = _stateProvinceService.GetStateProvinceById(id); if (sp == null) - //No state found with the specified id return RedirectToAction("List"); var model = sp.ToModel(); - //locales + AddLocales(_languageService, model.Locales, (locale, languageId) => { locale.Name = sp.GetLocalized(x => x.Name, languageId, false, false); @@ -358,12 +394,11 @@ public ActionResult StateEditPopup(int id) [HttpPost] public ActionResult StateEditPopup(string btnId, string formId, StateProvinceModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) return AccessDeniedView(); var sp = _stateProvinceService.GetStateProvinceById(model.Id); if (sp == null) - //No state found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) @@ -376,6 +411,7 @@ public ActionResult StateEditPopup(string btnId, string formId, StateProvinceMod ViewBag.RefreshPage = true; ViewBag.btnId = btnId; ViewBag.formId = formId; + return View(model); } @@ -386,19 +422,18 @@ public ActionResult StateEditPopup(string btnId, string formId, StateProvinceMod [GridAction(EnableCustomBinding = true)] public ActionResult StateDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCountries)) - return AccessDeniedView(); - - var state = _stateProvinceService.GetStateProvinceById(id); - if (state == null) - throw new ArgumentException("No state found with the specified id"); + var state = _stateProvinceService.GetStateProvinceById(id); + var countryId = state.CountryId; - if (_addressService.GetAddressTotalByStateProvinceId(state.Id) > 0) - return Content(_localizationService.GetResource("Admin.Configuration.Countries.States.CantDeleteWithAddresses")); - - int countryId = state.CountryId; - _stateProvinceService.DeleteStateProvince(state); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCountries)) + { + if (_addressService.GetAddressTotalByStateProvinceId(state.Id) > 0) + { + return Content(T("Admin.Configuration.Countries.States.CantDeleteWithAddresses")); + } + _stateProvinceService.DeleteStateProvince(state); + } return States(countryId, command); } @@ -406,23 +441,20 @@ public ActionResult StateDelete(int id, GridCommand command) [AcceptVerbs(HttpVerbs.Get)] public ActionResult GetStatesByCountryId(string countryId, bool? addEmptyStateIfRequired, bool? addAsterisk) { - //permission validation is not required here - + // permission validation is not required here // This action method gets called via an ajax request - int cid = 0; - int.TryParse(countryId, out cid); - /*if (String.IsNullOrEmpty(countryId)) - throw new ArgumentNullException("countryId");*/ + var country = _countryService.GetCountryById(countryId.ToInt()); - var country = _countryService.GetCountryById(cid /* Convert.ToInt32(countryId) */); var states = country != null ? _stateProvinceService.GetStateProvincesByCountryId(country.Id, true).ToList() : new List(); - var result = (from s in states - select new { id = s.Id, name = s.Name }).ToList(); + var result = (from s in states select new { id = s.Id, name = s.Name }).ToList(); + if (addEmptyStateIfRequired.HasValue && addEmptyStateIfRequired.Value && result.Count == 0) - result.Insert(0, new { id = 0, name = _localizationService.GetResource("Admin.Address.OtherNonUS") }); + result.Insert(0, new { id = 0, name = T("Admin.Address.OtherNonUS").Text }); + if (addAsterisk.HasValue && addAsterisk.Value) result.Insert(0, new { id = 0, name = "*" }); + return Json(result, JsonRequestBehavior.AllowGet); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CurrencyController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CurrencyController.cs index 181071a2d2..e20b59013c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CurrencyController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CurrencyController.cs @@ -239,18 +239,25 @@ public ActionResult Save(FormCollection formValues) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageCurrencies)) - return AccessDeniedView(); + var model = new GridModel(); + + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageCurrencies)) + { + var currencies = _currencyService.GetAllCurrencies(true); + + model.Data = currencies.Select(x => x.ToModel()); + model.Total = currencies.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var currencies = _currencyService.GetAllCurrencies(true); - var gridModel = new GridModel - { - Data = currencies.Select(x => x.ToModel()), - Total = currencies.Count() - }; return new JsonResult { - Data = gridModel + Data = model }; } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs index 892bbaf5c6..44b6c10a0f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs @@ -396,28 +396,37 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult CustomerList(GridCommand command, CustomerListModel model, [ModelBinderAttribute(typeof(CommaSeparatedModelBinder))] int[] searchCustomerRoleIds) { - //we use own own binder for searchCustomerRoleIds property - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) - return AccessDeniedView(); + // we use own own binder for searchCustomerRoleIds property + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) + { + var searchDayOfBirth = 0; + var searchMonthOfBirth = 0; + + if (!String.IsNullOrWhiteSpace(model.SearchDayOfBirth)) + searchDayOfBirth = Convert.ToInt32(model.SearchDayOfBirth); + + if (!String.IsNullOrWhiteSpace(model.SearchMonthOfBirth)) + searchMonthOfBirth = Convert.ToInt32(model.SearchMonthOfBirth); + + var customers = _customerService.GetAllCustomers(null, null, + searchCustomerRoleIds, model.SearchEmail, model.SearchUsername, + model.SearchFirstName, model.SearchLastName, + searchDayOfBirth, searchMonthOfBirth, + model.SearchCompany, model.SearchPhone, model.SearchZipPostalCode, + false, null, command.Page - 1, command.PageSize); + + gridModel.Data = customers.Select(PrepareCustomerModelForList); + gridModel.Total = customers.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var searchDayOfBirth = 0; - int searchMonthOfBirth = 0; - if (!String.IsNullOrWhiteSpace(model.SearchDayOfBirth)) - searchDayOfBirth = Convert.ToInt32(model.SearchDayOfBirth); - if (!String.IsNullOrWhiteSpace(model.SearchMonthOfBirth)) - searchMonthOfBirth = Convert.ToInt32(model.SearchMonthOfBirth); - - var customers = _customerService.GetAllCustomers(null, null, - searchCustomerRoleIds, model.SearchEmail, model.SearchUsername, - model.SearchFirstName, model.SearchLastName, - searchDayOfBirth, searchMonthOfBirth, - model.SearchCompany, model.SearchPhone, model.SearchZipPostalCode, - false, null, command.Page - 1, command.PageSize); - var gridModel = new GridModel - { - Data = customers.Select(PrepareCustomerModelForList), - Total = customers.TotalCount - }; return new JsonResult { Data = gridModel @@ -965,19 +974,22 @@ public ActionResult ChangePassword(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) { - var changePassRequest = new ChangePasswordRequest(model.Email, - false, _customerSettings.DefaultPasswordFormat, model.Password); + var changePassRequest = new ChangePasswordRequest(model.Email, false, _customerSettings.DefaultPasswordFormat, model.Password); var changePassResult = _customerRegistrationService.ChangePassword(changePassRequest); - if (changePassResult.Success) - NotifySuccess(_localizationService.GetResource("Admin.Customers.Customers.PasswordChanged")); - else - foreach (var error in changePassResult.Errors) + + if (changePassResult.Success) + { + NotifySuccess(_localizationService.GetResource("Admin.Customers.Customers.PasswordChanged")); + } + else + { + foreach (var error in changePassResult.Errors) NotifyError(error); + } } return RedirectToAction("Edit", customer.Id); @@ -992,7 +1004,6 @@ public ActionResult MarkVatNumberAsValid(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); _genericAttributeService.SaveAttribute(customer, @@ -1011,7 +1022,6 @@ public ActionResult MarkVatNumberAsInvalid(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); _genericAttributeService.SaveAttribute(customer, @@ -1029,30 +1039,33 @@ public ActionResult Delete(int id) var customer = _customerService.GetCustomerById(id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); try { _customerService.DeleteCustomer(customer); - //remove newsletter subscriptions (if exists) - var subscriptions = _newsLetterSubscriptionService.GetAllNewsLetterSubscriptions(customer.Email, 0, int.MaxValue, true); - - foreach (var subscription in subscriptions) + if (customer.Email.HasValue()) { - _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); + foreach (var store in _storeService.GetAllStores()) + { + var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(customer.Email, store.Id); + if (subscription != null) + { + _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); + } + } } - //activity log - _customerActivityService.InsertActivity("DeleteCustomer", _localizationService.GetResource("ActivityLog.DeleteCustomer"), customer.Id); + _customerActivityService.InsertActivity("DeleteCustomer", T("ActivityLog.DeleteCustomer", customer.Id)); + + NotifySuccess(T("Admin.Customers.Customers.Deleted")); - NotifySuccess(_localizationService.GetResource("Admin.Customers.Customers.Deleted")); return RedirectToAction("List"); } - catch (Exception exc) + catch (Exception exception) { - NotifyError(exc.Message); + NotifyError(exception.Message); return RedirectToAction("Edit", new { id = customer.Id }); } } @@ -1066,7 +1079,6 @@ public ActionResult Impersonate(int id) var customer = _customerService.GetCustomerById(id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); // ensure that a non-admin user cannot impersonate as an administrator @@ -1090,7 +1102,6 @@ public ActionResult SendEmail(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); try @@ -1106,7 +1117,7 @@ public ActionResult SendEmail(CustomerModel model) var emailAccount = _emailAccountService.GetDefaultEmailAccount(); if (emailAccount == null) - throw new SmartException("Email account can't be loaded"); + throw new SmartException(T("Common.Error.NoEmailAccount")); var email = new QueuedEmail { @@ -1119,8 +1130,10 @@ public ActionResult SendEmail(CustomerModel model) Body = model.SendEmail.Body, CreatedOnUtc = DateTime.UtcNow, }; - _queuedEmailService.InsertQueuedEmail(email); - NotifySuccess(_localizationService.GetResource("Admin.Customers.Customers.SendEmail.Queued")); + + _queuedEmailService.InsertQueuedEmail(email); + + NotifySuccess(T("Admin.Customers.Customers.SendEmail.Queued")); } catch (Exception exc) { @@ -1137,19 +1150,18 @@ public ActionResult SendPm(CustomerModel model) var customer = _customerService.GetCustomerById(model.Id); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); try { if (!_forumSettings.AllowPrivateMessages) - throw new SmartException("Private messages are disabled"); + throw new SmartException(T("PrivateMessages.Disabled")); if (customer.IsGuest()) - throw new SmartException("Customer should be registered"); + throw new SmartException(T("Common.MethodNotSupportedForGuests")); if (String.IsNullOrWhiteSpace(model.SendPm.Subject)) - throw new SmartException("PM subject is empty"); + throw new SmartException(T("Admin.Customers.Customers.SendPM.Subject.Hint")); if (String.IsNullOrWhiteSpace(model.SendPm.Message)) - throw new SmartException("PM message is empty"); + throw new SmartException(T("Admin.Customers.Customers.SendPM.Message.Hint")); var privateMessage = new PrivateMessage @@ -1301,7 +1313,6 @@ public ActionResult AddressCreate(int customerId) var customer = _customerService.GetCustomerById(customerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); var model = new CustomerAddressModel(); @@ -1346,7 +1357,6 @@ public ActionResult AddressCreate(CustomerAddressModel model) var customer = _customerService.GetCustomerById(model.CustomerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) @@ -1395,12 +1405,10 @@ public ActionResult AddressEdit(int addressId, int customerId) var customer = _customerService.GetCustomerById(customerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); var address = _addressService.GetAddressById(addressId); if (address == null) - //No address found with the specified id return RedirectToAction("Edit", new { id = customer.Id }); var model = new CustomerAddressModel(); @@ -1453,12 +1461,10 @@ public ActionResult AddressEdit(CustomerAddressModel model) var customer = _customerService.GetCustomerById(model.CustomerId); if (customer == null) - //No customer found with the specified id return RedirectToAction("List"); var address = _addressService.GetAddressById(model.Address.Id); if (address == null) - //No address found with the specified id return RedirectToAction("Edit", new { id = customer.Id }); if (ModelState.IsValid) @@ -1519,17 +1525,15 @@ public ActionResult AddressEdit(CustomerAddressModel model) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult OrderList(int customerId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) - return AccessDeniedView(); + var model = new GridModel(); - var orders = _orderService.SearchOrders(0, customerId, - null, null, null, null, null, null, null, null, 0, int.MaxValue); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) + { + var orders = _orderService.SearchOrders(0, customerId, null, null, null, null, null, null, null, null, 0, int.MaxValue); - var model = new GridModel - { - Data = orders.PagedForCommand(command) - .Select(order => - { + model.Data = orders.PagedForCommand(command) + .Select(order => + { var store = _storeService.GetStoreById(order.StoreId); var orderModel = new CustomerModel.OrderModel() { @@ -1538,13 +1542,20 @@ public ActionResult OrderList(int customerId, GridCommand command) PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_localizationService, _workContext), ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_localizationService, _workContext), OrderTotal = _priceFormatter.FormatPrice(order.OrderTotal, true, false), - StoreName = store != null ? store.Name : "Unknown", + StoreName = store != null ? store.Name : "".NaIfEmpty(), CreatedOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc), }; - return orderModel; - }), - Total = orders.Count - }; + return orderModel; + }); + + model.Total = orders.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1562,23 +1573,18 @@ public ActionResult Reports() return AccessDeniedView(); var model = new CustomerReportsModel(); + //customers by number of orders model.BestCustomersByNumberOfOrders = new BestCustomersReportModel(); model.BestCustomersByNumberOfOrders.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); - model.BestCustomersByNumberOfOrders.AvailableOrderStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); model.BestCustomersByNumberOfOrders.AvailablePaymentStatuses = PaymentStatus.Pending.ToSelectList(false).ToList(); - model.BestCustomersByNumberOfOrders.AvailablePaymentStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); model.BestCustomersByNumberOfOrders.AvailableShippingStatuses = ShippingStatus.NotYetShipped.ToSelectList(false).ToList(); - model.BestCustomersByNumberOfOrders.AvailableShippingStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); //customers by order total model.BestCustomersByOrderTotal = new BestCustomersReportModel(); model.BestCustomersByOrderTotal.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); - model.BestCustomersByOrderTotal.AvailableOrderStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); model.BestCustomersByOrderTotal.AvailablePaymentStatuses = PaymentStatus.Pending.ToSelectList(false).ToList(); - model.BestCustomersByOrderTotal.AvailablePaymentStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); model.BestCustomersByOrderTotal.AvailableShippingStatuses = ShippingStatus.NotYetShipped.ToSelectList(false).ToList(); - model.BestCustomersByOrderTotal.AvailableShippingStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); return View(model); } @@ -1596,9 +1602,8 @@ public ActionResult ReportBestCustomersByOrderTotalList(GridCommand command, Bes PaymentStatus? paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)(model.PaymentStatusId) : null; ShippingStatus? shippingStatus = model.ShippingStatusId > 0 ? (ShippingStatus?)(model.ShippingStatusId) : null; + var items = _customerReportService.GetBestCustomersReport(startDateValue, endDateValue, orderStatus, paymentStatus, shippingStatus, 1); - var items = _customerReportService.GetBestCustomersReport(startDateValue, endDateValue, - orderStatus, paymentStatus, shippingStatus, 1); var gridModel = new GridModel { Data = items.Select(x => @@ -1609,23 +1614,25 @@ public ActionResult ReportBestCustomersByOrderTotalList(GridCommand command, Bes OrderTotal = _priceFormatter.FormatPrice(x.OrderTotal, true, false), OrderCount = x.OrderCount, }; + var customer = _customerService.GetCustomerById(x.CustomerId); if (customer != null) { - m.CustomerName = customer.IsGuest() - ? _localizationService.GetResource("Admin.Customers.Guest") - : customer.Email; + m.CustomerName = customer.IsGuest() ? T("Admin.Customers.Guest").Text : customer.Email; } + return m; }), Total = items.Count }; + return new JsonResult { Data = gridModel }; } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult ReportBestCustomersByNumberOfOrdersList(GridCommand command, BestCustomersReportModel model) { DateTime? startDateValue = (model.StartDate == null) ? null @@ -1638,26 +1645,25 @@ public ActionResult ReportBestCustomersByNumberOfOrdersList(GridCommand command, PaymentStatus? paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)(model.PaymentStatusId) : null; ShippingStatus? shippingStatus = model.ShippingStatusId > 0 ? (ShippingStatus?)(model.ShippingStatusId) : null; + var items = _customerReportService.GetBestCustomersReport(startDateValue, endDateValue, orderStatus, paymentStatus, shippingStatus, 2); - var items = _customerReportService.GetBestCustomersReport(startDateValue, endDateValue, - orderStatus, paymentStatus, shippingStatus, 2); var gridModel = new GridModel { Data = items.Select(x => { - var m = new BestCustomerReportLineModel() + var m = new BestCustomerReportLineModel { CustomerId = x.CustomerId, OrderTotal = _priceFormatter.FormatPrice(x.OrderTotal, true, false), OrderCount = x.OrderCount, }; + var customer = _customerService.GetCustomerById(x.CustomerId); if (customer != null) { - m.CustomerName = customer.IsGuest() - ? _localizationService.GetResource("Admin.Customers.Guest") - : customer.Email; + m.CustomerName = customer.IsGuest() ? T("Admin.Customers.Guest").Text : customer.Email; } + return m; }), Total = items.Count @@ -1674,15 +1680,18 @@ public ActionResult ReportRegisteredCustomers() var model = GetReportRegisteredCustomersModel(); return PartialView(model); } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult ReportRegisteredCustomersList(GridCommand command) { var model = GetReportRegisteredCustomersModel(); - var gridModel = new GridModel + + var gridModel = new GridModel { Data = model, Total = model.Count }; + return new JsonResult { Data = gridModel @@ -1699,16 +1708,17 @@ public ActionResult GetCartList(int customerId, int cartTypeId) var customer = _customerService.GetCustomerById(customerId); var cart = customer.GetCartItems((ShoppingCartType)cartTypeId); - var gridModel = new GridModel() + var gridModel = new GridModel { Data = cart.Select(sci => { decimal taxRate; var store = _storeService.GetStoreById(sci.Item.StoreId); - var sciModel = new ShoppingCartItemModel() + + var sciModel = new ShoppingCartItemModel { Id = sci.Item.Id, - Store = store != null ? store.Name : "Unknown", + Store = store != null ? store.Name : "".NaIfEmpty(), ProductId = sci.Item.ProductId, Quantity = sci.Item.Quantity, ProductName = sci.Item.Product.Name, @@ -1722,6 +1732,7 @@ public ActionResult GetCartList(int customerId, int cartTypeId) }), Total = cart.Count }; + return new JsonResult { Data = gridModel @@ -1736,11 +1747,12 @@ public ActionResult GetCartList(int customerId, int cartTypeId) public JsonResult ListActivityLog(GridCommand command, int customerId) { var activityLog = _customerActivityService.GetAllActivities(null, null, customerId, 0, command.Page - 1, command.PageSize); + var gridModel = new GridModel { Data = activityLog.Select(x => { - var m = new CustomerModel.ActivityLogModel() + var m = new CustomerModel.ActivityLogModel { Id = x.Id, ActivityLogTypeName = x.ActivityLogType.Name, @@ -1748,11 +1760,11 @@ public JsonResult ListActivityLog(GridCommand command, int customerId) CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) }; return m; - }), Total = activityLog.TotalCount }; - return new JsonResult { Data = gridModel }; ; + + return new JsonResult { Data = gridModel }; } #endregion diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerRoleController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerRoleController.cs index 7cc7fa5575..6bb2d10b47 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerRoleController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerRoleController.cs @@ -102,18 +102,25 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCustomerRoles)) - return AccessDeniedView(); - - var customerRoles = _customerService.GetAllCustomerRoles(true); - var gridModel = new GridModel + var model = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageCustomerRoles)) { - Data = customerRoles.Select(x => x.ToModel()), - Total = customerRoles.Count() - }; + var customerRoles = _customerService.GetAllCustomerRoles(true); + + model.Data = customerRoles.Select(x => x.ToModel()); + model.Total = customerRoles.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { - Data = gridModel + Data = model }; } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/DiscountController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/DiscountController.cs index 6e18b8ec06..c9ab6e4496 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/DiscountController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/DiscountController.cs @@ -138,7 +138,6 @@ private void PrepareDiscountModel(DiscountModel model, Discount discount) #region Discounts - //list public ActionResult Index() { return RedirectToAction("List"); @@ -161,22 +160,28 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) - return AccessDeniedView(); + var model = new GridModel(); + + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) + { + var discounts = _discountService.GetAllDiscounts(null, null, true); + + model.Data = discounts.Select(x => x.ToModel()); + model.Total = discounts.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var discounts = _discountService.GetAllDiscounts(null, null, true); - var gridModel = new GridModel - { - Data = discounts.Select(x => x.ToModel()), - Total = discounts.Count() - }; return new JsonResult { - Data = gridModel + Data = model }; } - //create public ActionResult Create() { if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) @@ -212,7 +217,6 @@ public ActionResult Create(DiscountModel model, bool continueEditing) return View(model); } - //edit public ActionResult Edit(int id) { if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) @@ -281,7 +285,6 @@ public ActionResult Edit(DiscountModel model, bool continueEditing) return View(model); } - //delete [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { @@ -386,28 +389,31 @@ public ActionResult DeleteDiscountRequirement(int discountRequirementId, int dis [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult UsageHistoryList(int discountId, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) - return AccessDeniedView(); + var model = new GridModel(); - var discount = _discountService.GetDiscountById(discountId); - if (discount == null) - throw new ArgumentException("No discount found with the specified id"); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) + { + var discount = _discountService.GetDiscountById(discountId); + + var discountHistories = _discountService.GetAllDiscountUsageHistory(discount.Id, null, command.Page - 1, command.PageSize); + + model.Data = discountHistories.Select(x => new DiscountModel.DiscountUsageHistoryModel + { + Id = x.Id, + DiscountId = x.DiscountId, + OrderId = x.OrderId, + CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) + }); + + model.Total = discountHistories.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var duh = _discountService.GetAllDiscountUsageHistory(discount.Id, null, command.Page - 1, command.PageSize); - var model = new GridModel - { - Data = duh.Select(x => - { - return new DiscountModel.DiscountUsageHistoryModel - { - Id = x.Id, - DiscountId = x.DiscountId, - OrderId = x.OrderId, - CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) - }; - }), - Total = duh.TotalCount - }; return new JsonResult { Data = model @@ -417,16 +423,13 @@ public ActionResult UsageHistoryList(int discountId, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult UsageHistoryDelete(int discountId, int id, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageDiscounts)) + { + var discountHistory = _discountService.GetDiscountUsageHistoryById(id); + + _discountService.DeleteDiscountUsageHistory(discountHistory); + } - var discount = _discountService.GetDiscountById(discountId); - if (discount == null) - throw new ArgumentException("No discount found with the specified id"); - - var duh = _discountService.GetDiscountUsageHistoryById(id); - if (duh != null) - _discountService.DeleteDiscountUsageHistory(duh); return UsageHistoryList(discountId, command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs index 7a6d174ad1..c215d9b7e9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/DownloadController.cs @@ -73,7 +73,7 @@ public ActionResult AsyncUpload(bool minimalMode = false, string fieldName = nul var postedFile = Request.ToPostedFileResult(); if (postedFile == null) { - throw new ArgumentException("No file uploaded"); + throw new ArgumentException(T("Common.NoFileUploaded")); } var download = new Download diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs index c883d5f266..2d6716d284 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/EmailAccountController.cs @@ -1,11 +1,8 @@ using System; using System.Linq; -using System.Net.Mail; -using System.Threading.Tasks; using System.Web.Mvc; using SmartStore.Admin.Models.Messages; using SmartStore.Core; -using SmartStore.Core.Domain; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Email; using SmartStore.Services.Configuration; @@ -78,24 +75,32 @@ public ActionResult List(string id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageEmailAccounts)) - return AccessDeniedView(); + var model = new GridModel(); - var emailAccountModels = _emailAccountService.GetAllEmailAccounts() - .Select(x => x.ToModel()) - .ToList(); - foreach (var eam in emailAccountModels) - eam.IsDefaultEmailAccount = eam.Id == _emailAccountSettings.DefaultEmailAccountId; + if (_permissionService.Authorize(StandardPermissionProvider.ManageEmailAccounts)) + { + var emailAccountModels = _emailAccountService.GetAllEmailAccounts() + .Select(x => x.ToModel()) + .ToList(); - var gridModel = new GridModel - { - Data = emailAccountModels, - Total = emailAccountModels.Count() - }; + foreach (var eam in emailAccountModels) + { + eam.IsDefaultEmailAccount = eam.Id == _emailAccountSettings.DefaultEmailAccountId; + } + + model.Data = emailAccountModels; + model.Total = emailAccountModels.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { - Data = gridModel + Data = model }; } @@ -176,28 +181,32 @@ public ActionResult SendTestEmail(EmailAccountModel model) var emailAccount = _emailAccountService.GetEmailAccountById(model.Id); if (emailAccount == null) - //No email account found with the specified id return RedirectToAction("List"); try { - if (String.IsNullOrWhiteSpace(model.SendTestEmailTo)) - throw new SmartException("Enter test email address"); + if (model.SendTestEmailTo.IsEmpty()) + { + NotifyError(T("Admin.Common.EnterEmailAdress")); + } + else + { + var to = new EmailAddress(model.SendTestEmailTo); + var from = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + var subject = string.Concat(_storeContext.CurrentStore.Name, ". ", T("Admin.Configuration.EmailAccounts.TestingEmail")); + var body = T("Admin.Common.EmailSuccessfullySent"); - var to = new EmailAddress(model.SendTestEmailTo); - var from = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); - string subject = _storeContext.CurrentStore.Name + ". Testing email functionality."; - string body = "Email works fine."; + var msg = new EmailMessage(to, subject, body, from); - var msg = new EmailMessage(to, subject, body, from); - - _emailSender.SendEmail(new SmtpContext(emailAccount), msg); + _emailSender.SendEmail(new SmtpContext(emailAccount), msg); - NotifySuccess(_localizationService.GetResource("Admin.Configuration.EmailAccounts.SendTestEmail.Success"), false); + NotifySuccess(T("Admin.Configuration.EmailAccounts.SendTestEmail.Success"), false); + } } - catch (Exception exc) + catch (Exception exception) { - NotifyError(exc.Message, false); + model.TestEmailShortErrorMessage = exception.ToAllMessages(); + model.TestEmailFullErrorMessage = exception.ToString(); } //If we got this far, something failed, redisplay form diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs index 904d8a4089..95b26a0da0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ExportController.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Web; +using System.Net.Mime; +using System.Text; using System.Web.Mvc; using SmartStore.Admin.Extensions; using SmartStore.Admin.Models.DataExchange; @@ -18,10 +19,10 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.IO; using SmartStore.Core.Plugins; -using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Customers; using SmartStore.Services.DataExchange.Export; +using SmartStore.Services.DataExchange.Export.Deployment; using SmartStore.Services.Directory; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; @@ -41,7 +42,6 @@ namespace SmartStore.Admin.Controllers [AdminAuthorize] public class ExportController : AdminControllerBase { - private readonly ICommonServices _services; private readonly IExportProfileService _exportService; private readonly PluginMediator _pluginMediator; private readonly ICategoryService _categoryService; @@ -51,6 +51,7 @@ public class ExportController : AdminControllerBase private readonly ILanguageService _languageService; private readonly ICurrencyService _currencyService; private readonly IEmailAccountService _emailAccountService; + private readonly ICountryService _countryService; private readonly IDateTimeHelper _dateTimeHelper; private readonly DataExchangeSettings _dataExchangeSettings; private readonly ITaskScheduler _taskScheduler; @@ -58,7 +59,6 @@ public class ExportController : AdminControllerBase private readonly Lazy _customerSettings; public ExportController( - ICommonServices services, IExportProfileService exportService, PluginMediator pluginMediator, ICategoryService categoryService, @@ -68,13 +68,13 @@ public ExportController( ILanguageService languageService, ICurrencyService currencyService, IEmailAccountService emailAccountService, + ICountryService countryService, IDateTimeHelper dateTimeHelper, DataExchangeSettings dataExchangeSettings, ITaskScheduler taskScheduler, IDataExporter dataExporter, Lazy customerSettings) { - _services = services; _exportService = exportService; _pluginMediator = pluginMediator; _categoryService = categoryService; @@ -84,6 +84,7 @@ public ExportController( _languageService = languageService; _currencyService = currencyService; _emailAccountService = emailAccountService; + _countryService = countryService; _dateTimeHelper = dateTimeHelper; _dataExchangeSettings = dataExchangeSettings; _taskScheduler = taskScheduler; @@ -108,42 +109,142 @@ private string GetThumbnailUrl(Provider provider) return url; } - private ExportProfileDetailsModel PrepareProfileDetailsModel(ExportProfile profile, bool forEdit) + private void ModelToEntity(ExportDeploymentModel model, ExportDeployment deployment) + { + deployment.ProfileId = model.ProfileId; + deployment.Name = model.Name; + deployment.Enabled = model.Enabled; + deployment.DeploymentType = model.DeploymentType; + deployment.Username = model.Username; + deployment.Password = model.Password; + deployment.Url = model.Url; + deployment.HttpTransmissionType = model.HttpTransmissionType; + deployment.FileSystemPath = model.FileSystemPath; + deployment.SubFolder = model.SubFolder; + deployment.EmailAddresses = model.EmailAddresses; + deployment.EmailSubject = model.EmailSubject; + deployment.EmailAccountId = model.EmailAccountId; + deployment.PassiveMode = model.PassiveMode; + deployment.UseSsl = model.UseSsl; + } + + private ActionResult SmartRedirect(bool continueEditing, int profileId, int deploymentId) { - var model = new ExportProfileDetailsModel + if (!continueEditing) { - Id = profile.Id, - PublicFiles = new List() + TempData["SelectedTab.export-profile-edit"] = new SelectedTabInfo + { + TabId = "export-profile-edit-6", + Path = Url.Action("Edit", new { id = profileId }) + }; + } + + return (continueEditing ? + RedirectToAction("EditDeployment", new { id = deploymentId }) : + RedirectToAction("Edit", new { id = profileId })); + } + + private void AddFileInfo(List list, string path, string publicFolderUrl = null, Store store = null) + { + if (System.IO.File.Exists(path) && !list.Any(x => x.FilePath == path)) + { + var fileInfo = new ExportFileDetailsModel.FileInfo(); + fileInfo.FilePath = path; + fileInfo.FileName = Path.GetFileName(path); + fileInfo.FileExtension = Path.GetExtension(path); + fileInfo.DisplayOrder = (fileInfo.FileExtension.IsCaseInsensitiveEqual(".zip") ? 0 : 1); + + if (store != null) + { + fileInfo.StoreId = store.Id; + fileInfo.StoreName = store.Name; + } + + if (publicFolderUrl.HasValue()) + { + fileInfo.FileUrl = publicFolderUrl + fileInfo.FileName; + } + + list.Add(fileInfo); + } + } + + private ExportFileDetailsModel CreateFileDetailsModel(ExportProfile profile, Provider provider, ExportDeployment deployment) + { + var model = new ExportFileDetailsModel + { + Id = (deployment == null ? profile.Id : deployment.Id), + IsForDeployment = (deployment != null), + ExportFiles = new List(), + PublicFiles = new List() }; try { + // add export files var zipPath = profile.GetExportZipPath(); - model.ZipPath = (System.IO.File.Exists(zipPath) ? zipPath : null); - model.ExportFiles = profile.GetExportFiles(); + if (deployment == null) + { + AddFileInfo(model.ExportFiles, zipPath); - if (forEdit && profile.Deployments.Any(x => x.IsPublic && x.DeploymentType == ExportDeploymentType.FileSystem)) + foreach (var path in profile.GetExportFiles(provider)) + { + AddFileInfo(model.ExportFiles, path); + } + } + else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) { - var allStores = _services.StoreService.GetAllStores(); - var publicFolder = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); + var deploymentFolder = deployment.GetDeploymentFolder(); var resultInfo = XmlHelper.Deserialize(profile.ResultInfo); - if (resultInfo != null && resultInfo.Files != null) + if (resultInfo.Files != null) + { + foreach (var file in resultInfo.Files) + { + AddFileInfo(model.ExportFiles, Path.Combine(deploymentFolder, file.FileName)); + } + } + } + + // add public files + ExportDeployment publicDeployment = null; + + if (deployment == null) + publicDeployment = profile.Deployments.FirstOrDefault(x => x.DeploymentType == ExportDeploymentType.PublicFolder); + else + publicDeployment = (deployment.DeploymentType == ExportDeploymentType.PublicFolder ? deployment : null); + + if (publicDeployment != null) + { + var currentStore = Services.StoreContext.CurrentStore; + var deploymentFolder = publicDeployment.GetDeploymentFolder(); + + // note public folder not cleaned up during export, so only display files that has been created during last export. + // otherwise the merchant might publish URLs of old export files. + if (profile.CreateZipArchive) + { + AddFileInfo( + model.PublicFiles, + Path.Combine(deploymentFolder, Path.GetFileName(zipPath)), + publicDeployment.GetPublicFolderUrl(Services, currentStore)); + } + else { - foreach (var fileInfo in resultInfo.Files) + var resultInfo = XmlHelper.Deserialize(profile.ResultInfo); + if (resultInfo.Files != null) { - if (System.IO.File.Exists(Path.Combine(publicFolder, fileInfo.FileName)) && !model.PublicFiles.Any(x => x.FileName == fileInfo.FileName)) + var allStores = Services.StoreService.GetAllStores(); + + foreach (var file in resultInfo.Files) { - var store = allStores.FirstOrDefault(y => y.Id == fileInfo.StoreId) ?? _services.StoreContext.CurrentStore; - - model.PublicFiles.Add(new ExportProfileDetailsModel.PublicFile - { - StoreId = store.Id, - StoreName = store.Name, - FileName = fileInfo.FileName, - FileUrl = string.Concat(store.Url.EnsureEndsWith("/"), DataExporter.PublicFolder.EnsureEndsWith("/"), fileInfo.FileName) - }); + var store = (file.StoreId == 0 ? null : allStores.FirstOrDefault(x => x.Id == file.StoreId)); + + AddFileInfo( + model.PublicFiles, + Path.Combine(deploymentFolder, file.FileName), + publicDeployment.GetPublicFolderUrl(Services, store ?? currentStore), + store); } } } @@ -157,7 +258,61 @@ private ExportProfileDetailsModel PrepareProfileDetailsModel(ExportProfile profi return model; } - private void PrepareProfileModel(ExportProfileModel model, ExportProfile profile, Provider provider, bool forEdit) + private ExportDeploymentModel CreateDeploymentModel(ExportProfile profile, ExportDeployment deployment, Provider provider, bool forEdit) + { + var model = new ExportDeploymentModel + { + Id = deployment.Id, + ProfileId = deployment.ProfileId, + Name = deployment.Name, + Enabled = deployment.Enabled, + DeploymentType = deployment.DeploymentType, + DeploymentTypeName = deployment.DeploymentType.GetLocalizedEnum(Services.Localization, Services.WorkContext), + Username = deployment.Username, + Password = deployment.Password, + Url = deployment.Url, + HttpTransmissionType = deployment.HttpTransmissionType, + FileSystemPath = deployment.FileSystemPath, + SubFolder = deployment.SubFolder, + EmailAddresses = deployment.EmailAddresses, + EmailSubject = deployment.EmailSubject, + EmailAccountId = deployment.EmailAccountId, + PassiveMode = deployment.PassiveMode, + UseSsl = deployment.UseSsl + }; + + model.PublicFolderUrl = deployment.GetPublicFolderUrl(Services); + + if (forEdit) + { + var allEmailAccounts = _emailAccountService.GetAllEmailAccounts(); + + model.CreateZip = profile.CreateZipArchive; + model.AvailableDeploymentTypes = ExportDeploymentType.FileSystem.ToSelectList(false).ToList(); + model.AvailableHttpTransmissionTypes = ExportHttpTransmissionType.SimplePost.ToSelectList(false).ToList(); + + model.SerializedEmailAddresses = string.Join(",", deployment.EmailAddresses.SplitSafe(",").Select(x => x.EncodeJsString())); + + model.AvailableEmailAccounts = allEmailAccounts + .Select(x => new SelectListItem { Text = x.FriendlyName, Value = x.Id.ToString() }) + .ToList(); + + if (provider != null) + { + model.ThumbnailUrl = GetThumbnailUrl(provider); + } + } + else + { + var fileDetailsModel = CreateFileDetailsModel(profile, null, deployment); + + model.FileCount = fileDetailsModel.FileCount; + } + + return model; + } + + private void PrepareProfileModel(ExportProfileModel model, ExportProfile profile, Provider provider) { model.Id = profile.Id; model.Name = profile.Name; @@ -178,34 +333,31 @@ private void PrepareProfileModel(ExportProfileModel model, ExportProfile profile model.Provider = new ExportProfileModel.ProviderModel(); model.Provider.ThumbnailUrl = GetThumbnailUrl(provider); - model.Details = PrepareProfileDetailsModel(profile, forEdit); + var descriptor = provider.Metadata.PluginDescriptor; - if (provider != null) + if (descriptor != null) { - var descriptor = provider.Metadata.PluginDescriptor; - - if (descriptor != null) - { - model.Provider.Url = descriptor.Url; - model.Provider.ConfigurationUrl = Url.Action("ConfigurePlugin", "Plugin", new { systemName = descriptor.SystemName, area = "Admin" }); - model.Provider.Author = descriptor.Author; - model.Provider.Version = descriptor.Version.ToString(); - } - - model.Provider.FriendlyName = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); - model.Provider.Description = _pluginMediator.GetLocalizedDescription(provider.Metadata); - model.Provider.EntityType = provider.Value.EntityType; - model.Provider.EntityTypeName = provider.Value.EntityType.GetLocalizedEnum(_services.Localization, _services.WorkContext); - model.Provider.FileExtension = provider.Value.FileExtension; + model.Provider.Url = descriptor.Url; + model.Provider.Author = descriptor.Author; + model.Provider.Version = descriptor.Version.ToString(); } - } + + model.Provider.FriendlyName = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); + model.Provider.Description = _pluginMediator.GetLocalizedDescription(provider.Metadata); + model.Provider.EntityType = provider.Value.EntityType; + model.Provider.EntityTypeName = provider.Value.EntityType.GetLocalizedEnum(Services.Localization, Services.WorkContext); + model.Provider.FileExtension = provider.Value.FileExtension; + } private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile profile, Provider provider) { var filter = XmlHelper.Deserialize(profile.Filtering); var projection = XmlHelper.Deserialize(profile.Projection); - var allStores = _services.StoreService.GetAllStores(); + var language = Services.WorkContext.WorkingLanguage; + var store = Services.StoreContext.CurrentStore; + + var allStores = Services.StoreService.GetAllStores(); var allLanguages = _languageService.GetAllLanguages(true); var allCurrencies = _currencyService.GetAllCurrencies(true); var allEmailAccounts = _emailAccountService.GetAllEmailAccounts(); @@ -214,15 +366,15 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile model.UnspecifiedString = T("Common.Unspecified"); model.StoreCount = allStores.Count; model.Offset = profile.Offset; - model.Limit = profile.Limit; - model.BatchSize = profile.BatchSize; + model.Limit = (profile.Limit == 0 ? (int?)null : profile.Limit); + model.BatchSize = (profile.BatchSize == 0 ? (int?)null : profile.BatchSize); model.PerStore = profile.PerStore; model.EmailAccountId = profile.EmailAccountId; model.CompletedEmailAddresses = profile.CompletedEmailAddresses; model.CreateZipArchive = profile.CreateZipArchive; model.Cleanup = profile.Cleanup; - model.FileNamePatternExample = profile.ResolveFileNamePattern(_services.StoreContext.CurrentStore, 1, _dataExchangeSettings.MaxFileNameLength); + model.FileNamePatternExample = profile.ResolveFileNamePattern(store, 1, _dataExchangeSettings.MaxFileNameLength); model.AvailableEmailAccounts = allEmailAccounts .Select(x => new SelectListItem { Text = x.FriendlyName, Value = x.Id.ToString() }) @@ -237,6 +389,7 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile LanguageId = projection.LanguageId, CurrencyId = projection.CurrencyId, CustomerId = projection.CustomerId, + NumberOfPictures = projection.NumberOfPictures, DescriptionMergingId = projection.DescriptionMergingId, DescriptionToPlainText = projection.DescriptionToPlainText, AppendDescriptionText = projection.AppendDescriptionText, @@ -284,13 +437,22 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile WithoutManufacturers = filter.WithoutManufacturers, ProductTagId = filter.ProductTagId, FeaturedProducts = filter.FeaturedProducts, + IsActiveCustomer = filter.IsActiveCustomer, + IsTaxExempt = filter.IsTaxExempt, + BillingCountryIds = filter.BillingCountryIds, + ShippingCountryIds = filter.ShippingCountryIds, + LastActivityFrom = filter.LastActivityFrom, + LastActivityTo = filter.LastActivityTo, + HasSpentAtLeastAmount = filter.HasSpentAtLeastAmount, + HasPlacedAtLeastOrders = filter.HasPlacedAtLeastOrders, ProductType = filter.ProductType, IdMinimum = filter.IdMinimum, IdMaximum = filter.IdMaximum, OrderStatusIds = filter.OrderStatusIds, PaymentStatusIds = filter.PaymentStatusIds, ShippingStatusIds = filter.ShippingStatusIds, - CustomerRoleIds = filter.CustomerRoleIds + CustomerRoleIds = filter.CustomerRoleIds, + IsActiveSubscriber = filter.IsActiveSubscriber }; model.Filter.AvailableStores = allStores @@ -301,10 +463,19 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile model.Deployments = profile.Deployments .Select(x => { - var deploymentModel = PrepareDeploymentModel(x, null, false); + var deploymentModel = CreateDeploymentModel(profile, x, null, false); - deploymentModel.ProfileDetails = PrepareProfileDetailsModel(profile, true); + if (x.ResultInfo.HasValue()) + { + var resultInfo = XmlHelper.Deserialize(x.ResultInfo); + deploymentModel.LastResult = new ExportDeploymentModel.LastResultInfo + { + Execution = _dateTimeHelper.ConvertToUserTime(resultInfo.LastExecutionUtc, DateTimeKind.Utc), + ExecutionPretty = resultInfo.LastExecutionUtc.RelativeFormat(true, "f"), + Error = resultInfo.LastError + }; + } return deploymentModel; }) .ToList(); @@ -321,10 +492,15 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile var allManufacturers = _manufacturerService.GetAllManufacturers(true); var allProductTags = _productTagService.GetAllProductTags(); - model.Projection.AvailableDescriptionMergings = ExportDescriptionMerging.Description.ToSelectList(false); - model.Projection.AvailablePriceTypes = PriceDisplayType.LowestPrice.ToSelectList(false); model.Projection.AvailableAttributeCombinationValueMerging = ExportAttributeValueMerging.AppendAllValuesToName.ToSelectList(false); + model.Projection.AvailableDescriptionMergings = ExportDescriptionMerging.Description.ToSelectList(false); + + model.Projection.AvailablePriceTypes = PriceDisplayType.LowestPrice + .ToSelectList(false) + .Where(x => x.Value != ((int)PriceDisplayType.Hide).ToString()) + .ToList(); + model.Projection.SerializedAppendDescriptionText = string.Join(",", projection.AppendDescriptionText.SplitSafe(",").Select(x => x.EncodeJsString())); model.Projection.SerializedCriticalCharacters = string.Join(",", projection.CriticalCharacters.SplitSafe(",").Select(x => x.EncodeJsString())); @@ -343,20 +519,27 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile .ToList(); } - else if (model.Provider.EntityType == ExportEntityType.Order) + else if (model.Provider.EntityType == ExportEntityType.Customer) { var allCustomerRoles = _customerService.GetAllCustomerRoles(true); + var allCountries = _countryService.GetAllCountries(true); + model.Filter.AvailableCustomerRoles = allCustomerRoles + .OrderBy(x => x.Name) + .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) + .ToList(); + + model.Filter.AvailableCountries = allCountries + .Select(x => new SelectListItem { Text = x.GetLocalized(y => y.Name, language.Id, true, false), Value = x.Id.ToString() }) + .ToList(); + } + else if (model.Provider.EntityType == ExportEntityType.Order) + { model.Projection.AvailableOrderStatusChange = ExportOrderStatusChange.Processing.ToSelectList(false); model.Filter.AvailableOrderStates = OrderStatus.Pending.ToSelectList(false).ToList(); model.Filter.AvailablePaymentStates = PaymentStatus.Pending.ToSelectList(false).ToList(); model.Filter.AvailableShippingStates = ShippingStatus.NotYetShipped.ToSelectList(false).ToList(); - - model.Filter.AvailableCustomerRoles = allCustomerRoles - .OrderBy(x => x.Name) - .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) - .ToList(); } try @@ -388,88 +571,6 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile } } - private ExportDeploymentModel PrepareDeploymentModel(ExportDeployment deployment, Provider provider, bool forEdit) - { - var model = new ExportDeploymentModel - { - Id = deployment.Id, - ProfileId = deployment.ProfileId, - Name = deployment.Name, - Enabled = deployment.Enabled, - IsPublic = deployment.IsPublic, - CreateZip = deployment.CreateZip, - DeploymentType = deployment.DeploymentType, - DeploymentTypeName = deployment.DeploymentType.GetLocalizedEnum(_services.Localization, _services.WorkContext), - Username = deployment.Username, - Password = deployment.Password, - Url = deployment.Url, - HttpTransmissionType = deployment.HttpTransmissionType, - FileSystemPath = deployment.FileSystemPath, - EmailAddresses = deployment.EmailAddresses, - EmailSubject = deployment.EmailSubject, - EmailAccountId = deployment.EmailAccountId, - PassiveMode = deployment.PassiveMode, - UseSsl = deployment.UseSsl - }; - - if (forEdit) - { - var allEmailAccounts = _emailAccountService.GetAllEmailAccounts(); - - model.AvailableDeploymentTypes = ExportDeploymentType.FileSystem.ToSelectList(false).ToList(); - model.AvailableHttpTransmissionTypes = ExportHttpTransmissionType.SimplePost.ToSelectList(false).ToList(); - - model.SerializedEmailAddresses = string.Join(",", deployment.EmailAddresses.SplitSafe(",").Select(x => x.EncodeJsString())); - - model.AvailableEmailAccounts = allEmailAccounts - .Select(x => new SelectListItem { Text = x.FriendlyName, Value = x.Id.ToString() }) - .ToList(); - - if (provider != null) - { - model.ThumbnailUrl = GetThumbnailUrl(provider); - } - } - - return model; - } - - private void ModelToEntity(ExportDeploymentModel model, ExportDeployment deployment) - { - deployment.ProfileId = model.ProfileId; - deployment.Name = model.Name; - deployment.Enabled = model.Enabled; - deployment.DeploymentType = model.DeploymentType; - deployment.IsPublic = model.IsPublic; - deployment.CreateZip = model.CreateZip; - deployment.Username = model.Username; - deployment.Password = model.Password; - deployment.Url = model.Url; - deployment.HttpTransmissionType = model.HttpTransmissionType; - deployment.FileSystemPath = model.FileSystemPath; - deployment.EmailAddresses = model.EmailAddresses; - deployment.EmailSubject = model.EmailSubject; - deployment.EmailAccountId = model.EmailAccountId; - deployment.PassiveMode = model.PassiveMode; - deployment.UseSsl = model.UseSsl; - } - - private ActionResult SmartRedirect(bool continueEditing, int profileId, int deploymentId) - { - if (!continueEditing) - { - TempData["SelectedTab.export-profile-edit"] = new SelectedTabInfo - { - TabId = "export-profile-edit-6", - Path = Url.Action("Edit", new { id = profileId }) - }; - } - - return (continueEditing ? - RedirectToAction("EditDeployment", new { id = deploymentId }) : - RedirectToAction("Edit", new { id = profileId })); - } - #endregion public ActionResult Index() @@ -479,7 +580,7 @@ public ActionResult Index() public ActionResult List() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var providers = _exportService.LoadAllExportProviders(0, false).ToList(); @@ -488,13 +589,19 @@ public ActionResult List() foreach (var profile in profiles) { - var profileModel = new ExportProfileModel(); + var provider = providers.FirstOrDefault(x => x.Metadata.SystemName == profile.ProviderSystemName); + if (provider != null) + { + var profileModel = new ExportProfileModel(); + var fileDetailsModel = CreateFileDetailsModel(profile, provider, null); - PrepareProfileModel(profileModel, profile, providers.FirstOrDefault(x => x.Metadata.SystemName == profile.ProviderSystemName), false); + PrepareProfileModel(profileModel, profile, provider); - profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(_services.Localization, _dateTimeHelper, Url); + profileModel.FileCount = fileDetailsModel.FileCount; + profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(Services.Localization, _dateTimeHelper, Url); - model.Add(profileModel); + model.Add(profileModel); + } } return View(model); @@ -502,33 +609,48 @@ public ActionResult List() public ActionResult ProfileListDetails(int profileId) { - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { var profile = _exportService.GetExportProfileById(profileId); if (profile != null) { - var model = PrepareProfileDetailsModel(profile, false); - - return Json(new + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider != null && !provider.Metadata.IsHidden) { - exportFileCount = this.RenderPartialViewToString("ProfileFileCount", model) - }, JsonRequestBehavior.AllowGet); + var model = CreateFileDetailsModel(profile, provider, null); + return Json(this.RenderPartialViewToString("ProfileFileCount", model.FileCount), JsonRequestBehavior.AllowGet); + } } } return new EmptyResult(); } - public ActionResult ProfileFileDetails(int profileId) + public ActionResult ProfileFileDetails(int profileId, int deploymentId) { - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) { - var profile = _exportService.GetExportProfileById(profileId); - if (profile != null) + if (profileId != 0) { - var model = PrepareProfileDetailsModel(profile, true); - - return PartialView(model); + var profile = _exportService.GetExportProfileById(profileId); + if (profile != null) + { + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider != null && !provider.Metadata.IsHidden) + { + var model = CreateFileDetailsModel(profile, provider, null); + return PartialView(model); + } + } + } + else if (deploymentId != 0) + { + var deployment = _exportService.GetExportDeploymentById(deploymentId); + if (deployment != null) + { + var model = CreateFileDetailsModel(deployment.Profile, null, deployment); + return PartialView(model); + } } } @@ -537,7 +659,7 @@ public ActionResult ProfileFileDetails(int profileId) public ActionResult Create() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return Content(T("Admin.AccessDenied.Description")); var count = 0; @@ -584,7 +706,7 @@ public ActionResult Create() [HttpPost] public ActionResult Create(ExportProfileModel model) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); if (model.ProviderSystemName.HasValue()) @@ -605,7 +727,7 @@ public ActionResult Create(ExportProfileModel model) public ActionResult Edit(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -613,10 +735,12 @@ public ActionResult Edit(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); var model = new ExportProfileModel(); - PrepareProfileModel(model, profile, provider, true); + PrepareProfileModel(model, profile, provider); PrepareProfileModelForEdit(model, profile, provider); return View(model); @@ -626,7 +750,7 @@ public ActionResult Edit(int id) [FormValueRequired("save", "save-continue")] public ActionResult Edit(ExportProfileModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(model.Id); @@ -634,10 +758,12 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); if (!ModelState.IsValid) { - PrepareProfileModel(model, profile, provider, true); + PrepareProfileModel(model, profile, provider); PrepareProfileModelForEdit(model, profile, provider); return View(model); } @@ -647,8 +773,8 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) profile.FolderName = model.FolderName; profile.Enabled = model.Enabled; profile.Offset = model.Offset; - profile.Limit = model.Limit; - profile.BatchSize = model.BatchSize; + profile.Limit = model.Limit ?? 0; + profile.BatchSize = model.BatchSize ?? 0; profile.PerStore = model.PerStore; profile.CompletedEmailAddresses = model.CompletedEmailAddresses; profile.EmailAccountId = model.EmailAccountId ?? 0; @@ -670,6 +796,7 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) LanguageId = model.Projection.LanguageId, CurrencyId = model.Projection.CurrencyId, CustomerId = model.Projection.CustomerId, + NumberOfPictures = model.Projection.NumberOfPictures, DescriptionMergingId = model.Projection.DescriptionMergingId, DescriptionToPlainText = model.Projection.DescriptionToPlainText, AppendDescriptionText = model.Projection.AppendDescriptionText, @@ -710,13 +837,22 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) WithoutManufacturers = model.Filter.WithoutManufacturers, ProductTagId = model.Filter.ProductTagId, FeaturedProducts = model.Filter.FeaturedProducts, + IsActiveCustomer = model.Filter.IsActiveCustomer, + IsTaxExempt = model.Filter.IsTaxExempt, + BillingCountryIds = model.Filter.BillingCountryIds, + ShippingCountryIds = model.Filter.ShippingCountryIds, + LastActivityFrom = model.Filter.LastActivityFrom, + LastActivityTo = model.Filter.LastActivityTo, + HasSpentAtLeastAmount = model.Filter.HasSpentAtLeastAmount, + HasPlacedAtLeastOrders = model.Filter.HasPlacedAtLeastOrders, ProductType = model.Filter.ProductType, IdMinimum = model.Filter.IdMinimum, IdMaximum = model.Filter.IdMaximum, OrderStatusIds = model.Filter.OrderStatusIds, PaymentStatusIds = model.Filter.PaymentStatusIds, ShippingStatusIds = model.Filter.ShippingStatusIds, - CustomerRoleIds = model.Filter.CustomerRoleIds + CustomerRoleIds = model.Filter.CustomerRoleIds, + IsActiveSubscriber = model.Filter.IsActiveSubscriber }; profile.Filtering = XmlHelper.Serialize(filter); @@ -747,13 +883,17 @@ public ActionResult Edit(ExportProfileModel model, bool continueEditing) [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); if (profile == null) return RedirectToAction("List"); + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); + try { _exportService.DeleteExportProfile(profile); @@ -772,13 +912,17 @@ public ActionResult DeleteConfirmed(int id) public ActionResult Preview(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); if (profile == null) return RedirectToAction("List"); + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); + if (!profile.Enabled) { NotifyInfo(T("Admin.DataExchange.Export.EnableProfileForPreview")); @@ -786,8 +930,6 @@ public ActionResult Preview(int id) return RedirectToAction("Edit", new { id = profile.Id }); } - var provider = _exportService.LoadProvider(profile.ProviderSystemName); - var request = new DataExportRequest(profile, provider); var totalRecords = _dataExporter.GetDataCount(request); @@ -809,12 +951,19 @@ public ActionResult Preview(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult PreviewList(GridCommand command, int id, int totalRecords) { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + { + NotifyAccessDenied(); + + return new JsonResult { Data = Enumerable.Empty() }; + } + ExportProfile profile = null; Provider provider = null; - if (_services.Permissions.Authorize(StandardPermissionProvider.ManageExports) && - (profile = _exportService.GetExportProfileById(id)) != null && - (provider = _exportService.LoadProvider(profile.ProviderSystemName)) != null) + if ((profile = _exportService.GetExportProfileById(id)) != null && + (provider = _exportService.LoadProvider(profile.ProviderSystemName)) != null && + !provider.Metadata.IsHidden) { var productModel = new List(); var orderModel = new List(); @@ -845,7 +994,7 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) { Id = product.Id, ProductTypeId = product.ProductTypeId, - ProductTypeName = product.GetProductTypeLabel(_services.Localization), + ProductTypeName = product.GetProductTypeLabel(Services.Localization), ProductTypeLabelHint = product.ProductTypeLabelHint, Name = item.Name, Sku = item.Sku, @@ -926,7 +1075,7 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) var subscription = item.Entity as NewsLetterSubscription; if (allStores == null) - allStores = _services.StoreService.GetAllStores(); + allStores = Services.StoreService.GetAllStores(); var store = allStores.FirstOrDefault(x => x.Id == subscription.StoreId); @@ -969,30 +1118,35 @@ public ActionResult PreviewList(GridCommand command, int id, int totalRecords) return new JsonResult { Data = gridData }; } - return new EmptyResult(); + return new JsonResult { Data = Enumerable.Empty() }; } [HttpPost] public ActionResult Execute(int id, string selectedIds) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) - return AccessDeniedView(); + // permissions checked internally by DataExporter var profile = _exportService.GetExportProfileById(id); if (profile == null) return RedirectToAction("List"); - Dictionary taskParams = null; + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); + + var taskParams = new Dictionary(); + taskParams.Add(TaskExecutor.CurrentCustomerIdParamName, Services.WorkContext.CurrentCustomer.Id.ToString()); if (selectedIds.HasValue()) - { - taskParams = new Dictionary(); taskParams.Add("SelectedIds", selectedIds); - } _taskScheduler.RunSingleTask(profile.SchedulingTaskId, taskParams); - NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress")); + NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress.DataExportTask")); + + var referrer = Services.WebHelper.GetUrlReferrer(); + if (referrer.HasValue()) + return Redirect(referrer); return RedirectToAction("List"); } @@ -1029,54 +1183,98 @@ public ActionResult InfoProfile(string systemName, string returnUrl) public ActionResult DownloadLogFile(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); - if (profile == null) - return RedirectToAction("List"); - - var path = profile.GetExportLogPath(); - var stream = new FileStream(path, FileMode.Open); + if (profile != null) + { + var path = profile.GetExportLogPath(); + if (System.IO.File.Exists(path)) + { + try + { + var stream = new FileStream(path, FileMode.Open); + var result = new FileStreamResult(stream, MediaTypeNames.Text.Plain); - var result = new FileStreamResult(stream, "text/plain; charset=utf-8"); - result.FileDownloadName = profile.Name.ToValidFileName() + "-log.txt"; + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } + } + } - return result; + return RedirectToAction("List"); } - public ActionResult DownloadExportFile(int id, string name) + public ActionResult DownloadExportFile(int id, string name, bool? isDeployment) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) - return AccessDeniedView(); + string message = null; + string path = null; - var profile = _exportService.GetExportProfileById(id); - if (profile == null) - return RedirectToAction("List"); + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + { + if (isDeployment ?? false) + { + var deployment = _exportService.GetExportDeploymentById(id); + if (deployment != null) + { + var deploymentFolder = deployment.GetDeploymentFolder(); + if (deploymentFolder.HasValue()) + path = Path.Combine(deploymentFolder, name); + } + } + else + { + var profile = _exportService.GetExportProfileById(id); + if (profile != null) + { + path = Path.Combine(profile.GetExportFolder(true), name); + if (!System.IO.File.Exists(path)) + path = Path.Combine(profile.GetExportFolder(false), name); + } + } - var path = Path.Combine(profile.GetExportFolder(true), name); + if (System.IO.File.Exists(path)) + { + try + { + var stream = new FileStream(path, FileMode.Open); - if (!System.IO.File.Exists(path)) - path = Path.Combine(profile.GetExportFolder(false), name); + var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)); + result.FileDownloadName = Path.GetFileName(path); - var stream = new FileStream(path, FileMode.Open); + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } + } + } + else + { + message = T("Admin.AccessDenied.Description"); + } - var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)); - result.FileDownloadName = Path.GetFileName(path); + if (message.IsEmpty()) + { + message = T("Admin.Common.ResourceNotFound"); + } - return result; + return File(Encoding.UTF8.GetBytes(message), MediaTypeNames.Text.Plain, "DownloadExportFile.txt"); } public ActionResult ResolveFileNamePatternExample(int id, string pattern) { var profile = _exportService.GetExportProfileById(id); - _services.DbContext.DetachEntity(profile); + Services.DbContext.DetachEntity(profile); profile.FileNamePattern = pattern.EmptyNull(); - var provider = _exportService.LoadProvider(profile.ProviderSystemName); - - var resolvedPattern = profile.ResolveFileNamePattern(_services.StoreContext.CurrentStore, 1, _dataExchangeSettings.MaxFileNameLength); + var resolvedPattern = profile.ResolveFileNamePattern(Services.StoreContext.CurrentStore, 1, _dataExchangeSettings.MaxFileNameLength); return this.Content(resolvedPattern); } @@ -1085,7 +1283,7 @@ public ActionResult ResolveFileNamePatternExample(int id, string pattern) public ActionResult CreateDeployment(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(id); @@ -1093,10 +1291,10 @@ public ActionResult CreateDeployment(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider.Metadata.IsHidden) + return RedirectToAction("List"); - //var fileSystemName = ExportDeploymentType.FileSystem.GetLocalizedEnum(_services.Localization, _services.WorkContext); - - var model = PrepareDeploymentModel(new ExportDeployment + var model = CreateDeploymentModel(profile, new ExportDeployment { ProfileId = id, Enabled = true, @@ -1111,13 +1309,17 @@ public ActionResult CreateDeployment(int id) [FormValueRequired("save", "save-continue")] public ActionResult CreateDeployment(ExportDeploymentModel model, bool continueEditing, ExportDeploymentType deploymentType) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var profile = _exportService.GetExportProfileById(model.ProfileId); if (profile == null) return RedirectToAction("List"); + var provider = _exportService.LoadProvider(profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); + if (ModelState.IsValid) { var deployment = new ExportDeployment(); @@ -1136,7 +1338,7 @@ public ActionResult CreateDeployment(ExportDeploymentModel model, bool continueE public ActionResult EditDeployment(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var deployment = _exportService.GetExportDeploymentById(id); @@ -1144,8 +1346,10 @@ public ActionResult EditDeployment(int id) return RedirectToAction("List"); var provider = _exportService.LoadProvider(deployment.Profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); - var model = PrepareDeploymentModel(deployment, provider, true); + var model = CreateDeploymentModel(deployment.Profile, deployment, provider, true); return View(model); } @@ -1154,13 +1358,17 @@ public ActionResult EditDeployment(int id) [FormValueRequired("save", "save-continue")] public ActionResult EditDeployment(ExportDeploymentModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var deployment = _exportService.GetExportDeploymentById(model.Id); if (deployment == null) return RedirectToAction("List"); + var provider = _exportService.LoadProvider(deployment.Profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); + if (ModelState.IsValid) { ModelToEntity(model, deployment); @@ -1172,20 +1380,25 @@ public ActionResult EditDeployment(ExportDeploymentModel model, bool continueEdi return SmartRedirect(continueEditing, deployment.ProfileId, deployment.Id); } - model = PrepareDeploymentModel(deployment, _exportService.LoadProvider(deployment.Profile.ProviderSystemName), true); + model = CreateDeploymentModel(deployment.Profile, deployment, _exportService.LoadProvider(deployment.Profile.ProviderSystemName), true); return View(model); } + [HttpPost] public ActionResult DeleteDeployment(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageExports)) return AccessDeniedView(); var deployment = _exportService.GetExportDeploymentById(id); if (deployment == null) return RedirectToAction("List"); + var provider = _exportService.LoadProvider(deployment.Profile.ProviderSystemName); + if (provider == null || provider.Metadata.IsHidden) + return RedirectToAction("List"); + int profileId = deployment.ProfileId; _exportService.DeleteExportDeployment(deployment); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs index ea3eff5cd1..0900557d31 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/GiftCardController.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Web.Mvc; using SmartStore.Admin.Models.Orders; +using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Logging; using SmartStore.Services; @@ -32,19 +33,21 @@ public class GiftCardController : AdminControllerBase private readonly ILanguageService _languageService; private readonly ICustomerActivityService _customerActivityService; private readonly ICommonServices _services; + private readonly AdminAreaSettings _adminAreaSettings; - #endregion + #endregion - #region Constructors + #region Constructors - public GiftCardController(IGiftCardService giftCardService, + public GiftCardController(IGiftCardService giftCardService, IPriceFormatter priceFormatter, IWorkflowMessageService workflowMessageService, IDateTimeHelper dateTimeHelper, LocalizationSettings localizationSettings, ILanguageService languageService, ICustomerActivityService customerActivityService, - ICommonServices services) + ICommonServices services, + AdminAreaSettings adminAreaSettings) { this._giftCardService = giftCardService; this._priceFormatter = priceFormatter; @@ -54,6 +57,7 @@ public GiftCardController(IGiftCardService giftCardService, this._languageService = languageService; this._customerActivityService = customerActivityService; this._services = services; + this._adminAreaSettings = adminAreaSettings; } #endregion @@ -71,50 +75,60 @@ public ActionResult List() if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageGiftCards)) return AccessDeniedView(); - var model = new GiftCardListModel(); - model.ActivatedList.Add(new SelectListItem() - { - Value = "0", - Selected = true, - Text = _services.Localization.GetResource("Common.All", logIfNotFound: false, defaultValue: "All") - }); - model.ActivatedList.Add(new SelectListItem() + var model = new GiftCardListModel + { + GridPageSize = _adminAreaSettings.GridPageSize + }; + + model.ActivatedList.Add(new SelectListItem { Value = "1", Text = _services.Localization.GetResource("Common.Activated", logIfNotFound: false, defaultValue: "Activated") }); - model.ActivatedList.Add(new SelectListItem() + + model.ActivatedList.Add(new SelectListItem { Value = "2", Text = _services.Localization.GetResource("Common.Deactivated", logIfNotFound: false, defaultValue: "Deactivated") }); + return View(model); } [GridAction(EnableCustomBinding = true)] public ActionResult GiftCardList(GridCommand command, GiftCardListModel model) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageGiftCards)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageGiftCards)) + { + bool? isGiftCardActivated = null; + + if (model.ActivatedId == 1) + isGiftCardActivated = true; + else if (model.ActivatedId == 2) + isGiftCardActivated = false; + + var giftCards = _giftCardService.GetAllGiftCards(null, null, null, isGiftCardActivated, model.CouponCode); + + gridModel.Data = giftCards.PagedForCommand(command).Select(x => + { + var m = x.ToModel(); + m.RemainingAmountStr = _priceFormatter.FormatPrice(x.GetGiftCardRemainingAmount(), true, false); + m.AmountStr = _priceFormatter.FormatPrice(x.Amount, true, false); + m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + return m; + }); + + gridModel.Total = giftCards.Count(); + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - bool? isGiftCardActivated = null; - if (model.ActivatedId == 1) - isGiftCardActivated = true; - else if (model.ActivatedId == 2) - isGiftCardActivated = false; - var giftCards = _giftCardService.GetAllGiftCards(null, null, null, isGiftCardActivated, model.CouponCode); - var gridModel = new GridModel - { - Data = giftCards.PagedForCommand(command).Select(x => - { - var m = x.ToModel(); - m.RemainingAmountStr = _priceFormatter.FormatPrice(x.GetGiftCardRemainingAmount(), true, false); - m.AmountStr = _priceFormatter.FormatPrice(x.Amount, true, false); - m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - return m; - }), - Total = giftCards.Count() - }; return new JsonResult { Data = gridModel @@ -256,6 +270,8 @@ public ActionResult NotifyRecipient(GiftCardModel model) { giftCard.IsRecipientNotified = true; _giftCardService.UpdateGiftCard(giftCard); + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); } } catch (Exception exc) @@ -274,46 +290,48 @@ public ActionResult DeleteConfirmed(int id) var giftCard = _giftCardService.GetGiftCardById(id); if (giftCard == null) - //No gift card found with the specified id return RedirectToAction("List"); _giftCardService.DeleteGiftCard(giftCard); - //activity log _customerActivityService.InsertActivity("DeleteGiftCard", _services.Localization.GetResource("ActivityLog.DeleteGiftCard"), giftCard.GiftCardCouponCode); NotifySuccess(_services.Localization.GetResource("Admin.GiftCards.Deleted")); return RedirectToAction("List"); } - //Gif card usage history [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult UsageHistoryList(int giftCardId, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageGiftCards)) - return AccessDeniedView(); - - var giftCard = _giftCardService.GetGiftCardById(giftCardId); - if (giftCard == null) - throw new ArgumentException("No gift card found with the specified id"); - - var usageHistoryModel = giftCard.GiftCardUsageHistory.OrderByDescending(gcuh => gcuh.CreatedOnUtc) - .Select(x => - { - return new GiftCardModel.GiftCardUsageHistoryModel() - { - Id = x.Id, - OrderId = x.UsedWithOrderId, - UsedValue = _priceFormatter.FormatPrice(x.UsedValue, true, false), - CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) - }; - }) - .ToList(); - var model = new GridModel - { - Data = usageHistoryModel.PagedForCommand(command), - Total = usageHistoryModel.Count - }; + var model = new GridModel(); + + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageGiftCards)) + { + var giftCard = _giftCardService.GetGiftCardById(giftCardId); + + var usageHistoryModel = giftCard.GiftCardUsageHistory + .OrderByDescending(gcuh => gcuh.CreatedOnUtc) + .Select(x => + { + return new GiftCardModel.GiftCardUsageHistoryModel() + { + Id = x.Id, + OrderId = x.UsedWithOrderId, + UsedValue = _priceFormatter.FormatPrice(x.UsedValue, true, false), + CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc) + }; + }) + .ToList(); + + model.Data = usageHistoryModel.PagedForCommand(command); + model.Total = usageHistoryModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs new file mode 100644 index 0000000000..00a216f5d4 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ImportController.cs @@ -0,0 +1,771 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Web.Mvc; +using SmartStore.Admin.Extensions; +using SmartStore.Admin.Models.DataExchange; +using SmartStore.Core; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.IO; +using SmartStore.Services.Catalog.Importer; +using SmartStore.Services.Customers.Importer; +using SmartStore.Services.DataExchange.Csv; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Helpers; +using SmartStore.Services.Localization; +using SmartStore.Services.Messages.Importer; +using SmartStore.Services.Security; +using SmartStore.Services.Tasks; +using SmartStore.Utilities; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Security; + +namespace SmartStore.Admin.Controllers +{ + [AdminAuthorize] + public class ImportController : AdminControllerBase + { + private readonly IImportProfileService _importProfileService; + private readonly IDateTimeHelper _dateTimeHelper; + private readonly ITaskScheduler _taskScheduler; + private readonly ILanguageService _languageService; + + public ImportController( + IImportProfileService importService, + IDateTimeHelper dateTimeHelper, + ITaskScheduler taskScheduler, + ILanguageService languageService) + { + _importProfileService = importService; + _dateTimeHelper = dateTimeHelper; + _taskScheduler = taskScheduler; + _languageService = languageService; + } + + #region Utilities + + private bool IsValidImportFile(string path, out string error) + { + error = null; + + try + { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var unused = LightweightDataTable.FromFile(path, stream, stream.Length, CsvConfiguration.ExcelFriendlyConfiguration, 0, 1); + } + + return true; + } + catch (Exception exception) + { + error = exception.ToAllMessages(); + + FileSystemHelper.Delete(path); + + return false; + } + } + + private bool IsDefaultValueDisabled(string column, string property, string[] disabledFieldNames) + { + if (disabledFieldNames.Contains(property)) + return true; + + string columnWithoutIndex, columnIndex; + if (ColumnMap.ParseSourceName(property, out columnWithoutIndex, out columnIndex)) + return disabledFieldNames.Contains(columnWithoutIndex); + + return false; + } + + private string[] GetDisabledDefaultFieldNames(ImportProfile profile) + { + switch (profile.EntityType) + { + case ImportEntityType.Product: + return new string[] { "Name", "Sku", "ManufacturerPartNumber", "Gtin", "SeName" }; + case ImportEntityType.Category: + return new string[] { "Name", "SeName" }; + case ImportEntityType.Customer: + return new string[] { "CustomerGuid", "Email" }; + case ImportEntityType.NewsLetterSubscription: + return new string[] { "Email" }; + default: + return new string[0]; + } + } + + private string GetPropertyDescription(Dictionary allProperties, string property) + { + if (property.HasValue() && allProperties.ContainsKey(property)) + { + var result = allProperties[property]; + if (result.HasValue()) + return result; + } + return property; + } + + private void PrepareProfileModel(ImportProfileModel model, ImportProfile profile, bool forEdit, ColumnMap invalidMap = null) + { + model.Id = profile.Id; + model.Name = profile.Name; + model.EntityType = profile.EntityType; + model.Enabled = profile.Enabled; + model.Skip = (profile.Skip == 0 ? (int?)null : profile.Skip); + model.Take = (profile.Take == 0 ? (int?)null : profile.Take); + model.UpdateOnly = profile.UpdateOnly; + model.KeyFieldNames = profile.KeyFieldNames.SplitSafe(",").Distinct().ToArray(); + model.ScheduleTaskId = profile.SchedulingTaskId; + model.ScheduleTaskName = profile.ScheduleTask.Name.NaIfEmpty(); + model.IsTaskRunning = profile.ScheduleTask.IsRunning; + model.IsTaskEnabled = profile.ScheduleTask.Enabled; + model.LogFileExists = System.IO.File.Exists(profile.GetImportLogPath()); + model.EntityTypeName = profile.EntityType.GetLocalizedEnum(Services.Localization, Services.WorkContext); + + model.ExistingFileNames = profile.GetImportFiles() + .Select(x => Path.GetFileName(x)) + .ToList(); + + if (profile.ResultInfo.HasValue()) + model.ImportResult = XmlHelper.Deserialize(profile.ResultInfo); + + if (!forEdit) + return; + + CsvConfiguration csvConfiguration = null; + + if (profile.FileType == ImportFileType.CSV) + { + var csvConverter = new CsvConfigurationConverter(); + csvConfiguration = csvConverter.ConvertFrom(profile.FileTypeConfiguration) ?? CsvConfiguration.ExcelFriendlyConfiguration; + + model.CsvConfiguration = new CsvConfigurationModel(csvConfiguration); + } + else + { + csvConfiguration = CsvConfiguration.ExcelFriendlyConfiguration; + } + + // common configuration + var extraData = XmlHelper.Deserialize(profile.ExtraData); + model.ExtraData.NumberOfPictures = extraData.NumberOfPictures; + + // column mapping + model.AvailableSourceColumns = new List(); + model.AvailableEntityProperties = new List(); + model.AvailableKeyFieldNames = new List(); + model.ColumnMappings = new List(); + + try + { + string[] availableKeyFieldNames = null; + string[] disabledDefaultFieldNames = GetDisabledDefaultFieldNames(profile); + var mapConverter = new ColumnMapConverter(); + var storedMap = mapConverter.ConvertFrom(profile.ColumnMapping); + var map = (invalidMap ?? storedMap) ?? new ColumnMap(); + + // property name to localized property name + var allProperties = _importProfileService.GetImportableEntityProperties(profile.EntityType); + + switch (profile.EntityType) + { + case ImportEntityType.Product: + availableKeyFieldNames = ProductImporter.SupportedKeyFields; + break; + case ImportEntityType.Category: + availableKeyFieldNames = CategoryImporter.SupportedKeyFields; + break; + case ImportEntityType.Customer: + availableKeyFieldNames = CustomerImporter.SupportedKeyFields; + break; + case ImportEntityType.NewsLetterSubscription: + availableKeyFieldNames = NewsLetterSubscriptionImporter.SupportedKeyFields; + break; + } + + model.AvailableEntityProperties = allProperties + .Select(x => + { + var mapping = new ColumnMappingItemModel + { + Property = x.Key, + PropertyDescription = x.Value, + IsDefaultDisabled = IsDefaultValueDisabled(x.Key, x.Key, disabledDefaultFieldNames) + }; + + return mapping; + }) + .ToList(); + + model.AvailableKeyFieldNames = availableKeyFieldNames + .Select(x => + { + var item = new SelectListItem { Value = x, Text = x }; + + if (x == "Id") + item.Text = T("Admin.Common.Entity.Fields.Id"); + else if (allProperties.ContainsKey(x)) + item.Text = allProperties[x]; + + return item; + }) + .ToList(); + + model.ColumnMappings = map.Mappings + .Select(x => + { + var mapping = new ColumnMappingItemModel + { + Column = x.Value.MappedName, + Property = x.Key, + Default = x.Value.Default + }; + + if (x.Value.IgnoreProperty) + { + // explicitly ignore the property + mapping.Column = null; + mapping.Default = null; + } + + mapping.PropertyDescription = GetPropertyDescription(allProperties, mapping.Property); + mapping.IsDefaultDisabled = IsDefaultValueDisabled(mapping.Column, mapping.Property, disabledDefaultFieldNames); + + return mapping; + }) + .ToList(); + + var files = profile.GetImportFiles(); + if (!files.Any()) + return; + + var filePath = files.First(); + + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var dataTable = LightweightDataTable.FromFile(Path.GetFileName(filePath), stream, stream.Length, csvConfiguration, 0, 1); + + foreach (var column in dataTable.Columns.Where(x => x.Name.HasValue())) + { + string columnWithoutIndex, columnIndex; + ColumnMap.ParseSourceName(column.Name, out columnWithoutIndex, out columnIndex); + + model.AvailableSourceColumns.Add(new ColumnMappingItemModel + { + Index = dataTable.Columns.IndexOf(column), + Column = column.Name, + ColumnWithoutIndex = columnWithoutIndex, + ColumnIndex = columnIndex, + PropertyDescription = GetPropertyDescription(allProperties, column.Name) + }); + + // auto map where field equals property name + if (!model.ColumnMappings.Any(x => x.Column == column.Name)) + { + var kvp = allProperties.FirstOrDefault(x => x.Key.IsCaseInsensitiveEqual(column.Name)); + if (kvp.Key.IsEmpty()) + { + var alternativeName = LightweightDataTable.GetAlternativeColumnNameFor(column.Name); + kvp = allProperties.FirstOrDefault(x => x.Key.IsCaseInsensitiveEqual(alternativeName)); + } + + if (kvp.Key.HasValue() && !model.ColumnMappings.Any(x => x.Property == kvp.Key)) + { + model.ColumnMappings.Add(new ColumnMappingItemModel + { + Column = column.Name, + Property = kvp.Key, + PropertyDescription = kvp.Value, + IsDefaultDisabled = IsDefaultValueDisabled(column.Name, kvp.Key, disabledDefaultFieldNames) + }); + } + } + } + + // sorting + model.AvailableSourceColumns = model.AvailableSourceColumns + .OrderBy(x => x.PropertyDescription) + .ToList(); + + model.AvailableEntityProperties = model.AvailableEntityProperties + .OrderBy(x => x.PropertyDescription) + .ToList(); + + model.ColumnMappings = model.ColumnMappings + .OrderBy(x => x.PropertyDescription) + .ToList(); + } + } + catch (Exception exception) + { + NotifyError(exception, true, false); + } + } + + #endregion + + public ActionResult Index() + { + return RedirectToAction("List"); + } + + public ActionResult List() + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var model = new ImportProfileListModel + { + Profiles = new List(), + AvailableEntityTypes = ImportEntityType.Product.ToSelectList(false).ToList() + }; + + var profiles = _importProfileService.GetImportProfiles().ToList(); + + foreach (var profile in profiles) + { + var profileModel = new ImportProfileModel(); + + PrepareProfileModel(profileModel, profile, false); + + profileModel.TaskModel = profile.ScheduleTask.ToScheduleTaskModel(Services.Localization, _dateTimeHelper, Url); + + model.Profiles.Add(profileModel); + } + + return View(model); + } + + public ActionResult ProfileListDetails(int profileId) + { + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + { + var profile = _importProfileService.GetImportProfileById(profileId); + if (profile != null) + { + var importResult = XmlHelper.Deserialize(profile.ResultInfo); + + return Json(new + { + importResult = this.RenderPartialViewToString("ProfileImportResult", importResult) + }, + JsonRequestBehavior.AllowGet); + } + } + + return new EmptyResult(); + } + + [HttpPost] + public ActionResult Create(ImportProfileModel model) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var importFile = Path.Combine(FileSystemHelper.TempDir(), model.TempFileName.EmptyNull()); + + if (System.IO.File.Exists(importFile)) + { + var profile = _importProfileService.InsertImportProfile(model.TempFileName, model.Name, model.EntityType); + + if (profile != null && profile.Id != 0) + { + var importFileDestination = Path.Combine(profile.GetImportFolder(true, true), model.TempFileName); + + FileSystemHelper.Copy(importFile, importFileDestination, true, true); + + return RedirectToAction("Edit", new { id = profile.Id }); + } + } + else + { + NotifyError(T("Admin.DataExchange.Import.MissingImportFile")); + } + + return RedirectToAction("List"); + } + + public ActionResult Edit(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var profile = _importProfileService.GetImportProfileById(id); + if (profile == null) + return RedirectToAction("List"); + + var model = new ImportProfileModel(); + PrepareProfileModel(model, profile, true); + + return View(model); + } + + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [FormValueRequired("save", "save-continue")] + public ActionResult Edit(ImportProfileModel model, bool continueEditing, FormCollection form) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var profile = _importProfileService.GetImportProfileById(model.Id); + if (profile == null) + return RedirectToAction("List"); + + var map = new ColumnMap(); + var hasErrors = false; + var resetMappings = false; + + try + { + var propertyKeyPrefix = "ColumnMapping.Property."; + var allPropertyKeys = form.AllKeys.Where(x => x.HasValue() && x.StartsWith(propertyKeyPrefix)); + + if (allPropertyKeys.Any()) + { + var entityProperties = _importProfileService.GetImportableEntityProperties(profile.EntityType); + + foreach (var key in allPropertyKeys) + { + var index = key.Substring(propertyKeyPrefix.Length); + var property = form[key]; + var column = form["ColumnMapping.Column." + index]; + var defaultValue = form["ColumnMapping.Default." + index]; + + if (column.IsEmpty()) + { + // tell mapper to explicitly ignore the property + map.AddMapping(property, null, property, "[IGNOREPROPERTY]"); + } + else if (!column.IsCaseInsensitiveEqual(property) || defaultValue.HasValue()) + { + if (defaultValue.HasValue() && GetDisabledDefaultFieldNames(profile).Contains(property)) + defaultValue = null; + + map.AddMapping(property, null, column, defaultValue); + } + } + } + } + catch (Exception exception) + { + hasErrors = true; + NotifyError(exception, true, false); + } + + if (!ModelState.IsValid || hasErrors) + { + PrepareProfileModel(model, profile, true, map); + return View(model); + } + + profile.Name = model.Name; + profile.EntityType = model.EntityType; + profile.Enabled = model.Enabled; + profile.Skip = model.Skip ?? 0; + profile.Take = model.Take ?? 0; + profile.UpdateOnly = model.UpdateOnly; + profile.KeyFieldNames = (model.KeyFieldNames == null ? null : string.Join(",", model.KeyFieldNames)); + + try + { + if (profile.FileType == ImportFileType.CSV && model.CsvConfiguration != null) + { + var csvConverter = new CsvConfigurationConverter(); + + var oldCsvConfig = csvConverter.ConvertFrom(profile.FileTypeConfiguration); + var oldDelimiter = (oldCsvConfig != null ? oldCsvConfig.Delimiter.ToString() : null); + + // auto reset mappings cause they are invalid. note: delimiter can be whitespaced, so no oldDelimter.HasValue() etc. + resetMappings = (oldDelimiter != model.CsvConfiguration.Delimiter && profile.ColumnMapping.HasValue()); + + profile.FileTypeConfiguration = csvConverter.ConvertTo(model.CsvConfiguration.Clone()); + } + else + { + profile.FileTypeConfiguration = null; + } + + if (resetMappings) + { + profile.ColumnMapping = null; + } + else + { + var mapConverter = new ColumnMapConverter(); + profile.ColumnMapping = mapConverter.ConvertTo(map); + } + + if (model.ExtraData != null) + { + var extraData = new ImportExtraData + { + NumberOfPictures = model.ExtraData.NumberOfPictures + }; + + profile.ExtraData = XmlHelper.Serialize(extraData); + } + } + catch (Exception exception) + { + hasErrors = true; + NotifyError(exception, true, false); + } + + if (!hasErrors) + { + _importProfileService.UpdateImportProfile(profile); + + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + + if (resetMappings) + { + NotifyWarning(T("Admin.DataExchange.ColumnMapping.Validate.MappingsReset")); + } + } + + return (continueEditing ? RedirectToAction("Edit", new { id = profile.Id }) : RedirectToAction("List")); + } + + [HttpPost] + public ActionResult ResetColumnMappings(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var profile = _importProfileService.GetImportProfileById(id); + if (profile == null) + return RedirectToAction("List"); + + profile.ColumnMapping = null; + _importProfileService.UpdateImportProfile(profile); + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + + return RedirectToAction("Edit", new { id = profile.Id }); + } + + [HttpPost, ActionName("Delete")] + public ActionResult DeleteConfirmed(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var profile = _importProfileService.GetImportProfileById(id); + if (profile == null) + return RedirectToAction("List"); + + try + { + _importProfileService.DeleteImportProfile(profile); + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + + return RedirectToAction("List"); + } + catch (Exception exception) + { + NotifyError(exception); + } + + return RedirectToAction("Edit", new { id = profile.Id }); + } + + [HttpPost] + public JsonResult FileUpload(int id) + { + var success = false; + string error = null; + string tempFile = ""; + + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + { + var postedFile = Request.ToPostedFileResult(); + if (postedFile != null) + { + if (id == 0) + { + var path = Path.Combine(FileSystemHelper.TempDir(), postedFile.FileName); + FileSystemHelper.Delete(path); + + success = postedFile.Stream.ToFile(path); + if (success) + { + success = IsValidImportFile(path, out error); + if (success) + tempFile = postedFile.FileName; + } + } + else + { + var profile = _importProfileService.GetImportProfileById(id); + if (profile != null) + { + var files = profile.GetImportFiles(); + if (files.Any()) + { + var extension = Path.GetExtension(files.First()); + + if (!postedFile.FileExtension.IsCaseInsensitiveEqual(extension)) + error = T("Admin.Common.FileTypeMustEqual", extension.Substring(1).ToUpper()); + } + + if (!error.HasValue()) + { + var folder = profile.GetImportFolder(true, true); + var fileName = Path.GetFileName(postedFile.FileName); + var path = Path.Combine(folder, fileName); + + success = postedFile.Stream.ToFile(path); + if (success) + { + success = IsValidImportFile(path, out error); + if (success) + { + var fileType = (Path.GetExtension(fileName).IsCaseInsensitiveEqual(".xlsx") ? ImportFileType.XLSX : ImportFileType.CSV); + if (fileType != profile.FileType) + { + profile.FileType = fileType; + _importProfileService.UpdateImportProfile(profile); + } + } + } + } + } + } + } + } + else + { + error = T("Admin.AccessDenied.Description"); + } + + if (!success && error.IsEmpty()) + error = T("Admin.Common.UploadFileFailed"); + + if (error.HasValue()) + NotifyError(error); + + return Json(new { success = success, tempFile = tempFile, error = error }); + } + + [HttpPost] + public ActionResult Execute(int id) + { + // permissions checked internally by DataImporter + + var profile = _importProfileService.GetImportProfileById(id); + if (profile == null) + return RedirectToAction("List"); + + var taskParams = new Dictionary(); + taskParams.Add(TaskExecutor.CurrentCustomerIdParamName, Services.WorkContext.CurrentCustomer.Id.ToString()); + + _taskScheduler.RunSingleTask(profile.SchedulingTaskId, taskParams); + + NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress.DataImportTask")); + + var referrer = Services.WebHelper.GetUrlReferrer(); + if (referrer.HasValue()) + return Redirect(referrer); + + return RedirectToAction("List"); + } + + public ActionResult DownloadLogFile(int id) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + return AccessDeniedView(); + + var profile = _importProfileService.GetImportProfileById(id); + if (profile != null) + { + var path = profile.GetImportLogPath(); + if (System.IO.File.Exists(path)) + { + try + { + var stream = new FileStream(path, FileMode.Open); + var result = new FileStreamResult(stream, MediaTypeNames.Text.Plain); + + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } + } + } + + return RedirectToAction("List"); + } + + public ActionResult DownloadImportFile(int id, string name) + { + string message = null; + + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + { + var profile = _importProfileService.GetImportProfileById(id); + if (profile != null) + { + var path = Path.Combine(profile.GetImportFolder(true), name); + + if (!System.IO.File.Exists(path)) + path = Path.Combine(profile.GetImportFolder(false), name); + + if (System.IO.File.Exists(path)) + { + try + { + var stream = new FileStream(path, FileMode.Open); + + var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(path)); + result.FileDownloadName = Path.GetFileName(path); + + return result; + } + catch (IOException) + { + NotifyWarning(T("Admin.Common.FileInUse")); + } + } + } + } + else + { + message = T("Admin.AccessDenied.Description"); + } + + if (message.IsEmpty()) + { + message = T("Admin.Common.ResourceNotFound"); + } + + return File(Encoding.UTF8.GetBytes(message), MediaTypeNames.Text.Plain, "DownloadImportFile.txt"); + } + + [HttpPost] + public ActionResult DeleteImportFile(int id, string name) + { + if (Services.Permissions.Authorize(StandardPermissionProvider.ManageImports)) + { + var profile = _importProfileService.GetImportProfileById(id); + if (profile != null) + { + var importFiles = profile.GetImportFiles(); + var path = Path.Combine(profile.GetImportFolder(true), name); + FileSystemHelper.Delete(path); + } + } + return RedirectToAction("Edit", new { id = id }); + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs index 0d7cbbd0dd..dc777ae81e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Web.Mvc; using SmartStore.Admin.Models.Localization; -using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Plugins; -using SmartStore.Services.DataExchange.Import; +using SmartStore.Services; +using SmartStore.Services.Directory; using SmartStore.Services.Localization; using SmartStore.Services.Security; using SmartStore.Services.Stores; @@ -23,79 +24,104 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public partial class LanguageController : AdminControllerBase { #region Fields private readonly ILanguageService _languageService; - private readonly ILocalizationService _localizationService; - private readonly IStoreService _storeService; private readonly IStoreMappingService _storeMappingService; - private readonly IPermissionService _permissionService; - private readonly IWebHelper _webHelper; private readonly AdminAreaSettings _adminAreaSettings; private readonly IPluginFinder _pluginFinder; + private readonly ICountryService _countryService; + private readonly ICommonServices _services; #endregion #region Constructors public LanguageController(ILanguageService languageService, - ILocalizationService localizationService, - IStoreService storeService, IStoreMappingService storeMappingService, - IPermissionService permissionService, - IWebHelper webHelper, AdminAreaSettings adminAreaSettings, - IPluginFinder pluginFinder) + IPluginFinder pluginFinder, + ICountryService countryService, + ICommonServices services) { - this._localizationService = localizationService; this._languageService = languageService; - this._storeService = storeService; this._storeMappingService = storeMappingService; - this._permissionService = permissionService; - this._webHelper = webHelper; this._adminAreaSettings = adminAreaSettings; this._pluginFinder = pluginFinder; + this._countryService = countryService; + this._services = services; } - #endregion - #region Utilities + #endregion - [NonAction] - private void PrepareFlagsModel(LanguageModel model) - { - if (model == null) - throw new ArgumentNullException("model"); - - model.FlagFileNames = System.IO.Directory - .EnumerateFiles(_webHelper.MapPath("~/Content/Images/flags/"), "*.png", SearchOption.TopDirectoryOnly) - .Select(System.IO.Path.GetFileName) - .ToList(); - } + #region Utilities - [NonAction] - private void PrepareStoresMappingModel(LanguageModel model, Language language, bool excludeProperties) + private void PrepareLanguageModel(LanguageModel model, Language language, bool excludeProperties) { - if (model == null) - throw new ArgumentNullException("model"); + var languageId = _services.WorkContext.WorkingLanguage.Id; - model.AvailableStores = _storeService - .GetAllStores() - .Select(s => s.ToModel()) + var allCultures = CultureInfo.GetCultures(CultureTypes.SpecificCultures) + .OrderBy(x => x.DisplayName) .ToList(); - if (!excludeProperties) + + var allCountryNames = _countryService.GetAllCountries(true) + .ToDictionarySafe(x => x.TwoLetterIsoCode.EmptyNull().ToLower(), x => x.GetLocalized(y => y.Name, languageId, true, false)); + + model.AvailableCultures = allCultures + .Select(x => new SelectListItem { Text = "{0} [{1}]".FormatInvariant(x.DisplayName, x.IetfLanguageTag), Value = x.IetfLanguageTag }) + .ToList(); + + model.AvailableTwoLetterLanguageCodes = new List(); + model.AvailableFlags = new List(); + + foreach (var item in allCultures) { - if (language != null) - { - model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(language); - } - else + if (!model.AvailableTwoLetterLanguageCodes.Any(x => x.Value.IsCaseInsensitiveEqual(item.TwoLetterISOLanguageName))) { - model.SelectedStoreIds = new int[0]; + // display language name is not provided by net framework + var index = item.DisplayName.EmptyNull().IndexOf(" ("); + + if (index == -1) + index = item.DisplayName.EmptyNull().IndexOf(" ["); + + var displayName = "{0} [{1}]".FormatInvariant( + index == -1 ? item.DisplayName : item.DisplayName.Substring(0, index), + item.TwoLetterISOLanguageName); + + if (item.TwoLetterISOLanguageName.Length == 2) + { + model.AvailableTwoLetterLanguageCodes.Add(new SelectListItem { Text = displayName, Value = item.TwoLetterISOLanguageName }); + } } } + + foreach (var path in Directory.EnumerateFiles(_services.WebHelper.MapPath("~/Content/Images/flags/"), "*.png", SearchOption.TopDirectoryOnly)) + { + var name = Path.GetFileNameWithoutExtension(path).EmptyNull().ToLower(); + string countryDescription = null; + + if (allCountryNames.ContainsKey(name)) + countryDescription = "{0} [{1}]".FormatInvariant(allCountryNames[name], name); + + if (countryDescription.IsEmpty()) + countryDescription = name; + + model.AvailableFlags.Add(new SelectListItem { Text = countryDescription, Value = Path.GetFileName(path) }); + } + + model.AvailableFlags = model.AvailableFlags.OrderBy(x => x.Text).ToList(); + + model.AvailableStores = _services.StoreService.GetAllStores() + .Select(s => s.ToModel()) + .ToList(); + + if (!excludeProperties) + { + model.SelectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(language); + } } #endregion @@ -109,58 +135,61 @@ public ActionResult Index() public ActionResult List() { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var languages = _languageService.GetAllLanguages(true); + var gridModel = new GridModel { Data = languages.Select(x => x.ToModel()), Total = languages.Count() }; + return View(gridModel); } [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) - return AccessDeniedView(); + var model = new GridModel(); + + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) + { + var languages = _languageService.GetAllLanguages(true); + + model.Data = languages.Select(x => x.ToModel()); + model.Total = languages.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var languages = _languageService.GetAllLanguages(true); - var gridModel = new GridModel - { - Data = languages.Select(x => x.ToModel()), - Total = languages.Count() - }; return new JsonResult { - Data = gridModel + Data = model }; } public ActionResult Create() { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var model = new LanguageModel(); - //flags - PrepareFlagsModel(model); - - //Stores - PrepareStoresMappingModel(model, null, false); + PrepareLanguageModel(model, null, false); - //default values - //model.Published = true; return View(model); } [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Create(LanguageModel model, bool continueEditing) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); if (ModelState.IsValid) @@ -176,18 +205,14 @@ public ActionResult Create(LanguageModel model, bool continueEditing) foreach (var plugin in plugins) { - _localizationService.ImportPluginResourcesFromXml(plugin, null, false, filterLanguages); + _services.Localization.ImportPluginResourcesFromXml(plugin, null, false, filterLanguages); } - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Languages.Added")); + NotifySuccess(T("Admin.Configuration.Languages.Added")); return continueEditing ? RedirectToAction("Edit", new { id = language.Id }) : RedirectToAction("List"); } - //flags - PrepareFlagsModel(model); - - //Stores - PrepareStoresMappingModel(model, null, true); + PrepareLanguageModel(model, null, true); //If we got this far, something failed, redisplay form return View(model); @@ -195,7 +220,7 @@ public ActionResult Create(LanguageModel model, bool continueEditing) public ActionResult Edit(int id) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var language = _languageService.GetLanguageById(id); @@ -207,11 +232,7 @@ public ActionResult Edit(int id) var model = language.ToModel(); - //flags - PrepareFlagsModel(model); - - //Stores - PrepareStoresMappingModel(model, language, false); + PrepareLanguageModel(model, language, false); return View(model); } @@ -219,7 +240,7 @@ public ActionResult Edit(int id) [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Edit(LanguageModel model, bool continueEditing) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var language = _languageService.GetLanguageById(model.Id); @@ -245,15 +266,11 @@ public ActionResult Edit(LanguageModel model, bool continueEditing) _storeMappingService.SaveStoreMappings(language, model.SelectedStoreIds); //notification - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Languages.Updated")); + NotifySuccess(T("Admin.Configuration.Languages.Updated")); return continueEditing ? RedirectToAction("Edit", new { id = language.Id }) : RedirectToAction("List"); } - //flags - PrepareFlagsModel(model); - - //Stores - PrepareStoresMappingModel(model, language, true); + PrepareLanguageModel(model, language, true); //If we got this far, something failed, redisplay form return View(model); @@ -262,7 +279,7 @@ public ActionResult Edit(LanguageModel model, bool continueEditing) [HttpPost] public ActionResult Delete(int id) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var language = _languageService.GetLanguageById(id); @@ -281,7 +298,7 @@ public ActionResult Delete(int id) _languageService.DeleteLanguage(language); //notification - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Languages.Deleted")); + NotifySuccess(T("Admin.Configuration.Languages.Deleted")); return RedirectToAction("List"); } @@ -291,7 +308,7 @@ public ActionResult Delete(int id) public ActionResult Resources(int languageId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); ViewBag.AllLanguages = _languageService.GetAllLanguages(true) @@ -305,7 +322,7 @@ public ActionResult Resources(int languageId) ViewBag.LanguageId = languageId; ViewBag.LanguageName = language.Name; - var resources = _localizationService + var resources = _services.Localization .GetResourceValues(languageId, true) .Where(x => x.Key != "!!___EOF___!!" && x.Value != null) .OrderBy(x => x.Key) @@ -330,31 +347,37 @@ public ActionResult Resources(int languageId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Resources(int languageId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) - return AccessDeniedView(); + var model = new GridModel(); - var language = _languageService.GetLanguageById(languageId); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) + { + var language = _languageService.GetLanguageById(languageId); + + var resources = _services.Localization + .GetResourceValues(languageId, true) + .OrderBy(x => x.Key) + .Where(x => x.Key != "!!___EOF___!!" && x.Value != null) + .Select(x => new LanguageResourceModel + { + LanguageId = languageId, + LanguageName = language.Name, + Id = x.Value.Item1, + Name = x.Key, + Value = x.Value.Item2.EmptyNull(), + }) + .ForCommand(command) + .ToList(); + + model.Data = resources.PagedForCommand(command); + model.Total = resources.Count; + } + else + { + model.Data = Enumerable.Empty(); - var resources = _localizationService - .GetResourceValues(languageId, true) - .OrderBy(x => x.Key) - .Where(x => x.Key != "!!___EOF___!!" && x.Value != null) - .Select(x => new LanguageResourceModel - { - LanguageId = languageId, - LanguageName = language.Name, - Id = x.Value.Item1, - Name = x.Key, - Value = x.Value.Item2.EmptyNull(), - }) - .ForCommand(command) - .ToList(); + NotifyAccessDenied(); + } - var model = new GridModel - { - Data = resources.PagedForCommand(command), - Total = resources.Count - }; return new JsonResult { Data = model @@ -364,36 +387,36 @@ public ActionResult Resources(int languageId, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult ResourceUpdate(LanguageResourceModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) + { + if (model.Name != null) + model.Name = model.Name.Trim(); + if (model.Value != null) + model.Value = model.Value.Trim(); - if (model.Name != null) - model.Name = model.Name.Trim(); - if (model.Value != null) - model.Value = model.Value.Trim(); + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var resource = _services.Localization.GetLocaleStringResourceById(model.Id); + // if the resourceName changed, ensure it isn't being used by another resource + if (!resource.ResourceName.Equals(model.Name, StringComparison.InvariantCultureIgnoreCase)) + { + var res = _services.Localization.GetLocaleStringResourceByName(model.Name, model.LanguageId, false); + if (res != null && res.Id != resource.Id) + { + return Content(T("Admin.Configuration.Languages.Resources.NameAlreadyExists", res.ResourceName)); + } + } - var resource = _localizationService.GetLocaleStringResourceById(model.Id); - // if the resourceName changed, ensure it isn't being used by another resource - if (!resource.ResourceName.Equals(model.Name, StringComparison.InvariantCultureIgnoreCase)) - { - var res = _localizationService.GetLocaleStringResourceByName(model.Name, model.LanguageId, false); - if (res != null && res.Id != resource.Id) - { - return Content(string.Format(_localizationService.GetResource("Admin.Configuration.Languages.Resources.NameAlreadyExists"), res.ResourceName)); - } - } + resource.ResourceName = model.Name; + resource.ResourceValue = model.Value; + resource.IsTouched = true; - resource.ResourceName = model.Name; - resource.ResourceValue = model.Value; - resource.IsTouched = true; - _localizationService.UpdateLocaleStringResource(resource); + _services.Localization.UpdateLocaleStringResource(resource); + } return Resources(model.LanguageId, command); } @@ -401,47 +424,47 @@ public ActionResult ResourceUpdate(LanguageResourceModel model, GridCommand comm [GridAction(EnableCustomBinding = true)] public ActionResult ResourceAdd(int id, LanguageResourceModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) + { + if (model.Name != null) + model.Name = model.Name.Trim(); + if (model.Value != null) + model.Value = model.Value.Trim(); - if (model.Name != null) - model.Name = model.Name.Trim(); - if (model.Value != null) - model.Value = model.Value.Trim(); + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var res = _services.Localization.GetLocaleStringResourceByName(model.Name, model.LanguageId, false); + if (res == null) + { + var resource = new LocaleStringResource { LanguageId = id }; + resource.ResourceName = model.Name; + resource.ResourceValue = model.Value; + resource.IsTouched = true; + + _services.Localization.InsertLocaleStringResource(resource); + } + else + { + return Content(T("Admin.Configuration.Languages.Resources.NameAlreadyExists", model.Name)); + } + } - var res = _localizationService.GetLocaleStringResourceByName(model.Name, model.LanguageId, false); - if (res == null) - { - var resource = new LocaleStringResource { LanguageId = id }; - resource.ResourceName = model.Name; - resource.ResourceValue = model.Value; - resource.IsTouched = true; - _localizationService.InsertLocaleStringResource(resource); - } - else - { - return Content(string.Format(_localizationService.GetResource("Admin.Configuration.Languages.Resources.NameAlreadyExists"), model.Name)); - } return Resources(id, command); } [GridAction(EnableCustomBinding = true)] public ActionResult ResourceDelete(int id, int languageId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) + { + var resource = _services.Localization.GetLocaleStringResourceById(id); - var resource = _localizationService.GetLocaleStringResourceById(id); - if (resource == null) - throw new ArgumentException("No resource found with the specified id"); - _localizationService.DeleteLocaleStringResource(resource); + _services.Localization.DeleteLocaleStringResource(resource); + } return Resources(languageId, command); } @@ -452,7 +475,7 @@ public ActionResult ResourceDelete(int id, int languageId, GridCommand command) public ActionResult ExportXml(int id) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var language = _languageService.GetLanguageById(id); @@ -461,7 +484,7 @@ public ActionResult ExportXml(int id) try { - var xml = _localizationService.ExportResourcesToXml(language); + var xml = _services.Localization.ExportResourcesToXml(language); return new XmlDownloadResult(xml, "language-pack-{0}.xml".FormatInvariant(language.UniqueSeoCode)); } catch (Exception exc) @@ -474,7 +497,7 @@ public ActionResult ExportXml(int id) [HttpPost] public ActionResult ImportXml(int id, FormCollection form, ImportModeFlags mode, bool updateTouched) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageLanguages)) + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageLanguages)) return AccessDeniedView(); var language = _languageService.GetLanguageById(id); @@ -489,15 +512,15 @@ public ActionResult ImportXml(int id, FormCollection form, ImportModeFlags mode, var file = Request.Files["importxmlfile"]; if (file != null && file.ContentLength > 0) { - _localizationService.ImportResourcesFromXml(language, file.InputStream.AsString(), mode: mode, updateTouchedResources: updateTouched); + _services.Localization.ImportResourcesFromXml(language, file.InputStream.AsString(), mode: mode, updateTouchedResources: updateTouched); } else { - NotifyError(_localizationService.GetResource("Admin.Common.UploadFile")); + NotifyError(T("Admin.Common.UploadFile")); return RedirectToAction("Edit", new { id = language.Id }); } - NotifySuccess(_localizationService.GetResource("Admin.Configuration.Languages.Imported")); + NotifySuccess(T("Admin.Configuration.Languages.Imported")); return RedirectToAction("Edit", new { id = language.Id }); } catch (Exception exc) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/LogController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/LogController.cs index 8b82d5d894..3d0129a164 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/LogController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/LogController.cs @@ -55,8 +55,8 @@ public ActionResult List() return AccessDeniedView(); var model = new LogListModel(); - model.AvailableLogLevels = LogLevel.Debug.ToSelectList(false).ToList(); - model.AvailableLogLevels.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); + + model.AvailableLogLevels = LogLevel.Debug.ToSelectList(false).ToList(); return View(model); } @@ -64,49 +64,55 @@ public ActionResult List() [GridAction(EnableCustomBinding = true)] public ActionResult LogList(GridCommand command, LogListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageSystemLog)) - return AccessDeniedView(); - - DateTime? createdOnFromValue = (model.CreatedOnFrom == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnFrom.Value, _dateTimeHelper.CurrentTimeZone); - - DateTime? createdToFromValue = (model.CreatedOnTo == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnTo.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); - - LogLevel? logLevel = model.LogLevelId > 0 ? (LogLevel?)(model.LogLevelId) : null; - - - var logItems = Logger.GetAllLogs(createdOnFromValue, createdToFromValue, model.Message, - logLevel, command.Page - 1, command.PageSize, model.MinFrequency); - - var gridModel = new GridModel - { - Data = logItems.Select(x => - { - var logModel = new LogModel() - { - Id = x.Id, - LogLevelHint = s_logLevelHintMap[x.LogLevel], - LogLevel = x.LogLevel.GetLocalizedEnum(_localizationService, _workContext), - ShortMessage = x.ShortMessage, - FullMessage = x.FullMessage, - IpAddress = x.IpAddress, - CustomerId = x.CustomerId, - CustomerEmail = x.Customer != null ? x.Customer.Email : null, - PageUrl = x.PageUrl, - ReferrerUrl = x.ReferrerUrl, - CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageSystemLog)) + { + DateTime? createdOnFromValue = (model.CreatedOnFrom == null) ? null + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnFrom.Value, _dateTimeHelper.CurrentTimeZone); + + DateTime? createdToFromValue = (model.CreatedOnTo == null) ? null + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnTo.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + + LogLevel? logLevel = model.LogLevelId > 0 ? (LogLevel?)(model.LogLevelId) : null; + + var logItems = Logger.GetAllLogs(createdOnFromValue, createdToFromValue, model.Message, + logLevel, command.Page - 1, command.PageSize, model.MinFrequency); + + gridModel.Data = logItems.Select(x => + { + var logModel = new LogModel + { + Id = x.Id, + LogLevelHint = s_logLevelHintMap[x.LogLevel], + LogLevel = x.LogLevel.GetLocalizedEnum(_localizationService, _workContext), + ShortMessage = x.ShortMessage, + FullMessage = x.FullMessage, + IpAddress = x.IpAddress, + CustomerId = x.CustomerId, + CustomerEmail = x.Customer != null ? x.Customer.Email : null, + PageUrl = x.PageUrl, + ReferrerUrl = x.ReferrerUrl, + CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), Frequency = x.Frequency, ContentHash = x.ContentHash - }; + }; if (x.UpdatedOnUtc.HasValue) logModel.UpdatedOn = _dateTimeHelper.ConvertToUserTime(x.UpdatedOnUtc.Value, DateTimeKind.Utc); return logModel; - }), - Total = logItems.TotalCount - }; + }); + + gridModel.Total = logItems.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = gridModel diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs index e107a81fac..62ea281f19 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ManufacturerController.cs @@ -18,9 +18,9 @@ using SmartStore.Services.Stores; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; -using Telerik.Web.Mvc; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; +using Telerik.Web.Mvc; namespace SmartStore.Admin.Controllers { @@ -152,6 +152,8 @@ private void PrepareManufacturerModel(ManufacturerModel model, Manufacturer manu if (model == null) throw new ArgumentNullException("model"); + model.GridPageSize = _adminAreaSettings.GridPageSize; + model.AvailableStores = _storeService .GetAllStores() .Select(s => s.ToModel()) @@ -248,22 +250,32 @@ public ActionResult List() GridPageSize = _adminAreaSettings.GridPageSize }; + model.AvailableStores = _storeService.GetAllStores().ToSelectListItems(); + return View(model); } [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, ManufacturerListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var manufacturers = _manufacturerService.GetAllManufacturers(model.SearchManufacturerName, command.Page - 1, command.PageSize, true); + model.AvailableStores = _storeService.GetAllStores().ToSelectListItems(); - var gridModel = new GridModel - { - Data = manufacturers.Select(x => x.ToModel()), - Total = manufacturers.TotalCount - }; + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var manufacturers = _manufacturerService.GetAllManufacturers(model.SearchManufacturerName, command.Page - 1, command.PageSize, + model.SearchStoreId, true); + + gridModel.Data = manufacturers.Select(x => x.ToModel()); + gridModel.Total = manufacturers.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -441,35 +453,43 @@ public ActionResult Delete(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductList(GridCommand command, int manufacturerId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var productManufacturers = _manufacturerService.GetProductManufacturersByManufacturerId(manufacturerId, - command.Page - 1, command.PageSize, true); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productManufacturers = _manufacturerService.GetProductManufacturersByManufacturerId(manufacturerId, command.Page - 1, command.PageSize, true); - var model = new GridModel - { - Data = productManufacturers - .Select(x => - { - var product = _productService.GetProductById(x.ProductId); - - return new ManufacturerModel.ManufacturerProductModel() - { - Id = x.Id, - ManufacturerId = x.ManufacturerId, - ProductId = x.ProductId, - ProductName = product.Name, - Sku = product.Sku, - ProductTypeName = product.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = product.ProductTypeLabelHint, - Published = product.Published, - IsFeaturedProduct = x.IsFeaturedProduct, - DisplayOrder1 = x.DisplayOrder - }; - }), - Total = productManufacturers.TotalCount - }; + var productIds = productManufacturers.Select(x => x.ProductId).ToArray(); + var products = _productService.GetProductsByIds(productIds); + + model.Data = productManufacturers + .Select(x => + { + var product = products.FirstOrDefault(y => y.Id == x.ProductId); + + return new ManufacturerModel.ManufacturerProductModel + { + Id = x.Id, + ManufacturerId = x.ManufacturerId, + ProductId = x.ProductId, + ProductName = product.Name, + Sku = product.Sku, + ProductTypeName = product.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = product.ProductTypeLabelHint, + Published = product.Published, + IsFeaturedProduct = x.IsFeaturedProduct, + DisplayOrder1 = x.DisplayOrder + }; + }); + + model.Total = productManufacturers.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -480,16 +500,15 @@ public ActionResult ProductList(GridCommand command, int manufacturerId) [GridAction(EnableCustomBinding = true)] public ActionResult ProductUpdate(GridCommand command, ManufacturerModel.ManufacturerProductModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productManufacturer = _manufacturerService.GetProductManufacturerById(model.Id); - var productManufacturer = _manufacturerService.GetProductManufacturerById(model.Id); - if (productManufacturer == null) - throw new ArgumentException("No product manufacturer mapping found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + productManufacturer.IsFeaturedProduct = model.IsFeaturedProduct; + productManufacturer.DisplayOrder = model.DisplayOrder1; - productManufacturer.IsFeaturedProduct = model.IsFeaturedProduct; - productManufacturer.DisplayOrder = model.DisplayOrder1; - _manufacturerService.UpdateProductManufacturer(productManufacturer); + _manufacturerService.UpdateProductManufacturer(productManufacturer); + } return ProductList(command, productManufacturer.ManufacturerId); } @@ -497,141 +516,56 @@ public ActionResult ProductUpdate(GridCommand command, ManufacturerModel.Manufac [GridAction(EnableCustomBinding = true)] public ActionResult ProductDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var productManufacturer = _manufacturerService.GetProductManufacturerById(id); - if (productManufacturer == null) - throw new ArgumentException("No product manufacturer mapping found with the specified id"); + var productManufacturer = _manufacturerService.GetProductManufacturerById(id); + var manufacturerId = productManufacturer.ManufacturerId; - var manufacturerId = productManufacturer.ManufacturerId; - _manufacturerService.DeleteProductManufacturer(productManufacturer); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _manufacturerService.DeleteProductManufacturer(productManufacturer); + } return ProductList(command, manufacturerId); - } + } - public ActionResult ProductAddPopup(int manufacturerId) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var ctx = new ProductSearchContext(); - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageSize = _adminAreaSettings.GridPageSize; - ctx.ShowHidden = true; - - var products = _productService.SearchProducts(ctx); - - var model = new ManufacturerModel.AddManufacturerProductModel(); - model.Products = new GridModel + [HttpPost] + public ActionResult ProductAdd(int manufacturerId, string selectedProductIds) + { + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - Data = products.Select(x => - { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); + var productIds = selectedProductIds.SplitSafe(",").Select(x => x.ToInt()).ToArray(); + var products = _productService.GetProductsByIds(productIds); + ProductManufacturer productManu = null; + var maxDisplayOrder = -1; - return productModel; - }), - Total = products.TotalCount - }; - - //categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) - { - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - } - - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult ProductAddPopupList(GridCommand command, ManufacturerModel.AddManufacturerProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var gridModel = new GridModel(); - - var ctx = new ProductSearchContext(); - - if (model.SearchCategoryId > 0) - ctx.CategoryIds.Add(model.SearchCategoryId); - - ctx.ManufacturerId = model.SearchManufacturerId; - ctx.Keywords = model.SearchProductName; - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageIndex = command.Page - 1; - ctx.PageSize = command.PageSize; - ctx.ShowHidden = true; - ctx.ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null; - - var products = _productService.SearchProducts(ctx); + foreach (var product in products) + { + var existingProductManus = _manufacturerService.GetProductManufacturersByManufacturerId(manufacturerId, 0, int.MaxValue, true); - gridModel.Data = products.Select(x => + if (!existingProductManus.Any(x => x.ProductId == product.Id && x.ManufacturerId == manufacturerId)) + { + if (maxDisplayOrder == -1 && (productManu = existingProductManus.OrderByDescending(x => x.DisplayOrder).FirstOrDefault()) != null) + { + maxDisplayOrder = productManu.DisplayOrder; + } + + _manufacturerService.InsertProductManufacturer(new ProductManufacturer + { + ManufacturerId = manufacturerId, + ProductId = product.Id, + IsFeaturedProduct = false, + DisplayOrder = ++maxDisplayOrder + }); + } + } + } + else { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }); - gridModel.Total = products.TotalCount; - return new JsonResult - { - Data = gridModel - }; - } - - [HttpPost] - [FormValueRequired("save")] - public ActionResult ProductAddPopup(string btnId, string formId, ManufacturerModel.AddManufacturerProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (model.SelectedProductIds != null) - { - foreach (int id in model.SelectedProductIds) - { - var product = _productService.GetProductById(id); - if (product != null) - { - var existingProductmanufacturers = _manufacturerService.GetProductManufacturersByManufacturerId(model.ManufacturerId, 0, int.MaxValue, true); - - if (!existingProductmanufacturers.Any(x => x.ProductId == id && x.ManufacturerId == model.ManufacturerId)) - { - _manufacturerService.InsertProductManufacturer(new ProductManufacturer - { - ManufacturerId = model.ManufacturerId, - ProductId = id, - IsFeaturedProduct = false, - DisplayOrder = 1 - }); - } - } - } - } + NotifyAccessDenied(); + } - ViewBag.RefreshPage = true; - ViewBag.btnId = btnId; - ViewBag.formId = formId; - model.Products = new GridModel(); - return View(model); - } + return new EmptyResult(); + } - #endregion - } + #endregion + } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/MeasureController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/MeasureController.cs index ec1265db77..f8b33e6102 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/MeasureController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/MeasureController.cs @@ -79,20 +79,29 @@ public ActionResult Weights(string id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Weights(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + var model = new GridModel(); - var weightsModel = _measureService.GetAllMeasureWeights() - .Select(x => x.ToModel()) - .ForCommand(command) - .ToList(); - foreach (var wm in weightsModel) - wm.IsPrimaryWeight = wm.Id == _measureSettings.BaseWeightId; - var model = new GridModel - { - Data = weightsModel, - Total = weightsModel.Count - }; + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + var weightsModel = _measureService.GetAllMeasureWeights() + .Select(x => x.ToModel()) + .ForCommand(command) + .ToList(); + + foreach (var wm in weightsModel) + { + wm.IsPrimaryWeight = wm.Id == _measureSettings.BaseWeightId; + } + + model.Data = weightsModel; + model.Total = weightsModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -100,22 +109,22 @@ public ActionResult Weights(GridCommand command) }; } - [GridAction(EnableCustomBinding=true)] + [GridAction(EnableCustomBinding = true)] public ActionResult WeightUpdate(MeasureWeightModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); - - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - var weight = _measureService.GetMeasureWeightById(model.Id); - weight = model.ToEntity(weight); - _measureService.UpdateMeasureWeight(weight); + var weight = _measureService.GetMeasureWeightById(model.Id); + weight = model.ToEntity(weight); + + _measureService.UpdateMeasureWeight(weight); + } return Weights(command); } @@ -123,19 +132,19 @@ public ActionResult WeightUpdate(MeasureWeightModel model, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult WeightAdd([Bind(Exclude="Id")] MeasureWeightModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var weight = new MeasureWeight(); + weight = model.ToEntity(weight); - var weight = new MeasureWeight(); - weight = model.ToEntity(weight); - _measureService.InsertMeasureWeight(weight); + _measureService.InsertMeasureWeight(weight); + } return Weights(command); } @@ -143,17 +152,17 @@ public ActionResult WeightAdd([Bind(Exclude="Id")] MeasureWeightModel model, Gri [GridAction(EnableCustomBinding = true)] public ActionResult WeightDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); - - var weight = _measureService.GetMeasureWeightById(id); - if (weight == null) - throw new ArgumentException("No weight found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + var weight = _measureService.GetMeasureWeightById(id); - if (weight.Id == _measureSettings.BaseWeightId) - return Content(_localizationService.GetResource("Admin.Configuration.Measures.Weights.CantDeletePrimary")); + if (weight.Id == _measureSettings.BaseWeightId) + { + return Content(T("Admin.Configuration.Measures.Weights.CantDeletePrimary")); + } - _measureService.DeleteMeasureWeight(weight); + _measureService.DeleteMeasureWeight(weight); + } return Weights(command); } @@ -195,20 +204,29 @@ public ActionResult Dimensions(string id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Dimensions(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + var model = new GridModel(); - var dimensionsModel = _measureService.GetAllMeasureDimensions() - .Select(x => x.ToModel()) - .ForCommand(command) - .ToList(); - foreach (var wm in dimensionsModel) - wm.IsPrimaryDimension = wm.Id == _measureSettings.BaseDimensionId; - var model = new GridModel - { - Data = dimensionsModel, - Total = dimensionsModel.Count - }; + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + var dimensionsModel = _measureService.GetAllMeasureDimensions() + .Select(x => x.ToModel()) + .ForCommand(command) + .ToList(); + + foreach (var wm in dimensionsModel) + { + wm.IsPrimaryDimension = wm.Id == _measureSettings.BaseDimensionId; + } + + model.Data = dimensionsModel; + model.Total = dimensionsModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -219,19 +237,19 @@ public ActionResult Dimensions(GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult DimensionUpdate(MeasureDimensionModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var dimension = _measureService.GetMeasureDimensionById(model.Id); + dimension = model.ToEntity(dimension); - var dimension = _measureService.GetMeasureDimensionById(model.Id); - dimension = model.ToEntity(dimension); - _measureService.UpdateMeasureDimension(dimension); + _measureService.UpdateMeasureDimension(dimension); + } return Dimensions(command); } @@ -239,19 +257,19 @@ public ActionResult DimensionUpdate(MeasureDimensionModel model, GridCommand com [GridAction(EnableCustomBinding = true)] public ActionResult DimensionAdd([Bind(Exclude="Id")] MeasureDimensionModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var dimension = new MeasureDimension(); + dimension = model.ToEntity(dimension); - var dimension = new MeasureDimension(); - dimension = model.ToEntity(dimension); - _measureService.InsertMeasureDimension(dimension); + _measureService.InsertMeasureDimension(dimension); + } return Dimensions(command); } @@ -259,17 +277,17 @@ public ActionResult DimensionAdd([Bind(Exclude="Id")] MeasureDimensionModel mode [GridAction(EnableCustomBinding = true)] public ActionResult DimensionDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); - - var dimension = _measureService.GetMeasureDimensionById(id); - if (dimension == null) - throw new ArgumentException("No dimension found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageMeasures)) + { + var dimension = _measureService.GetMeasureDimensionById(id); - if (dimension.Id == _measureSettings.BaseDimensionId) - return Content(_localizationService.GetResource("Admin.Configuration.Measures.Dimensions.CantDeletePrimary")); + if (dimension.Id == _measureSettings.BaseDimensionId) + { + return Content(T("Admin.Configuration.Measures.Dimensions.CantDeletePrimary")); + } - _measureService.DeleteMeasureDimension(dimension); + _measureService.DeleteMeasureDimension(dimension); + } return Dimensions(command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs index aa64078b5f..b55656e0cc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/MediaController.cs @@ -53,7 +53,7 @@ private UploadFileResult UploadImageInternal() var postedFile = Request.ToPostedFileResult(); if (postedFile == null) { - return new UploadFileResult { Message = "No file uploaded" }; + return new UploadFileResult { Message = T("Common.NoFileUploaded") }; } if (postedFile.FileName.IsEmpty()) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs index be6c73a92b..e3a63e295d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/MessageTemplateController.cs @@ -149,11 +149,9 @@ public ActionResult List() var model = new MessageTemplateListModel(); - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); foreach (var s in _storeService.GetAllStores()) { - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + model.AvailableStores.Add(new SelectListItem { Text = s.Name, Value = s.Id.ToString() }); } return View(model); @@ -162,15 +160,21 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, MessageTemplateListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMessageTemplates)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var messageTemplates = _messageTemplateService.GetAllMessageTemplates(model.SearchStoreId); - var gridModel = new GridModel + if (_permissionService.Authorize(StandardPermissionProvider.ManageMessageTemplates)) { - Data = messageTemplates.Select(x => x.ToModel()), - Total = messageTemplates.Count - }; + var messageTemplates = _messageTemplateService.GetAllMessageTemplates(model.SearchStoreId); + + gridModel.Data = messageTemplates.Select(x => x.ToModel()); + gridModel.Total = messageTemplates.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/NewsController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/NewsController.cs index 2ac51b9184..9cf0e59ae5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/NewsController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/NewsController.cs @@ -104,10 +104,11 @@ public ActionResult List() return AccessDeniedView(); var model = new NewsItemListModel(); - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); + foreach (var s in _storeService.GetAllStores()) - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + { + model.AvailableStores.Add(new SelectListItem { Text = s.Name, Value = s.Id.ToString() }); + } return View(model); } @@ -115,26 +116,38 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, NewsItemListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNews)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageNews)) + { + var news = _newsService.GetAllNews(0, model.SearchStoreId, command.Page - 1, command.PageSize, true); + + gridModel.Data = news.Select(x => + { + var m = x.ToModel(); + + if (x.StartDateUtc.HasValue) + m.StartDate = _dateTimeHelper.ConvertToUserTime(x.StartDateUtc.Value, DateTimeKind.Utc); + + if (x.EndDateUtc.HasValue) + m.EndDate = _dateTimeHelper.ConvertToUserTime(x.EndDateUtc.Value, DateTimeKind.Utc); + + m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + m.LanguageName = x.Language.Name; + m.Comments = x.ApprovedCommentCount + x.NotApprovedCommentCount; + + return m; + }); + + gridModel.Total = news.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var news = _newsService.GetAllNews(0, model.SearchStoreId, command.Page - 1, command.PageSize, true); - var gridModel = new GridModel - { - Data = news.Select(x => - { - var m = x.ToModel(); - if (x.StartDateUtc.HasValue) - m.StartDate = _dateTimeHelper.ConvertToUserTime(x.StartDateUtc.Value, DateTimeKind.Utc); - if (x.EndDateUtc.HasValue) - m.EndDate = _dateTimeHelper.ConvertToUserTime(x.EndDateUtc.Value, DateTimeKind.Utc); - m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - m.LanguageName = x.Language.Name; - m.Comments = x.ApprovedCommentCount + x.NotApprovedCommentCount; - return m; - }), - Total = news.TotalCount - }; return new JsonResult { Data = gridModel @@ -297,47 +310,54 @@ public ActionResult Comments(int? filterByNewsItemId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Comments(int? filterByNewsItemId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNews)) - return AccessDeniedView(); + var gridModel = new GridModel(); - IList comments; - if (filterByNewsItemId.HasValue) - { - //filter comments by news item - var newsItem = _newsService.GetNewsById(filterByNewsItemId.Value); - comments = newsItem.NewsComments.OrderBy(bc => bc.CreatedOnUtc).ToList(); - } - else - { - //load all news comments - comments = _customerContentService.GetAllCustomerContent(0, null); - } + if (_permissionService.Authorize(StandardPermissionProvider.ManageNews)) + { + IList comments; + if (filterByNewsItemId.HasValue) + { + //filter comments by news item + var newsItem = _newsService.GetNewsById(filterByNewsItemId.Value); + comments = newsItem.NewsComments.OrderBy(bc => bc.CreatedOnUtc).ToList(); + } + else + { + //load all news comments + comments = _customerContentService.GetAllCustomerContent(0, null); + } - var gridModel = new GridModel - { - Data = comments.PagedForCommand(command).Select(newsComment => - { - var commentModel = new NewsCommentModel(); + gridModel.Data = comments.PagedForCommand(command).Select(newsComment => + { + var commentModel = new NewsCommentModel(); var customer = _customerService.GetCustomerById(newsComment.CustomerId); - commentModel.Id = newsComment.Id; - commentModel.NewsItemId = newsComment.NewsItemId; - commentModel.NewsItemTitle = newsComment.NewsItem.Title; - commentModel.CustomerId = newsComment.CustomerId; - commentModel.IpAddress = newsComment.IpAddress; - commentModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(newsComment.CreatedOnUtc, DateTimeKind.Utc); - commentModel.CommentTitle = newsComment.CommentTitle; - commentModel.CommentText = Core.Html.HtmlUtils.FormatText(newsComment.CommentText, false, true, false, false, false, false); + commentModel.Id = newsComment.Id; + commentModel.NewsItemId = newsComment.NewsItemId; + commentModel.NewsItemTitle = newsComment.NewsItem.Title; + commentModel.CustomerId = newsComment.CustomerId; + commentModel.IpAddress = newsComment.IpAddress; + commentModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(newsComment.CreatedOnUtc, DateTimeKind.Utc); + commentModel.CommentTitle = newsComment.CommentTitle; + commentModel.CommentText = Core.Html.HtmlUtils.FormatText(newsComment.CommentText, false, true, false, false, false, false); if (customer == null) commentModel.CustomerName = "".NaIfEmpty(); else commentModel.CustomerName = customer.GetFullName(); - return commentModel; - }), - Total = comments.Count, - }; + return commentModel; + }); + + gridModel.Total = comments.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = gridModel @@ -347,17 +367,16 @@ public ActionResult Comments(int? filterByNewsItemId, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult CommentDelete(int? filterByNewsItemId, int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNews)) - return AccessDeniedView(); - - var comment = _customerContentService.GetCustomerContentById(id) as NewsComment; - if (comment == null) - throw new ArgumentException("No comment found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageNews)) + { + var comment = _customerContentService.GetCustomerContentById(id) as NewsComment; - var newsItem = comment.NewsItem; - _customerContentService.DeleteCustomerContent(comment); - //update totals - _newsService.UpdateCommentTotals(newsItem); + var newsItem = comment.NewsItem; + _customerContentService.DeleteCustomerContent(comment); + + //update totals + _newsService.UpdateCommentTotals(newsItem); + } return Comments(filterByNewsItemId, command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/NewsLetterSubscriptionController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/NewsLetterSubscriptionController.cs index 8844c884fc..ac4bfd4929 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/NewsLetterSubscriptionController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/NewsLetterSubscriptionController.cs @@ -42,8 +42,7 @@ private void PrepareNewsLetterSubscriptionListModel(NewsLetterSubscriptionListMo model.GridPageSize = _adminAreaSettings.GridPageSize; - model.AvailableStores.Add(new SelectListItem { Text = T("Admin.Common.All"), Value = "0" }); - model.AvailableStores.AddRange(stores.ToSelectListItems()); + model.AvailableStores = stores.ToSelectListItems(); } public ActionResult Index() @@ -68,7 +67,7 @@ public ActionResult List() var store = _storeService.GetStoreById(x.StoreId); m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - m.StoreName = store != null ? store.Name : "Unknown"; + m.StoreName = store != null ? store.Name : "".NaIfEmpty(); return m; }), @@ -80,26 +79,33 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult SubscriptionList(GridCommand command, NewsLetterSubscriptionListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var newsletterSubscriptions = _newsLetterSubscriptionService.GetAllNewsLetterSubscriptions( - model.SearchEmail, command.Page - 1, command.PageSize, true, model.StoreId); + if (_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) + { + var newsletterSubscriptions = _newsLetterSubscriptionService.GetAllNewsLetterSubscriptions( + model.SearchEmail, command.Page - 1, command.PageSize, true, model.StoreId); - var gridModel = new GridModel - { - Data = newsletterSubscriptions.Select(x => + gridModel.Data = newsletterSubscriptions.Select(x => { var m = x.ToModel(); var store = _storeService.GetStoreById(x.StoreId); m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - m.StoreName = store != null ? store.Name : "Unknown"; + m.StoreName = store != null ? store.Name : "".NaIfEmpty(); return m; - }), - Total = newsletterSubscriptions.TotalCount - }; + }); + + gridModel.Total = newsletterSubscriptions.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = gridModel @@ -109,72 +115,41 @@ public ActionResult SubscriptionList(GridCommand command, NewsLetterSubscription [GridAction(EnableCustomBinding = true)] public ActionResult SubscriptionUpdate(NewsLetterSubscriptionModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) - return AccessDeniedView(); - - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + if (_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionById(model.Id); - subscription.Email = model.Email; - subscription.Active = model.Active; + var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionById(model.Id); + subscription.Email = model.Email; + subscription.Active = model.Active; - _newsLetterSubscriptionService.UpdateNewsLetterSubscription(subscription); + _newsLetterSubscriptionService.UpdateNewsLetterSubscription(subscription); + } var listModel = new NewsLetterSubscriptionListModel(); PrepareNewsLetterSubscriptionListModel(listModel); - return SubscriptionList(command, listModel); + return SubscriptionList(command, listModel); } [GridAction(EnableCustomBinding = true)] public ActionResult SubscriptionDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) - return AccessDeniedView(); - - var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionById(id); - if (subscription == null) - throw new ArgumentException("No subscription found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) + { + var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionById(id); - _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); + _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); + } var listModel = new NewsLetterSubscriptionListModel(); PrepareNewsLetterSubscriptionListModel(listModel); return SubscriptionList(command, listModel); } - - [HttpPost] - public ActionResult ImportCsv(FormCollection form) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers)) - return AccessDeniedView(); - - try - { - var file = Request.Files["importcsvfile"]; - if (file != null && file.ContentLength > 0) - { - var result = _newsLetterSubscriptionService.ImportSubscribers(file.InputStream); - - NotifySuccess(T("Admin.Promotions.NewsLetterSubscriptions.ImportEmailsSuccess", result.NewRecords, result.ModifiedRecords)); - } - else - { - NotifyError(T("Admin.Common.UploadFile")); - } - } - catch (Exception exc) - { - NotifyError(exc); - } - - return RedirectToAction("List"); - } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs index 222bb0863e..3aca0ddd54 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs @@ -81,27 +81,35 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) - return AccessDeniedView(); + var model = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) + { + var lastActivityFrom = DateTime.UtcNow.AddMinutes(-_customerSettings.OnlineCustomerMinutes); + var customers = _customerService.GetOnlineCustomers(lastActivityFrom, null, command.Page - 1, command.PageSize); + + model.Data = customers.Select(x => + { + return new OnlineCustomerModel + { + Id = x.Id, + CustomerInfo = x.IsRegistered() ? x.Email : T("Admin.Customers.Guest").Text, + LastIpAddress = x.LastIpAddress, + Location = _geoCountryLookup.LookupCountryName(x.LastIpAddress), + LastActivityDate = _dateTimeHelper.ConvertToUserTime(x.LastActivityDateUtc, DateTimeKind.Utc), + LastVisitedPage = x.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage) + }; + }); + + model.Total = customers.TotalCount; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var customers = _customerService.GetOnlineCustomers(DateTime.UtcNow.AddMinutes(-_customerSettings.OnlineCustomerMinutes), - null, command.Page - 1, command.PageSize); - var model = new GridModel - { - Data = customers.Select(x => - { - return new OnlineCustomerModel() - { - Id = x.Id, - CustomerInfo = x.IsRegistered() ? x.Email : _localizationService.GetResource("Admin.Customers.Guest"), - LastIpAddress = x.LastIpAddress, - Location = _geoCountryLookup.LookupCountryName(x.LastIpAddress), - LastActivityDate = _dateTimeHelper.ConvertToUserTime(x.LastActivityDateUtc, DateTimeKind.Utc), - LastVisitedPage = x.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage) - }; - }), - Total = customers.TotalCount - }; return new JsonResult { Data = model diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs index 0bdc537cac..e73b37d270 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs @@ -15,6 +15,7 @@ using SmartStore.Core.Domain.Tax; using SmartStore.Core.Events; using SmartStore.Core.Html; +using SmartStore.Core.Logging; using SmartStore.Services; using SmartStore.Services.Affiliates; using SmartStore.Services.Catalog; @@ -80,6 +81,7 @@ public partial class OrderController : AdminControllerBase private readonly ICustomerService _customerService; private readonly PluginMediator _pluginMediator; private readonly IAffiliateService _affiliateService; + private readonly ICustomerActivityService _customerActivityService; private readonly CatalogSettings _catalogSettings; private readonly CurrencySettings _currencySettings; @@ -87,8 +89,9 @@ public partial class OrderController : AdminControllerBase private readonly MeasureSettings _measureSettings; private readonly PdfSettings _pdfSettings; private readonly AddressSettings _addressSettings; + private readonly AdminAreaSettings _adminAreaSettings; - private readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter; + private readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter; private readonly IPdfConverter _pdfConverter; private readonly ICommonServices _services; private readonly Lazy _pictureService; @@ -119,9 +122,11 @@ public OrderController(IOrderService orderService, ICustomerService customerService, PluginMediator pluginMediator, IAffiliateService affiliateService, - CatalogSettings catalogSettings, CurrencySettings currencySettings, TaxSettings taxSettings, + ICustomerActivityService customerActivityService, + CatalogSettings catalogSettings, CurrencySettings currencySettings, TaxSettings taxSettings, MeasureSettings measureSettings, PdfSettings pdfSettings, AddressSettings addressSettings, - IPdfConverter pdfConverter, ICommonServices services, Lazy pictureService) + AdminAreaSettings adminAreaSettings, + IPdfConverter pdfConverter, ICommonServices services, Lazy pictureService) { this._orderService = orderService; this._orderReportService = orderReportService; @@ -156,6 +161,7 @@ public OrderController(IOrderService orderService, this._customerService = customerService; this._pluginMediator = pluginMediator; this._affiliateService = affiliateService; + this._customerActivityService = customerActivityService; this._catalogSettings = catalogSettings; this._currencySettings = currencySettings; @@ -163,6 +169,7 @@ public OrderController(IOrderService orderService, this._measureSettings = measureSettings; this._pdfSettings = pdfSettings; this._addressSettings = addressSettings; + this._adminAreaSettings = adminAreaSettings; this._checkoutAttributeFormatter = checkoutAttributeFormatter; _pdfConverter = pdfConverter; @@ -203,6 +210,7 @@ protected void PrepareOrderDetailsModel(OrderModel model, Order order) model.AffiliateId = order.AffiliateId; model.CustomerComment = order.CustomerOrderComment; model.HasNewPaymentNotification = order.HasNewPaymentNotification; + model.AcceptThirdPartyEmailHandOver = order.AcceptThirdPartyEmailHandOver; if (order.AffiliateId != 0) { @@ -577,7 +585,7 @@ protected OrderModel.AddOrderProductModel.ProductDetailsModel PrepareAddProductT { 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 customer = _workContext.CurrentCustomer; // TODO: we need a customer representing entity instance for backend work var order = _orderService.GetOrderById(orderId); @@ -783,70 +791,83 @@ public ActionResult List(OrderListModel model) if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) return AccessDeniedView(); - model.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); + var allStores = _storeService.GetAllStores(); + + model.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); model.AvailablePaymentStatuses = PaymentStatus.Pending.ToSelectList(false).ToList(); model.AvailableShippingStatuses = ShippingStatus.NotYetShipped.ToSelectList(false).ToList(); - //stores - model.AvailableStores.AddRange(_storeService.GetAllStores().ToList().ToSelectListItems()); - model.AvailableStores.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); + model.AvailableStores = allStores + .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) + .ToList(); - return View(model); + model.GridPageSize = _adminAreaSettings.GridPageSize; + + return View(model); } [GridAction(EnableCustomBinding = true)] public ActionResult OrderList(GridCommand command, OrderListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var gridModel = new GridModel(); - DateTime? startDateValue = (model.StartDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); - DateTime? endDateValue = (model.EndDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + DateTime? startDateValue = (model.StartDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); + DateTime? endDateValue = (model.EndDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); - var orderStatusIds = model.OrderStatusIds.ToIntArray(); - var paymentStatusIds = model.PaymentStatusIds.ToIntArray(); - var shippingStatusIds = model.ShippingStatusIds.ToIntArray(); + var orderStatusIds = model.OrderStatusIds.ToIntArray(); + var paymentStatusIds = model.PaymentStatusIds.ToIntArray(); + var shippingStatusIds = model.ShippingStatusIds.ToIntArray(); - var orders = _orderService.SearchOrders(model.StoreId, 0, startDateValue, endDateValue, orderStatusIds, paymentStatusIds, shippingStatusIds, - model.CustomerEmail, model.OrderGuid, model.OrderNumber, command.Page - 1, command.PageSize, model.CustomerName); + var orders = _orderService.SearchOrders(model.StoreId, 0, startDateValue, endDateValue, orderStatusIds, paymentStatusIds, shippingStatusIds, + model.CustomerEmail, model.OrderGuid, model.OrderNumber, command.Page - 1, command.PageSize, model.CustomerName); - var gridModel = new GridModel - { - Data = orders.Select(x => - { + gridModel.Data = orders.Select(x => + { var store = _storeService.GetStoreById(x.StoreId); - return new OrderModel - { - Id = x.Id, - OrderNumber = x.GetOrderNumber(), - StoreName = store != null ? store.Name : "Unknown", - OrderTotal = _priceFormatter.FormatPrice(x.OrderTotal, true, false), - OrderStatus = x.OrderStatus.GetLocalizedEnum(_localizationService, _workContext), - PaymentStatus = x.PaymentStatus.GetLocalizedEnum(_localizationService, _workContext), - ShippingStatus = x.ShippingStatus.GetLocalizedEnum(_localizationService, _workContext), - CustomerEmail = x.BillingAddress.Email, - CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), + return new OrderModel + { + Id = x.Id, + OrderNumber = x.GetOrderNumber(), + StoreName = (store != null ? store.Name : "".NaIfEmpty()), + OrderTotal = _priceFormatter.FormatPrice(x.OrderTotal, true, false), + OrderStatus = x.OrderStatus.GetLocalizedEnum(_localizationService, _workContext), + PaymentStatus = x.PaymentStatus.GetLocalizedEnum(_localizationService, _workContext), + ShippingStatus = x.ShippingStatus.GetLocalizedEnum(_localizationService, _workContext), + CustomerName = x.BillingAddress.GetFullName(), + CustomerEmail = x.BillingAddress.Email, + CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), HasNewPaymentNotification = x.HasNewPaymentNotification - }; - }), - Total = orders.TotalCount - }; + }; + }); + + gridModel.Total = orders.TotalCount; + + //summary report + //implemented as a workaround described here: http://www.telerik.com/community/forums/aspnet-mvc/grid/gridmodel-aggregates-how-to-use.aspx + var reportSummary = _orderReportService.GetOrderAverageReportLine(model.StoreId, orderStatusIds, + paymentStatusIds, shippingStatusIds, startDateValue, endDateValue, model.CustomerEmail); + + var profit = _orderReportService.ProfitReport(model.StoreId, orderStatusIds, + paymentStatusIds, shippingStatusIds, startDateValue, endDateValue, model.CustomerEmail); + + var aggregator = new OrderModel + { + aggregatorprofit = _priceFormatter.FormatPrice(profit, true, false), + aggregatortax = _priceFormatter.FormatPrice(reportSummary.SumTax, true, false), + aggregatortotal = _priceFormatter.FormatPrice(reportSummary.SumOrders, true, false) + }; + + gridModel.Aggregates = aggregator; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - //summary report - //implemented as a workaround described here: http://www.telerik.com/community/forums/aspnet-mvc/grid/gridmodel-aggregates-how-to-use.aspx - var reportSummary = _orderReportService.GetOrderAverageReportLine(model.StoreId, orderStatusIds, - paymentStatusIds, shippingStatusIds, startDateValue, endDateValue, model.CustomerEmail); - - var profit = _orderReportService.ProfitReport(model.StoreId, orderStatusIds, - paymentStatusIds, shippingStatusIds, startDateValue, endDateValue, model.CustomerEmail); - - var aggregator = new OrderModel() - { - aggregatorprofit = _priceFormatter.FormatPrice(profit, true, false), - aggregatortax = _priceFormatter.FormatPrice(reportSummary.SumTax, true, false), - aggregatortotal = _priceFormatter.FormatPrice(reportSummary.SumOrders, true, false) - }; - gridModel.Aggregates = aggregator; return new JsonResult { Data = gridModel @@ -1230,7 +1251,14 @@ public ActionResult Delete(int id) return RedirectToAction("List"); _orderProcessingService.DeleteOrder(order); - return RedirectToAction("List"); + + var msg = T("ActivityLog.DeleteOrder", order.GetOrderNumber()); + + _customerActivityService.InsertActivity("DeleteOrder", msg); + + NotifySuccess(msg); + + return RedirectToAction("List"); } public ActionResult Print(int orderId, bool pdf = false) @@ -1632,7 +1660,6 @@ public ActionResult AddProductToOrder(int orderId) var model = new OrderModel.AddOrderProductModel(); model.OrderId = orderId; - //categories var allCategories = _categoryService.GetAllCategories(showHidden: true); var mappedCategories = allCategories.ToDictionary(x => x.Id); foreach (var c in allCategories) @@ -1640,15 +1667,12 @@ public ActionResult AddProductToOrder(int orderId) model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); } - //manufacturers foreach (var m in _manufacturerService.GetAllManufacturers(true)) { model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); } - //product types model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); return View(model); } @@ -1656,36 +1680,46 @@ public ActionResult AddProductToOrder(int orderId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult AddProductToOrder(GridCommand command, OrderModel.AddOrderProductModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var gridModel = new GridModel(); - var searchContext = new ProductSearchContext() + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) { - CategoryIds = new List() { model.SearchCategoryId }, - ManufacturerId = model.SearchManufacturerId, - Keywords = model.SearchProductName, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - ShowHidden = true, - ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null - }; + var searchContext = new ProductSearchContext + { + CategoryIds = new List { model.SearchCategoryId }, + ManufacturerId = model.SearchManufacturerId, + Keywords = model.SearchProductName, + PageIndex = command.Page - 1, + PageSize = command.PageSize, + ShowHidden = true, + ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null + }; - var products = _productService.SearchProducts(searchContext); - gridModel.Data = products.Select(x => - { - var productModel = new OrderModel.AddOrderProductModel.ProductModel - { - Id = x.Id, - Name = x.Name, - Sku = x.Sku, - ProductTypeName = x.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = x.ProductTypeLabelHint - }; + var products = _productService.SearchProducts(searchContext); + + gridModel.Data = products.Select(x => + { + var productModel = new OrderModel.AddOrderProductModel.ProductModel + { + Id = x.Id, + Name = x.Name, + Sku = x.Sku, + ProductTypeName = x.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = x.ProductTypeLabelHint + }; + + return productModel; + }); + + gridModel.Total = products.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - return productModel; - }); - gridModel.Total = products.TotalCount; return new JsonResult { Data = gridModel @@ -1963,23 +1997,29 @@ public ActionResult ShipmentList() [GridAction(EnableCustomBinding = true)] public ActionResult ShipmentListSelect(GridCommand command, ShipmentListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var gridModel = new GridModel(); - DateTime? startDateValue = (model.StartDate == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + DateTime? startDateValue = (model.StartDate == null) ? null + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); - DateTime? endDateValue = (model.EndDate == null) ? null - :(DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + DateTime? endDateValue = (model.EndDate == null) ? null + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + + //load shipments + var shipments = _shipmentService.GetAllShipments(model.TrackingNumber, startDateValue, endDateValue, command.Page - 1, command.PageSize); + + gridModel.Data = shipments.Select(shipment => PrepareShipmentModel(shipment, false, false)); + gridModel.Total = shipments.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - //load shipments - var shipments = _shipmentService.GetAllShipments(model.TrackingNumber, startDateValue, endDateValue, - command.Page - 1, command.PageSize); - var gridModel = new GridModel - { - Data = shipments.Select(shipment => PrepareShipmentModel(shipment, false, false)), - Total = shipments.TotalCount - }; return new JsonResult { Data = gridModel @@ -1989,24 +2029,29 @@ public ActionResult ShipmentListSelect(GridCommand command, ShipmentListModel mo [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ShipmentsSelect(int orderId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var model = new GridModel(); - var order = _orderService.GetOrderById(orderId); - if (order == null) - throw new ArgumentException("No order found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var order = _orderService.GetOrderById(orderId); - //shipments - var shipmentModels = new List(); - var shipments = order.Shipments.OrderBy(s => s.CreatedOnUtc).ToList(); - foreach (var shipment in shipments) - shipmentModels.Add(PrepareShipmentModel(shipment, false, false)); + var shipmentModels = new List(); + var shipments = order.Shipments.OrderBy(s => s.CreatedOnUtc).ToList(); - var model = new GridModel - { - Data = shipmentModels, - Total = shipmentModels.Count - }; + foreach (var shipment in shipments) + { + shipmentModels.Add(PrepareShipmentModel(shipment, false, false)); + } + + model.Data = shipmentModels; + model.Total = shipmentModels.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -2291,37 +2336,40 @@ public ActionResult PdfPackagingSlips(bool all, string selectedIds = null) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult OrderNotesSelect(int orderId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var model = new GridModel(); - var order = _orderService.GetOrderById(orderId); - if (order == null) - throw new ArgumentException("No order found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var order = _orderService.GetOrderById(orderId); - var orderNoteModels = new List(); + var orderNoteModels = new List(); - foreach (var orderNote in order.OrderNotes.OrderByDescending(on => on.CreatedOnUtc)) - { - orderNoteModels.Add(new OrderModel.OrderNote - { - Id = orderNote.Id, - OrderId = orderNote.OrderId, - DisplayToCustomer = orderNote.DisplayToCustomer, - Note = orderNote.FormatOrderNoteText(), - CreatedOn = _dateTimeHelper.ConvertToUserTime(orderNote.CreatedOnUtc, DateTimeKind.Utc) - }); - } + foreach (var orderNote in order.OrderNotes.OrderByDescending(on => on.CreatedOnUtc)) + { + orderNoteModels.Add(new OrderModel.OrderNote + { + Id = orderNote.Id, + OrderId = orderNote.OrderId, + DisplayToCustomer = orderNote.DisplayToCustomer, + Note = orderNote.FormatOrderNoteText(), + CreatedOn = _dateTimeHelper.ConvertToUserTime(orderNote.CreatedOnUtc, DateTimeKind.Utc) + }); + } - var model = new GridModel - { - Data = orderNoteModels, - Total = orderNoteModels.Count - }; + model.Data = orderNoteModels; + model.Total = orderNoteModels.Count; - if (order.HasNewPaymentNotification) + if (order.HasNewPaymentNotification) + { + order.HasNewPaymentNotification = false; + _orderService.UpdateOrder(order); + } + } + else { - order.HasNewPaymentNotification = false; - _orderService.UpdateOrder(order); + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); } return new JsonResult @@ -2365,17 +2413,13 @@ public ActionResult OrderNoteAdd(int orderId, bool displayToCustomer, string mes [GridAction(EnableCustomBinding = true)] public ActionResult OrderNoteDelete(int orderId, int orderNoteId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); - - var order = _orderService.GetOrderById(orderId); - if (order == null) - throw new ArgumentException("No order found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var order = _orderService.GetOrderById(orderId); + var orderNote = order.OrderNotes.Where(on => on.Id == orderNoteId).FirstOrDefault(); - var orderNote = order.OrderNotes.Where(on => on.Id == orderNoteId).FirstOrDefault(); - if (orderNote == null) - throw new ArgumentException("No order note found with the specified id"); - _orderService.DeleteOrderNote(orderNote); + _orderService.DeleteOrderNote(orderNote); + } return OrderNotesSelect(orderId, command); } @@ -2413,12 +2457,14 @@ protected IList GetBestsellersBriefReportModel(int r return model; } - public ActionResult BestsellersBriefReportByQuantity() + + public ActionResult BestsellersBriefReportByQuantity() { var model = GetBestsellersBriefReportModel(5, 1); return PartialView(model); } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult BestsellersBriefReportByQuantityList(GridCommand command) { var model = GetBestsellersBriefReportModel(5, 1); @@ -2432,12 +2478,14 @@ public ActionResult BestsellersBriefReportByQuantityList(GridCommand command) Data = gridModel }; } - public ActionResult BestsellersBriefReportByAmount() + + public ActionResult BestsellersBriefReportByAmount() { var model = GetBestsellersBriefReportModel(5, 2); return PartialView(model); } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult BestsellersBriefReportByAmountList(GridCommand command) { var model = GetBestsellersBriefReportModel(5, 2); @@ -2452,51 +2500,46 @@ public ActionResult BestsellersBriefReportByAmountList(GridCommand command) }; } - - - public ActionResult BestsellersReport() { var model = new BestsellersReportModel(); - //order statuses model.AvailableOrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); - model.AvailableOrderStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - //payment statuses model.AvailablePaymentStatuses = PaymentStatus.Pending.ToSelectList(false).ToList(); - model.AvailablePaymentStatuses.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - //billing countries foreach (var c in _countryService.GetAllCountriesForBilling()) { - 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.AvailableCountries.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Address.SelectCountry"), Value = "0" }); + + model.AvailableCountries.Insert(0, new SelectListItem { Text = T("Admin.Address.SelectCountry"), Value = "0" }); return View(model); } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult BestsellersReportList(GridCommand command, BestsellersReportModel model) { DateTime? startDateValue = (model.StartDate == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); DateTime? endDateValue = (model.EndDate == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); OrderStatus? orderStatus = model.OrderStatusId > 0 ? (OrderStatus?)(model.OrderStatusId) : null; PaymentStatus? paymentStatus = model.PaymentStatusId > 0 ? (PaymentStatus?)(model.PaymentStatusId) : null; var items = _orderReportService.BestSellersReport(0, startDateValue, endDateValue, orderStatus, paymentStatus, null, model.BillingCountryId, 100, 2, true); + var gridModel = new GridModel { Data = items.Select(x => { var product = _productService.GetProductById(x.ProductId); - var m = new BestsellersReportLineModel() + var m = new BestsellersReportLineModel { ProductId = x.ProductId, TotalAmount = _priceFormatter.FormatPrice(x.TotalAmount, true, false), @@ -2513,6 +2556,7 @@ public ActionResult BestsellersReportList(GridCommand command, BestsellersReport }), Total = items.Count }; + return new JsonResult { Data = gridModel @@ -2526,17 +2570,18 @@ public ActionResult NeverSoldReport() var model = new NeverSoldReportModel(); return View(model); } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult NeverSoldReportList(GridCommand command, NeverSoldReportModel model) { DateTime? startDateValue = (model.StartDate == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.StartDate.Value, _dateTimeHelper.CurrentTimeZone); DateTime? endDateValue = (model.EndDate == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.EndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); - var items = _orderReportService.ProductsNeverSold(startDateValue, endDateValue, - command.Page - 1, command.PageSize, true); + var items = _orderReportService.ProductsNeverSold(startDateValue, endDateValue, command.Page - 1, command.PageSize, true); + var gridModel = new GridModel { Data = items.Select(x => @@ -2552,6 +2597,7 @@ public ActionResult NeverSoldReportList(GridCommand command, NeverSoldReportMode }), Total = items.TotalCount }; + return new JsonResult { Data = gridModel @@ -2660,13 +2706,15 @@ protected virtual IList GetOrderIncompleteReport return model; } - [ChildActionOnly] + + [ChildActionOnly] public ActionResult OrderIncompleteReport() { var model = GetOrderIncompleteReportModel(); return PartialView(model); } - [GridAction(EnableCustomBinding = true)] + + [GridAction(EnableCustomBinding = true)] public ActionResult OrderIncompleteReportList(GridCommand command) { var model = GetOrderIncompleteReportModel(); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs index 9cbc47f5f6..12b1cb92dd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PackagingController.cs @@ -45,11 +45,6 @@ public ActionResult UploadPackage(bool isTheme) [HttpPost] public ActionResult UploadPackage(FormCollection form, string returnUrl = "") { - if (returnUrl.IsEmpty()) - { - returnUrl = _services.WebHelper.GetUrlReferrer(); - } - bool isTheme = false; try @@ -69,7 +64,7 @@ public ActionResult UploadPackage(FormCollection form, string returnUrl = "") if (!file.FileExtension.IsCaseInsensitiveEqual(".nupkg")) { NotifyError(T("Admin.Packaging.NotAPackage")); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } var location = CommonHelper.MapPath("~/App_Data"); @@ -102,7 +97,7 @@ public ActionResult UploadPackage(FormCollection form, string returnUrl = "") else { NotifyError(T("Admin.Common.UploadFile")); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } if (!isTheme) @@ -110,13 +105,13 @@ public ActionResult UploadPackage(FormCollection form, string returnUrl = "") _services.WebHelper.RestartAppDomain(); } NotifySuccess(T("Admin.Packaging.InstallSuccess")); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } catch (Exception exc) { NotifyError(exc); Logger.Error(exc); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs index c3a1a7c9f9..971c96eaa9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PluginController.cs @@ -255,8 +255,8 @@ public ActionResult ReloadList() if (!_permissionService.Authorize(StandardPermissionProvider.ManagePlugins)) return AccessDeniedView(); - //restart application - _services.WebHelper.RestartAppDomain(); + // restart application + _services.WebHelper.RestartAppDomain(aggressive: true); return RedirectToAction("List"); } @@ -574,12 +574,7 @@ public ActionResult UpdateStringResources(string systemName, string returnUrl = NotifySuccess(T("Admin.Configuration.Plugins.Resources.UpdateSuccess")); } - if (returnUrl.IsEmpty()) - { - return RedirectToAction("List"); - } - - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl, () => RedirectToAction("List")); } public ActionResult UpdateAllStringResources() diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PollController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PollController.cs index c6c6abac89..efbf8ad4ad 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PollController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PollController.cs @@ -103,24 +103,36 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) + { + var polls = _pollService.GetPolls(0, false, command.Page - 1, command.PageSize, true); + + gridModel.Data = polls.Select(x => + { + var m = x.ToModel(); + + if (x.StartDateUtc.HasValue) + m.StartDate = _dateTimeHelper.ConvertToUserTime(x.StartDateUtc.Value, DateTimeKind.Utc); + + if (x.EndDateUtc.HasValue) + m.EndDate = _dateTimeHelper.ConvertToUserTime(x.EndDateUtc.Value, DateTimeKind.Utc); + + m.LanguageName = x.Language.Name; + + return m; + }); + + gridModel.Total = polls.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var polls = _pollService.GetPolls(0, false, command.Page - 1, command.PageSize, true); - var gridModel = new GridModel - { - Data = polls.Select(x => - { - var m = x.ToModel(); - if (x.StartDateUtc.HasValue) - m.StartDate = _dateTimeHelper.ConvertToUserTime(x.StartDateUtc.Value, DateTimeKind.Utc); - if (x.EndDateUtc.HasValue) - m.EndDate = _dateTimeHelper.ConvertToUserTime(x.EndDateUtc.Value, DateTimeKind.Utc); - m.LanguageName = x.Language.Name; - return m; - }), - Total = polls.TotalCount - }; return new JsonResult { Data = gridModel @@ -245,30 +257,35 @@ public ActionResult DeleteConfirmed(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult PollAnswers(int pollId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) - return AccessDeniedView(); + var model = new GridModel(); - var poll = _pollService.GetPollById(pollId); - if (poll == null) - throw new ArgumentException("No poll found with the specified id", "pollId"); + if (_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) + { + var poll = _pollService.GetPollById(pollId); + + var answers = poll.PollAnswers.OrderBy(x => x.DisplayOrder).ToList(); + + model.Data = answers.Select(x => + { + return new PollAnswerModel + { + Id = x.Id, + PollId = x.PollId, + Name = x.Name, + NumberOfVotes = x.NumberOfVotes, + DisplayOrder1 = x.DisplayOrder + }; + }); + + model.Total = answers.Count; + } + else + { + model.Data = Enumerable.Empty(); - var answers = poll.PollAnswers.OrderBy(x=>x.DisplayOrder).ToList(); + NotifyAccessDenied(); + } - var model = new GridModel - { - Data = answers.Select(x => - { - return new PollAnswerModel() - { - Id = x.Id, - PollId = x.PollId, - Name = x.Name, - NumberOfVotes = x.NumberOfVotes, - DisplayOrder1 = x.DisplayOrder - }; - }), - Total = answers.Count - }; return new JsonResult { Data = model @@ -279,23 +296,21 @@ public ActionResult PollAnswers(int pollId, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult PollAnswerUpdate(PollAnswerModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) - return AccessDeniedView(); - - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var pollAnswer = _pollService.GetPollAnswerById(model.Id); - var pollAnswer = _pollService.GetPollAnswerById(model.Id); - if (pollAnswer == null) - throw new ArgumentException("No poll answer found with the specified id", "id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } + + pollAnswer.Name = model.Name; + pollAnswer.DisplayOrder = model.DisplayOrder1; - pollAnswer.Name = model.Name; - pollAnswer.DisplayOrder = model.DisplayOrder1; - _pollService.UpdatePoll(pollAnswer.Poll); + _pollService.UpdatePoll(pollAnswer.Poll); + } return PollAnswers(pollAnswer.PollId, command); } @@ -303,44 +318,39 @@ public ActionResult PollAnswerUpdate(PollAnswerModel model, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult PollAnswerAdd(int pollId, PollAnswerModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) - return AccessDeniedView(); - - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + if (_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - var poll = _pollService.GetPollById(pollId); - if (poll == null) - throw new ArgumentException("No poll found with the specified id", "pollId"); + var poll = _pollService.GetPollById(pollId); - poll.PollAnswers.Add(new PollAnswer - { - Name = model.Name, - DisplayOrder = model.DisplayOrder1 - }); - _pollService.UpdatePoll(poll); + poll.PollAnswers.Add(new PollAnswer + { + Name = model.Name, + DisplayOrder = model.DisplayOrder1 + }); + + _pollService.UpdatePoll(poll); + } - return PollAnswers(poll.Id, command); + return PollAnswers(pollId, command); } [GridAction(EnableCustomBinding = true)] public ActionResult PollAnswerDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) - return AccessDeniedView(); - - var pollAnswer = _pollService.GetPollAnswerById(id); - if (pollAnswer == null) - throw new ArgumentException("No poll answer found with the specified id", "id"); - - int pollId = pollAnswer.PollId; - _pollService.DeletePollAnswer(pollAnswer); + var pollAnswer = _pollService.GetPollAnswerById(id); + var pollId = pollAnswer.PollId; + if (_permissionService.Authorize(StandardPermissionProvider.ManagePolls)) + { + _pollService.DeletePollAnswer(pollAnswer); + } return PollAnswers(pollId, command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductAttributeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductAttributeController.cs index 9381bd50ee..ea2a6ae606 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductAttributeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductAttributeController.cs @@ -91,29 +91,35 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productAttributes = _productAttributeService.GetAllProductAttributes(); + + gridModel.Data = productAttributes.Select(x => x.ToModel()); + gridModel.Total = productAttributes.Count(); + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var productAttributes = _productAttributeService.GetAllProductAttributes(); - var gridModel = new GridModel - { - Data = productAttributes.Select(x => x.ToModel()), - Total = productAttributes.Count() - }; return new JsonResult { Data = gridModel }; } - //create public ActionResult Create() { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) return AccessDeniedView(); var model = new ProductAttributeModel(); - //locales + AddLocales(_languageService, model.Locales); return View(model); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs index d677730a02..752a19c41c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductController.cs @@ -3,16 +3,13 @@ using System.ComponentModel; using System.Data; using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Web; using System.Web.Mvc; using Autofac; using Newtonsoft.Json; using SmartStore.Admin.Models.Catalog; using SmartStore.Collections; using SmartStore.Core; -using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; @@ -22,13 +19,11 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Seo; using SmartStore.Core.Events; -using SmartStore.Core.Infrastructure; using SmartStore.Core.Logging; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; -using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Directory; using SmartStore.Services.Discounts; using SmartStore.Services.Helpers; @@ -41,10 +36,10 @@ using SmartStore.Services.Tax; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; -using Telerik.Web.Mvc; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; +using Telerik.Web.Mvc; namespace SmartStore.Admin.Controllers { @@ -115,7 +110,6 @@ public ProductController( ITaxCategoryService taxCategoryService, IProductTagService productTagService, ICopyProductService copyProductService, - IImportManager importManager, ICustomerActivityService customerActivityService, IPermissionService permissionService, IAclService aclService, @@ -222,7 +216,7 @@ protected void UpdateProductGeneralInfo(Product product, ProductModel model) p.GiftCardTypeId = m.GiftCardTypeId; p.IsDownload = m.IsDownload; - p.DownloadId = m.DownloadId; + p.DownloadId = m.DownloadId ?? 0; p.UnlimitedDownloads = m.UnlimitedDownloads; p.MaxNumberOfDownloads = m.MaxNumberOfDownloads; p.DownloadExpirationDays = m.DownloadExpirationDays; @@ -878,7 +872,6 @@ public ActionResult List(ProductListModel model) model.DisplayProductPictures = _adminAreaSettings.DisplayProductPictures; model.GridPageSize = _adminAreaSettings.GridPageSize; - model.StoreCount = allStores.Count; var allCategories = _categoryService.GetAllCategories(showHidden: true); var mappedCategories = allCategories.ToDictionary(x => x.Id); @@ -902,67 +895,84 @@ public ActionResult List(ProductListModel model) return View(model); } - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult ProductList(GridCommand command, ProductListModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + [HttpPost, GridAction(EnableCustomBinding = true)] + public ActionResult ProductList(GridCommand command, ProductListModel model) + { + var gridModel = new GridModel(); - var gridModel = new GridModel(); - var searchContext = new ProductSearchContext - { - ManufacturerId = model.SearchManufacturerId, - StoreId = model.SearchStoreId, - Keywords = model.SearchProductName, - SearchSku = !_catalogSettings.SuppressSkuSearch, - LanguageId = _workContext.WorkingLanguage.Id, - OrderBy = ProductSortingEnum.Position, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - ShowHidden = true, - ProductType = (model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null), - WithoutCategories = model.SearchWithoutCategories, - WithoutManufacturers = model.SearchWithoutManufacturers, - IsPublished = model.SearchIsPublished, - HomePageProducts = model.SearchHomePageProducts - }; + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var searchContext = new ProductSearchContext + { + ManufacturerId = model.SearchManufacturerId, + StoreId = model.SearchStoreId, + Keywords = model.SearchProductName, + SearchSku = !_catalogSettings.SuppressSkuSearch, + LanguageId = _workContext.WorkingLanguage.Id, + OrderBy = ProductSortingEnum.Position, + PageIndex = command.Page - 1, + PageSize = command.PageSize, + ShowHidden = true, + ProductType = (model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null), + WithoutCategories = model.SearchWithoutCategories, + WithoutManufacturers = model.SearchWithoutManufacturers, + IsPublished = model.SearchIsPublished, + HomePageProducts = model.SearchHomePageProducts + }; - if (model.SearchCategoryId > 0) - searchContext.CategoryIds.Add(model.SearchCategoryId); + if (model.SearchCategoryId > 0) + searchContext.CategoryIds.Add(model.SearchCategoryId); - if (command.SortDescriptors != null && command.SortDescriptors.Count > 0) - { - var sort = command.SortDescriptors.First(); - if (sort.Member == "Name") - { - searchContext.OrderBy = (sort.SortDirection == ListSortDirection.Ascending ? ProductSortingEnum.NameAsc : ProductSortingEnum.NameDesc); - } - else if (sort.Member == "Price") + if (command.SortDescriptors != null && command.SortDescriptors.Count > 0) { - searchContext.OrderBy = (sort.SortDirection == ListSortDirection.Ascending ? ProductSortingEnum.PriceAsc : ProductSortingEnum.PriceDesc); + var sort = command.SortDescriptors.First(); + if (sort.Member == "Name") + { + searchContext.OrderBy = (sort.SortDirection == ListSortDirection.Ascending ? ProductSortingEnum.NameAsc : ProductSortingEnum.NameDesc); + } + else if (sort.Member == "Price") + { + searchContext.OrderBy = (sort.SortDirection == ListSortDirection.Ascending ? ProductSortingEnum.PriceAsc : ProductSortingEnum.PriceDesc); + } + else if (sort.Member == "CreatedOn") + { + searchContext.OrderBy = (sort.SortDirection == ListSortDirection.Ascending ? ProductSortingEnum.CreatedOnAsc : ProductSortingEnum.CreatedOn); + } } - else if (sort.Member == "CreatedOn") + + var products = _productService.SearchProducts(searchContext); + + gridModel.Data = products.Select(x => { - searchContext.OrderBy = (sort.SortDirection == ListSortDirection.Ascending ? ProductSortingEnum.CreatedOnAsc : ProductSortingEnum.CreatedOn); - } - } + var productModel = new ProductModel + { + Sku = x.Sku, + Published = x.Published, + ProductTypeLabelHint = x.ProductTypeLabelHint, + Name = x.Name, + Id = x.Id, + StockQuantity = x.StockQuantity, + Price = x.Price, + LimitedToStores = x.LimitedToStores + }; - var products = _productService.SearchProducts(searchContext); + PrepareProductPictureThumbnailModel(productModel, x); - gridModel.Data = products.Select(x => - { - var productModel = x.ToModel(); - productModel.FullDescription = ""; // Perf - PrepareProductPictureThumbnailModel(productModel, x); + productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); + productModel.UpdatedOn = _dateTimeHelper.ConvertToUserTime(x.UpdatedOnUtc, DateTimeKind.Utc); + productModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - productModel.UpdatedOn = _dateTimeHelper.ConvertToUserTime(x.UpdatedOnUtc, DateTimeKind.Utc); - productModel.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + return productModel; + }); - return productModel; - }); + gridModel.Total = products.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); - gridModel.Total = products.TotalCount; + NotifyAccessDenied(); + } return new JsonResult { @@ -1277,13 +1287,13 @@ public ActionResult CopyProduct(ProductModel model) var product = _productService.GetProductById(copyModel.Id); var newProduct = _copyProductService.CopyProduct(product, copyModel.Name, copyModel.Published, copyModel.CopyImages); - NotifySuccess("The product is copied"); + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); return RedirectToAction("Edit", new { id = newProduct.Id }); } catch (Exception exc) { - NotifyError(exc.Message); + NotifyError(exc.ToAllMessages()); return RedirectToAction("Edit", new { id = copyModel.Id }); } } @@ -1295,30 +1305,35 @@ public ActionResult CopyProduct(ProductModel model) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductCategoryList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var productCategories = _categoryService.GetProductCategoriesByProductId(productId, true); - var productCategoriesModel = productCategories - .Select(x => - { - return new ProductModel.ProductCategoryModel() - { - Id = x.Id, - Category = _categoryService.GetCategoryById(x.CategoryId).GetCategoryBreadCrumb(_categoryService), - ProductId = x.ProductId, - CategoryId = x.CategoryId, - IsFeaturedProduct = x.IsFeaturedProduct, - DisplayOrder = x.DisplayOrder - }; - }) - .ToList(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productCategories = _categoryService.GetProductCategoriesByProductId(productId, true); + var productCategoriesModel = productCategories + .Select(x => + { + return new ProductModel.ProductCategoryModel() + { + Id = x.Id, + Category = _categoryService.GetCategoryById(x.CategoryId).GetCategoryBreadCrumb(_categoryService), + ProductId = x.ProductId, + CategoryId = x.CategoryId, + IsFeaturedProduct = x.IsFeaturedProduct, + DisplayOrder = x.DisplayOrder + }; + }) + .ToList(); - var model = new GridModel - { - Data = productCategoriesModel, - Total = productCategoriesModel.Count - }; + model.Data = productCategoriesModel; + model.Total = productCategoriesModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1329,23 +1344,23 @@ public ActionResult ProductCategoryList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult ProductCategoryInsert(GridCommand command, ProductModel.ProductCategoryModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productCategory = new ProductCategory + { + ProductId = model.ProductId, + CategoryId = Int32.Parse(model.Category), //use Category property (not CategoryId) because appropriate property is stored in it + IsFeaturedProduct = model.IsFeaturedProduct, + DisplayOrder = model.DisplayOrder + }; - var productCategory = new ProductCategory() - { - ProductId = model.ProductId, - CategoryId = Int32.Parse(model.Category), //use Category property (not CategoryId) because appropriate property is stored in it - IsFeaturedProduct = model.IsFeaturedProduct, - DisplayOrder = model.DisplayOrder - }; - _categoryService.InsertProductCategory(productCategory); + _categoryService.InsertProductCategory(productCategory); - - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), - model.Category, _catalogSettings.MostRecentlyUsedCategoriesMaxSize); + var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), + model.Category, _catalogSettings.MostRecentlyUsedCategoriesMaxSize); - _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedCategories, mru.ToString()); + _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedCategories, mru.ToString()); + } return ProductCategoryList(command, model.ProductId); } @@ -1353,27 +1368,25 @@ public ActionResult ProductCategoryInsert(GridCommand command, ProductModel.Prod [GridAction(EnableCustomBinding = true)] public ActionResult ProductCategoryUpdate(GridCommand command, ProductModel.ProductCategoryModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productCategory = _categoryService.GetProductCategoryById(model.Id); - var productCategory = _categoryService.GetProductCategoryById(model.Id); - if (productCategory == null) - throw new ArgumentException("No product category mapping found with the specified id"); - - bool categoryChanged = (Int32.Parse(model.Category) != productCategory.CategoryId); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var categoryChanged = (Int32.Parse(model.Category) != productCategory.CategoryId); - //use Category property (not CategoryId) because appropriate property is stored in it - productCategory.CategoryId = Int32.Parse(model.Category); - productCategory.IsFeaturedProduct = model.IsFeaturedProduct; - productCategory.DisplayOrder = model.DisplayOrder; - _categoryService.UpdateProductCategory(productCategory); + //use Category property (not CategoryId) because appropriate property is stored in it + productCategory.CategoryId = Int32.Parse(model.Category); + productCategory.IsFeaturedProduct = model.IsFeaturedProduct; + productCategory.DisplayOrder = model.DisplayOrder; + _categoryService.UpdateProductCategory(productCategory); - if (categoryChanged) - { - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), - model.Category, _catalogSettings.MostRecentlyUsedCategoriesMaxSize); + if (categoryChanged) + { + var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedCategories), + model.Category, _catalogSettings.MostRecentlyUsedCategoriesMaxSize); - _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedCategories, mru.ToString()); + _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedCategories, mru.ToString()); + } } return ProductCategoryList(command, productCategory.ProductId); @@ -1382,15 +1395,13 @@ public ActionResult ProductCategoryUpdate(GridCommand command, ProductModel.Prod [GridAction(EnableCustomBinding = true)] public ActionResult ProductCategoryDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var productCategory = _categoryService.GetProductCategoryById(id); - if (productCategory == null) - throw new ArgumentException("No product category mapping found with the specified id"); + var productCategory = _categoryService.GetProductCategoryById(id); + var productId = productCategory.ProductId; - var productId = productCategory.ProductId; - _categoryService.DeleteProductCategory(productCategory); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _categoryService.DeleteProductCategory(productCategory); + } return ProductCategoryList(command, productId); } @@ -1402,30 +1413,35 @@ public ActionResult ProductCategoryDelete(int id, GridCommand command) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductManufacturerList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var productManufacturers = _manufacturerService.GetProductManufacturersByProductId(productId, true); - var productManufacturersModel = productManufacturers - .Select(x => - { - return new ProductModel.ProductManufacturerModel() - { - Id = x.Id, - Manufacturer = _manufacturerService.GetManufacturerById(x.ManufacturerId).Name, - ProductId = x.ProductId, - ManufacturerId = x.ManufacturerId, - IsFeaturedProduct = x.IsFeaturedProduct, - DisplayOrder = x.DisplayOrder - }; - }) - .ToList(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productManufacturers = _manufacturerService.GetProductManufacturersByProductId(productId, true); + var productManufacturersModel = productManufacturers + .Select(x => + { + return new ProductModel.ProductManufacturerModel + { + Id = x.Id, + Manufacturer = _manufacturerService.GetManufacturerById(x.ManufacturerId).Name, + ProductId = x.ProductId, + ManufacturerId = x.ManufacturerId, + IsFeaturedProduct = x.IsFeaturedProduct, + DisplayOrder = x.DisplayOrder + }; + }) + .ToList(); - var model = new GridModel - { - Data = productManufacturersModel, - Total = productManufacturersModel.Count - }; + model.Data = productManufacturersModel; + model.Total = productManufacturersModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1436,22 +1452,23 @@ public ActionResult ProductManufacturerList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult ProductManufacturerInsert(GridCommand command, ProductModel.ProductManufacturerModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productManufacturer = new ProductManufacturer + { + ProductId = model.ProductId, + ManufacturerId = Int32.Parse(model.Manufacturer), //use Manufacturer property (not ManufacturerId) because appropriate property is stored in it + IsFeaturedProduct = model.IsFeaturedProduct, + DisplayOrder = model.DisplayOrder + }; - var productManufacturer = new ProductManufacturer() - { - ProductId = model.ProductId, - ManufacturerId = Int32.Parse(model.Manufacturer), //use Manufacturer property (not ManufacturerId) because appropriate property is stored in it - IsFeaturedProduct = model.IsFeaturedProduct, - DisplayOrder = model.DisplayOrder - }; - _manufacturerService.InsertProductManufacturer(productManufacturer); + _manufacturerService.InsertProductManufacturer(productManufacturer); - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), - model.Manufacturer, _catalogSettings.MostRecentlyUsedManufacturersMaxSize); + var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), + model.Manufacturer, _catalogSettings.MostRecentlyUsedManufacturersMaxSize); - _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedManufacturers, mru.ToString()); + _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedManufacturers, mru.ToString()); + } return ProductManufacturerList(command, model.ProductId); } @@ -1459,27 +1476,26 @@ public ActionResult ProductManufacturerInsert(GridCommand command, ProductModel. [GridAction(EnableCustomBinding = true)] public ActionResult ProductManufacturerUpdate(GridCommand command, ProductModel.ProductManufacturerModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productManufacturer = _manufacturerService.GetProductManufacturerById(model.Id); - var productManufacturer = _manufacturerService.GetProductManufacturerById(model.Id); - if (productManufacturer == null) - throw new ArgumentException("No product manufacturer mapping found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var manufacturerChanged = (Int32.Parse(model.Manufacturer) != productManufacturer.ManufacturerId); - bool manufacturerChanged = (Int32.Parse(model.Manufacturer) != productManufacturer.ManufacturerId); + //use Manufacturer property (not ManufacturerId) because appropriate property is stored in it + productManufacturer.ManufacturerId = Int32.Parse(model.Manufacturer); + productManufacturer.IsFeaturedProduct = model.IsFeaturedProduct; + productManufacturer.DisplayOrder = model.DisplayOrder; - //use Manufacturer property (not ManufacturerId) because appropriate property is stored in it - productManufacturer.ManufacturerId = Int32.Parse(model.Manufacturer); - productManufacturer.IsFeaturedProduct = model.IsFeaturedProduct; - productManufacturer.DisplayOrder = model.DisplayOrder; - _manufacturerService.UpdateProductManufacturer(productManufacturer); + _manufacturerService.UpdateProductManufacturer(productManufacturer); - if (manufacturerChanged) - { - var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), - model.Manufacturer, _catalogSettings.MostRecentlyUsedManufacturersMaxSize); + if (manufacturerChanged) + { + var mru = new MostRecentlyUsedList(_workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.MostRecentlyUsedManufacturers), + model.Manufacturer, _catalogSettings.MostRecentlyUsedManufacturersMaxSize); - _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedManufacturers, mru.ToString()); + _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.MostRecentlyUsedManufacturers, mru.ToString()); + } } return ProductManufacturerList(command, productManufacturer.ProductId); @@ -1488,15 +1504,13 @@ public ActionResult ProductManufacturerUpdate(GridCommand command, ProductModel. [GridAction(EnableCustomBinding = true)] public ActionResult ProductManufacturerDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productManufacturer = _manufacturerService.GetProductManufacturerById(id); + var productId = productManufacturer.ProductId; - var productManufacturer = _manufacturerService.GetProductManufacturerById(id); - if (productManufacturer == null) - throw new ArgumentException("No product manufacturer mapping found with the specified id"); - - var productId = productManufacturer.ProductId; - _manufacturerService.DeleteProductManufacturer(productManufacturer); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _manufacturerService.DeleteProductManufacturer(productManufacturer); + } return ProductManufacturerList(command, productId); } @@ -1508,34 +1522,39 @@ public ActionResult ProductManufacturerDelete(int id, GridCommand command) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult RelatedProductList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var relatedProducts = _productService.GetRelatedProductsByProductId1(productId, true); - var relatedProductsModel = relatedProducts - .Select(x => - { - var product2 = _productService.GetProductById(x.ProductId2); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var relatedProducts = _productService.GetRelatedProductsByProductId1(productId, true); + var relatedProductsModel = relatedProducts + .Select(x => + { + var product2 = _productService.GetProductById(x.ProductId2); - return new ProductModel.RelatedProductModel() - { - Id = x.Id, - ProductId2 = x.ProductId2, - Product2Name = product2.Name, - ProductTypeName = product2.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = product2.ProductTypeLabelHint, - DisplayOrder = x.DisplayOrder, - Product2Sku = product2.Sku, - Product2Published = product2.Published - }; - }) - .ToList(); + return new ProductModel.RelatedProductModel() + { + Id = x.Id, + ProductId2 = x.ProductId2, + Product2Name = product2.Name, + ProductTypeName = product2.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = product2.ProductTypeLabelHint, + DisplayOrder = x.DisplayOrder, + Product2Sku = product2.Sku, + Product2Published = product2.Published + }; + }) + .ToList(); - var model = new GridModel - { - Data = relatedProductsModel, - Total = relatedProductsModel.Count - }; + model.Data = relatedProductsModel; + model.Total = relatedProductsModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1546,15 +1565,13 @@ public ActionResult RelatedProductList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult RelatedProductUpdate(GridCommand command, ProductModel.RelatedProductModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var relatedProduct = _productService.GetRelatedProductById(model.Id); - if (relatedProduct == null) - throw new ArgumentException("No related product found with the specified id"); + var relatedProduct = _productService.GetRelatedProductById(model.Id); - relatedProduct.DisplayOrder = model.DisplayOrder; - _productService.UpdateRelatedProduct(relatedProduct); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + relatedProduct.DisplayOrder = model.DisplayOrder; + _productService.UpdateRelatedProduct(relatedProduct); + } return RelatedProductList(command, relatedProduct.ProductId1); } @@ -1562,148 +1579,56 @@ public ActionResult RelatedProductUpdate(GridCommand command, ProductModel.Relat [GridAction(EnableCustomBinding = true)] public ActionResult RelatedProductDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var relatedProduct = _productService.GetRelatedProductById(id); - if (relatedProduct == null) - throw new ArgumentException("No related product found with the specified id"); + var relatedProduct = _productService.GetRelatedProductById(id); + var productId = relatedProduct.ProductId1; - var productId = relatedProduct.ProductId1; - _productService.DeleteRelatedProduct(relatedProduct); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _productService.DeleteRelatedProduct(relatedProduct); + } return RelatedProductList(command, productId); } - public ActionResult RelatedProductAddPopup(int productId) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var ctx = new ProductSearchContext(); - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageSize = _adminAreaSettings.GridPageSize; - ctx.ShowHidden = true; - - var products = _productService.SearchProducts(ctx); + [HttpPost] + public ActionResult RelatedProductAdd(int productId, string selectedProductIds) + { + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productIds = selectedProductIds.SplitSafe(",").Select(x => x.ToInt()).ToArray(); + var products = _productService.GetProductsByIds(productIds); + RelatedProduct relation = null; + var maxDisplayOrder = -1; - var model = new ProductModel.AddRelatedProductModel(); - model.Products = new GridModel - { - Data = products.Select(x => + foreach (var product in products) { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }), - Total = products.TotalCount - }; - - //categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) - { - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - } - - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - foreach (var s in _storeService.GetAllStores()) - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); - - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult RelatedProductAddPopupList(GridCommand command, ProductModel.AddRelatedProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var gridModel = new GridModel(); - - var ctx = new ProductSearchContext(); - - if (model.SearchCategoryId > 0) - ctx.CategoryIds.Add(model.SearchCategoryId); + var existingRelations = _productService.GetRelatedProductsByProductId1(productId); - ctx.ManufacturerId = model.SearchManufacturerId; - ctx.StoreId = model.SearchStoreId; - ctx.Keywords = model.SearchProductName; - ctx.SearchSku = !_catalogSettings.SuppressSkuSearch; - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageIndex = command.Page - 1; - ctx.PageSize = command.PageSize; - ctx.ShowHidden = true; - ctx.ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null; + if (existingRelations.FindRelatedProduct(productId, product.Id) == null) + { + if (maxDisplayOrder == -1 && (relation = existingRelations.OrderByDescending(x => x.DisplayOrder).FirstOrDefault()) != null) + { + maxDisplayOrder = relation.DisplayOrder; + } - var products = _productService.SearchProducts(ctx); - gridModel.Data = products.Select(x => + _productService.InsertRelatedProduct(new RelatedProduct + { + ProductId1 = productId, + ProductId2 = product.Id, + DisplayOrder = ++maxDisplayOrder + }); + } + } + } + else { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }); - gridModel.Total = products.TotalCount; - return new JsonResult - { - Data = gridModel - }; - } - - [HttpPost] - [FormValueRequired("save")] - public ActionResult RelatedProductAddPopup(string btnId, string formId, ProductModel.AddRelatedProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (model.SelectedProductIds != null) - { - foreach (int id in model.SelectedProductIds) - { - var product = _productService.GetProductById(id); - if (product != null) - { - var existingRelatedProducts = _productService.GetRelatedProductsByProductId1(model.ProductId); - if (existingRelatedProducts.FindRelatedProduct(model.ProductId, id) == null) - { - _productService.InsertRelatedProduct( - new RelatedProduct() - { - ProductId1 = model.ProductId, - ProductId2 = id, - DisplayOrder = 1 - }); - } - } - } - } + NotifyAccessDenied(); + } - ViewBag.RefreshPage = true; - ViewBag.btnId = btnId; - ViewBag.formId = formId; - model.Products = new GridModel(); - return View(model); - } + return new EmptyResult(); + } - [HttpPost] - [ValidateInput(false)] + [HttpPost, ValidateInput(false)] public ActionResult CreateAllMutuallyRelatedProducts(int productId) { string message = null; @@ -1713,13 +1638,9 @@ public ActionResult CreateAllMutuallyRelatedProducts(int productId) var product = _productService.GetProductById(productId); if (product != null) { - int count = _productService.EnsureMutuallyRelatedProducts(productId); + var count = _productService.EnsureMutuallyRelatedProducts(productId); message = T("Admin.Common.CreateMutuallyAssociationsResult", count); } - else - { - message = "No product found with the specified id"; - } } else { @@ -1739,33 +1660,38 @@ public ActionResult CreateAllMutuallyRelatedProducts(int productId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult CrossSellProductList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var crossSellProducts = _productService.GetCrossSellProductsByProductId1(productId, true); - var crossSellProductsModel = crossSellProducts - .Select(x => - { - var product2 = _productService.GetProductById(x.ProductId2); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var crossSellProducts = _productService.GetCrossSellProductsByProductId1(productId, true); + var crossSellProductsModel = crossSellProducts + .Select(x => + { + var product2 = _productService.GetProductById(x.ProductId2); - return new ProductModel.CrossSellProductModel() - { - Id = x.Id, - ProductId2 = x.ProductId2, - Product2Name = product2.Name, - ProductTypeName = product2.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = product2.ProductTypeLabelHint, - Product2Sku = product2.Sku, - Product2Published = product2.Published - }; - }) - .ToList(); + return new ProductModel.CrossSellProductModel + { + Id = x.Id, + ProductId2 = x.ProductId2, + Product2Name = product2.Name, + ProductTypeName = product2.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = product2.ProductTypeLabelHint, + Product2Sku = product2.Sku, + Product2Published = product2.Published + }; + }) + .ToList(); - var model = new GridModel - { - Data = crossSellProductsModel, - Total = crossSellProductsModel.Count - }; + model.Data = crossSellProductsModel; + model.Total = crossSellProductsModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1776,148 +1702,48 @@ public ActionResult CrossSellProductList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult CrossSellProductDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var crossSellProduct = _productService.GetCrossSellProductById(id); + var productId = crossSellProduct.ProductId1; - var crossSellProduct = _productService.GetCrossSellProductById(id); - if (crossSellProduct == null) - throw new ArgumentException("No cross-sell product found with the specified id"); - - var productId = crossSellProduct.ProductId1; - _productService.DeleteCrossSellProduct(crossSellProduct); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _productService.DeleteCrossSellProduct(crossSellProduct); + } return CrossSellProductList(command, productId); } - public ActionResult CrossSellProductAddPopup(int productId) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var ctx = new ProductSearchContext(); - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageSize = _adminAreaSettings.GridPageSize; - ctx.ShowHidden = true; - - var products = _productService.SearchProducts(ctx); + [HttpPost] + public ActionResult CrossSellProductAdd(int productId, string selectedProductIds) + { + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productIds = selectedProductIds.SplitSafe(",").Select(x => x.ToInt()).ToArray(); + var products = _productService.GetProductsByIds(productIds); - var model = new ProductModel.AddCrossSellProductModel(); - model.Products = new GridModel - { - Data = products.Select(x => + foreach (var product in products) { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }), - Total = products.TotalCount - }; - - //categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) - { - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - } - - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - foreach (var s in _storeService.GetAllStores()) - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); - - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult CrossSellProductAddPopupList(GridCommand command, ProductModel.AddCrossSellProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var gridModel = new GridModel(); - - var ctx = new ProductSearchContext(); - - if (model.SearchCategoryId > 0) - ctx.CategoryIds.Add(model.SearchCategoryId); - - ctx.ManufacturerId = model.SearchManufacturerId; - ctx.StoreId = model.SearchStoreId; - ctx.Keywords = model.SearchProductName; - ctx.SearchSku = !_catalogSettings.SuppressSkuSearch; - ctx.LanguageId = _workContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageIndex = command.Page - 1; - ctx.PageSize = command.PageSize; - ctx.ShowHidden = true; - ctx.ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null; - - var products = _productService.SearchProducts(ctx); + var existingRelations = _productService.GetCrossSellProductsByProductId1(productId); - gridModel.Data = products.Select(x => + if (existingRelations.FindCrossSellProduct(productId, product.Id) == null) + { + _productService.InsertCrossSellProduct(new CrossSellProduct + { + ProductId1 = productId, + ProductId2 = product.Id + }); + } + } + } + else { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }); - gridModel.Total = products.TotalCount; - return new JsonResult - { - Data = gridModel - }; - } - - [HttpPost] - [FormValueRequired("save")] - public ActionResult CrossSellProductAddPopup(string btnId, string formId, ProductModel.AddCrossSellProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (model.SelectedProductIds != null) - { - foreach (int id in model.SelectedProductIds) - { - var product = _productService.GetProductById(id); - if (product != null) - { - var existingCrossSellProducts = _productService.GetCrossSellProductsByProductId1(model.ProductId); - if (existingCrossSellProducts.FindCrossSellProduct(model.ProductId, id) == null) - { - _productService.InsertCrossSellProduct( - new CrossSellProduct() - { - ProductId1 = model.ProductId, - ProductId2 = id, - }); - } - } - } - } + NotifyAccessDenied(); + } - ViewBag.RefreshPage = true; - ViewBag.btnId = btnId; - ViewBag.formId = formId; - model.Products = new GridModel(); - return View(model); - } + return new EmptyResult(); + } - [HttpPost] - [ValidateInput(false)] + [HttpPost, ValidateInput(false)] public ActionResult CreateAllMutuallyCrossSellProducts(int productId) { string message = null; @@ -1927,13 +1753,9 @@ public ActionResult CreateAllMutuallyCrossSellProducts(int productId) var product = _productService.GetProductById(productId); if (product != null) { - int count = _productService.EnsureMutuallyCrossSellProducts(productId); + var count = _productService.EnsureMutuallyCrossSellProducts(productId); message = T("Admin.Common.CreateMutuallyAssociationsResult", count); } - else - { - message = "No product found with the specified id"; - } } else { @@ -1953,38 +1775,44 @@ public ActionResult CreateAllMutuallyCrossSellProducts(int productId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult AssociatedProductList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var searchContext = new ProductSearchContext() + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - ParentGroupedProductId = productId, - PageSize = int.MaxValue, - ShowHidden = true - }; - - var associatedProducts = _productService.SearchProducts(searchContext); - var associatedProductsModel = associatedProducts - .Select(x => + var searchContext = new ProductSearchContext { - return new ProductModel.AssociatedProductModel() + OrderBy = ProductSortingEnum.Position, + ParentGroupedProductId = productId, + PageSize = int.MaxValue, + ShowHidden = true + }; + + var associatedProducts = _productService.SearchProducts(searchContext); + var associatedProductsModel = associatedProducts + .Select(x => { - Id = x.Id, - ProductName = x.Name, - ProductTypeName = x.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = x.ProductTypeLabelHint, - DisplayOrder = x.DisplayOrder, - Sku = x.Sku, - Published = x.Published - }; - }) - .ToList(); + return new ProductModel.AssociatedProductModel + { + Id = x.Id, + ProductName = x.Name, + ProductTypeName = x.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = x.ProductTypeLabelHint, + DisplayOrder = x.DisplayOrder, + Sku = x.Sku, + Published = x.Published + }; + }) + .ToList(); - var model = new GridModel + model.Data = associatedProductsModel; + model.Total = associatedProductsModel.Count; + } + else { - Data = associatedProductsModel, - Total = associatedProductsModel.Count - }; + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1995,15 +1823,13 @@ public ActionResult AssociatedProductList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult AssociatedProductUpdate(GridCommand command, ProductModel.AssociatedProductModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var associatedProduct = _productService.GetProductById(model.Id); - if (associatedProduct == null) - throw new ArgumentException("No associated product found with the specified id"); - associatedProduct.DisplayOrder = model.DisplayOrder; - _productService.UpdateProduct(associatedProduct); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + associatedProduct.DisplayOrder = model.DisplayOrder; + _productService.UpdateProduct(associatedProduct); + } return AssociatedProductList(command, associatedProduct.ParentGroupedProductId); } @@ -2011,117 +1837,54 @@ public ActionResult AssociatedProductUpdate(GridCommand command, ProductModel.As [GridAction(EnableCustomBinding = true)] public ActionResult AssociatedProductDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var product = _productService.GetProductById(id); - if (product == null) - throw new ArgumentException("No associated product found with the specified id"); - var originalParentGroupedProductId = product.ParentGroupedProductId; - product.ParentGroupedProductId = 0; - _productService.UpdateProduct(product); - - return AssociatedProductList(command, originalParentGroupedProductId); - } - - public ActionResult AssociatedProductAddPopup(int productId) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var model = new ProductModel.AddAssociatedProductModel(); - - //categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) - { - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - } - - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - foreach (var s in _storeService.GetAllStores()) + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + product.ParentGroupedProductId = 0; + _productService.UpdateProduct(product); } - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - return View(model); + return AssociatedProductList(command, originalParentGroupedProductId); } - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult AssociatedProductAddPopupList(GridCommand command, ProductModel.AddAssociatedProductModel model) + [HttpPost] + public ActionResult AssociatedProductAdd(int productId, string selectedProductIds) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var searchContext = new ProductSearchContext() - { - CategoryIds = new List() { model.SearchCategoryId }, - ManufacturerId = model.SearchManufacturerId, - StoreId = model.SearchStoreId, - Keywords = model.SearchProductName, - SearchSku = !_catalogSettings.SuppressSkuSearch, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - ShowHidden = true, - ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null - }; - - var products = _productService.SearchProducts(searchContext); - - var gridModel = new GridModel(); - gridModel.Data = products.Select(x => + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }); - gridModel.Total = products.TotalCount; + var searchContext = new ProductSearchContext + { + ParentGroupedProductId = productId, + PageSize = 1, + ShowHidden = true + }; - return new JsonResult - { - Data = gridModel - }; - } + var maxDisplayOrder = _productService.PrepareProductSearchQuery(searchContext, x => x.DisplayOrder) + .OrderByDescending(x => x) + .FirstOrDefault(); - [HttpPost] - [FormValueRequired("save")] - public ActionResult AssociatedProductAddPopup(string btnId, string formId, ProductModel.AddAssociatedProductModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productIds = selectedProductIds.SplitSafe(",").Select(x => x.ToInt()).ToArray(); + var products = _productService.GetProductsByIds(productIds); - if (model.SelectedProductIds != null) - { - foreach (int id in model.SelectedProductIds) + foreach (var product in products) { - var product = _productService.GetProductById(id); - if (product != null) + if (product.ParentGroupedProductId != productId) { - product.ParentGroupedProductId = model.ProductId; + product.ParentGroupedProductId = productId; + product.DisplayOrder = ++maxDisplayOrder; + _productService.UpdateProduct(product); } } } + else + { + NotifyAccessDenied(); + } - ViewBag.RefreshPage = true; - ViewBag.btnId = btnId; - ViewBag.formId = formId; - return View(model); + return new EmptyResult(); } #endregion @@ -2229,34 +1992,39 @@ private void SaveFilteredAttributes(ProductBundleItem bundleItem, FormCollection [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult BundleItemList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var bundleItems = _productService.GetBundleItems(productId, true).Select(x => x.Item); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var bundleItems = _productService.GetBundleItems(productId, true).Select(x => x.Item); - var bundleItemsModel = bundleItems.Select(x => - { - return new ProductModel.BundleItemModel() + var bundleItemsModel = bundleItems.Select(x => { - Id = x.Id, - ProductId = x.Product.Id, - ProductName = x.Product.Name, - ProductTypeName = x.Product.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = x.Product.ProductTypeLabelHint, - Sku = x.Product.Sku, - Quantity = x.Quantity, - Discount = x.Discount, - DisplayOrder = x.DisplayOrder, - Visible = x.Visible, - Published = x.Published - }; - }).ToList(); + return new ProductModel.BundleItemModel + { + Id = x.Id, + ProductId = x.Product.Id, + ProductName = x.Product.Name, + ProductTypeName = x.Product.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = x.Product.ProductTypeLabelHint, + Sku = x.Product.Sku, + Quantity = x.Quantity, + Discount = x.Discount, + DisplayOrder = x.DisplayOrder, + Visible = x.Visible, + Published = x.Published + }; + }).ToList(); - var model = new GridModel() + model.Data = bundleItemsModel; + model.Total = bundleItemsModel.Count; + } + else { - Data = bundleItemsModel, - Total = bundleItemsModel.Count - }; + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { Data = model }; } @@ -2264,79 +2032,29 @@ public ActionResult BundleItemList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult BundleItemDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var bundleItem = _productService.GetBundleItemById(id); - if (bundleItem == null) - throw new ArgumentException("No product bundle item found with the specified id"); - - _productService.DeleteBundleItem(bundleItem); - - return BundleItemList(command, bundleItem.BundleProductId); - } - - public ActionResult BundleItemAddPopup(int productId) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var product = _productService.GetProductById(productId); - - if (product.ProductType != ProductType.BundledProduct) - throw new ArgumentException("Bundle items can only be added to bundles."); - - var model = new ProductModel.AddBundleItemModel() - { - IsPerItemPricing = product.BundlePerItemPricing, - IsPerItemShipping = product.BundlePerItemShipping - }; - - //categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) - { - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - } - - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - foreach (var s in _storeService.GetAllStores()) + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + _productService.DeleteBundleItem(bundleItem); } - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - ViewBag.RefreshPage = false; - ViewBag.CloseWindow = false; - - return View(model); + return BundleItemList(command, bundleItem.BundleProductId); } [HttpPost] - [FormValueRequired("save")] - public ActionResult BundleItemAddPopup(string btnId, string formId, ProductModel.AddBundleItemModel model) + public ActionResult BundleItemAdd(int productId, string selectedProductIds) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - bool closeWindow = true; - - if (model.SelectedProductIds != null) + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - var products = _productService.GetProductsByIds(model.SelectedProductIds); var utcNow = DateTime.UtcNow; + var productIds = selectedProductIds.SplitSafe(",").Select(x => x.ToInt()).ToArray(); + var products = _productService.GetProductsByIds(productIds); + + var maxDisplayOrder = _productService.GetBundleItems(productId, true) + .OrderByDescending(x => x.Item.DisplayOrder) + .Select(x => x.Item.DisplayOrder) + .FirstOrDefault(); foreach (var product in products.Where(x => x.CanBeBundleItem())) { @@ -2344,19 +2062,18 @@ public ActionResult BundleItemAddPopup(string btnId, string formId, ProductModel if (attributes.Count > 0 && attributes.Any(a => a.ProductVariantAttributeValues.Any(v => v.ValueType == ProductVariantAttributeValueType.ProductLinkage))) { - NotifyError(_localizationService.GetResource("Admin.Catalog.Products.BundleItems.NoAttributeWithProductLinkage")); - closeWindow = false; + NotifyError(T("Admin.Catalog.Products.BundleItems.NoAttributeWithProductLinkage")); } else { - var bundleItem = new ProductBundleItem() + var bundleItem = new ProductBundleItem { ProductId = product.Id, - BundleProductId = model.ProductId, + BundleProductId = productId, Quantity = 1, Visible = true, Published = true, - DisplayOrder = products.IndexOf(product) + 1, + DisplayOrder = ++maxDisplayOrder, CreatedOnUtc = utcNow, UpdatedOnUtc = utcNow }; @@ -2365,50 +2082,12 @@ public ActionResult BundleItemAddPopup(string btnId, string formId, ProductModel } } } - - ViewBag.RefreshPage = true; - ViewBag.CloseWindow = closeWindow; - ViewBag.btnId = btnId; - ViewBag.formId = formId; - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult BundleItemAddPopupList(GridCommand command, ProductModel.AddBundleItemModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var searchContext = new ProductSearchContext() - { - CategoryIds = new List() { model.SearchCategoryId }, - ManufacturerId = model.SearchManufacturerId, - StoreId = model.SearchStoreId, - Keywords = model.SearchProductName, - SearchSku = !_catalogSettings.SuppressSkuSearch, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - ShowHidden = true, - ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null - }; - - var products = _productService.SearchProducts(searchContext); - - var gridModel = new GridModel(); - gridModel.Data = products.Select(x => + else { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - productModel.ProductSelectCheckboxClass = (!x.CanBeBundleItem() ? " hide" : ""); - - return productModel; - }); - gridModel.Total = products.TotalCount; + NotifyAccessDenied(); + } - return new JsonResult - { - Data = gridModel - }; + return new EmptyResult(); } public ActionResult BundleItemEditPopup(int id, string btnId, string formId) @@ -2484,7 +2163,7 @@ public ActionResult ProductPictureAdd(int pictureId, int displayOrder, int produ 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 productPicture = new ProductPicture { @@ -2505,29 +2184,34 @@ public ActionResult ProductPictureAdd(int pictureId, int displayOrder, int produ [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductPictureList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var productPictures = _productService.GetProductPicturesByProductId(productId); - var productPicturesModel = productPictures - .Select(x => - { - return new ProductModel.ProductPictureModel() - { - Id = x.Id, - ProductId = x.ProductId, - PictureId = x.PictureId, - PictureUrl = _pictureService.GetPictureUrl(x.PictureId), - DisplayOrder = x.DisplayOrder - }; - }) - .ToList(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productPictures = _productService.GetProductPicturesByProductId(productId); + var productPicturesModel = productPictures + .Select(x => + { + return new ProductModel.ProductPictureModel + { + Id = x.Id, + ProductId = x.ProductId, + PictureId = x.PictureId, + PictureUrl = _pictureService.GetPictureUrl(x.PictureId), + DisplayOrder = x.DisplayOrder + }; + }) + .ToList(); - var model = new GridModel - { - Data = productPicturesModel, - Total = productPicturesModel.Count - }; + model.Data = productPicturesModel; + model.Total = productPicturesModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -2538,15 +2222,13 @@ public ActionResult ProductPictureList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult ProductPictureUpdate(ProductModel.ProductPictureModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var productPicture = _productService.GetProductPictureById(model.Id); - if (productPicture == null) - throw new ArgumentException("No product picture found with the specified id"); + var productPicture = _productService.GetProductPictureById(model.Id); - productPicture.DisplayOrder = model.DisplayOrder; - _productService.UpdateProductPicture(productPicture); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + productPicture.DisplayOrder = model.DisplayOrder; + _productService.UpdateProductPicture(productPicture); + } return ProductPictureList(command, productPicture.ProductId); } @@ -2554,18 +2236,16 @@ public ActionResult ProductPictureUpdate(ProductModel.ProductPictureModel model, [GridAction(EnableCustomBinding = true)] public ActionResult ProductPictureDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var productPicture = _productService.GetProductPictureById(id); + var productId = productPicture.ProductId; - var productPicture = _productService.GetProductPictureById(id); - if (productPicture == null) - throw new ArgumentException("No product picture found with the specified id"); - - var productId = productPicture.ProductId; - _productService.DeleteProductPicture(productPicture); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _productService.DeleteProductPicture(productPicture); - var picture = _pictureService.GetPictureById(productPicture.PictureId); - _pictureService.DeletePicture(picture); + var picture = _pictureService.GetPictureById(productPicture.PictureId); + _pictureService.DeletePicture(picture); + } return ProductPictureList(command, productId); } @@ -2596,51 +2276,56 @@ public ActionResult ProductSpecificationAttributeAdd(int specificationAttributeO [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductSpecAttrList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var productrSpecs = _specificationAttributeService.GetProductSpecificationAttributesByProductId(productId); + var model = new GridModel(); - var productrSpecsModel = productrSpecs - .Select(x => - { - var psaModel = new ProductSpecificationAttributeModel() - { - Id = x.Id, - SpecificationAttributeName = x.SpecificationAttributeOption.SpecificationAttribute.Name, - SpecificationAttributeOptionName = x.SpecificationAttributeOption.Name, - SpecificationAttributeOptionAttributeId = x.SpecificationAttributeOption.SpecificationAttributeId, - SpecificationAttributeOptionId = x.SpecificationAttributeOptionId, - AllowFiltering = x.AllowFiltering, - ShowOnProductPage = x.ShowOnProductPage, - DisplayOrder = x.DisplayOrder - }; - return psaModel; - }) - .ToList(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productrSpecs = _specificationAttributeService.GetProductSpecificationAttributesByProductId(productId); - foreach(var attr in productrSpecsModel) { + var productrSpecsModel = productrSpecs + .Select(x => + { + var psaModel = new ProductSpecificationAttributeModel + { + Id = x.Id, + SpecificationAttributeName = x.SpecificationAttributeOption.SpecificationAttribute.Name, + SpecificationAttributeOptionName = x.SpecificationAttributeOption.Name, + SpecificationAttributeOptionAttributeId = x.SpecificationAttributeOption.SpecificationAttributeId, + SpecificationAttributeOptionId = x.SpecificationAttributeOptionId, + AllowFiltering = x.AllowFiltering, + ShowOnProductPage = x.ShowOnProductPage, + DisplayOrder = x.DisplayOrder + }; + return psaModel; + }) + .ToList(); - var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(attr.SpecificationAttributeOptionAttributeId); + foreach (var attr in productrSpecsModel) + { + var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(attr.SpecificationAttributeOptionAttributeId); - foreach(var option in options) { + foreach (var option in options) + { + attr.SpecificationAttributeOptions.Add(new ProductSpecificationAttributeModel.SpecificationAttributeOption + { + id = option.Id, + name = option.Name, + text = option.Name + }); + } - attr.SpecificationAttributeOptions.Add(new ProductSpecificationAttributeModel.SpecificationAttributeOption() - { - id = option.Id, - name = option.Name, - text = option.Name - }); - } + attr.SpecificationAttributeOptionsJsonString = HttpUtility.HtmlEncode(JsonConvert.SerializeObject(attr.SpecificationAttributeOptions)); + } - attr.SpecificationAttributeOptionsJsonString = JsonConvert.SerializeObject(attr.SpecificationAttributeOptions); - } + model.Data = productrSpecsModel; + model.Total = productrSpecsModel.Count; + } + else + { + model.Data = Enumerable.Empty(); - var model = new GridModel - { - Data = productrSpecsModel, - Total = productrSpecsModel.Count - }; + NotifyAccessDenied(); + } return new JsonResult { @@ -2649,17 +2334,17 @@ public ActionResult ProductSpecAttrList(GridCommand command, int productId) } [GridAction(EnableCustomBinding = true)] - public ActionResult ProductSpecAttrUpdate(int psaId, ProductSpecificationAttributeModel model, - GridCommand command) + public ActionResult ProductSpecAttrUpdate(int psaId, ProductSpecificationAttributeModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var psa = _specificationAttributeService.GetProductSpecificationAttributeById(psaId); - var psa = _specificationAttributeService.GetProductSpecificationAttributeById(psaId); - psa.AllowFiltering = model.AllowFiltering; - psa.ShowOnProductPage = model.ShowOnProductPage; - psa.DisplayOrder = model.DisplayOrder; - _specificationAttributeService.UpdateProductSpecificationAttribute(psa); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + psa.AllowFiltering = model.AllowFiltering; + psa.ShowOnProductPage = model.ShowOnProductPage; + psa.DisplayOrder = model.DisplayOrder; + _specificationAttributeService.UpdateProductSpecificationAttribute(psa); + } return ProductSpecAttrList(command, psa.ProductId); } @@ -2667,15 +2352,13 @@ public ActionResult ProductSpecAttrUpdate(int psaId, ProductSpecificationAttribu [GridAction(EnableCustomBinding = true)] public ActionResult ProductSpecAttrDelete(int psaId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var psa = _specificationAttributeService.GetProductSpecificationAttributeById(psaId); - if (psa == null) - throw new ArgumentException("No specification attribute found with the specified id"); + var psa = _specificationAttributeService.GetProductSpecificationAttributeById(psaId); + var productId = psa.ProductId; - var productId = psa.ProductId; - _specificationAttributeService.DeleteProductSpecificationAttribute(psa); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _specificationAttributeService.DeleteProductSpecificationAttribute(psa); + } return ProductSpecAttrList(command, productId); } @@ -2695,28 +2378,33 @@ public ActionResult ProductTags() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductTags(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var tags = _productTagService.GetAllProductTags() - //order by product count - .OrderByDescending(x => _productTagService.GetProductCount(x.Id, 0)) - .Select(x => - { - return new ProductTagModel() - { - Id = x.Id, - Name = x.Name, - ProductCount = _productTagService.GetProductCount(x.Id, 0) - }; - }) - .ForCommand(command); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var tags = _productTagService.GetAllProductTags() + .OrderByDescending(x => _productTagService.GetProductCount(x.Id, 0)) + .Select(x => + { + return new ProductTagModel + { + Id = x.Id, + Name = x.Name, + ProductCount = _productTagService.GetProductCount(x.Id, 0) + }; + }) + .ForCommand(command); + + model.Data = tags.PagedForCommand(command); + model.Total = tags.Count(); + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var model = new GridModel - { - Data = tags.PagedForCommand(command), - Total = tags.Count() - }; return new JsonResult { Data = model @@ -2726,13 +2414,12 @@ public ActionResult ProductTags(GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult ProductTagDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var tag = _productTagService.GetProductTagById(id); - var tag = _productTagService.GetProductTagById(id); - if (tag == null) - throw new ArgumentException("No product tag found with the specified id"); - _productTagService.DeleteProductTag(tag); + _productTagService.DeleteProductTag(tag); + } return ProductTags(command); } @@ -2793,157 +2480,6 @@ public ActionResult EditProductTag(string btnId, string formId, ProductTagModel #endregion - #region Import - - [HttpPost] - public ActionResult ImportExcel(FormCollection form) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - try - { - var file = Request.Files["importfile"]; - if (file != null) - { - IDataTable table = null; - - try - { - table = LightweightDataTable.FromPostedFile(file); - } - catch (Exception ex) - { - NotifyError(ex); - return RedirectToAction("List"); - } - - var options = TaskCreationOptions.LongRunning; - var cts = new CancellationTokenSource(); - var scheduler = TaskScheduler.Default; - - var task = AsyncRunner.Run((c, ct) => - { - var progress = new Progress(p => - { - AsyncState.Current.Set(p); - }); - - try - { - AsyncState.Current.SetCancelTokenSource(cts); - var importManager = c.Resolve(); - var result = importManager.ImportProducts(table, ct, progress); - - // Saving the result enables us to show a report for last import. - AsyncState.Current.Set(result, neverExpires: true); - } - catch (AggregateException ae) - { - foreach (var e in ae.Flatten().InnerExceptions) - { - // TBD: do exactly WHAT now?! Logging? Nothing? - // [...] - } - } - finally - { - AsyncState.Current.Remove(); - } - - }, cts.Token, options, scheduler); - - task.Wait(500); - } - else - { - NotifyError(T("Admin.Common.UploadFile")); - return RedirectToAction("List"); - } - - NotifySuccess(T("Admin.Common.ImportFromExcel.InProgress")); - return RedirectToAction("List"); - } - catch (Exception exc) - { - NotifyError(exc); - return RedirectToAction("List"); - } - } - - [HttpPost] - public ActionResult ImportExcelProgress() - { - var progress = AsyncState.Current.Get(); - - //object lastResult = null; - string lastResultSummary = null; - string lastResultClass = ""; - var result = AsyncState.Current.Get(); - if (result != null) - { - var title = _localizationService.GetResource("Admin.Common.ImportFromExcel.LastResultTitle").FormatCurrent( - result.EndDateUtc.ToLocalTime(), - result.Cancelled ? " (" + _localizationService.GetResource("Common.Cancelled").ToLower() + ")" : ""); - var counts = _localizationService.GetResource("Admin.Common.ImportFromExcel.ProcessedCount").FormatCurrent( - result.AffectedRecords, - result.TotalRecords); - var stats = _localizationService.GetResource("Admin.Common.ImportFromExcel.QuickStats").FormatCurrent( - result.NewRecords, - result.ModifiedRecords, - result.Messages.Count(x => x.MessageType == ImportMessageType.Warning), - result.Messages.Count(x => x.MessageType == ImportMessageType.Error)); - - lastResultSummary = "{0} {1} {2}".FormatInvariant(title, counts, stats); - - if (!result.Cancelled) - { - lastResultClass = result.HasErrors ? "alert-error" : "alert-success"; - } - } - - if (progress == null) - { - return Json(new { NoRunningTask = true, LastResultSummary = lastResultSummary, LastResultClass = lastResultClass }, JsonRequestBehavior.DenyGet); - } - - return Json(new { Progress = progress, LastResultSummary = lastResultSummary, LastResultClass = lastResultClass }, JsonRequestBehavior.DenyGet); - } - - public ActionResult ImportExcelCancel() - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var tcs = AsyncState.Current.GetCancelTokenSource(); - if (tcs != null) - { - tcs.Cancel(); - NotifySuccess(_localizationService.GetResource("Admin.Common.ImportFromExcel.Cancelled")); - } - - return RedirectToAction("List"); - } - - public ActionResult ImportExcelDownloadReport() - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var result = AsyncState.Current.Get(); - if (result == null) - { - return Content(_localizationService.GetResource("Admin.Common.ImportFromExcel.NoReportAvailable")); - } - - var importManager = EngineContext.Current.Resolve(); - string report = importManager.CreateTextReport(result); - - return File(Encoding.UTF8.GetBytes(report), "text/plain", "excelimport-report-{0}.txt".FormatInvariant(result.StartDateUtc.ToLocalTime())); - } - - #endregion - #region Low stock reports public ActionResult LowStockReport() @@ -2957,21 +2493,29 @@ public ActionResult LowStockReport() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult LowStockReportList(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var allProducts = _productService.GetLowStockProducts(); - var model = new GridModel() + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - Data = allProducts.PagedForCommand(command).Select(x => + var allProducts = _productService.GetLowStockProducts(); + + model.Data = allProducts.PagedForCommand(command).Select(x => { var productModel = x.ToModel(); productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); return productModel; - }), - Total = allProducts.Count - }; + }); + + model.Total = allProducts.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = model @@ -2991,7 +2535,6 @@ public ActionResult BulkEdit() var model = new BulkEditListModel(); model.GridPageSize = _adminAreaSettings.GridPageSize; - model.StoreCount = allStores.Count; var allCategories = _categoryService.GetAllCategories(showHidden: true); var mappedCategories = allCategories.ToDictionary(x => x.Id); @@ -3018,47 +2561,54 @@ public ActionResult BulkEdit() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult BulkEditSelect(GridCommand command, BulkEditListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var searchContext = new ProductSearchContext + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - StoreId = model.SearchStoreId, - ManufacturerId = model.SearchManufacturerId, - Keywords = model.SearchProductName, - SearchSku = !_catalogSettings.SuppressSkuSearch, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - ShowHidden = true, - ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null - }; + var searchContext = new ProductSearchContext + { + StoreId = model.SearchStoreId, + ManufacturerId = model.SearchManufacturerId, + Keywords = model.SearchProductName, + SearchSku = !_catalogSettings.SuppressSkuSearch, + PageIndex = command.Page - 1, + PageSize = command.PageSize, + ShowHidden = true, + ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null + }; - if (model.SearchCategoryId > 0) - searchContext.CategoryIds.Add(model.SearchCategoryId); + if (model.SearchCategoryId > 0) + searchContext.CategoryIds.Add(model.SearchCategoryId); + + var products = _productService.SearchProducts(searchContext); + + gridModel.Data = products.Select(x => + { + var productModel = new BulkEditProductModel + { + Id = x.Id, + Name = x.Name, + ProductTypeName = x.GetProductTypeLabel(_localizationService), + ProductTypeLabelHint = x.ProductTypeLabelHint, + Sku = x.Sku, + OldPrice = x.OldPrice, + Price = x.Price, + ManageInventoryMethod = x.ManageInventoryMethod.GetLocalizedEnum(_localizationService, _workContext.WorkingLanguage.Id), + StockQuantity = x.StockQuantity, + Published = x.Published + }; - var products = _productService.SearchProducts(searchContext); - var gridModel = new GridModel(); + return productModel; + }); - gridModel.Data = products.Select(x => + gridModel.Total = products.TotalCount; + } + else { - var productModel = new BulkEditProductModel() - { - Id = x.Id, - Name = x.Name, - ProductTypeName = x.GetProductTypeLabel(_localizationService), - ProductTypeLabelHint = x.ProductTypeLabelHint, - Sku = x.Sku, - OldPrice = x.OldPrice, - Price = x.Price, - ManageInventoryMethod = x.ManageInventoryMethod.GetLocalizedEnum(_localizationService, _workContext.WorkingLanguage.Id), - StockQuantity = x.StockQuantity, - Published = x.Published - }; - - return productModel; - }); + gridModel.Data = Enumerable.Empty(); - gridModel.Total = products.TotalCount; + NotifyAccessDenied(); + } return new JsonResult { @@ -3073,39 +2623,39 @@ public ActionResult BulkEditSave(GridCommand command, [Bind(Prefix = "deleted")]IEnumerable deletedProducts, BulkEditListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (updatedProducts != null) + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - foreach (var pModel in updatedProducts) + if (updatedProducts != null) { - //update - var product = _productService.GetProductById(pModel.Id); - if (product != null) + foreach (var pModel in updatedProducts) { - product.Sku = pModel.Sku; - product.Price = pModel.Price; - product.OldPrice = pModel.OldPrice; - product.StockQuantity = pModel.StockQuantity; - product.Published = pModel.Published; + var product = _productService.GetProductById(pModel.Id); + if (product != null) + { + product.Sku = pModel.Sku; + product.Price = pModel.Price; + product.OldPrice = pModel.OldPrice; + product.StockQuantity = pModel.StockQuantity; + product.Published = pModel.Published; - _productService.UpdateProduct(product); + _productService.UpdateProduct(product); + } } } - } - if (deletedProducts != null) - { - foreach (var pModel in deletedProducts) + if (deletedProducts != null) { - var product = _productService.GetProductById(pModel.Id); - if (product != null) + foreach (var pModel in deletedProducts) { - _productService.DeleteProduct(product); + var product = _productService.GetProductById(pModel.Id); + if (product != null) + { + _productService.DeleteProduct(product); + } } } } + return BulkEditSelect(command, model); } @@ -3116,64 +2666,67 @@ public ActionResult BulkEditSave(GridCommand command, [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult TierPriceList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var product = _productService.GetProductById(productId); - if (product == null) - throw new ArgumentException("No product variant found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var product = _productService.GetProductById(productId); - var allStores = _services.StoreService.GetAllStores(); - var allCustomerRoles = _customerService.GetAllCustomerRoles(true); - string allRolesString = T("Admin.Catalog.Products.TierPrices.Fields.CustomerRole.AllRoles"); - string allStoresString = T("Admin.Common.StoresAll"); - string deletedString = "[{0}]".FormatInvariant(T("Admin.Common.Deleted")); - - var tierPricesModel = product.TierPrices - .OrderBy(x => x.StoreId) - .ThenBy(x => x.Quantity) - .ThenBy(x => x.CustomerRoleId) - .Select(x => - { - var tierPriceModel = new ProductModel.TierPriceModel + var allStores = _services.StoreService.GetAllStores(); + var allCustomerRoles = _customerService.GetAllCustomerRoles(true); + string allRolesString = T("Admin.Catalog.Products.TierPrices.Fields.CustomerRole.AllRoles"); + string allStoresString = T("Admin.Common.StoresAll"); + string deletedString = "[{0}]".FormatInvariant(T("Admin.Common.Deleted")); + + var tierPricesModel = product.TierPrices + .OrderBy(x => x.StoreId) + .ThenBy(x => x.Quantity) + .ThenBy(x => x.CustomerRoleId) + .Select(x => { - Id = x.Id, - StoreId = x.StoreId, - ProductId = x.ProductId, - CustomerRoleId = x.CustomerRoleId ?? 0, - Quantity = x.Quantity, - Price1 = x.Price - }; + var tierPriceModel = new ProductModel.TierPriceModel + { + Id = x.Id, + StoreId = x.StoreId, + ProductId = x.ProductId, + CustomerRoleId = x.CustomerRoleId ?? 0, + Quantity = x.Quantity, + Price1 = x.Price + }; - if (x.CustomerRoleId.HasValue) - { - var role = allCustomerRoles.FirstOrDefault(r => r.Id == x.CustomerRoleId.Value); - tierPriceModel.CustomerRole = (role == null ? allRolesString : role.Name); - } - else - { - tierPriceModel.CustomerRole = allRolesString; - } + if (x.CustomerRoleId.HasValue) + { + var role = allCustomerRoles.FirstOrDefault(r => r.Id == x.CustomerRoleId.Value); + tierPriceModel.CustomerRole = (role == null ? allRolesString : role.Name); + } + else + { + tierPriceModel.CustomerRole = allRolesString; + } - if (x.StoreId > 0) - { - var store = allStores.FirstOrDefault(s => s.Id == x.StoreId); - tierPriceModel.Store = (store == null ? deletedString : store.Name); - } - else - { - tierPriceModel.Store = allStoresString; - } + if (x.StoreId > 0) + { + var store = allStores.FirstOrDefault(s => s.Id == x.StoreId); + tierPriceModel.Store = (store == null ? deletedString : store.Name); + } + else + { + tierPriceModel.Store = allStoresString; + } - return tierPriceModel; - }) - .ToList(); + return tierPriceModel; + }) + .ToList(); - var model = new GridModel + model.Data = tierPricesModel; + model.Total = tierPricesModel.Count; + } + else { - Data = tierPricesModel, - Total = tierPricesModel.Count - }; + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -3184,27 +2737,26 @@ public ActionResult TierPriceList(GridCommand command, int productId) [GridAction(EnableCustomBinding = true)] public ActionResult TierPriceInsert(GridCommand command, ProductModel.TierPriceModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var product = _productService.GetProductById(model.ProductId); - var product = _productService.GetProductById(model.ProductId); - if (product == null) - throw new ArgumentException("No product found with the specified id"); + var tierPrice = new TierPrice + { + ProductId = model.ProductId, + // use Store property (not Store propertyId) because appropriate property is stored in it + StoreId = model.Store.ToInt(), + // use CustomerRole property (not CustomerRoleId) because appropriate property is stored in it + CustomerRoleId = model.CustomerRole.IsNumeric() && Int32.Parse(model.CustomerRole) != 0 ? Int32.Parse(model.CustomerRole) : (int?)null, + Quantity = model.Quantity, + Price = model.Price1 + }; - var tierPrice = new TierPrice() - { - ProductId = model.ProductId, - // use Store property (not Store propertyId) because appropriate property is stored in it - StoreId = model.Store.ToInt(), - // use CustomerRole property (not CustomerRoleId) because appropriate property is stored in it - CustomerRoleId = model.CustomerRole.IsNumeric() && Int32.Parse(model.CustomerRole) != 0 ? Int32.Parse(model.CustomerRole) : (int?)null, - Quantity = model.Quantity, - Price = model.Price1 - }; - _productService.InsertTierPrice(tierPrice); + _productService.InsertTierPrice(tierPrice); - //update "HasTierPrices" property - _productService.UpdateHasTierPricesProperty(product); + //update "HasTierPrices" property + _productService.UpdateHasTierPricesProperty(product); + } return TierPriceList(command, model.ProductId); } @@ -3212,20 +2764,19 @@ public ActionResult TierPriceInsert(GridCommand command, ProductModel.TierPriceM [GridAction(EnableCustomBinding = true)] public ActionResult TierPriceUpdate(GridCommand command, ProductModel.TierPriceModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var tierPrice = _productService.GetTierPriceById(model.Id); - if (tierPrice == null) - throw new ArgumentException("No tier price found with the specified id"); - //use Store property (not Store propertyId) because appropriate property is stored in it - tierPrice.StoreId = model.Store.ToInt(); - //use CustomerRole property (not CustomerRoleId) because appropriate property is stored in it - tierPrice.CustomerRoleId = model.CustomerRole.IsNumeric() && Int32.Parse(model.CustomerRole) != 0 ? Int32.Parse(model.CustomerRole) : (int?)null; - tierPrice.Quantity = model.Quantity; - tierPrice.Price = model.Price1; - _productService.UpdateTierPrice(tierPrice); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + //use Store property (not Store propertyId) because appropriate property is stored in it + tierPrice.StoreId = model.Store.ToInt(); + //use CustomerRole property (not CustomerRoleId) because appropriate property is stored in it + tierPrice.CustomerRoleId = model.CustomerRole.IsNumeric() && Int32.Parse(model.CustomerRole) != 0 ? Int32.Parse(model.CustomerRole) : (int?)null; + tierPrice.Quantity = model.Quantity; + tierPrice.Price = model.Price1; + + _productService.UpdateTierPrice(tierPrice); + } return TierPriceList(command, tierPrice.ProductId); } @@ -3233,22 +2784,18 @@ public ActionResult TierPriceUpdate(GridCommand command, ProductModel.TierPriceM [GridAction(EnableCustomBinding = true)] public ActionResult TierPriceDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var tierPrice = _productService.GetTierPriceById(id); - if (tierPrice == null) - throw new ArgumentException("No tier price found with the specified id"); - var productId = tierPrice.ProductId; - var product = _productService.GetProductById(productId); - if (product == null) - throw new ArgumentException("No product found with the specified id"); - _productService.DeleteTierPrice(tierPrice); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var product = _productService.GetProductById(productId); + + _productService.DeleteTierPrice(tierPrice); - //update "HasTierPrices" property - _productService.UpdateHasTierPricesProperty(product); + //update "HasTierPrices" property + _productService.UpdateHasTierPricesProperty(product); + } return TierPriceList(command, productId); } @@ -3260,40 +2807,45 @@ public ActionResult TierPriceDelete(int id, GridCommand command) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductVariantAttributeList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - var productVariantAttributes = _productAttributeService.GetProductVariantAttributesByProductId(productId); - var productVariantAttributesModel = productVariantAttributes - .Select(x => - { - var pvaModel = new ProductModel.ProductVariantAttributeModel() + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var productVariantAttributes = _productAttributeService.GetProductVariantAttributesByProductId(productId); + var productVariantAttributesModel = productVariantAttributes + .Select(x => { - Id = x.Id, - ProductId = x.ProductId, - ProductAttribute = _productAttributeService.GetProductAttributeById(x.ProductAttributeId).Name, - ProductAttributeId = x.ProductAttributeId, - TextPrompt = x.TextPrompt, - IsRequired = x.IsRequired, - AttributeControlType = x.AttributeControlType.GetLocalizedEnum(_localizationService, _workContext), - AttributeControlTypeId = x.AttributeControlTypeId, - DisplayOrder1 = x.DisplayOrder - }; + var pvaModel = new ProductModel.ProductVariantAttributeModel + { + Id = x.Id, + ProductId = x.ProductId, + ProductAttribute = _productAttributeService.GetProductAttributeById(x.ProductAttributeId).Name, + ProductAttributeId = x.ProductAttributeId, + TextPrompt = x.TextPrompt, + IsRequired = x.IsRequired, + AttributeControlType = x.AttributeControlType.GetLocalizedEnum(_localizationService, _workContext), + AttributeControlTypeId = x.AttributeControlTypeId, + DisplayOrder1 = x.DisplayOrder + }; - if (x.ShouldHaveValues()) - { - pvaModel.ViewEditUrl = Url.Action("EditAttributeValues", "Product", new { productVariantAttributeId = x.Id }); - pvaModel.ViewEditText = string.Format(_localizationService.GetResource("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.ViewLink"), x.ProductVariantAttributeValues != null ? x.ProductVariantAttributeValues.Count : 0); - } - return pvaModel; - }) - .ToList(); + if (x.ShouldHaveValues()) + { + pvaModel.ViewEditUrl = Url.Action("EditAttributeValues", "Product", new { productVariantAttributeId = x.Id }); + pvaModel.ViewEditText = string.Format(_localizationService.GetResource("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.ViewLink"), x.ProductVariantAttributeValues != null ? x.ProductVariantAttributeValues.Count : 0); + } + return pvaModel; + }) + .ToList(); - var model = new GridModel + model.Data = productVariantAttributesModel; + model.Total = productVariantAttributesModel.Count; + } + else { - Data = productVariantAttributesModel, - Total = productVariantAttributesModel.Count - }; + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -3304,19 +2856,20 @@ public ActionResult ProductVariantAttributeList(GridCommand command, int product [GridAction(EnableCustomBinding = true)] public ActionResult ProductVariantAttributeInsert(GridCommand command, ProductModel.ProductVariantAttributeModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var pva = new ProductVariantAttribute() + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - ProductId = model.ProductId, - ProductAttributeId = Int32.Parse(model.ProductAttribute), //use ProductAttribute property (not ProductAttributeId) because appropriate property is stored in it - TextPrompt = model.TextPrompt, - IsRequired = model.IsRequired, - AttributeControlTypeId = Int32.Parse(model.AttributeControlType), //use AttributeControlType property (not AttributeControlTypeId) because appropriate property is stored in it - DisplayOrder = model.DisplayOrder1 - }; - _productAttributeService.InsertProductVariantAttribute(pva); + var pva = new ProductVariantAttribute + { + ProductId = model.ProductId, + ProductAttributeId = Int32.Parse(model.ProductAttribute), //use ProductAttribute property (not ProductAttributeId) because appropriate property is stored in it + TextPrompt = model.TextPrompt, + IsRequired = model.IsRequired, + AttributeControlTypeId = Int32.Parse(model.AttributeControlType), //use AttributeControlType property (not AttributeControlTypeId) because appropriate property is stored in it + DisplayOrder = model.DisplayOrder1 + }; + + _productAttributeService.InsertProductVariantAttribute(pva); + } return ProductVariantAttributeList(command, model.ProductId); } @@ -3324,21 +2877,20 @@ public ActionResult ProductVariantAttributeInsert(GridCommand command, ProductMo [GridAction(EnableCustomBinding = true)] public ActionResult ProductVariantAttributeUpdate(GridCommand command, ProductModel.ProductVariantAttributeModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var pva = _productAttributeService.GetProductVariantAttributeById(model.Id); - if (pva == null) - throw new ArgumentException("No product variant attribute found with the specified id"); - //use ProductAttribute property (not ProductAttributeId) because appropriate property is stored in it - pva.ProductAttributeId = Int32.Parse(model.ProductAttribute); - pva.TextPrompt = model.TextPrompt; - pva.IsRequired = model.IsRequired; - //use AttributeControlType property (not AttributeControlTypeId) because appropriate property is stored in it - pva.AttributeControlTypeId = Int32.Parse(model.AttributeControlType); - pva.DisplayOrder = model.DisplayOrder1; - _productAttributeService.UpdateProductVariantAttribute(pva); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + //use ProductAttribute property (not ProductAttributeId) because appropriate property is stored in it + pva.ProductAttributeId = Int32.Parse(model.ProductAttribute); + pva.TextPrompt = model.TextPrompt; + pva.IsRequired = model.IsRequired; + //use AttributeControlType property (not AttributeControlTypeId) because appropriate property is stored in it + pva.AttributeControlTypeId = Int32.Parse(model.AttributeControlType); + pva.DisplayOrder = model.DisplayOrder1; + + _productAttributeService.UpdateProductVariantAttribute(pva); + } return ProductVariantAttributeList(command, pva.ProductId); } @@ -3346,16 +2898,13 @@ public ActionResult ProductVariantAttributeUpdate(GridCommand command, ProductMo [GridAction(EnableCustomBinding = true)] public ActionResult ProductVariantAttributeDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var pva = _productAttributeService.GetProductVariantAttributeById(id); - if (pva == null) - throw new ArgumentException("No product variant attribute found with the specified id"); - var productId = pva.ProductId; - _productAttributeService.DeleteProductVariantAttribute(pva); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _productAttributeService.DeleteProductVariantAttribute(pva); + } return ProductVariantAttributeList(command, productId); } @@ -3392,11 +2941,11 @@ public ActionResult EditAttributeValues(int productVariantAttributeId) var pva = _productAttributeService.GetProductVariantAttributeById(productVariantAttributeId); if (pva == null) - throw new ArgumentException("No product variant attribute found with the specified id"); + throw new ArgumentException(T("Products.Variants.NotFound", productVariantAttributeId)); var product = _productService.GetProductById(pva.ProductId); if (product == null) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", pva.ProductId)); var model = new ProductModel.ProductVariantAttributeValueListModel() { @@ -3412,22 +2961,19 @@ public ActionResult EditAttributeValues(int productVariantAttributeId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductAttributeValueList(int productVariantAttributeId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var pva = _productAttributeService.GetProductVariantAttributeById(productVariantAttributeId); - if (pva == null) - throw new ArgumentException("No product variant attribute found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var pva = _productAttributeService.GetProductVariantAttributeById(productVariantAttributeId); - var values = _productAttributeService.GetProductVariantAttributeValues(productVariantAttributeId); + var values = _productAttributeService.GetProductVariantAttributeValues(productVariantAttributeId); - var gridModel = new GridModel - { - Data = values.Select(x => + gridModel.Data = values.Select(x => { var linkedProduct = _productService.GetProductById(x.LinkedProductId); - var model = new ProductModel.ProductVariantAttributeValueModel() + var model = new ProductModel.ProductVariantAttributeValueModel { Id = x.Id, ProductVariantAttributeId = x.ProductVariantAttributeId, @@ -3458,9 +3004,17 @@ public ActionResult ProductAttributeValueList(int productVariantAttributeId, Gri } return model; - }), - Total = values.Count() - }; + }); + + gridModel.Total = values.Count(); + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = gridModel @@ -3474,18 +3028,19 @@ public ActionResult ProductAttributeValueCreatePopup(int productAttributeAttribu var pva = _productAttributeService.GetProductVariantAttributeById(productAttributeAttributeId); if (pva == null) - throw new ArgumentException("No product variant attribute found with the specified id"); + throw new ArgumentException(T("Products.Variants.NotFound", productAttributeAttributeId)); - var model = new ProductModel.ProductVariantAttributeValueModel() + var model = new ProductModel.ProductVariantAttributeValueModel { + ProductId = pva.ProductId, ProductVariantAttributeId = productAttributeAttributeId, DisplayColorSquaresRgb = pva.AttributeControlType == AttributeControlType.ColorSquares, ColorSquaresRgb = "#000000", Quantity = 1 }; - //locales AddLocales(_languageService, model.Locales); + return View(model); } @@ -3516,7 +3071,7 @@ public ActionResult ProductAttributeValueCreatePopup(string btnId, string formId if (ModelState.IsValid) { - var pvav = new ProductVariantAttributeValue() + var pvav = new ProductVariantAttributeValue { ProductVariantAttributeId = model.ProductVariantAttributeId, Name = model.Name, @@ -3555,8 +3110,9 @@ public ActionResult ProductAttributeValueEditPopup(int id) var linkedProduct = _productService.GetProductById(pvav.LinkedProductId); - var model = new ProductModel.ProductVariantAttributeValueModel() + var model = new ProductModel.ProductVariantAttributeValueModel { + ProductId = pvav.ProductVariantAttribute.ProductId, ProductVariantAttributeId = pvav.ProductVariantAttributeId, Name = pvav.Name, Alias = pvav.Alias, @@ -3583,7 +3139,6 @@ public ActionResult ProductAttributeValueEditPopup(int id) model.QuantityInfo = " × {0}".FormatWith(model.Quantity); } - //locales AddLocales(_languageService, model.Locales, (locale, languageId) => { locale.Name = pvav.GetLocalized(x => x.Name, languageId, false, false); @@ -3651,114 +3206,14 @@ public ActionResult ProductAttributeValueEditPopup(string btnId, string formId, [GridAction(EnableCustomBinding = true)] public ActionResult ProductAttributeValueDelete(int pvavId, int productVariantAttributeId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var pvav = _productAttributeService.GetProductVariantAttributeValueById(pvavId); - if (pvav == null) - throw new ArgumentException("No product variant attribute value found with the specified id"); - - _productAttributeService.DeleteProductVariantAttributeValue(pvav); - - return ProductAttributeValueList(productVariantAttributeId, command); - } - - public ActionResult ProductAttributeValueLinkagePopup() - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var model = new ProductModel.ProductVariantAttributeValueModel.AddProductLinkageModel(); - - //categories - var allCategories = _categoryService.GetAllCategories(showHidden: true); - var mappedCategories = allCategories.ToDictionary(x => x.Id); - foreach (var c in allCategories) - { - model.AvailableCategories.Add(new SelectListItem() { Text = c.GetCategoryNameWithPrefix(_categoryService, mappedCategories), Value = c.Id.ToString() }); - } - - //manufacturers - foreach (var m in _manufacturerService.GetAllManufacturers(true)) + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) { - model.AvailableManufacturers.Add(new SelectListItem() { Text = m.Name, Value = m.Id.ToString() }); - } + var pvav = _productAttributeService.GetProductVariantAttributeValueById(pvavId); - //stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - foreach (var s in _storeService.GetAllStores()) - { - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + _productAttributeService.DeleteProductVariantAttributeValue(pvav); } - //product types - model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); - model.AvailableProductTypes.Insert(0, new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); - - return View(model); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult ProductAttributeValueLinkagePopupList(GridCommand command, ProductModel.ProductVariantAttributeValueModel.AddProductLinkageModel model) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var searchContext = new ProductSearchContext() - { - CategoryIds = new List() { model.SearchCategoryId }, - ManufacturerId = model.SearchManufacturerId, - StoreId = model.SearchStoreId, - Keywords = model.SearchProductName, - SearchSku = !_catalogSettings.SuppressSkuSearch, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - ShowHidden = true, - ProductType = model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null - }; - - var products = _productService.SearchProducts(searchContext); - - var gridModel = new GridModel(); - gridModel.Data = products.Select(x => - { - var productModel = x.ToModel(); - productModel.ProductTypeName = x.GetProductTypeLabel(_localizationService); - - return productModel; - }); - gridModel.Total = products.TotalCount; - - return new JsonResult - { - Data = gridModel - }; - } - - [HttpPost] - [FormValueRequired("save")] - public ActionResult ProductAttributeValueLinkagePopup(ProductModel.ProductVariantAttributeValueModel.AddProductLinkageModel model, string openerProductId, string openerProductName) - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - ViewBag.RefreshPage = true; - ViewBag.openerProductId = openerProductId; - ViewBag.openerProductName = openerProductName; - - var linkedProduct = _productService.GetProductById(model.ProductId); - - if (linkedProduct == null) - { - ViewBag.productId = 0; - ViewBag.productName = ""; - } - else - { - ViewBag.productId = linkedProduct.Id; - ViewBag.productName = linkedProduct.Name; - } - return View(model); + return ProductAttributeValueList(productVariantAttributeId, command); } #endregion @@ -3862,65 +3317,69 @@ private void PrepareDeliveryTimes(ProductVariantAttributeCombinationModel model, [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult ProductVariantAttributeCombinationList(GridCommand command, int productId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var model = new GridModel(); - // TODO: Replace ProductModel.ProductVariantAttributeCombinationModel by AddProductVariantAttributeCombinationModel - // when there's no grid-inline-editing anymore. + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + // TODO: Replace ProductModel.ProductVariantAttributeCombinationModel by AddProductVariantAttributeCombinationModel + // when there's no grid-inline-editing anymore. - var product = _productService.GetProductById(productId); - if (product == null) - throw new ArgumentException("No product found with the specified id"); + var product = _productService.GetProductById(productId); - var allCombinations = _productAttributeService.GetAllProductVariantAttributeCombinations(product.Id, command.Page - 1, command.PageSize); + var allCombinations = _productAttributeService.GetAllProductVariantAttributeCombinations(product.Id, command.Page - 1, command.PageSize); - var productUrlTitle = _localizationService.GetResource("Common.OpenInShop"); - var productSeName = product.GetSeName(); + var productUrlTitle = _localizationService.GetResource("Common.OpenInShop"); + var productSeName = product.GetSeName(); - var productVariantAttributesModel = allCombinations.Select(x => - { - var pvacModel = x.ToModel(); - PrepareProductAttributeCombinationModel(pvacModel, x, product, true); + var productVariantAttributesModel = allCombinations.Select(x => + { + var pvacModel = x.ToModel(); + PrepareProductAttributeCombinationModel(pvacModel, x, product, true); - pvacModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(x.AttributesXml, product.Id, productSeName); - pvacModel.ProductUrlTitle = productUrlTitle; + pvacModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(x.AttributesXml, product.Id, productSeName); + pvacModel.ProductUrlTitle = productUrlTitle; - try - { - var firstAttribute = _productAttributeParser.DeserializeProductVariantAttributes(x.AttributesXml).FirstOrDefault(); - - var attribute = x.Product.ProductVariantAttributes.FirstOrDefault(y => y.Id == firstAttribute.Key); - var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(y => y.Id == int.Parse(firstAttribute.Value.First())); + try + { + var firstAttribute = _productAttributeParser.DeserializeProductVariantAttributes(x.AttributesXml).FirstOrDefault(); - pvacModel.DisplayOrder = attributeValue.DisplayOrder; - } - catch (Exception exc) - { - exc.Dump(); - } + var attribute = x.Product.ProductVariantAttributes.FirstOrDefault(y => y.Id == firstAttribute.Key); + var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(y => y.Id == int.Parse(firstAttribute.Value.First())); - //if (x.IsDefaultCombination) - // pvacModel.AttributesXml = "{0}".FormatWith(pvacModel.AttributesXml); + pvacModel.DisplayOrder = attributeValue.DisplayOrder; + } + catch (Exception exc) + { + exc.Dump(); + } - //warnings - var warnings = _shoppingCartService.GetShoppingCartItemAttributeWarnings( - _workContext.CurrentCustomer, - ShoppingCartType.ShoppingCart, - x.Product, - x.AttributesXml, - combination: x); - - pvacModel.Warnings.AddRange(warnings); + //if (x.IsDefaultCombination) + // pvacModel.AttributesXml = "{0}".FormatWith(pvacModel.AttributesXml); - return pvacModel; - }) - .OrderBy(x => x.DisplayOrder).ToList(); + //warnings + var warnings = _shoppingCartService.GetShoppingCartItemAttributeWarnings( + _workContext.CurrentCustomer, + ShoppingCartType.ShoppingCart, + x.Product, + x.AttributesXml, + combination: x); + + pvacModel.Warnings.AddRange(warnings); + + return pvacModel; + }) + .OrderBy(x => x.DisplayOrder) + .ToList(); - var model = new GridModel + model.Data = productVariantAttributesModel; + model.Total = allCombinations.TotalCount; + } + else { - Data = productVariantAttributesModel, - Total = allCombinations.TotalCount, - }; + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -3931,19 +3390,16 @@ public ActionResult ProductVariantAttributeCombinationList(GridCommand command, [GridAction(EnableCustomBinding = true)] public ActionResult ProductVariantAttributeCombinationDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - var pvac = _productAttributeService.GetProductVariantAttributeCombinationById(id); - if (pvac == null) - throw new ArgumentException("No product variant attribute combination found with the specified id"); - var productId = pvac.ProductId; - _productAttributeService.DeleteProductVariantAttributeCombination(pvac); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + _productAttributeService.DeleteProductVariantAttributeCombination(pvac); - var product = _productService.GetProductById(productId); - _productService.UpdateLowestAttributeCombinationPriceProperty(product); + var product = _productService.GetProductById(productId); + _productService.UpdateLowestAttributeCombinationPriceProperty(product); + } return ProductVariantAttributeCombinationList(command, productId); } @@ -4085,7 +3541,7 @@ public ActionResult CreateAllAttributeCombinations(ProductVariantAttributeCombin 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)); _productAttributeService.CreateAllProductVariantAttributeCombinations(product); @@ -4103,7 +3559,7 @@ public ActionResult DeleteAllAttributeCombinations(ProductVariantAttributeCombin 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)); _pvacRepository.DeleteAll(x => x.ProductId == product.Id); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductReviewController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductReviewController.cs index 9858dc1745..5f63d63b5b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ProductReviewController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ProductReviewController.cs @@ -17,7 +17,7 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class ProductReviewController : AdminControllerBase { #region Fields @@ -109,34 +109,40 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, ProductReviewListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + DateTime? createdOnFromValue = (model.CreatedOnFrom == null) ? null + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnFrom.Value, _dateTimeHelper.CurrentTimeZone); - DateTime? createdOnFromValue = (model.CreatedOnFrom == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnFrom.Value, _dateTimeHelper.CurrentTimeZone); + DateTime? createdToFromValue = (model.CreatedOnTo == null) ? null + : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnTo.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); - DateTime? createdToFromValue = (model.CreatedOnTo == null) ? null - : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.CreatedOnTo.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + var productReviews = _customerContentService.GetAllCustomerContent(0, null, createdOnFromValue, createdToFromValue); + + gridModel.Data = productReviews.PagedForCommand(command).Select(x => + { + var m = new ProductReviewModel(); + PrepareProductReviewModel(m, x, false, true); + return m; + }); + + gridModel.Total = productReviews.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var productReviews = _customerContentService.GetAllCustomerContent(0, null, - createdOnFromValue, createdToFromValue); - var gridModel = new GridModel - { - Data = productReviews.PagedForCommand(command).Select(x => - { - var m = new ProductReviewModel(); - PrepareProductReviewModel(m, x, false, true); - return m; - }), - Total = productReviews.Count, - }; return new JsonResult { Data = gridModel }; } - //edit public ActionResult Edit(int id) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) @@ -144,11 +150,11 @@ public ActionResult Edit(int id) var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; if (productReview == null) - //No product review found with the specified id return RedirectToAction("List"); var model = new ProductReviewModel(); PrepareProductReviewModel(model, productReview, false, false); + return View(model); } @@ -160,7 +166,6 @@ public ActionResult Edit(ProductReviewModel model, bool continueEditing) var productReview = _customerContentService.GetCustomerContentById(model.Id) as ProductReview; if (productReview == null) - //No product review found with the specified id return RedirectToAction("List"); if (ModelState.IsValid) @@ -180,13 +185,11 @@ public ActionResult Edit(ProductReviewModel model, bool continueEditing) return continueEditing ? RedirectToAction("Edit", productReview.Id) : RedirectToAction("List"); } - //If we got this far, something failed, redisplay form PrepareProductReviewModel(model, productReview, true, false); return View(model); } - //delete [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { @@ -195,11 +198,11 @@ public ActionResult DeleteConfirmed(int id) var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; if (productReview == null) - //No product review found with the specified id return RedirectToAction("List"); var product = productReview.Product; _customerContentService.DeleteCustomerContent(productReview); + //update product totals _productService.UpdateProductReviewTotals(product); @@ -210,84 +213,107 @@ public ActionResult DeleteConfirmed(int id) [HttpPost] public ActionResult ApproveSelected(ICollection selectedIds) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var result = true; - if (selectedIds != null) - { - foreach (var id in selectedIds) - { - var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; - if (productReview != null) - { - productReview.IsApproved = true; - _customerContentService.UpdateCustomerContent(productReview); - - //update product totals - _productService.UpdateProductReviewTotals(productReview.Product); - - _customerService.RewardPointsForProductReview(productReview.Customer, productReview.Product, true); - } - } - } + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + if (selectedIds != null) + { + foreach (var id in selectedIds) + { + var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; + if (productReview != null) + { + productReview.IsApproved = true; + _customerContentService.UpdateCustomerContent(productReview); + + //update product totals + _productService.UpdateProductReviewTotals(productReview.Product); + + _customerService.RewardPointsForProductReview(productReview.Customer, productReview.Product, true); + } + } + } + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + result = false; + NotifyAccessDenied(); + } - return Json(new { Result = true }); + return Json(new { Result = result }); } [HttpPost] public ActionResult DeleteSelected(ICollection selectedIds) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (selectedIds != null) - { - foreach (var id in selectedIds) - { - var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; - if (productReview != null) - { - var product = productReview.Product; - _customerContentService.DeleteCustomerContent(productReview); - //update product totals - _productService.UpdateProductReviewTotals(product); - } - } - } + var result = true; - return Json(new { Result = true }); - } + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + if (selectedIds != null) + { + foreach (var id in selectedIds) + { + var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; + if (productReview != null) + { + var product = productReview.Product; + _customerContentService.DeleteCustomerContent(productReview); + //update product totals + _productService.UpdateProductReviewTotals(product); + } + } + } + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + result = false; + NotifyAccessDenied(); + } + return Json(new { Result = result }); + } [HttpPost] public ActionResult DisapproveSelected(ICollection selectedIds) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - if (selectedIds != null) - { - foreach (var id in selectedIds) - { - var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; - if (productReview != null) - { - productReview.IsApproved = false; - _customerContentService.UpdateCustomerContent(productReview); - - //update product totals - _productService.UpdateProductReviewTotals(productReview.Product); - - _customerService.RewardPointsForProductReview(productReview.Customer, productReview.Product, false); - } - } - } - - return Json(new { Result = true }); - } - + var result = true; + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + if (selectedIds != null) + { + foreach (var id in selectedIds) + { + var productReview = _customerContentService.GetCustomerContentById(id) as ProductReview; + if (productReview != null) + { + productReview.IsApproved = false; + _customerContentService.UpdateCustomerContent(productReview); + + //update product totals + _productService.UpdateProductReviewTotals(productReview.Product); + + _customerService.RewardPointsForProductReview(productReview.Customer, productReview.Product, false); + } + } + } + + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + } + else + { + result = false; + NotifyAccessDenied(); + } + return Json(new { Result = result }); + } #endregion } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs index 58057c2d80..7814196042 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/QueuedEmailController.cs @@ -53,41 +53,47 @@ public ActionResult List() [GridAction(EnableCustomBinding = true)] public ActionResult QueuedEmailList(GridCommand command, QueuedEmailListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageMessageQueue)) - return AccessDeniedView(); - - DateTime? startDateValue = (model.SearchStartDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.SearchStartDate.Value, _dateTimeHelper.CurrentTimeZone); - DateTime? endDateValue = (model.SearchEndDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.SearchEndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); + var gridModel = new GridModel(); - var q = new SearchEmailsQuery + if (_permissionService.Authorize(StandardPermissionProvider.ManageMessageQueue)) { - EndTime = endDateValue, - From = model.SearchFromEmail, - MaxSendTries = model.SearchMaxSentTries, - OrderByLatest = true, - PageIndex = command.Page - 1, - PageSize = command.PageSize, - SendManually = model.SearchSendManually, - StartTime = startDateValue, - To = model.SearchToEmail, - UnsentOnly = model.SearchLoadNotSent - }; - var queuedEmails = _queuedEmailService.SearchEmails(q); + DateTime? startDateValue = (model.SearchStartDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.SearchStartDate.Value, _dateTimeHelper.CurrentTimeZone); + DateTime? endDateValue = (model.SearchEndDate == null) ? null : (DateTime?)_dateTimeHelper.ConvertToUtcTime(model.SearchEndDate.Value, _dateTimeHelper.CurrentTimeZone).AddDays(1); - var gridModel = new GridModel - { - Data = queuedEmails.Select(x => + var q = new SearchEmailsQuery { - var m = x.ToModel(); - m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); + EndTime = endDateValue, + From = model.SearchFromEmail, + MaxSendTries = model.SearchMaxSentTries, + OrderByLatest = true, + PageIndex = command.Page - 1, + PageSize = command.PageSize, + SendManually = model.SearchSendManually, + StartTime = startDateValue, + To = model.SearchToEmail, + UnsentOnly = model.SearchLoadNotSent + }; + var queuedEmails = _queuedEmailService.SearchEmails(q); + + gridModel.Data = queuedEmails.Select(x => + { + var m = x.ToModel(); + m.CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc); - if (x.SentOnUtc.HasValue) - m.SentOn = _dateTimeHelper.ConvertToUserTime(x.SentOnUtc.Value, DateTimeKind.Utc); + if (x.SentOnUtc.HasValue) + m.SentOn = _dateTimeHelper.ConvertToUserTime(x.SentOnUtc.Value, DateTimeKind.Utc); - return m; - }), - Total = queuedEmails.TotalCount - }; + return m; + }); + + gridModel.Total = queuedEmails.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -273,6 +279,18 @@ public ActionResult DownloadAttachment(int id) { path = CommonHelper.MapPath(VirtualPathUtility.ToAppRelative(path), false); } + + if (!System.IO.File.Exists(path)) + { + NotifyError(string.Concat(T("Admin.Common.FileNotFound"), ": ", path)); + + var referrer = Services.WebHelper.GetUrlReferrer(); + if (referrer.HasValue()) + return Redirect(referrer); + + return RedirectToAction("List"); + } + return File(path, qea.MimeType, qea.Name); } @@ -284,7 +302,6 @@ public ActionResult DownloadAttachment(int id) NotifyError(T("Admin.System.QueuedEmails.CouldNotDownloadAttachment")); return RedirectToAction("List"); - } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/RecurringPaymentController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/RecurringPaymentController.cs index 39d549fc08..0f4b15968e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/RecurringPaymentController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/RecurringPaymentController.cs @@ -113,7 +113,6 @@ private void PrepareRecurringPaymentHistoryModel(RecurringPaymentModel.Recurring #region Recurring payment - //list public ActionResult Index() { return RedirectToAction("List"); @@ -131,27 +130,34 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var payments = _orderService.SearchRecurringPayments(0, 0, 0, null, true); + + gridModel.Data = payments.PagedForCommand(command).Select(x => + { + var m = new RecurringPaymentModel(); + PrepareRecurringPaymentModel(m, x, false); + return m; + }); + + gridModel.Total = payments.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var payments = _orderService.SearchRecurringPayments(0, 0, 0, null, true); - var gridModel = new GridModel - { - Data = payments.PagedForCommand(command).Select(x => - { - var m = new RecurringPaymentModel(); - PrepareRecurringPaymentModel(m, x, false); - return m; - }), - Total = payments.Count, - }; return new JsonResult { Data = gridModel }; } - //edit public ActionResult Edit(int id) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) @@ -189,7 +195,6 @@ public ActionResult Edit(RecurringPaymentModel model, bool continueEditing) return continueEditing ? RedirectToAction("Edit", payment.Id) : RedirectToAction("List"); } - //delete [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { @@ -214,26 +219,30 @@ public ActionResult DeleteConfirmed(int id) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult HistoryList(int recurringPaymentId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); - - var payment = _orderService.GetRecurringPaymentById(recurringPaymentId); - if (payment == null) - throw new ArgumentException("No recurring payment found with the specified id"); - - var historyModel = payment.RecurringPaymentHistory.OrderBy(x => x.CreatedOnUtc) - .Select(x => - { - var m = new RecurringPaymentModel.RecurringPaymentHistoryModel(); - PrepareRecurringPaymentHistoryModel(m, x); - return m; - }) - .ToList(); - var model = new GridModel - { - Data = historyModel, - Total = historyModel.Count - }; + var model = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var payment = _orderService.GetRecurringPaymentById(recurringPaymentId); + + var historyModel = payment.RecurringPaymentHistory.OrderBy(x => x.CreatedOnUtc) + .Select(x => + { + var m = new RecurringPaymentModel.RecurringPaymentHistoryModel(); + PrepareRecurringPaymentHistoryModel(m, x); + return m; + }) + .ToList(); + + model.Data = historyModel; + model.Total = historyModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ReturnRequestController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ReturnRequestController.cs index 1180d71fc7..9e65217d0c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ReturnRequestController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ReturnRequestController.cs @@ -24,7 +24,7 @@ namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public class ReturnRequestController : AdminControllerBase { #region Fields @@ -78,15 +78,11 @@ public ReturnRequestController(IOrderService orderService, private void PrepareReturnRequestListModel(ReturnRequestListModel model) { - string allString = _localizationService.GetResource("Admin.Common.All"); - model.GridPageSize = _adminAreaSettings.GridPageSize; - model.AvailableStores.Add(new SelectListItem() { Text = allString, Value = "0" }); - model.AvailableStores.AddRange(_storeService.GetAllStores().ToSelectListItems()); + model.AvailableStores = _storeService.GetAllStores().ToSelectListItems(); model.AvailableReturnRequestStatus = ReturnRequestStatus.Pending.ToSelectList(false).ToList(); - model.AvailableReturnRequestStatus.Insert(0, new SelectListItem() { Text = allString }); } private bool PrepareReturnRequestModel(ReturnRequestModel model, ReturnRequest returnRequest, bool excludeProperties) @@ -171,7 +167,6 @@ private bool PrepareReturnRequestModel(ReturnRequestModel model, ReturnRequest r #region Methods - //list public ActionResult Index() { return RedirectToAction("List"); @@ -191,26 +186,31 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, ReturnRequestListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageReturnRequests)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var data = new List(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageReturnRequests)) + { + var data = new List(); - var returnRequests = _orderService.SearchReturnRequests(model.SearchStoreId, 0, 0, model.SearchReturnRequestStatus, - command.Page - 1, command.PageSize, model.SearchId ?? 0); + var returnRequests = _orderService.SearchReturnRequests(model.SearchStoreId, 0, 0, model.SearchReturnRequestStatus, + command.Page - 1, command.PageSize, model.SearchId ?? 0); - foreach (var rr in returnRequests) - { - var m = new ReturnRequestModel(); - if (PrepareReturnRequestModel(m, rr, false)) - data.Add(m); - } + foreach (var rr in returnRequests) + { + var m = new ReturnRequestModel(); + if (PrepareReturnRequestModel(m, rr, false)) + data.Add(m); + } - var gridModel = new GridModel + gridModel.Data = data; + gridModel.Total = returnRequests.TotalCount; + } + else { - Data = data, - Total = returnRequests.TotalCount, - }; + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -218,7 +218,6 @@ public ActionResult List(GridCommand command, ReturnRequestListModel model) }; } - //edit public ActionResult Edit(int id) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageReturnRequests)) @@ -300,7 +299,6 @@ public ActionResult NotifyCustomer(ReturnRequestModel model) return RedirectToAction("Edit", returnRequest.Id); } - //delete [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs new file mode 100644 index 0000000000..e0c2ca36c2 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/RoxyFileManagerController.cs @@ -0,0 +1,828 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.IO.Compression; +using System.Text.RegularExpressions; +using System.Web; +using SmartStore.Services.Security; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; + +namespace SmartStore.Admin.Controllers +{ + [AdminAuthorize] + public class RoxyFileManagerController : AdminControllerBase + { + private const string CONFIG_FILE = "~/Administration/Content/filemanager/conf.json"; + + private Dictionary _lang = null; + private Dictionary _settings = null; + + private readonly IPermissionService _permissionService; + private readonly HttpContextBase _context; + private readonly HttpResponseBase _response; + + public RoxyFileManagerController( + IPermissionService permissionService, + HttpContextBase context) + { + _permissionService = permissionService; + _context = context; + _response = _context.Response; + } + + #region Utilities + + private string FixPath(string path) + { + if (path == null) + { + path = ""; + } + + if (!path.StartsWith("~")) + { + if (!path.StartsWith("/")) + path = "/" + path; + path = "~" + path; + } + + //var filesRoot = GetSetting("FILES_ROOT"); + //if (!path.ToLowerInvariant().Contains(filesRoot.ToLowerInvariant())) + //{ + // path = filesRoot; + //} + + return _context.Server.MapPath(path); + } + + private string GetLangFile() + { + //return "../lang/en.json"; + + var filename = "../lang/" + GetSetting("LANG") + ".json"; + + if (!System.IO.File.Exists(_context.Server.MapPath(filename))) + filename = "../lang/en.json"; + + return filename; + } + + 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 = System.IO.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(CONFIG_FILE); + } + + 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 (!System.IO.File.Exists(Path.Combine(dest, file.Name))) + { + System.IO.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); + } + _response.Write(GetSuccessRes()); + } + + protected string MakeUniqueFilename(string dir, string filename) + { + string ret = filename; + int i = 0; + while (System.IO.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 + { + System.IO.File.Copy(file.FullName, Path.Combine(newPath, newName)); + _response.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); + _response.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); + _response.Write(GetSuccessRes()); + } + catch + { + throw new Exception(LangRes("E_CannotDeleteDir")); + } + } + } + + protected void DeleteFile(string path) + { + CheckPath(path); + path = FixPath(path); + if (!System.IO.File.Exists(path)) + throw new Exception(LangRes("E_DeleteFileInvalidPath")); + else + { + try + { + System.IO.File.Delete(path); + _response.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("~/"); + _response.Write("["); + for (int i = 0; i < dirs.Count; i++) + { + string dir = (string)dirs[i]; + _response.Write("{\"p\":\"/" + dir.Replace(localPath, "").Replace("\\", "/") + "\",\"f\":\"" + GetFiles(dir, type).Count.ToString() + "\",\"d\":\"" + Directory.GetDirectories(dir).Length.ToString() + "\"}"); + if (i < dirs.Count - 1) + _response.Write(","); + } + _response.Write("]"); + } + + protected double LinuxTimestamp(DateTime d) + { + DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0).ToLocalTime(); + TimeSpan timeSpan = (d.ToLocalTime() - epoch); + + return timeSpan.TotalSeconds; + + } + + protected void ListFiles(string path, string type) + { + CheckPath(path); + string fullPath = FixPath(path); + List files = GetFiles(fullPath, type); + _response.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; + } + } + _response.Write("{"); + _response.Write("\"p\":\"" + path + "/" + f.Name + "\""); + _response.Write(",\"t\":\"" + Math.Ceiling(LinuxTimestamp(f.LastWriteTime)).ToString() + "\""); + _response.Write(",\"s\":\"" + f.Length.ToString() + "\""); + _response.Write(",\"w\":\"" + w.ToString() + "\""); + _response.Write(",\"h\":\"" + h.ToString() + "\""); + _response.Write("}"); + if (i < files.Count - 1) + _response.Write(","); + } + _response.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 (System.IO.File.Exists(tmpZip)) + System.IO.File.Delete(tmpZip); + ZipFile.CreateFromDirectory(path, tmpZip, CompressionLevel.Fastest, true); + _response.Clear(); + _response.Headers.Add("Content-Disposition", "attachment; filename=\"" + dirName + ".zip\""); + _response.ContentType = "application/force-download"; + _response.TransmitFile(tmpZip); + _response.Flush(); + System.IO.File.Delete(tmpZip); + _response.End(); + } + + protected void DownloadFile(string path) + { + CheckPath(path); + FileInfo file = new FileInfo(FixPath(path)); + if (file.Exists) + { + _response.Clear(); + _response.Headers.Add("Content-Disposition", "attachment; filename=\"" + file.Name + "\""); + _response.ContentType = "application/force-download"; + _response.TransmitFile(file.FullName); + _response.Flush(); + _response.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); + _response.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); + _response.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); + _response.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); + _response.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); + + _response.AddHeader("Content-Type", "image/png"); + cropImg.GetThumbnailImage(width, height, imgCallback, IntPtr.Zero).Save(_response.OutputStream, ImageFormat.Png); + _response.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 < Request.Files.Count; i++) + { + if (CanHandleFile(Request.Files[i].FileName)) + { + string filename = MakeUniqueFilename(path, Request.Files[i].FileName); + string dest = Path.Combine(path, filename); + 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); + } + _response.Write(""); + } + + public bool IsReusable + { + get + { + return false; + } + } + + #endregion + + public void ProcessRequest() + { + if (!_permissionService.Authorize(StandardPermissionProvider.UploadPictures)) + { + _response.Write(GetErrorRes("You do not have the required permission")); + return; + } + + 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: + _response.Write(GetErrorRes("This action is not implemented.")); + break; + } + } + catch (Exception ex) + { + if (action == "UPLOAD") + { + _response.Write(""); + } + else + { + _response.Write(GetErrorRes(ex.Message)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs index e2d1a99084..453ae689c8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ScheduleTaskController.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Collections.Generic; using System.Threading; using System.Web.Mvc; using SmartStore.Admin.Extensions; @@ -13,10 +14,11 @@ using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; +using SmartStore.Core; namespace SmartStore.Admin.Controllers { - [AdminAuthorize] + [AdminAuthorize] public partial class ScheduleTaskController : AdminControllerBase { private readonly IScheduleTaskService _scheduleTaskService; @@ -24,19 +26,22 @@ public partial class ScheduleTaskController : AdminControllerBase private readonly IPermissionService _permissionService; private readonly IDateTimeHelper _dateTimeHelper; private readonly ILocalizationService _localizationService; + private readonly IWorkContext _workContext; - public ScheduleTaskController( + public ScheduleTaskController( IScheduleTaskService scheduleTaskService, ITaskScheduler taskScheduler, IPermissionService permissionService, IDateTimeHelper dateTimeHelper, - ILocalizationService localizationService) + ILocalizationService localizationService, + IWorkContext workContext) { this._scheduleTaskService = scheduleTaskService; this._taskScheduler = taskScheduler; this._permissionService = permissionService; this._dateTimeHelper = dateTimeHelper; this._localizationService = localizationService; + this._workContext = workContext; } private bool IsTaskVisible(ScheduleTask task) @@ -52,7 +57,30 @@ private bool IsTaskVisible(ScheduleTask task) return false; } - public ActionResult Index() + private string GetTaskMessage(ScheduleTask task, string resourceKey) + { + string message = null; + + var taskClassName = task.Type + .SplitSafe(",") + .SafeGet(0) + .SplitSafe(".") + .LastOrDefault(); + + if (taskClassName.HasValue()) + { + message = _localizationService.GetResource(string.Concat(resourceKey, ".", taskClassName), returnEmptyIfNotFound: true); + } + + if (message.IsEmpty()) + { + message = T(resourceKey); + } + + return message; + } + + public ActionResult Index() { return RedirectToAction("List"); } @@ -114,9 +142,10 @@ public ActionResult RunJob(int id, string returnUrl = "") if (!_permissionService.Authorize(StandardPermissionProvider.ManageScheduleTasks)) return AccessDeniedView(); - returnUrl = returnUrl.NullEmpty() ?? Request.UrlReferrer.ToString(); + var taskParams = new Dictionary(); + taskParams[TaskExecutor.CurrentCustomerIdParamName] = _workContext.CurrentCustomer.Id.ToString(); - _taskScheduler.RunSingleTask(id); + _taskScheduler.RunSingleTask(id, taskParams); // The most tasks are completed rather quickly. Wait a while... var start = DateTime.UtcNow; @@ -128,7 +157,7 @@ public ActionResult RunJob(int id, string returnUrl = "") { if (task.IsRunning) { - NotifyInfo(T("Admin.System.ScheduleTasks.RunNow.Progress")); + NotifyInfo(GetTaskMessage(task, "Admin.System.ScheduleTasks.RunNow.Progress")); } else { @@ -138,13 +167,12 @@ public ActionResult RunJob(int id, string returnUrl = "") } else { - NotifySuccess(T("Admin.System.ScheduleTasks.RunNow.Success")); + NotifySuccess(GetTaskMessage(task, "Admin.System.ScheduleTasks.RunNow.Success")); } } - var now = DateTime.UtcNow; } - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } public ActionResult CancelJob(int id /* scheduleTaskId */, string returnUrl = "") @@ -159,8 +187,7 @@ public ActionResult CancelJob(int id /* scheduleTaskId */, string returnUrl = "" NotifyWarning(T("Admin.System.ScheduleTasks.CancellationRequested")); } - returnUrl = returnUrl.NullEmpty() ?? Request.UrlReferrer.ToString(); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } public ActionResult Edit(int id /* taskId */, string returnUrl = null) @@ -195,7 +222,7 @@ public ActionResult Edit(ScheduleTaskModel model, bool continueEditing, string r } var reloadResult = RedirectToAction("Edit", new { id = model.Id, returnUrl = returnUrl }); - var returnResult = Redirect(returnUrl.NullEmpty() ?? Url.Action("List")); + var returnResult = RedirectToReferrer(returnUrl, () => RedirectToAction("List")); var scheduleTask = _scheduleTaskService.GetTaskById(model.Id); if (scheduleTask == null) @@ -246,7 +273,7 @@ public ActionResult FutureSchedules(string expression) } [ChildActionOnly] - public ActionResult MinimalTask(int taskId, string returnUrl /* mandatory on purpose */, bool cancellable = true) + public ActionResult MinimalTask(int taskId, string returnUrl /* mandatory on purpose */, bool cancellable = true, bool reloadPage = false) { var task = _scheduleTaskService.GetTaskById(taskId); if (task == null) @@ -257,6 +284,7 @@ public ActionResult MinimalTask(int taskId, string returnUrl /* mandatory on pur ViewBag.HasPermission = _permissionService.Authorize(StandardPermissionProvider.ManageScheduleTasks); ViewBag.ReturnUrl = returnUrl; ViewBag.Cancellable = cancellable; + ViewBag.ReloadPage = reloadPage; var model = task.ToScheduleTaskModel(_localizationService, _dateTimeHelper, Url); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs index 794c33e0f8..45b5aa901a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SecurityController.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using SmartStore.Admin.Models.Customers; using SmartStore.Admin.Models.Security; using SmartStore.Core; -using SmartStore.Services.Customers; -using SmartStore.Services.Localization; using SmartStore.Core.Logging; +using SmartStore.Services.Customers; using SmartStore.Services.Security; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; @@ -22,7 +20,6 @@ public class SecurityController : AdminControllerBase private readonly IWorkContext _workContext; private readonly IPermissionService _permissionService; private readonly ICustomerService _customerService; - private readonly ILocalizationService _localizationService; #endregion @@ -30,12 +27,11 @@ public class SecurityController : AdminControllerBase public SecurityController(IWorkContext workContext, IPermissionService permissionService, - ICustomerService customerService, ILocalizationService localizationService) + ICustomerService customerService) { this._workContext = workContext; this._permissionService = permissionService; this._customerService = customerService; - this._localizationService = localizationService; } #endregion  @@ -45,14 +41,15 @@ public SecurityController(IWorkContext workContext, public ActionResult AccessDenied(string pageUrl) { var currentCustomer = _workContext.CurrentCustomer; + if (currentCustomer == null || currentCustomer.IsGuest()) { - Logger.Information(string.Format("Access denied to anonymous request on {0}", pageUrl)); + Logger.Information(T("Admin.System.Warnings.AccessDeniedToAnonymousRequest", pageUrl.NaIfEmpty())); return View(); } - Logger.Information(string.Format("Access denied to user #{0} '{1}' on {2}", currentCustomer.Email, currentCustomer.Email, pageUrl)); - + Logger.Information(T("Admin.System.Warnings.AccessDeniedToUser", + currentCustomer.Email.NaIfEmpty(), currentCustomer.Email.NaIfEmpty(), pageUrl.NaIfEmpty())); return View(); } @@ -66,30 +63,38 @@ public ActionResult Permissions() var permissionRecords = _permissionService.GetAllPermissionRecords(); var customerRoles = _customerService.GetAllCustomerRoles(true); + foreach (var pr in permissionRecords) { - model.AvailablePermissions.Add(new PermissionRecordModel() + model.AvailablePermissions.Add(new PermissionRecordModel { Name = pr.Name, - SystemName = pr.SystemName + SystemName = pr.SystemName, + Category = pr.Category }); } + foreach (var cr in customerRoles) { - model.AvailableCustomerRoles.Add(new CustomerRoleModel() + model.AvailableCustomerRoles.Add(new CustomerRoleModel { Id = cr.Id, Name = cr.Name }); } - foreach (var pr in permissionRecords) - foreach (var cr in customerRoles) - { - bool allowed = pr.CustomerRoles.Where(x => x.Id == cr.Id).ToList().Count() > 0; - if (!model.Allowed.ContainsKey(pr.SystemName)) - model.Allowed[pr.SystemName] = new Dictionary(); - model.Allowed[pr.SystemName][cr.Id] = allowed; - } + + foreach (var pr in permissionRecords) + { + foreach (var cr in customerRoles) + { + var allowed = pr.CustomerRoles.Any(x => x.Id == cr.Id); + + if (!model.Allowed.ContainsKey(pr.SystemName)) + model.Allowed[pr.SystemName] = new Dictionary(); + + model.Allowed[pr.SystemName][cr.Id] = allowed; + } + } return View(model); } @@ -103,38 +108,38 @@ public ActionResult PermissionsSave(FormCollection form) var permissionRecords = _permissionService.GetAllPermissionRecords(); var customerRoles = _customerService.GetAllCustomerRoles(true); - foreach (var cr in customerRoles) { - string formKey = "allow_" + cr.Id; - var permissionRecordSystemNamesToRestrict = form[formKey] != null ? form[formKey].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() : new List(); + var restrictedSystemNames = form["allow_" + cr.Id.ToString()].SplitSafe(",").ToList(); - foreach (var pr in permissionRecords) + foreach (var permission in permissionRecords) { + bool allow = restrictedSystemNames.Contains(permission.SystemName); - bool allow = permissionRecordSystemNamesToRestrict.Contains(pr.SystemName); if (allow) { - if (pr.CustomerRoles.Where(x => x.Id == cr.Id).FirstOrDefault() == null) + if (!permission.CustomerRoles.Any(x => x.Id == cr.Id)) { - pr.CustomerRoles.Add(cr); - _permissionService.UpdatePermissionRecord(pr); + permission.CustomerRoles.Add(cr); + _permissionService.UpdatePermissionRecord(permission); } } else { - if (pr.CustomerRoles.Where(x => x.Id == cr.Id).FirstOrDefault() != null) + if (permission.CustomerRoles.Any(x => x.Id == cr.Id)) { - pr.CustomerRoles.Remove(cr); - _permissionService.UpdatePermissionRecord(pr); + permission.CustomerRoles.Remove(cr); + _permissionService.UpdatePermissionRecord(permission); } } } } - NotifySuccess(_localizationService.GetResource("Admin.Configuration.ACL.Updated")); + NotifySuccess(T("Admin.Configuration.ACL.Updated")); + return RedirectToAction("Permissions"); } + #endregion } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs index 8ad100f9ee..d26e835c28 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs @@ -9,6 +9,7 @@ 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.Forums; using SmartStore.Core.Domain.Localization; @@ -31,6 +32,7 @@ using SmartStore.Services.Orders; using SmartStore.Services.Security; using SmartStore.Services.Tax; +using SmartStore.Utilities; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; @@ -112,7 +114,9 @@ public SettingController( this._services = services; } - #endregion  + #endregion + + #region Utilities private StoreDependingSettingHelper StoreDependingSettings { @@ -124,7 +128,9 @@ private StoreDependingSettingHelper StoreDependingSettings } } - #region Methods + #endregion + + #region Methods [ChildActionOnly] public ActionResult StoreScopeConfiguration() @@ -133,14 +139,14 @@ public ActionResult StoreScopeConfiguration() if (allStores.Count < 2) return Content(""); - var model = new StoreScopeConfigurationModel() + var model = new StoreScopeConfigurationModel { StoreId = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext) }; foreach (var store in allStores) { - model.AllStores.Add(new SelectListItem() + model.AllStores.Add(new SelectListItem { Text = store.Name, Selected = (store.Id == model.StoreId), @@ -148,7 +154,7 @@ public ActionResult StoreScopeConfiguration() }); } - model.AllStores.Insert(0, new SelectListItem() + model.AllStores.Insert(0, new SelectListItem { Text = _services.Localization.GetResource("Admin.Common.StoresAll"), Selected = (0 == model.StoreId), @@ -166,16 +172,8 @@ public ActionResult ChangeStoreScopeConfiguration(int storeid, string returnUrl _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, SystemCustomerAttributeNames.AdminAreaStoreScopeConfiguration, storeid); } - - //url referrer - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = _services.WebHelper.GetUrlReferrer(); - - //home page - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = Url.Action("Index", "Home", new { area = "Admin" }); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl, () => RedirectToAction("Index", "Home", new { area = "Admin" })); } public ActionResult Blog() @@ -641,35 +639,31 @@ public ActionResult RewardPoints(RewardPointsSettingsModel model, FormCollection if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) return AccessDeniedView(); - if (ModelState.IsValid) - { - //load settings for a chosen store scope - var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var rewardPointsSettings = _services.Settings.LoadSetting(storeScope); - rewardPointsSettings = model.ToEntity(rewardPointsSettings); + if (!ModelState.IsValid) + return RewardPoints(); - StoreDependingSettings.UpdateSettings(rewardPointsSettings, form, storeScope, _services.Settings); + ModelState.Clear(); - bool pointsForPurchases = StoreDependingSettings.IsOverrideChecked(rewardPointsSettings, "PointsForPurchases", form); + //load settings for a chosen store scope + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var rewardPointsSettings = _services.Settings.LoadSetting(storeScope); + rewardPointsSettings = model.ToEntity(rewardPointsSettings); - _services.Settings.UpdateSetting(rewardPointsSettings, x => x.PointsForPurchases_Amount, pointsForPurchases, storeScope); - _services.Settings.UpdateSetting(rewardPointsSettings, x => x.PointsForPurchases_Points, pointsForPurchases, storeScope); + StoreDependingSettings.UpdateSettings(rewardPointsSettings, form, storeScope, _services.Settings); - //now clear settings cache - _services.Settings.ClearCache(); + bool pointsForPurchases = StoreDependingSettings.IsOverrideChecked(rewardPointsSettings, "PointsForPurchases", form); - //activity log - _customerActivityService.InsertActivity("EditSettings", _services.Localization.GetResource("ActivityLog.EditSettings")); + _services.Settings.UpdateSetting(rewardPointsSettings, x => x.PointsForPurchases_Amount, pointsForPurchases, storeScope); + _services.Settings.UpdateSetting(rewardPointsSettings, x => x.PointsForPurchases_Points, pointsForPurchases, storeScope); + + //now clear settings cache + _services.Settings.ClearCache(); + + //activity log + _customerActivityService.InsertActivity("EditSettings", _services.Localization.GetResource("ActivityLog.EditSettings")); + + NotifySuccess(_services.Localization.GetResource("Admin.Configuration.Updated")); - NotifySuccess(_services.Localization.GetResource("Admin.Configuration.Updated")); - } - else - { - //If we got this far, something failed, redisplay form - foreach (var modelState in ModelState.Values) - foreach (var error in modelState.Errors) - NotifyError(error.ErrorMessage); - } return RedirectToAction("RewardPoints"); } @@ -684,13 +678,16 @@ public ActionResult Order() //load settings for a chosen store scope var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); var orderSettings = _services.Settings.LoadSetting(storeScope); - var store = (storeScope == 0 ? _services.StoreContext.CurrentStore : _services.StoreService.GetStoreById(storeScope)); + + var allStores = _services.StoreService.GetAllStores(); + var store = (storeScope == 0 ? _services.StoreContext.CurrentStore : allStores.FirstOrDefault(x => x.Id == storeScope)); var model = orderSettings.ToModel(); StoreDependingSettings.GetOverrideKeys(orderSettings, model, storeScope, _services.Settings); model.PrimaryStoreCurrencyCode = store.PrimaryStoreCurrency.CurrencyCode; + model.StoreCount = allStores.Count; //gift card activation/deactivation model.GiftCards_Activated_OrderStatuses = OrderStatus.Pending.ToSelectList(false).ToList(); @@ -713,64 +710,58 @@ public ActionResult Order(OrderSettingsModel model, FormCollection form) if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) return AccessDeniedView(); - if (ModelState.IsValid) - { - //load settings for a chosen store scope - var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var orderSettings = _services.Settings.LoadSetting(storeScope); - orderSettings = model.ToEntity(orderSettings); + if (!ModelState.IsValid) + return Order(); - StoreDependingSettings.UpdateSettings(orderSettings, form, storeScope, _services.Settings); + ModelState.Clear(); - _services.Settings.SaveSetting(orderSettings, x => x.ReturnRequestActions, 0, false); - _services.Settings.SaveSetting(orderSettings, x => x.ReturnRequestReasons, 0, false); + //load settings for a chosen store scope + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var orderSettings = _services.Settings.LoadSetting(storeScope); + orderSettings = model.ToEntity(orderSettings); - foreach (var localized in model.Locales) - { - _localizedEntityService.SaveLocalizedValue(orderSettings, x => x.ReturnRequestActions, localized.ReturnRequestActions, localized.LanguageId); - _localizedEntityService.SaveLocalizedValue(orderSettings, x => x.ReturnRequestReasons, localized.ReturnRequestReasons, localized.LanguageId); - } + StoreDependingSettings.UpdateSettings(orderSettings, form, storeScope, _services.Settings); - if (model.GiftCards_Activated_OrderStatusId.HasValue) - _services.Settings.SaveSetting(orderSettings, x => x.GiftCards_Activated_OrderStatusId, 0, false); - else - _services.Settings.DeleteSetting(orderSettings, x => x.GiftCards_Activated_OrderStatusId); + _services.Settings.SaveSetting(orderSettings, x => x.ReturnRequestActions, 0, false); + _services.Settings.SaveSetting(orderSettings, x => x.ReturnRequestReasons, 0, false); - if (model.GiftCards_Deactivated_OrderStatusId.HasValue) - _services.Settings.SaveSetting(orderSettings, x => x.GiftCards_Deactivated_OrderStatusId, 0, false); - else - _services.Settings.DeleteSetting(orderSettings, x => x.GiftCards_Deactivated_OrderStatusId); + foreach (var localized in model.Locales) + { + _localizedEntityService.SaveLocalizedValue(orderSettings, x => x.ReturnRequestActions, localized.ReturnRequestActions, localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(orderSettings, x => x.ReturnRequestReasons, localized.ReturnRequestReasons, localized.LanguageId); + } - //now clear settings cache - _services.Settings.ClearCache(); + if (model.GiftCards_Activated_OrderStatusId.HasValue) + _services.Settings.SaveSetting(orderSettings, x => x.GiftCards_Activated_OrderStatusId, 0, false); + else + _services.Settings.DeleteSetting(orderSettings, x => x.GiftCards_Activated_OrderStatusId); - //order ident - if (model.OrderIdent.HasValue) - { - try - { - _maintenanceService.SetTableIdent(model.OrderIdent.Value); - } - catch (Exception exc) - { - NotifyError(exc.Message); - } - } + if (model.GiftCards_Deactivated_OrderStatusId.HasValue) + _services.Settings.SaveSetting(orderSettings, x => x.GiftCards_Deactivated_OrderStatusId, 0, false); + else + _services.Settings.DeleteSetting(orderSettings, x => x.GiftCards_Deactivated_OrderStatusId); - //activity log - _customerActivityService.InsertActivity("EditSettings", _services.Localization.GetResource("ActivityLog.EditSettings")); + //now clear settings cache + _services.Settings.ClearCache(); - NotifySuccess(_services.Localization.GetResource("Admin.Configuration.Updated")); - } - else + //order ident + if (model.OrderIdent.HasValue) { - //If we got this far, something failed, redisplay form - foreach (var modelState in ModelState.Values) - { - foreach (var error in modelState.Errors) - NotifyError(error.ErrorMessage); - } + try + { + _maintenanceService.SetTableIdent(model.OrderIdent.Value); + } + catch (Exception exc) + { + NotifyError(exc.Message); + } } + + //activity log + _customerActivityService.InsertActivity("EditSettings", _services.Localization.GetResource("ActivityLog.EditSettings")); + + NotifySuccess(_services.Localization.GetResource("Admin.Configuration.Updated")); + return RedirectToAction("Order"); } @@ -782,13 +773,21 @@ public ActionResult ShoppingCart() if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) return AccessDeniedView(); - //load settings for a chosen store scope + var allLanguages = _languageService.GetAllLanguages(true); var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); var shoppingCartSettings = _services.Settings.LoadSetting(storeScope); + var model = shoppingCartSettings.ToModel(); + model.AvailableNewsLetterSubscriptions = shoppingCartSettings.NewsLetterSubscription.ToSelectList(); + model.AvailableThirdPartyEmailHandOver = shoppingCartSettings.ThirdPartyEmailHandOver.ToSelectList(); + StoreDependingSettings.GetOverrideKeys(shoppingCartSettings, model, storeScope, _services.Settings); + AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.ThirdPartyEmailHandOverLabel = shoppingCartSettings.GetLocalized(x => x.ThirdPartyEmailHandOverLabel, languageId, false, false); + }); return View(model); } @@ -798,20 +797,29 @@ public ActionResult ShoppingCart(ShoppingCartSettingsModel model, FormCollection if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) return AccessDeniedView(); - //load settings for a chosen store scope + if (!ModelState.IsValid) + return ShoppingCart(); + + ModelState.Clear(); + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); var shoppingCartSettings = _services.Settings.LoadSetting(storeScope); shoppingCartSettings = model.ToEntity(shoppingCartSettings); StoreDependingSettings.UpdateSettings(shoppingCartSettings, form, storeScope, _services.Settings); - //now clear settings cache + _services.Settings.SaveSetting(shoppingCartSettings, x => x.ThirdPartyEmailHandOverLabel, 0, false); + + foreach (var localized in model.Locales) + { + _localizedEntityService.SaveLocalizedValue(shoppingCartSettings, x => x.ThirdPartyEmailHandOverLabel, localized.ThirdPartyEmailHandOverLabel, localized.LanguageId); + } + _services.Settings.ClearCache(); - //activity log - _customerActivityService.InsertActivity("EditSettings", _services.Localization.GetResource("ActivityLog.EditSettings")); + _customerActivityService.InsertActivity("EditSettings", T("ActivityLog.EditSettings")); - NotifySuccess(_services.Localization.GetResource("Admin.Configuration.Updated")); + NotifySuccess(T("Admin.Configuration.Updated")); return RedirectToAction("ShoppingCart"); } @@ -901,13 +909,15 @@ public ActionResult CustomerUser() var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); StoreDependingSettings.CreateViewDataObject(storeScope); + var allCustomerRoles = _customerService.GetAllCustomerRoles(true); + var customerSettings = _services.Settings.LoadSetting(storeScope); var addressSettings = _services.Settings.LoadSetting(storeScope); var dateTimeSettings = _services.Settings.LoadSetting(storeScope); var externalAuthenticationSettings = _services.Settings.LoadSetting(storeScope); - //merge settings - var model = new CustomerUserSettingsModel(); + //merge settings + var model = new CustomerUserSettingsModel(); model.CustomerSettings = customerSettings.ToModel(); StoreDependingSettings.GetOverrideKeys(customerSettings, model.CustomerSettings, storeScope, _services.Settings, false); @@ -918,14 +928,15 @@ public ActionResult CustomerUser() model.DateTimeSettings.AllowCustomersToSetTimeZone = dateTimeSettings.AllowCustomersToSetTimeZone; model.DateTimeSettings.DefaultStoreTimeZoneId = _dateTimeHelper.DefaultStoreTimeZone.Id; + foreach (TimeZoneInfo timeZone in _dateTimeHelper.GetSystemTimeZones()) { - model.DateTimeSettings.AvailableTimeZones.Add(new SelectListItem() - { - Text = timeZone.DisplayName, - Value = timeZone.Id, - Selected = timeZone.Id.Equals(_dateTimeHelper.DefaultStoreTimeZone.Id, StringComparison.InvariantCultureIgnoreCase) - }); + model.DateTimeSettings.AvailableTimeZones.Add(new SelectListItem + { + Text = timeZone.DisplayName, + Value = timeZone.Id, + Selected = timeZone.Id.Equals(_dateTimeHelper.DefaultStoreTimeZone.Id, StringComparison.InvariantCultureIgnoreCase) + }); } StoreDependingSettings.GetOverrideKeys(dateTimeSettings, model.DateTimeSettings, storeScope, _services.Settings, false); @@ -934,10 +945,15 @@ public ActionResult CustomerUser() StoreDependingSettings.GetOverrideKeys(externalAuthenticationSettings, model.ExternalAuthenticationSettings, storeScope, _services.Settings, false); - model.CustomerSettings.AvailableCustomerNumberMethods = customerSettings.CustomerNumberMethod.ToSelectList(); - model.CustomerSettings.AvailableCustomerNumberVisibilities = customerSettings.CustomerNumberVisibility.ToSelectList(); + model.CustomerSettings.AvailableCustomerNumberMethods = customerSettings.CustomerNumberMethod.ToSelectList(false); + model.CustomerSettings.AvailableCustomerNumberVisibilities = customerSettings.CustomerNumberVisibility.ToSelectList(false); - return View(model); + model.CustomerSettings.AvailableRegisterCustomerRoles = allCustomerRoles + .Where(x => x.SystemName != SystemCustomerRoleNames.Registered && x.SystemName != SystemCustomerRoleNames.Guests) + .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) + .ToList(); + + return View(model); } [HttpPost] @@ -1013,7 +1029,10 @@ public ActionResult GeneralCommon() model.SeoSettings.DefaultTitle = seoSettings.DefaultTitle; model.SeoSettings.DefaultMetaKeywords = seoSettings.DefaultMetaKeywords; model.SeoSettings.DefaultMetaDescription = seoSettings.DefaultMetaDescription; + model.SeoSettings.MetaRobotsContent = seoSettings.MetaRobotsContent; model.SeoSettings.ConvertNonWesternChars = seoSettings.ConvertNonWesternChars; + model.SeoSettings.AllowUnicodeCharsInUrls = seoSettings.AllowUnicodeCharsInUrls; + model.SeoSettings.SeoNameCharConversion = seoSettings.SeoNameCharConversion; model.SeoSettings.CanonicalUrlsEnabled = seoSettings.CanonicalUrlsEnabled; model.SeoSettings.CanonicalHostNameRule = seoSettings.CanonicalHostNameRule; model.SeoSettings.ExtraRobotsDisallows = String.Join(Environment.NewLine, seoSettings.ExtraRobotsDisallows); @@ -1187,11 +1206,16 @@ public ActionResult GeneralCommon(GeneralCommonSettingsModel model, FormCollecti //seo settings var seoSettings = _services.Settings.LoadSetting(storeScope); + var resetUserSeoCharacterTable = (seoSettings.SeoNameCharConversion != model.SeoSettings.SeoNameCharConversion); + seoSettings.PageTitleSeparator = model.SeoSettings.PageTitleSeparator; seoSettings.PageTitleSeoAdjustment = model.SeoSettings.PageTitleSeoAdjustment; seoSettings.DefaultTitle = model.SeoSettings.DefaultTitle; seoSettings.DefaultMetaKeywords = model.SeoSettings.DefaultMetaKeywords; seoSettings.DefaultMetaDescription = model.SeoSettings.DefaultMetaDescription; + seoSettings.MetaRobotsContent = model.SeoSettings.MetaRobotsContent; + seoSettings.AllowUnicodeCharsInUrls = model.SeoSettings.AllowUnicodeCharsInUrls; + seoSettings.SeoNameCharConversion = model.SeoSettings.SeoNameCharConversion; seoSettings.ConvertNonWesternChars = model.SeoSettings.ConvertNonWesternChars; seoSettings.CanonicalUrlsEnabled = model.SeoSettings.CanonicalUrlsEnabled; seoSettings.CanonicalHostNameRule = model.SeoSettings.CanonicalHostNameRule; @@ -1199,6 +1223,11 @@ public ActionResult GeneralCommon(GeneralCommonSettingsModel model, FormCollecti StoreDependingSettings.UpdateSettings(seoSettings, form, storeScope, _services.Settings); + if (resetUserSeoCharacterTable) + { + SeoHelper.ResetUserSeoCharacterTable(); + } + //security settings var securitySettings = _services.Settings.LoadSetting(storeScope); if (securitySettings.AdminAreaAllowedIpAddresses == null) @@ -1492,11 +1521,74 @@ public ActionResult ToggleFullText(GeneralCommonSettingsModel model) return RedirectToAction("GeneralCommon"); } + [HttpPost] + public ActionResult TestSeoNameCreation(GeneralCommonSettingsModel model) + { + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var seoSettings = _services.Settings.LoadSetting(storeScope); + // we always test against persisted settings + var result = SeoHelper.GetSeName(model.SeoSettings.TestSeoNameCreation, + seoSettings.ConvertNonWesternChars, + seoSettings.AllowUnicodeCharsInUrls, + seoSettings.SeoNameCharConversion); - //all settings - public ActionResult AllSettings() + return Content(result); + } + + public ActionResult DataExchange() + { + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + return AccessDeniedView(); + + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var settings = _services.Settings.LoadSetting(storeScope); + + var model = new DataExchangeSettingsModel + { + MaxFileNameLength = settings.MaxFileNameLength, + ImageImportFolder = settings.ImageImportFolder, + ImageDownloadTimeout = settings.ImageDownloadTimeout + }; + + StoreDependingSettings.GetOverrideKeys(settings, model, storeScope, _services.Settings); + + return View(model); + } + + [HttpPost] + public ActionResult DataExchange(DataExchangeSettingsModel model, FormCollection form) + { + if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + return AccessDeniedView(); + + if (!ModelState.IsValid) + return DataExchange(); + + ModelState.Clear(); + + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var settings = _services.Settings.LoadSetting(storeScope); + + settings.MaxFileNameLength = model.MaxFileNameLength; + settings.ImageImportFolder = model.ImageImportFolder; + settings.ImageDownloadTimeout = model.ImageDownloadTimeout; + + StoreDependingSettings.UpdateSettings(settings, form, storeScope, _services.Settings); + + _services.Settings.ClearCache(); + + _customerActivityService.InsertActivity("EditSettings", T("ActivityLog.EditSettings")); + + NotifySuccess(T("Admin.Configuration.Updated")); + + return RedirectToAction("DataExchange"); + } + + + //all settings + public ActionResult AllSettings() { if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) return AccessDeniedView(); @@ -1507,44 +1599,49 @@ public ActionResult AllSettings() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult AllSettings(GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) - return AccessDeniedView(); + var model = new GridModel(); - var stores = _services.StoreService.GetAllStores(); - string allStoresString = _services.Localization.GetResource("Admin.Common.StoresAll"); - - var settings = _services.Settings - .GetAllSettings() - .Select(x => - { - var settingModel = new SettingModel() - { - Id = x.Id, - Name = x.Name, - Value = x.Value, - StoreId = x.StoreId - }; + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + { + var stores = _services.StoreService.GetAllStores(); + string allStoresString = _services.Localization.GetResource("Admin.Common.StoresAll"); - if (x.StoreId == 0) - { - settingModel.Store = allStoresString; - } - else + var settings = _services.Settings + .GetAllSettings() + .Select(x => { - var store = stores.FirstOrDefault(s => s.Id == x.StoreId); - settingModel.Store = store != null ? store.Name : "Unknown"; - } - - return settingModel; - }) - .ForCommand(command) - .ToList(); - - var model = new GridModel - { - Data = settings.PagedForCommand(command), - Total = settings.Count - }; + var settingModel = new SettingModel() + { + Id = x.Id, + Name = x.Name, + Value = x.Value, + StoreId = x.StoreId + }; + + if (x.StoreId == 0) + { + settingModel.Store = allStoresString; + } + else + { + var store = stores.FirstOrDefault(s => s.Id == x.StoreId); + settingModel.Store = store != null ? store.Name : "".NaIfEmpty(); + } + + return settingModel; + }) + .ForCommand(command) + .ToList(); + + model.Data = settings.PagedForCommand(command); + model.Total = settings.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -1555,80 +1652,76 @@ public ActionResult AllSettings(GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult SettingUpdate(SettingModel model, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) - return AccessDeniedView(); - - if (model.Name != null) - model.Name = model.Name.Trim(); - if (model.Value != null) - model.Value = model.Value.Trim(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + { + if (model.Name != null) + model.Name = model.Name.Trim(); + if (model.Value != null) + model.Value = model.Value.Trim(); - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - var setting = _services.Settings.GetSettingById(model.Id); - if (setting == null) - return Content(_services.Localization.GetResource("Admin.Configuration.Settings.NoneWithThatId")); + var setting = _services.Settings.GetSettingById(model.Id); + if (setting == null) + return Content(T("Admin.Configuration.Settings.NoneWithThatId")); - var storeId = model.Store.ToInt(); //use Store property (not StoreId) because appropriate property is stored in it + var storeId = model.Store.ToInt(); //use Store property (not StoreId) because appropriate property is stored in it - if (!setting.Name.Equals(model.Name, StringComparison.InvariantCultureIgnoreCase) || - setting.StoreId != storeId) - { - //setting name or store has been changed - _services.Settings.DeleteSetting(setting); - } + if (!setting.Name.Equals(model.Name, StringComparison.InvariantCultureIgnoreCase) || setting.StoreId != storeId) + { + //setting name or store has been changed + _services.Settings.DeleteSetting(setting); + } - _services.Settings.SetSetting(model.Name, model.Value, storeId); + _services.Settings.SetSetting(model.Name, model.Value, storeId); - //activity log - _customerActivityService.InsertActivity("EditSettings", _services.Localization.GetResource("ActivityLog.EditSettings")); + //activity log + _customerActivityService.InsertActivity("EditSettings", T("ActivityLog.EditSettings")); + } return AllSettings(command); } [GridAction(EnableCustomBinding = true)] public ActionResult SettingAdd([Bind(Exclude = "Id")] SettingModel model, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) - return AccessDeniedView(); - - if (model.Name != null) - model.Name = model.Name.Trim(); - if (model.Value != null) - model.Value = model.Value.Trim(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + { + if (model.Name != null) + model.Name = model.Name.Trim(); + if (model.Value != null) + model.Value = model.Value.Trim(); - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - var storeId = model.Store.ToInt(); //use Store property (not StoreId) because appropriate property is stored in it - _services.Settings.SetSetting(model.Name, model.Value, storeId); + var storeId = model.Store.ToInt(); //use Store property (not StoreId) because appropriate property is stored in it + _services.Settings.SetSetting(model.Name, model.Value, storeId); - //activity log - _customerActivityService.InsertActivity("AddNewSetting", _services.Localization.GetResource("ActivityLog.AddNewSetting"), model.Name); + //activity log + _customerActivityService.InsertActivity("AddNewSetting", T("ActivityLog.AddNewSetting", model.Name)); + } return AllSettings(command); } [GridAction(EnableCustomBinding = true)] public ActionResult SettingDelete(int id, GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) - return AccessDeniedView(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + { + var setting = _services.Settings.GetSettingById(id); - var setting = _services.Settings.GetSettingById(id); - if (setting == null) - throw new ArgumentException("No setting found with the specified id"); - _services.Settings.DeleteSetting(setting); + _services.Settings.DeleteSetting(setting); - //activity log - _customerActivityService.InsertActivity("DeleteSetting", _services.Localization.GetResource("ActivityLog.DeleteSetting"), setting.Name); + //activity log + _customerActivityService.InsertActivity("DeleteSetting", T("ActivityLog.DeleteSetting", setting.Name)); + } return AllSettings(command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs index a63f02506f..ed497b0175 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ShippingController.cs @@ -73,10 +73,17 @@ private void PrepareShippingMethodModel(ShippingMethodModel model, ShippingMetho { var allFilters = _shippingService.GetAllShippingMethodFilters(); - model.FilterConfigurationUrls = allFilters - .Select(x => "'" + x.GetConfigurationUrl(shippingMethod.Id) + "'") - .OrderBy(x => x) - .ToList(); + if (shippingMethod != null) + { + model.FilterConfigurationUrls = allFilters + .Select(x => "'" + x.GetConfigurationUrl(shippingMethod.Id) + "'") + .OrderBy(x => x) + .ToList(); + } + else + { + model.FilterConfigurationUrls = new List(); + } } #endregion @@ -149,19 +156,24 @@ public ActionResult Methods() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Methods(GridCommand command) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageShippingSettings)) - return AccessDeniedView(); + var model = new GridModel(); - var shippingMethodsModel = _shippingService.GetAllShippingMethods() - .Select(x => x.ToModel()) - .ForCommand(command) - .ToList(); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageShippingSettings)) + { + var shippingMethodsModel = _shippingService.GetAllShippingMethods() + .Select(x => x.ToModel()) + .ForCommand(command) + .ToList(); - var model = new GridModel - { - Data = shippingMethodsModel, - Total = shippingMethodsModel.Count - }; + model.Data = shippingMethodsModel; + model.Total = shippingMethodsModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ShoppingCartController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ShoppingCartController.cs index b6868c18ce..fccebd040d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ShoppingCartController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ShoppingCartController.cs @@ -65,28 +65,32 @@ public ActionResult CurrentCarts() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult CurrentCarts(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); - - var customers = _customerService.GetAllCustomers(null, null, null, null, null, - null, null, 0, 0, null, null, null, true, ShoppingCartType.ShoppingCart, - command.Page - 1, command.PageSize); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var customers = _customerService.GetAllCustomers(null, null, null, null, null, null, null, + 0, 0, null, null, null, true, ShoppingCartType.ShoppingCart, command.Page - 1, command.PageSize); + + gridModel.Data = customers.Select(x => + { + return new ShoppingCartModel + { + CustomerId = x.Id, + CustomerEmail = x.IsGuest() ? T("Admin.Customers.Guest").Text : x.Email, + TotalItems = x.CountProductsInCart(ShoppingCartType.ShoppingCart) + }; + }); + + gridModel.Total = customers.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var gridModel = new GridModel - { - Data = customers.Select(x => - { - return new ShoppingCartModel() - { - CustomerId = x.Id, - CustomerEmail = x.IsGuest() ? - _localizationService.GetResource("Admin.Customers.Guest") : - x.Email, - TotalItems = x.CountProductsInCart(ShoppingCartType.ShoppingCart) - }; - }), - Total = customers.TotalCount - }; return new JsonResult { Data = gridModel @@ -96,35 +100,43 @@ public ActionResult CurrentCarts(GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult GetCartDetails(int customerId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var customer = _customerService.GetCustomerById(customerId); - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var customer = _customerService.GetCustomerById(customerId); + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart); - var gridModel = new GridModel() - { - Data = cart.Select(sci => - { - decimal taxRate; + gridModel.Data = cart.Select(sci => + { + decimal taxRate; var store = _storeService.GetStoreById(sci.Item.StoreId); - var sciModel = new ShoppingCartItemModel() - { - Id = sci.Item.Id, - Store = store != null ? store.Name : "Unknown", + + var sciModel = new ShoppingCartItemModel + { + Id = sci.Item.Id, + Store = store != null ? store.Name : "".NaIfEmpty(), ProductId = sci.Item.ProductId, - Quantity = sci.Item.Quantity, + Quantity = sci.Item.Quantity, ProductName = sci.Item.Product.Name, ProductTypeName = sci.Item.Product.GetProductTypeLabel(_localizationService), ProductTypeLabelHint = sci.Item.Product.ProductTypeLabelHint, - UnitPrice = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, true), out taxRate)), - Total = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetSubTotal(sci, true), out taxRate)), - UpdatedOn = _dateTimeHelper.ConvertToUserTime(sci.Item.UpdatedOnUtc, DateTimeKind.Utc) - }; - return sciModel; - }), - Total = cart.Count - }; + UnitPrice = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, true), out taxRate)), + Total = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetSubTotal(sci, true), out taxRate)), + UpdatedOn = _dateTimeHelper.ConvertToUserTime(sci.Item.UpdatedOnUtc, DateTimeKind.Utc) + }; + return sciModel; + }); + + gridModel.Total = cart.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = gridModel @@ -144,28 +156,32 @@ public ActionResult CurrentWishlists() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult CurrentWishlists(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); - - var customers = _customerService.GetAllCustomers(null, null, null, null, null, - null, null, 0, 0, null, null, null, - true, ShoppingCartType.Wishlist, command.Page - 1, command.PageSize); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var customers = _customerService.GetAllCustomers(null, null, null, null, null, null, null, + 0, 0, null, null, null, true, ShoppingCartType.Wishlist, command.Page - 1, command.PageSize); + + gridModel.Data = customers.Select(x => + { + return new ShoppingCartModel + { + CustomerId = x.Id, + CustomerEmail = x.IsGuest() ? T("Admin.Customers.Guest").Text : x.Email, + TotalItems = x.CountProductsInCart(ShoppingCartType.Wishlist) + }; + }); + + gridModel.Total = customers.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var gridModel = new GridModel - { - Data = customers.Select(x => - { - return new ShoppingCartModel() - { - CustomerId = x.Id, - CustomerEmail = x.IsGuest() ? - _localizationService.GetResource("Admin.Customers.Guest") : - x.Email, - TotalItems = x.CountProductsInCart(ShoppingCartType.Wishlist) - }; - }), - Total = customers.TotalCount - }; return new JsonResult { Data = gridModel @@ -175,35 +191,43 @@ public ActionResult CurrentWishlists(GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult GetWishlistDetails(int customerId) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var customer = _customerService.GetCustomerById(customerId); - var cart = customer.GetCartItems(ShoppingCartType.Wishlist); + if (_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) + { + var customer = _customerService.GetCustomerById(customerId); + var cart = customer.GetCartItems(ShoppingCartType.Wishlist); - var gridModel = new GridModel() - { - Data = cart.Select(sci => - { - decimal taxRate; - var store = _storeService.GetStoreById(sci.Item.StoreId); - var sciModel = new ShoppingCartItemModel() - { - Id = sci.Item.Id, - Store = store != null ? store.Name : "Unknown", - ProductId = sci.Item.ProductId, - Quantity = sci.Item.Quantity, + gridModel.Data = cart.Select(sci => + { + decimal taxRate; + var store = _storeService.GetStoreById(sci.Item.StoreId); + + var sciModel = new ShoppingCartItemModel + { + Id = sci.Item.Id, + Store = store != null ? store.Name : "".NaIfEmpty(), + ProductId = sci.Item.ProductId, + Quantity = sci.Item.Quantity, ProductName = sci.Item.Product.Name, ProductTypeName = sci.Item.Product.GetProductTypeLabel(_localizationService), ProductTypeLabelHint = sci.Item.Product.ProductTypeLabelHint, - UnitPrice = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, true), out taxRate)), - Total = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetSubTotal(sci, true), out taxRate)), - UpdatedOn = _dateTimeHelper.ConvertToUserTime(sci.Item.UpdatedOnUtc, DateTimeKind.Utc) - }; - return sciModel; - }), - Total = cart.Count - }; + UnitPrice = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, true), out taxRate)), + Total = _priceFormatter.FormatPrice(_taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetSubTotal(sci, true), out taxRate)), + UpdatedOn = _dateTimeHelper.ConvertToUserTime(sci.Item.UpdatedOnUtc, DateTimeKind.Utc) + }; + return sciModel; + }); + + gridModel.Total = cart.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { Data = gridModel diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs index 92877bbb9b..a15ca73afc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs @@ -121,28 +121,31 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var optionsText = _localizationService.GetResource("Admin.Catalog.Attributes.SpecificationAttributes.Options"); - - var data = _specificationAttributeService.GetSpecificationAttributes() - .Expand(x => x.SpecificationAttributeOptions) - .ForCommand(command) - .Select(x => - { - var model = x.ToModel(); - model.OptionCount = x.SpecificationAttributeOptions.Count; - - return model; - }) - .ToList(); + var gridModel = new GridModel(); - var gridModel = new GridModel + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var data = _specificationAttributeService.GetSpecificationAttributes() + .Expand(x => x.SpecificationAttributeOptions) + .ForCommand(command) + .Select(x => + { + var model = x.ToModel(); + model.OptionCount = x.SpecificationAttributeOptions.Count; + + return model; + }) + .ToList(); + + gridModel.Data = data.PagedForCommand(command); + gridModel.Total = data.Count; + } + else { - Data = data.PagedForCommand(command), - Total = data.Count - }; + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -150,7 +153,6 @@ public ActionResult List(GridCommand command) }; } - //create public ActionResult Create() { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) @@ -185,7 +187,6 @@ public ActionResult Create(SpecificationAttributeModel model, bool continueEditi return View(model); } - //edit public ActionResult Edit(int id) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) @@ -233,7 +234,6 @@ public ActionResult Edit(SpecificationAttributeModel model, bool continueEditing return View(model); } - //delete [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { @@ -289,35 +289,31 @@ public ActionResult ProductMappingEdit(int specificationAttributeId, string fiel #region Specification attribute options - //list [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult OptionList(int specificationAttributeId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); + var gridModel = new GridModel(); + + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(specificationAttributeId); + + gridModel.Data = options.Select(x => x.ToModel()); + gridModel.Total = options.Count(); + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } - var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(specificationAttributeId); - var gridModel = new GridModel - { - Data = options.Select(x => - { - var model = x.ToModel(); - //locales - //AddLocales(_languageService, model.Locales, (locale, languageId) => - //{ - // locale.Name = x.GetLocalized(y => y.Name, languageId, false, false); - //}); - return model; - }), - Total = options.Count() - }; return new JsonResult { Data = gridModel }; } - //create public ActionResult OptionCreatePopup(int specificationAttributeId) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) @@ -367,7 +363,6 @@ public ActionResult OptionCreatePopup(string btnId, string formId, Specification return View(model); } - //edit public ActionResult OptionEditPopup(int id) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) @@ -414,23 +409,19 @@ public ActionResult OptionEditPopup(string btnId, string formId, SpecificationAt return View(model); } - //delete [GridAction(EnableCustomBinding = true)] public ActionResult OptionDelete(int optionId, int specificationAttributeId, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) - return AccessDeniedView(); - - var sao = _specificationAttributeService.GetSpecificationAttributeOptionById(optionId); - if (sao == null) - throw new ArgumentException("No specification attribute option found with the specified id"); + if (_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) + { + var sao = _specificationAttributeService.GetSpecificationAttributeOptionById(optionId); - _specificationAttributeService.DeleteSpecificationAttributeOption(sao); + _specificationAttributeService.DeleteSpecificationAttributeOption(sao); + } return OptionList(specificationAttributeId, command); } - //ajax [AcceptVerbs(HttpVerbs.Get)] public ActionResult GetOptionsByAttributeId(string attributeId) @@ -468,7 +459,6 @@ public ActionResult SetAttributeValue(string pk, string value, string name, Form } } - #endregion } } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/StoreController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/StoreController.cs index 78a441460e..38df324a4c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/StoreController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/StoreController.cs @@ -81,27 +81,32 @@ from m in stores [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageStores)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var storeModels = _storeService.GetAllStores() - .Select(x => - { - var model = x.ToModel(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageStores)) + { + var storeModels = _storeService.GetAllStores() + .Select(x => + { + var model = x.ToModel(); - PrepareStoreModel(model, x); + PrepareStoreModel(model, x); - model.Hosts = model.Hosts.EmptyNull().Replace(",", "
"); + model.Hosts = model.Hosts.EmptyNull().Replace(",", "
"); - return model; - }) - .ToList(); + return model; + }) + .ToList(); - var gridModel = new GridModel + gridModel.Data = storeModels; + gridModel.Total = storeModels.Count(); + } + else { - Data = storeModels, - Total = storeModels.Count() - }; + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/TaxController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/TaxController.cs index b78404407d..a694f7df48 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/TaxController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/TaxController.cs @@ -116,18 +116,24 @@ public ActionResult Categories() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult Categories(GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) - return AccessDeniedView(); + var model = new GridModel(); - var categoriesModel = _taxCategoryService.GetAllTaxCategories() - .Select(x => x.ToModel()) - .ForCommand(command) - .ToList(); - var model = new GridModel - { - Data = categoriesModel, - Total = categoriesModel.Count - }; + if (_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) + { + var categoriesModel = _taxCategoryService.GetAllTaxCategories() + .Select(x => x.ToModel()) + .ForCommand(command) + .ToList(); + + model.Data = categoriesModel; + model.Total = categoriesModel.Count; + } + else + { + model.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { @@ -138,19 +144,19 @@ public ActionResult Categories(GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult CategoryUpdate(TaxCategoryModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var taxCategory = _taxCategoryService.GetTaxCategoryById(model.Id); + taxCategory = model.ToEntity(taxCategory); - var taxCategory = _taxCategoryService.GetTaxCategoryById(model.Id); - taxCategory = model.ToEntity(taxCategory); - _taxCategoryService.UpdateTaxCategory(taxCategory); + _taxCategoryService.UpdateTaxCategory(taxCategory); + } return Categories(command); } @@ -158,19 +164,19 @@ public ActionResult CategoryUpdate(TaxCategoryModel model, GridCommand command) [GridAction(EnableCustomBinding = true)] public ActionResult CategoryAdd([Bind(Exclude = "Id")] TaxCategoryModel model, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) + { + if (!ModelState.IsValid) + { + var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); + return Content(modelStateErrors.FirstOrDefault()); + } - if (!ModelState.IsValid) - { - //display the first model error - var modelStateErrors = this.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage); - return Content(modelStateErrors.FirstOrDefault()); - } + var taxCategory = new TaxCategory(); + taxCategory = model.ToEntity(taxCategory); - var taxCategory = new TaxCategory(); - taxCategory = model.ToEntity(taxCategory); - _taxCategoryService.InsertTaxCategory(taxCategory); + _taxCategoryService.InsertTaxCategory(taxCategory); + } return Categories(command); } @@ -178,13 +184,12 @@ public ActionResult CategoryAdd([Bind(Exclude = "Id")] TaxCategoryModel model, G [GridAction(EnableCustomBinding = true)] public ActionResult CategoryDelete(int id, GridCommand command) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) - return AccessDeniedView(); + if (_permissionService.Authorize(StandardPermissionProvider.ManageTaxSettings)) + { + var taxCategory = _taxCategoryService.GetTaxCategoryById(id); - var taxCategory = _taxCategoryService.GetTaxCategoryById(id); - if (taxCategory == null) - throw new ArgumentException("No tax category found with the specified id"); - _taxCategoryService.DeleteTaxCategory(taxCategory); + _taxCategoryService.DeleteTaxCategory(taxCategory); + } return Categories(command); } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs index 0e4129951d..c47332e250 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/ThemeController.cs @@ -305,8 +305,7 @@ private string ValidateLess(ThemeManifest manifest, int storeId) storeId, manifest.ThemeName); - HttpWebRequest request = WebRequest.CreateHttp(url); - request.UserAgent = "SmartStore.NET {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); + var request = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); WebResponse response = null; try @@ -541,7 +540,7 @@ public ActionResult PreviewToolPost(string theme, int storeId, string returnUrl) _services.StoreContext.SetPreviewStore(storeId); } - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [HttpPost, ActionName("PreviewTool"), FormValueRequired("PreviewMode.Exit")] @@ -556,12 +555,7 @@ public ActionResult ExitPreview() } var returnUrl = (string)TempData["PreviewModeReturnUrl"]; - if (returnUrl.IsEmpty()) - { - returnUrl = Url.Action("Index", "Home", new { area = (string)null }); - } - - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [HttpPost, ActionName("PreviewTool"), FormValueRequired("PreviewMode.Apply")] diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs index 29f29762c6..065b2eadf0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs @@ -124,11 +124,9 @@ public ActionResult List() var model = new TopicListModel(); - // stores - model.AvailableStores.Add(new SelectListItem() { Text = _localizationService.GetResource("Admin.Common.All"), Value = "0" }); foreach (var s in _storeService.GetAllStores()) { - model.AvailableStores.Add(new SelectListItem() { Text = s.Name, Value = s.Id.ToString() }); + model.AvailableStores.Add(new SelectListItem { Text = s.Name, Value = s.Id.ToString() }); } return View(model); @@ -137,20 +135,29 @@ public ActionResult List() [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, TopicListModel model) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageTopics)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var topics = _topicService.GetAllTopics(model.SearchStoreId); - var gridModel = new GridModel - { - Data = topics.Select(x => { + if (_permissionService.Authorize(StandardPermissionProvider.ManageTopics)) + { + var topics = _topicService.GetAllTopics(model.SearchStoreId); + + gridModel.Data = topics.Select(x => + { var item = x.ToModel(); // otherwise maxJsonLength could be exceeded item.Body = ""; return item; - }), - Total = topics.Count - }; + }); + + gridModel.Total = topics.Count; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } + return new JsonResult { MaxJsonLength = int.MaxValue, diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/UrlRecordController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/UrlRecordController.cs index c088c0805d..d51b32bb19 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/UrlRecordController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/UrlRecordController.cs @@ -107,20 +107,19 @@ public ActionResult List(string entityName, int? entityId) [HttpPost, GridAction(EnableCustomBinding = true)] public ActionResult List(GridCommand command, UrlRecordListModel model) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageUrlRecords)) - return AccessDeniedView(); + var gridModel = new GridModel(); - var allLanguages = _languageService.GetAllLanguages(true); - var defaultLanguageName = T("Admin.System.SeNames.Language.Standard"); + if (_services.Permissions.Authorize(StandardPermissionProvider.ManageUrlRecords)) + { + var allLanguages = _languageService.GetAllLanguages(true); + var defaultLanguageName = T("Admin.System.SeNames.Language.Standard"); - var urlRecords = _urlRecordService.GetAllUrlRecords(command.Page - 1, command.PageSize, - model.SeName, model.EntityName, model.EntityId, model.LanguageId, model.IsActive); + var urlRecords = _urlRecordService.GetAllUrlRecords(command.Page - 1, command.PageSize, + model.SeName, model.EntityName, model.EntityId, model.LanguageId, model.IsActive); - var slugsPerEntity = _urlRecordService.CountSlugsPerEntity(urlRecords.Select(x => x.Id).Distinct().ToArray()); + var slugsPerEntity = _urlRecordService.CountSlugsPerEntity(urlRecords.Select(x => x.Id).Distinct().ToArray()); - var gridModel = new GridModel - { - Data = urlRecords.Select(x => + gridModel.Data = urlRecords.Select(x => { string languageName; @@ -141,9 +140,16 @@ public ActionResult List(GridCommand command, UrlRecordListModel model) urlRecordModel.SlugsPerEntity = (slugsPerEntity.ContainsKey(x.Id) ? slugsPerEntity[x.Id] : 0); return urlRecordModel; - }), - Total = urlRecords.TotalCount - }; + }); + + gridModel.Total = urlRecords.TotalCount; + } + else + { + gridModel.Data = Enumerable.Empty(); + + NotifyAccessDenied(); + } return new JsonResult { diff --git a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminStartupTask.cs b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminStartupTask.cs index fd624d63ce..c9f1b6ef7e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminStartupTask.cs +++ b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AdminStartupTask.cs @@ -1,7 +1,7 @@ using SmartStore.ComponentModel; using SmartStore.Core.Infrastructure; -using SmartStore.Services.DataExchange; using SmartStore.Services.DataExchange.Csv; +using SmartStore.Services.DataExchange.Import; namespace SmartStore.Admin.Infrastructure { diff --git a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs index be8da41f25..bee55cf932 100644 --- a/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs +++ b/src/Presentation/SmartStore.Web/Administration/Infrastructure/AutoMapperStartupTask.cs @@ -129,16 +129,21 @@ public void Execute() .ForMember(dest => dest.Country, mo => mo.Ignore()); //language Mapper.CreateMap() - .ForMember(dest => dest.FlagFileNames, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableFlags, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableCultures, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableTwoLetterLanguageCodes, mo => mo.Ignore()) .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) - .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()); + .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) + .ForMember(dest => dest.FlagFileNames, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.LocaleStringResources, mo => mo.Ignore()); - //email account - Mapper.CreateMap() - .ForMember(dest => dest.IsDefaultEmailAccount, mo => mo.Ignore()) - .ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore()); - Mapper.CreateMap(); + //email account + Mapper.CreateMap() + .ForMember(dest => dest.IsDefaultEmailAccount, mo => mo.Ignore()) + .ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore()) + .ForMember(dest => dest.TestEmailShortErrorMessage, mo => mo.Ignore()) + .ForMember(dest => dest.TestEmailFullErrorMessage, mo => mo.Ignore()); + Mapper.CreateMap(); //message template Mapper.CreateMap() .ForMember(dest => dest.TokensTree, mo => mo.Ignore()) @@ -197,7 +202,8 @@ public void Execute() .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()) - .ForMember(dest => dest.UpdatedOn, mo => mo.Ignore()); + .ForMember(dest => dest.UpdatedOn, mo => mo.Ignore()) + .ForMember(dest => dest.GridPageSize, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.HasDiscountsApplied, mo => mo.Ignore()) .ForMember(dest => dest.CreatedOnUtc, mo => mo.Ignore()) @@ -213,7 +219,8 @@ public void Execute() .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()) - .ForMember(dest => dest.UpdatedOn, mo => mo.Ignore()); + .ForMember(dest => dest.UpdatedOn, mo => mo.Ignore()) + .ForMember(dest => dest.GridPageSize, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.CreatedOnUtc, mo => mo.Ignore()) .ForMember(dest => dest.UpdatedOnUtc, mo => mo.Ignore()) @@ -315,8 +322,10 @@ public void Execute() Mapper.CreateMap() .ForMember(dest => dest.ActivityLogTypeName, mo => mo.MapFrom(src => src.ActivityLogType.Name)) .ForMember(dest => dest.CustomerEmail, mo => mo.MapFrom(src => src.Customer.Email)) - .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()); - //currencies + .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()) + .ForMember(dest => dest.IsSystemAccount, mo => mo.Ignore()) + .ForMember(dest => dest.SystemAccountName, mo => mo.Ignore()); + //currencies Mapper.CreateMap() .ForMember(dest => dest.CreatedOn, mo => mo.Ignore()) .ForMember(dest => dest.IsPrimaryExchangeRateCurrency, mo => mo.Ignore()) @@ -346,8 +355,7 @@ public void Execute() Mapper.CreateMap() .ForMember(dest => dest.Id, mo => mo.Ignore()) .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()) - .ForMember(dest => dest.SearchStoreId, mo => mo.Ignore()) - .ForMember(dest => dest.StoreCount, mo => mo.Ignore()); + .ForMember(dest => dest.SearchStoreId, mo => mo.Ignore()); Mapper.CreateMap(); Mapper.CreateMap() @@ -512,12 +520,14 @@ public void Execute() Mapper.CreateMap() .ForMember(dest => dest.SpecificationAttribute, mo => mo.Ignore()) .ForMember(dest => dest.ProductSpecificationAttributes, mo => mo.Ignore()); - //checkout attributes - Mapper.CreateMap() - .ForMember(dest => dest.AvailableTaxCategories, mo => mo.Ignore()) - .ForMember(dest => dest.AttributeControlTypeName, mo => mo.Ignore()) - .ForMember(dest => dest.Locales, mo => mo.Ignore()); - Mapper.CreateMap() + //checkout attributes + Mapper.CreateMap() + .ForMember(dest => dest.AvailableTaxCategories, mo => mo.Ignore()) + .ForMember(dest => dest.AttributeControlTypeName, mo => mo.Ignore()) + .ForMember(dest => dest.Locales, mo => mo.Ignore()) + .ForMember(dest => dest.SelectedStoreIds, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableStores, mo => mo.Ignore()); + Mapper.CreateMap() .ForMember(dest => dest.AttributeControlType, mo => mo.Ignore()) .ForMember(dest => dest.CheckoutAttributeValues, mo => mo.Ignore()); Mapper.CreateMap() @@ -639,13 +649,18 @@ public void Execute() .ForMember(dest => dest.GiftCards_Activated_OrderStatuses, mo => mo.Ignore()) .ForMember(dest => dest.GiftCards_Deactivated_OrderStatuses, mo => mo.Ignore()) .ForMember(dest => dest.PrimaryStoreCurrencyCode, mo => mo.Ignore()) + .ForMember(dest => dest.StoreCount, mo => mo.Ignore()) .ForMember(dest => dest.OrderIdent, mo => mo.Ignore()) .ForMember(dest => dest.Locales, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.MinimumOrderPlacementInterval, mo => mo.Ignore()) .ForMember(dest => dest.Id, mo => mo.Ignore()); - Mapper.CreateMap(); + Mapper.CreateMap() + .ForMember(dest => dest.AvailableNewsLetterSubscriptions, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableThirdPartyEmailHandOver, mo => mo.Ignore()) + .ForMember(dest => dest.Locales, mo => mo.Ignore()); Mapper.CreateMap() + .ForMember(dest => dest.Id, mo => mo.Ignore()) .ForMember(dest => dest.MoveItemsFromWishlistToCart, mo => mo.Ignore()) .ForMember(dest => dest.ShowItemsFromWishlistToCartButton, mo => mo.Ignore()); Mapper.CreateMap() @@ -655,11 +670,12 @@ public void Execute() //.ForMember(dest => dest.DefaultPictureZoomEnabled, mo => mo.Ignore()) .ForMember(dest => dest.DefaultImageQuality, mo => mo.Ignore()) .ForMember(dest => dest.MultipleThumbDirectories, mo => mo.Ignore()) - .ForMember(dest => dest.AutoCompleteSearchThumbPictureSize, mo => mo.Ignore()); + .ForMember(dest => dest.VariantValueThumbPictureSize, mo => mo.Ignore()); Mapper.CreateMap() .ForMember(dest => dest.AvailableCustomerNumberMethods, mo => mo.Ignore()) - .ForMember(dest => dest.AvailableCustomerNumberVisibilities, mo => mo.Ignore()); - Mapper.CreateMap() + .ForMember(dest => dest.AvailableCustomerNumberVisibilities, mo => mo.Ignore()) + .ForMember(dest => dest.AvailableRegisterCustomerRoles, mo => mo.Ignore()); + Mapper.CreateMap() .ForMember(dest => dest.HashedPasswordFormat, mo => mo.Ignore()) .ForMember(dest => dest.PasswordMinLength, mo => mo.Ignore()) .ForMember(dest => dest.AvatarMaximumSizeBytes, mo => mo.Ignore()) diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/BulkEditListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/BulkEditListModel.cs index dde1e7a45c..b1e21d83b2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/BulkEditListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/BulkEditListModel.cs @@ -33,7 +33,6 @@ public BulkEditListModel() public IList AvailableProductTypes { get; set; } public int GridPageSize { get; set; } - public int StoreCount { get; set; } public IList AvailableCategories { get; set; } public IList AvailableManufacturers { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs index 699ecb32ff..49a10c1d46 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs @@ -21,14 +21,17 @@ public CategoryModel() { if (PageSize < 1) { - PageSize = 5; + PageSize = 12; } + Locales = new List(); AvailableCategoryTemplates = new List(); AvailableDefaultViewModes = new List(); } - [SmartResourceDisplayName("Admin.Catalog.Categories.Fields.Name")] + public int GridPageSize { get; set; } + + [SmartResourceDisplayName("Admin.Catalog.Categories.Fields.Name")] [AllowHtml] public string Name { get; set; } @@ -164,35 +167,6 @@ public class CategoryProductModel : EntityModelBase public int DisplayOrder1 { get; set; } } - public class AddCategoryProductModel : ModelBase - { - public AddCategoryProductModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableProductTypes = new List(); - } - public GridModel Products { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableProductTypes { get; set; } - - public int CategoryId { get; set; } - - public int[] SelectedProductIds { get; set; } - } - #endregion } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerListModel.cs index bdfc988570..81163d6df0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerListModel.cs @@ -1,4 +1,5 @@ -using System.Web.Mvc; +using System.Collections.Generic; +using System.Web.Mvc; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -10,6 +11,10 @@ public class ManufacturerListModel : ModelBase [AllowHtml] public string SearchManufacturerName { get; set; } - public int GridPageSize { get; set; } + [SmartResourceDisplayName("Admin.Common.Store.SearchFor")] + public int SearchStoreId { get; set; } + public IList AvailableStores { get; set; } + + public int GridPageSize { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerModel.cs index d44e43f9fa..6ec6dac6ca 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ManufacturerModel.cs @@ -8,11 +8,10 @@ using SmartStore.Web.Framework; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Modelling; -using Telerik.Web.Mvc; namespace SmartStore.Admin.Models.Catalog { - [Validator(typeof(ManufacturerValidator))] + [Validator(typeof(ManufacturerValidator))] public class ManufacturerModel : EntityModelBase, ILocalizedModel { public ManufacturerModel() @@ -21,11 +20,14 @@ public ManufacturerModel() { PageSize = 5; } + Locales = new List(); AvailableManufacturerTemplates = new List(); } - [SmartResourceDisplayName("Admin.Catalog.Manufacturers.Fields.Name")] + public int GridPageSize { get; set; } + + [SmartResourceDisplayName("Admin.Catalog.Manufacturers.Fields.Name")] [AllowHtml] public string Name { get; set; } @@ -127,35 +129,6 @@ public class ManufacturerProductModel : EntityModelBase public int DisplayOrder1 { get; set; } } - public class AddManufacturerProductModel : ModelBase - { - public AddManufacturerProductModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableProductTypes = new List(); - } - public GridModel Products { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableProductTypes { get; set; } - - public int ManufacturerId { get; set; } - - public int[] SelectedProductIds { get; set; } - } - #endregion } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductListModel.cs index 7259adfc8e..a93008ad9c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductListModel.cs @@ -52,7 +52,6 @@ public ProductListModel() public bool DisplayProductPictures { get; set; } public int GridPageSize { get; set; } - public int StoreCount { get; set; } public IList AvailableCategories { get; set; } public IList AvailableManufacturers { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs index 461eda3244..d24020eaea 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/ProductModel.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; -using AutoMapper; using FluentValidation.Attributes; using SmartStore.Admin.Models.Customers; using SmartStore.Admin.Models.Stores; @@ -11,11 +10,10 @@ using SmartStore.Web.Framework; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Modelling; -using Telerik.Web.Mvc; namespace SmartStore.Admin.Models.Catalog { - [Validator(typeof(ProductValidator))] + [Validator(typeof(ProductValidator))] public class ProductModel : TabbableModel, ILocalizedModel { public ProductModel() @@ -142,7 +140,7 @@ public ProductModel() [SmartResourceDisplayName("Admin.Catalog.Products.Fields.Download")] [UIHint("Download")] - public int DownloadId { get; set; } + public int? DownloadId { get; set; } [SmartResourceDisplayName("Admin.Catalog.Products.Fields.UnlimitedDownloads")] public bool UnlimitedDownloads { get; set; } @@ -161,7 +159,7 @@ public ProductModel() [SmartResourceDisplayName("Admin.Catalog.Products.Fields.SampleDownload")] [UIHint("Download")] - public int SampleDownloadId { get; set; } + public int? SampleDownloadId { get; set; } [SmartResourceDisplayName("Admin.Catalog.Products.Fields.HasUserAgreement")] public bool HasUserAgreement { get; set; } @@ -487,43 +485,6 @@ public class RelatedProductModel : EntityModelBase public bool Product2Published { get; set; } } - public class AddRelatedProductModel : ModelBase - { - public AddRelatedProductModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableStores = new List(); - AvailableProductTypes = new List(); - } - public GridModel Products { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchStore")] - public int SearchStoreId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableStores { get; set; } - public IList AvailableProductTypes { get; set; } - - public int ProductId { get; set; } - - public int[] SelectedProductIds { get; set; } - } - public partial class AssociatedProductModel : EntityModelBase { [SmartResourceDisplayName("Admin.Catalog.Products.AssociatedProducts.Fields.Product")] @@ -542,37 +503,6 @@ public partial class AssociatedProductModel : EntityModelBase [SmartResourceDisplayName("Admin.Catalog.Products.Fields.Published")] public bool Published { get; set; } } - public partial class AddAssociatedProductModel : ModelBase - { - public AddAssociatedProductModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableStores = new List(); - AvailableProductTypes = new List(); - } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchStore")] - public int SearchStoreId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableStores { get; set; } - public IList AvailableProductTypes { get; set; } - - public int ProductId { get; set; } - - public int[] SelectedProductIds { get; set; } - } public partial class BundleItemModel : EntityModelBase { @@ -603,39 +533,6 @@ public partial class BundleItemModel : EntityModelBase [SmartResourceDisplayName("Admin.Catalog.Products.BundleItems.Fields.Published")] public bool Published { get; set; } } - public partial class AddBundleItemModel : ModelBase - { - public AddBundleItemModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableStores = new List(); - AvailableProductTypes = new List(); - } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchStore")] - public int SearchStoreId { get; set; } - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableStores { get; set; } - public IList AvailableProductTypes { get; set; } - - public int ProductId { get; set; } - public bool IsPerItemPricing { get; set; } - public bool IsPerItemShipping { get; set; } - - public int[] SelectedProductIds { get; set; } - } public class CrossSellProductModel : EntityModelBase { @@ -654,42 +551,6 @@ public class CrossSellProductModel : EntityModelBase [SmartResourceDisplayName("Admin.Catalog.Products.Fields.Published")] public bool Product2Published { get; set; } } - public class AddCrossSellProductModel : ModelBase - { - public AddCrossSellProductModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableStores = new List(); - AvailableProductTypes = new List(); - } - public GridModel Products { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchStore")] - public int SearchStoreId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableStores { get; set; } - public IList AvailableProductTypes { get; set; } - - public int ProductId { get; set; } - - public int[] SelectedProductIds { get; set; } - } public class TierPriceModel : EntityModelBase { @@ -767,6 +628,7 @@ public ProductVariantAttributeValueModel() Locales = new List(); } + public int ProductId { get; set; } public int ProductVariantAttributeId { get; set; } [SmartResourceDisplayName("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.Alias")] @@ -815,41 +677,6 @@ public ProductVariantAttributeValueModel() public string QuantityInfo { get; set; } public IList Locales { get; set; } - - public class AddProductLinkageModel : ModelBase - { - public AddProductLinkageModel() - { - AvailableCategories = new List(); - AvailableManufacturers = new List(); - AvailableStores = new List(); - AvailableProductTypes = new List(); - } - public GridModel Products { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] - [AllowHtml] - public string SearchProductName { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] - public int SearchCategoryId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] - public int SearchManufacturerId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchStore")] - public int SearchStoreId { get; set; } - - [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] - public int SearchProductTypeId { get; set; } - - public IList AvailableCategories { get; set; } - public IList AvailableManufacturers { get; set; } - public IList AvailableStores { get; set; } - public IList AvailableProductTypes { get; set; } - - public int ProductId { get; set; } - } } public class ProductVariantAttributeValueLocalizedModel : ILocalizedModelLocal diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/CsvConfigurationModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/CsvConfigurationModel.cs new file mode 100644 index 0000000000..63aa1aaf29 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/CsvConfigurationModel.cs @@ -0,0 +1,64 @@ +using System; +using System.Text.RegularExpressions; +using FluentValidation.Attributes; +using SmartStore.Admin.Validators.DataExchange; +using SmartStore.Services.DataExchange.Csv; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.DataExchange +{ + [Validator(typeof(CsvConfigurationValidator))] + public class CsvConfigurationModel : ModelBase, ICloneable + { + public CsvConfigurationModel() + { + } + + public CsvConfigurationModel(CsvConfiguration config) + { + QuoteAllFields = config.QuoteAllFields; + TrimValues = config.TrimValues; + SupportsMultiline = config.SupportsMultiline; + Delimiter = Regex.Escape(config.Delimiter.ToString()); + Quote = Regex.Escape(config.Quote.ToString()); + Escape = Regex.Escape(config.Escape.ToString()); + } + + [SmartResourceDisplayName("Admin.DataExchange.Csv.QuoteAllFields")] + public bool QuoteAllFields { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Csv.TrimValues")] + public bool TrimValues { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Csv.SupportsMultiline")] + public bool SupportsMultiline { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Csv.Delimiter")] + public string Delimiter { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Csv.Quote")] + public string Quote { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Csv.Escape")] + public string Escape { get; set; } + + object ICloneable.Clone() + { + return this.Clone(); + } + + public CsvConfiguration Clone() + { + var config = new CsvConfiguration(); + config.QuoteAllFields = QuoteAllFields; + config.TrimValues = TrimValues; + config.SupportsMultiline = SupportsMultiline; + config.Delimiter = Delimiter.ToChar(true); + config.Quote = Quote.ToChar(true); + config.Escape = Escape.ToChar(true); + + return config; + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs index d60f2b8f5f..f5cb8f82c6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportDeploymentModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; @@ -13,6 +14,8 @@ namespace SmartStore.Admin.Models.DataExchange public class ExportDeploymentModel : EntityModelBase { public int ProfileId { get; set; } + public bool CreateZip { get; set; } + public string PublicFolderUrl { get; set; } [SmartResourceDisplayName("Common.Image")] public string ThumbnailUrl { get; set; } @@ -30,32 +33,6 @@ public class ExportDeploymentModel : EntityModelBase [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.DeploymentType")] public string DeploymentTypeName { get; set; } - public string DeploymentTypeIconClass - { - get - { - switch (DeploymentType) - { - 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"; - default: - return "fa-question"; - } - } - } - - [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.CreateZip")] - public bool CreateZip { get; set; } - - [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.IsPublic")] - public bool IsPublic { get; set; } - [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.Username")] public string Username { get; set; } @@ -73,6 +50,9 @@ public string DeploymentTypeIconClass [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.FileSystemPath")] public string FileSystemPath { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.SubFolder")] + public string SubFolder { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.EmailAddresses")] public string EmailAddresses { get; set; } public string SerializedEmailAddresses { get; set; } @@ -90,6 +70,20 @@ public string DeploymentTypeIconClass [SmartResourceDisplayName("Admin.DataExchange.Export.Deployment.UseSsl")] public bool UseSsl { get; set; } - public ExportProfileDetailsModel ProfileDetails { get; set; } + public LastResultInfo LastResult { get; set; } + + public int FileCount { get; set; } + + public class LastResultInfo + { + public DateTime Execution { get; set; } + public string ExecutionPretty { get; set; } + public string Error { get; set; } + + public bool Succeeded + { + get { return Error.IsEmpty(); } + } + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportFilterModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportFilterModel.cs index 6e8f8910f3..224f6671c6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportFilterModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportFilterModel.cs @@ -20,6 +20,12 @@ public class ExportFilterModel [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.CreatedTo")] public DateTime? CreatedTo { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.CustomerRoleIds")] + public int[] CustomerRoleIds { get; set; } + public List AvailableCustomerRoles { get; set; } + + public List AvailableCountries { get; set; } + #endregion #region Product @@ -72,6 +78,34 @@ public class ExportFilterModel #endregion + #region Customer + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.IsActiveCustomer")] + public bool? IsActiveCustomer { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.IsTaxExempt")] + public bool? IsTaxExempt { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.BillingCountryIds")] + public int[] BillingCountryIds { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.ShippingCountryIds")] + public int[] ShippingCountryIds { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.LastActivityFrom")] + public DateTime? LastActivityFrom { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.LastActivityTo")] + public DateTime? LastActivityTo { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.HasSpentAtLeastAmount")] + public decimal? HasSpentAtLeastAmount { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.HasPlacedAtLeastOrders")] + public int? HasPlacedAtLeastOrders { get; set; } + + #endregion + #region Order [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.OrderStatusIds")] @@ -86,9 +120,12 @@ public class ExportFilterModel public int[] ShippingStatusIds { get; set; } public List AvailableShippingStates { get; set; } - [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.CustomerRoleIds")] - public int[] CustomerRoleIds { get; set; } - public List AvailableCustomerRoles { get; set; } + #endregion + + #region Newsletter Subscription + + [SmartResourceDisplayName("Admin.DataExchange.Export.Filter.IsActiveSubscriber")] + public bool? IsActiveSubscriber { get; set; } #endregion } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs index 9c01bef987..5c2e77b6ef 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProfileModel.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Web; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Admin.Models.Tasks; @@ -49,18 +51,14 @@ public partial class ExportProfileModel : EntityModelBase public bool IsTaskRunning { get; set; } public bool IsTaskEnabled { get; set; } - [SmartResourceDisplayName("Admin.DataExchange.Export.LastExecution")] - [AllowHtml] - public string LastExecution { get; set; } - - [SmartResourceDisplayName("Admin.DataExchange.Export.Offset")] + [SmartResourceDisplayName("Admin.Common.RecordsSkip")] public int Offset { get; set; } - [SmartResourceDisplayName("Admin.DataExchange.Export.Limit")] - public int Limit { get; set; } + [SmartResourceDisplayName("Admin.Common.RecordsTake")] + public int? Limit { get; set; } [SmartResourceDisplayName("Admin.DataExchange.Export.BatchSize")] - public int BatchSize { get; set; } + public int? BatchSize { get; set; } [SmartResourceDisplayName("Admin.DataExchange.Export.PerStore")] public bool PerStore { get; set; } @@ -93,7 +91,7 @@ public partial class ExportProfileModel : EntityModelBase public ScheduleTaskModel TaskModel { get; set; } - public ExportProfileDetailsModel Details { get; set; } + public int FileCount { get; set; } public class ProviderModel { @@ -109,9 +107,6 @@ public class ProviderModel [SmartResourceDisplayName("Common.Website")] public string Url { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Plugins.Fields.Configure")] - public string ConfigurationUrl { get; set; } - [SmartResourceDisplayName("Common.Provider")] public string FriendlyName { get; set; } @@ -154,30 +149,54 @@ public class ProviderSelectItem } - public partial class ExportProfileDetailsModel : EntityModelBase + public partial class ExportFileDetailsModel : EntityModelBase { - [SmartResourceDisplayName("Admin.DataExchange.Export.ExportFiles")] - public List ExportFiles { get; set; } - - public string ZipPath { get; set; } - - [SmartResourceDisplayName("Admin.DataExchange.Export.ExportFiles")] - public int ExportFileCount + public int FileCount { get { - return (ExportFiles.Count + (ZipPath.HasValue() ? 1 : 0)); + var result = ExportFiles.Count; + + if (result == 0) + result = PublicFiles.Count; + + return result; } } - public List PublicFiles { get; set; } + public List ExportFiles { get; set; } + public List PublicFiles { get; set; } + + public bool IsForDeployment { get; set; } - public class PublicFile + public class FileInfo { public int StoreId { get; set; } public string StoreName { get; set; } - public string FileName { get; set; } + + public int DisplayOrder { get; set; } + + public string FilePath { get; set; } public string FileUrl { get; set; } + + public string FileName { get; set; } + public string FileExtension { get; set; } + + public string FileRootPath + { + get + { + var rootPath = ""; + var appPath = HttpRuntime.AppDomainAppPath; + + if (FilePath.StartsWith(appPath)) + rootPath = FilePath.Replace(appPath, "~/"); + + rootPath = rootPath.Replace('\\', '/'); + + return rootPath; + } + } } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProjectionModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProjectionModel.cs index a2df29a5b4..03fb387c53 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProjectionModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ExportProjectionModel.cs @@ -1,12 +1,18 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Web.Framework; namespace SmartStore.Admin.Models.DataExchange { public class ExportProjectionModel { + public ExportProjectionModel() + { + DescriptionMergingId = (int)ExportDescriptionMerging.Description; + } + #region All entity types [SmartResourceDisplayName("Admin.DataExchange.Export.Projection.StoreId")] @@ -49,7 +55,7 @@ public class ExportProjectionModel [SmartResourceDisplayName("Admin.DataExchange.Export.Projection.PriceType")] public PriceDisplayType? PriceType { get; set; } - public SelectList AvailablePriceTypes { get; set; } + public IList AvailablePriceTypes { get; set; } [SmartResourceDisplayName("Admin.DataExchange.Export.Projection.ConvertNetToGrossPrices")] public bool ConvertNetToGrossPrices { get; set; } @@ -57,6 +63,9 @@ public class ExportProjectionModel [SmartResourceDisplayName("Admin.DataExchange.Export.Projection.Brand")] public string Brand { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Projection.NumberOfPictures")] + public int? NumberOfPictures { get; set; } + [SmartResourceDisplayName("Admin.DataExchange.Export.Projection.PictureSize")] public int PictureSize { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileListModel.cs new file mode 100644 index 0000000000..af9f7af39e --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileListModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.DataExchange +{ + public partial class ImportProfileListModel : EntityModelBase + { + [SmartResourceDisplayName("Admin.Common.Entity")] + public ImportEntityType EntityType { get; set; } + public List AvailableEntityTypes { get; set; } + + public List Profiles { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs new file mode 100644 index 0000000000..5baf801477 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/DataExchange/ImportProfileModel.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using FluentValidation.Attributes; +using SmartStore.Admin.Models.Tasks; +using SmartStore.Admin.Validators.DataExchange; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.DataExchange +{ + [Validator(typeof(ImportProfileValidator))] + public partial class ImportProfileModel : EntityModelBase + { + public ImportProfileModel() + { + ExtraData = new ExtraDataModel(); + } + + [SmartResourceDisplayName("Admin.DataExchange.Import.Name")] + public string Name { get; set; } + + [SmartResourceDisplayName("Admin.Common.ImportFiles")] + public List ExistingFileNames { get; set; } + + [SmartResourceDisplayName("Admin.Common.Entity")] + public ImportEntityType EntityType { get; set; } + + [SmartResourceDisplayName("Admin.Common.Entity")] + public string EntityTypeName { get; set; } + + [SmartResourceDisplayName("Common.Enabled")] + public bool Enabled { get; set; } + + [SmartResourceDisplayName("Admin.Common.RecordsSkip")] + public int? Skip { get; set; } + + [SmartResourceDisplayName("Admin.Common.RecordsTake")] + public int? Take { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Import.UpdateOnly")] + public bool UpdateOnly { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Import.KeyFieldNames")] + public string[] KeyFieldNames { get; set; } + public List AvailableKeyFieldNames { get; set; } + + [SmartResourceDisplayName("Common.Execution")] + public int ScheduleTaskId { get; set; } + public string ScheduleTaskName { get; set; } + public bool IsTaskRunning { get; set; } + public bool IsTaskEnabled { get; set; } + + public ScheduleTaskModel TaskModel { get; set; } + + [SmartResourceDisplayName("Admin.DataExchange.Import.LastImportResult")] + public SerializableImportResult ImportResult { get; set; } + + public bool LogFileExists { get; set; } + public string TempFileName { get; set; } + + public CsvConfigurationModel CsvConfiguration { get; set; } + + public ExtraDataModel ExtraData { get; set; } + + public List ColumnMappings { get; set; } + public List AvailableSourceColumns { get; set; } + public List AvailableEntityProperties { get; set; } + + public int MaxMappingLabelTextLength { get { return 42; } } + + public class ExtraDataModel + { + [SmartResourceDisplayName("Admin.DataExchange.Import.NumberOfPictures")] + public int? NumberOfPictures { get; set; } + } + } + + + public class ColumnMappingItemModel + { + public int Index { get; set; } + + public string Column { get; set; } + public string ColumnWithoutIndex { get; set; } + public string ColumnIndex { get; set; } + + public string Property { get; set; } + public string PropertyDescription { get; set; } + + public string Default { get; set; } + public bool IsDefaultDisabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Directory/CountryListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Directory/CountryListModel.cs new file mode 100644 index 0000000000..ece27d8805 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Directory/CountryListModel.cs @@ -0,0 +1,9 @@ +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.Directory +{ + public class CountryListModel : ModelBase + { + public int StoreCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Localization/LanguageModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Localization/LanguageModel.cs index b720c69d5f..28d3dfaf30 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Localization/LanguageModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Localization/LanguageModel.cs @@ -8,7 +8,7 @@ namespace SmartStore.Admin.Models.Localization { - [Validator(typeof(LanguageValidator))] + [Validator(typeof(LanguageValidator))] public class LanguageModel : EntityModelBase { public LanguageModel() @@ -23,18 +23,21 @@ public LanguageModel() [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.LanguageCulture")] [AllowHtml] public string LanguageCulture { get; set; } + public List AvailableCultures { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.UniqueSeoCode")] + [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.UniqueSeoCode")] [AllowHtml] public string UniqueSeoCode { get; set; } + public List AvailableTwoLetterLanguageCodes { get; set; } - //flags - [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.FlagImageFileName")] + //flags + [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.FlagImageFileName")] [AllowHtml] public string FlagImageFileName { get; set; } public IList FlagFileNames { get; set; } + public List AvailableFlags { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.Rtl")] + [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.Rtl")] public bool Rtl { get; set; } [SmartResourceDisplayName("Admin.Configuration.Languages.Fields.Published")] diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogModel.cs index f686e115bb..7b5b9a57ac 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogModel.cs @@ -20,5 +20,9 @@ public partial class ActivityLogModel : EntityModelBase [SmartResourceDisplayName("Admin.Configuration.ActivityLog.ActivityLog.Fields.CreatedOn")] public DateTime CreatedOn { get; set; } + + [SmartResourceDisplayName("Admin.Customers.Customers.Fields.IsSystemAccount")] + public bool IsSystemAccount { get; set; } + public string SystemAccountName { get; set; } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogSearchModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogSearchModel.cs index 210b7d644a..9e17c5bbcf 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogSearchModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Logging/ActivityLogSearchModel.cs @@ -10,6 +10,10 @@ public class ActivityLogSearchModel : ModelBase { public int GridPageSize { get; set; } + [SmartResourceDisplayName("Admin.Configuration.ActivityLog.ActivityLog.Fields.ActivityLogType")] + public int ActivityLogTypeId { get; set; } + public IList ActivityLogType { get; set; } + [SmartResourceDisplayName("Admin.Configuration.ActivityLog.ActivityLog.Fields.CreatedOnFrom")] public DateTime? CreatedOnFrom { get; set; } @@ -20,8 +24,7 @@ public class ActivityLogSearchModel : ModelBase [AllowHtml] public string CustomerEmail { get; set; } - [SmartResourceDisplayName("Admin.Configuration.ActivityLog.ActivityLog.Fields.ActivityLogType")] - public int ActivityLogTypeId { get; set; } - public IList ActivityLogType { get; set; } - } + [SmartResourceDisplayName("Admin.Configuration.ActivityLog.ActivityLog.Fields.CustomerSystemAccount")] + public bool? CustomerSystemAccount { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs index 0604e18a0c..8a3fa54a2d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Messages/EmailAccountModel.cs @@ -48,5 +48,7 @@ public class EmailAccountModel : EntityModelBase [AllowHtml] public string SendTestEmailTo { get; set; } - } + public string TestEmailShortErrorMessage { get; set; } + public string TestEmailFullErrorMessage { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeListModel.cs new file mode 100644 index 0000000000..6084bd6210 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeListModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Admin.Models.Orders +{ + public class CheckoutAttributeListModel : ModelBase + { + public int GridPageSize { get; set; } + + public IList AvailableStores { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs index 5e26016af9..6f6a404473 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/CheckoutAttributeModel.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using FluentValidation.Attributes; +using SmartStore.Admin.Models.Stores; using SmartStore.Admin.Validators.Orders; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Localization; @@ -51,9 +52,16 @@ public CheckoutAttributeModel() public int DisplayOrder { get; set; } public IList Locales { get; set; } - } - public class CheckoutAttributeLocalizedModel : ILocalizedModelLocal + [SmartResourceDisplayName("Admin.Common.Store.LimitedTo")] + public bool LimitedToStores { get; set; } + + [SmartResourceDisplayName("Admin.Common.Store.AvailableFor")] + public List AvailableStores { get; set; } + public int[] SelectedStoreIds { get; set; } + } + + public class CheckoutAttributeLocalizedModel : ILocalizedModelLocal { public int LanguageId { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/GiftCardListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/GiftCardListModel.cs index 3e0ed7c992..3ae20d0982 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/GiftCardListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/GiftCardListModel.cs @@ -12,6 +12,8 @@ public GiftCardListModel() ActivatedList = new List(); } + public int GridPageSize { get; set; } + [SmartResourceDisplayName("Admin.GiftCards.List.CouponCode")] [AllowHtml] public string CouponCode { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs index 2d64f5216c..ce42e552ae 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderListModel.cs @@ -54,9 +54,9 @@ public OrderListModel() [AllowHtml] public string GoDirectlyToNumber { get; set; } - + public int GridPageSize { get; set; } - public IList AvailableOrderStatuses { get; set; } + public IList AvailableOrderStatuses { get; set; } public IList AvailablePaymentStatuses { get; set; } public IList AvailableShippingStatuses { get; set; } public IList AvailableStores { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs index 17c2b7be51..d40ee84e5e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Orders/OrderModel.cs @@ -37,10 +37,13 @@ public OrderModel() //customer info [SmartResourceDisplayName("Admin.Orders.Fields.Customer")] public int CustomerId { get; set; } + + [SmartResourceDisplayName("Admin.Orders.List.CustomerName")] public string CustomerName { get; set; } [SmartResourceDisplayName("Admin.Orders.Fields.CustomerEmail")] public string CustomerEmail { get; set; } + [SmartResourceDisplayName("Admin.Orders.Fields.CustomerIP")] public string CustomerIp { get; set; } @@ -226,7 +229,10 @@ public OrderModel() [SmartResourceDisplayName("Common.UpdatedOn")] public DateTime UpdatedOn { get; set; } - public string CustomerComment { get; set; } + [SmartResourceDisplayName("Admin.Orders.Fields.AcceptThirdPartyEmailHandOver")] + public bool AcceptThirdPartyEmailHandOver { get; set; } + + public string CustomerComment { get; set; } //checkout attributes public string CheckoutAttributeInfo { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Security/PermissionRecordModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Security/PermissionRecordModel.cs index 10840b978c..b444eaa04b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Security/PermissionRecordModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Security/PermissionRecordModel.cs @@ -6,5 +6,25 @@ public class PermissionRecordModel : ModelBase { public string Name { get; set; } public string SystemName { get; set; } - } + public string Category { get; set; } + + public string CategoryLabel + { + get + { + switch (Category) + { + case "PublicStore": + case "Standard": + return "label-success"; + + case "Plugin": + return ""; + + default: + return "label-info"; + } + } + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs index 8fd5aa14b1..168e4b0a95 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CatalogSettingsModel.cs @@ -72,14 +72,23 @@ public CatalogSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowManufacturerPictures")] public bool ShowManufacturerPictures { get; set; } - #endregion + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.HideManufacturerDefaultPictures")] + public bool HideManufacturerDefaultPictures { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.HideCategoryDefaultPictures")] + public bool HideCategoryDefaultPictures { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.HideProductDefaultPictures")] + public bool HideProductDefaultPictures { get; set; } + + #endregion - #region Product lists + #region Product lists - #region Navigation + #region Navigation - //filter - [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowProductsFromSubcategories")] + //filter + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowProductsFromSubcategories")] public bool ShowProductsFromSubcategories { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.IncludeFeaturedProductsInNormalLists")] @@ -103,6 +112,9 @@ public CatalogSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ExpandAllFilterCriteria")] public bool ExpandAllFilterCriteria { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.SortFilterResultsByMatches")] + public bool SortFilterResultsByMatches { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.SubCategoryDisplayType")] public SubCategoryDisplayType SubCategoryDisplayType { get; set; } public SelectList AvailableSubCategoryDisplayTypes { get; set; } @@ -225,7 +237,10 @@ public CatalogSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.DisplayAllImagesNumber")] public int DisplayAllImagesNumber { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowDeliveryTimesInProductDetail")] + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowManufacturerPicturesInProductDetail")] + public bool ShowManufacturerPicturesInProductDetail { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.ShowDeliveryTimesInProductDetail")] public bool ShowDeliveryTimesInProductDetail { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.DeliveryTimeIdForEmptyStock")] @@ -272,7 +287,10 @@ public CatalogSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.SuppressSkuSearch")] public bool SuppressSkuSearch { get; set; } - #endregion + [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.SearchDescriptions")] + public bool SearchDescriptions { get; set; } + + #endregion - } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/ContentSliderSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/ContentSliderSettingsModel.cs index f6e8fda39f..e0d904bf4e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/ContentSliderSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/ContentSliderSettingsModel.cs @@ -45,8 +45,6 @@ public ContentSliderSettingsModel() [SmartResourceDisplayName("Admin.Common.Store.SearchFor")] public int SearchStoreId { get; set; } - - public int StoreCount { get; set; } } [Validator(typeof(ContentSliderSlideValidator))] diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs index 1ad5872150..f395584d6c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/CustomerUserSettingsModel.cs @@ -26,11 +26,12 @@ public partial class CustomerSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.UsernamesEnabled")] public bool UsernamesEnabled { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.DefaultSortOrderMode")] + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.CustomerNumberMethod")] public CustomerNumberMethod CustomerNumberMethod { get; set; } public SelectList AvailableCustomerNumberMethods { get; set; } + public IList AvailableRegisterCustomerRoles { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.Catalog.DefaultSortOrderMode")] + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.CustomerNumberVisibility")] public CustomerNumberVisibility CustomerNumberVisibility { get; set; } public SelectList AvailableCustomerNumberVisibilities { get; set; } @@ -43,7 +44,10 @@ public partial class CustomerSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.UserRegistrationType")] public int UserRegistrationType { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.AllowCustomersToUploadAvatars")] + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.RegisterCustomerRole")] + public int RegisterCustomerRoleId { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.AllowCustomersToUploadAvatars")] public bool AllowCustomersToUploadAvatars { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.DefaultAvatarEnabled")] @@ -85,6 +89,9 @@ public partial class CustomerSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.StoreLastVisitedPage")] public bool StoreLastVisitedPage { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs")] + public bool DisplayPrivacyAgreementOnContactUs { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.GenderEnabled")] public bool GenderEnabled { get; set; } @@ -131,7 +138,7 @@ public partial class CustomerSettingsModel public bool FaxEnabled { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.CustomerUser.FaxRequired")] public bool FaxRequired { get; set; } - } + } public partial class AddressSettingsModel { diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/DataExchangeSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/DataExchangeSettingsModel.cs new file mode 100644 index 0000000000..539505d733 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/DataExchangeSettingsModel.cs @@ -0,0 +1,28 @@ +using SmartStore.Web.Framework; + +namespace SmartStore.Admin.Models.Settings +{ + public partial class DataExchangeSettingsModel + { + #region General + + [SmartResourceDisplayName("Admin.Configuration.Settings.DataExchange.MaxFileNameLength")] + public int MaxFileNameLength { get; set; } + + #endregion + + #region Import + + [SmartResourceDisplayName("Admin.Configuration.Settings.DataExchange.ImageImportFolder")] + public string ImageImportFolder { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.DataExchange.ImageDownloadTimeout")] + public int ImageDownloadTimeout { get; set; } + + #endregion + + #region Export + + #endregion + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs index db49b69053..57332d0726 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/GeneralCommonSettingsModel.cs @@ -78,10 +78,22 @@ public partial class SeoSettingsModel [AllowHtml] public string DefaultMetaDescription { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.ConvertNonWesternChars")] + [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.MetaRobotsContent")] + public string MetaRobotsContent { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.ConvertNonWesternChars")] public bool ConvertNonWesternChars { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.CanonicalUrlsEnabled")] + [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.AllowUnicodeCharsInUrls")] + public bool AllowUnicodeCharsInUrls { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.SeoNameCharConversion")] + public string SeoNameCharConversion { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.TestSeoNameCreation")] + public string TestSeoNameCreation { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.CanonicalUrlsEnabled")] public bool CanonicalUrlsEnabled { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.GeneralCommon.CanonicalHostNameRule")] diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/MediaSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/MediaSettingsModel.cs index 6d9a8282b2..164949eeff 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/MediaSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/MediaSettingsModel.cs @@ -27,13 +27,16 @@ public MediaSettingsModel() [SmartResourceDisplayName("Admin.Configuration.Settings.Media.ProductThumbPictureSizeOnProductDetailsPage")] public int ProductThumbPictureSizeOnProductDetailsPage { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Media.MessageProductThumbPictureSize")] + public int MessageProductThumbPictureSize { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Media.AssociatedProductPictureSize")] public int AssociatedProductPictureSize { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.Media.BundledProductPictureSize")] public int BundledProductPictureSize { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.Media.CategoryThumbPictureSize")] + [SmartResourceDisplayName("Admin.Configuration.Settings.Media.CategoryThumbPictureSize")] public int CategoryThumbPictureSize { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.Media.ManufacturerThumbPictureSize")] diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs index ea2bc65410..c0e73f70fb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/OrderSettingsModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Admin.Validators.Settings; @@ -58,10 +59,17 @@ public OrderSettingsModel() public IList GiftCards_Deactivated_OrderStatuses { get; set; } public string PrimaryStoreCurrencyCode { get; set; } + public int StoreCount { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.Order.OrderIdent")] public int? OrderIdent { get; set; } + [SmartResourceDisplayName("Admin.Configuration.Settings.Order.DisplayOrdersOfAllStores")] + public bool DisplayOrdersOfAllStores { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.Order.OrderListPageSize")] + public int OrderListPageSize { get; set; } + public IList Locales { get; set; } } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/RewardPointsSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/RewardPointsSettingsModel.cs index 57f5669179..1f73a57113 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/RewardPointsSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/RewardPointsSettingsModel.cs @@ -13,7 +13,10 @@ public class RewardPointsSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.RewardPoints.ExchangeRate")] public decimal ExchangeRate { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.RewardPoints.PointsForRegistration")] + [SmartResourceDisplayName("Admin.Configuration.Settings.RewardPoints.RoundDownRewardPoints")] + public bool RoundDownRewardPoints { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.RewardPoints.PointsForRegistration")] public int PointsForRegistration { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.RewardPoints.PointsForProductReview")] diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Settings/ShoppingCartSettingsModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Settings/ShoppingCartSettingsModel.cs index 34d639051b..72d23ec58f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Settings/ShoppingCartSettingsModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Settings/ShoppingCartSettingsModel.cs @@ -1,9 +1,18 @@ -using SmartStore.Web.Framework; +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Core.Domain.Orders; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Localization; namespace SmartStore.Admin.Models.Settings { - public class ShoppingCartSettingsModel - { + public class ShoppingCartSettingsModel : ILocalizedModel + { + public ShoppingCartSettingsModel() + { + Locales = new List(); + } + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.DisplayCartAfterAddingProduct")] public bool DisplayCartAfterAddingProduct { get; set; } @@ -40,7 +49,10 @@ public class ShoppingCartSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.CrossSellsNumber")] public int CrossSellsNumber { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.EmailWishlistEnabled")] + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation")] + public bool RoundPricesDuringCalculation { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.EmailWishlistEnabled")] public bool EmailWishlistEnabled { get; set; } [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.AllowAnonymousUsersToEmailWishlist")] @@ -76,8 +88,29 @@ public class ShoppingCartSettingsModel [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.ShowCommentBox")] public bool ShowCommentBox { get; set; } - [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation")] - public bool RoundPricesDuringCalculation { get; set; } - - } + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.ShowEsdRevocationWaiverBox")] + public bool ShowEsdRevocationWaiverBox { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.NewsLetterSubscription")] + public CheckoutNewsLetterSubscription NewsLetterSubscription { get; set; } + public SelectList AvailableNewsLetterSubscriptions { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOver")] + public CheckoutThirdPartyEmailHandOver ThirdPartyEmailHandOver { get; set; } + public SelectList AvailableThirdPartyEmailHandOver { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel")] + public string ThirdPartyEmailHandOverLabel { get; set; } + + public IList Locales { get; set; } + } + + + public class ShoppingCartSettingsLocalizedModel : ILocalizedModelLocal + { + public int LanguageId { get; set; } + + [SmartResourceDisplayName("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel")] + public string ThirdPartyEmailHandOverLabel { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj index 8beba42ebc..603f623aa1 100644 --- a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj +++ b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj @@ -92,9 +92,9 @@ 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\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -102,6 +102,7 @@ + False @@ -181,6 +182,7 @@ + @@ -212,6 +214,7 @@ + @@ -250,21 +253,27 @@ + + + + + + @@ -374,6 +383,8 @@ + + @@ -420,29 +431,268 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - @@ -471,21 +721,22 @@ - + + + + + + - - Always - - - Always - + + Designer @@ -526,13 +777,20 @@ - + + + + + + + + @@ -732,15 +990,6 @@ - - - - - - - - - @@ -1033,9 +1282,6 @@ - - - @@ -1223,9 +1469,6 @@ - - - @@ -1238,15 +1481,9 @@ - - - - - - diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/CsvConfigurationValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/CsvConfigurationValidator.cs new file mode 100644 index 0000000000..4910a98192 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/CsvConfigurationValidator.cs @@ -0,0 +1,35 @@ +using System.Linq; +using FluentValidation; +using SmartStore.Admin.Models.DataExchange; +using SmartStore.Services.DataExchange.Csv; +using SmartStore.Services.Localization; + +namespace SmartStore.Admin.Validators.DataExchange +{ + public partial class CsvConfigurationValidator : AbstractValidator + { + public CsvConfigurationValidator(ILocalizationService localization) + { + RuleFor(x => x.Delimiter) + .Must(x => !CsvConfiguration.PresetCharacters.Contains(x.ToChar(true))) + .WithMessage(localization.GetResource("Admin.DataExchange.Csv.Delimiter.Validation")); + + RuleFor(x => x.Quote) + .Must(x => !CsvConfiguration.PresetCharacters.Contains(x.ToChar(true))) + .WithMessage(localization.GetResource("Admin.DataExchange.Csv.Quote.Validation")); + + RuleFor(x => x.Escape) + .Must(x => !CsvConfiguration.PresetCharacters.Contains(x.ToChar(true))) + .WithMessage(localization.GetResource("Admin.DataExchange.Csv.Escape.Validation")); + + + RuleFor(x => x.Escape) + .Must((model, x) => x != model.Delimiter) + .WithMessage(localization.GetResource("Admin.DataExchange.Csv.EscapeDelimiter.Validation")); + + RuleFor(x => x.Quote) + .Must((model, x) => x != model.Delimiter) + .WithMessage(localization.GetResource("Admin.DataExchange.Csv.QuoteDelimiter.Validation")); + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs index 9382c5db27..dc15762b5c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs +++ b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ExportProfileValidator.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Linq; -using FluentValidation; +using FluentValidation; using SmartStore.Admin.Models.DataExchange; using SmartStore.Services.Localization; @@ -15,12 +13,24 @@ public ExportProfileValidator(ILocalizationService localization) .WithMessage(localization.GetResource("Admin.Validation.Name")); RuleFor(x => x.FolderName) - .Must(x => x.HasValue() && !x.IsCaseInsensitiveEqual("con") && !Path.GetInvalidFileNameChars().Any(y => x.Contains(y))) - .WithMessage(localization.GetResource("Admin.DataExchange.Export.FolderAndFileName.Validate")); + .Must(x => x.HasValue() && !x.IsCaseInsensitiveEqual("con")) + .WithMessage(localization.GetResource("Admin.DataExchange.Export.FolderName.Validate")); RuleFor(x => x.FileNamePattern) .NotEmpty() - .WithMessage(localization.GetResource("Admin.DataExchange.Export.FolderAndFileName.Validate")); + .WithMessage(localization.GetResource("Admin.DataExchange.Export.FileNamePattern.Validate")); + + RuleFor(x => x.Offset) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.DataExchange.Export.Partition.Validate")); + + RuleFor(x => x.Limit) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.DataExchange.Export.Partition.Validate")); + + RuleFor(x => x.BatchSize) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.DataExchange.Export.Partition.Validate")); } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs new file mode 100644 index 0000000000..95277d0d85 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Validators/DataExchange/ImportProfileValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using SmartStore.Admin.Models.DataExchange; +using SmartStore.Services.Localization; + +namespace SmartStore.Admin.Validators.DataExchange +{ + public partial class ImportProfileValidator : AbstractValidator + { + public ImportProfileValidator(ILocalizationService localization) + { + RuleFor(x => x.Name) + .NotEmpty() + .WithMessage(localization.GetResource("Admin.Validation.Name")); + + RuleFor(x => x.KeyFieldNames) + .NotEmpty() + .When(x => x.Id != 0) + .WithMessage(localization.GetResource("Admin.DataExchange.Import.Validate.OneKeyFieldRequired")); + + RuleFor(x => x.Skip) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.Common.SkipAndTakeGreaterThanOrEqualZero")); + + RuleFor(x => x.Take) + .GreaterThanOrEqualTo(0) + .WithMessage(localization.GetResource("Admin.Common.SkipAndTakeGreaterThanOrEqualZero")); + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/Localization/LanguageValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/Localization/LanguageValidator.cs index ea6ba1314e..891c093efc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Validators/Localization/LanguageValidator.cs +++ b/src/Presentation/SmartStore.Web/Administration/Validators/Localization/LanguageValidator.cs @@ -9,29 +9,32 @@ public partial class LanguageValidator : AbstractValidator { public LanguageValidator(ILocalizationService localizationService) { - RuleFor(x => x.Name).NotEmpty().WithMessage(localizationService.GetResource("Admin.Configuration.Languages.Fields.Name.Required")); + RuleFor(x => x.Name) + .NotEmpty() + .WithMessage(localizationService.GetResource("Admin.Configuration.Languages.Fields.Name.Required")); + RuleFor(x => x.LanguageCulture) .Must(x => - { - try - { - var culture = new CultureInfo(x); - return culture != null; - } - catch - { - return false; - } - }) + { + try + { + var culture = new CultureInfo(x); + return culture != null; + } + catch + { + return false; + } + }) .WithMessage(localizationService.GetResource("Admin.Configuration.Languages.Fields.LanguageCulture.Validation")); RuleFor(x => x.UniqueSeoCode) .NotNull() .WithMessage(localizationService.GetResource("Admin.Configuration.Languages.Fields.UniqueSeoCode.Required")); + RuleFor(x => x.UniqueSeoCode) .Length(2) .WithMessage(localizationService.GetResource("Admin.Configuration.Languages.Fields.UniqueSeoCode.Length")); - } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs b/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs index eab92160eb..547872cac6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs +++ b/src/Presentation/SmartStore.Web/Administration/Validators/Settings/OrderSettingsValidator.cs @@ -11,8 +11,13 @@ public OrderSettingsValidator(ILocalizationService localizationService) { RuleFor(x => x.GiftCards_Activated_OrderStatusId).NotEqual((int)OrderStatus.Pending) .WithMessage(localizationService.GetResource("Admin.Configuration.Settings.RewardPoints.PointsForPurchases_Awarded.Pending")); + RuleFor(x => x.GiftCards_Deactivated_OrderStatusId).NotEqual((int)OrderStatus.Pending) .WithMessage(localizationService.GetResource("Admin.Configuration.Settings.RewardPoints.PointsForPurchases_Canceled.Pending")); - } + + RuleFor(x => x.OrderListPageSize) + .GreaterThan(0) + .WithMessage(localizationService.GetResource("Admin.Validation.ValueGreaterZero")); + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml index bf1eb8813b..dbd3b7329e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ActivityLog/ListLogs.cshtml @@ -23,22 +23,30 @@

- - - - - - - - + + + + + + + + + + + + - - - - + + + + -
- @Html.SmartLabelFor(model => model.CreatedOnFrom) - - @Html.EditorFor(model => model.CreatedOnFrom) -
- @Html.SmartLabelFor(model => model.CreatedOnTo) - - @Html.EditorFor(model => model.CreatedOnTo) -
+ @Html.SmartLabelFor(model => model.CreatedOnFrom) + + @Html.EditorFor(model => model.CreatedOnFrom) +
+ @Html.SmartLabelFor(model => model.CreatedOnTo) + + @Html.EditorFor(model => model.CreatedOnTo) +
+ @Html.SmartLabelFor(model => model.ActivityLogTypeId) + + @Html.DropDownListFor(ModelBase => ModelBase.ActivityLogTypeId, Model.ActivityLogType, T("Common.Unspecified")) +
@Html.SmartLabelFor(model => model.CustomerEmail) @@ -47,14 +55,14 @@ @Html.EditorFor(model => model.CustomerEmail)
- @Html.SmartLabelFor(model => model.ActivityLogTypeId) - - @Html.DropDownListFor(ModelBase => ModelBase.ActivityLogTypeId, Model.ActivityLogType, T("Common.Unspecified")) -
+ @Html.SmartLabelFor(model => model.CustomerSystemAccount) + + @Html.EditorFor(model => Model.CustomerSystemAccount) +
  @@ -89,12 +97,14 @@ .HtmlAttributes(new { style = "text-align:center" }) .HeaderHtmlAttributes(new { style = "text-align:center" }); columns.Bound(x => x.ActivityLogTypeName); + columns.Bound(x => x.Comment); columns.Bound(x => x.CustomerId) - .Template(x => Html.ActionLink(x.CustomerEmail, "Edit", "Customer", new { id = x.CustomerId }, new { })) .ClientTemplate("\"><#= CustomerEmail #>"); - columns.Bound(x => x.Comment); + columns.Bound(x => x.IsSystemAccount) + .ClientTemplate(" <#= SystemAccountName #>") + .Width(220); columns.Bound(x => x.CreatedOn) - .Width(180); + .Width(200); }) .DataBinding(dataBinding => { @@ -146,20 +156,20 @@ $('.checkboxGroups').attr('checked', $(this).is(':checked')).change(); }); - //wire up checkboxes. + //wire up checkboxes. $(document).on('change', '#activityLog-grid input[type=checkbox][id!=mastercheckbox]', function (e) { var $check = $(this); if ($check.is(":checked") == true) { var checked = jQuery.inArray($check.val(), selectedIds); if (checked == -1) { - //add id to selectedIds. + //add id to selectedIds. selectedIds.push($check.val()); } } else { var checked = jQuery.inArray($check.val(), selectedIds); if (checked > -1) { - //remove id from selectedIds. + //remove id from selectedIds. selectedIds = $.grep(selectedIds, function (item, index) { return item != $check.val(); }); @@ -179,11 +189,12 @@ }); function onDataBinding(e) { - var searchModel = { + var searchModel = { + ActivityLogTypeId: $('#ActivityLogTypeId').val(), CreatedOnFrom: $('#@Html.FieldIdFor(model => model.CreatedOnFrom)').val(), CreatedOnTo: $('#@Html.FieldIdFor(model => model.CreatedOnTo)').val(), - CustomerEmail: $('#@Html.FieldIdFor(model => model.CustomerEmail)').val(), - ActivityLogTypeId: $('#ActivityLogTypeId').val() + CustomerEmail: $('#@Html.FieldIdFor(model => model.CustomerEmail)').val(), + CustomerSystemAccount: $('#@Html.FieldIdFor(model => model.CustomerSystemAccount)').val() }; e.data = searchModel; } @@ -192,7 +203,7 @@ $('#activityLog-grid input[type=checkbox][id!=mastercheckbox]').each(function () { var currentId = $(this).val(); var checked = jQuery.inArray(currentId, selectedIds); - //set checked based on if current checkbox's value is in selectedIds. + //set checked based on if current checkbox's value is in selectedIds. $(this).attr('checked', checked > -1); }); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/_CreateOrUpdate.cshtml index bf2d05b394..36bc46357c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Affiliate/_CreateOrUpdate.cshtml @@ -13,7 +13,7 @@ x.Add().Text(T("Admin.Affiliates.Orders").Text).Content(TabOrders()); } - //generate an event + // generate an event EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "affiliate-edit", this.Html, this.Model)); }) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml index 7a820a5858..a57724aeaf 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/List.cshtml @@ -17,7 +17,7 @@ - +
- - - - + @if (Model.AvailableStores.Count > 1) + { + + + + + }
@Html.SmartLabelFor(model => model.SearchCategoryName) @@ -34,14 +34,17 @@ @Html.EditorFor(model => Model.SearchAlias)
- @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
+ @Html.SmartLabelFor(model => model.SearchStoreId) + + @Html.DropDownList("SearchStoreId", Model.AvailableStores, T("Admin.Common.All")) +
  @@ -88,6 +91,7 @@ .Width(160) .Template(item => @Html.SymbolForBool(item.LimitedToStores)) .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) + .Hidden(Model.AvailableStores.Count <= 1) .Centered(); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) @@ -99,61 +103,61 @@
- } - - - - - - - - - - - - - - - - - - - - - - - -
- @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
- @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
- @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
- @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
-   - - -
-

-

- - - - - - - -
- @(Html.Telerik().Grid(Model.Products.Data) - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .Template(x => string.Format("", x.Id)) - .ClientTemplate("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Name) - .Template(x => @Html.LabeledProductName(0, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.Total(Model.Products.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("ProductAddPopupList", "Category")) - .ClientEvents(events => events.OnDataBinding("onDataBinding")) - .EnableCustomBinding(true)) -
-

- -
- -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/Tree.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/Tree.cshtml index 68162d1e32..1f3d747077 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/Tree.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/Tree.cshtml @@ -15,26 +15,29 @@ -@using (Html.BeginForm()) +@if (Model.AvailableStores.Count > 1) { - - - - - - - - - -
- @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
-   - - -
+ using (Html.BeginForm()) + { + + + + + + + + + +
+ @Html.SmartLabelFor(model => model.SearchStoreId) + + @Html.DropDownList("SearchStoreId", Model.AvailableStores, T("Admin.Common.All")) +
+   + + +
+ } }

diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml index 9f98f0d5a0..f78aa0983e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml @@ -14,8 +14,25 @@ //generate an event EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "category-edit", this.Html, this.Model)); }) -@helper TabInfo() { - +@helper TabInfo() +{ + if (Model.Id != 0) + { + + + + + +
+ @Html.SmartLabelFor(model => model.Id) + + @Html.DisplayFor(model => model.Id) + @Html.ValidationMessageFor(model => model.Id) +
+ +

+ } + @(Html.LocalizedEditor("category-info-localized", @ @@ -36,7 +53,7 @@ @Html.ValidationMessageFor(model => model.Locales[item].FullName) - + @@ -45,7 +62,7 @@ @Html.ValidationMessageFor(model => model.Locales[item].Description) - + @@ -54,6 +71,16 @@ @Html.ValidationMessageFor(model => model.Locales[item].BottomDescription) + + + + - + @@ -89,7 +116,7 @@ @Html.ValidationMessageFor(model => model.Description) - + @@ -98,8 +125,19 @@ @Html.ValidationMessageFor(model => model.BottomDescription) + + + +
@Html.SmartLabelFor(model => model.Locales[item].Description)
@Html.SmartLabelFor(model => model.Locales[item].BottomDescription)
+   + + +
@Html.HiddenFor(model => model.Locales[item].LanguageId) @@ -80,7 +107,7 @@ @Html.ValidationMessageFor(model => model.FullName)
@Html.SmartLabelFor(model => model.Description)
@Html.SmartLabelFor(model => model.BottomDescription)
+   + + +
- )) + )) +
@@ -214,7 +252,9 @@ }
} -@helper TabSeo() { + +@helper TabSeo() +{ @(Html.LocalizedEditor("category-seo-localized", @ @@ -332,18 +372,22 @@
} -@helper TabProducts() { - if (Model.Id > 0) { - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - - + + + + +
- @(Html.Telerik().Grid() - .Name("products-grid") - .DataKeys(keys => { + +@helper TabProducts() +{ + if (Model.Id > 0) + { + + + - - - - -
+ @(Html.Telerik().Grid() + .Name("products-grid") + .DataKeys(keys => + { keys.Add(x => x.Id); }) - .DataBinding(dataBinding => { + .DataBinding(dataBinding => + { dataBinding.Ajax() .Select("ProductList", "Category", new { categoryId = Model.Id }) .Update("ProductUpdate", "Category") @@ -352,102 +396,74 @@ .Columns(columns => { columns.Bound(x => x.ProductName) - .ReadOnly() - .Template(x => @Html.LabeledProductName(x.Id, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) + .ReadOnly() + .Width(520) .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); columns.Bound(x => x.Sku) .ReadOnly(); columns.Bound(x => x.Published) .ReadOnly() - .Width(100) - .Template(item => @Html.SymbolForBool(item.Published)) .ClientTemplate(@Html.SymbolForBool("Published")) .Centered(); columns.Bound(x => x.IsFeaturedProduct) - .Width(100) - .Template(item => @Html.SymbolForBool(item.IsFeaturedProduct)) .ClientTemplate(@Html.SymbolForBool("IsFeaturedProduct")) .Centered(); columns.Bound(x => x.DisplayOrder1) - .Width(100) .Centered(); columns.Command(commands => { commands.Edit().Localize(T); commands.Delete().Localize(T); }) - .Width(180); + .Width(220); }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) .PreserveGridState() .EnableCustomBinding(true)) -
- - - -
- } - else { - @T("Admin.Catalog.Categories.Products.SaveBeforeEdit") - } +
+ +
+ } + else + { + @T("Admin.Catalog.Categories.Products.SaveBeforeEdit") + } } -@helper TabDiscounts() { - if (Model.AvailableDiscounts != null && Model.AvailableDiscounts.Count > 0) { - foreach (var discount in Model.AvailableDiscounts) { +@helper TabDiscounts() +{ + if (Model.AvailableDiscounts != null && Model.AvailableDiscounts.Count > 0) + { + foreach (var discount in Model.AvailableDiscounts) + { } } - else { - @T("Admin.Catalog.Categories.Discounts.NoDiscounts") + else + { +
+ @T("Admin.Catalog.Categories.Discounts.NoDiscounts") +
} } -@helper TabAcl() { - - +@helper TabAcl() +{ - - - - + + + + + +
@@ -463,13 +479,15 @@ @Html.SmartLabelFor(model => model.AvailableCustomerRoles) - @if (Model.AvailableCustomerRoles != null && Model.AvailableCustomerRoles.Count > 0) { - foreach (var customerRole in Model.AvailableCustomerRoles) { -
- -
+ @if (Model.AvailableCustomerRoles != null && Model.AvailableCustomerRoles.Count > 0) + { + foreach (var customerRole in Model.AvailableCustomerRoles) + { +
+ +
} } else @@ -480,13 +498,12 @@ }

-   - -
+
 
+
@T("Admin.Configuration.Category.Acl.AssignToSubCategoriesAndProducts.Hint")
@@ -500,25 +517,6 @@ @helper TabStores() { - - - - - - + + + + - -
@@ -538,10 +536,10 @@ { foreach (var store in Model.AvailableStores) { - + } } else @@ -550,13 +548,11 @@ }

-   - +
 
@T("Admin.Configuration.Category.Stores.AssignToSubCategoriesAndProducts.Hint")
@@ -570,18 +566,54 @@ } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml index d80b3f8bfb..b3707ed7fe 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/CheckoutAttribute/List.cshtml @@ -1,5 +1,5 @@ @using Telerik.Web.Mvc.UI - +@model CheckoutAttributeListModel @{ ViewBag.Title = T("Admin.Catalog.Attributes.CheckoutAttributes").Text; } @@ -8,7 +8,9 @@ @T("Admin.Catalog.Attributes.CheckoutAttributes") -
- } - else - { - @T("Admin.Catalog.Attributes.CheckoutAttributes.Values.SaveBeforeEdit") - } - } \ No newline at end of file + //return false to don't reload a page + return false; + }); + }); + +
+ } + else + { + @T("Admin.Catalog.Attributes.CheckoutAttributes.Values.SaveBeforeEdit") + } +} + +@helper TabStores() +{ + + + + + + + + + + + +
+ @Html.SmartLabelFor(model => model.LimitedToStores) + + @Html.EditorFor(model => model.LimitedToStores) + @Html.ValidationMessageFor(model => model.LimitedToStores) +
+ @Html.SmartLabelFor(model => model.AvailableStores) + + @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) + { + foreach (var store in Model.AvailableStores) + { + + } + } + else + { +
@T("Admin.Configuration.Stores.NoStoresDefined")
+ } +
+} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ContentSlider/Index.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ContentSlider/Index.cshtml index 51251e2491..e847170de8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ContentSlider/Index.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ContentSlider/Index.cshtml @@ -120,24 +120,27 @@ - - - - - - - - - -
- @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("countries-grid") - .BindTo(Model.Data) - .Columns(columns => - { - columns.Bound(x => x.Name) - .Width(250) - .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })) - .ClientTemplate("<#= Name #>"); - columns.Bound(x => x.AllowsBilling) - .Width(50) - .Template(item => @Html.SymbolForBool(item.AllowsBilling)) - .ClientTemplate(@Html.SymbolForBool("AllowsBilling")) - .Centered(); - columns.Bound(x => x.AllowsShipping) - .Width(50) - .Template(item => @Html.SymbolForBool(item.AllowsShipping)) - .ClientTemplate(@Html.SymbolForBool("AllowsShipping")) - .Centered(); - columns.Bound(x => x.TwoLetterIsoCode) - .Width(50) - .Centered(); - columns.Bound(x => x.ThreeLetterIsoCode) - .Width(50) - .Centered(); - columns.Bound(x => x.NumericIsoCode) - .Width(50) - .Centered(); - columns.Bound(x => x.SubjectToVat) - .Width(50) - .Template(item => @Html.SymbolForBool(item.SubjectToVat)) - .ClientTemplate(@Html.SymbolForBool("SubjectToVat")) - .Centered(); - columns.Bound(x => x.NumberOfStates) - .Width(50) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Width(50) - .Centered(); + .Name("countries-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("Id"); + }) + .Editable(x => + { + x.Mode(GridEditMode.InLine); + }) + .Columns(columns => + { + columns.Bound(x => x.Name) + .ClientTemplate("<#= Name #>"); + columns.Bound(x => x.AllowsBilling) + .Width(50) + .ClientTemplate(@Html.SymbolForBool("AllowsBilling")) + .Centered(); + columns.Bound(x => x.AllowsShipping) + .ClientTemplate(@Html.SymbolForBool("AllowsShipping")) + .Centered(); + columns.Bound(x => x.TwoLetterIsoCode) + .Hidden(true); + columns.Bound(x => x.ThreeLetterIsoCode) + .Hidden(true); + columns.Bound(x => x.NumericIsoCode) + .Hidden(true); + columns.Bound(x => x.SubjectToVat) + .ClientTemplate(@Html.SymbolForBool("SubjectToVat")) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Centered(); columns.Bound(x => x.LimitedToStores) - .Template(item => @Html.SymbolForBool(item.LimitedToStores)) - .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) - .Centered() - .Width(100) - .Hidden(ViewBag.StoreCount <= 1); - columns.Bound(x => x.Published) - .Width(50) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - }) - .DataBinding(dataBinding => dataBinding.Ajax().Select("CountryList", "Country")) - .EnableCustomBinding(true)) + .Hidden(true); + columns.Bound(x => x.Published) + .ClientTemplate(@Html.SymbolForBool("Published")) + .Centered(); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }); + }) + .DataBinding(dataBinding => dataBinding.Ajax() + .Select("CountryList", "Country") + .Update("CountryUpdate", "Country") + .Delete("CountryDelete", "Country") + ) + .ClientEvents(x => x.OnError("CountryGrid_onError")) + .PreserveGridState() + .EnableCustomBinding(true))
+ + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Country/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Country/_CreateOrUpdate.cshtml index c96abce377..990ee0124b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Country/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Country/_CreateOrUpdate.cshtml @@ -6,7 +6,7 @@ @Html.SmartStore().TabStrip().Name("country-edit").Items(x => { x.Add().Text(T("Admin.Configuration.Countries.Info").Text).Content(TabInfo()).Selected(true); - x.Add().Text(T("Admin.Configuration.Countries.States").Text).Content(TabStates()); + x.Add().Text("{0} ({1})".FormatInvariant(T("Admin.Configuration.Countries.States"), Model.NumberOfStates)).Content(TabStates()); x.Add().Text(T("Admin.Common.Stores").Text).Content(TabStores()); //generate an event @@ -144,14 +144,13 @@ { columns.Bound(x => x.Name); //TODO display localized values here - columns.Bound(x => x.Abbreviation); + columns.Bound(x => x.Abbreviation) + .Centered(); columns.Bound(x => x.Published) .Template(item => @Html.SymbolForBool(item.Published)) .ClientTemplate(@Html.SymbolForBool("Published")) - .Width(120) .Centered(); columns.Bound(x => x.DisplayOrder1) - .Width(120) .Centered(); columns.Bound(x => x.Id) .Centered() @@ -168,7 +167,7 @@
+
} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml index 090c3da2d5..a6ddb41d85 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_CreateOrUpdate.cshtml @@ -36,7 +36,7 @@ EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "customer-edit", this.Html, this.Model)); }) @helper TabInfo() - { +{ if (Model.CountryEnabled && Model.StateProvinceEnabled) { } + @if (Model.Id != 0) + { + + + + + } @if (Model.UsernamesEnabled) { if (Model.Id == 0 || Model.AllowUsersToChangeUsernames) @@ -495,13 +507,16 @@ columns.Bound(x => x.Message); columns.Bound(x => x.CreatedOn).ReadOnly(); })) -
-
+
-
-

@T("Admin.Customers.Customers.RewardPoints.AddTitle")

-
+ @Html.SmartLabelFor(model => model.Id) + + @Html.DisplayFor(model => model.Id) + @Html.ValidationMessageFor(model => model.Id) +
+ + +
+
+
@T("Admin.Customers.Customers.RewardPoints.AddTitle")
+
+
@Html.SmartLabelFor(model => model.AddRewardPointsValue) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml index 98165844ae..919a1af379 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByNumberOfOrders.cshtml @@ -1,59 +1,59 @@ @model BestCustomersReportModel - @using Telerik.Web.Mvc.UI - - - - - - - - - - - - - - - - - - - - - - - - - -
- @Html.SmartLabelFor(model => model.StartDate) - - @Html.EditorFor(model => model.StartDate) -
- @Html.SmartLabelFor(model => model.EndDate) - - @Html.EditorFor(model => Model.EndDate) -
- @Html.SmartLabelFor(model => model.OrderStatusId) - - @Html.DropDownList("OrderStatusId", Model.AvailableOrderStatuses) -
- @Html.SmartLabelFor(model => model.PaymentStatusId) - - @Html.DropDownList("PaymentStatusId", Model.AvailablePaymentStatuses) -
- @Html.SmartLabelFor(model => model.ShippingStatusId) - - @Html.DropDownList("ShippingStatusId", Model.AvailableShippingStatuses) -
-   - - -
-

-

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ @Html.SmartLabelFor(model => model.StartDate) + + @Html.EditorFor(model => model.StartDate) +
+ @Html.SmartLabelFor(model => model.EndDate) + + @Html.EditorFor(model => Model.EndDate) +
+ @Html.SmartLabelFor(model => model.OrderStatusId) + + @Html.DropDownList("OrderStatusId", Model.AvailableOrderStatuses, T("Admin.Common.All")) +
+ @Html.SmartLabelFor(model => model.PaymentStatusId) + + @Html.DropDownList("PaymentStatusId", Model.AvailablePaymentStatuses, T("Admin.Common.All")) +
+ @Html.SmartLabelFor(model => model.ShippingStatusId) + + @Html.DropDownList("ShippingStatusId", Model.AvailableShippingStatuses, T("Admin.Common.All")) +
+   + + +
+ +

+ @@ -209,6 +207,11 @@ @helper TabPartition() { +
+ + @Html.Raw(T("Admin.DataExchange.Export.Partition.Note")) +
+
diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml index 13c0f4570f..af0e21e9bb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Customer/_ReportBestCustomersByOrderTotal.cshtml @@ -4,7 +4,7 @@ @using (Html.BeginForm()) { - +
@@ -34,7 +34,7 @@ @Html.SmartLabelFor(model => model.PaymentStatusId) @@ -42,7 +42,7 @@ @Html.SmartLabelFor(model => model.ShippingStatusId) @@ -54,8 +54,9 @@
@Html.SmartLabelFor(model => model.StartDate) @@ -26,7 +26,7 @@ @Html.SmartLabelFor(model => model.OrderStatusId) - @Html.DropDownList("OrderStatusId", Model.AvailableOrderStatuses) + @Html.DropDownList("OrderStatusId", Model.AvailableOrderStatuses, T("Admin.Common.All"))
- @Html.DropDownList("PaymentStatusId", Model.AvailablePaymentStatuses) + @Html.DropDownList("PaymentStatusId", Model.AvailablePaymentStatuses, T("Admin.Common.All"))
- @Html.DropDownList("ShippingStatusId", Model.AvailableShippingStatuses) + @Html.DropDownList("ShippingStatusId", Model.AvailableShippingStatuses, T("Admin.Common.All"))
-

-

+ +

+ - } + }
diff --git a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml index 9028265175..64d56e6030 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml @@ -100,6 +100,7 @@
+  
+ +@if (Model.TestEmailShortErrorMessage.HasValue()) +{ +

+

@Model.TestEmailShortErrorMessage
+
@Model.TestEmailFullErrorMessage
+

+} + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Create.cshtml index f1f162fedf..ffe062fd5b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Create.cshtml @@ -15,7 +15,7 @@
- @foreach (var item in Model.AvailableProfiles) + @foreach (var item in Model.AvailableProfiles.OrderBy(x => x.FriendlyName)) { } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml index bf7866d3df..875a7268f5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/CreateDeployment.cshtml @@ -2,7 +2,7 @@ @using SmartStore.Core.Domain.DataExchange; @using SmartStore.Admin.Models.DataExchange; @{ - var title = string.Concat(T("Common.AddNew"), " ", T("Common.Deployment")); + var title = string.Concat(T("Common.Publishing"), " - ", T("Common.AddNew")); ViewBag.Title = title; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml index 17a71d5ebc..f47ae86481 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/Edit.cshtml @@ -2,7 +2,7 @@ @using SmartStore.Core.Domain.DataExchange; @using SmartStore.Admin.Models.DataExchange; @{ - var title = string.Concat(T("Admin.Common.Edit"), " ", T("Common.Profile")); + var title = string.Concat(T("Common.Profile"), " - ", T("Admin.Common.Edit")); ViewBag.Title = title; } @@ -19,13 +19,20 @@
@if (Model.HasActiveProvider) { + if (Model.Id != 0) + { + + } +  @T("Admin.Common.Preview") } @if (Model.LogFileExists) { - +  @T("Admin.Configuration.ActivityLog") } @@ -124,16 +131,7 @@ @Html.SmartLabelFor(x => x.Provider.FriendlyName)
- @if (Model.Provider.ConfigurationUrl.HasValue()) - { - - @Model.Provider.FriendlyName.NaIfEmpty() - - } - else - { -
@Model.Provider.FriendlyName.NaIfEmpty()
- } +
@Model.Provider.FriendlyName.NaIfEmpty()
@(Model.ProviderSystemName.NaIfEmpty())
} @foreach (var profile in Model.Profiles) { - - - + + + if (profile.ScheduleTaskId.HasValue) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml index 9ddcf27347..05bd582199 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/List.cshtml @@ -1,139 +1,38 @@ -@using SmartStore.Core.Domain.DataExchange; -@using SmartStore.Admin.Models.DataExchange; -@using Telerik.Web.Mvc.UI; +@using SmartStore.Admin.Models.DataExchange; @model List @{ ViewBag.Title = T("Common.Export").Text; + + var hasUserProfiles = Model.Any(x => !x.IsSystemProfile); + var hasSystemProfiles = Model.Any(x => x.IsSystemProfile); }
- - @T("Common.Export") -
-
-  @T("Admin.Common.AddNew") +  @T("Common.Export")
-
- - - -@if(Model.Count > 0) -{ -
@@ -251,7 +254,10 @@ @helper TabFilter() { - if (Model.Provider.EntityType == ExportEntityType.Product || Model.Provider.EntityType == ExportEntityType.Order) + if (Model.Provider.EntityType == ExportEntityType.Product || + Model.Provider.EntityType == ExportEntityType.Customer || + Model.Provider.EntityType == ExportEntityType.Order || + Model.Provider.EntityType == ExportEntityType.NewsLetterSubscription) {
@Html.Raw(T("Admin.DataExchange.Export.Filter.Note"))
@Html.Partial("_Tab.Filter", Model) @@ -282,7 +288,7 @@ @if (Model.Provider.ConfigPartialViewName.HasValue()) {
- + @Html.Raw(T("Admin.DataExchange.Export.Configuration.Note"))
@@ -290,7 +296,18 @@ viewData.TemplateInfo.HtmlFieldPrefix = "CustomProperties[ProviderConfigData]"; viewData.ModelState.Merge(ViewData.ModelState); @Html.Hidden("CustomProperties[ProviderConfigData].__Type__", Model.Provider.ConfigDataType.AssemblyQualifiedName) - @Html.Partial(Model.Provider.ConfigPartialViewName, Model.Provider.ConfigData, viewData) + + try + { + @Html.Partial(Model.Provider.ConfigPartialViewName, Model.Provider.ConfigData, viewData) + } + catch (Exception exception) + { +
+ @T("Admin.Configuration.Plugins.UnknownError") +
+
@exception.ToString()
+ } } else { @@ -368,5 +385,14 @@ } }); }); + + // execute export + $('#ProfileExportButton').click(function (e) { + e.preventDefault(); + $(this).doPostData({ + ask: '@T("Admin.Common.AskToProceed")' + }); + return false; + }); }); \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml index 07d42f416f..7e6a01c1ca 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/EditDeployment.cshtml @@ -2,7 +2,7 @@ @using SmartStore.Core.Domain.DataExchange; @using SmartStore.Admin.Models.DataExchange; @{ - var title = string.Concat(T("Admin.Common.Edit"), " ", T("Common.Deployment")); + var title = string.Concat(T("Common.Publishing"), " - ", T("Admin.Common.Edit")); ViewBag.Title = title; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoDeployment.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoDeployment.cshtml deleted file mode 100644 index fce2ac18c7..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoDeployment.cshtml +++ /dev/null @@ -1,54 +0,0 @@ -@using SmartStore.Core.Domain.DataExchange -@using SmartStore.Admin.Models.DataExchange -@model ExportDeploymentModel -@{ - Layout = null; -} -@helper FileDownloadLink(string path, string fileName) -{ -
- @Html.IconForFileExtension(Path.GetExtension(fileName), false) - @fileName -
-} -
-  @Model.DeploymentTypeName - @if (Model.DeploymentType == ExportDeploymentType.FileSystem && Model.IsPublic) - { - (@T("Common.Public")) - } -
-
- @if (Model.DeploymentType == ExportDeploymentType.FileSystem) - { - foreach (var path in Model.ProfileDetails.ExportFiles) - { - @FileDownloadLink(path, Path.GetFileName(path)) - } - - if (Model.ProfileDetails.PublicFiles.Count > 0) - { - foreach (var grp in Model.ProfileDetails.PublicFiles.OrderBy(x => x.StoreId).GroupBy(x => x.StoreId)) - { -

-
@(grp.First().StoreName):
- foreach (var file in grp) - { - - } - } - } - } - else if (Model.DeploymentType == ExportDeploymentType.Email) - { -
@Model.EmailAddresses
- } - else if (Model.DeploymentType == ExportDeploymentType.Http) - { -
@Model.Url
- } - else if (Model.DeploymentType == ExportDeploymentType.Ftp) - { -
@Model.Url
- } -
\ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml index 24b066d505..912809feb9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/InfoProfile.cshtml @@ -8,21 +8,23 @@ {
- @Html.Raw(T("Admin.DataExchange.Export.NoProfilesForProvider", Model.SystemName, Url.Action("List", "Export", new { area = "Admin" }))) +
+ @Html.Raw(T("Admin.DataExchange.Export.NoProfilesForProvider", Model.SystemName, Url.Action("List", "Export", new { area = "Admin" }))) +
- @Html.SmartLabelFor(m => m.Profiles) - - - @profile.Name - -
+ @Html.SmartLabelFor(m => m.Profiles) + + + @profile.Name + +
- - - - - - - - - - - - - - - - - @foreach (var profile in Model) - { - - - - - - - - - - - - - - } - -
@T("Common.Image")@T("Admin.DataExchange.Export.Name")@T("Admin.DataExchange.Export.IsSystemProfile")@T("Common.Provider")@T("Common.Enabled")@T("Admin.DataExchange.Export.EntityType")@T("Admin.DataExchange.Export.FileExtension")@T("Admin.DataExchange.Export.ExportFiles")@T("Admin.System.ScheduleTasks.LastStart")@T("Admin.System.ScheduleTasks.NextRun")@T("Admin.Common.Actions")
- @profile.Provider.FriendlyName - - @profile.Name - @if (profile.SystemName.HasValue()) - { -
@profile.SystemName
- } -
- @Html.SymbolForBool(profile.IsSystemProfile) - - @if (profile.Provider.ConfigurationUrl.HasValue()) - { - @profile.Provider.FriendlyName.NaIfEmpty() - } - else - { - @profile.Provider.FriendlyName.NaIfEmpty() - } -
@profile.ProviderSystemName
-
- @Html.SymbolForBool(profile.Enabled) - - @profile.Provider.EntityTypeName.NaIfEmpty() - - @Html.IconForFileExtension(profile.Provider.FileExtension, true) - - @Html.Partial("ProfileFileCount", profile.Details) - -
- @Html.Partial("~/Administration/Views/ScheduleTask/_LastRun.cshtml", profile.TaskModel) -
-
-
- @Html.Partial("~/Administration/Views/ScheduleTask/_NextRun.cshtml", profile.TaskModel) -
-
-
- - - - @T("Common.Cancel") - - - -  @T("Admin.Configuration.ActivityLog") - -
-} -else -{ -
- - @T("Admin.DataExchange.Export.NoProfiles") -
-} +
+ @Html.Partial("_ProfileList", Model.Where(x => !x.IsSystemProfile), new ViewDataDictionary { { "IsSystemProfile", false } }) + @Html.Partial("_ProfileList", Model.Where(x => x.IsSystemProfile), new ViewDataDictionary { { "IsSystemProfile", true } }) +
- } + + @helper FileNamePatternDescription() { @@ -186,7 +252,7 @@ else -
@Model.FileNamePatternDescriptions.SafeGet(7)
+
} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Filter.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Filter.cshtml index a55b17962c..eac0b67326 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Filter.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Filter.cshtml @@ -103,8 +103,8 @@ @Html.ListBoxFor(x => x.Filter.CategoryIds, - new MultiSelectList(Model.Filter.AvailableCategories, "Value", "Text"), - new { multiple = "multiple" }) + new MultiSelectList(Model.Filter.AvailableCategories, "Value", "Text"), + new { multiple = "multiple" }) @Html.ValidationMessageFor(x => x.Filter.CategoryIds) @@ -145,6 +145,96 @@ } + else if (Model.Provider.EntityType == ExportEntityType.Customer) + { + + + @Html.SmartLabelFor(x => x.Filter.IsActiveCustomer) + + + @Html.EditorFor(x => x.Filter.IsActiveCustomer) + @Html.ValidationMessageFor(x => x.Filter.IsActiveCustomer) + + + + + @Html.SmartLabelFor(x => x.Filter.IsTaxExempt) + + + @Html.EditorFor(x => x.Filter.IsTaxExempt) + @Html.ValidationMessageFor(x => x.Filter.IsTaxExempt) + + + + + @Html.SmartLabelFor(x => x.Filter.CustomerRoleIds) + + + @Html.ListBoxFor(x => x.Filter.CustomerRoleIds, + new MultiSelectList(Model.Filter.AvailableCustomerRoles, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) + @Html.ValidationMessageFor(x => x.Filter.CustomerRoleIds) + + + + + @Html.SmartLabelFor(x => x.Filter.BillingCountryIds) + + + @Html.ListBoxFor(x => x.Filter.BillingCountryIds, + new MultiSelectList(Model.Filter.AvailableCountries, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) + @Html.ValidationMessageFor(x => x.Filter.BillingCountryIds) + + + + + @Html.SmartLabelFor(x => x.Filter.ShippingCountryIds) + + + @Html.ListBoxFor(x => x.Filter.ShippingCountryIds, + new MultiSelectList(Model.Filter.AvailableCountries, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) + @Html.ValidationMessageFor(x => x.Filter.ShippingCountryIds) + + + + + @Html.SmartLabelFor(x => x.Filter.LastActivityFrom) + + + @Html.EditorFor(x => x.Filter.LastActivityFrom) + @Html.ValidationMessageFor(x => x.Filter.LastActivityFrom) + + + + + @Html.SmartLabelFor(x => x.Filter.LastActivityTo) + + + @Html.EditorFor(x => x.Filter.LastActivityTo) + @Html.ValidationMessageFor(x => x.Filter.LastActivityTo) + + + + + @Html.SmartLabelFor(x => x.Filter.HasSpentAtLeastAmount) + + + @Html.EditorFor(x => x.Filter.HasSpentAtLeastAmount) + @Html.ValidationMessageFor(x => x.Filter.HasSpentAtLeastAmount) + + + + + @Html.SmartLabelFor(x => x.Filter.HasPlacedAtLeastOrders) + + + @Html.EditorFor(x => x.Filter.HasPlacedAtLeastOrders) + @Html.ValidationMessageFor(x => x.Filter.HasPlacedAtLeastOrders) + + + } else if (Model.Provider.EntityType == ExportEntityType.Order) { if (Model.StoreCount > 1) @@ -183,8 +273,8 @@ @Html.ListBoxFor(x => x.Filter.OrderStatusIds, - new MultiSelectList(Model.Filter.AvailableOrderStates, "Value", "Text"), - new { multiple = "multiple", @class = "control-xlarge" }) + new MultiSelectList(Model.Filter.AvailableOrderStates, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) @Html.ValidationMessageFor(x => x.Filter.OrderStatusIds) @@ -194,8 +284,8 @@ @Html.ListBoxFor(x => x.Filter.PaymentStatusIds, - new MultiSelectList(Model.Filter.AvailablePaymentStates, "Value", "Text"), - new { multiple = "multiple", @class = "control-xlarge" }) + new MultiSelectList(Model.Filter.AvailablePaymentStates, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) @Html.ValidationMessageFor(x => x.Filter.PaymentStatusIds) @@ -205,20 +295,39 @@ @Html.ListBoxFor(x => x.Filter.ShippingStatusIds, - new MultiSelectList(Model.Filter.AvailableShippingStates, "Value", "Text"), - new { multiple = "multiple", @class = "control-xlarge" }) + new MultiSelectList(Model.Filter.AvailableShippingStates, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) @Html.ValidationMessageFor(x => x.Filter.ShippingStatusIds) + } + else if (Model.Provider.EntityType == ExportEntityType.NewsLetterSubscription) + { - @Html.SmartLabelFor(x => x.Filter.CustomerRoleIds) + @Html.SmartLabelFor(x => x.Filter.IsActiveSubscriber) - @Html.ListBoxFor(x => x.Filter.CustomerRoleIds, - new MultiSelectList(Model.Filter.AvailableCustomerRoles, "Value", "Text"), - new { multiple = "multiple", @class = "control-xlarge" }) - @Html.ValidationMessageFor(x => x.Filter.CustomerRoleIds) + @Html.EditorFor(x => x.Filter.IsActiveSubscriber) + @Html.ValidationMessageFor(x => x.Filter.IsActiveSubscriber) + + + + + @Html.SmartLabelFor(x => x.Filter.CreatedFrom) + + + @Html.EditorFor(x => x.Filter.CreatedFrom) + @Html.ValidationMessageFor(x => x.Filter.CreatedFrom) + + + + + @Html.SmartLabelFor(x => x.Filter.CreatedTo) + + + @Html.EditorFor(x => x.Filter.CreatedTo) + @Html.ValidationMessageFor(x => x.Filter.CreatedTo) } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Projection.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Projection.cshtml index 02337cf8b9..ebe842977e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Projection.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Export/_Tab.Projection.cshtml @@ -96,7 +96,7 @@ @Html.SmartLabelFor(x => x.Projection.PriceType) - @Html.DropDownListFor(x => x.Projection.PriceType, Model.Projection.AvailablePriceTypes, Model.UnspecifiedString) + @Html.DropDownListFor(x => x.Projection.PriceType, Model.Projection.AvailablePriceTypes, T("Admin.Catalog.Products.Fields.Price")) @Html.ValidationMessageFor(x => x.Projection.PriceType) @@ -145,6 +145,16 @@ } + + + @Html.SmartLabelFor(x => x.Projection.NumberOfPictures) + + + @Html.EditorFor(x => x.Projection.NumberOfPictures) + @Html.ValidationMessageFor(x => x.Projection.NumberOfPictures) + + + if (Model.Provider.Feature.HasFlag(ExportFeatures.CanIncludeMainPicture)) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml index fdcacfa7ea..96727f6ecb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/GiftCard/List.cshtml @@ -1,9 +1,6 @@ @model GiftCardListModel @using Telerik.Web.Mvc.UI @{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - //page title ViewBag.Title = T("Admin.GiftCards").Text; } @using (Html.BeginForm()) @@ -20,34 +17,36 @@
- - - - - - - - - - - + +
- @Html.SmartLabelFor(model => model.ActivatedId) - - @Html.DropDownListFor(model => model.ActivatedId, Model.ActivatedList) -
- @Html.SmartLabelFor(model => model.CouponCode) - - @Html.EditorFor(model => model.CouponCode) -
+ + + + + + + + + + + + - -
+ @Html.SmartLabelFor(model => model.ActivatedId) + + @Html.DropDownListFor(model => model.ActivatedId, Model.ActivatedList, T("Admin.Common.All")) +
+ @Html.SmartLabelFor(model => model.CouponCode) + + @Html.EditorFor(model => model.CouponCode) +
  - -
-

-

+ +
+ +

+ + +
@@ -75,7 +74,7 @@ .HeaderTemplate("{0}".FormatWith(T("Admin.Common.Edit").Text)) .Filterable(false); }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) .DataBinding(dataBinding => dataBinding.Ajax().Select("GiftCardList", "GiftCard", Model)) .PreserveGridState() .EnableCustomBinding(true)) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml index 066d52d62c..d8f6f806a1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Home/About.cshtml @@ -49,16 +49,16 @@
  • @MITLicense()
  • @Apache2License()
  • @@ -175,19 +175,19 @@
  • @MITLicense()
  • @MITLicense()
  • @@ -211,7 +211,7 @@
  • - NuGet + NuGet
    @Apache2License()
  • @@ -266,12 +266,12 @@
  • - @GPLv3License() + @Apache2License()
  • diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml new file mode 100644 index 0000000000..1adf73df3b --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/Edit.cshtml @@ -0,0 +1,43 @@ +@using SmartStore.Admin.Models.DataExchange; +@model ImportProfileModel +@{ + var title = string.Concat(T("Common.Profile"), " - ", T("Admin.Common.Edit")); + ViewBag.Title = title; +} + +@using (Html.BeginForm()) +{ +
    +
    +
    + + @title - @Model.Name @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List") +
    +
    + @if (Model.Id != 0) + { + + } + @if (Model.LogFileExists) + { + +  @T("Admin.Configuration.ActivityLog") + + } + + + +
    +
    + @Html.Partial("_Update", Model) +
    +} +@Html.DeleteConfirmation("import-profile-delete") \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml new file mode 100644 index 0000000000..d97745db61 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/List.cshtml @@ -0,0 +1,256 @@ +@using SmartStore.Core.Domain.DataExchange; +@using SmartStore.Admin.Models.DataExchange; +@model ImportProfileListModel +@{ + Html.AddScriptParts("~/bundles/fileupload"); + Html.AddCssFileParts("~/css/fileupload"); + + ViewBag.Title = T("Common.Import").Text; +} + +
    +
    + + @T("Common.Import") +
    + +
    + +@if(Model.Profiles.Any()) +{ + + + + + + + + + + + + + + @foreach (var profile in Model.Profiles) + { + + + + + + + + + + } + +
    @T("Admin.DataExchange.Import.Name")@T("Admin.Common.Entity")@T("Admin.Common.ImportFiles")@T("Admin.DataExchange.Import.LastImportResult")@T("Admin.System.ScheduleTasks.LastStart")@T("Admin.System.ScheduleTasks.NextRun")@T("Admin.Common.Actions")
    + @profile.Name + + @profile.EntityTypeName + + @foreach (var fileName in profile.ExistingFileNames) + { +
      +
    • + @Html.IconForFileExtension(Path.GetExtension(fileName), "fa-li", false) + @fileName +
    • +
    + } +
    + @if (profile.ImportResult != null) + { + @Html.Partial("ProfileImportResult", profile.ImportResult) + } + else + { + @("".NaIfEmpty()) + } + +
    + @Html.Partial("~/Administration/Views/ScheduleTask/_LastRun.cshtml", profile.TaskModel) +
    +
    +
    + @Html.Partial("~/Administration/Views/ScheduleTask/_NextRun.cshtml", profile.TaskModel) +
    +
    +
    + + + + @T("Common.Cancel") + + + +  @T("Admin.Configuration.ActivityLog") + +
    +} +else +{ +
    + + @T("Admin.DataExchange.Import.NoProfiles") +
    +} + +
    + +
    + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/ProfileImportResult.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/ProfileImportResult.cshtml new file mode 100644 index 0000000000..03d0192fab --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/ProfileImportResult.cshtml @@ -0,0 +1,17 @@ +@using SmartStore.Services.DataExchange.Import; +@model SerializableImportResult +@{ + Layout = null; +} +
    + @T("Admin.Common.TotalRows"): @Model.TotalRecords · + @T("Admin.Common.Skipped"): @Model.SkippedRecords +
    +
    + @T("Admin.Common.NewRecords"): @Model.NewRecords · + @T("Admin.Common.Updated"): @Model.ModifiedRecords +
    +
    + @T("Admin.Common.Errors"): @Model.Errors · + @T("Admin.Common.Warnings"): @Model.Warnings +
    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml new file mode 100644 index 0000000000..be30b9c8e5 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/_ColumnMappings.cshtml @@ -0,0 +1,180 @@ +@using SmartStore.Admin.Models.DataExchange; +@model ImportProfileModel +@{ + var delete = T("Common.Delete"); + var ignore = @T("Admin.Common.Ignore"); + var selectTargetProperty = T("Admin.DataExchange.Import.SelectTargetProperty"); +} + +
    + + + + + + + +
    +
    +
    @T("Admin.DataExchange.Import.ColumnMapping")
    +
    +
    +
    + + @Html.Raw(T("Admin.DataExchange.ColumnMapping.Note")) +
    + @if (Model.ColumnMappings.Any()) + { + + } +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    + + + + + + + + + +
    +
    + +
    + + + + + +
    + +
    +
    +
    + +
    + +
    + +
    +
    + + + + +
    +
    + +
    + + + + + + + + + + + @foreach (var mapping in Model.ColumnMappings) + { + var index = Model.ColumnMappings.IndexOf(mapping) + 1; + + + + + + + } + +
    @T("Admin.DataExchange.ColumnMapping.EntityProperty")@T("Admin.DataExchange.ColumnMapping.ImportField")@T("Admin.DataExchange.ColumnMapping.DefaultValue") 
    +
    + +
    + + + + @if (mapping.PropertyDescription.Length > Model.MaxMappingLabelTextLength) + { + @(mapping.PropertyDescription.Substring(0, Model.MaxMappingLabelTextLength))… + } + else + { + @mapping.PropertyDescription + } + @mapping.Property +
    +
    +
    +
    + +
    + + @if (mapping.Column.HasValue()) + { + @mapping.Column + } + else + { + @ignore + } + +
    +
    +
    + + + +
    +
    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Import/_Update.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Import/_Update.cshtml new file mode 100644 index 0000000000..e15b6534fe --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Import/_Update.cshtml @@ -0,0 +1,534 @@ +@using SmartStore.Admin.Models.DataExchange; +@using SmartStore.Core.Domain.DataExchange; +@model ImportProfileModel +@{ + Html.AddScriptParts("~/bundles/fileupload"); + Html.AddCssFileParts("~/css/fileupload"); +} + +@Html.ValidationSummary(false) +@Html.HiddenFor(model => model.Id) +@Html.HiddenFor(model => model.TempFileName) +@Html.HiddenFor(model => model.EntityType) + +@if (Model.ExistingFileNames.Count > 1) +{ +
    + + @T("Admin.DataExchange.Import.MultipleFilesSameFileTypeNote") +
    +} + +
    + + + + + + + + + + @if (Model.ScheduleTaskId > 0) + { + + + + + } + @if (Model.ImportResult != null) + { + + + + + } + + + + + + + + + + + + + + + + + + + + @if (Model.KeyFieldNames != null && Model.KeyFieldNames.Contains("Id", StringComparer.OrdinalIgnoreCase)) + { + + + + } + + + + + + + + + + + + + @if (Model.EntityType == ImportEntityType.Product) + { + + + + + } +
    +   + + +
    + + + @T(Model.ExistingFileNames.Any() ? "Admin.DataExchange.Import.AddAnotherFile" : "Admin.DataExchange.Import.FileUpload") + + + + + +
    +
    +
    +
    +
     
    +
    +
    + +
    +   + + @foreach (var fileName in Model.ExistingFileNames) + { +

    + @Html.IconForFileExtension(Path.GetExtension(fileName), false) + @fileName + + +

    + } +
    + @Html.SmartLabelFor(x => x.ScheduleTaskId) + + @Html.Action("MinimalTask", "ScheduleTask", new { taskId = Model.ScheduleTaskId, returnUrl = Request.RawUrl, cancellable = true, reloadPage = true, area = "admin" }) +
    + @Html.SmartLabelFor(x => x.ImportResult) + + @Html.Partial("ProfileImportResult", Model.ImportResult) +
     
    + @Html.SmartLabelFor(x => x.Enabled) + + @Html.EditorFor(x => x.Enabled) + @Html.ValidationMessageFor(x => x.Enabled) +
    + @Html.SmartLabelFor(x => x.EntityTypeName) + + @Html.DisplayFor(x => x.EntityTypeName) +
    + @Html.SmartLabelFor(x => x.Name) + + @Html.EditorFor(x => x.Name) + @Html.ValidationMessageFor(x => x.Name) +
    + @Html.SmartLabelFor(model => model.KeyFieldNames) + + @*ListBoxFor is insufficient here because the user cannot influence the stored order*@ + + @Html.ValidationMessageFor(model => model.KeyFieldNames) +
    +
    + @T("Admin.DataExchange.Import.KeyFieldNames.Note") +
    +
    + @Html.SmartLabelFor(x => x.Skip) + + @Html.EditorFor(x => x.Skip) + @Html.ValidationMessageFor(x => x.Skip) +
    + @Html.SmartLabelFor(x => x.Take) + + @Html.EditorFor(x => x.Take) + @Html.ValidationMessageFor(x => x.Take) +
    + @Html.SmartLabelFor(x => x.UpdateOnly) + + @Html.EditorFor(x => x.UpdateOnly) + @Html.ValidationMessageFor(x => x.UpdateOnly) +
    + @Html.SmartLabelFor(x => x.ExtraData.NumberOfPictures) + + @Html.EditorFor(x => x.ExtraData.NumberOfPictures) + @Html.ValidationMessageFor(x => x.ExtraData.NumberOfPictures) +
    +
    + +@if (Model.CsvConfiguration != null) +{ + var viewData = new ViewDataDictionary(); + viewData.Add("ShowGroupCaption", true); + viewData.TemplateInfo.HtmlFieldPrefix = "CsvConfiguration"; + viewData.ModelState.Merge(ViewData.ModelState); + @Html.Partial("CsvConfiguration", Model.CsvConfiguration, viewData) +} + +@if (Model.ExistingFileNames.Any()) +{ + @Html.Partial("_ColumnMappings") +} + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml index 362dd5f5a6..91c1fe8140 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Language/List.cshtml @@ -1,7 +1,6 @@ @model Telerik.Web.Mvc.GridModel @using Telerik.Web.Mvc.UI @{ - //page title ViewBag.Title = T("Admin.Configuration.Languages").Text; }
    @@ -24,7 +23,7 @@ columns.Bound(x => x.Name) .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })); columns.Bound(x => x.LanguageCulture) - .Width(150) + .Width(200) .Centered(); columns.Template( @
    @@ -34,10 +33,10 @@ .Centered() .Title(T("Admin.Configuration.Languages.Resources.View").Text); columns.Bound(x => x.DisplayOrder) - .Width(100) + .Width(200) .Centered(); columns.Bound(x => x.Published) - .Width(100) + .Width(200) .Template(item => @Html.SymbolForBool(item.Published)) .ClientTemplate(@Html.SymbolForBool("Published")) .Centered(); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Language/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Language/_CreateOrUpdate.cshtml index 621e963481..7f393dd8c5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Language/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Language/_CreateOrUpdate.cshtml @@ -1,9 +1,7 @@ -@model LanguageModel +@using SmartStore.Web.Framework.UI; +@model LanguageModel -@using Telerik.Web.Mvc.UI; -@using SmartStore.Web.Framework.UI; - -@Html.ValidationSummary(true) +@Html.ValidationSummary(false) @Html.HiddenFor(model => model.Id) @Html.SmartStore().TabStrip().Name("language-edit").Style(TabsStyle.Tabs).Position(TabsPosition.Top).Items(x => { @@ -16,20 +14,7 @@ @helper TabInfo() { - var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.SpecificCultures).ToList(); - - - +
    @@ -54,7 +38,7 @@ @Html.SmartLabelFor(model => model.UniqueSeoCode) @@ -63,20 +47,15 @@ @Html.SmartLabelFor(model => model.FlagImageFileName) @@ -118,9 +97,40 @@ toggleStoreMapping(); - $("#pnlAllFlags a.flag").click(function () { - $("#FlagImageFileName").val($(this).data("filename")); + // init flags select + $('#LanguageEditTable').find('select[name=FlagImageFileName]').select2({ + allowClear: true, + minimumResultsForSearch: 16, + formatResult: flagSelectItemFormatting, + formatSelection: flagSelectItemFormatting }); + + function flagSelectItemFormatting(item) { + var flagUrl = '@Url.Content("~/Content/Images/flags/")'; + + try { + if (item.text.length > 0) { + var option = $(item.element), + html = '
    '; + + // item.element is undefined for selection formatting + if (option.length === 0) { + option = $('select[name="FlagImageFileName"]').find('option[value="' + item.id + '"]'); + } + + if (option.length !== 0) { + html += '
    '; + } + + html += '' + item.text + '
    '; + + return html; + } + } + catch (e) { } + + return item.text; + } }); @@ -132,8 +142,8 @@ $('#pnl-available-stores').hide(); } } - +
    @Html.SmartLabelFor(model => model.Name) @@ -44,8 +29,7 @@ @Html.SmartLabelFor(model => model.LanguageCulture) - @Html.DropDownListFor(x => x.LanguageCulture, cultures.Select(x => - new SelectListItem { Value = x.IetfLanguageTag, Text = string.Format("{0} [{1}]", x.DisplayName, x.IetfLanguageTag) })) + @Html.DropDownListFor(x => x.LanguageCulture, Model.AvailableCultures) @Html.ValidationMessageFor(model => model.LanguageCulture)
    - @Html.EditorFor(model => model.UniqueSeoCode) + @Html.DropDownListFor(x => x.UniqueSeoCode, Model.AvailableTwoLetterLanguageCodes) @Html.ValidationMessageFor(model => model.UniqueSeoCode)
    - @Html.EditorFor(model => model.FlagImageFileName) + @Html.ValidationMessageFor(model => model.FlagImageFileName) -
    - @T("Admin.Common.Show") -
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml index fbd9a6d127..05d898f309 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Log/List.cshtml @@ -21,7 +21,8 @@ - + +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml index c5f8fccc99..eeb3816f1c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/List.cshtml @@ -19,23 +19,35 @@ @Html.Widget("admin_button_toolbar_after") -
    @Html.SmartLabelFor(model => model.CreatedOnFrom) @@ -51,7 +52,7 @@ @Html.SmartLabelFor(model => model.LogLevelId) - @Html.DropDownList("LogLevelId", Model.AvailableLogLevels) + @Html.DropDownList("LogLevelId", Model.AvailableLogLevels, T("Admin.Common.All"))
    - - - - - + +
    - @Html.SmartLabelFor(model => model.SearchManufacturerName) - - @Html.EditorFor(model => Model.SearchManufacturerName) -
    + + + + + @if (Model.AvailableStores.Count > 1) + { + + + + + } + - - + +
    + @Html.SmartLabelFor(model => model.SearchManufacturerName) + + @Html.EditorFor(model => Model.SearchManufacturerName) +
    + @Html.SmartLabelFor(model => model.SearchStoreId) + + @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All")) +
      - -
    + +

    @@ -57,11 +69,9 @@ .HtmlAttributes(new { style = "text-align:center" }) .HeaderHtmlAttributes(new { style = "text-align:center" }); columns.Bound(x => x.Name) - .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })) .ClientTemplate("\"><#= Name #>"); columns.Bound(x => x.Published) .Width(160) - .Template(item => @Html.SymbolForBool(item.Published)) .ClientTemplate(@Html.SymbolForBool("Published")) .Centered(); columns.Bound(x => x.DisplayOrder) @@ -69,8 +79,8 @@ .Centered(); columns.Bound(x => x.LimitedToStores) .Width(160) - .Template(item => @Html.SymbolForBool(item.LimitedToStores)) .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) + .Hidden(Model.AvailableStores.Count <= 1) .Centered(); }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) @@ -82,57 +92,58 @@
    - } - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
    -   - - -
    -

    -

    - - - - - - - -
    - @(Html.Telerik().Grid(Model.Products.Data) - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .Template(x => string.Format("", x.Id)) - .ClientTemplate("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Name) - .Template(x => @Html.LabeledProductName(0, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.Total(Model.Products.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("ProductAddPopupList", "Manufacturer")) - .ClientEvents(events => events.OnDataBinding("onDataBinding")) - .EnableCustomBinding(true)) -
    -

    - -
    - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml index 44385835cb..41f3f1b8c8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml @@ -15,7 +15,24 @@ EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "manufacturer-edit", this.Html, this.Model)); }) @helper TabInfo() - { +{ + if (Model.Id != 0) + { + + + + + +
    + @Html.SmartLabelFor(model => model.Id) + + @Html.DisplayFor(model => model.Id) + @Html.ValidationMessageFor(model => model.Id) +
    + +

    + } + @(Html.LocalizedEditor("manufacturer-info-localized", @ @@ -63,7 +80,8 @@
    - )) +)) +
    @@ -254,83 +272,63 @@
    } @helper TabProducts() - { +{ if (Model.Id > 0) { - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - - - - - - -
    - @(Html.Telerik().Grid() - .Name("products-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("ProductList", "Manufacturer", new { manufacturerId = Model.Id }) - .Update("ProductUpdate", "Manufacturer") - .Delete("ProductDelete", "Manufacturer"); - }) - .Columns(columns => - { - columns.Bound(x => x.ProductName) - .ReadOnly() - .Template(x => @Html.LabeledProductName(x.ProductId, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); - columns.Bound(x => x.Sku) - .ReadOnly(); - columns.Bound(x => x.Published) - .ReadOnly() - .Width(100) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.IsFeaturedProduct) - .Width(100) - .Template(item => @Html.SymbolForBool(item.IsFeaturedProduct)) - .ClientTemplate(@Html.SymbolForBool("IsFeaturedProduct")) - .Centered(); - columns.Bound(x => x.DisplayOrder1) - .Width(100) - .Centered(); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }) - .Width(180); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .PreserveGridState() - .EnableCustomBinding(true)) -
    - - - -
    + + + + + + + +
    + @(Html.Telerik().Grid() + .Name("products-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("ProductList", "Manufacturer", new { manufacturerId = Model.Id }) + .Update("ProductUpdate", "Manufacturer") + .Delete("ProductDelete", "Manufacturer"); + }) + .Columns(columns => + { + columns.Bound(x => x.ProductName) + .ReadOnly() + .Width(520) + .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); + columns.Bound(x => x.Sku) + .ReadOnly(); + columns.Bound(x => x.Published) + .ReadOnly() + .ClientTemplate(@Html.SymbolForBool("Published")) + .Centered(); + columns.Bound(x => x.IsFeaturedProduct) + .ClientTemplate(@Html.SymbolForBool("IsFeaturedProduct")) + .Centered(); + columns.Bound(x => x.DisplayOrder1) + .Centered(); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .Width(220); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .PreserveGridState() + .EnableCustomBinding(true)) +
    + +
    } else { @@ -344,7 +342,6 @@ $(document).ready(function () { $("#@Html.FieldIdFor(model => model.LimitedToStores)").click(toggleStoreMapping); - toggleStoreMapping(); }); @@ -357,8 +354,8 @@ $('#pnl-available-stores').hide(); } } - + - @if (Model.StoreCount > 1) + @if (Model.AvailableStores.Count > 1) { - @if (Model.StoreCount > 1) + @if (Model.AvailableStores.Count > 1) {
    @@ -393,9 +390,36 @@ } - -@*import emails form*@ -@{Html.SmartStore().Window() - .Name("importcsv-window") - .Title(T("Admin.Common.ImportFromCsv").Text) - .Content(@ - @using (Html.BeginForm("ImportCsv", "NewsLetterSubscription", FormMethod.Post, new { enctype = "multipart/form-data" })) - { -
    - @T("Admin.Common.CsvFile"): -
    - } -
    ) - .FooterContent(@ - - ) - .Width(400) - .Modal(true) - .Visible(false) - .Render(); -} - - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrder.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrder.cshtml index 574db39957..2c99ed1a86 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrder.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/AddProductToOrder.cshtml @@ -2,7 +2,6 @@ @{ var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - //page title ViewBag.Title = string.Format(T("Admin.Orders.Products.AddNew.Title1").Text, Model.OrderId); } @using Telerik.Web.Mvc.UI; @@ -19,7 +18,7 @@ @T("Admin.Orders.Products.AddNew.Note1") - +
    @@ -41,7 +40,7 @@ @Html.SmartLabelFor(model => model.SearchManufacturerId) @@ -49,7 +48,7 @@ @Html.SmartLabelFor(model => model.SearchProductTypeId) @@ -63,7 +62,9 @@
    @Html.SmartLabelFor(model => model.SearchProductName) @@ -33,7 +32,7 @@ @Html.SmartLabelFor(model => model.SearchCategoryId) - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) + @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All"))
    - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) + @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All"))
    - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) + @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes, T("Admin.Common.All"))
    +

    +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/BestsellersReport.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/BestsellersReport.cshtml index c6fb84b696..e035acfa1e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/BestsellersReport.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/BestsellersReport.cshtml @@ -1,7 +1,6 @@ @model BestsellersReportModel @using Telerik.Web.Mvc.UI @{ - //page title ViewBag.Title = T("Admin.SalesReport.Bestsellers").Text; } @using (Html.BeginForm()) @@ -14,7 +13,8 @@
    - + +
    @@ -44,7 +44,7 @@ @Html.SmartLabelFor(model => model.PaymentStatusId) @@ -56,8 +56,9 @@
    @Html.SmartLabelFor(model => model.StartDate) @@ -36,7 +36,7 @@ @Html.SmartLabelFor(model => model.OrderStatusId) - @Html.DropDownList("OrderStatusId", Model.AvailableOrderStatuses) + @Html.DropDownList("OrderStatusId", Model.AvailableOrderStatuses, T("Admin.Common.All"))
    - @Html.DropDownList("PaymentStatusId", Model.AvailablePaymentStatuses) + @Html.DropDownList("PaymentStatusId", Model.AvailablePaymentStatuses, T("Admin.Common.All"))
    -

    -

    + +

    + + + + + + +
    @@ -106,5 +107,4 @@ } - } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml index efaef854a2..75fefead92 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/Edit.cshtml @@ -159,6 +159,9 @@ } + + @Html.Widget("order_edit_top") + + + + +
    @@ -889,6 +892,14 @@
    + @Html.SmartLabelFor(model => model.AcceptThirdPartyEmailHandOver) + + @(Model.AcceptThirdPartyEmailHandOver ? T("Common.Yes") : T("Common.No")) +
    @if (Model.CustomerComment.HasValue()) @@ -1750,16 +1761,21 @@
    -
    -

    @T("Admin.Orders.OrderNotes.AddTitle")

    -
    + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml index 3e50f7232c..8cc2132d70 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml @@ -1,10 +1,10 @@ @model OrderListModel @using Telerik.Web.Mvc.UI @{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - var hideProfitReport = false; - - ViewBag.Title = T("Admin.Orders").Text; + var hideProfitReport = false; + var allString = T("Admin.Common.All").Text; + + ViewBag.Title = T("Admin.Orders").Text; } @using (Html.BeginForm()) @@ -39,39 +39,40 @@ @Html.Widget("admin_button_toolbar_after") -
    +
    +
    @T("Admin.Orders.OrderNotes.AddTitle")
    +
    +
    @Html.SmartLabelFor(model => model.AddOrderNoteMessage) - @Html.TextAreaFor(model => model.AddOrderNoteMessage, new { style = "Width: 350px; Height: 150px;" }) + @Html.TextAreaFor(model => model.AddOrderNoteMessage, new { @class = "control-xlarge", style = "height: 120px;" }) @Html.ValidationMessageFor(model => model.AddOrderNoteMessage)
    - - - - - - - - - - - - - - - - + +
    - @Html.SmartLabelFor(model => model.StartDate) - - @Html.EditorFor(model => model.StartDate) -
    - @Html.SmartLabelFor(model => model.EndDate) - - @Html.EditorFor(model => Model.EndDate) -
    - @Html.SmartLabelFor(model => model.CustomerName) - - @Html.EditorFor(model => Model.CustomerName) -
    - @Html.SmartLabelFor(model => model.CustomerEmail) - - @Html.EditorFor(model => Model.CustomerEmail) -
    + + + + + + + + + + + + + + + + - - - + + - - - - - - - - - + + - - - + + + + @if (Model.AvailableStores.Count > 1) + { + + + + + } - - - - - - + + + + + + + + + - - - - - -
    + @Html.SmartLabelFor(model => model.StartDate) + + @Html.EditorFor(model => model.StartDate) +
    + @Html.SmartLabelFor(model => model.EndDate) + + @Html.EditorFor(model => Model.EndDate) +
    + @Html.SmartLabelFor(model => model.CustomerName) + + @Html.EditorFor(model => Model.CustomerName) +
    + @Html.SmartLabelFor(model => model.CustomerEmail) + + @Html.EditorFor(model => Model.CustomerEmail) +
    @Html.SmartLabelFor(model => model.OrderStatusIds) @@ -80,69 +81,72 @@ @Html.DropDownList("OrderStatusIds", Model.AvailableOrderStatuses, null, new { multiple = "multiple" })
    - @Html.SmartLabelFor(model => model.PaymentStatusIds) - +
    + @Html.SmartLabelFor(model => model.PaymentStatusIds) + @Html.DropDownList("PaymentStatusIds", Model.AvailablePaymentStatuses, null, new { multiple = "multiple" }) -
    - @Html.SmartLabelFor(model => model.ShippingStatusIds) - - @Html.DropDownList("ShippingStatusIds", Model.AvailableShippingStatuses, null, new { multiple = "multiple" }) -
    - @Html.SmartLabelFor(model => model.StoreId) - - @Html.DropDownList("StoreId", Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.OrderGuid) - - @Html.EditorFor(model => Model.OrderGuid) -
    + @Html.SmartLabelFor(model => model.ShippingStatusIds) + + @Html.DropDownList("ShippingStatusIds", Model.AvailableShippingStatuses, null, new { multiple = "multiple" }) +
    + @Html.SmartLabelFor(model => model.StoreId) + + @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores, allString) +
    - @Html.SmartLabelFor(model => model.OrderNumber) - - @Html.EditorFor(model => Model.OrderNumber) -
    - @Html.SmartLabelFor(model => model.GoDirectlyToNumber) - - @Html.EditorFor(model => Model.GoDirectlyToNumber) - + @Html.SmartLabelFor(model => model.OrderGuid) + + @Html.EditorFor(model => Model.OrderGuid) +
    + @Html.SmartLabelFor(model => model.OrderNumber) + + @Html.EditorFor(model => Model.OrderNumber) +
    + @Html.SmartLabelFor(model => model.GoDirectlyToNumber) + + @Html.EditorFor(model => Model.GoDirectlyToNumber) + -
    -   - - -
    +
    +   + + +

    @@ -150,29 +154,31 @@
    @(Html.Telerik().Grid() - .Name("orders-grid") - .ClientEvents(events => events - .OnDataBinding("onDataBinding") - .OnDataBound("onDataBound") - .OnComplete("onComplete")) - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }) - .HeaderHtmlAttributes(new { style = "text-align:center" }); + .Name("orders-grid") + .ClientEvents(events => events + .OnDataBinding("onDataBinding") + .OnDataBound("onDataBound") + .OnComplete("onComplete")) + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .Width(50) + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.OrderNumber) + columns.Bound(x => x.OrderNumber) .ClientTemplate(@Html.LabeledOrderNumber()); - columns.Bound(x => x.OrderStatus); - columns.Bound(x => x.PaymentStatus); - columns.Bound(x => x.ShippingStatus); - columns.Bound(x => x.CustomerEmail); - columns.Bound(x => x.StoreName); - columns.Bound(x => x.CreatedOn); - columns.Bound(x => x.OrderTotal) + columns.Bound(x => x.OrderStatus); + columns.Bound(x => x.PaymentStatus); + columns.Bound(x => x.ShippingStatus); + columns.Bound(x => x.CustomerName); + columns.Bound(x => x.CustomerEmail); + columns.Bound(x => x.StoreName) + .Hidden(Model.AvailableStores.Count <= 1); + columns.Bound(x => x.CreatedOn); + columns.Bound(x => x.OrderTotal) .Width(180) .RightAlign() .FooterTemplate( @@ -184,141 +190,135 @@ (hideProfitReport ? "" : @T("Admin.Orders.Report.Profit").Text), @T("Admin.Orders.Report.Tax").Text, @T("Admin.Orders.Report.Total").Text)); - columns.Bound(x => x.Id) - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", new { id = x.Id })) - .ClientTemplate("\">" + T("Admin.Common.View").Text + "") - .Width(50) - .Centered() - .Title(T("Admin.Common.View").Text); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("OrderList", "Order")) + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("OrderList", "Order")) .PreserveGridState() - .EnableCustomBinding(true)) + .EnableCustomBinding(true))
    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Payment/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Payment/Edit.cshtml index ab5a7d7a15..5dd49c46d2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Payment/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Payment/Edit.cshtml @@ -129,10 +129,15 @@ } function getFilterConfiguration(url, container) { - $.get(url, function (html) { - container.append(html); - if (++i < configUrls.length) - getFilterConfiguration(configUrls[i], container); + $.ajax({ + url: url, + cache: false, + dataType: 'html', + success: function (html) { + container.append(html); + if (++i < configUrls.length) + getFilterConfiguration(configUrls[i], container); + } }); } }); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigurePlugin.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigurePlugin.cshtml index 2122382080..74f552e016 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigurePlugin.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigurePlugin.cshtml @@ -20,7 +20,18 @@ @if (Model.ConfigurationRoute != null && Model.ConfigurationRoute.Action.HasValue()) { - @Html.Action(Model.ConfigurationRoute.Action, Model.ConfigurationRoute.Controller, Model.ConfigurationRoute.RouteValues); + try + { + @Html.Action(Model.ConfigurationRoute.Action, Model.ConfigurationRoute.Controller, Model.ConfigurationRoute.RouteValues); + } + catch (Exception exception) + { +
    + @T("Admin.Configuration.Plugins.UnknownError") +
    + +
    @exception.ToString()
    + } } else { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigureProvider.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigureProvider.cshtml index d61b5d78d5..74edd98572 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigureProvider.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Plugin/ConfigureProvider.cshtml @@ -23,7 +23,18 @@ @if (Model.ConfigurationRoute != null && Model.ConfigurationRoute.Action.HasValue()) { - @Html.Action(Model.ConfigurationRoute.Action, Model.ConfigurationRoute.Controller, Model.ConfigurationRoute.RouteValues); + try + { + @Html.Action(Model.ConfigurationRoute.Action, Model.ConfigurationRoute.Controller, Model.ConfigurationRoute.RouteValues); + } + catch (Exception exception) + { +
    + @T("Admin.Configuration.Plugins.UnknownError") +
    + +
    @exception.ToString()
    + } } else { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Plugin/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Plugin/List.cshtml index 6458d88ec1..e8c60696f4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Plugin/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Plugin/List.cshtml @@ -289,6 +289,10 @@ btnExecuteTasks = $('#btn-execute-tasks'); // Execute filtering + setTimeout(function() { + $('#filter-query').focus(); + }, 800); + var mainContainer = $('#local-plugins'); $('#filter-query').on('keyup', function (e) { var el = $(this); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/AssociatedProductAddPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/AssociatedProductAddPopup.cshtml deleted file mode 100644 index b5fd79b0e4..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/AssociatedProductAddPopup.cshtml +++ /dev/null @@ -1,147 +0,0 @@ -@{ - Layout = "~/Administration/Views/Shared/_AdminPopupLayout.cshtml"; -} -@model ProductModel.AddAssociatedProductModel -@{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - ViewBag.Title = T("Admin.Catalog.Products.AssociatedProducts.AddNew").Text; -} - -@using Telerik.Web.Mvc.UI; - -@using (Html.BeginForm()) -{ -
    -
    - @T("Admin.Catalog.Products.AssociatedProducts.AddNew") -
    -
    - - if (ViewBag.RefreshPage == true) - { - - } - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
    -   - - -
    - -

    - - - - - - - - -
    - @(Html.Telerik().Grid() - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .Template(x => string.Format("", x.Id)) - .ClientTemplate("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Name) - .Template(x => @Html.LabeledProductName(0, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("AssociatedProductAddPopupList", "Product")) - .ClientEvents(events => events.OnDataBinding("associatedProducts_onDataBinding")) - .EnableCustomBinding(true)) -
    -

    - -
    - - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml index 6eb786b6ba..ebdea889fa 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml @@ -3,7 +3,6 @@ @{ var allString = T("Admin.Common.All").Text; - //page title ViewBag.Title = T("Admin.Catalog.BulkEdit").Text; } @using (Html.BeginForm()) @@ -38,7 +37,7 @@ @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes, allString)
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemAddPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemAddPopup.cshtml deleted file mode 100644 index b31b86f3a2..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemAddPopup.cshtml +++ /dev/null @@ -1,167 +0,0 @@ -@{ - Layout = "~/Administration/Views/Shared/_AdminPopupLayout.cshtml"; -} -@model ProductModel.AddBundleItemModel -@{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - ViewBag.Title = T("Admin.Catalog.Products.BundleItems.AddNew").Text; -} - -@using Telerik.Web.Mvc.UI; - -@using (Html.BeginForm()) -{ -
    -
    - @T("Admin.Catalog.Products.BundleItems.AddNew") -
    -
    - -
    - - - @Html.Raw(T("Admin.Catalog.Products.BundleItems.AdminNoteGeneral")) - - @Html.Raw(T("Admin.Catalog.Products.BundleItems." + (Model.IsPerItemPricing ? "AdminNotePerItemPricing" : "AdminNoteBundlePricing"))) - - @if (Model.IsPerItemShipping) - { - @Html.Raw(T("Admin.Catalog.Products.BundleItems.AdminNotePerItemShipping")) - } -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
    -   - - -
    - -

    - - - - - - - - -
    - @(Html.Telerik().Grid() - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center;" }); - columns.Bound(x => x.Name) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("BundleItemAddPopupList", "Product")) - .ClientEvents(events => events.OnDataBinding("bundleItem_onDataBinding")) - .EnableCustomBinding(true)) -
    - -
    - - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/CrossSellProductAddPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/CrossSellProductAddPopup.cshtml deleted file mode 100644 index ac06795dec..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/CrossSellProductAddPopup.cshtml +++ /dev/null @@ -1,147 +0,0 @@ -@{ - Layout = "~/Administration/Views/Shared/_AdminPopupLayout.cshtml"; -} -@model ProductModel.AddCrossSellProductModel -@{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - //page title - ViewBag.Title = T("Admin.Catalog.Products.CrossSells.AddNew").Text; -} -@using Telerik.Web.Mvc.UI; -@using (Html.BeginForm()) -{ -
    -
    - @T("Admin.Catalog.Products.CrossSells.AddNew") -
    -
    - - if (ViewBag.RefreshPage == true) - { - - } - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
    -   - - -
    - -

    - - - - - - - - -
    - @(Html.Telerik().Grid(Model.Products.Data) - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .Template(x => string.Format("", x.Id)) - .ClientTemplate("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Name) - .Template(x => @Html.LabeledProductName(0, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.Total(Model.Products.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("CrossSellProductAddPopupList", "Product")) - .ClientEvents(events => events.OnDataBinding("crossSellProducts_onDataBinding")) - .EnableCustomBinding(true)) -
    -

    - -
    - - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml index 3c268ef816..85c43950dc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/List.cshtml @@ -1,7 +1,6 @@ @model ProductListModel @using Telerik.Web.Mvc.UI @{ - var trueTemplate = "#if (Published) {# #} #"; var allString = T("Admin.Common.All").Text; ViewBag.Title = T("Admin.Catalog.Products.Manage").Text; @@ -24,10 +23,6 @@  @T("Admin.Common.Delete.Selected") - -  @T("Admin.Common.ImportFromExcel") - - @Html.Widget("admin_button_toolbar_after") @@ -49,7 +44,7 @@ @Html.DropDownListFor(model => model.SearchProductTypeId, Model.AvailableProductTypes, allString)
    @@ -175,7 +170,7 @@ .Sortable(false) .Template(item => @Html.SymbolForBool(item.LimitedToStores)) .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) - .Hidden(Model.StoreCount <= 1) + .Hidden(Model.AvailableStores.Count <= 1) .Centered(); columns.Bound(x => x.CreatedOn); columns.Bound(x => x.UpdatedOn) @@ -201,268 +196,114 @@
    -} -@*import products form*@ -@{Html.SmartStore().Window() - .Name("importexcel-window") - .Title(T("Admin.Common.ImportFromExcel").Text) - .Content(@ - @using (Html.BeginForm("ImportExcel", "Product", FormMethod.Post, new { enctype = "multipart/form-data", @class = "form-horizontal" })) - { -
    - -
    - -
    - -
    - -
    -
    - -
    -
    -
    -
    -
    - @Html.Raw(T("Admin.Common.ImportFromExcel.ProcessedCount").ToString().FormatInvariant("0", "0")) - @Html.Raw(T("Admin.Common.ImportFromExcel.ActiveSince").ToString().FormatCurrent("")) -
    -
    - @(Html.Raw(T("Admin.Common.ImportFromExcel.QuickStats").ToString().FormatInvariant( - "0", - "0", - "0", - "0"))) -
    -
    - } - -
    ) - .FooterContent(@ - - @T("Common.Close") - ) - .Modal(true) - .Visible(false) - .Render(); -} - - +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/ProductAttributeValueLinkagePopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/ProductAttributeValueLinkagePopup.cshtml deleted file mode 100644 index 0ef7426f3c..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/ProductAttributeValueLinkagePopup.cshtml +++ /dev/null @@ -1,159 +0,0 @@ -@model ProductModel.ProductVariantAttributeValueModel.AddProductLinkageModel -@{ - Layout = "~/Administration/Views/Shared/_AdminPopupLayout.cshtml"; -} -@{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - ViewBag.Title = T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.LinkedProduct.AddNew").Text; -} - -@using Telerik.Web.Mvc.UI; - -@using (Html.BeginForm()) -{ -
    -
    - @T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.LinkedProduct.AddNew") -
    -
    - - if (ViewBag.RefreshPage == true) - { - - } - - @Html.HiddenFor(model => model.ProductId) - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
    -   - - -
    - -

    - - - - - - - - -
    - @(Html.Telerik().Grid() - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title(T("Admin.Telerik.GridLocalization.Select").Text) - .Width(50); - columns.Bound(x => x.Name) - .Template(x => @Html.LabeledProductName(0, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("ProductAttributeValueLinkagePopupList", "Product")) - .ClientEvents(events => events.OnDataBinding("productLinkage_onDataBinding")) - .EnableCustomBinding(true)) -
    - -
    - - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/RelatedProductAddPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/RelatedProductAddPopup.cshtml deleted file mode 100644 index 7d5db715ca..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/RelatedProductAddPopup.cshtml +++ /dev/null @@ -1,146 +0,0 @@ -@{ - Layout = "~/Administration/Views/Shared/_AdminPopupLayout.cshtml"; -} -@model ProductModel.AddRelatedProductModel -@{ - var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - //page title - ViewBag.Title = T("Admin.Catalog.Products.RelatedProducts.AddNew").Text; -} -@using Telerik.Web.Mvc.UI; -@using (Html.BeginForm()) -{ -
    -
    - @T("Admin.Catalog.Products.RelatedProducts.AddNew") -
    -
    - - if (ViewBag.RefreshPage == true) - { - - } - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, T("Admin.Common.All").Text) -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes) -
    -   - - -
    -

    - - - - - - - - -
    - @(Html.Telerik().Grid(Model.Products.Data) - .Name("products-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .Template(x => string.Format("", x.Id)) - .ClientTemplate("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Name) - .Template(x => @Html.LabeledProductName(0, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName(null, "Name")); - columns.Bound(x => x.Sku); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered() - .Width(100); - }) - .Pageable(settings => settings.Total(Model.Products.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("RelatedProductAddPopupList", "Product")) - .ClientEvents(events => events.OnDataBinding("relatedProducts_onDataBinding")) - .EnableCustomBinding(true)) -
    -

    - -
    - - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml index ebf79c151d..0827ea946f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml @@ -4,83 +4,93 @@ @if (Model.Id > 0) { -
    +
    @T("Admin.Catalog.Products.AssociatedProducts.Note1") @T("Admin.Catalog.Products.AssociatedProducts.Note2")
    - -
    - - - - - - - -
    - @(Html.Telerik().Grid() - .Name("associatedproducts-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("AssociatedProductList", "Product", new { productId = Model.Id }) - .Update("AssociatedProductUpdate", "Product") - .Delete("AssociatedProductDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.ProductName) - .ReadOnly() - .Template(x => @Html.LabeledProductName(x.Id, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName("Id", "ProductName")); - columns.Bound(x => x.Sku) - .ReadOnly(); - columns.Bound(x => x.Published) - .ReadOnly() - .Width(100) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Width(100) - .Centered(); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }) - .Width(180); - }) - .EnableCustomBinding(true)) -
    - - +

    - -
    -
    + }); + + }); + +
  • } else { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml index edab6a805b..1d3bbb0a9e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml @@ -11,7 +11,25 @@ } } - + +
    + +
    + @T("Admin.Catalog.Products.BundleItems.NotesOnProductBundles"): + @T("Common.Show") +
    +
    + @Html.Raw(T("Admin.Catalog.Products.BundleItems.AdminNoteGeneral")) + + @Html.Raw(T(Model.BundlePerItemPricing ? "Admin.Catalog.Products.BundleItems.AdminNotePerItemPricing" : "Admin.Catalog.Products.BundleItems.AdminNoteBundlePricing")) + + @if (Model.BundlePerItemShipping) + { + @Html.Raw(T("Admin.Catalog.Products.BundleItems.AdminNotePerItemShipping")) + } +
    +
    +
    @(Html.LocalizedEditor("product-bundle-localized", @ @@ -70,77 +88,67 @@
    -
    - - - + +
    - @(Html.Telerik().Grid() - .Name("bundleitems-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("BundleItemList", "Product", new { productId = Model.Id }) - .Delete("BundleItemDelete", "Product"); - }) - .Columns(columns => +

    + + + + - - - + + + - -
    + @(Html.Telerik().Grid() + .Name("bundleitems-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("BundleItemList", "Product", new { productId = Model.Id }) + .Delete("BundleItemDelete", "Product"); + }) + .Columns(columns => + { + columns.Bound(x => x.ProductName) + .ReadOnly() + .Width(520) + .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); + columns.Bound(x => x.Sku) + .ReadOnly(); + columns.Bound(x => x.Quantity) + .Centered(); + columns.Bound(x => x.Discount) + .Centered(); + columns.Bound(x => x.Visible) + .ClientTemplate(@Html.SymbolForBool("Visible")) + .Centered(); + columns.Bound(x => x.Published) + .ClientTemplate(@Html.SymbolForBool("Published")) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Centered(); + columns.Bound(x => x.Id) + .Centered() + .ClientTemplate("") + .Title(""); + columns.Command(commands => { - columns.Bound(x => x.ProductName) - .ReadOnly() - .Template(x => @Html.LabeledProductName(x.ProductId, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); - columns.Bound(x => x.Sku) - .ReadOnly(); - columns.Bound(x => x.Quantity) - .Width(80) - .Centered(); - columns.Bound(x => x.Discount) - .Width(80) - .Centered(); - columns.Bound(x => x.Visible) - .Width(100) - .Template(item => @Html.SymbolForBool(item.Visible)) - .ClientTemplate(@Html.SymbolForBool("Visible")) - .Centered(); - columns.Bound(x => x.Published) - .Width(100) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Width(100) - .Centered(); - columns.Bound(x => x.Id) - .Width(90) - .Centered() - .ClientTemplate("") - .Title(""); - columns.Command(commands => - { - commands.Delete().Localize(T); - }) - .Width(90); - }) - .ClientEvents(events => events.OnRowDataBound("onRowDataBound_bundleItems")) - .EnableCustomBinding(true)) -
    - + commands.Delete().Localize(T); + }); + }) + .ClientEvents(events => events.OnRowDataBound("onRowDataBound_bundleItems")) + .EnableCustomBinding(true)) +
    + - -
    - + +
    } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml index 23e3aa1efb..694263f00a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml @@ -6,13 +6,15 @@ { } } else { - @T("Admin.Catalog.Products.Discounts.NoDiscounts") +
    + @T("Admin.Catalog.Products.Discounts.NoDiscounts") +
    } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml index 8b4f0e6dba..8d295edfb2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml @@ -34,10 +34,25 @@ toggleShipping(); toggleTax(); - // show/hide homepage display order + // toggle homepage display order $('#ShowOnHomePage').change(function () { $('#pnlHomePageDisplayOrder').toggle($(this).is(':checked')); - }).trigger('change'); + }).trigger('change'); + + // add required products + $(document).on('click', '#SearchRequiredProductsButton', function () { + $('#@(Html.FieldIdFor(x => x.RequiredProductIds))').entityPicker('loadDialog', { + url: '@Url.Action("EntityPicker", "Common", new { area = "" })', + caption: '@T("Admin.Catalog.Products.Fields.RequireOtherProducts")', + returnValueDelimiter: ',', + onLoadDialogBefore: function () { + $('#SearchRequiredProductsButton').button('loading').prop('disabled', true); + }, + onLoadDialogComplete: function () { + $('#SearchRequiredProductsButton').prop('disabled', false).button('reset'); + } + }); + }); }); function toggleProductType() { @@ -191,14 +206,14 @@
    - @if (Model.Id > 0) + @if (Model.Id != 0) { @@ -465,6 +480,11 @@ @@ -579,7 +599,7 @@ @Html.SmartLabelFor(model => model.UserAgreementText) @@ -598,7 +618,7 @@
    @Html.SmartLabelFor(model => model.Id) - @Model.Id.ToString() + @Html.DisplayFor(model => model.Id) @Html.ValidationMessageFor(model => model.Id)
    @Html.EditorFor(model => model.RequiredProductIds) + + + @Html.ValidationMessageFor(model => model.RequiredProductIds)
    - @Html.TextAreaFor(model => model.UserAgreementText) + @Html.EditorFor(model => model.UserAgreementText, Html.RichEditorFlavor()) @Html.ValidationMessageFor(model => model.UserAgreementText)
    @Html.EditorFor(model => model.SampleDownloadId) - @*@Html.ValidationMessageFor(model => model.SampleDownloadId)*@ + @Html.ValidationMessageFor(model => model.SampleDownloadId)
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml index 7c08977c2b..a8e66f1e74 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml @@ -38,9 +38,7 @@ }) .EnableCustomBinding(true))
    -

    -

    @T("Admin.Catalog.Products.Pictures.AddNew")

    -

    + + + + + - + +
    +
    +
    @T("Admin.Catalog.Products.Pictures.AddNew")
    +
    +
    @Html.SmartLabelFor(model => model.AddPictureModel.PictureId) @@ -92,12 +98,15 @@
    - - +   + + +
    } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml index ce16846d32..bebc6fa0c7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml @@ -16,7 +16,6 @@ fieldBasePriceBaseAmount = $("#@Html.FieldIdFor(model => model.BasePriceBaseAmount)"); fieldBasePriceMeasureUnit.on("change", getCurrentBasePrice); - fieldBasePriceAmount.on("blur", getCurrentBasePrice); fieldBasePriceBaseAmount.on("blur", getCurrentBasePrice); @@ -25,7 +24,9 @@ toggleBasePrice(); function getCurrentBasePrice() { - if (parseFloat(fieldBasePriceAmount.val()) > 0 && parseInt(fieldBasePriceBaseAmount.val()) > 0) { + var basePriceAmount = Globalize.parseFloat(fieldBasePriceAmount.val()); + var basePriceBaseAmount = Globalize.parseInt(fieldBasePriceBaseAmount.val()); + if (basePriceAmount > 0 && basePriceBaseAmount > 0) { $.ajax({ cache:false, type: "POST", @@ -40,7 +41,7 @@ $("#BasePriceAmountInfo").html(data.BasePrice); }, error: function (xhr, ajaxOptions, thrownError){ - alert(thrownError); + //alert(thrownError); } }); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml index f47318846b..cdeda617b9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml @@ -26,56 +26,38 @@ { columns.Bound(x => x.Product2Name) .ReadOnly() - .Template(x => @Html.LabeledProductName(x.ProductId2, x.Product2Name, x.ProductTypeName, x.ProductTypeLabelHint)) + .Width(520) .ClientTemplate(@Html.LabeledProductName("ProductId2", "Product2Name")); columns.Bound(x => x.Product2Sku) .ReadOnly(); columns.Bound(x => x.Product2Published) .ReadOnly() - .Width(100) - .Template(item => @Html.SymbolForBool(item.Product2Published)) .ClientTemplate(@Html.SymbolForBool("Product2Published")) .Centered(); columns.Bound(x => x.DisplayOrder) - .Width(100) .Centered(); columns.Command(commands => { commands.Edit().Localize(T); commands.Delete().Localize(T); - }) - .Width(180); + }).Width(220); }) .EnableCustomBinding(true)) - - + - - - @@ -88,7 +70,9 @@ }
    -
    +

     

    + +

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

    @if (Model.Id > 0) @@ -112,49 +96,32 @@ { columns.Bound(x => x.Product2Name) .ReadOnly() - .Template(x => @Html.LabeledProductName(x.ProductId2, x.Product2Name, x.ProductTypeName, x.ProductTypeLabelHint)) + .Width(520) .ClientTemplate(@Html.LabeledProductName("ProductId2", "Product2Name")); columns.Bound(x => x.Product2Sku); columns.Bound(x => x.Product2Published) - .Width(100) - .Template(item => @Html.SymbolForBool(item.Product2Published)) .ClientTemplate(@Html.SymbolForBool("Product2Published")) .Centered(); columns.Command(commands => { commands.Delete().Localize(T); - }) - .Width(90); + }).Width(220); }) .EnableCustomBinding(true)) - - + + + - - - @@ -169,6 +136,7 @@ \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml index fd1512ae37..87982efc85 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml @@ -1,7 +1,5 @@ -@model ProductModel - -@using Telerik.Web.Mvc.UI; - +@using Telerik.Web.Mvc.UI; +@model ProductModel @{ Layout = ""; } @@ -11,9 +9,6 @@ /*hide "add spec" table if no attributes are defined*/ if (Model.AddSpecificationAttributeModel.AvailableAttributes.Count > 0) { -
    -

    @T("Admin.Catalog.Products.SpecificationAttributes.AddNew")

    -
    - + +
    + + + - - + +
    +
    +
    @T("Admin.Catalog.Products.SpecificationAttributes.AddNew")
    +
    +
    @Html.SmartLabelFor(model => model.AddSpecificationAttributeModel.SpecificationAttributeId) @@ -112,13 +115,15 @@
    - - +   + + +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml index 92de51ade3..9e07dff329 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml @@ -29,12 +29,13 @@ @if (!ViewBag.IsEdit) { -
    +
    @if (Model.Warnings.Count > 0) { -
    - @foreach (var warning in Model.Warnings) { -

     @warning

    +
    + @foreach (var warning in Model.Warnings) + { +

     @warning

    }
    } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml index 0bc012f616..697da14da4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml @@ -36,19 +36,44 @@ } }).trigger('change'); - $('#remove-product-linkage').click(function () { + $('#RemoveProductLinkageButton').click(function () { $('#linked-product-name').empty(); $('#@Html.FieldIdFor(model => model.LinkedProductId)').val('0'); toggleLinkageButtons(); }); toggleLinkageButtons(); + + // add product linkage + $(document).on('click', '#AddProductLinkageButton', function () { + $('#@(Html.FieldIdFor(x => x.LinkedProductId))').entityPicker('loadDialog', { + url: '@Url.Action("EntityPicker", "Common", new { area = "" })', + caption: '@T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.LinkedProduct.AddNew")', + maxReturnValues: 1, + disableIds: '@Model.ProductId', + onLoadDialogBefore: function () { + $('#AddProductLinkageButton').button('loading').prop('disabled', true); + }, + onLoadDialogComplete: function () { + $('#AddProductLinkageButton').prop('disabled', false).button('reset'); + }, + onOkClicked: function (ids) { + var dialog = $('#entity-picker-product-dialog'), + item = dialog.find('.entity-picker-list .selected').first(); + + $('#linked-product-name').html(item.find('.title').attr('title')); + + return true; + } + }); + }); + }); function toggleLinkageButtons() { var unlinked = ($('#@Html.FieldIdFor(model => model.LinkedProductId)').val() === '0'); - $('#add-product-linkage').toggle(unlinked); - $('#remove-product-linkage').toggle(!unlinked); + $('#AddProductLinkageButton').toggle(unlinked); + $('#RemoveProductLinkageButton').toggle(!unlinked); } @@ -70,14 +95,15 @@ @Html.HiddenFor(model => model.LinkedProductId)
    @Model.LinkedProductName
    - - + + @Html.ValidationMessageFor(model => model.LinkedProductId) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml index 6d1cf21a03..dd1c42971f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/List.cshtml @@ -145,8 +145,11 @@ //reload grid var grid = $('#productreviews-grid').data('tGrid'); grid.ajaxRequest(); + //clear selected checkboxes + $('.checkboxGroups').attr('checked', false).change(); + selectedIds = []; }, - error:function (xhr, ajaxOptions, thrownError){ + error:function (xhr, ajaxOptions, thrownError) { alert(thrownError); }, traditional: true diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml index e8f2359885..c9a62c9b5c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml @@ -13,83 +13,88 @@
    - - - - - - - - - - - - - - +
    - @Html.SmartLabelFor(model => model.SearchId) - - @Html.EditorFor(model => Model.SearchId) -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores) -
    - @Html.SmartLabelFor(model => model.SearchReturnRequestStatusId) - - @Html.DropDownListFor(model => model.SearchReturnRequestStatusId, Model.AvailableReturnRequestStatus) -
    + + + + + @if (Model.AvailableStores.Count > 1) + { + + + + + } + + + + + - - + +
    + @Html.SmartLabelFor(model => model.SearchId) + + @Html.EditorFor(model => Model.SearchId) +
    + @Html.SmartLabelFor(model => model.SearchStoreId) + + @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All")) +
    + @Html.SmartLabelFor(model => model.SearchReturnRequestStatusId) + + @Html.DropDownListFor(model => model.SearchReturnRequestStatusId, Model.AvailableReturnRequestStatus, T("Admin.Common.All")) +
      - -
    + +
    +

    @(Html.Telerik().Grid() - .Name("returnrequests-grid") - .Columns(columns => - { + .Name("returnrequests-grid") + .Columns(columns => + { columns.Bound(x => x.Id) .Centered() .Width(80); columns.Bound(x => x.ProductName) .Template(x => @Html.LabeledProductName(x.ProductId, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); - columns.Bound(x => x.Quantity) + columns.Bound(x => x.Quantity) .Width(80) - .Centered(); - columns.Bound(x => x.CustomerId) - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Customer", new { id = x.CustomerId }, new { })) + .Centered(); + columns.Bound(x => x.CustomerId) + .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Customer", new { id = x.CustomerId }, new { })) .ClientTemplate("\"><#= CustomerFullName #>"); - columns.Bound(x => x.OrderId) + columns.Bound(x => x.OrderId) .Width(80) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = x.OrderId }, new { })) + .Centered() + .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = x.OrderId }, new { })) .ClientTemplate("\"><#= OrderId #>"); columns.Bound(x => x.ReturnRequestStatusStr) - .Width(180); - columns.Bound(x => x.CreatedOn) .Width(180); - columns.Bound(x => x.StoreName); - columns.Bound(x => x.Id) + columns.Bound(x => x.CreatedOn) + .Width(180); + columns.Bound(x => x.StoreName) + .Hidden(Model.AvailableStores.Count <= 1); + columns.Bound(x => x.Id) .Width(100) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) - .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") - .Title(T("Admin.Common.Edit").Text); - }) + .Centered() + .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) + .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") + .Title(T("Admin.Common.Edit").Text); + }) .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "ReturnRequest")) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "ReturnRequest")) .ClientEvents(events => events.OnDataBinding("returnRequestGrid_onDataBinding")) .PreserveGridState() - .EnableCustomBinding(true)) + .EnableCustomBinding(true))
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml index 281220e2df..a1018ad9ea 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml @@ -3,7 +3,6 @@ @using SmartStore.Core @{ ViewBag.Title = T("Admin.System.ScheduleTasks.EditTask") + " - " + Model.Name; - //var cronHelpLink = WorkContext.WorkingLanguage.UniqueSeoCode.ToLower() == "de" ? "https://de.wikipedia.org/wiki/Cron" : "https://en.wikipedia.org/wiki/Cron"; var cronHelpLink = GenerateHelpUrl("Managing+Scheduled+Tasks#ManagingScheduledTasks-Cron"); var returnUrl = ((string)ViewBag.ReturnUrl).NullEmpty(); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml index 3723fa5819..c7e498cf2f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml @@ -12,13 +12,13 @@
    - +
    - + @@ -39,13 +39,13 @@ } - - @foreach (var cr in Model.AvailableCustomerRoles) @@ -66,5 +73,4 @@
    @T("Admin.System.ScheduleTasks.Name") @T("Admin.System.ScheduleTasks.Enabled") @T("Admin.System.ScheduleTasks.CronExpression")@T("Admin.System.ScheduleTasks.LastStart")@T("Admin.System.ScheduleTasks.LastStart") @T("Admin.System.ScheduleTasks.NextRun") @T("Admin.Common.Actions")
    +
    @Html.Partial("_LastRun", task)
    +
    @Html.Partial("_NextRun", task)
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml index 5842a4185d..530a08f62e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml @@ -5,6 +5,7 @@ var returnUrl = (string)ViewBag.ReturnUrl; var hasPermission = ViewBag.HasPermission == true; var cancellable = ViewBag.Cancellable == true; + var reloadPage = ViewBag.ReloadPage == true; }
    @@ -33,16 +34,25 @@ onTaskCompleted: function (taskId, el) { el.prev().removeClass('hide'); // .task-info el.next().addClass('hide'); // .btn-cancel-task - $.ajax({ - cache: false, - global: false, - type: 'POST', - url: '@Url.Action("GetMinimalTaskWidget")', - data: { taskId: taskId, returnUrl: '@returnUrl' }, - success: function (data) { - el.prev().html(data); - } - }); + + @if (reloadPage) + { + location.href = '@returnUrl'; + } + else + { + $.ajax({ + cache: false, + global: false, + type: 'POST', + url: '@Url.Action("GetMinimalTaskWidget")', + data: { taskId: taskId, returnUrl: '@returnUrl' }, + success: function (data) { + el.prev().html(data); + } + }); + } + } }); }); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml index 0d02bfd4b2..743c8f9d8f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml @@ -1,6 +1,6 @@ -@model PermissionMappingModel +@using SmartStore.Utilities; +@model PermissionMappingModel @{ - //page title ViewBag.Title = T("Admin.Configuration.ACL").Text; } @using (Html.BeginForm()) @@ -11,7 +11,9 @@ @T("Admin.Configuration.ACL")
    - +
    @@ -19,12 +21,16 @@
    @if (Model.AvailablePermissions.Count == 0) - { - No permissions defined - } - else if (Model.AvailableCustomerRoles.Count == 0) - { - No customer roles available + { +
    + @T("Admin.System.Warnings.NoPermissionsDefined") +
    + } + else if (Model.AvailableCustomerRoles.Count == 0) + { +
    + @T("Admin.System.Warnings.NoCustomerRolesDefined") +
    } else { @@ -48,6 +54,7 @@ {
    + @Inflector.Titleize(pr.Category).NaIfEmpty() @pr.Name
    - } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml index 3e8d9e4a19..56664aafbb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml @@ -30,7 +30,10 @@ $("#@Html.FieldIdFor(model => model.ProductSearchAutoCompleteEnabled)").click(toggleProductSearchAutoComplete); $("#@Html.FieldIdFor(model => model.CompareProductsEnabled)").click(toggleCompareProducts); $("#@Html.FieldIdFor(model => model.EnableHtmlTextCollapser)").click(toggleHtmlTextCollapsedHeight); - $("#@Html.FieldIdFor(model => model.FilterEnabled)").click(toggleFilterEnabled); + + $("#@Html.FieldIdFor(model => model.FilterEnabled)").change(function () { + $('#ProductFilterTable').find('.product-filter-option').toggle($(this).is(':checked')); + }).trigger('change'); toggleShowCategoryProductNumberIncludingSubcategories(); toggleEmailAFriend(); @@ -43,7 +46,6 @@ toggleProductSearchAutoComplete(); toggleCompareProducts(); toggleHtmlTextCollapsedHeight(); - toggleFilterEnabled(); toggleManufacturersOnHomepage(); }); @@ -157,16 +159,6 @@ $('#pnlHtmlTextCollapsedHeight').hide(); } } - - function toggleFilterEnabled() { - if ($('#@Html.FieldIdFor(model => model.FilterEnabled)').is(':checked')) { - $('#pnlMaxFilterItemsToDisplay').show(); - $('#pnlExpandAllFilterCriteria').show(); - } else { - $('#pnlMaxFilterItemsToDisplay').hide(); - $('#pnlExpandAllFilterCriteria').hide(); - } - } @Html.Action("StoreScopeConfiguration", "Setting") @@ -257,8 +249,7 @@ @Html.ValidationMessageFor(model => model.IgnoreFeaturedProducts) - - + @Html.SmartLabelFor(model => model.CompareProductsEnabled) @@ -331,7 +322,33 @@ @Html.ValidationMessageFor(model => model.ShowManufacturerPictures) - + + + @Html.SmartLabelFor(model => model.HideManufacturerDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideManufacturerDefaultPictures) + @Html.ValidationMessageFor(model => model.HideManufacturerDefaultPictures) + + + + + @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) + @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures) + + + + + @Html.SmartLabelFor(model => model.HideProductDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideProductDefaultPictures) + @Html.ValidationMessageFor(model => model.HideProductDefaultPictures) + + @@ -365,257 +382,289 @@ @helper TabProductListSettings() { -
    - @T("Common.Navigation") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    - @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) - - @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) - @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories) -
    - @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) - - @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists) - @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists) -
    - @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumber) - @Html.ValidationMessageFor(model => model.ShowCategoryProductNumber) -
    - @Html.SmartLabelFor(model => model.ShowCategoryProductNumberIncludingSubcategories) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumberIncludingSubcategories) - @Html.ValidationMessageFor(model => model.ShowCategoryProductNumberIncludingSubcategories) -
    - @Html.SmartLabelFor(model => model.CategoryBreadcrumbEnabled) - - @Html.SettingEditorFor(model => model.CategoryBreadcrumbEnabled) - @Html.ValidationMessageFor(model => model.CategoryBreadcrumbEnabled) -
    - @Html.SmartLabelFor(model => model.FilterEnabled) - - @Html.SettingEditorFor(model => model.FilterEnabled) - @Html.ValidationMessageFor(model => model.FilterEnabled) -
    - @Html.SmartLabelFor(model => model.MaxFilterItemsToDisplay) - - @Html.SettingEditorFor(model => model.MaxFilterItemsToDisplay) - @Html.ValidationMessageFor(model => model.MaxFilterItemsToDisplay) -
    - @Html.SmartLabelFor(model => model.ExpandAllFilterCriteria) - - @Html.SettingEditorFor(model => model.ExpandAllFilterCriteria) - @Html.ValidationMessageFor(model => model.ExpandAllFilterCriteria) -
    - @Html.SmartLabelFor(model => model.SubCategoryDisplayType) - - @Html.SettingOverrideCheckbox(model => Model.SubCategoryDisplayType) - @Html.DropDownListFor(model => model.SubCategoryDisplayType, Model.AvailableSubCategoryDisplayTypes) - @Html.ValidationMessageFor(model => model.SubCategoryDisplayType) -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Common.Navigation")
    +
    +
    + @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) + + @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) + @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories) +
    + @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) + + @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists) + @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists) +
    + @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) + + @Html.SettingEditorFor(model => model.ShowCategoryProductNumber) + @Html.ValidationMessageFor(model => model.ShowCategoryProductNumber) +
    + @Html.SmartLabelFor(model => model.ShowCategoryProductNumberIncludingSubcategories) + + @Html.SettingEditorFor(model => model.ShowCategoryProductNumberIncludingSubcategories) + @Html.ValidationMessageFor(model => model.ShowCategoryProductNumberIncludingSubcategories) +
    + @Html.SmartLabelFor(model => model.CategoryBreadcrumbEnabled) + + @Html.SettingEditorFor(model => model.CategoryBreadcrumbEnabled) + @Html.ValidationMessageFor(model => model.CategoryBreadcrumbEnabled) +
    + @Html.SmartLabelFor(model => model.SubCategoryDisplayType) + + @Html.SettingOverrideCheckbox(model => Model.SubCategoryDisplayType) + @Html.DropDownListFor(model => model.SubCategoryDisplayType, Model.AvailableSubCategoryDisplayTypes) + @Html.ValidationMessageFor(model => model.SubCategoryDisplayType) +
    - -
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Filtering.FilterResults")
    +
    +
    + @Html.SmartLabelFor(model => model.FilterEnabled) + + @Html.SettingEditorFor(model => model.FilterEnabled) + @Html.ValidationMessageFor(model => model.FilterEnabled) +
    + @Html.SmartLabelFor(model => model.MaxFilterItemsToDisplay) + + @Html.SettingEditorFor(model => model.MaxFilterItemsToDisplay) + @Html.ValidationMessageFor(model => model.MaxFilterItemsToDisplay) +
    + @Html.SmartLabelFor(model => model.ExpandAllFilterCriteria) + + @Html.SettingEditorFor(model => model.ExpandAllFilterCriteria) + @Html.ValidationMessageFor(model => model.ExpandAllFilterCriteria) +
    + @Html.SmartLabelFor(model => model.SortFilterResultsByMatches) + + @Html.SettingEditorFor(model => model.SortFilterResultsByMatches) + @Html.ValidationMessageFor(model => model.SortFilterResultsByMatches) +
    -
    - @T("Common.List") - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.AllowProductSorting) - - @Html.SettingEditorFor(model => model.AllowProductSorting) - @Html.ValidationMessageFor(model => model.AllowProductSorting) -
    - @Html.SmartLabelFor(model => model.DefaultViewMode) - - @Html.SettingOverrideCheckbox(model => Model.DefaultViewMode) - @Html.DropDownListFor(model => model.DefaultViewMode, Model.AvailableDefaultViewModes) - @Html.ValidationMessageFor(model => model.DefaultViewMode) -
    - @Html.SmartLabelFor(model => model.DefaultSortOrder) - - @Html.SettingOverrideCheckbox(model => Model.DefaultSortOrder) - @Html.DropDownListFor(model => model.DefaultSortOrder, Model.AvailableSortOrderModes) - @Html.ValidationMessageFor(model => model.DefaultSortOrder) -
    - @Html.SmartLabelFor(model => model.AllowProductViewModeChanging) - - @Html.SettingEditorFor(model => model.AllowProductViewModeChanging) - @Html.ValidationMessageFor(model => model.AllowProductViewModeChanging) -
    - @Html.SmartLabelFor(model => model.DefaultPageSizeOptions) - - @Html.SettingEditorFor(model => model.DefaultPageSizeOptions) - @Html.ValidationMessageFor(model => model.DefaultPageSizeOptions) -
    - @Html.SmartLabelFor(model => model.PriceDisplayType) - - @Html.SettingOverrideCheckbox(model => Model.PriceDisplayType) - @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes) - @Html.ValidationMessageFor(model => model.PriceDisplayType) -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Common.List")
    +
    +
    + @Html.SmartLabelFor(model => model.AllowProductSorting) + + @Html.SettingEditorFor(model => model.AllowProductSorting) + @Html.ValidationMessageFor(model => model.AllowProductSorting) +
    + @Html.SmartLabelFor(model => model.DefaultViewMode) + + @Html.SettingOverrideCheckbox(model => Model.DefaultViewMode) + @Html.DropDownListFor(model => model.DefaultViewMode, Model.AvailableDefaultViewModes) + @Html.ValidationMessageFor(model => model.DefaultViewMode) +
    + @Html.SmartLabelFor(model => model.DefaultSortOrder) + + @Html.SettingOverrideCheckbox(model => Model.DefaultSortOrder) + @Html.DropDownListFor(model => model.DefaultSortOrder, Model.AvailableSortOrderModes) + @Html.ValidationMessageFor(model => model.DefaultSortOrder) +
    + @Html.SmartLabelFor(model => model.AllowProductViewModeChanging) + + @Html.SettingEditorFor(model => model.AllowProductViewModeChanging) + @Html.ValidationMessageFor(model => model.AllowProductViewModeChanging) +
    + @Html.SmartLabelFor(model => model.DefaultPageSizeOptions) + + @Html.SettingEditorFor(model => model.DefaultPageSizeOptions) + @Html.ValidationMessageFor(model => model.DefaultPageSizeOptions) +
    + @Html.SmartLabelFor(model => model.PriceDisplayType) + + @Html.SettingOverrideCheckbox(model => Model.PriceDisplayType) + @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes) + @Html.ValidationMessageFor(model => model.PriceDisplayType) +
    -
    - @T("Admin.Catalog.Products") - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductLists) - - @Html.SettingEditorFor(model => model.ShowDeliveryTimesInProductLists) - @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductLists) -
    - @Html.SmartLabelFor(model => model.ShowBasePriceInProductLists) - - @Html.SettingEditorFor(model => model.ShowBasePriceInProductLists) - @Html.ValidationMessageFor(model => model.ShowBasePriceInProductLists) -
    - @Html.SmartLabelFor(model => model.ShowColorSquaresInLists) - - @Html.SettingEditorFor(model => model.ShowColorSquaresInLists) - @Html.ValidationMessageFor(model => model.ShowColorSquaresInLists) -
    - @Html.SmartLabelFor(model => model.HideBuyButtonInLists) - - @Html.SettingEditorFor(model => model.HideBuyButtonInLists) - @Html.ValidationMessageFor(model => model.HideBuyButtonInLists) -
    - @Html.SmartLabelFor(model => model.LabelAsNewForMaxDays) - - @Html.SettingEditorFor(model => model.LabelAsNewForMaxDays) - @Html.ValidationMessageFor(model => model.LabelAsNewForMaxDays) -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Admin.Catalog.Products")
    +
    +
    + @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductLists) + + @Html.SettingEditorFor(model => model.ShowDeliveryTimesInProductLists) + @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductLists) +
    + @Html.SmartLabelFor(model => model.ShowBasePriceInProductLists) + + @Html.SettingEditorFor(model => model.ShowBasePriceInProductLists) + @Html.ValidationMessageFor(model => model.ShowBasePriceInProductLists) +
    + @Html.SmartLabelFor(model => model.ShowColorSquaresInLists) + + @Html.SettingEditorFor(model => model.ShowColorSquaresInLists) + @Html.ValidationMessageFor(model => model.ShowColorSquaresInLists) +
    + @Html.SmartLabelFor(model => model.HideBuyButtonInLists) + + @Html.SettingEditorFor(model => model.HideBuyButtonInLists) + @Html.ValidationMessageFor(model => model.HideBuyButtonInLists) +
    + @Html.SmartLabelFor(model => model.LabelAsNewForMaxDays) + + @Html.SettingEditorFor(model => model.LabelAsNewForMaxDays) + @Html.ValidationMessageFor(model => model.LabelAsNewForMaxDays) +
    -
    - @T("Admin.Catalog.ProductTags") - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.NumberOfProductTags) - - @Html.SettingEditorFor(model => model.NumberOfProductTags) - @Html.ValidationMessageFor(model => model.NumberOfProductTags) -
    - @Html.SmartLabelFor(model => model.ProductsByTagPageSize) - - @Html.SettingEditorFor(model => model.ProductsByTagPageSize) - @Html.ValidationMessageFor(model => model.ProductsByTagPageSize) -
    - @Html.SmartLabelFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) - - @Html.SettingEditorFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) - @Html.ValidationMessageFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) -
    - @Html.SmartLabelFor(model => model.ProductsByTagPageSizeOptions) - - @Html.SettingEditorFor(model => model.ProductsByTagPageSizeOptions) - @Html.ValidationMessageFor(model => model.ProductsByTagPageSizeOptions) -
    -
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Admin.Catalog.ProductTags")
    +
    +
    + @Html.SmartLabelFor(model => model.NumberOfProductTags) + + @Html.SettingEditorFor(model => model.NumberOfProductTags) + @Html.ValidationMessageFor(model => model.NumberOfProductTags) +
    + @Html.SmartLabelFor(model => model.ProductsByTagPageSize) + + @Html.SettingEditorFor(model => model.ProductsByTagPageSize) + @Html.ValidationMessageFor(model => model.ProductsByTagPageSize) +
    + @Html.SmartLabelFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) + + @Html.SettingEditorFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) + @Html.ValidationMessageFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) +
    + @Html.SmartLabelFor(model => model.ProductsByTagPageSizeOptions) + + @Html.SettingEditorFor(model => model.ProductsByTagPageSizeOptions) + @Html.ValidationMessageFor(model => model.ProductsByTagPageSizeOptions) +
    } @helper TabUserSettings() { - +
    - @@ -465,9 +538,9 @@ - + - + - - -
    @Html.SmartLabelFor(model => model.ShowProductReviewsInProductLists) @@ -693,7 +742,7 @@ @helper TabProductDetailSettings() { - +
    + + + + - - + @@ -169,8 +168,8 @@ @Html.ValidationMessageFor(model => model.NotifyAboutPrivateMessages) - - + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml index 60c57ccd7f..5162311aec 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml @@ -27,8 +27,8 @@ $("#@Html.FieldIdFor(model => model.StoreInformationSettings.StoreClosed)").click(toggleStoreClosed); $("#@Html.FieldIdFor(model => model.CaptchaSettings.Enabled)").change(function () { $('#SecuritySettingTable').find('.captcha-setting').toggle($(this).is(':checked')); - }).trigger('change'); - $("#@Html.FieldIdFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled)").click(toggleSeoLanguageUrls); + }).trigger('change'); + $("#@Html.FieldIdFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled)").click(toggleSeoLanguageUrls); toggleStoreClosed(); toggleSeoLanguageUrls(); @@ -36,6 +36,17 @@ $("#@Html.FieldIdFor(model => model.SocialSettings.ShowSocialLinksInFooter)").change(function () { $('#SocialLinkTable').find('.social-link').toggle($(this).is(':checked')); }).trigger('change'); + + // test creation of SEO names + $('#TestSeoNameCreationButton').click(function () { + $(this).closest('form').doAjax({ + type: 'POST', + url: '@Url.Action("TestSeoNameCreation")', + callbackSuccess: function (resp) { + $('#TestSeoNameCreationResult').text(resp); + } + }); + }); }); function toggleStoreClosed() { @@ -148,15 +159,24 @@ @Html.ValidationMessageFor(model => model.SeoSettings.DefaultMetaDescription) - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @Html.SmartLabelFor(model => model.RecentlyViewedProductsEnabled) @@ -766,6 +815,15 @@ @Html.ValidationMessageFor(model => model.DisplayAllImagesNumber)
    + @Html.SmartLabelFor(model => model.ShowManufacturerPicturesInProductDetail) + + @Html.SettingEditorFor(model => model.ShowManufacturerPicturesInProductDetail) + @Html.ValidationMessageFor(model => model.ShowManufacturerPicturesInProductDetail) +
    @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductDetail) @@ -834,8 +892,7 @@ } @helper TabProductSearchSettings() { - - +
    - + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml index b22985819c..ccb0eb2876 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml @@ -3,7 +3,6 @@ @using SmartStore.Core.Domain.Customers; @using SmartStore.Core.Domain.Security; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.CustomerUser").Text; } @using (Html.BeginForm()) @@ -44,6 +43,10 @@ toggleAvatar(); toggleUsername(); + + $('#@Html.FieldIdFor(model => model.CustomerSettings.CustomerNumberMethod)').change(function () { + $('#pnlCustomerNumberVisibility').toggle($(this).val() !== '@((int)CustomerNumberMethod.Disabled)'); + }).trigger('change'); }); function toggleAvatar() { @@ -77,29 +80,6 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.UsernamesEnabled) - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + +
    @Html.SmartLabelFor(model => model.SearchPageProductsPerPage) @@ -873,7 +930,16 @@
    + + @Html.SmartLabelFor(model => model.SearchDescriptions) + + @Html.SettingEditorFor(model => model.SearchDescriptions) + @Html.ValidationMessageFor(model => model.SearchDescriptions) +

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

    +

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

    + + @(Html.LocalizedEditor("setting-shopping-cart-localized", + @ + + + + + + + +
    + @Html.SmartLabelFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel) + + @Html.TextAreaFor(model => Model.Locales[item].ThirdPartyEmailHandOverLabel, new { @class = "input-large" }) + @Html.ValidationMessageFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel) +
    + @Html.HiddenFor(model => model.Locales[item].LanguageId) +
    + , + @ + + + + +
    + @Html.SmartLabelFor(model => model.ThirdPartyEmailHandOverLabel) + + @Html.TextAreaFor(model => model.ThirdPartyEmailHandOverLabel, new { @class = "input-large" }) + @Html.ValidationMessageFor(model => model.ThirdPartyEmailHandOverLabel) +
    + )) +
    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml index 150886779f..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()) @@ -150,11 +149,6 @@ @Html.ValidationMessageFor(model => model.HideTaxInOrderSummary) - - -
    - - @Html.SmartLabelFor(model => model.ShowLegalHintsInProductList) @@ -182,8 +176,8 @@ @Html.ValidationMessageFor(model => model.ShowLegalHintsInFooter) - - + +
    @@ -197,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") - - + +
    @@ -243,8 +237,8 @@ @Html.ValidationMessageFor(model => model.ShippingTaxClassId) - - + +
    @@ -276,8 +270,8 @@ @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeTaxClassId) - - + +
    @@ -336,6 +330,5 @@ @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/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/Navbar.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml index 5188aa5b37..eae6d73f32 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml @@ -1,6 +1,7 @@ @using SmartStore.Core.Domain.Stores; @using SmartStore.Services.Customers; @using SmartStore.Services.Stores; +@using SmartStore.Core; @{ var currentCustomer = this.WorkContext.CurrentCustomer; @@ -11,7 +12,7 @@ string communityUrl = (currentLanguageCode == "de" ? "http://community.smartstore.com" : "http://community.smartstore.com/index.php?/&k=880ea6a14ea49e853634fbdc5015a024&setlanguage=1&cal_id=&langid=1"); - string docsUrl = "http://docs.smartstore.com/display/SMNET25/SmartStore.NET+Documentation+Home"; + string docsUrl = GenerateHelpUrl(""); var checkUpdateResult = ViewBag.CheckUpdateResult as CheckUpdateResult; } @@ -33,7 +34,7 @@
    - + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml index a66ddaadb3..f5a5e22b33 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml @@ -3,15 +3,15 @@ @using SmartStore.Web.Framework.UI; @{ - var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; - - // page title - string adminPageTitle = ""; - if (!string.IsNullOrWhiteSpace(ViewBag.Title)) - { - adminPageTitle = ViewBag.Title + " / "; - } - adminPageTitle += T("Admin.PageTitle").Text; + var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; + + // page title + string adminPageTitle = ""; + if (!string.IsNullOrWhiteSpace(ViewBag.Title)) + { + adminPageTitle = ViewBag.Title + " / "; + } + adminPageTitle += T("Admin.PageTitle").Text; // add css assets var telerikCssRootPath = "~/Content/2012.2.607/"; @@ -20,77 +20,79 @@ telerikCssRootPath + "telerik.common.css", "~/Content/jquery.pnotify.default.css", "~/Content/jquery.pnotify.default.icons.css", + "~/Content/smartstore.entitypicker.css", "~/Administration/Content/theme.less"); - // add js assets (Head) - Html.AppendScriptParts(ResourceLocation.Head, - "~/Scripts/modernizr-2.7.2.js", + // add js assets (Head) + Html.AppendScriptParts(ResourceLocation.Head, + "~/Scripts/modernizr-2.7.2.js", // Telerik doesn't like jQuery 1.9+, a shame! - "~/Administration/Scripts/jquery-1.8.3.js", - "~/Scripts/globalize/globalize.js", + "~/Administration/Scripts/jquery-1.8.3.js", + "~/Scripts/globalize/globalize.js", "~/Scripts/smartstore.globalize.adapter.js"); - - Html.AddScriptParts(ResourceLocation.Head, true, - "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); - // add js assets (Foot) - var bootstrapJsRoot = "~/Content/bootstrap/js/"; - Html.AppendScriptParts(ResourceLocation.Foot, - // jQuery UI Core - "~/Scripts/jquery-ui/effect.js", - "~/Scripts/jquery-ui/effect-transfer.js", - "~/Scripts/jquery-ui/effect-shake.js", - "~/Scripts/jquery-ui/position.js", - // jQuery Validation - "~/Scripts/jquery.unobtrusive-ajax.js", - "~/Scripts/jquery.validate.js", - "~/Scripts/jquery.validate.unobtrusive.js", - // SmartStore system - "~/Scripts/smartstore.system.js", - "~/Scripts/underscore.js", - "~/Scripts/underscore.string.js", - "~/Scripts/underscore.mixins.js", - "~/Scripts/smartstore.jquery.utils.js", - "~/Scripts/jquery.ba-outside-events.js", - "~/Scripts/jquery.preload.js", - "~/Scripts/jquery.transit.js", - "~/Scripts/jquery.menu-aim.js", - "~/Scripts/smartstore.doAjax.js", - "~/Scripts/jquery.addeasing.js", - "~/Scripts/smartstore.eventbroker.js", - "~/Scripts/smartstore.hacks.js", + Html.AddScriptParts(ResourceLocation.Head, true, + "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); + + // add js assets (Foot) + var bootstrapJsRoot = "~/Content/bootstrap/js/"; + Html.AppendScriptParts(ResourceLocation.Foot, + // jQuery UI Core + "~/Scripts/jquery-ui/effect.js", + "~/Scripts/jquery-ui/effect-transfer.js", + "~/Scripts/jquery-ui/effect-shake.js", + "~/Scripts/jquery-ui/position.js", + // jQuery Validation + "~/Scripts/jquery.unobtrusive-ajax.js", + "~/Scripts/jquery.validate.js", + "~/Scripts/jquery.validate.unobtrusive.js", + // SmartStore system + "~/Scripts/smartstore.system.js", + "~/Scripts/underscore.js", + "~/Scripts/underscore.string.js", + "~/Scripts/underscore.mixins.js", + "~/Scripts/smartstore.jquery.utils.js", + "~/Scripts/jquery.ba-outside-events.js", + "~/Scripts/jquery.preload.js", + "~/Scripts/jquery.transit.js", + "~/Scripts/jquery.menu-aim.js", + "~/Scripts/smartstore.doAjax.js", + "~/Scripts/smartstore.entityPicker.js", + "~/Scripts/jquery.addeasing.js", + "~/Scripts/smartstore.eventbroker.js", + "~/Scripts/smartstore.hacks.js", "~/Scripts/smartstore.common.js", - // Bootstrap - bootstrapJsRoot + "bootstrap-transition.js", - bootstrapJsRoot + "bootstrap-alert.js", - bootstrapJsRoot + "bootstrap-button.js", - bootstrapJsRoot + "bootstrap-collapse.js", - bootstrapJsRoot + "bootstrap-dropdown.js", - bootstrapJsRoot + "bootstrap-modal.js", - bootstrapJsRoot + "bootstrap-tooltip.js", - bootstrapJsRoot + "bootstrap-popover.js", - bootstrapJsRoot + "bootstrap-tab.js", + // Bootstrap + bootstrapJsRoot + "bootstrap-transition.js", + bootstrapJsRoot + "bootstrap-alert.js", + bootstrapJsRoot + "bootstrap-button.js", + bootstrapJsRoot + "bootstrap-collapse.js", + bootstrapJsRoot + "bootstrap-dropdown.js", + bootstrapJsRoot + "bootstrap-modal.js", + bootstrapJsRoot + "bootstrap-tooltip.js", + bootstrapJsRoot + "bootstrap-popover.js", + bootstrapJsRoot + "bootstrap-tab.js", bootstrapJsRoot + "bootstrap-typeahead.js", - // Bootstrap custom - bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", - // Shared UI - "~/Scripts/smartstore.placeholder.js", - "~/Scripts/select2.js", - "~/Scripts/smartstore.selectwrapper.js", - "~/Scripts/smartstore.throbber.js", - "~/Scripts/smartstore.navbar.js", - "~/Scripts/smartstore.thumbzoomer.js", - "~/Scripts/smartstore.column-equalizer.js", - "~/Scripts/smartstore.shrinkmenu.js", - "~/Scripts/smartstore.scrollbutton.js", - "~/Scripts/smartstore.tabselector.js", - "~/Scripts/jquery.pnotify.js", - "~/Scripts/jquery.scrollTo.js", - "~/Scripts/jquery.serialScroll.js", + // Bootstrap custom + bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", + // Shared UI + "~/Scripts/smartstore.placeholder.js", + "~/Scripts/select2.js", + "~/Scripts/smartstore.selectwrapper.js", + "~/Scripts/smartstore.throbber.js", + "~/Scripts/smartstore.navbar.js", + "~/Scripts/smartstore.thumbzoomer.js", + "~/Scripts/smartstore.column-equalizer.js", + "~/Scripts/smartstore.shrinkmenu.js", + "~/Scripts/smartstore.scrollbutton.js", + "~/Scripts/smartstore.tabselector.js", + "~/Scripts/jquery.pnotify.js", + "~/Scripts/jquery.scrollTo.js", + "~/Scripts/jquery.serialScroll.js", "~/Scripts/jquery.sortable.js", - // Admin - "~/Administration/Scripts/admin.common.js", - "~/Administration/Scripts/admin.globalinit.js"); + // Admin + "~/Administration/Scripts/admin.common.js", + "~/Administration/Scripts/admin.globalinit.js"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml index f5161a54ae..8d63b6e67b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml @@ -87,7 +87,13 @@ @helper TabRestrictions() { - if (Model.FilterConfigurationUrls.Count == 0) + if (Model.Id == 0) + { +
    + @T("Admin.Configuration.Restriction.SaveBeforeEdit") +
    + } + else if (Model.FilterConfigurationUrls.Count == 0) {
    @T("Admin.Configuration.Shipping.Methods.RestrictionNote") @@ -109,10 +115,15 @@ } function getFilterConfiguration(url, container) { - $.get(url, function (html) { - container.append(html); - if (++i < configUrls.length) - getFilterConfiguration(configUrls[i], container); + $.ajax({ + url: url, + cache: false, + dataType: 'html', + success: function (html) { + container.append(html); + if (++i < configUrls.length) + getFilterConfiguration(configUrls[i], container); + } }); } }); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml index e0d957c60f..15e975c4ce 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml @@ -230,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 08de2f50ab..18fda90102 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml @@ -16,26 +16,29 @@
    - - - - - - - - - -
    - @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")) +
    +   + + +
    +}

    diff --git a/src/Presentation/SmartStore.Web/Administration/Web.config b/src/Presentation/SmartStore.Web/Administration/Web.config index 82720de3ee..7f12a993a8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Web.config +++ b/src/Presentation/SmartStore.Web/Administration/Web.config @@ -64,7 +64,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Administration/packages.config b/src/Presentation/SmartStore.Web/Administration/packages.config index 2f861314d9..7e1b1648ae 100644 --- a/src/Presentation/SmartStore.Web/Administration/packages.config +++ b/src/Presentation/SmartStore.Web/Administration/packages.config @@ -15,6 +15,6 @@ - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/sitemap.config b/src/Presentation/SmartStore.Web/Administration/sitemap.config index 573d1227b0..a1501d7653 100644 --- a/src/Presentation/SmartStore.Web/Administration/sitemap.config +++ b/src/Presentation/SmartStore.Web/Administration/sitemap.config @@ -94,6 +94,7 @@ + @@ -127,7 +128,9 @@ - + + + @@ -144,7 +147,6 @@ - diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml index cc58de88a2..8e379a8055 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml @@ -135,6 +135,9 @@ Keine Aufträge + + Dieser Auftrag konnte Ihnen nicht zugeordnet werden. + Auftragsdatum @@ -489,6 +492,15 @@ Registrierung + + Der Kunde ist bereits registriert. + + + Eine Suchmaschine kann nicht registriert werden. + + + Das Konto einer geplanten Aufgabe kann nicht registriert werden. + Die eingegebene E-Mail-Adresse wird bereits verwendet. @@ -597,6 +609,9 @@ Hersteller ('{0}') gelöscht + + Auftrag {0} gelöscht + Produkt ('{0}') gelöscht @@ -1365,6 +1380,9 @@ Die Warengruppe wurde gelöscht. + + Andere Beschreibung anzeigen + Rabatte @@ -1411,10 +1429,10 @@ Gelöscht - Beschreibung + Obere Beschreibung - Beschreibung der Warengruppe + Beschreibung der Warengruppe, die auf der Warengruppenseite oberhalb der Produkte angezeigt wird. Reihenfolge @@ -1473,6 +1491,9 @@ Legt die Vater-Warengruppe fest. Lassen Sie das Feld leer, um eine Warengruppe erster Ebene zu erzeugen. + + Übergeordnete Warengruppe + Bild @@ -1930,6 +1951,9 @@ Produkte, die Attributwerte vom Typ "Produkt" haben, können nicht Bestandteil eines Bundles sein. + + Hinweise zu Produkt-Bundles + Das Produkt muss gespeichert werden, bevor Produkte zur Stückliste hinzugefügt werden können. @@ -1976,7 +2000,7 @@ Checkout-Selling - Hinzufügen + Checkout-Selling-Produkt hinzufügen Produkt @@ -2032,6 +2056,12 @@ Eine kommagetrennte Liste der zulässigen Bestellmengen, die für dieses Produkt gelten. Kunden wählen i.d.F. eine Bestellmenge aus einem Dropdown-Menü aus, anstatt eine freie Eingabe zu tätigen. + + Summe genehmigter Bewertungen + + + Summe genehmigter Rezensionen + Verknüpftes Produkt @@ -2096,7 +2126,7 @@ {0} pro Einheit (Grundpreis: {1} pro {2}) - "Maßeinheit" ist erforderlich, wenn der Grundpreis berechnet werden soll. + Grundpreis Maßeinheit "Maßeinheit" ist erforderlich, wenn der Grundpreis berechnet werden soll. @@ -2203,12 +2233,18 @@ EAN (Europa), GTIN (global trade item number), UPC (USA), JAN (Japan) oder ISBN (Bücher). + + Hat angewendete Rabatte + Hat Probedownload Legt fest, ob der Kunde eine Beispieldatei vor dem Checkout runterladen kann. + + Hat Staffelpreise + Hat Benutzervereinbarung @@ -2221,6 +2257,12 @@ Die Höhe des Produktes. + + Homepage Reihenfolge + + + Legt die Anzeige-Reihenfolge der Produkte auf der Homepage fest (1 steht bspw. für das erste Element in der Liste). + ID @@ -2275,6 +2317,9 @@ Die Länge des Produktes. + + Niedrigster Attributkombinationspreis + Aktion bei Erreichen des Meldebestandes @@ -2344,6 +2389,12 @@ Produktname ist erforderlich + + Summe nicht genehmigter Bewertungen + + + Summe nicht genehmigter Rezensionen + Benachrichtigt den Administrator, wenn die Mindestmenge unterschritten wird. @@ -2566,6 +2617,18 @@ Warengruppe eingrenzen + + Auf Homepage angezeigt + + + Filtert nach Produkten, die auf der Homepage angezeigt oder nicht angezeigt werden. + + + Veröffentlicht + + + Filtert nach veröffentlichten bzw. unveröffentlichten Produkten. + Hersteller @@ -2831,7 +2894,7 @@ Cross-Selling - Hinzufügen + Cross-Selling-Produkt hinzufügen Reihenfolge @@ -2840,7 +2903,7 @@ Produkt - Sie müssen das Produkt speichern, bevor Sie verwandte Produkte festlegen können + Sie müssen das Produkt speichern, bevor Sie Cross-Selling-Produkte hinzufügen können. Spezifikationsattribute @@ -3034,12 +3097,15 @@ Es wurden {0} gegenseitige Zuordnung(en) erstellt. - - CSV-Datei + + CSV Konfiguration Die Daten wurden erfolgreich geändert. + + Datenaustausch + Die Daten wurden erfolgreich gespeichert. @@ -3052,38 +3118,59 @@ Ausgewählte Löschen + + Alle löschen + Möchten Sie dieses Element wirklich löschen? Möchten Sie "{0}" wirklich löschen? + + Gelöscht + Ausgewählte löschen Bearbeiten + + Die E-Mail wurde erfolgreich versendet. + + + Bitte geben Sie eine E-Mail-Adresse ein. + + + Objekt + ID Die eindeutige nummerische ID des Datensatzes + + Fehler + Fehler beim Senden einer E-Mail - - Excel-Datei + + PDF Export Bitte haben Sie einen Augenblick Geduld, während der Export durchgeführt wird + + Alle exportieren + Keine Daten zu exportieren. - - Als CSV-Datei exportieren + + Ausgewählte exportieren Nach Excel exportieren @@ -3100,20 +3187,29 @@ Nur Ausgewählte nach PDF exportieren - - Inhaltsverzeichnis - Zu viele Objekte! Mehr als 500 Objekte können nicht konvertiert werden. Bitte reduzieren Sie die Anzahl ausgewählter Datensätze. Als XML exportieren - - Alles als XML exportieren + + Die Datei ist in Benutzung und kann daher nicht geöffnet werden. + + + Datei nicht gefunden + + + {0} Dateien wurden gelöscht + + + Die Datei muss vom Typ {0} sein. - - Nur Ausgewählte als XML exportieren + + {0} Verzeichnisse wurden gelöscht + + + FTP-Status {0} ({1}). Allgemein @@ -3139,41 +3235,17 @@ Verbergen - - Von CSV-Datei importieren - - - Von Excel importieren - - - Aktiv seit: {0}. - - - Import abbrechen - - - Import wurde abgebrochen - - - Soll der aktive Importvorgang wirklich abgebrochen werden? Bislang importierte Produkte werden nicht gelöscht. + + HTTP-Status {0} ({1}). - - Vollständigen Bericht runterladen... + + Ignorieren - - Der Import läuft jetzt im Hintergrund . Sie können den Fortschritt bzw. das Ergebnis des letzten Importvorganges jederzeit im Import-Dialog einsehen. + + Importdatei - - <b>Letzter Import</b>: {0}{1}. - - - Kein Bericht verfügbar - - - {0} von {1} Zeilen verarbeitet. - - - {0} neu, {1} aktualisiert - bei {2} Warnung(en) und {3} Fehler(n). + + Importdateien Bitte warten Sie, der Importvorgang wird durchgeführt @@ -3181,6 +3253,9 @@ Info + + Letzte Ausführung + Lizenzieren @@ -3193,18 +3268,51 @@ Alle nicht gespeicherten Änderungen gehen verloren. Sind Sie sicher? + + Neue Datensätze + Nein Nein, abbrechen + + Es wurden keine Einträge ausgewählt. + + + von + + + Platzhalter + Bitte auswählen Vorschau + + Der Provider {0} konnte nicht geladen werden. + + + Öffentliche Dateien + + + Es wurden {0} Datensätze gelöscht. + + + Überspringen + + + Legt die Anzahl der zu überspringenden Datensätze fest. + + + Begrenzen + + + Legt die maximale Anzahl der zu verarbeitenden Datensätze fest. + Auf Standardwerte zurücksetzen @@ -3214,6 +3322,9 @@ Sie müssen die Anwendung neu starten, damit die Änderungen wirksam werden. + + Einschränkungen + Speichern @@ -3223,6 +3334,9 @@ Suchen + + Ausgewählte + Jetzt senden @@ -3235,6 +3349,12 @@ Anzeigen + + Werte für Überspringen und Begrenzen müssen größer oder gleich 0 sein. + + + Ausgelassen + Standard @@ -3265,18 +3385,30 @@ Alle Shops + + Erfolgreich am + Der Vorgang wurde erfolgreich ausgeführt. + + Zeilen insgesamt + Baumansicht Unlizenziert + + Nicht unterstützter Entitätstyp '{0}' + Aktualisieren + + Aktualisiert + Bitte eine Datei hochladen. @@ -3286,6 +3418,9 @@ Bitte warten, die Verarbeitung läuft... + + Warnungen + Falsche E-Mail @@ -3319,9 +3454,6 @@ Filtert Ereignisse nach Ereignistypen. - - Ereignistyp - Nachricht @@ -3346,6 +3478,15 @@ Kunden-E-Mail + + Filtert Ergebnisse nach E-Mail-Adresse der Kunden. + + + Kundensystemkonto + + + Filtert Ergebnisse nach Kundenystemkonten. + Aktivitätstypen @@ -3523,6 +3664,9 @@ Zurück zur Länderliste + + Das Land kann nicht gelöscht werden, weil ihm Adressen zugeordnet sind. + Das Land wurde gelöscht. @@ -3611,7 +3755,7 @@ Neue Region hinzufügen - Das Land kann nicht gelöscht werden. Es wird bei vorhandenen Adressen verwendet. + Das Bundesland\Region kann nicht gelöscht werden, weil ihm Adressen zugeordnet sind. Region bearbeiten @@ -3664,15 +3808,12 @@ Zurück zur Währungsliste - - Die Leitwährung für Währungsumrechnungen kann nicht gelöscht werden. - - - Die Leitwährung des Shops kann nicht gelöscht werden. - Die Währung wurde gelöscht. + + Die Währung kann nicht gelöscht oder deaktiviert werden, weil sie dem Shop "{0}" als Leit- oder Umrechnungswährung zugeordnet ist. + Währungsdetails bearbeiten @@ -3731,16 +3872,10 @@ Online Wechselkursdienst - Hauptwährung für Währungsumrechnung + Umrechnungswährung - Leitwährung im Shop - - - Als Hauptwährung für Währungsumrechnung festlegen - - - Als Leitwährung im Shop festlegen + Leitwährung Name @@ -3754,6 +3889,18 @@ Der Name ist erforderlich. + + Ist Umrechnungswährung für + + + Eine Liste mit Shops, in denen die Währung Umrechnungswährung ist. + + + Ist Leitwährung für + + + Eine Liste mit Shops, in denen die Währung Leitwährung ist. + Veröffentlicht @@ -3922,6 +4069,9 @@ Die E-Mail wurde erfolgreich versandt. + + Test der E-Mail-Funktion. + Das E-Mail-Konto wurde erfolgreich bearbeitet. @@ -3962,10 +4112,10 @@ Legt die Anzeige-Priorität fest (1 steht bspw. für das erste Element in der Liste). - Dateiname des Flaggenbildes + Flaggenbild - Dateiname des Flaggenbildes. Die Datei sollte in folgendem Verzeichnis gespeichert werden: \\images\\flags. + Legt das Flaggenbild fest. Die Dateien der Flaggenbilder müssen in /Content/Images/flags/ liegen. Gebietsschema @@ -4007,7 +4157,7 @@ Der SEO-Code muss 2 Zeichen haben. - Der SEO-Code muss eindeutig sein. + Bitte legen Sie einen SEO Sprach-Code fest. Ressourcen importieren @@ -4174,6 +4324,21 @@ Stornierung möglich + + Langtext + + + Legt eine vollständige Beschreibung der Zahlungsmethode fest. Sie erscheint in der Zahlungsliste im Kassenbereich. + + + Es wurden keine Möglichkeiten zur Einschränkung von Zahlungsarten gefunden. + + + Kurzbeschreibung + + + Legt eine Kurzbeschreibung der Zahlungsmethode fest. + Plugins @@ -4303,6 +4468,9 @@ Das Plugin wurde deinstalliert. + + Beim Aufruf eines Plugins ist ein unbekannter Fehler aufgetreten. Details entnehmen Sie bitte der folgenden Meldung. + Verpackungseinheiten @@ -4333,6 +4501,9 @@ Regionale Einstellungen + + Sie müssen zunächst speichern, bevor Sie Einschränkungen festlegen können. + Einstellungen @@ -4372,6 +4543,12 @@ Aktiviert den Blog. + + Maximales Alter (in Tagen) + + + Legt das maximale Blog-Alter in Tagen fest. Ältere Blog-Einträge werden im RSS-Feed nicht exportiert. + Benachrichtigung bei neuen Kommentaren @@ -4394,7 +4571,7 @@ RSS-Feed URL im Browser in der Adressleiste anzeigen - Legt fest, ob der RSS-Feed-Link in der Browser-Adressleiste zu angezeigt werden soll. + Legt fest, ob der RSS-Feed-Link in der Adressleiste des Browsers angezeigt werden soll. Katalog-Einstellungen @@ -4453,6 +4630,12 @@ Legt die Anzahl der dargestellten Produkte pro Seite fest. + + Standardsortierreihenfolge für Produkte + + + Legt die Standardsortierreihenfolge für Produkte fest. + Standard Listendarstellung @@ -4507,6 +4690,24 @@ Legt fest, ob der Kaufen-Button in Produktlisten ausgeblendet werden soll. + + Standardbild bei Warengruppen ausblenden + + + Legt fest, ob das Standardbild bei Warengruppen ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn der Warengruppe kein Bild zugeordnet ist. + + + Standardbild bei Herstellern ausblenden + + + Legt fest, ob das Standardbild bei Herstellern ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Hersteller kein Bild zugeordnet ist. + + + Standardbild bei Produkten ausblenden + + + Legt fest, ob das Standardbild bei Produkten ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Produkt kein Bild zugeordnet ist. + Höhe des gekürzten Langtextes @@ -4582,6 +4783,12 @@ Anzahl der angezeigten Produkt-Tags + + Preisanzeige + + + Legt fest, ob bzw. welcher Typ von Preis in Produktlisten angezeigt werden soll. + Produktdetail @@ -4699,6 +4906,12 @@ Legt die Anzahl der sichtbaren Produkte in der Liste der kürzlich angesehenen Produkte fest. + + Produktbeschreibung durchsuchen + + + Legt fest, ob die Produktbeschreibung in der Suche einbezogen werden soll. + Anzahl Ergebnisse pro Seite auf Suchseite @@ -4795,6 +5008,12 @@ Bestimmt ob Hersteller auf der Homepage als Bilder oder textuelle Links angezeigt werden. + + Bilder von Herstellern anzeigen + + + Legt fest, ob Herstellerbilder auf der Produktdetailseite angezeigt werden sollen. + Zeige Hersteller auf der Homepage @@ -4849,6 +5068,12 @@ Zeigt das Gewicht des Produktes an. + + Filterergebnisse nach Trefferanzahl sortieren + + + Legt fest, das Filterergebnisse absteigend nach der Anzahl an Übereinstimmungen sortiert werden. Ist diese Option deaktiviert, so wird in der für die Werte festgelegten Reihenfolge sortiert. + Unterwarengruppen anzeigen @@ -5029,6 +5254,12 @@ Legt fest, ob das Eingabefeld "Land" während der Registrierung aktiviert ist. + + Kunden können Kundennummer hinterlegen + + + Bestimmt ob Kunden eine Kundennummer angeben können, wenn für diese noch kein Wert hinterlegt wurde. + Benutzerformular-Felder @@ -5053,6 +5284,18 @@ Bestimmt ob Kundennummern hinterlegt werden können. + + Kundennummern + + + Legt fest, ob Kundennummern vergeben werden und ob diese automatisch vergeben werden sollen. + + + Darstellung der Kundennummer + + + Legt die Darstellung und Handhabung der Kundennummer gegenüber dem Kunden fest. + Kundeneinstellungen @@ -5083,6 +5326,18 @@ Legt die Standard-Zeitzone für den Shop fest. + + Kundennummern im Frontend anzeigen + + + Bestimmt ob Kunden ihre Kundennummer in Ihrem Account-Bereich einsehen können. + + + Einwilligungserklärung im Kontaktformular fordern + + + Bestimmt ob im Kontaktformular eine Checkbox angezeigt wird, die den Benutzer auffordert der Speicherung seiner Daten zuzustimmen. + Automatische Registrierung aktiviert @@ -5152,6 +5407,12 @@ Legt fest, ob die Eingabe der Telefonnummer erforderlich ist. + + Kundengruppe bei Registrierungen + + + Legt eine Kundengruppe fest, die neu registrierten Kunden zugeordnet wird. + Anzeige des Registrierungsdatums eines Kunden @@ -5230,6 +5491,24 @@ Legt fest, ob Angabe der Postleitzahl erforderlich ist. + + Zeitlimit für Bilder-Download (Minuten) + + + Legt das Zeitlimit für den Bilder-Download in Minuten fest. + + + Bilderordner (relativer Pfad) + + + Legt einen relativen Pfad zu einem Ordner mit zu importierenden Bildern fest (z.B. Inhalt\Bilder). + + + Maximale Länge von Datei- und Ordnernamen + + + Legt die maximale Länge von Datei- und Ordnernamen fest, die im Rahmen eines Imports\Exports erzeugt wurden. + Forum @@ -5365,6 +5644,24 @@ Der Zugriff auf den Adminbereich kann nur noch über diese IP-Adresse erfolgen. + + Unicode-Zeichen erlauben + + + Legt fest, ob als Unicode-Zeichen eingestufte Buchstaben in SEO relevanten Namen erlaubt sind. + + + Bei Abschluss einer Bestellung PDF mitsenden + + + Erstellt bei Abschluss einer Bestellung das Auftrags-PDF-Dokument und hängt es der Kunden-Benachrichtigungs-Email an + + + Bei Bestelleingang PDF mitsenden + + + Erstellt bei Bestelleingang das Auftrags-PDF-Dokument und hängt es der Kunden-Benachrichtigungs-Email an + Bankverbindung @@ -5707,6 +6004,12 @@ Der Sicherheitsschlüssel muss 16 Zeichen haben. + + Extra Disallows für robots.txt + + + Geben Sie hier zusätzliche Pfade an, die als Disallow-Einträge zur robots.txt hinzugefügt werden sollen. Jeder Eintrag muss in einer neuen Zeile erfolgen. + Einstellungen zur Volltextsuche @@ -5761,6 +6064,12 @@ Lokalisierung + + Meta Robots + + + Legt fest, ob und wie Suchmaschinen die Seiten Ihres Shops indexieren. + Seiten-Titel-Anpassung @@ -5815,6 +6124,12 @@ Wenn aktiviert, werden Suchmaschinenfreundliche URLs generiert. + + Zu konvertierende Zeichen + + + Ermöglicht das individuelle Konvertieren von Zeichen bei der Erstellung SEO Namen. Geben Sie hier durch Semikolon getrennt das alte und das neue Zeichen ein, z.B. ä;ae. Jeder Eintrag muss in einer neuen Zeile erfolgen. + Suchmaschinen @@ -5869,6 +6184,12 @@ Shop Information + + Zeichenkette prüfen + + + Geben Sie eine beliebige Zeichenkette ein, um daraus den SEO Namen zu erstellen. Geänderte Einstellungen müssen zuvor gespeichert werden. + Bilder zur Sprachauswahl verwenden @@ -5929,6 +6250,12 @@ Legt die maximal erlaubte Größe (längste Seite) für Bilduploads fest. + + Thumbnail-Größe von Produkten in E-Mails + + + Legt die Thumbnail-Bildgröße (in Pixel) von Produkten in E-Mails fest. Geben Sie 0 ein, um keine Thumbnails anzuzeigen. + Miniwarenkorb-Symbolbild-Größe @@ -6007,6 +6334,12 @@ Legt die Anzahl der angezeigten News auf der Startseite fest. + + Maximales Alter (in Tagen) + + + Legt das maximale News-Alter in Tagen fest. Ältere News werden im RSS-Feed nicht exportiert. + Seitengröße @@ -6049,6 +6382,12 @@ Der Kunde wird direkt auf die Auftrags-Detail-Seite geleitet, falls diese Einstellung aktiviert ist. + + Aufträge aller Shops anzeigen + + + Legt fest, ob dem Kunden die Aufträge aller Shops angezeigt werden sollen. Ist diese Option deaktiviert, so werden nur die Aufträge des aktuellen Shops angezeigt. + Geschenkgutschein ist eingelöst, wenn Auftragsstatus... @@ -6059,7 +6398,7 @@ Der Auftragsstatus konnte nicht auf "in Bearbeitung" gesetzt werden - Geschenkgutschein wird deactiviert, wenn Auftragsstatus ... + Geschenkgutschein wird deaktiviert, wenn Auftragsstatus... Geschenkgutscheine sind deaktiviert, wenn der Auftragsstatus ist … @@ -6103,6 +6442,12 @@ Legen Sie einen Startwert für Bestellnummern fest. Dies ist nützlich, wenn Sie Ihre Aufträge ab einem Bestimmten Wert starten lassen möchten. Der Wert muss größer sein, als die aktuell höchste Bestellnummer und gilt für zukünftige Bestellungen. + + Anzahl der Aufträge pro Seite + + + Legt die Anzahl der dargestellten Aufträge pro Seite fest. + Auftragseinstellungen @@ -6208,6 +6553,12 @@ Bonuspunkte, die für eine Registrierung gewährt werden. + + Punkte abrunden + + + Legt fest, ob bei der Punkteberechnung abgerundet werden soll. Ansonsten werden Bonuspunkte aufgerundet. + Versand-Einstellungen @@ -6322,6 +6673,15 @@ Aktivieren, um Artikel aus der Wunschliste in den Warenkorb zu verschieben. + + Abonnieren von Newslettern + + + Legt fest, ob Kunden bei einer Bestellung Newsletter abonnieren können und ob die Checkbox standardmäßig aktiviert ist. + + + Bestellabschlussseite + Preise bei der Berechnung runden @@ -6335,16 +6695,16 @@ Bestimmt ob der Grundpreis im Warenkorb angezeigt werden soll. - Zeige Kommentarbox auf Bestellabschlussseite + Kommentarbox anzeigen - Legt fest ob der Kunde auf der Bestellabschlussseite einen Kommentar zu seiner Bestellung hinterlegen kann. + Legt fest, ob der Kunde auf der Bestellabschlussseite einen Kommentar zu seiner Bestellung hinterlegen kann. - Rechtliche Hinweise in der Warenkorbübersicht auf der Bestellabschlußseite anzeigen + Rechtliche Hinweise in der Warenkorbübersicht anzeigen - Bestimmt, ob rechtliche Hinweise in der Warenkorbübersicht auf der Bestellabschlußseite angezeigt werden. Dieser Text kann in den Sprachresourcen geändert werden. + Legt fest, ob rechtliche Hinweise in der Warenkorbübersicht auf der Bestellabschlußseite angezeigt werden. Dieser Text kann in den Sprachresourcen geändert werden. Lieferzeiten anzeigen @@ -6358,6 +6718,12 @@ Zeigt die Rabattbox im Warenkorb an. + + Widerrufsverzichtbox für elektronische Leistungen anzeigen + + + Legt fest, ob der Kunde auf der Bestellabschlussseite einem Widerrufsverzicht für elektronische Leistungen zustimmen muss. + Zeige die Geschenkgutschein-Box im Warenkorb @@ -6412,6 +6778,21 @@ Legt fest ob das Produktgewicht im Warenkorb angezeigt wird. + + Zustimmung zur E-Mail Weitergabe an Dritte + + + Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen können und ob die Checkbox dafür standardmäßig aktiviert ist. + + + Text für E-Mail Weitergabe + + + Mit der Übermittlung und Speicherung meiner E-Mail-Adresse durch dritte Parteien bin ich einverstanden. + + + Legt den Text für die Zustimmung zur Weitergabe der E-Mail Adresse an Dritte fest. Wählen Sie bitte einen konkreten Grund für die Weitergabe, z.B. 'Mit der Übermittlung und Speicherung meiner E-Mail-Adresse zur Abwicklung des Käuferschutzes durch Trusted Shops bin ich einverstanden.' + Wunschzettel @@ -6622,6 +7003,12 @@ Der Name ist erforderlich + + Es konnten keine Versandarten geladen werden. + + + Es wurden keine Möglichkeiten zur Einschränkung von Versandarten gefunden. + Die Versandart wurde erfolgreich bearbeitet. @@ -6637,18 +7024,6 @@ Reihenfolge - - Versandart-Einschränkungen - - - Land - - - Markieren Sie die Länder, bei denen die Einschränkung gelten soll - - - Die Einstellungen wurden erfolgreich bearbeitet - SMS-Anbieter @@ -6718,6 +7093,18 @@ Bitte einen Namen angeben. + + Umrechnungswährung + + + Legt die Umrechnungswährung für diesen Shop fest. + + + Leitwährung + + + Legt die Leitwährung des Shops fest. + Gesicherte URL @@ -6725,10 +7112,10 @@ Die gesicherte URL des Shops, z.B. https://www.mein-shop.de/ or http://sharedssl.mein-shop.de/. Die gesicherte URL wird automatisch erkannt, wenn dieses Feld leer ist. - SSL aktivieren + SSL - Aktiviert SSL, falls der Shop SSL gesichert werden soll. + Legt fest, ob der Shop SSL gesichert werden soll. Shop Logo @@ -6823,6 +7210,9 @@ Name + + Theme benötigt keine Konfiguration + LESS CSS Parser Fehler: Ihre Änderungen wurden nicht gespeichert, da Ihre Konfiguration zu einem Fehler im Shop führen würde. Details siehe Fehlerbericht. @@ -7180,6 +7570,24 @@ Liste erlaubter Platzhalter. Diese können in E-Mail-Vorlagen benutzt werden. + + Anhang 1 + + + Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.) + + + Anhang 2 + + + Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.) + + + Anhang 3 + + + Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.) + BCC-Adresse @@ -7660,6 +8068,9 @@ Zurück zur Gesamtliste + + Die Kundengruppe "{0}" wurde nicht gefunden. + Die Kundengruppe wurde gelöscht. @@ -7843,6 +8254,9 @@ Erstellt am + + Kunden GUID + Kundengruppen @@ -7900,6 +8314,9 @@ IP-Adresse, mit der sich der Kunde zuletzt im Shop aktiv war. + + Ist Systemkonto + Mehrwertsteuerfrei @@ -7912,6 +8329,9 @@ Zeigt an, wann der Kunde zuletzt im Shop aktiv war. + + Letztes Login-Datum + Nachname @@ -7933,6 +8353,9 @@ Passwort des Kunden. + + Passwort Salt + Telefon @@ -8287,97 +8710,820 @@ Dashboard - - Herunterladen der hochgeladenen Datei + + Neues Profil - - Download URL + + Standardwert - - Das Download-Objekt wurde gespeichert + + Eigenschaft des Objektes - - Download entfernen + + Importfeld - - Download speichern + + Sie können optional für jedes Feld der Importdatei festlegen, ob und nach welcher Objekteigenschaft dessen Daten zu importieren sind. Gleichnamige Felder werden grundsätzlich immer importiert, sofern sie nicht explizit ignoriert werden sollen. Noch nicht ausgewählte Eigenschaften sind in der Auswahlliste hervorgehoben. Zudem ist die Angabe eines Standardwertes möglich, der angewendet wird, wenn das Importfeld leer ist. Durch Änderung des Trennzeichens werden gespeicherte Zuordnungen ungültig und zurückgesetzt. - - Upload Datei + + Die gespeicherten Feldzuordnungen sind aufgrund der Änderung des Trennzeichens ungültig und wurden zurückgesetzt. - - Benutze Download-URL + + Trennzeichen - - Geschenkgutscheine + + Legt das zu verwendende Trennzeichen für die Felder fest. - - Der neue Geschenkgutschein wurde hinzugefügt + + Geben Sie bitte ein gültiges Trennzeichen ein. - - Geschenkgutschein hinzufügen + + Inneres Anführungszeichen - - Zurück zur Gesamtliste + + Legt das innere Anführungszeichen (Escaping) fest. - - Der Geschenkgutschein wurde gelöscht + + Geben Sie bitte ein gültiges, inneres Anführungszeichen (Escaping) ein. - - Geschenkgutscheindetails + + Trennzeichen und inneres Anführungszeichen können in CSV Dateien nicht gleich sein. - - Gesamtbetrag + + Anführungszeichen - - Wert des Geschenkgutscheines + + Legt das zu verwendende Anführungszeichen fest. - - Erstellt am + + Geben Sie bitte ein gültiges Anführungszeichen ein. - - Gibt an, an welchem Datum der Geschenkgutschein erstellt wurde. + + Alle Felder in Anführungszeichen - - Gutscheinnummer + + Legt fest, ob die Werte aller Felder in Anführungszeichen gestellt werden sollen. - - Gutscheinnummer generieren + + Trennzeichen und Anführungszeichen können in CSV Dateien nicht gleich sein. - - Gutscheinnummer. Kann während des Checkouts eingelöst werden. + + Mehrzeilen erlaubt - - Typ des Geschenkgutscheines + + Legt fest, ob mehrzeilige Feldwerte unterstützt werden. - - Gibt den Typ des Geschenkgutscheines an. + + Überflüssige Leerzeichen entfernen - - Aktiviert + + Legt fest, ob Leerzeichen am Anfang und am Ende eines Feldwertes entfernt werden sollen. - - Gibt an, ob der Geschenkgutschein aktiviert und einlösbar ist. + + Stapelgröße - - Empfänger ist benachrichtigt + + Legt die maximale Anzahl der Datensätze pro Exportdatei fest. 0 ist der Standard und bedeutet, dass alle Datensätze in eine Datei exportiert werden. - - Empfänger ist benachrichtigt + + Ein System-Exportprofil kann nicht gelöscht werden. - - Empfänger benachrichtigen + + Nach erfolgreicher Veröffentlichung aufräumen - - Nachricht + + Legt fest, ob nicht mehr benötigte Dateien gelöscht werden sollen, nachdem alle Veröffentlichungen erfolgreich waren. - - Nachricht eingeben. + + Einstellungen übernehmen von - - Auftrag + + Legt das Exportprofil fest, von welchem die Einstellungen übernommen werden sollen. - + + Dies ist eine automatische Benachrichtung von Shop "{0}" über einen erfolgten Datenexport. + + + Export von Profil "{0}" ist abgeschlossen + + + E-Mail Adressen an + + + Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll. + + + Die folgenden spezifischen Angaben werden durch den Provider beim Export berücksichtigt. + + + Der Export-Provider <b>{0}</b> benötigt keine weitergehende Konfiguration. + + + ZIP-Archiv erstellen + + + Legt fest, ob die Exportdateien in einem ZIP-Archiv zusammengefasst werden sollen. Das Archiv verbleibt im temporären Ordner des Exportprofils ohne weitere Vearbeitung. + + + Mindestens eine Datei konnte nicht kopiert werden. + + + Legt fest, ob die Exportdateien in einem ZIP-Archiv zusammengefasst und nur das Archiv bereitgestellt werden soll. + + + Art der Veröffentlichung + + + Legt die Art Veröffentlichung fest. + + + E-Mail Konto + + + Legt das E-Mail Konto fest, über welches die Daten verschickt werden sollen. + + + E-Mail Adressen an + + + Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll. + + + E-Mail Betreff + + + Legt den Betreff der verschickten Daten fest. + + + Ordnerpfad + + + Legt den Pfad (relativ oder absolut) zu einem Ordner fest, in den die Daten bereitgestellt werden sollen. + + + HTTP Übertragungsart + + + Legt fest, aus welcher Art die Exportdateien per HTTP übertragen werden sollen. + + + Legt fest, ob die exportierten Daten in einen übers Internet zugänglichen Ordner kopiert werden sollen. + + + Name des Profils + + + Legt den Namen des Veröffentlichungsprofils fest. + + + Es liegen keine Veröffentlichungsprofile vor. + + + Legen Sie über <b>Neues Profil</b> ein oder mehrere Veröffentlichungsprofile an, um festzulegen wie mit den Exportdateien weiter zu verfahren ist. + + + Passiver Modus + + + Legt fest, ob Daten im aktiven oder passiven Modus ausgetauscht werden sollen. + + + Passwort + + + Legt das Passwort fest. + + + Veröffentlichungsprofile + + + Veröffentlichungsziel + + + Name des Unterordners + + + Legt den Namen eines Unterordners fest, in den die Daten veröffentlicht werden sollen. + + + URL\Host + + + Legt die URL bzw. den Host-Namen fest, an die die Daten übermittelt werden sollen. + + + Benutzername + + + Legt den Benutzernamen fest. + + + SSL verwenden + + + Legt fest, ob einen SSL (Secure Sockets Layer) Verbindung genutzt werden soll. + + + Bei einer großen Anzahl an Exportdateien wird empfohlen die Option <b>ZIP-Archiv erstellen</b> zu benutzen. Das spart Zeit und vermeidet Probleme, wie z.B. ein volles E-Mail Postfach. + + + E-Mail Benachrichtigung + + + Legt das E-Mail Konto fest, über welches eine Benachrichtigung über die Fertigstellung des Exports verschickt werden soll. + + + Das Exportprofil ist deaktiviert. Für eine Exportvorschau muss das Exportprofil aktiviert sein. + + + Objekt + + + Der Objekttyp, den der Provider verarbeitet. + + + Exportdateien + + + Dateityp + + + Der Dateityp der exportierten Daten. + + + Muster für Dateinamen + + + Legt das Muster fest, nach dem Dateinamen erzeugt werden. + + + Bitte ein gültiges Muster für Dateinamen eingeben. Beispiel: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName% + + + ID des Exportprofils;Ordername des Exportprofils;SEO Name des Exportprofils;Shop ID;SEO Name des Shops;Mit 1 beginnender Dateiindex;Zufallszahl;UTC Zeitstempel + + + Verfügbar bis + + + Nach der Verfügbarkeitsmenge filtern. + + + Verfügbar von + + + Nach der Verfügbarkeitsmenge filtern. + + + Rechnungsländer + + + Nach Rechnungsländern filtern. + + + Warengruppen + + + Nach Warengruppen filtern. + + + Erstellt von + + + Nach dem Erstellungsdatum filtern. + + + Erstellt bis + + + Nach dem Erstellungsdatum filtern. + + + Kundengruppen + + + Nach Kundengruppen filtern. + + + Nur empfohlene Produkte + + + Nach empfohlenen Produkten filtern. Wird nur bei der Filterung nach Warengruppen und Hersteller angewendet. + + + Hat x Bestellungen + + + Nach der Anzahl der getätigten Bestellungen filtern. + + + Hat Betrag x ausgegeben + + + Nach dem insgesamt ausgegebenen Betrag filtern. + + + Produkt-ID bis + + + Nach der Produkt-ID filtern. + + + Produkt-ID von + + + Nach der Produkt-ID filtern. + + + Nur aktive Kunden + + + Nach aktiven bzw. inaktiven Kunden filtern. + + + Nur aktive Abonnenten + + + Nach aktiven bzw. inaktiven Newsletter Abonnenten filtern. + + + Veröffentlicht + + + Nach Veröffentlichung filtern. + + + Nur MwSt. befreite Kunden + + + Nach MwSt. befreiten Kunden filtern. + + + Zuletzt aktiv von + + + Nach dem Datum der letzten Shop Aktivität filtern. + + + Zuletzt aktiv bis + + + Nach dem Datum der letzten Shop-Aktivität filtern. + + + Hersteller + + + Nach Hersteller filtern. + + + Legen Sie individuelle Filter fest, um die zu exportierenden Daten einzugrenzen. + + + Auftragsstatus + + + Nach Auftragsstaus filtern. + + + Zahlungsstatus + + + Nach Zahlungsstatus filtern. + + + Preis bis + + + Nach dem Preis filtern. + + + Preis von + + + Nach dem Preis filtern. + + + Produkt-Tag + + + Nach Produkt-Tag filtern. + + + Produkttyp + + + Nach Produkttyp filtern. + + + Versandländer + + + Nach Versandländern filtern. + + + Versandstatus + + + Nach Versandstatus filtern. + + + Shop + + + Nach Shop filtern. + + + Ohne Warengruppenzuordnung + + + Nach fehlender Warengruppenzuordnung filtern. + + + Ohne Herstellerzuordnung + + + Nach fehlender Herstellerzuordnung filtern. + + + Ordnerpfad + + + Legt den relativen Pfad des Ordners fest, in den die Daten exportiert werden. + + + Bitte einen gültigen, relativen Ordnerpfad für die zu exportierenden Daten eingeben. + + + Systemprofil + + + Gibt an, ob es sich bei dem Exportprofil um eine Systemprofil handelt. Systemprofile können nicht entfernt werden. + + + Das System-Exportprofil {0} wurde nicht gefunden. + + + Name des Profils + + + Legt den Namen des Exportprofils fest. + + + Es wurden keine Export-Provider gefunden. + + + Möglichkeiten der Filterung stehen nicht zur Verfügung. + + + Der Export-Provider unterstützt keinen expliziten Dateityp. Für eine weitere Bereitstellung der Exportdaten ist daher der Export-Provider verantwortlich. + + + Eine Vorschau steht für diesen Entitätstyp nicht zur Verfügung. + + + Es wurden keine Exportprofile gefunden. + + + Es wurde kein Exportprofil vom Typ <b>{0}</b> gefunden. Jetzt ein <a href="{1}">neues Exportprofil anlegen</a>. + + + Möglichkeiten der Projektion stehen nicht zur Verfügung. + + + Diese Option wird in der Vorschau nicht berücksichtigt. + + + Mit den folgenden Einstellungen lassen sich die zu exportierenden Daten aufteilen. Dazu zählt<ul><li>Das Überspringen der ersten n Datensätze</li><li>Die maximale Anzahl zu exportierender Datensätze</li><li>Die Anzahl der Datensätze pro Exportdatei</li><li>Daten von jedem Shop in eine separate Datei exportieren</li></ul>Standardmäßig werden alle Daten eines Shops in eine Datei exportiert. + + + Einstellungen zur Aufteilung müssen größer oder gleich 0 sein. + + + Per Shop + + + Legt fest, ob für jeden Shop ein separater Verarbeitungsdurchlauf erfolgen soll. Für jeden Shop wird eine neue Datei erzeugt. + + + Exportprofil + + + Das Exportprofil für diesen Export-Provider. + + + {0} von {1} Datensätzen exportiert + + + Anzuhängender Text + + + Legt den an die Artikelbeschreibung anzuhängenden Text fest. Bei mehreren Texten wird einer per Zufall ausgewählt. + + + Attributkombinationen exportieren + + + Legt fest, ob für jede aktive Attributkombination ein eigenständiges Produkt exportiert werden soll. + + + Attributwerte + + + Legt fest, ob und wie die Werte der Attribute weiter verarbeitet werden sollen. + + + Hersteller\Marke + + + Legt den zu exportierenden Hersteller bzw. die Marke fest, wenn für ein Produkt kein Hersteller zugeordnet ist. + + + Netto- in Bruttopreise umrechnen + + + Legt fest, dass Netto- in Bruttopreise umgerechnet werden sollen. + + + Kritische Zeichen + + + Liste mit Zeichen, die aus der Detailbeschreibung entfernt werden sollen. + + + Währung + + + Legt die auf den Export anzuwendende Währung fest. + + + Kunden-ID + + + Legt die ID des Kunden fest, auf den sich der Export beziehen soll. Wird z.B. bei Preisberechnungen berücksichtigt. + + + Artikelbeschreibung + + + Legt fest, welche Informationen zur Beschreibung des Artikel wie verwendet werden sollen. + + + HTML aus der Beschreibung entfernen + + + Legt fest, ob für den Export alle HTML-Auszeichnungen aus der Artikelbeschreibung entfernt werden sollen. + + + Kostenloser Versand ab + + + Legt den Betrag fest, ab dem keine Versandkosten anfallen. + + + Sprache + + + Legt die auf den Export anzuwendende Sprache fest. + + + Keine Gruppenprodukte exportieren + + + Legt fest, ob Gruppenprodukte exportiert werden sollen. Ist diese Option aktiviert, so werden die zur Gruppe gehörenden Produkte exportiert. + + + Die folgenden Angaben werden beim Export berücksichtigt und an entsprechenden Stellen in den Vorgang eingebunden. + + + Anzahl der Bilder + + + Legt die Anzahl der zu exportierenden Bilder pro Objekt fest. + + + Auftragsstatus ändern in + + + Legt fest, ob und wie der Status der exportierten Aufträge geändert werden soll. + + + Produktbildgröße + + + Legt die Größe (in Pixel) des Produktbildes fest. + + + Produktpreis + + + Legt den zu exportierenden Produktpreis fest. + + + Kritische Zeichen entfernen + + + Legt fest, ob kritische Zeichen (wie z.B. ½) aus der Detailsbeschreibung entfernt werden sollen. + + + Versandkosten + + + Die zu exportierenden Versandkosten. + + + Lieferzeit + + + Legt die Lieferzeit für Produkte fest, wo diese nicht angegeben ist. + + + Shop + + + Legt den auf den Export anzuwendenden Shop fest. + + + Provider + + + Legt den Export-Provider fest. Er ist für die individuelle Formatierung der zu exportierenden Daten zuständig. + + + Systemname des Profils + + + Der Systemname des Exportprofils. + + + Die folgende Liste enthält Systemprofile, die von Plugins wie bspw. dem <a href='http://community.smartstore.com/index.php?/files/file/85-smartstorenet-common-export-providers/' target='_blank'>Datenexporte Plugin</a> bereitgestellt werden. Sie können Systemprofile nach Belieben anpassen, aber keine Neuen erstellen. Für diese Profile stehen außerdem zusätzliche Aktions-Buttons zur Verfügung. Sie finden diese über den entsprechenden Listen, wie z.B. der Produkt- oder Auftragsliste. + + + Systemprofile + + + Benutzerprofile + + + Importdatei hinzufügen... + + + Zuordnung der Importfelder + + + Dies ist eine automatische Benachrichtung von Shop "{0}" über einen erfolgten Datenimport. Zusammenfassung: + + + Import von "{0}" ist abgeschlossen + + + Mein Produktimport;Mein Warengruppenimport;Mein Kundenimport;Mein Newsletter-Abonnement-Import + + + Importdatei hochladen... + + + Schlüsselfelder + + + Anhand von Schlüsselfeldern können vorhandene Datensätze zwecks Aktualisierung identifiziert werden. Die Schlüsselfelder werden in der hier festgelegten Reihenfolge verarbeitet. + + + Benutzen Sie das ID-Feld bitte nur dann als Schlüsselfeld, wenn die Daten aus der derselben Datenbank stammen, in der sie importiert werden sollen. Ansonsten werden u.U. die falschen Datensätze aktualisiert. + + + Letztes Importergebnis + + + Bitte laden Sie eine Importdatei hoch. + + + Bei mehreren Importdateien ist darauf zu achten, dass diese vom selben Dateityp sind und deren Inhalt demselben Schema folgt (z.B. gleiche Spaltenüberschriften). + + + Name des Profils + + + Legt den Namen des Importprofils fest. + + + Es wurden keine Importprofile gefunden. + + + Anzahl der Bilder + + + Legt die Anzahl der zu importierenden Bilder pro Objekt fest. + + + Wählen Sie bitte das zu importierende Objekt und laden Sie eine Importdatei hoch. + + + {0} von {1} Datensätzen verarbeitet + + + Hier neue Zuordnung vornehmen + + + Nur aktualisieren + + + Ist diese Option aktiviert, werden nur vorhandene Daten aktualisiert, aber keine neue Datensätze hinzugefügt. + + + Es ist mindestens ein Schlüsselfeld erforderlich. + + + Herunterladen der hochgeladenen Datei + + + Download URL + + + Das Download-Objekt wurde gespeichert + + + Download entfernen + + + Download speichern + + + Upload Datei + + + Benutze Download-URL + + + Geschenkgutscheine + + + Der neue Geschenkgutschein wurde hinzugefügt + + + Geschenkgutschein hinzufügen + + + Zurück zur Gesamtliste + + + Der Geschenkgutschein wurde gelöscht + + + Geschenkgutscheindetails + + + Gesamtbetrag + + + Wert des Geschenkgutscheines + + + Erstellt am + + + Gibt an, an welchem Datum der Geschenkgutschein erstellt wurde. + + + Gutscheinnummer + + + Gutscheinnummer generieren + + + Gutscheinnummer. Kann während des Checkouts eingelöst werden. + + + Typ des Geschenkgutscheines + + + Gibt den Typ des Geschenkgutscheines an. + + + Aktiviert + + + Gibt an, ob der Geschenkgutschein aktiviert und einlösbar ist. + + + Empfänger ist benachrichtigt + + + Empfänger ist benachrichtigt + + + Empfänger benachrichtigen + + + Nachricht + + + Nachricht eingeben. + + + Auftrag + + Der Geschenkgutschein wurde mit diesem Auftrag gekauft. @@ -8512,6 +9658,12 @@ "Auftrag eingegangen" E-Mail (an Shopbetreiber) wurde gequeued. Queued Email ID: {0} + + Newsletter wurde abonniert + + + Newsletter-Abonnent wurde entfernt + Auftrag wurde storniert @@ -8519,7 +9671,7 @@ Zahlung wurde erfasst - Fehler bei Zahlungserfassung für Auftrag #{0}. Fehler: {1} + Es ist ein Fehler bei der Zahlungsbuchung zu Auftrag {0} aufgetreten. Auftrag wurde gelöscht @@ -8543,7 +9695,7 @@ Auftrag wurde teilweise erstattet. Betrag: {0} - Fehler bei Teilerstattung: {0} + Es ist ein Fehler bei einer Teilerstattung zu Auftrag {0} aufgetreten. Auftrag ist eingegangen @@ -8552,7 +9704,7 @@ Auftrag wurde erstattet. Betrag: {0} - Fehler bei Erstattung: {0} + Es ist ein Fehler bei einer Rückerstattung zu Auftrag {0} aufgetreten. Auftragsstatus geändert. Neuer Status: {0} @@ -8561,10 +9713,10 @@ Zahlungstransaktion wurde storniert - Fehler bei Stornierung der Zahlungstransktion: {0} + Es ist ein Fehler bei der Stornierung einer Zahlungstransaktion zu Auftrag {0} aufgetreten. - Konnte wiederkehrende Zahlung nicht stornieren. Fehler: {0} + Es ist ein Fehler bei der Stornierung einer wiederkehrenden Zahlung für Auftrag {0} aufgetreten. Wiederkehrende Zahlung wurde storniert @@ -8626,6 +9778,12 @@ Auftragsdetails bearbeiten + + Akzeptiert Weitergabe der E-Mail + + + Gibt an, ob der Kunde bei der Bestellung einer Weitergabe seiner E-Mail Adresse an Dritte zugestimmt hat oder nicht. + Partner @@ -8633,10 +9791,10 @@ Das Partner-Unternehmen, dem dieser Auftrag zugeordnet ist. - Authorizations-Transaktions-ID + Transaktions-ID für Autorisierung - Vom Zahlungsdienstleister erhaltene Authorizations-Transaktions-ID. + Vom Zahlungsanbieter erhaltene Transaktions-ID für die Autorisierung. Transaktionsergebnis für Autorisierung @@ -8663,10 +9821,10 @@ Zieht eine zuvor reservierte Zahlung über den Zahlungsanbieter ein. - Transaction ID buchen + Transaktions-ID für Buchung - Über vom Zahlungsdienstleister erhaltenene transaction id buchen. + Vom Zahlungsanbieter erhaltene Transaktions-ID für die Buchung. Transaktionsergebnis für Buchung @@ -9838,8 +10996,8 @@ E-Mail-Adresse erforderlich - - Es wurden {0} E-Mail(s) importiert und {1} aktualisiert. + + Abonnement GUID Bereitstellendes Plugin @@ -10366,6 +11524,9 @@ Fügen Sie die SQL-Abfrage hier ein: + + Die SQL Anweisung wurde erfolgreich ausgeführt. + Ausführen @@ -10378,6 +11539,9 @@ Zurück zur Übersicht + + E-Mail Anhang konnte nicht herunterladen: Daten nicht verfügbar. + Die E-Mail wurde gelöscht @@ -10387,6 +11551,18 @@ E-Mail bearbeiten + + Während der Erstellung des E-Mail-Anhangs ist ein Fehler aufgetreten + + + Daten für den E-Mail Anhang konnten nicht heruntergeladen werden. Pfad: {0} + + + Der Inhaltstyp des E-Mail Anhangs muss 'application/pdf' sein + + + Anhänge + BCC @@ -10492,6 +11668,9 @@ Der Name des Empfängers. + + Anzahl Anhänge + Enddatum @@ -10549,77 +11728,164 @@ Die E-Mail wurde erfolgreich bearbeitet + + Geplante Aufgabe + - Aufgabenplanung + Geplante Aufgaben + + + Abbruch erzwungen durch Herunterfahren der Anwendung + + + Die geplante Aufgabe "{0}" wurde abgebrochen + + + Abbruchanforderung wurde übermittelt. + + + Cron Ausdruck + + + Ein Ausdruck, der den Zeitplan für die automatische Ausführung der Aufgabe festlegt. + + + Hilfe zu <a href='{0}' target='_blank'>Cron-Ausdrücken</a> + + + Dauer der letzten Ausführung ([Std.]:[Min.]:[Sek.]) + + + Aufgabe bearbeiten Aktiviert + + Aktiviert die geplante Ausführung der Aufgabe gemäß Cron Ausdruck + + + Zukünftige Zeitpläne + + + Der Cron-Ausdruck ist ungültig + Zuletzt beendet Letzte Ausführung + + Startdatum der letzten Ausführung + Letzte erfolgreiche Ausführung + + Startdatum der letzten erfolgreichen Ausführung + Name Ein Name für die Aufgabe ist erforderlich. - - Nach dem eine Aufgabe verändert wurde, ist ein Neustart erforderlich. + + Nächste Ausführung in + + + Datum der nächsten geplanten Ausführung + + + Fehler beim Ausführen der Aufgabe "{0}" Jetzt ausführen - - Ausführung der Aufgabe abgeschlossen - Wird ausgeführt... Aufgabe wird jetzt im Hintergrund ausgeführt - - Sekunden (Intervall) + + Die Aufgabe wird jetzt im Hintergrund ausgeführt. Sie erhalten eine E-Mail, sobald sie abgeschlossen ist. Den Fortschritt können Sie in der Exportprofilliste verfolgen. + + + Die Aufgabe wird jetzt im Hintergrund ausgeführt. Sie erhalten eine E-Mail, sobald sie abgeschlossen ist. Den Fortschritt können Sie in der Importprofilliste verfolgen. - - Sekunden müssen größer als 0 sein. + + Aufgabe wurde erfolgreich ausgeführt + + + Ausführung planen - Bei Fehler anhalten + Bei Fehler deaktivieren + + + Aktivieren Sie das Kästchen, wenn die Aufgabe bei Auftreten eines Fehlers während der Ausführung deaktiviert werden soll + + + Die Aufgabe kann nicht bearbeitet werden, während sie ausgeführt wird. + + + Die Aufgabe wurde erfolgreich bearbeitet SEO Namen - - Lösche die ausgewählten Elemente + + Pro Sprache darf nur ein aktiver SEO Name festgelegt werden. - ID der Entität + Objekt-ID + + + Legt die ID des zugehörigen Objektes fest. - Name der Entität + Objekt + + + Legt den Namen der zugehörigen Objektes fest. Ist aktiv + + Legt fest, ob der SEO Name aktiv oder inaktiv ist. + Sprache + + Legt die Sprache des SEO Namens fest. + Standard - Name + SEO Name - Der zu findende Name. + Legt den SEO Namen fest. + + + Namen pro Objekt + + + Die Anzahl der SEO Namen pro Objekt. + + + Geplante Aufgabe + + + PDF-Konvertierer + + + Suchmaschine Systeminformation @@ -10654,6 +11920,12 @@ Der Name des Daten-Providers. + + Aufräumen + + + Der Arbeitsspeicher wurde erfolgreich aufgeräumt. + HTTP_HOST @@ -10690,6 +11962,9 @@ Zeitzone des Servers + + Benutzter Speicher (RAM) + GMT/UTC @@ -10699,6 +11974,12 @@ Warnungen + + Zugriffsverweigerung durch anonyme Anfrage bei {0}. + + + Zugriffsverweigerung durch Kunde #{0} '{1}' bei {2}. + Die Standard-Maßeinheit wurde nicht festgelegt. @@ -10717,6 +11998,9 @@ Die Standard-Gewichtseinheit wurde festgelegt. + + Bitte nur Ziffern eingeben. + Alle Verzeichnisberechtigungen sind OK. @@ -10741,6 +12025,15 @@ '{0}' Plugin ist nicht kompatibel mit Ihrer SmartStore.NET-Version. Löschen Sie es oder installieren Sie die richtige Version. + + Es sind keine Kundengruppen festgelegt. + + + Es sind keine Zugriffsrechte festgelegt. + + + Keine Versand-Artikel + Es existieren keine aktiven Zahlungsarten. @@ -10756,6 +12049,15 @@ Es wird empfohlen, nur eine Versandkosten-Offline-Berechnungsmethode zu benutzen. + + Die Erreichbarkeit der Sitemap konnte nicht überprüft werden. + + + Die Sitemap für den Shop ist erreichbar. + + + Die Sitemap für den Shop ist nicht erreichbar. + Die angegebene URL stimmt mit der URL des Shops überein. @@ -11041,6 +12343,21 @@ Tabellen/Container + + Bitte geben Sie eine gültige E-Mail Adresse ein + + + Bitte geben Sie einen Namen ein + + + Bitte geben Sie eine gültige URL ein + + + Bitte geben Sie Benutzername und Passwort ein + + + Der Wert muss größer 0 sein. + API ist nicht erreichbar. @@ -11203,6 +12520,9 @@ Zur Kasse + + Ein anonymer Checkout ist nicht zulässig. + Rechnungsanschrift @@ -11227,12 +12547,21 @@ Weiter einkaufen + + Bitte stimmen Sie der Nutzungsvereinbarung für herunterladbare Produkte zu. + Rechnungsanschrift eingeben Versandanschrift eingeben + + Ja, ich möchte sofort Zugang zu dem digitalen Inhalt und weiß, dass mein Widerrufsrecht mit dem Zugang erlischt. + + + Bitte bestätigen Sie, dass Sie sofort Zugang zu dem digitalen Inhalt wünschen. + Bitte warten Sie einige Sekunden, bevor Sie einen neuen Auftrag platzieren. @@ -11326,6 +12655,9 @@ Übermittele Auftragsinformationen + + Newsletter abonnieren + Geschäftsbedingungen (AGB) @@ -11410,6 +12742,9 @@ Abgebrochen + + Der API-Aufruf zur Prüfung eines CAPTCHAs ist fehlgeschlagen. + Produkt bearbeiten @@ -11431,6 +12766,15 @@ Fortsetzen + + In die Zwischenablage kopieren + + + Kopieren ist fehlgeschlagen. + + + Kopiert! + Anzahl @@ -11443,6 +12787,9 @@ Querverweise + + Die von Ihnen gewählte Kundennummer existiert bereits. Bitte geben Sie eine andere Kundennummer an. + Datum @@ -11461,36 +12808,84 @@ Sind Sie sicher "{0}" zu löschen? + + Bereitstellung + Beschreibung Beschreibung + + Detailbeschreibung + Reihenfolge Download + + Für die Produktvariante ist der Download einer Beispieldatei nicht verfügbar. + + + Sie haben die maximale Anzahl an Downloads {0} erreicht. + + + Es sind keine Daten zum Herunterladen mehr verfügbar. + + + Downloads sind nicht gestattet. + + + Der Download ist nicht mehr verfügbar. + + + Der Download einer Beispieldatei ist nicht mehr verfügbar. + Dauer Bearbeiten + + Aktiviert + Wert + + Klicken Sie auf ein Element, um es aus- bzw. abzuwählen und OK, um die Auswahl zu übernehmen. + + + Es wurden keine weiteren Elemente gefunden. + + + Klicken Sie auf ein Element, um es auszuwählen und OK, um es zu übernehmen. + Fehler + + Die E-Mail-Adresse ist ungültig. + + + Es wurde keine aktive Sprache gefunden. + + + Es wurde kein E-Mail-Konto gefunden. + Die gewählte Zahlungsart verursachte leider einen Fehler. Bitte korrigieren Sie Ihre Eingaben, versuchen Sie es erneut oder wählen Sie eine andere Zahlungsart. Fehler beim Versenden der Email. Bitte versuchen Sie es später erneut. + + Beispiel + Ausführung @@ -11518,12 +12913,21 @@ Dateien für den Upload hier ablegen + + URL eingeben + Fehlgeschlagen Datei hochladen + + Filter + + + Versandkostenfrei + Anzeigename @@ -11539,6 +12943,9 @@ Startseite + + Bild + Importieren @@ -11548,12 +12955,21 @@ Aktiv + + Sprache + Liste + + Lade + Lade nächsten Schritt… + + Diese Funktion steht für Gäste nicht zur Verfügung. + Verschiedenes @@ -11563,6 +12979,9 @@ Mehr Info + + Mein + Navigation @@ -11578,6 +12997,12 @@ Nein, abbrechen + + Es sind keine Dateien vorhanden. + + + Es wurde keine Datei hochgeladen. + Der Vorgang wurde aus Sicherheitsgründen nicht ausgeführt. @@ -11587,6 +13012,9 @@ Benachrichtigung + + Nicht auswählbar + Aus @@ -11602,6 +13030,12 @@ Optional + + Optionen + + + Aufteilung + Capture wird nicht unterstützt @@ -11626,6 +13060,24 @@ Artikel + + Profil + + + Projektion + + + Provider + + + öffentlich + + + Veröffentlicht + + + Veröffentlichung + Frage @@ -11638,9 +13090,21 @@ Entfernen + + Ersetzen + + + Die Anfrage konnte nicht ausgeführt werden.<br />Controller: {0}, Action: {1}, Grund: {2}. + + + Regel + Speichern + + Geplant + Suchen @@ -11674,6 +13138,12 @@ Anzeigen + + Alle anzeigen + + + Mehr anzeigen + Verkleinern @@ -11686,11 +13156,23 @@ Systemname + + Nicht verfügbar + undefiniert + + Unbekannt + + + Unveröffentlicht + + + Ungeplant + - nicht spezifiziert + Nicht spezifiziert Geändert am @@ -11707,11 +13189,17 @@ Bitte warten... + + Wartend + Warnung + + Web-Seite + - Die von Ihnen eingegebenen Zeichen stimmen nicht mit dem Bild überein. Bitte versuchen Sie es erneut. + Bitte bestätigen Sie, dass Sie kein "Roboter" sind. Falsche E-Mail @@ -11761,6 +13249,15 @@ Namen eingeben + + Einwilligungserklärung Datenschutz + + + Ja, ich habe die <a href="{0}">Datenschutzerklärung</a> zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt. + + + Bitte stimmen Sie der Speicherung Ihrer Daten zu. + Ihre Anfrage wurde erfolgreich übermittelt. @@ -11770,6 +13267,9 @@ Währungen + + Der Kunde existiert nicht. + Gast @@ -11791,6 +13291,9 @@ Produkt + + Das Produkt besitzt keine Nutzungsvereinbarung. + Ich bin einverstanden. @@ -11800,6 +13303,9 @@ Nutzungsvereinbarung + + Ja, ich stimme der <a href='javascript:void(0)' data-id='{0}' class='download-user-agreement'>Nutzungsvereinbarung</a> für dieses Produkt zu. + Kontrollkästchen @@ -11866,6 +13372,18 @@ Lagerbestand mit Attributen führen + + Keine Preisanzeige + + + Minimal realisierbarer Preis + + + Auf der Detailseite vorgewählter Preis + + + Preis ohne Rabatte und Attribute + Erstellt am @@ -11956,6 +13474,27 @@ Benutzernamen anzeigen + + Automatisch vergeben + + + Deaktiviert + + + Aktiviert + + + Anzeigen + + + Immer editierbar + + + Editierbar falls leer + + + Nicht anzeigen + Lesbar @@ -11965,6 +13504,102 @@ Gehashed + + Alle Werte an den Produktnamen anhängen + + + Nicht spezifiziert + + + E-Mail + + + Dateisystem + + + FTP + + + HTTP POST + + + Öffentlicher Ordner + + + Detailbeschreibung + + + Hersteller + Produktname + Detailbeschreibung + + + Hersteller + Produktname + Kurzbeschreibung + + + Produktname + Detailbeschreibung + + + Produktname + Kurzbeschreibung + + + Keine + + + Kurzbeschreibung + + + Kurzbeschreibung oder Name falls leer + + + Warengruppe + + + Kunde + + + Hersteller + + + Newsletter Abonnenten + + + Auftrag + + + Produkt + + + Multipart-Form-Data POST + + + Einfacher POST + + + Komplett + + + Keine + + + Wird bearbeitet + + + Warengruppe + + + Kunde + + + Newsletter Abonnent + + + Produkt + + + Trennzeichen getrennte Werte (.csv, .txt, .tab) + + + Excel (.xlsx) + Nur N mal @@ -12049,6 +13684,24 @@ Warnung + + Aktiviert anzeigen + + + Deaktiviert anzeigen + + + Nicht anzeigen + + + Aktiviert anzeigen + + + Deaktiviert anzeigen + + + Nicht anzeigen + Abgebrochen @@ -12338,9 +13991,6 @@ Forenthemen mit den neuesten Beiträgen - - {0} - Forum: {1} - Forumname @@ -12581,6 +14231,9 @@ Informationen + + Die Installationssprache '{0}' ist nicht registriert. + Bitte geben Sie eine gültige Kreditkartennummer ein. @@ -12830,12 +14483,48 @@ Rechnungsanschrift + + Die Rechnungsanschrift fehlt. + + + Die Auftragssumme konnte nicht berechnet werden. + + + Die Versandkosten konnten nicht berechnet werden. + + + Der Auftrag kann nicht storniert werden. + + + Der Auftrag kann nicht gebucht werden. + + + Der Auftrag kann nicht als abgeschlossen markiert werden. + + + Der Auftrag kann nicht als bezahlt markiert werden. + + + Eine Teilrückerstattung ist für diesen Auftrag nicht möglich. + + + Eine Rückerstattung ist für diesen Auftrag nicht möglich. + + + Eine Stornierung dieses Auftrages ist nicht möglich. + Zahlung veranlassen Der Auftrag wurde noch nicht bezahlt. Um die Zahlung nun vorzunehmen, klicken Sie den Button *Zahlung veranlassen* + + Eine Rechnungslegung ist für das Land '{0}' unzulässig. + + + Ein Versand ist für das Land '{0}' unzulässig. + E-Mail @@ -12848,6 +14537,12 @@ Geschenkgutschein ({0}) + + Für die wiederkehrende Zahlung existiert kein Ausgangsauftrag. + + + Keine wiederkehrenden Produkte. + Notizen @@ -12857,6 +14552,9 @@ Notiz + + Der Auftrag {0} wurde nicht gefunden. + Auftragsnummer @@ -12878,6 +14576,9 @@ Zahlungsgebühr + + bestellung-{0}.pdf + Telefon @@ -12989,6 +14690,9 @@ Versandanschrift + + Die Lieferanschrift fehlt. + Versandart @@ -13139,6 +14843,9 @@ Wunschliste per E-Mail an einen Freund senden + + Das Datum der nächsten Zahlung kann nicht ermittelt werden. + Kartencode @@ -13163,6 +14870,9 @@ Falsche Kartennummer + + Die Zahlungsart konnte nicht geladen werden. + Gültig bis @@ -13172,6 +14882,24 @@ Ablaufjahr ist erforderlich + + Die Zahlungsart steht nicht zur Verfügung. + + + Mindestens ein Zahlungsart-Provider muss aktiviert sein. + + + Leider können wir diesen Einkauf nicht über die gewünschte Zahlungsart abwickeln. Bitte wählen Sie eine alternative Zahlungsoption aus, um Ihre Bestellung abzuschließen. + + + Wiederkehrende Zahlung ist inaktiv. + + + Wiederkehrende Zahlungen sind für die gewählte Zahlungsart nicht möglich. + + + Der Typ von wiederkehrender Zahlung wird nicht unterstützt. + Wähle Kreditkarte aus @@ -13289,12 +15017,6 @@ Gewicht - - Gruppierte Produkte - - - Produktset besteht aus - E-Mail @@ -13310,32 +15032,14 @@ Kontaktdaten - - Höhe - - - Länge - - - Hersteller - - - Preis - - - Artikelnummer - - - Spezifikation - Lagermenge - - Gewicht + + Eine Umfrageantwort {0} wurde nicht gefunden. - - Breite + + Die Umfrage ist nicht verfügbar. Nur registrierte Benutzer können abstimmen @@ -13358,6 +15062,9 @@ Datenschutzerklärung + + Private Nachrichten sind deaktiviert. + Posteingang @@ -13488,7 +15195,10 @@ Nicht am Lager - Grundpreis: {0} pro {1} + Inhalt: {0} {1} ({2} / {3} {1}) + + + {0} {1} ({2} / {3} {1}) Home @@ -13689,7 +15399,10 @@ Dieses Produkt ist ausverkauft. - Dieses Produkt ist ausverkauft. + Keine Bundle-Bestandteile vorhanden + + + Das Produkt {0} wurde nicht gefunden. Preis: @@ -13769,6 +15482,9 @@ Menge + + Die Produktvariante {0} wurde nicht gefunden. + Gewicht @@ -13814,6 +15530,9 @@ Der EZB-Wechselkursdienst kann nur genutzt werden, wenn der Wechselkurs-Währungscode auf EUR gesetzt ist. + + Der EZB-Wechselkursdienst kann nur genutzt werden, wenn der Wechselkurs-Währungscode auf EUR gesetzt ist. + EZB-Wechselkursdienst @@ -13904,6 +15623,9 @@ Sie können nicht Ihre eigene Bewertung beurteilen. + + Die Produktbewertung {0} wurde nicht gefunden. + Nur registrierte Benutzer können eine Bewertung verfassen. @@ -14018,6 +15740,18 @@ Die Mindestlänge für den Suchbegriff beträgt {0} Buchstaben. + + Diese Sendung wird bereits zugestellt. + + + Diese Sendung wird bereits ausgeliefert. + + + Die Berechnungsmethode für Versandkosten konnte nicht geladen werden. + + + Mindestens ein Provider zur Berechnung von Versandkosten muss aktiviert sein. + Versandinfos @@ -14150,6 +15884,9 @@ Die eingegebene Nummer des Geschenkgutscheines konnte nicht angewendet werden. + + Der Warenkorb ist deaktiviert. + Gesamt @@ -14262,7 +15999,7 @@ Geben Sie einen gültigen Absender-Namen ein. - Die maximale Anzahl von Produkten in Ihrer Wunschliste wurde erreicht + Eine vollständige Liste aller Versandkosten finden Sie <a href="{0}">hier</a>. Artikelnummer @@ -14343,10 +16080,10 @@ Shop Name hinter Seiten Name - Dieser Shop ist zur Zeit geschlossen. + Wir sind bald wieder da. - Bitte Besuchen Sie uns später. + Wir aktualisieren gerade das Angebot in unserem Online-Shop. Die Seite ist demnächst wieder verfügbar. Theme für den Shop auswählen. @@ -14366,9 +16103,15 @@ * Alle Preise {0}, zzgl. <a href="{1}">Versandkosten</a> + + * Alle Preise {0}, zzgl. Versandkosten + {0} {1} {2} zzgl. <a href="{3}">Versandkosten</a> + + {0} {1} {2} zzgl. Versandkosten + {0} {1} {2} zzgl. <a href="{3}">Versandkosten</a> @@ -14735,15 +16478,57 @@ Well-Hintergrundfarbe + + Tag + + + Tg. + + + Tage + + + Tg. + Vor {0} Tagen + + Stunde + + + Std. + + + Stunden + + + Std. + Vor {0} Stunden + + Minute + + + Min. + + + Minuten + + + Min. + Vor {0} Minuten + + Monat + + + Monate + Vor {0} Monaten @@ -14765,9 +16550,33 @@ Vor einem Jahr + + Sekunde + + + Sek. + + + Sekunden + + + Sek. + Vor {0} Sekunden + + Woche + + + Wochen + + + Jahr + + + Jahre + Vor {0} Jahren @@ -14828,6 +16637,9 @@ Bitte geben Sie Ihre E-Mail-Adresse ein. + + Die Wunschliste ist deaktiviert. + Wunschliste ansehen diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt index afe64e4f3b..f822a9cb76 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt @@ -1 +1 @@ -201506181858349_AclRecordCustomerRole \ No newline at end of file +201605201911421_ExportRevision \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml index 1faea7a6fb..734f05e1de 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml @@ -135,6 +135,9 @@ No orders + + This is not your order. + Order Date @@ -489,6 +492,15 @@ Register + + The customer is already registered. + + + A search engine can't be registered. + + + A background task account can't be registered. + The specified email already exists @@ -597,6 +609,9 @@ Deleted a manufacturer ('{0}') + + Deleted order {0} + Deleted a product ('{0}') @@ -1401,6 +1416,9 @@ The category has been deleted successfully. + + Show other description + Discounts @@ -1450,10 +1468,10 @@ Deleted - Description + Top description - The description of the category. + Description of the category that is displayed above products on the category page. Display order @@ -1512,6 +1530,9 @@ Select a parent category for this category. Leave this field empty to make this the root level category + + Parent category + Picture @@ -1990,6 +2011,9 @@ Products with attribute values of type "product" cannot be part of a bundle. + + Notes on product bundles + You need to save the product before you can add bundled products for this product page. @@ -2039,7 +2063,7 @@ Cross-sells - Add new cross-sell product + Add checkout-selling product Product @@ -2048,7 +2072,7 @@ Product - You need to save the product before you can add cross-sell products for this product page. + You need to save the product before you can add checkout-selling products for this product page. The product has been deleted successfully. @@ -2101,6 +2125,12 @@ Enter a comma separated list of quantities you want this product to be restricted to. Instead of a quantity textbox that allows them to enter any quantity, they will receive a dropdown list of the values you enter here. + + Approved rating sum + + + Approved total reviews + Associated to product @@ -2165,7 +2195,7 @@ {0} per unit (base price: {1} per {2}) - "Measure unit" is required to calculate the base price. + Base price measure unit "Measure unit" is required to calculate the base price. @@ -2272,12 +2302,18 @@ Enter global trade item number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). + + Has discounts applied + Has sample download file Check the box if this product has a sample download file that can be downloaded before checkout. + + Has tier prices + Has user agreement @@ -2290,6 +2326,12 @@ The height of the product. + + Homepage display order + + + Specifies the display order for products displayed on homepage. 1 represents the first element in the list. + ID @@ -2344,6 +2386,9 @@ The length of the product. + + Lowest attribute combination price + Low stock activity @@ -2413,6 +2458,12 @@ Please provide a name. + + Not approved rating sum + + + Not approved total reviews + Notify admin for quantity below @@ -2638,6 +2689,18 @@ Search by a specific category. + + Showed on home page + + + Filters for products displayed or not displayed on homepage. + + + Published + + + Filters for published or unpublished products. + Manufacturer @@ -2909,7 +2972,7 @@ Related products - Add new related product + Add cross-selling product Display order @@ -2921,7 +2984,7 @@ Product - You need to save the product before you can add related products for this product page. + You need to save the product before you can add cross-selling products for this product page. Specification attributes @@ -3130,12 +3193,15 @@ There were {0} mutual association(s) created. - - CSV file + + CSV Configuration The data was successfully changed. + + Data exchange + The data were saved successfully. @@ -3148,38 +3214,59 @@ Delete (selected) + + Delete all + Are you sure you want to delete this item? Are you sure you want to delete "{0}"? + + Deleted + Delete selected Edit + + The email has been successfully sent. + + + Please enter an email address. + + + Object + ID The unique numeric id of the entity + + Errors + Error sending an email - - Excel file + + PDF Export Please wait while the export is being executed + + Export all + No data to export. - - Export to CSV + + Export selected Export to Excel @@ -3196,20 +3283,29 @@ Export to PDF (selected) - - Table of contents - Too many items! The PDF conversion is limited to 500 items. Please reduce the amount of selected records. Export to XML - - Export to XML (all) + + The file is in use and cannot be opened. + + + File not found + + + {0} files were deleted + + + The file must be of the type {0}. + + + {0} folders were deleted - - Export to XML (selected) + + FTP status {0} ({1}). General @@ -3235,41 +3331,17 @@ Hide - - Import from CSV - - - Import from Excel - - - Active since: {0}. - - - Cancel import - - - Import process has been cancelled - - - Do you really want to cancel the import? Products imported so far will not be removed. - - - Download full report... - - - The import is being performed in the background now. You can view the progress or the result of the last completed import in the import dialog at any time. - - - <b>Last import</b>: {0}{1}. + + HTTP status {0} ({1}). - - No report available + + Ignore - - {0} of {1} rows processed. + + Import file - - {0} new, {1} updated - with {2} warning(s) und {3} error(s). + + Import files Please wait while the import is being executed @@ -3277,6 +3349,9 @@ Info + + Last run + License @@ -3289,18 +3364,51 @@ You are going to lose any unsaved changes. Are you sure? + + New records + No No, cancel + + No entries have been selected. + + + of + + + Placeholder + Please select Preview + + Cannot load the provider {0}. + + + Public files + + + {0} records were deleted. + + + Skip + + + Specifies the number of records to be skipped. + + + Limit + + + Specifies the maximum number of records to be processed. + Restore defaults @@ -3310,6 +3418,9 @@ You must restart the application for the changes to take effect. + + Restrictions + Save @@ -3319,6 +3430,9 @@ Search + + Selected + Send now @@ -3331,6 +3445,12 @@ Show + + Values for skip and limit must be greater than or equal to 0. + + + Skipped + Standard @@ -3361,18 +3481,30 @@ All stores + + Successful on + The task was successfully processed. + + Total rows + Tree view Unlicensed + + Unsupported entity type '{0}' + Update + + Updated + Please upload a file @@ -3382,6 +3514,9 @@ Please wait while processing is running... + + Warnings + Wrong email @@ -3418,9 +3553,6 @@ The Activity Log Type. - - Activity Log Type - Message @@ -3445,6 +3577,15 @@ Customer Email + + Filters results by customer email address. + + + Customer system account + + + Filters results by customer system accounts. + Activity Types @@ -3628,6 +3769,9 @@ back to country list + + The country cannot be deleted because it has associated addresses. + The country has been deleted successfully. @@ -3719,7 +3863,7 @@ Add a new state/province - The state can't be deleted. It has associated addresses. + The state\province cannot be deleted because it has associated addresses. Edit state/province @@ -3775,15 +3919,12 @@ back to currency list - - The primary exchange rate currency can't be deleted. - - - The primary store currency can't be deleted. - The currency has been deleted successfully. + + The currency cannot be deleted or deactivated because it is attached to the store "{0}" as primary or exchange rate currency. + Edit currency details @@ -3845,16 +3986,10 @@ Current exchange rate provider - Is primary exchange rate currency + Exchange rate currency - Is primary store currency - - - Mark as primary exchange rate currency - - - Mark as primary store currency + Primary currency Name @@ -3868,6 +4003,18 @@ Please provide a name. + + Is exchange rate currency for + + + A list of stores where the currency is primary exchange rate currency. + + + Is primary store currency for + + + A list of stores where the currency is primary store currency. + Published @@ -4048,6 +4195,9 @@ Email has been successfully sent. + + Testing email functionality. + The email account has been updated successfully. @@ -4094,10 +4244,10 @@ The display order of this language. 1 represents the top of the list. - Flag image file name + Flag image - The flag image file name. The image should be saved into \\images\\flags\\ directory. + Specifies the flag image. The files for the flag images must be stored in /Content/Images/flags/. Language culture @@ -4139,7 +4289,7 @@ Two letter SEO code should be 2 characters long. - Please provide a unique SEO code. + Please select a SEO language code. Import resources @@ -4318,6 +4468,21 @@ Supports void + + Full description + + + Specifies a full description of the payment method. It appears in the payment list in checkout. + + + There were no possibilities found to restrict payment methods. + + + Short description + + + Specifies a short description of the payment method. + Plugins @@ -4456,6 +4621,9 @@ The plugin has been uninstalled. + + An unknown error occurred when calling a plugin. Please refer to the following message for details. + Quantity units @@ -4486,6 +4654,9 @@ Regional Settings + + You need to save before you can specify restrictions. + Settings @@ -4528,6 +4699,12 @@ Check the box to enable the blog in your store. + + Maximum age (in days) + + + Specifies the maximum news age in days. Older blog posts are not exported in the RSS feed. + Notify about new blog comments @@ -4550,7 +4727,7 @@ Display blog RSS feed link in the browser address bar - Check the box to enable the blog RSS feed link in customers browser address bar + Check to enable the blog RSS feed link in customers browser address bar. Catalog settings @@ -4604,11 +4781,17 @@ Check the box to allow customers to use the 'Compare Products' option in your store - Amount of displayed products per page + Number of displayed products per page Determines the amount of displayed products per page. + + Default product sort order + + + Specifies the default product sort order. + Default view mode @@ -4663,6 +4846,24 @@ Check the box to hide the buy-button in product lists. + + Hide default picture for categories + + + Specifies whether to hide the default image for categories. The default image is shown when no image is assigned to a category. + + + Hide default picture for manufacturers + + + Specifies whether to hide the default image for manufacturers. The default image is shown when no image is assigned to a manufacturer. + + + Hide default picture for products + + + Specifies whether to hide the default image for products. The default image is shown when no image is assigned to a product. + Height of truncated long text @@ -4738,6 +4939,12 @@ The number of product tags that appear in the tag cloud + + Price display + + + Specifies whether or what type of price to be displayed in product lists. + Product detail @@ -4855,6 +5062,12 @@ The number of 'Recently viewed products' to display when 'Recently viewed products' option is enabled. + + Search product description + + + Specifies whether the product description should be included in the search. + Search page. Products per page @@ -4951,6 +5164,12 @@ Specifies whether manufacturers will be displayed as images or textual links on the homepage. + + Show manufacturer pictures + + + Specifies whether to show manufacturer pictures on product detail page. + Show manufacturers on homepage @@ -5005,6 +5224,12 @@ Check the box to display the weight. When enabled, customers will see the weight of the product. + + Sort filter results by number of matches + + + Specifies to sort filter results by number of matches in descending order. If this option is deactivated then the result is sorted by the display order of the values. + Show subcategories @@ -5185,6 +5410,12 @@ Set if 'Country' is enabled. + + Customers can enter a customer number + + + Specifies whether customers can enter a customer number if the customer number doesn't contain a value yet. + Customer form fields @@ -5209,6 +5440,18 @@ Specifies whether customer numbers can be saved. + + Customer numbers + + + Specifies whether to assign customer numbers and whether they should be created automatically. + + + Customer number presentation + + + Specifies the presentation and handling of the customer number to the customer. + Customer settings @@ -5239,6 +5482,18 @@ The default store time zone used to display dates. + + Display customer numbers in frontend + + + Specifies whether customer numbers will be displayed to customers in their account area. + + + Get privacy consent for contact requests + + + Specifies whether a checkbox will be displayed on the contact page which requests the user to agree on storage of his data. + Auto register enabled @@ -5308,6 +5563,12 @@ Check the box if 'Phone number' is required. + + Customer role at registrations + + + Specifies a customer role that will be assigned to newly registered customers. + Show customers' join date @@ -5386,6 +5647,24 @@ Check the box if 'Zip / postal code' is required. + + Timeout for image download (minutes) + + + Specifies the timeout for the image download in minutes. + + + Image folder (relative path) + + + Specifies a relative path to a folder with images to be imported (e.g. Content\Images). + + + Maximum length of file and folder names + + + Specifies the maximum length of file and folder names created during an import or export. + Forum settings @@ -5530,6 +5809,24 @@ IP addresses allowed to access the Back End. Leave this field empty if you do not want to restrict access to the Back End. Use comma to separate them (e.g. 127.0.0.10,232.18.204.16) + + Allow unicode characters + + + Check whether SEO names can contain letters that are classified as unicode characters. + + + Attach order PDF to 'Order Completed' email + + + Dynamically creates and attaches the order PDF to the 'Order Completed' customer notification email. + + + Attach order PDF to 'Order Placed' email + + + Dynamically creates and attaches the order PDF to the 'Order Placed' customer notification email. + Bank connection @@ -5875,6 +6172,12 @@ Encryption private key must be 16 characters long + + Extra Disallows for robots.txt + + + Enter additional paths that should be included as Disallow entries in your robots.txt. Each entry has to be entered in a new line. + Full-Text settings @@ -5929,6 +6232,12 @@ Localization settings + + Meta robots + + + Specifies if and how search engines indexing the pages of your store. + Page title SEO adjustment @@ -5983,6 +6292,12 @@ When enabled, your URLs will be http://www.yourStore.com/en/ or http://www.yourStore.com/ru/ (SEO friendly) + + Characters to be converted + + + Allows an individual conversion of characters for SEO name creation. Enter the old and the new character separated by a semicolon, e.g. ä;ae. Each entry has to be entered in a new line. + SEO settings @@ -6037,6 +6352,12 @@ Store information + + Check string + + + Enter any string to check the SEO name creation. Changed settings must be saved before. + Use images for language selection @@ -6097,6 +6418,12 @@ The maximum image size (longest side) allowed for image uploads. + + Thumbnail size of products in emails + + + Specifies the thumbnail image size (pixels) of products in emails. Enter 0 to not display thumbnails. + Mini-shopping cart thumbnail image size @@ -6175,6 +6502,12 @@ The number of news items to display on your home page. + + Maximum age (in days) + + + Specifies the maximum news age in days. Older news are not exported in the RSS feed. + News archive page size @@ -6217,6 +6550,12 @@ When disabled, customers will be automatically redirected to the order details page. + + Display orders of all stores + + + Specifies whether to display the orders of all stores to the customer. If this option is disabled, only the orders of the current store are displayed. + Gift card activation order status @@ -6271,6 +6610,12 @@ Set the order ID counter. This is useful if you want your orders to start at a certain number. This only affects orders created going forward. The value must be greater than the current maximum order ID. + + Number of displayed orders per page + + + Specifies the number of displayed orders per page. + Order settings @@ -6376,6 +6721,12 @@ Specify number of points awarded for registration. + + Round down points + + + Specifies whether to round down calculated points. Otherwise the bonus points will be rounded up. + Shipping settings @@ -6490,6 +6841,15 @@ Check the box to move products from wishlist to the cart when clicking "Add to cart" button. Otherwise, they are copied. + + Subscribe to newsletters + + + Specifies id customers can subscribe to newsletters when ordering and if the checkbox is enabled by default. + + + Order confirmation page + Round prices during calculation @@ -6503,16 +6863,16 @@ Determines whether base price should be displayed in the shopping cart. - Show comment box on confirm order page + Show comment box - Determines whether comment box is displayed on confirm order page + Specifies whether comment box is displayed on the order confirmation page. - Show legal hints in order summary on the confirm order page + Show legal hints in order summary - Determines whether to show hints in order summary on the confirm order page. This text can be altered in the language resources. + Specifies whether to show hints in order summary on the confirm order page. This text can be altered in the language resources. Display delivery times @@ -6526,6 +6886,12 @@ Check the box if you want the discount coupon box to be displayed on shopping cart page + + Show revocation waiver box for electronic services + + + Specifies whether the customer must agree a revocation waiver for electronic services on the order confirmation page. + Show gift card box @@ -6580,6 +6946,21 @@ Determines whether the product weight is shown in shopping cart + + Consent for email transfer to third parties + + + Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout. + + + Text for email transfer consent + + + I agree to the transfer and storage of my email address by third parties. + + + Specifies the text to be displayed to the customer. Please choose a specific reason, e.g. 'I agree to the transfer and storage of my email address for TrustedShops buyer protection.' + Wishlist @@ -6793,6 +7174,12 @@ Please provide a name. + + No shipping methods could be loaded. + + + There were no possibilities found to restrict shipping methods. + The shipping method has been updated successfully. @@ -6811,19 +7198,6 @@ Display order - - Shipping method restrictions - - - Country - - - Please mark the checkbox(es) for the country or countries in which you want the - shipping method(s) not available - - - The settings have been updated successfully. - SMS providers @@ -6896,6 +7270,18 @@ Please provide a name. + + Exchange rate currency + + + Specifies the primary exchange rate currency for this store. + + + Primary store currency + + + Specifies the the primary store currency. + Secure URL @@ -6903,10 +7289,10 @@ The secure URL of your store e.g. https://www.yourstore.com/ or http://sharedssl.yourstore.com/. Leave it empty if you want secure URL to be detected automatically. - SSL enabled + SSL - Check the box if your store will be SSL secured. + Specifies whether the store should be SSL secured. Store logo @@ -7004,6 +7390,9 @@ Name + + Theme requires no configuration + Variables were successfully updated. @@ -7382,6 +7771,24 @@ This is a list of the message tokens you can use in your emails. + + Attachment 1 + + + A file that is to be appended to each sent email (eg Terms, Conditions etc.) + + + Attachment 2 + + + A file that is to be appended to each sent email (eg Terms, Conditions etc.) + + + Attachment 3 + + + A file that is to be appended to each sent email (eg Terms, Conditions etc.) + BCC @@ -7892,6 +8299,9 @@ back to customer role list + + The customer role "{0}" cannot be found. + The customer role has been deleted successfully. @@ -8084,6 +8494,9 @@ The date the customer record is created. + + Customer GUID + Customer roles @@ -8141,6 +8554,9 @@ The last used IP address. + + Is system account + Is tax exempt @@ -8153,6 +8569,9 @@ The last activity date. + + Last login date + Last name @@ -8174,6 +8593,9 @@ The customer's password. + + Password salt + Phone @@ -8546,101 +8968,824 @@ Store Statistics - - Download uploaded file + + New profile - - Download uploaded file + + Default Value - - Download URL + + Object property - - Download object is saved + + Import Field - - Remove download + + You can optionally set for each field of the import file whether and for which object property the data should be imported. Fields with equal names are always imported as long as they are not explicitly ignored. Not yet selected properties are highlighted in the selection list. It is also possible to define a default value which is applied when the import field is empty. Stored assignments becomes invalid and reset when the delimiter changes. - - Save download + + The stored field assignments are invalid due to the change of the delimiter and were reset. - - Upload file + + Delimiter - - Use download URL + + Specifies the field separator. - - Gift cards + + Please enter a valid delimiter. - - The new gift card has been added successfully. + + Inner quote character - - Add a new gift card + + Specifies the inner quote character used for escaping. - - back to gift card list + + Please enter a valid inner quote character (escaping). - - The gift card has been deleted successfully. + + Delimiter and inner quote character cannot be equal in CSV files. - - Edit gift card details + + Quote character - - Initial value + + Specifies the quotation character. - - Initial value + + Please enter a valid quote character. - - The initial value of this gift card. + + Quote all fields - - Creation date + + Specifies whether to set quotation marks around all field values. - - The date/time the gift card was created/purchased. + + Delimiter and quote character cannot be equal in CSV files. - - Coupon code + + Supports multilines - - Generate code + + Specifies whether field values with multilines are supported. - - The gift card coupon code (used during checkout). + + Trim values - - Gift card type + + Specifies whether to remove space characters at start and end of a field value. - - The gift card type. + + Batch size - - Is gift card activated + + Specifies the maximum number of records per export file. 0 is the default and means that all the records are exported in one file. - - Determines whether gift this card is activated and can be used. + + Cannot delete a system export profile. - - Is recipient notified + + Clean up after successful deployment - - Is recipient notified. + + Specifies whether to delete unneeded files after all deployments succeeded. - - Notify recipient + + Apply settings from - - Message + + Specifies an export profile from which to apply the settings. - - Enter message. + + This is an automatic notification of store "{0}" about a recent data export. - - Order + + Export of profile "{0}" has been finished + + + Email addresses to + + + Specifies the email addresses where to send the notification message. + + + The following specific information will be taken into account by the provider during the export. + + + The export provider <b>{0}</b> requires no further configuration. + + + Create ZIP archive + + + Specifies whether to combine the export files in temporary a ZIP archive. The archive remains in the temporary folder of the export profile without further processing. + + + At least one file could not be copied. + + + Specifies whether to combine the export files in a ZIP archive and only to deploy the archive. + + + Publishing type + + + Specifies the type of publishing. + + + Email account + + + Specifies the email account used to sent the data. + + + Email addresses to + + + Specifies the email addresses where to send the data. + + + Email subject + + + Specifies the subject of the email. + + + Directory path + + + Specifies the path (relative or absolute) where to deploy the data. + + + HTTP transmission type + + + Specifies how to transmit the export files via HTTP. + + + Specifies whether to copy the exported data into a folder that is accessible through the internet. + + + Name of profile + + + Specifies the name of the publishing profile. + + + There are no publishing profiles. + + + Click <b>New profile</b> to add one or multiple publishing profiles to specify how to further proceed with the export files. + + + Passive mode + + + Specifies whether to exchange data in active or passive mode. + + + Password + + + Specifies the password. + + + Publishing profiles + + + Publishing target + + + Name of subfolder + + + Specifies the name of a subfolder where to publish the data. + + + URL\Host + + + Specifies the URL or host name where to send the data. + + + User name + + + Specifies the user name. + + + Use SSL + + + Specifies whether to use a SSL (Secure Sockets Layer) connection. + + + If there are a large number of export files, it is recommended to use the option <b>Create ZIP archive</b>. This saves time and avoids problems, such as a full email mailbox. + + + Email notification + + + Specifies the email account used to send a notification message of the completion of the export. + + + The export profile is disabled. It must be enabled to preview the export data. + + + Object + + + The object type the provider processes. + + + Export files + + + File type + + + The file type of the exported data. + + + Pattern for file names + + + Specifies the pattern for creating file names. + + + Please enter a valid pattern for file names. Example for file names: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName% + + + ID of export profil;Folder name of export profil;SEO name of export profil;Store ID;SEO name of store;One based file index;Random number;UTC timestamp + + + Availability to + + + Filter by availability quantity. + + + Availability from + + + Filter by availability quantity. + + + Billing countries + + + Filter by billing countries. + + + Categories + + + Filter by categtories. + + + Created from + + + Filter by created date. + + + Created to + + + Filter by created date. + + + Customer roles + + + Filter by customer roles. + + + Only featured products + + + Filter by featured products. Is only applied when the filtering by categories and manufacturers. + + + Has placed x orders + + + Filter by number of placed orders. + + + Has spent amount x + + + Filter by spent amount. + + + Product Id to + + + Filter by product identifier. + + + Product Id from + + + Filter by product identifier. + + + Only active customers + + + Filter by active or inactive customers. + + + Only active subscribers + + + Filter by active or inactive newsletter subscribers. + + + Published + + + Filter by publishing. + + + Only tax exempt customers + + + Filter by tax exempt customers. + + + Last activity from + + + Filter by date of last store activity. + + + Last active until + + + Filter by date of last store activity. + + + Manufacturer + + + Filter by manufacturer. + + + Specify individual filters to limit the exported data. + + + Order status + + + Filter by order status. + + + Payment status + + + Filter by payment status. + + + Price to + + + Filter by price. + + + Price from + + + Filter by price. + + + Product tag + + + Filter by product tag. + + + Product type + + + Filter by product type. + + + Shipping countries + + + Filter by shipping countries. + + + Shipping status + + + Filter by shipping status. + + + Store + + + Filter by store. + + + Without category mapping + + + Filter by missing category mapping. + + + Without manufacturer mapping + + + Filter by missing manufacturer mapping. + + + Folder path + + + Specifies the relative path of the folder where to export the data. + + + Please enter a valid, relative folder path for the export data. + + + System profile + + + Indicates whether the export profile is a system profile. System profiles cannot be removed. + + + The system export profile {0} was not found. + + + Name of profile + + + Specifies the name of the export profile. + + + There were no export provider found. + + + There is no filtering available. + + + The export provider does not explicit support any file type. Therefore, the export provider is responsible for futher deployment of export data. + + + There is no preview available for this entity type. + + + There were no export profiles found. + + + There was no export profile of type <b>{0}</b> found. Create now a <a href="{1}">new export profile</a>. + + + There is no projection available. + + + This option is not taken into account in the preview. + + + With the following settings you can partition the data to be exported. This includes<ul><li>Skipping the first n records</li><li>The maximum number of records to be exported</li><li>The number of records per export file</li><li>Export data for each shop in a separate file</li></ul>By default, all data of a store will be exported into one file. + + + Partitioning setting values must be greater than or equal to 0. + + + Per store + + + Specifies whether to start a separate run-through for each store. For each shop a new file will be created. + + + Export profile + + + The export profile for this export provider. + + + {0} of {1} records exported + + + Text to be appended + + + Specifies the text to be attached to the product description. If there are multiple texts then one of it is selected randomly. + + + Export attribute combinations + + + Specifies whether to export a standalone product for each active attribute combination. + + + Attribute values + + + Specifies if and how to further process the attribute values. + + + Manufacturer\Brand + + + Specifies the manufacturer or brand to be exported, if a product has no manufacturer assigned. + + + Convert net into gross prices + + + Specifies to convert net into gross prices. + + + Critical characters + + + List with characters to be removed from the detail description. + + + Currency + + + Specifies the currency to be applied to the export. + + + Customer ID + + + Specifies the ID of the customer to be applied to the export. Is taken into account for price calculations for example. + + + Product description + + + Specifies what information to use for the description of the product. + + + Remove HTML from description + + + Specifies whether to remove all HTML from the product description for the export. + + + Free shipping threshold + + + Specifies the amount as from shipping is free. + + + Language + + + Specifies the language to be applied to the export. + + + Do not export grouped products + + + Specifies whether to export grouped products. If this option is activated, then the associated products will be exported. + + + The following information will be taken into account during the export and integrated in the process. + + + Number of pictures + + + Specifies the number of images per object to be exported. + + + Change order status to + + + Specifies if and how to change the status of the exported orders. + + + Product picture size + + + Specifies the size (in pixel) of the product image. + + + Product price + + + Specifies the product price to be exported. + + + Remove critical characters + + + Specifies whether to remove critical characters (like ½) from the detail description. + + + Shipping costs + + + The shipping costs to be exported. + + + Shipping time + + + Specifies the shipping time for products where it is unspecified. + + + Store + + + Specifies the store to be applied to the export. + + + Provider + + + Specifies the export provider. It is responsible for the individual formatting of the export data. + + + System name of profile + + + The system name of the export profile. + + + The following list contains system profiles, which are provided by plugins such as the <a href='http://community.smartstore.com/index.php?/files/file/85-smartstorenet-common-export-providers/' target='_blank'>Data Export Plugin</a>. You can customize system profiles as desired, but cannot create new ones. These profiles also add additional action buttons. You will find these above data lists, such as the product or order list. + + + System profiles + + + User profiles + + + Add import file... + + + Assignment of import fields + + + This is an automatic notification of store "{0}" about a recent data import. Summary: + + + Import of "{0}" has been finished + + + My product import;My category import;My customer import;My newsletter subscription import + + + Upload import file... + + + Key fields + + + Existing records can be identified for updates on the basis of key fields. The key fields are processed in the order how they are defined here. + + + Please only use the ID field as a key field, if the data sourced from the same database to which it will be imported. Otherwise it is possible that the wrong records are updated. + + + Last import result + + + Please upload an import file. + + + For multiple import files please make sure that they are of the same file type and that the content follows the same pattern (e.g. same column headings). + + + Name of profile + + + Specifies the name of the import profile. + + + There were no import profiles found. + + + Number of pictures + + + Specifies the number of images per object to be imported. + + + Please select the import object and upload an import file. + + + {0} of {1} records processed + + + Create new assignment here + + + Only update + + + If this option is enabled, only existing data is updated but no new records are added. + + + At least one key field is required. + + + Download uploaded file + + + Download uploaded file + + + Download URL + + + Download object is saved + + + Remove download + + + Save download + + + Upload file + + + Use download URL + + + Gift cards + + + The new gift card has been added successfully. + + + Add a new gift card + + + back to gift card list + + + The gift card has been deleted successfully. + + + Edit gift card details + + + Initial value + + + Initial value + + + The initial value of this gift card. + + + Creation date + + + The date/time the gift card was created/purchased. + + + Coupon code + + + Generate code + + + The gift card coupon code (used during checkout). + + + Gift card type + + + The gift card type. + + + Is gift card activated + + + Determines whether gift this card is activated and can be used. + + + Is recipient notified + + + Is recipient notified. + + + Notify recipient + + + Message + + + Enter message. + + + Order The gift card was purchased with this order. @@ -8786,6 +9931,12 @@ "Order placed" email (to store owner) has been queued. Queued email identifier: {0} + + Subscribed to newsletter + + + Newsletter subscriber has been removed + Order cancelled @@ -8793,7 +9944,7 @@ Order has been captured - Error capturing order #{0}. Error: {1} + Unable to capture payment for order {0}. Order has been deleted @@ -8817,7 +9968,7 @@ Order has been partially refunded. Amount = {0} - Unable to partially refund order. {0} + Unable to partially refund order {0}. Order placed @@ -8826,7 +9977,7 @@ Order has been refunded. Amount = {0} - Unable to refund order. {0} + Unable to refund order {0}. Order status has been changed to {0} @@ -8835,10 +9986,10 @@ Order has been voided - Unable to void order. {0} + Unable to void payment transaction of order {0}. - Unable to cancel recurring payment. {0} + Unable to cancel recurring payment for order {0}. Recurring payment has been cancelled @@ -8906,6 +10057,12 @@ Affiliate + + Accepts transfer of email + + + Indicates whether the customer has agreed to a transfer of his email address to third parties. + Affiliate @@ -10145,8 +11302,8 @@ Email is required. - - {0} email(s) were imported and {1} updated. + + Subscription GUID Providing plugin @@ -10694,6 +11851,9 @@ Insert your SQL query here + + The SQL command was executed successfully. + Execute @@ -10706,6 +11866,9 @@ back to message queue + + Could not download e-mail attachment: no data. + The queued email has been deleted successfully. @@ -10715,9 +11878,21 @@ Edit message queue item + + An error occured while creating e-mail attachment + + + The e-mail attachment data could not be downloaded from path '{0}' + + + The content type of the e-mail attachment must be 'application/pdf' + Bcc + + Attachments + Bcc @@ -10826,6 +12001,9 @@ End date + + Number of attachments + End date @@ -10883,77 +12061,164 @@ The queued email has been updated successfully. + + Scheduled task + - Schedule tasks + Scheduled Tasks + + + Abnormally aborted due to application shutdown + + + The scheduled task "{0}" has been canceled + + + Cancellation request has been submitted. + + + Cron Expression + + + An expression that defines the schedule for the automatic execution of the task. + + + <a href='{0}' target='_blank'>Cron Expressions</a> help + + + Duration of the latest execution ([h]:[min]:[sec]) + + + Edit task Enabled + + Enables the scheduled execution of the task in accordance with the cron expression + + + Future schedules + + + The cron expression is invalid + Last end - Last run + Last Run + + + Start date of the last execution Last success + + Start date of the last successful execution + Name Name is required - - Do not forgot to restart the application once a task has been modified. + + Next Run in + + + Date of the next scheduled execution + + + Error while running scheduled task "{0}" Run now - - Task execution completed - Is running... Task is now running in the background - - Seconds (run period) + + The task is now running in the background. You will receive an email as soon as it is completed. The progress can be tracked in the export profile list. + + + The task is now running in the background. You will receive an email as soon as it is completed. The progress can be tracked in the import profile list. + + + The task has been executed successfully - - Seconds should be positive. + + Schedule execution - Stop on error + Disable on error + + + Check the box if the task should be disabled automatically when an error occurs during execution + + + The task can not be edited while it is running. + + + The task has been updated successfully - Search engine friendly page names + SEO Names - - Delete selected + + Only one active SEO name should be set per language. - Entity ID + Object ID + + + Specifies the ID of the associated object. - Entity name + Object + + + Specifies the name of the associated object. Is active + + Specifies whether the SEO name is active or inactive. + Language + + Specifies the language of the SEO name. + Standard - Name + SEO Name - A name to find. + Specifies the SEO name. + + + Names per object + + + The number of SEO names per object. + + + Background Task + + + PDF Converter + + + Search Engine System information @@ -10988,6 +12253,12 @@ The name of the data provider. + + Collect + + + The memory has been successfully cleaned up. + HTTP_HOST @@ -11024,6 +12295,9 @@ Server time zone + + Used memory (RAM) + Greenwich Mean Time (GMT/UTC) @@ -11033,6 +12307,12 @@ Warnings + + Access denied to anonymous request on {0}. + + + Access denied to user #{0} '{1}' on {2}. + Default dimension is not set @@ -11057,6 +12337,9 @@ Default weight is set + + Please enter digits only. + All directory permissions are OK @@ -11090,6 +12373,15 @@ '{0}' plugin is incompatible with your SmartStore.NET version. Delete it or update to the latest version. + + There are no customer roles defined. + + + There are no permissions defined. + + + No shipment items + You don't have active payment methods @@ -11114,6 +12406,15 @@ Only one offline shipping rate computation method is recommended to use + + The reachability of the sitemap could not be validated. + + + The sitemap for the store is reachable. + + + The sitemap for the store is not reachable. + Specified store URL matches this store URL @@ -11417,6 +12718,21 @@ Tables/Container + + Please enter a valid email address + + + Please enter a name + + + Please enter a valid URL + + + Please enter username and password + + + The value must be greater than 0. + API not available. @@ -11579,6 +12895,9 @@ Checkout + + An anonymous checkout is not allowed. + Billing address @@ -11603,12 +12922,21 @@ Continue shopping + + Please agree to the user agreement for downloadable products. + Enter billing address Enter shipping address + + Yes, I want access to the digital content immediately and know that my right of revocation expires with the access. + + + Please confirm that you would like access to the digital content immediately. + Please wait several seconds before placing a new order (already placed another order several seconds ago). @@ -11702,6 +13030,9 @@ Submitting order information... + + Subscribed to newsletter + Terms of service @@ -11786,6 +13117,9 @@ Cancelled + + The API call to verify a CAPTCHA has failed. + Edit product @@ -11807,6 +13141,15 @@ Continue + + Copy to clipboard + + + Failed to copy. + + + Copied! + Count @@ -11819,6 +13162,9 @@ Cross references + + Customer number already exists, please choose another. + Date @@ -11837,36 +13183,84 @@ Are you sure you want to delete "{0}"? + + Deployment + Description Description + + Detail description + Display order Download + + The product variant doesn't have a sample download. + + + You have reached the maximum number of downloads {0}. + + + Download data is not available anymore. + + + Downloads are not allowed. + + + Download is not available any more. + + + Sample download is not available anymore. + Duration Edit + + Enabled + Enter value + + Click on an item to select or deselect it and OK to apply the selection. + + + There were no more items found. + + + Click on an item to select it and OK to apply it. + Error + + The email address is not valid. + + + No active language could be loaded. + + + No email account could be loaded. + Unfortunately the selected payment method caused an error. Please correct your entries, try it again or select another payment method. Error while sending the email. Please try again later. + + Example + Execution @@ -11894,12 +13288,21 @@ Drop files here to upload + + Enter URL + Failed Upload a file + + Filter + + + Free shipping + Friendly name @@ -11915,6 +13318,9 @@ Home + + Image + Import @@ -11924,12 +13330,21 @@ Is active + + Language + List + + Loading + Loading next step... + + This function is not available for guests. + Miscellaneous @@ -11939,6 +13354,9 @@ More info + + My + Navigation @@ -11954,6 +13372,12 @@ No, cancel + + There are no files available. + + + There was no file uploaded. + The operation was not carried out for security reasons. @@ -11963,6 +13387,9 @@ Alert + + Not selectable + Off @@ -11978,6 +13405,12 @@ Optional + + Options + + + Partition + Capture method not supported @@ -12002,6 +13435,24 @@ Product + + Profile + + + Projection + + + Provider + + + public + + + Published + + + Publishing + Question @@ -12014,9 +13465,21 @@ Remove + + Replace + + + The request could not be processed.<br />Controller: {0}, Action: {1}, Reason: {2}. + + + Rule + Save + + Scheduled + Search @@ -12050,6 +13513,12 @@ Show + + Show all + + + Show more + Shrink @@ -12062,9 +13531,21 @@ System name + + Unavailable + Undefined + + Unknown + + + Unpublished + + + Unscheduled + Unspecified @@ -12083,11 +13564,17 @@ Wait... + + Waiting + Warning + + Website + - The characters didn't match the picture. Please try again. + Please confirm that you are not a "robot". Wrong email @@ -12137,6 +13624,15 @@ Enter your name + + Privacy consent + + + Yes I've read the <a href="{0}">privacy terms</a> and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry. + + + Please agree to the storage of your data. + Your enquiry has been successfully sent to the store owner. @@ -12146,6 +13642,9 @@ Currencies + + The customer does not exist. + Guest @@ -12167,6 +13666,9 @@ Product + + The product has no user agreement. + I agree @@ -12176,6 +13678,9 @@ User agreement + + Yes, I agree to the <a href='javascript:void(0)' data-id='{0}' class='download-user-agreement'>user agreement</a> for this product. + Checkboxes @@ -12239,6 +13744,18 @@ Track inventory by product attributes + + No price indication + + + Minimum feasible price + + + Price preselected on detail page + + + Price without discounts and attributes + Created on @@ -12329,6 +13846,27 @@ Show usernames + + Automatically assigned + + + Disabled + + + Enabled + + + Display + + + Always editable + + + Editable if empty + + + Do not display + Clear @@ -12338,6 +13876,102 @@ Hashed + + Append all values to the product name + + + Not specified + + + Email + + + File system + + + FTP + + + HTTP POST + + + Public folder + + + Description + + + Manufacturer + Product name + long description + + + Manufacturer + Product name + short description + + + Product name + long description + + + Product name + short description + + + None + + + Short description + + + Short description or name if empty + + + Category + + + Customer + + + Manufacturer + + + Newsletter Subscribers + + + Order + + + Product + + + Multipart form data POST + + + Simple POST + + + Complete + + + None + + + Processing + + + Category + + + Customer + + + Newsletter Subscriber + + + Product + + + Delimiter separated values (.csv, .txt, .tab) + + + Excel (.xlsx) + N times only @@ -12422,6 +14056,24 @@ Warning + + Show activated + + + Show deactivated + + + Do not show + + + Show activated + + + Show deactivated + + + Do not show + Cancelled @@ -12688,9 +14340,6 @@ Forum's topics with newest posts. - - {0} - Forum: {1} - Forum Name @@ -12931,6 +14580,9 @@ Information + + The install language '{0}' is not registered. + Please enter a valid credit card number. @@ -13180,12 +14832,48 @@ Billing Address + + The billing address is missing. + + + The order total could not be calculated. + + + The shipping total could not be calculated. + + + Cannot cancel order. + + + Cannot capture order. + + + Cannot mark order as completed. + + + Cannot mark order as paid. + + + Cannot do partial refund for order. + + + Cannot do refund for order. + + + Cannot do void for order. + Complete payment This order is not yet paid for. To pay now click the "Complete payment" button. + + The country '{0}' is not allowed for billing. + + + The country '{0}' is not allowed for shipping. + Email @@ -13198,6 +14886,12 @@ Gift card ({0}) + + No initial order exists for the recurring payment. + + + There are no recurring products. + Note(s) @@ -13207,6 +14901,9 @@ Note + + The order {0} was not found. + Order # @@ -13228,6 +14925,9 @@ Payment method additional fee + + order-{0}.pdf + Phone @@ -13339,6 +15039,9 @@ Shipping Address + + The shipping address is missing. + Shipping Method @@ -13489,6 +15192,9 @@ Email wishlist to a friend + + The next payment date could not be calculated. + Card code @@ -13513,6 +15219,9 @@ Wrong card number + + The payment method could not be loaded. + Valid until @@ -13522,6 +15231,24 @@ Expire year is required + + The payment method is not available. + + + At least one payment method provider is required to be active. + + + Unfortunately we can not handle this purchasing via your preferred payment method. Please select an alternate payment option to complete your order. + + + Recurring payment is not active. + + + Recurring payments are not supported by selected payment method. + + + The recurring payment type is not supported. + Select credit card @@ -13639,12 +15366,6 @@ Weight - - Associated products - - - Bundled items - Mail @@ -13660,32 +15381,14 @@ Contact data - - Height - - - Legth - - - Manufacturer - - - Price - - - SKU - - - Specification attributes - Stock quantity - - Weight + + The poll answer {0} was not found. - - Width + + The poll is not available. Only registered users can vote. @@ -13708,6 +15411,9 @@ Privacy Notice + + Private messages are disabled. + Inbox @@ -13838,7 +15544,10 @@ Out of stock - base price: {0} per {1} + Content: {0} {1} ({2} / {3} {1}) + + + {0} {1} ({2} / {3} {1}) Top @@ -14039,7 +15748,10 @@ This product is sold out. - This product is sold out. + No bundle items available + + + The product {0} was not found. Price: @@ -14116,6 +15828,9 @@ Quantity + + The product variant {0} was not found. + Weight @@ -14161,6 +15876,9 @@ You can use ECB (European central bank) exchange rate provider only when exchange rate currency code is set to EURO + + You can use ECB (European central bank) exchange rate provider only when exchange rate currency code is set to EURO. + ECB exchange rate provider @@ -14251,6 +15969,9 @@ You cannot vote for your own review + + The product review {0} was not found. + Only registered users can write reviews @@ -14365,6 +16086,18 @@ Search term minimum length is {0} characters + + This shipment is already delivered. + + + This shipment is already shipped. + + + The shipping rate computation method could not be loaded. + + + At least one shipping rate computation method provider is required to be active. + Shipping & Returns @@ -14497,6 +16230,9 @@ The coupon code you entered couldn't be applied to your order + + The shoping cart is disabled. + Total @@ -14609,7 +16345,7 @@ Enter valid sender name - For a complete listing of all shipping costs please click here. + For a complete listing of all shipping costs please click <a href="{0}">here</a>. SKU @@ -14690,10 +16426,10 @@ Store name comes after page name - This store is currently closed + We'll be back. - Please check back in a little while. + We're busy updating our online store for you and will be back soon. Select store theme @@ -14713,9 +16449,15 @@ * All prices {0}, plus <a href="{1}">shipping</a> + + * All prices {0}, plus shipping + {0} {1} {2}plus <a href="{3}">shipping</a> + + {0} {1} {2}plus shipping + {0} {1} {2}plus <a href="{3}">shipping</a> @@ -15082,15 +16824,57 @@ Well background + + Day + + + d + + + Days + + + d + {0} days ago + + Hour + + + h + + + Hours + + + h + {0} hours ago + + Minute + + + min + + + Minutes + + + min + {0} minutes ago + + Month + + + Months + {0} months ago @@ -15112,9 +16896,33 @@ one year ago + + Second + + + sec + + + Seconds + + + sec + {0} seconds ago + + Week + + + Weeks + + + Year + + + Years + {0} years ago @@ -15175,6 +16983,9 @@ Enter your email + + The wishlist is disabled. + View Wishlist diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt index afe64e4f3b..f822a9cb76 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt @@ -1 +1 @@ -201506181858349_AclRecordCustomerRole \ No newline at end of file +201605201911421_ExportRevision \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml index acd910374c..3582e6f7d8 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml @@ -183,6 +183,9 @@ Schreibe Beispiel Daten - + + + Optionen + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml index a11658d45b..4603606855 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml @@ -183,5 +183,9 @@ Creating sample data - + + + Options + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml b/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml index 6ef74a0d89..c160ade711 100644 --- a/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml +++ b/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml @@ -17,6 +17,10 @@ user_agent_parsers: - 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' @@ -106,7 +110,7 @@ user_agent_parsers: # 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 Mini)(?:/att)?/?(\d+)?(?:\.(\d+))?(?:\.(\d+))?' - regex: '(Opera)/.+Opera Mobi.+Version/(\d+)\.(\d+)' family_replacement: 'Opera Mobile' - regex: '(Opera)/(\d+)\.(\d+).+Opera Mobi' @@ -205,6 +209,10 @@ user_agent_parsers: - 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' @@ -272,12 +280,12 @@ user_agent_parsers: - 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: 'MSOffice 12' + - regex: 'Microsoft Office Outlook 12\.\d+\.\d+|MSOffice 12' family_replacement: 'Outlook' v1_replacement: '2007' # Outlook 2010 - - regex: 'MSOffice 14' + - regex: 'Microsoft Outlook 14\.\d+\.\d+|MSOffice 14' family_replacement: 'Outlook' v1_replacement: '2010' @@ -286,6 +294,15 @@ user_agent_parsers: 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+))?' @@ -299,6 +316,10 @@ user_agent_parsers: # 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+)' @@ -388,15 +409,15 @@ user_agent_parsers: - 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 UIWebView' + 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 UIWebView' + family_replacement: 'Mobile Safari UI/WKWebView' - regex: '(iPod|iPhone|iPad).* Safari' family_replacement: 'Mobile Safari' - regex: '(iPod|iPhone|iPad)' - family_replacement: 'Mobile Safari UIWebView' + family_replacement: 'Mobile Safari UI/WKWebView' - regex: '(AvantGo) (\d+).(\d+)' @@ -498,7 +519,7 @@ user_agent_parsers: # apple mail - not directly detectable, have it after Safari stuff - regex: '(AppleWebKit)/(\d+)\.(\d+)\.(\d+)' - family_replacement: 'AppleMail' + family_replacement: 'Apple Mail' # AFTER THE EDGE CASES ABOVE! # AFTER IE11 @@ -514,6 +535,9 @@ user_agent_parsers: - 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 @@ -981,6 +1005,9 @@ os_parsers: - regex: 'SunOS' os_replacement: 'Solaris' + # Roku Digital-Video-Players https://www.roku.com/ + - regex: '^(Roku)/DVP-(\d+)\.(\d+)' + device_parsers: ######### @@ -2049,7 +2076,7 @@ device_parsers: device_replacement: 'Huawei Ideos$1' brand_replacement: 'Huawei' model_replacement: 'Ideos$1' - - regex: '; *(Orange Daytona|Pulse|Pulse Mini|Vodafone 858|C8500|C8600|C8650|C8660) Build' + - 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' @@ -2571,7 +2598,7 @@ device_parsers: # 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 [45]|GT540f?|Optimus (?:2X|G|4X HD)|OptimusX4HD) *(?:Build|;)' + - 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' @@ -3841,7 +3868,7 @@ device_parsers: # XiaoMi # @ref: http://www.xiaomi.com/event/buyphone ######### - - regex: '; *((MI|HM|MI-ONE|Redmi)[ -](NOTE |Note )?[^;/]*) Build/' + - regex: '; *((MI|HM|MI-ONE|Redmi)[ -](NOTE |Note )?[^;/]*) (Build|MIUI)/' device_replacement: 'XiaoMi $1' brand_replacement: 'XiaoMi' model_replacement: '$1' @@ -4700,6 +4727,10 @@ device_parsers: - 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 @@ -4764,4 +4795,4 @@ device_parsers: regex_flag: 'i' device_replacement: 'Generic Feature Phone' brand_replacement: 'Generic' - model_replacement: 'Feature Phone' + model_replacement: 'Feature Phone' \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000019_5_virtual_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000019_5_virtual_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000019_5_virtual_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card.jpeg deleted file mode 100644 index 231fcaca34..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_125.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_125.jpeg deleted file mode 100644 index 23b629526d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_125.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000021_50_physical_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000021_50_physical_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000021_50_physical_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000022_100_physical_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000022_100_physical_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000022_100_physical_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000024_acer_aspire_one_89_mini_notebook_case_black_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000024_acer_aspire_one_89_mini_notebook_case_black_75.jpeg deleted file mode 100644 index 4b9ef163cf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000024_acer_aspire_one_89_mini_notebook_case_black_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000026_adidas_womens_supernova_csh_7_running_shoe_75.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000026_adidas_womens_supernova_csh_7_running_shoe_75.jpg deleted file mode 100644 index 72b7a3b370..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000026_adidas_womens_supernova_csh_7_running_shoe_75.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000028_adobe_photoshop_elements_7_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000028_adobe_photoshop_elements_7_75.jpeg deleted file mode 100644 index 6482f9153e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000028_adobe_photoshop_elements_7_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000029_apc_back_ups_rs_800va_ups_800_va_ups_battery_lead_acid_br800blk__75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000029_apc_back_ups_rs_800va_ups_800_va_ups_battery_lead_acid_br800blk__75.jpeg deleted file mode 100644 index efb2e46f2c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000029_apc_back_ups_rs_800va_ups_800_va_ups_battery_lead_acid_br800blk__75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000034_black_white_diamond_heart_75.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000034_black_white_diamond_heart_75.jpg deleted file mode 100644 index feac89b5fb..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000034_black_white_diamond_heart_75.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000035_blackberry_bold_9000_phone_black_att_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000035_blackberry_bold_9000_phone_black_att_75.jpeg deleted file mode 100644 index ae48a960b3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000035_blackberry_bold_9000_phone_black_att_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer.jpeg deleted file mode 100644 index af6447c685..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer_125.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer_125.jpeg deleted file mode 100644 index 41ddd6848a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer_125.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker.jpg deleted file mode 100644 index 67198f7d85..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker_125.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker_125.jpg deleted file mode 100644 index 80885934b0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker_125.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/Images/uploaded/placeholder.txt b/src/Presentation/SmartStore.Web/Content/Images/uploaded/placeholder.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/Presentation/SmartStore.Web/Content/Images/uploaded/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less index 46611f8534..67aeef972b 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less @@ -211,6 +211,9 @@ span.field-validation-error { .btn-below { margin-top: 5px !important; } +.btn-right { + margin-left: 10px !important; +} // FORMS (GENERAL) @@ -487,10 +490,81 @@ th label { .modal-backdrop, .modal-backdrop.fade.in { - .opacity(60); + opacity: 0.6; } .modal-header .close { font-size: 30px; text-shadow: none; +} + +.modal-large { + width: 800px !important; + margin: -250px 0 0 -400px !important; +} + + +// XLARGE MODAL +// ------------------------------ + +.modal-xlarge { + width: 960px !important; + margin: -250px 0 0 -480px !important; +} + + +// FLEX MODAL +// ------------------------------ + +.modal-flex { + top: 3% !important; + bottom: 3% !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + + &.fade.in { + display: -webkit-box !important; + display: -webkit-flex !important; + display: -ms-flexbox !important; + display: flex !important; + } + + .modal-body { + max-height: none; + overflow-y: auto; + overflow-x: hidden; + .display-flex(); + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: stretch; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + } + + .modal-flex-fill-area { + //overflow: hidden; + min-height: 100%; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-16.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-16.gif deleted file mode 100644 index edcef28713..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-16.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-32.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-32.gif deleted file mode 100644 index 9fafab009a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-32.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-16.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-16.gif deleted file mode 100644 index 152084b58c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-16.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32-alt.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32-alt.gif deleted file mode 100644 index f201c9a0a0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32-alt.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32.gif deleted file mode 100644 index d663892719..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less index 298d86b427..f0e9eb81ea 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less @@ -140,10 +140,8 @@ @bg: ~'top, @{c1} 0%, @{c2} @{stop1}, @{c3} @{stop}, @{c4} 100%'; background-color: mix(@c2, @c3, 50%); background-image: -webkit-linear-gradient(@bg); - background-image: -o-linear-gradient(@bg); background-image: linear-gradient(@bg); background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@c1),argb(@c4))); } } @@ -152,13 +150,11 @@ // Drop shadow extras .box-shadow(@shadow1, @shadow2) { - -webkit-box-shadow: @shadow1, @shadow2; - box-shadow: @shadow1, @shadow2; + box-shadow: @shadow1, @shadow2; } .box-shadow(@shadow1, @shadow2, @shadow3) { - -webkit-box-shadow: @shadow1, @shadow2, @shadow3; - box-shadow: @shadow1, @shadow2, @shadow3; + box-shadow: @shadow1, @shadow2, @shadow3; } @@ -171,13 +167,11 @@ .transition(@transition1, @transition2) { -webkit-transition: @transition1, @transition2; - -o-transition: @transition1, @transition2; transition: @transition1, @transition2; } .transition(@transition1, @transition2, @transition3) { -webkit-transition: @transition1, @transition2, @transition3; - -o-transition: @transition1, @transition2, @transition3; transition: @transition1, @transition2, @transition3; } @@ -186,7 +180,6 @@ .animation(@animation) { -webkit-animation: @animation; - -o-animation: @animation; animation: @animation; } @@ -194,7 +187,27 @@ .transform(@expr) { -webkit-transform: @expr; - -moz-transform: @expr; - -ms-transform: @expr; transform: @expr; +} + + +/* Flexbox Extras +----------------------------------------*/ + +.display-flex() { + display: -webkit-flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +.flex-center() { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/spinner.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/spinner.less new file mode 100644 index 0000000000..660161e472 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/spinner.less @@ -0,0 +1,116 @@ +/* +SmartStore Circular Spinner +--------------------------- + Version: 1, + Timestamp: 10.02.2016 + Author: Murat Cakir +*/ + +.spinner-container { + // for centering the spinner + position: relative; + display: none; + .flex-center(); +} + +.spinner { + position: relative; + text-align: center; + vertical-align: middle; + display: none; +} + +.spinner-container.active > .spinner, +.spinner.active { + background-color: transparent; + border: none; + display: inline-block; +} + +.spinner.spinner-boxed { + border-radius: 50%; + padding: 0.2em; + background-color: #fff !important; + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), + 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); +} + +.spinner-container.active { + .display-flex(); +} + +.spinner svg { + -webkit-transform-origin: 50% 50% 0; + transform-origin: 50% 50% 0; + + -webkit-animation: spinner-rotate 1.333s linear infinite; + animation: spinner-rotate 1.333s linear infinite; + + html.ie & { + animation: spinner-rotate-ie 2.5s linear infinite; + } +} + +.spinner circle { + fill: transparent; + stroke: #ff9800; + stroke-linecap: round; + stroke-dasharray: 200.96; + stroke-dashoffset: 58px; + + html.ie & { + stroke-dashoffset: 80px; + } + + -webkit-animation: spinner-dash 1.333s linear infinite, spinner-colors 10.644s linear infinite; + animation: spinner-dash 1.333s linear infinite, spinner-colors 10.644s linear infinite; +} + +@keyframes spinner-dash { + 0% { stroke-dashoffset: 58px; } + 50% { stroke-dashoffset: 200.96px; } + 100% { stroke-dashoffset: 58px; } +} +@-webkit-keyframes spinner-dash { + 0% { stroke-dashoffset: 58px; } + 50% { stroke-dashoffset: 200.96px; } + 100% { stroke-dashoffset: 58px; } +} + +@keyframes spinner-rotate { + 50% { transform: rotate(600deg); } + 100% { transform: rotate(720deg); } +} +@-webkit-keyframes spinner-rotate { + 50% { transform: rotate(600deg); } + 100% { transform: rotate(720deg); } +} +@keyframes spinner-rotate-ie { + 50% { transform: rotate(360deg); } + 100% { transform: rotate(720deg); } +} + +@keyframes spinner-colors { + 0% { stroke: #3F51B5; } + 20% { stroke: #09b7bf; } + 40% { stroke: #90d36b; } + 60% { stroke: #F44336; } + 80% { stroke: #f90; } + 100% { stroke: #3F51B5; } +} +@-webkit-keyframes spinner-colors { + 0% { stroke: #3F51B5; } + 20% { stroke: #09b7bf; } + 40% { stroke: #90d36b; } + 60% { stroke: #F44336; } + 80% { stroke: #f90; } + 100% { stroke: #3F51B5; } +} + +.spinner.white circle { + stroke: #fff; + -webkit-animation-name: spinner-dash, spinner-rotate; + animation-name: spinner-dash, spinner-rotate; +} + diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/telerik/telerik.smartstore.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/telerik/telerik.smartstore.less index ab27daa31b..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; diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less index 31aa8cdfbd..f579532a64 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less @@ -5,65 +5,57 @@ Version: 1, Timestamp: 18.08.2012 .throbber { display: none; - .opacity(0); - - & .throbber-overlay { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - background-color: #000; - .opacity(70); - z-index: 2147483640; + opacity: 0; + overflow: hidden; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 2147483640; + background-color: rgba(0,0,0, 0.7); + + &.white { + background-color: rgba(255,255,255, 0.85); } - - &.white .throbber-overlay { - background-color: #fff; + + body > & { + position: fixed; } - - & .throbber-content { + + .throbber-flex { + width: 100%; + height: 100%; position: absolute; - z-index: 2147483640; - color: #fff; - text-shadow: 1px 1px 0 #666; - background: url('images/throbber-progress-white-32.gif') 4px 6px no-repeat; + .display-flex(); + .flex-center(); + + > * { + text-align: center; + } + } + + .throbber-content { + display: block; + overflow: hidden; + line-height: 1.3; + + &:not(:empty) { + margin-bottom: 30px; + } } - + &.large .throbber-content { - min-height: 38px; - line-height: 32px; - max-width: 400px; - font-size: 24px; - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, 'Helvetica Neue', Helvetica, Arial, 'sans-serif'; + font-size: 32px; font-weight: 100; - padding-left: 44px; } + &.small .throbber-content { - .text-overflow(); - height: 16px; - font-size: @baseFontSize; - line-height: @baseLineHeight; - vertical-align: middle; - padding-left: 24px; - background: url('images/throbber-progress-white-32.gif') 0 6px no-repeat; + font-size: 16px; + font-weight: 600; } - &.white .throbber-content { - color: #000; - text-shadow: none; - } - - &.large .throbber-content { background-image: url('images/throbber-progress-white-32.gif'); } - &.small .throbber-content { background-image: url('images/throbber-progress-white-16.gif'); background-position: 0 0; } - &.white.large .throbber-content { background-image: url('images/throbber-progress-black-32.gif'); } - &.white.small .throbber-content { background-image: url('images/throbber-progress-black-16.gif'); background-position: 0 0; } - - &.global .throbber-overlay { - position: fixed; - width: 100%; - height: 100%; - } - &.global .throbber-content { - position: fixed; + + &:not(.white) .throbber-content { + color: #fff; } -} \ No newline at end of file +} diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less b/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less index 50e2ad9aed..917cc2e396 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less @@ -124,7 +124,7 @@ input[type="color"], .uneditable-input { background-color: @inputBackground; border: 1px solid @inputBorder; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + //.box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Focus state &:focus { @@ -195,7 +195,7 @@ input[type="checkbox"]:focus { color: @grayLight; background-color: darken(@inputBackground, 1%); border-color: @inputBorder; - .box-shadow(inset 0 1px 2px rgba(0,0,0,.025)); + //.box-shadow(inset 0 1px 2px rgba(0,0,0,.025)); cursor: not-allowed; } @@ -385,8 +385,8 @@ select:focus:required:invalid { border-color: #ee5f5b; &:focus { border-color: darken(#ee5f5b, 10%); - @shadow: 0 0 6px lighten(#ee5f5b, 20%); - .box-shadow(@shadow); + //@shadow: 0 0 6px lighten(#ee5f5b, 20%); + //.box-shadow(@shadow); } } diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less b/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less index 8b0edb15cb..c1d9950469 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less @@ -240,14 +240,12 @@ // Drop shadows .box-shadow(@shadow) { - -webkit-box-shadow: @shadow; - box-shadow: @shadow; + box-shadow: @shadow; } // Transitions .transition(@transition) { -webkit-transition: @transition; - -o-transition: @transition; transition: @transition; } .transition-delay(@transition-delay) { @@ -258,33 +256,23 @@ // Transformations .rotate(@degrees) { -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - -o-transform: rotate(@degrees); transform: rotate(@degrees); } .scale(@ratio) { -webkit-transform: scale(@ratio); - -ms-transform: scale(@ratio); - -o-transform: scale(@ratio); transform: scale(@ratio); } .translate(@x, @y) { -webkit-transform: translate(@x, @y); - -ms-transform: translate(@x, @y); - -o-transform: translate(@x, @y); transform: translate(@x, @y); } .skew(@x, @y) { -webkit-transform: skew(@x, @y); - -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twitter/bootstrap/issues/4885 - -o-transform: skew(@x, @y); transform: skew(@x, @y); -webkit-backface-visibility: hidden; // See https://github.com/twitter/bootstrap/issues/5319 } .translate3d(@x, @y, @z) { -webkit-transform: translate3d(@x, @y, @z); - -moz-transform: translate3d(@x, @y, @z); - -o-transform: translate3d(@x, @y, @z); transform: translate3d(@x, @y, @z); } @@ -294,30 +282,24 @@ // See git pull https://github.com/dannykeane/bootstrap.git backface-visibility for examples .backface-visibility(@visibility){ -webkit-backface-visibility: @visibility; - -moz-backface-visibility: @visibility; backface-visibility: @visibility; } // Background clipping // Heads up: FF 3.6 and under need "padding" instead of "padding-box" .background-clip(@clip) { - -webkit-background-clip: @clip; - background-clip: @clip; + background-clip: @clip; } // Background sizing .background-size(@size) { - -webkit-background-size: @size; - -o-background-size: @size; - background-size: @size; + background-size: @size; } // Box sizing .box-sizing(@boxmodel) { - -webkit-box-sizing: @boxmodel; - -moz-box-sizing: @boxmodel; - box-sizing: @boxmodel; + box-sizing: @boxmodel; } // User select @@ -351,14 +333,12 @@ -webkit-hyphens: @mode; -moz-hyphens: @mode; -ms-hyphens: @mode; - -o-hyphens: @mode; hyphens: @mode; } // Opacity .opacity(@opacity) { - opacity: @opacity / 100; - filter: ~"alpha(opacity=@{opacity})"; + opacity: @opacity / 100; } @@ -390,33 +370,26 @@ .horizontal(@startColor: #555, @endColor: #333) { background-color: @endColor; background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@startColor),argb(@endColor))); // IE9 and down } .vertical(@startColor: #555, @endColor: #333) { background-color: mix(@startColor, @endColor, 60%); background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down } .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { background-color: @endColor; background-repeat: repeat-x; background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 } .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { background-color: mix(@midColor, @endColor, 80%); background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); - background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor); background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback } .radial(@innerColor: #555, @outerColor: #333) { background-color: @outerColor; @@ -427,7 +400,6 @@ .striped(@color: #555, @angle: 45deg) { background-color: @color; background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); } } diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/asp_net/main.ashx b/src/Presentation/SmartStore.Web/Content/filemanager/asp_net/main.ashx deleted file mode 100644 index 72c5f5feb9..0000000000 --- a/src/Presentation/SmartStore.Web/Content/filemanager/asp_net/main.ashx +++ /dev/null @@ -1,719 +0,0 @@ -<%@ WebHandler Language="C#" Class="RoxyFilemanHandler" Debug="true" %> -/* - RoxyFileman - web based file manager. Ready to use with CKEditor, TinyMCE. - Can be easily integrated with any other WYSIWYG editor or CMS. - - Copyright (C) 2013, RoxyFileman.com - Lyubomir Arsov. All rights reserved. - For licensing, see LICENSE.txt or http://RoxyFileman.com/license - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - Contact: Lyubomir Arsov, liubo (at) web-lobby.com -*/ -using System; -using System.Reflection; -using System.Drawing; -using System.Collections; -using System.IO; -using System.Collections.Generic; -using System.Web; -using System.Text.RegularExpressions; -using System.Drawing.Imaging; -using System.IO.Compression; - -public class RoxyFilemanHandler : IHttpHandler, System.Web.SessionState.IRequiresSessionState -{ - Dictionary _settings = null; - Dictionary _lang = null; - HttpResponse _r = null; - HttpContext _context = null; - string confFile = "../conf.json"; - public void ProcessRequest (HttpContext context) { - _context = context; - _r = context.Response; - string action = "DIRLIST"; - try{ - if (_context.Request["a"] != null) - action = (string)_context.Request["a"]; - - VerifyAction(action); - switch (action.ToUpper()) - { - case "DIRLIST": - ListDirTree(_context.Request["type"]); - break; - case "FILESLIST": - ListFiles(_context.Request["d"], _context.Request["type"]); - break; - case "COPYDIR": - CopyDir(_context.Request["d"], _context.Request["n"]); - break; - case "COPYFILE": - CopyFile(_context.Request["f"], _context.Request["n"]); - break; - case "CREATEDIR": - CreateDir(_context.Request["d"], _context.Request["n"]); - break; - case "DELETEDIR": - DeleteDir(_context.Request["d"]); - break; - case "DELETEFILE": - DeleteFile(_context.Request["f"]); - break; - case "DOWNLOAD": - DownloadFile(_context.Request["f"]); - break; - case "DOWNLOADDIR": - DownloadDir(_context.Request["d"]); - break; - case "MOVEDIR": - MoveDir(_context.Request["d"], _context.Request["n"]); - break; - case "MOVEFILE": - MoveFile(_context.Request["f"], _context.Request["n"]); - break; - case "RENAMEDIR": - RenameDir(_context.Request["d"], _context.Request["n"]); - break; - case "RENAMEFILE": - RenameFile(_context.Request["f"], _context.Request["n"]); - break; - case "GENERATETHUMB": - int w = 140, h = 0; - int.TryParse(_context.Request["width"].Replace("px", ""), out w); - int.TryParse(_context.Request["height"].Replace("px", ""), out h); - ShowThumbnail(_context.Request["f"], w, h); - break; - case "UPLOAD": - Upload(_context.Request["d"]); - break; - default: - _r.Write(GetErrorRes("This action is not implemented.")); - break; - } - - } - catch(Exception ex){ - if (action == "UPLOAD"){ - _r.Write(""); - } - else{ - _r.Write(GetErrorRes(ex.Message)); - } - } - - } - private string FixPath(string path) - { - if (!path.StartsWith("~")){ - if (!path.StartsWith("/")) - path = "/" + path; - path = "~" + path; - } - return _context.Server.MapPath(path); - } - private string GetLangFile(){ - return "../lang/en.json"; - } - protected string LangRes(string name) - { - string ret = name; - if (_lang == null) - _lang = ParseJSON(GetLangFile()); - if (_lang.ContainsKey(name)) - ret = _lang[name]; - - return ret; - } - protected string GetFileType(string ext){ - string ret = "file"; - ext = ext.ToLower(); - if(ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif") - ret = "image"; - else if(ext == ".swf" || ext == ".flv") - ret = "flash"; - return ret; - } - protected bool CanHandleFile(string filename) - { - bool ret = false; - FileInfo file = new FileInfo(filename); - string ext = file.Extension.Replace(".", "").ToLower(); - string setting = GetSetting("FORBIDDEN_UPLOADS").Trim().ToLower(); - if (setting != "") - { - ArrayList tmp = new ArrayList(); - tmp.AddRange(Regex.Split(setting, "\\s+")); - if (!tmp.Contains(ext)) - ret = true; - } - setting = GetSetting("ALLOWED_UPLOADS").Trim().ToLower(); - if (setting != "") - { - ArrayList tmp = new ArrayList(); - tmp.AddRange(Regex.Split(setting, "\\s+")); - if (!tmp.Contains(ext)) - ret = false; - } - - return ret; - } - protected Dictionary ParseJSON(string file){ - Dictionary ret = new Dictionary(); - string json = ""; - try{ - json = File.ReadAllText(_context.Server.MapPath(file), System.Text.Encoding.UTF8); - } - catch {} - - json = json.Trim(); - if(json != ""){ - if (json.StartsWith("{")) - json = json.Substring(1, json.Length - 2); - json = json.Trim(); - json = json.Substring(1, json.Length - 2); - string[] lines = Regex.Split(json, "\"\\s*,\\s*\""); - foreach(string line in lines){ - string[] tmp = Regex.Split(line, "\"\\s*:\\s*\""); - try{ - if (tmp[0] != "" && !ret.ContainsKey(tmp[0])) - { - ret.Add(tmp[0], tmp[1]); - } - } - catch {} - } - } - return ret; - } - protected string GetFilesRoot(){ - string ret = GetSetting("FILES_ROOT"); - if (GetSetting("SESSION_PATH_KEY") != "" && _context.Session[GetSetting("SESSION_PATH_KEY")] != null) - ret = (string)_context.Session[GetSetting("SESSION_PATH_KEY")]; - - if(ret == "") - ret = _context.Server.MapPath("../Uploads"); - else - ret = FixPath(ret); - return ret; - } - protected void LoadConf(){ - if(_settings == null) - _settings = ParseJSON(confFile); - } - protected string GetSetting(string name){ - string ret = ""; - LoadConf(); - if(_settings.ContainsKey(name)) - ret = _settings[name]; - - return ret; - } - protected void CheckPath(string path) - { - if (FixPath(path).IndexOf(GetFilesRoot()) != 0) - { - throw new Exception("Access to " + path + " is denied"); - } - } - protected void VerifyAction(string action) - { - string setting = GetSetting(action); - if (setting.IndexOf("?") > -1) - setting = setting.Substring(0, setting.IndexOf("?")); - if (!setting.StartsWith("/")) - setting = "/" + setting; - setting = ".." + setting; - - if (_context.Server.MapPath(setting) != _context.Server.MapPath(_context.Request.Url.LocalPath)) - throw new Exception(LangRes("E_ActionDisabled")); - } - protected string GetResultStr(string type, string msg) - { - return "{\"res\":\"" + type + "\",\"msg\":\"" + msg.Replace("\"","\\\"") + "\"}"; - } - protected string GetSuccessRes(string msg) - { - return GetResultStr("ok", msg); - } - protected string GetSuccessRes() - { - return GetSuccessRes(""); - } - protected string GetErrorRes(string msg) - { - return GetResultStr("error", msg); - } - private void _copyDir(string path, string dest){ - if(!Directory.Exists(dest)) - Directory.CreateDirectory(dest); - foreach(string f in Directory.GetFiles(path)){ - FileInfo file = new FileInfo(f); - if (!File.Exists(Path.Combine(dest, file.Name))){ - File.Copy(f, Path.Combine(dest, file.Name)); - } - } - foreach (string d in Directory.GetDirectories(path)) - { - DirectoryInfo dir = new DirectoryInfo(d); - _copyDir(d, Path.Combine(dest, dir.Name)); - } - } - protected void CopyDir(string path, string newPath) - { - CheckPath(path); - CheckPath(newPath); - DirectoryInfo dir = new DirectoryInfo(FixPath(path)); - DirectoryInfo newDir = new DirectoryInfo(FixPath(newPath + "/" + dir.Name)); - - if (!dir.Exists) - { - throw new Exception(LangRes("E_CopyDirInvalidPath")); - } - else if (newDir.Exists) - { - throw new Exception(LangRes("E_DirAlreadyExists")); - } - else{ - _copyDir(dir.FullName, newDir.FullName); - } - _r.Write(GetSuccessRes()); - } - protected string MakeUniqueFilename(string dir, string filename){ - string ret = filename; - int i = 0; - while (File.Exists(Path.Combine(dir, ret))) - { - i++; - ret = Path.GetFileNameWithoutExtension(filename) + " - Copy " + i.ToString() + Path.GetExtension(filename); - } - return ret; - } - protected void CopyFile(string path, string newPath) - { - CheckPath(path); - FileInfo file = new FileInfo(FixPath(path)); - newPath = FixPath(newPath); - if (!file.Exists) - throw new Exception(LangRes("E_CopyFileInvalisPath")); - else{ - string newName = MakeUniqueFilename(newPath, file.Name); - try{ - File.Copy(file.FullName, Path.Combine(newPath, newName)); - _r.Write(GetSuccessRes()); - } - catch { - throw new Exception(LangRes("E_CopyFile")); - } - } - } - protected void CreateDir(string path, string name) - { - CheckPath(path); - path = FixPath(path); - if(!Directory.Exists(path)) - throw new Exception(LangRes("E_CreateDirInvalidPath")); - else{ - try - { - path = Path.Combine(path, name); - if(!Directory.Exists(path)) - Directory.CreateDirectory(path); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_CreateDirFailed")); - } - } - } - protected void DeleteDir(string path) - { - CheckPath(path); - path = FixPath(path); - if (!Directory.Exists(path)) - throw new Exception(LangRes("E_DeleteDirInvalidPath")); - else if (path == GetFilesRoot()) - throw new Exception(LangRes("E_CannotDeleteRoot")); - else if(Directory.GetDirectories(path).Length > 0 || Directory.GetFiles(path).Length > 0) - throw new Exception(LangRes("E_DeleteNonEmpty")); - else - { - try - { - Directory.Delete(path); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_CannotDeleteDir")); - } - } - } - protected void DeleteFile(string path) - { - CheckPath(path); - path = FixPath(path); - if (!File.Exists(path)) - throw new Exception(LangRes("E_DeleteFileInvalidPath")); - else - { - try - { - File.Delete(path); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_DeletеFile")); - } - } - } - private List GetFiles(string path, string type){ - List ret = new List(); - if(type == "#") - type = ""; - string[] files = Directory.GetFiles(path); - foreach(string f in files){ - if ((GetFileType(new FileInfo(f).Extension) == type) || (type == "")) - ret.Add(f); - } - return ret; - } - private ArrayList ListDirs(string path){ - string[] dirs = Directory.GetDirectories(path); - ArrayList ret = new ArrayList(); - foreach(string dir in dirs){ - ret.Add(dir); - ret.AddRange(ListDirs(dir)); - } - return ret; - } - protected void ListDirTree(string type) - { - DirectoryInfo d = new DirectoryInfo(GetFilesRoot()); - if(!d.Exists) - throw new Exception("Invalid files root directory. Check your configuration."); - - ArrayList dirs = ListDirs(d.FullName); - dirs.Insert(0, d.FullName); - - string localPath = _context.Server.MapPath("~/"); - _r.Write("["); - for(int i = 0; i files = GetFiles(fullPath, type); - _r.Write("["); - for(int i = 0; i < files.Count; i++){ - FileInfo f = new FileInfo(files[i]); - int w = 0, h = 0; - if (GetFileType(f.Extension) == "image"){ - try{ - FileStream fs = new FileStream(f.FullName, FileMode.Open); - Image img = Image.FromStream(fs); - w = img.Width; - h = img.Height; - fs.Close(); - fs.Dispose(); - img.Dispose(); - } - catch(Exception ex) - { - throw ex; - } - } - _r.Write("{"); - _r.Write("\"p\":\""+path + "/" + f.Name+"\""); - _r.Write(",\"t\":\"" + Math.Ceiling(LinuxTimestamp(f.LastWriteTime)).ToString() + "\""); - _r.Write(",\"s\":\""+f.Length.ToString()+"\""); - _r.Write(",\"w\":\""+w.ToString()+"\""); - _r.Write(",\"h\":\""+h.ToString()+"\""); - _r.Write("}"); - if (i < files.Count - 1) - _r.Write(","); - } - _r.Write("]"); - } - public void DownloadDir(string path) - { - path = FixPath(path); - if(!Directory.Exists(path)) - throw new Exception(LangRes("E_CreateArchive")); - string dirName = new FileInfo(path).Name; - string tmpZip = _context.Server.MapPath("../tmp/" + dirName + ".zip"); - if(File.Exists(tmpZip)) - File.Delete(tmpZip); - ZipFile.CreateFromDirectory(path, tmpZip,CompressionLevel.Fastest, true); - _r.Clear(); - _r.Headers.Add("Content-Disposition", "attachment; filename=\"" + dirName + ".zip\""); - _r.ContentType = "application/force-download"; - _r.TransmitFile(tmpZip); - _r.Flush(); - File.Delete(tmpZip); - _r.End(); - } - protected void DownloadFile(string path) - { - CheckPath(path); - FileInfo file = new FileInfo(FixPath(path)); - if(file.Exists){ - _r.Clear(); - _r.Headers.Add("Content-Disposition", "attachment; filename=\"" + file.Name + "\""); - _r.ContentType = "application/force-download"; - _r.TransmitFile(file.FullName); - _r.Flush(); - _r.End(); - } - } - protected void MoveDir(string path, string newPath) - { - CheckPath(path); - CheckPath(newPath); - DirectoryInfo source = new DirectoryInfo(FixPath(path)); - DirectoryInfo dest = new DirectoryInfo(FixPath(Path.Combine(newPath, source.Name))); - if(dest.FullName.IndexOf(source.FullName) == 0) - throw new Exception(LangRes("E_CannotMoveDirToChild")); - else if (!source.Exists) - throw new Exception(LangRes("E_MoveDirInvalisPath")); - else if (dest.Exists) - throw new Exception(LangRes("E_DirAlreadyExists")); - else{ - try{ - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch { - throw new Exception(LangRes("E_MoveDir") + " \"" + path + "\""); - } - } - - } - protected void MoveFile(string path, string newPath) - { - CheckPath(path); - CheckPath(newPath); - FileInfo source = new FileInfo(FixPath(path)); - FileInfo dest = new FileInfo(FixPath(newPath)); - if (!source.Exists) - throw new Exception(LangRes("E_MoveFileInvalisPath")); - else if (dest.Exists) - throw new Exception(LangRes("E_MoveFileAlreadyExists")); - else - { - try - { - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_MoveFile") + " \"" + path + "\""); - } - } - } - protected void RenameDir(string path, string name) - { - CheckPath(path); - DirectoryInfo source = new DirectoryInfo(FixPath(path)); - DirectoryInfo dest = new DirectoryInfo(Path.Combine(source.Parent.FullName, name)); - if(source.FullName == GetFilesRoot()) - throw new Exception(LangRes("E_CannotRenameRoot")); - else if (!source.Exists) - throw new Exception(LangRes("E_RenameDirInvalidPath")); - else if (dest.Exists) - throw new Exception(LangRes("E_DirAlreadyExists")); - else - { - try - { - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_RenameDir") + " \"" + path + "\""); - } - } - } - protected void RenameFile(string path, string name) - { - CheckPath(path); - FileInfo source = new FileInfo(FixPath(path)); - FileInfo dest = new FileInfo(Path.Combine(source.Directory.FullName, name)); - if (!source.Exists) - throw new Exception(LangRes("E_RenameFileInvalidPath")); - else if (!CanHandleFile(name)) - throw new Exception(LangRes("E_FileExtensionForbidden")); - else - { - try - { - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch (Exception ex) - { - throw new Exception(ex.Message + "; " + LangRes("E_RenameFile") + " \"" + path + "\""); - } - } - } - public bool ThumbnailCallback() - { - return false; - } - - protected void ShowThumbnail(string path, int width, int height) - { - CheckPath(path); - FileStream fs = new FileStream(FixPath(path), FileMode.Open); - Bitmap img = new Bitmap(Bitmap.FromStream(fs)); - fs.Close(); - fs.Dispose(); - int cropWidth = img.Width, cropHeight = img.Height; - int cropX = 0, cropY = 0; - - double imgRatio = (double)img.Width / (double)img.Height; - - if(height == 0) - height = Convert.ToInt32(Math.Floor((double)width / imgRatio)); - - if (width > img.Width) - width = img.Width; - if (height > img.Height) - height = img.Height; - - double cropRatio = (double)width / (double)height; - cropWidth = Convert.ToInt32(Math.Floor((double)img.Height * cropRatio)); - cropHeight = Convert.ToInt32(Math.Floor((double)cropWidth / cropRatio)); - if (cropWidth > img.Width) - { - cropWidth = img.Width; - cropHeight = Convert.ToInt32(Math.Floor((double)cropWidth / cropRatio)); - } - if (cropHeight > img.Height) - { - cropHeight = img.Height; - cropWidth = Convert.ToInt32(Math.Floor((double)cropHeight * cropRatio)); - } - if(cropWidth < img.Width){ - cropX = Convert.ToInt32(Math.Floor((double)(img.Width - cropWidth) / 2)); - } - if(cropHeight < img.Height){ - cropY = Convert.ToInt32(Math.Floor((double)(img.Height - cropHeight) / 2)); - } - - Rectangle area = new Rectangle(cropX, cropY, cropWidth, cropHeight); - Bitmap cropImg = img.Clone(area, System.Drawing.Imaging.PixelFormat.DontCare); - img.Dispose(); - Image.GetThumbnailImageAbort imgCallback = new Image.GetThumbnailImageAbort(ThumbnailCallback); - - _r.AddHeader("Content-Type", "image/png"); - cropImg.GetThumbnailImage(width, height, imgCallback, IntPtr.Zero).Save(_r.OutputStream, ImageFormat.Png); - _r.OutputStream.Close(); - cropImg.Dispose(); - } - private ImageFormat GetImageFormat(string filename){ - ImageFormat ret = ImageFormat.Jpeg; - switch(new FileInfo(filename).Extension.ToLower()){ - case ".png": ret = ImageFormat.Png; break; - case ".gif": ret = ImageFormat.Gif; break; - } - return ret; - } - protected void ImageResize(string path, string dest, int width, int height) - { - FileStream fs = new FileStream(path, FileMode.Open); - Image img = Image.FromStream(fs); - fs.Close(); - fs.Dispose(); - float ratio = (float)img.Width / (float)img.Height; - if ((img.Width <= width && img.Height <= height) || (width == 0 && height == 0)) - return; - - int newWidth = width; - int newHeight = Convert.ToInt16(Math.Floor((float)newWidth / ratio)); - if ((height > 0 && newHeight > height) || (width == 0)) - { - newHeight = height; - newWidth = Convert.ToInt16(Math.Floor((float)newHeight * ratio)); - } - Bitmap newImg = new Bitmap(newWidth, newHeight); - Graphics g = Graphics.FromImage((Image)newImg); - g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - g.DrawImage(img, 0, 0, newWidth, newHeight); - img.Dispose(); - g.Dispose(); - if(dest != ""){ - newImg.Save(dest, GetImageFormat(dest)); - } - newImg.Dispose(); - } - - protected void Upload(string path) - { - CheckPath(path); - path = FixPath(path); - string res = GetSuccessRes(); - try{ - for(int i = 0; i < HttpContext.Current.Request.Files.Count; i++){ - if (CanHandleFile(HttpContext.Current.Request.Files[i].FileName)) - { - string filename = MakeUniqueFilename(path, HttpContext.Current.Request.Files[i].FileName); - string dest = Path.Combine(path, filename); - HttpContext.Current.Request.Files[i].SaveAs(dest); - if(GetFileType(new FileInfo(filename).Extension) == "image"){ - int w = 0; - int h = 0; - int.TryParse(GetSetting("MAX_IMAGE_WIDTH"), out w); - int.TryParse(GetSetting("MAX_IMAGE_HEIGHT"), out h); - ImageResize(dest, dest, w, h); - } - } - else - res = GetSuccessRes(LangRes("E_UploadNotAll")); - } - } - catch(Exception ex){ - res = GetErrorRes(ex.Message); - } - _r.Write(""); - } - - public bool IsReusable { - get { - return false; - } - } - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/conf.json b/src/Presentation/SmartStore.Web/Content/filemanager/conf.json deleted file mode 100644 index 3df29bf505..0000000000 --- a/src/Presentation/SmartStore.Web/Content/filemanager/conf.json +++ /dev/null @@ -1,34 +0,0 @@ -{ -"FILES_ROOT": "~/Media/Uploaded", -"SESSION_PATH_KEY": "", -"THUMBS_VIEW_WIDTH": "140", -"THUMBS_VIEW_HEIGHT": "120", -"PREVIEW_THUMB_WIDTH":"300", -"PREVIEW_THUMB_HEIGHT":"200", -"MAX_IMAGE_WIDTH": "0", -"MAX_IMAGE_HEIGHT": "0", -"INTEGRATION": "ckeditor", -"DIRLIST": "asp_net/main.ashx?a=DIRLIST", -"CREATEDIR": "asp_net/main.ashx?a=CREATEDIR", -"DELETEDIR": "asp_net/main.ashx?a=DELETEDIR", -"MOVEDIR": "asp_net/main.ashx?a=MOVEDIR", -"COPYDIR": "asp_net/main.ashx?a=COPYDIR", -"RENAMEDIR": "asp_net/main.ashx?a=RENAMEDIR", -"FILESLIST": "asp_net/main.ashx?a=FILESLIST", -"UPLOAD": "asp_net/main.ashx?a=UPLOAD", -"DOWNLOAD": "asp_net/main.ashx?a=DOWNLOAD", -"DOWNLOADDIR": "asp_net/main.ashx?a=DOWNLOADDIR", -"DOWNLOADDIR": "asp_net/main.ashx?a=DOWNLOADDIR", -"DELETEFILE": "asp_net/main.ashx?a=DELETEFILE", -"MOVEFILE": "asp_net/main.ashx?a=MOVEFILE", -"COPYFILE": "asp_net/main.ashx?a=COPYFILE", -"RENAMEFILE": "asp_net/main.ashx?a=RENAMEFILE", -"GENERATETHUMB": "asp_net/main.ashx?a=GENERATETHUMB", -"DEFAULTVIEW": "list", -"FORBIDDEN_UPLOADS": "zip js jsp jsb mhtml mht xhtml xht php phtml php3 php4 php5 phps shtml jhtml pl sh py cgi exe scr dll msi vbs bat com pif cmd vxd cpl htpasswd htaccess", -"ALLOWED_UPLOADS": "", -"FILEPERMISSIONS": "0644", -"DIRPERMISSIONS": "0755", -"LANG": "auto", -"DATEFORMAT": "dd.MM.yyyy HH:mm" -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/install/install.less b/src/Presentation/SmartStore.Web/Content/install/install.less index e079affb89..bdb13586e8 100644 --- a/src/Presentation/SmartStore.Web/Content/install/install.less +++ b/src/Presentation/SmartStore.Web/Content/install/install.less @@ -11,17 +11,6 @@ /* Fonts (Chrome DirectWrite & FF fix) -------------------------------------------------------------- */ -@font-face { - font-family: "Segoe UI Light"; - font-weight: 100; - src: local("Segoe UI Light"), url("../fonts/segoeui-semilight.eot") format('embedded-opentype'), url("../fonts/segoeui-semilight.woff") format('woff'), url("../fonts/segoeui-semilight.ttf") format('truetype'); -} -@font-face { - font-family: "Segoe UI Semibold"; - font-weight: 100; - src: local("Segoe UI Semibold"), url("../fonts/segoeui-semibold.eot") format('embedded-opentype'), url("../fonts/segoeui-semibold.woff") format('woff'), url("../fonts/segoeui-semibold.ttf") format('truetype'); -} - // CSS Reset @import "~/Content/bootstrap/reset.less"; @@ -66,6 +55,7 @@ @import "~/Content/bootstrap/progress-bars.less"; // (MC) extra 3rd party or own components +@import "~/Content/bootstrap/custom/spinner.less"; @import "~/Content/bootstrap/custom/throbber.less"; // (MC) extra tweaks and corrections @@ -77,63 +67,132 @@ /* INSTALL STYLES --------------------------------------------------------*/ -@installBaseColor: #29b6d9; body { - background: @installBaseColor url('images/bg.png') 50% 0% no-repeat; - background-attachment: fixed; + background: #f5f5f5; +} + +input:not([type=checkbox]):not([type=radio]), +select { + display: block; + box-sizing: border-box; + width: 100%; + height: 34px; + box-shadow: none; + border-color: #d2d2d2; +} + +textarea { + height: initial; + min-height: 34px; + box-shadow: none; + border-color: #d2d2d2; +} + +.bg-top { + position: fixed; + z-index: 0; + background: #3F51B5; + width: 100%; + height: 140px; +} + +.dropdown-menu li > a { + padding: 8px 20px 8px 12px; + > .fa { margin-right: 4px; } +} + +.help-block { + color: #a1a1a1; + margin-top: 8px; + font-size: 12px; } .install-head { - padding: 8px 0; - line-height: 40px; + position: relative; + z-index: 1; + margin: 20px 0; vertical-align: middle; } .install-panel { - margin-top: 4px; + position: relative; + z-index: 1; + margin-top: 20px; margin-bottom: 20px; padding: 30px; + padding-bottom: 0; background-color: #fff; - background-color: rgba(255,255,255, .5); - .box-shadow(~'0 0 15px rgba(0,0,0, .15), 0 10px 6px -10px rgba(0,0,0, .4)'); - .border-radius(4px); + .box-shadow(~'0 1px 1px 0 rgba(0,0,0,.06),0 2px 5px 0 rgba(0,0,0,.2)'); + .border-radius(3px); +} + +.buttonbar { + text-align: left; + background: #fafafa; + padding: 20px 30px; + margin-left: -30px; + margin-right: -30px; + border-radius: 0 0 3px 3px; +} + +.btn-install { + font-size: 22px; + line-height: 32px; + font-weight: 600; + padding-left: 1.5em; + padding-right: 1.5em; } .install-title { margin: 0; - color: #fff; - color: lighten(@installBaseColor, 40%); - text-shadow: 0 1px 0 darken(@installBaseColor, 12%); font-family: @pageTitleFontFamily; font-weight: @pageTitleFontWeight; + font-size: 22px; + color: #fff; + padding-left: 8px; + vertical-align: bottom; +} + +.install-intro { + font-size: 16px; + line-height: 22px; + color: #818181; + margin-bottom: 30px; } -.install-content { - //padding: 0 30px; +.install-content fieldset { + margin-bottom: 30px; } .install-content legend { - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; + font-weight: 600; font-size: 22px; - border-bottom-color: lighten(@installBaseColor, 12%); border-bottom-color: rgba(0,0,0, 0.1); color: @headingsColor; margin-bottom: 0; padding-bottom: 8px; + + > .fa { + color: #717171; + margin-right: 4px; + } } .install-icon { - font-size: 32px; - line-height: 32px; - vertical-align: bottom; - margin-right: 4px; - display: none; + margin-right: 10px; +} + +.install-icon .fa-circle { + color: #f90; +} +.install-icon .fa-inverse { + font-size: 0.7em; } .form-horizontal .control-label { width: 240px; + text-align: left; } .form-horizontal .controls { @@ -145,8 +204,10 @@ label.control-label { } .field-validation-error { - color: red; + display: inline-block; + color: @red; text-shadow: none !important; + padding-top: 4px; } label.control-label { @@ -154,3 +215,54 @@ label.control-label { } +#navbar-tools { + + .active-tool() { + .box-shadow(~'inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)'); + } + + position: absolute; + z-index: 100; + right: 0; + top: 15px; + margin: 0; + + .btn, + .btn-group { + margin-top: 0; + } + + .open .navbar-tool { + .active-tool(); + } + + .navbar-tool { + color: rgba(255,255,255, .87); + border: none; + background: transparent; + .box-shadow(none); + padding: 8px 6px; + .transition(color 0.1s linear); + + &:hover { + color: #fff; + } + + &:active { + .active-tool(); + } + + > .fa { + font-size: 20px; + } + + span { + display: inline-block; + margin-left: 3px; + vertical-align: top; + } + + } +} + + diff --git a/src/Presentation/SmartStore.Web/Content/install/variables.custom.less b/src/Presentation/SmartStore.Web/Content/install/variables.custom.less index 76db5fcef8..2bcad0ceeb 100644 --- a/src/Presentation/SmartStore.Web/Content/install/variables.custom.less +++ b/src/Presentation/SmartStore.Web/Content/install/variables.custom.less @@ -5,6 +5,6 @@ // Nicer page titles // ------------------------- -@pageTitleFontFamily: 'Segoe UI Light', 'Segoe UI', Arial, Helvetica, sans-serif; -@pageTitleFontWeight: 100; +@pageTitleFontFamily: 'Segoe UI', Arial, Helvetica, sans-serif; +@pageTitleFontWeight: 400; @pageTitleColor: @infoText; // @linkColor; // #009fff; diff --git a/src/Presentation/SmartStore.Web/Content/install/variables.less b/src/Presentation/SmartStore.Web/Content/install/variables.less index 4d85eb8b75..d4daaab546 100644 --- a/src/Presentation/SmartStore.Web/Content/install/variables.less +++ b/src/Presentation/SmartStore.Web/Content/install/variables.less @@ -71,9 +71,9 @@ @paddingSmall: 2px 10px; // 26px @paddingMini: 1px 6px; // 24px -@baseBorderRadius: 0; +@baseBorderRadius: 3px; @borderRadiusLarge: 4px; -@borderRadiusSmall: 0; +@borderRadiusSmall: 2px; // Tables @@ -128,10 +128,10 @@ @dropdownLinkColor: @grayDark; -@dropdownLinkColorHover: @white; -@dropdownLinkColorActive: @dropdownLinkColor; -@dropdownLinkBackgroundActive: #666; //@linkColor; -@dropdownLinkBackgroundHover: @dropdownLinkBackgroundActive; +@dropdownLinkColorHover: inherit; +@dropdownLinkColorActive: inherit; +@dropdownLinkBackgroundActive: #f5f5f5; +@dropdownLinkBackgroundHover: #f5f5f5; diff --git a/src/Presentation/SmartStore.Web/Content/smartstore.entitypicker.css b/src/Presentation/SmartStore.Web/Content/smartstore.entitypicker.css new file mode 100644 index 0000000000..2c821195fb --- /dev/null +++ b/src/Presentation/SmartStore.Web/Content/smartstore.entitypicker.css @@ -0,0 +1,124 @@ +/* -------------------------------------------------------------- + SmartStore Component: smartstore.entitypicker.css +-------------------------------------------------------------- */ + +.entity-picker-list { + position: relative; + margin-left: -5px; + margin-right: -5px; +} + +.entity-picker-list .item-wrap { + position: relative; + box-sizing: border-box; + display: block; + float: left; + width: 33.3332%; + padding: 0 5px; +} + +.entity-picker-list .item { + position: relative; + box-sizing: border-box; + display: block; + height: 70px; + padding: 8px; + margin: 5px 0; + border-radius: 3px; +} +.entity-picker-list .item:not(.disable):hover { + cursor: pointer; +} +.entity-picker-list .item:not(.selected):hover { + background-color: #f5f5f5; +} + +.entity-picker-list .disable { + opacity: 0.4; +} + +.entity-picker-list .title { + font-weight: 400; + max-height: 36px; + overflow: hidden; + text-overflow: ellipsis; +} + +.entity-picker-list .highlight { + font-weight: 700; +} + +.entity-picker-list .summary { + color: #aaa; + font-size: 12px; + height: 18px; + line-height: 18px; + vertical-align: middle; + overflow: hidden; +} +.entity-picker-list .published { + margin-left: 1px; + margin-right: 6px; + font-size: 15px; + line-height: 18px; + vertical-align: sub; +} +.entity-picker-list .published.fa-globe { + color: inherit; +} +.entity-picker-list .published.fa-eye-slash { + color: #aaa; +} +.entity-picker-list .item.selected .published { + color: #fff; +} + +.entity-picker-list .list-footer { + clear: both; + padding: 12px; + text-align: center; +} + +.entity-picker-list .thumb, +.entity-picker-list .data { + position: relative; + box-sizing: border-box; + display: block; +} +.entity-picker-list .thumb { + display: table-cell; + width: 54px; + height: 54px; + max-width: 54px; + max-height: 54px; + padding: 2px; + text-align: center; + vertical-align: middle; +} +.entity-picker-list .item:hover .thumb, +.entity-picker-list .item.selected .thumb { + background: #fff; + box-shadow: 1px 1px 2px rgba(0,0,0, 0.1); +} +.entity-picker-list .thumb img { + max-width: 100%; + max-height: 100%; +} +.entity-picker-list .thumb + .data { + position: absolute; + left: 72px; + right: 8px; + top: 8px; + bottom: 8px; +} + +.entity-picker-list .item.selected { + background-color: #4060c0; +} +.entity-picker-list .item.selected .title { + color: #fff; +} +.entity-picker-list .item.selected .summary { + color: rgba(255,255,255, 0.6); +} + diff --git a/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css b/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css index 2e196f8168..3ab48b4a6b 100644 --- a/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css +++ b/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css @@ -9,6 +9,7 @@ } .sg-image-wrapper { + position: relative !important; width: 100%; min-height: 300px; /* just to avoid page load flickering. This is overwritten per inline css anyway */ margin-bottom: 5px; @@ -16,8 +17,6 @@ overflow: hidden; outline: 1px solid transparent; text-align: center; - /*background-color: #fff; - border: 1px solid #d5d5d5;*/ } .sg-image-wrapper > img { @@ -27,9 +26,10 @@ .sg-loader { position: absolute; z-index: 10; - top: 48%; - left: 48%; - border: 1px solid #ccc; + left: 0; + top: 0; + right: 0; + bottom: 0; } .sg-image { diff --git a/src/Presentation/SmartStore.Web/Controllers/BlogController.cs b/src/Presentation/SmartStore.Web/Controllers/BlogController.cs index 17f7e8b5bc..bae872f9e9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/BlogController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/BlogController.cs @@ -339,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, @@ -356,6 +353,7 @@ public ActionResult BlogCommentAdd(int blogPostId, BlogPostModel model, bool cap requestContext: this.ControllerContext.RequestContext, includeImplicitMvcValues: true /*helps fill in the nulls above*/ ); + return Redirect(url); } diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs index b39bc4f0ec..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; @@ -33,7 +31,7 @@ namespace SmartStore.Web.Controllers { - public partial class CatalogController : PublicControllerBase + public partial class CatalogController : PublicControllerBase { #region Fields @@ -135,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; @@ -212,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) }; @@ -365,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(""); @@ -405,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) }; @@ -442,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; @@ -563,7 +567,7 @@ public ActionResult Manufacturer(int manufacturerId, CatalogPagingFilteringModel public ActionResult ManufacturerAll() { var model = new List(); - var manufacturers = _manufacturerService.GetAllManufacturers(); + var manufacturers = _manufacturerService.GetAllManufacturers(null, _services.StoreContext.CurrentStore.Id); foreach (var manufacturer in manufacturers) { var modelMan = manufacturer.ToModel(); @@ -582,13 +586,19 @@ public ActionResult ManufacturerNavigation(int currentManufacturerId) if (_catalogSettings.ManufacturersBlockItemsToDisplay == 0 || _catalogSettings.ShowManufacturersOnHomepage == false) return Content(""); - string cacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_NAVIGATION_MODEL_KEY, currentManufacturerId, _services.WorkContext.WorkingLanguage.Id, _services.StoreContext.CurrentStore.Id); + var cacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_NAVIGATION_MODEL_KEY, + currentManufacturerId, + !_catalogSettings.HideManufacturerDefaultPictures, + _services.WorkContext.WorkingLanguage.Id, + _services.StoreContext.CurrentStore.Id); + var cacheModel = _services.Cache.Get(cacheKey, () => { var currentManufacturer = _manufacturerService.GetManufacturerById(currentManufacturerId); - var manufacturers = _manufacturerService.GetAllManufacturers(); - var model = new ManufacturerNavigationModel() + var manufacturers = _manufacturerService.GetAllManufacturers(null, _services.StoreContext.CurrentStore.Id); + + var model = new ManufacturerNavigationModel { TotalManufacturers = manufacturers.Count, DisplayManufacturers = _catalogSettings.ShowManufacturersOnHomepage, @@ -602,7 +612,8 @@ public ActionResult ManufacturerNavigation(int currentManufacturerId) Id = manufacturer.Id, Name = manufacturer.GetLocalized(x => x.Name), SeName = manufacturer.GetSeName(), - PictureUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), _mediaSettings.ManufacturerThumbPictureSize), + PictureUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), _mediaSettings.ManufacturerThumbPictureSize, + !_catalogSettings.HideManufacturerDefaultPictures), IsActive = currentManufacturer != null && currentManufacturer.Id == manufacturer.Id, }; model.Manufacturers.Add(modelMan); @@ -1012,7 +1023,7 @@ public ActionResult CompareProducts() var products = _compareProductsService.GetComparedProducts(); - _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, prepareFullDescription: true) + _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, prepareFullDescription: true, isCompareList: true) .ToList() .ForEach(model.Products.Add); @@ -1062,7 +1073,7 @@ public ActionResult FlyoutCompare() var products = _compareProductsService.GetComparedProducts(); - _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true) + _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, isCompareList: true) .ToList() .ForEach(model.Products.Add); @@ -1081,10 +1092,13 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command model = new SearchModel(); // 'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageSize <= 0) command.PageSize = _catalogSettings.SearchPageProductsPerPage; @@ -1092,9 +1106,7 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command command.PageNumber = 1; if (command.OrderBy == (int)ProductSortingEnum.Initial) - { command.OrderBy = (int)_catalogSettings.DefaultSortOrder; - } _helper.PreparePagingFilteringModel(model.PagingFilteringContext, command, new PageSizeContext { @@ -1103,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); @@ -1122,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 @@ -1136,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) { @@ -1161,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 @@ -1182,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); } @@ -1202,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; @@ -1223,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); @@ -1252,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 56e504c7d3..ebc506887c 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Linq.Expressions; using System.Web; using System.Web.Mvc; using SmartStore.Collections; @@ -25,14 +25,12 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Tax; +using SmartStore.Services.Topics; using SmartStore.Web.Framework.UI; using SmartStore.Web.Framework.UI.Captcha; using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Media; -using SmartStore.Core.Data; -using System.Collections.Specialized; -using SmartStore.Services.Topics; namespace SmartStore.Web.Controllers { @@ -152,7 +150,8 @@ public ProductDetailsModel PrepareProductDetailsPageModel( bool isAssociatedProduct = false, ProductBundleItemData productBundleItem = null, IList productBundleItems = null, - NameValueCollection selectedAttributes = null) + NameValueCollection selectedAttributes = null, + NameValueCollection queryData = null) { if (product == null) throw new ArgumentNullException("product"); @@ -170,7 +169,7 @@ public ProductDetailsModel PrepareProductDetailsPageModel( ProductType = product.ProductType, VisibleIndividually = product.VisibleIndividually, //Manufacturers = _manufacturerService.GetProductManufacturersByProductId(product.Id), - Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id)), + Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id), null, true), ReviewCount = product.ApprovedTotalReviews, DisplayAdminLink = _services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel), //EnableHtmlTextCollapser = Convert.ToBoolean(_settingService.GetSettingByKey("CatalogSettings.EnableHtmlTextCollapser")), @@ -187,6 +186,30 @@ public ProductDetailsModel PrepareProductDetailsPageModel( IsCurrentCustomerRegistered = _services.WorkContext.CurrentCustomer.IsRegistered() }; + // get gift card values from query string + if (queryData != null && queryData.Count > 0) + { + var giftCardItems = queryData.AllKeys + .Where(x => x.EmptyNull().StartsWith("giftcard_")) + .SelectMany(queryData.GetValues, (k, v) => new { key = k, value = v.TrimSafe() }); + + foreach (var item in giftCardItems) + { + var key = item.key.EmptyNull().ToLower(); + + if (key.EndsWith("recipientname")) + model.GiftCard.RecipientName = item.value; + else if (key.EndsWith("recipientemail")) + model.GiftCard.RecipientEmail = item.value; + else if (key.EndsWith("sendername")) + model.GiftCard.SenderName = item.value; + else if (key.EndsWith("senderemail")) + model.GiftCard.SenderEmail = item.value; + else if (key.EndsWith("message")) + model.GiftCard.Message = item.value; + } + } + // Back in stock subscriptions if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && product.BackorderMode == BackorderMode.NoBackorders && @@ -216,6 +239,7 @@ public ProductDetailsModel PrepareProductDetailsPageModel( { var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, StoreId = _services.StoreContext.CurrentStore.Id, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, @@ -235,11 +259,15 @@ public ProductDetailsModel PrepareProductDetailsPageModel( { var item = itemData.Item; var bundleItemAttributes = new NameValueCollection(); - var keyPrefix = "product_attribute_{0}_{1}".FormatInvariant(item.ProductId, item.Id); - foreach (var key in selectedAttributes.AllKeys.Where(x => x.HasValue() && x.StartsWith(keyPrefix))) + if (selectedAttributes != null) { - bundleItemAttributes.Add(key, selectedAttributes[key]); + var keyPrefix = "product_attribute_{0}_{1}".FormatInvariant(item.ProductId, item.Id); + + foreach (var key in selectedAttributes.AllKeys.Where(x => x.HasValue() && x.StartsWith(keyPrefix))) + { + bundleItemAttributes.Add(key, selectedAttributes[key]); + } } var bundledProductModel = PrepareProductDetailsPageModel(item.Product, false, itemData, null, bundleItemAttributes); @@ -320,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 @@ -411,12 +439,16 @@ public void PrepareProductDetailsPictureModel( { model.DefaultPictureModel = new PictureModel { - ThumbImageUrl = _pictureService.GetDefaultPictureUrl(_mediaSettings.ProductThumbPictureSizeOnProductDetailsPage), - ImageUrl = _pictureService.GetDefaultPictureUrl(defaultPictureSize), - FullSizeImageUrl = _pictureService.GetDefaultPictureUrl(), Title = T("Media.Product.ImageLinkTitleFormat", model.Name), AlternateText = model.AlternateText }; + + if (!_catalogSettings.HideProductDefaultPictures) + { + model.DefaultPictureModel.ThumbImageUrl = _pictureService.GetDefaultPictureUrl(_mediaSettings.ProductThumbPictureSizeOnProductDetailsPage); + model.DefaultPictureModel.ImageUrl = _pictureService.GetDefaultPictureUrl(defaultPictureSize); + model.DefaultPictureModel.FullSizeImageUrl = _pictureService.GetDefaultPictureUrl(); + } } else { @@ -496,9 +528,31 @@ public ProductDetailsModel PrepareProductDetailModel( pvaModel.BeginYear = match.Groups[1].Value.ToInt(); pvaModel.EndYear = match.Groups[2].Value.ToInt(); } + + if (hasSelectedAttributes) + { + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + var day = selectedAttributes[attributeKey + "_day"].ToInt(); + var month = selectedAttributes[attributeKey + "_month"].ToInt(); + var year = selectedAttributes[attributeKey + "_year"].ToInt(); + if (day > 0 && month > 0 && year > 0) + { + pvaModel.SelectedDay = day; + pvaModel.SelectedMonth = month; + pvaModel.SelectedYear = year; + } + } + } + else if (attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox) + { + if (hasSelectedAttributes) + { + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + pvaModel.TextValue = selectedAttributes[attributeKey]; + } } - int preSelectedValueId = 0; + var preSelectedValueId = 0; var pvaValues = (attribute.ShouldHaveValues() ? _productAttributeService.GetProductVariantAttributeValues(attribute.Id) : new List()); foreach (var pvaValue in pvaValues) @@ -562,7 +616,7 @@ public ProductDetailsModel PrepareProductDetailModel( { var linkagePicture = _pictureService.GetPicturesByProductId(pvaValue.LinkedProductId, 1).FirstOrDefault(); if (linkagePicture != null) - pvaValueModel.ImageUrl = _pictureService.GetPictureUrl(linkagePicture, _mediaSettings.AutoCompleteSearchThumbPictureSize, false); + pvaValueModel.ImageUrl = _pictureService.GetPictureUrl(linkagePicture, _mediaSettings.VariantValueThumbPictureSize, false); } pvaModel.Values.Add(pvaValueModel); @@ -1050,7 +1104,8 @@ public IEnumerable PrepareProductOverviewModels( bool prepareColorAttributes = false, bool prepareManufacturers = false, bool isCompact = false, - bool prepareFullDescription = false) + bool prepareFullDescription = false, + bool isCompareList = false) { if (products == null) throw new ArgumentNullException("products"); @@ -1098,6 +1153,7 @@ public IEnumerable PrepareProductOverviewModels( foreach (var product in products) { var contextProduct = product; + var finalPrice = decimal.Zero; var model = new ProductOverviewModel { @@ -1133,6 +1189,7 @@ public IEnumerable PrepareProductOverviewModels( var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, StoreId = currentStore.Id, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, @@ -1177,7 +1234,7 @@ public IEnumerable PrepareProductOverviewModels( decimal taxRate = decimal.Zero; decimal oldPriceBase = _taxService.GetProductPrice(contextProduct, contextProduct.OldPrice, out taxRate); decimal finalPriceBase = _taxService.GetProductPrice(contextProduct, displayPrice.Value, out taxRate); - decimal finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); + finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); priceModel.OldPrice = null; @@ -1245,7 +1302,7 @@ public IEnumerable PrepareProductOverviewModels( decimal finalPriceBase = _taxService.GetProductPrice(product, displayPrice, out taxRate); decimal oldPrice = _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, workingCurrency); - decimal finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); + finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); priceModel.HasDiscount = (finalPriceBase != oldPriceBase && oldPriceBase != decimal.Zero); @@ -1325,8 +1382,8 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) var picture = product.GetDefaultProductPicture(_pictureService); var pictureModel = new PictureModel { - ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize), - FullSizeImageUrl = _pictureService.GetPictureUrl(picture), + ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize, !_catalogSettings.HideProductDefaultPictures), + FullSizeImageUrl = _pictureService.GetPictureUrl(picture, 0, !_catalogSettings.HideProductDefaultPictures), Title = string.Format(res["Media.Product.ImageLinkTitleFormat"], model.Name), AlternateText = string.Format(res["Media.Product.ImageAlternateTextFormat"], model.Name) }; @@ -1382,19 +1439,22 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) if (prepareManufacturers) { - model.Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id), cachedManufacturerModels); + model.Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id), cachedManufacturerModels, false); } - if (_catalogSettings.ShowBasePriceInProductLists) + if (finalPrice != decimal.Zero && (_catalogSettings.ShowBasePriceInProductLists || isCompareList)) { - model.BasePriceInfo = contextProduct.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, workingCurrency); + model.BasePriceInfo = contextProduct.GetBasePriceInfo(finalPrice, _localizationService, _priceFormatter, workingCurrency); } - var addShippingPrice = _currencyService.ConvertCurrency(contextProduct.AdditionalShippingCharge, currentStore.PrimaryStoreCurrency, workingCurrency); - - if (addShippingPrice > 0 && displayPrices) + if (displayPrices) { - model.TransportSurcharge = res["Common.AdditionalShippingSurcharge"].Text.FormatWith(_priceFormatter.FormatPrice(addShippingPrice, true, false)); + var addShippingPrice = _currencyService.ConvertCurrency(contextProduct.AdditionalShippingCharge, currentStore.PrimaryStoreCurrency, workingCurrency); + + if (addShippingPrice > 0) + { + model.TransportSurcharge = res["Common.AdditionalShippingSurcharge"].Text.FormatCurrent(_priceFormatter.FormatPrice(addShippingPrice, true, false)); + } } if (contextProduct.Weight > 0) @@ -1405,7 +1465,7 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) // IsNew if (_catalogSettings.LabelAsNewForMaxDays.HasValue) { - model.IsNew = (DateTime.UtcNow - product.CreatedOnUtc).Days <= _catalogSettings.LabelAsNewForMaxDays.Value; + model.IsNew = ((DateTime.UtcNow - product.CreatedOnUtc).Days <= _catalogSettings.LabelAsNewForMaxDays.Value); } models.Add(model); @@ -1713,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(); @@ -1738,7 +1799,10 @@ public List PrepareManufacturersOverviewModel( }; - item.PictureModel = PrepareManufacturerPictureModel(manufacturer, manufacturer.GetLocalized(x => x.Name)); + if (_catalogSettings.ShowManufacturerPicturesInProductDetail) + { + item.PictureModel = PrepareManufacturerPictureModel(manufacturer, manufacturer.GetLocalized(x => x.Name)); + } cachedModels.Add(item.Id, item); } @@ -1753,11 +1817,11 @@ public PictureModel PrepareManufacturerPictureModel(Manufacturer manufacturer, s { var model = new PictureModel(); - int pictureSize = _mediaSettings.ManufacturerThumbPictureSize; + var pictureSize = _mediaSettings.ManufacturerThumbPictureSize; var manufacturerPictureCacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_PICTURE_MODEL_KEY, manufacturer.Id, pictureSize, - true, + !_catalogSettings.HideManufacturerDefaultPictures, _services.WorkContext.WorkingLanguage.Id, _services.WebHelper.IsCurrentConnectionSecured(), _services.StoreContext.CurrentStore.Id); @@ -1768,7 +1832,7 @@ public PictureModel PrepareManufacturerPictureModel(Manufacturer manufacturer, s { PictureId = manufacturer.PictureId.GetValueOrDefault(), //FullSizeImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault()), - ImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), pictureSize), + ImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), pictureSize, !_catalogSettings.HideManufacturerDefaultPictures), Title = string.Format(T("Media.Manufacturer.ImageLinkTitleFormat"), localizedName), AlternateText = string.Format(T("Media.Manufacturer.ImageAlternateTextFormat"), localizedName) }; diff --git a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs index e0ec2e2fd1..5047764d52 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs @@ -360,6 +360,7 @@ protected CheckoutConfirmModel PrepareConfirmOrderModel(IList( - SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, _storeContext.CurrentStore.Id); + processPaymentRequest.StoreId = storeId; + processPaymentRequest.CustomerId = customer.Id; + processPaymentRequest.PaymentMethodSystemName = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, storeId); var placeOrderExtraData = new Dictionary(); placeOrderExtraData["CustomerComment"] = form["customercommenthidden"]; + placeOrderExtraData["SubscribeToNewsLetter"] = form["SubscribeToNewsLetterHidden"]; + placeOrderExtraData["AcceptThirdPartyEmailHandOver"] = form["AcceptThirdPartyEmailHandOverHidden"]; - var placeOrderResult = _orderProcessingService.PlaceOrder(processPaymentRequest, placeOrderExtraData); - - if (placeOrderResult.Success) - { - var postProcessPaymentRequest = new PostProcessPaymentRequest - { - Order = placeOrderResult.PlacedOrder - }; - - _paymentService.PostProcessPayment(postProcessPaymentRequest); - - _httpContext.Session["PaymentData"] = null; - _httpContext.Session["OrderPaymentInfo"] = null; - _httpContext.RemoveCheckoutState(); - - if (postProcessPaymentRequest.RedirectUrl.HasValue()) - { - return Redirect(postProcessPaymentRequest.RedirectUrl); - } + placeOrderResult = _orderProcessingService.PlaceOrder(processPaymentRequest, placeOrderExtraData); - return RedirectToAction("Completed"); - } - else + if (!placeOrderResult.Success) { - model.Warnings.AddRange(placeOrderResult.Errors); + model.Warnings.AddRange(placeOrderResult.Errors.Select(x => HtmlUtils.ConvertPlainTextToHtml(x))); } } - catch (Exception exc) + catch (Exception exception) { - Logger.Warning(exc.Message, exc); - model.Warnings.Add(exc.Message); + Logger.Warning(exception.Message, exception); + + if (!model.Warnings.Any(x => x == exception.Message)) + { + model.Warnings.Add(exception.Message); + } } - return View(model); - } + if (placeOrderResult == null || !placeOrderResult.Success || model.Warnings.Any()) + { + return View(model); + } + + try + { + postProcessPaymentRequest = new PostProcessPaymentRequest + { + Order = placeOrderResult.PlacedOrder + }; + + _paymentService.PostProcessPayment(postProcessPaymentRequest); + } + catch (Exception exception) + { + NotifyError(exception); + } + finally + { + _httpContext.Session["PaymentData"] = null; + _httpContext.Session["OrderPaymentInfo"] = null; + _httpContext.RemoveCheckoutState(); + } + + if (postProcessPaymentRequest != null && postProcessPaymentRequest.RedirectUrl.HasValue()) + { + return Redirect(postProcessPaymentRequest.RedirectUrl); + } + + return RedirectToAction("Completed"); + } public ActionResult Completed() diff --git a/src/Presentation/SmartStore.Web/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Controllers/CommonController.cs index 877c920a2d..05cdc3bdc6 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CommonController.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Globalization; using System.Linq; using System.Text; using System.Web.Mvc; +using SmartStore.Core.Data; using SmartStore.Core.Domain; using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Catalog; @@ -22,6 +22,7 @@ using SmartStore.Core.Domain.Tax; using SmartStore.Core.Domain.Themes; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; using SmartStore.Core.Themes; using SmartStore.Services; using SmartStore.Services.Catalog; @@ -34,6 +35,7 @@ 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; @@ -41,6 +43,7 @@ using SmartStore.Web.Framework.UI; using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Common; +using SmartStore.Services.Seo; namespace SmartStore.Web.Controllers { @@ -48,8 +51,7 @@ public partial class CommonController : PublicControllerBase { private readonly static string[] s_hints = new string[] { "Shopsystem", "Onlineshop Software", "Shopsoftware", "E-Commerce Solution" }; - #region Fields - + private readonly ICommonServices _services; private readonly ITopicService _topicService; private readonly Lazy _languageService; private readonly Lazy _currencyService; @@ -59,6 +61,7 @@ public partial class CommonController : PublicControllerBase private readonly Lazy _genericAttributeService; private readonly Lazy _mobileDeviceHelper; private readonly Lazy _compareProductsService; + private readonly Lazy _urlRecordService; private readonly StoreInformationSettings _storeInfoSettings; private readonly CustomerSettings _customerSettings; @@ -72,18 +75,18 @@ public partial class CommonController : PublicControllerBase private readonly LocalizationSettings _localizationSettings; private readonly Lazy _securitySettings; 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, @@ -93,6 +96,7 @@ public CommonController( Lazy genericAttributeService, Lazy mobileDeviceHelper, Lazy compareProductsService, + Lazy urlRecordService, StoreInformationSettings storeInfoSettings, CustomerSettings customerSettings, TaxSettings taxSettings, @@ -105,14 +109,18 @@ public CommonController( LocalizationSettings localizationSettings, Lazy securitySettings, 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; @@ -121,6 +129,7 @@ public CommonController( this._genericAttributeService = genericAttributeService; this._mobileDeviceHelper = mobileDeviceHelper; this._compareProductsService = compareProductsService; + this._urlRecordService = urlRecordService; this._storeInfoSettings = storeInfoSettings; this._customerSettings = customerSettings; @@ -133,6 +142,7 @@ public CommonController( this._localizationSettings = localizationSettings; this._securitySettings = securitySettings; this._socialSettings = socialSettings; + this._mediaSettings = mediaSettings; this._orderTotalCalculationService = orderTotalCalculationService; this._priceFormatter = priceFormatter; @@ -140,11 +150,11 @@ public CommonController( this._themeSettings = themeSettings; this._pageAssetsBuilder = pageAssetsBuilder; this._pictureService = pictureService; - this._services = services; + this._manufacturerService = manufacturerService; + this._categoryService = categoryService; + this._productService = productService; } - #endregion - #region Utilities [NonAction] @@ -158,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 @@ -169,7 +179,7 @@ protected LanguageSelectorModel PrepareLanguageSelectorModel() var workingLanguage = _services.WorkContext.WorkingLanguage; - var model = new LanguageSelectorModel() + var model = new LanguageSelectorModel { CurrentLanguageId = workingLanguage.Id, AvailableLanguages = availableLanguages, @@ -180,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) { @@ -200,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() @@ -234,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; @@ -248,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() { @@ -367,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) { @@ -387,10 +376,9 @@ public ActionResult SetLanguage(int langid, string returnUrl = "") returnUrl = helper.GetAbsolutePath(); } - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } - //currency [ChildActionOnly] public ActionResult CurrencySelector() { @@ -401,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) @@ -683,6 +662,8 @@ public ActionResult Menu() HasContactUsPage = (_topicService.GetTopicBySystemName("ContactUs", store.Id) != null) }; + model.DisplayLoginLink = _storeInfoSettings.StoreClosed && !model.DisplayAdminLink; + return PartialView(model); } @@ -760,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] @@ -811,13 +787,13 @@ public ActionResult Favicon() [HttpPost] public ActionResult ChangeDevice(bool dontUseMobileVersion) { - _genericAttributeService.Value.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.DontUseMobileVersion, dontUseMobileVersion, _services.StoreContext.CurrentStore.Id); + _genericAttributeService.Value.SaveAttribute( + _services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.DontUseMobileVersion, + dontUseMobileVersion, + _services.StoreContext.CurrentStore.Id); - string returnurl = _services.WebHelper.GetUrlReferrer(); - if (String.IsNullOrEmpty(returnurl)) - returnurl = Url.RouteUrl("HomePage"); - return Redirect(returnurl); + return RedirectToReferrer(); } [ChildActionOnly] @@ -887,7 +863,9 @@ public ActionResult RobotsTextFile() "/Product/EmailAFriend", "/Search", "/Config", - "/Settings" + "/Settings", + "/Login", + "/Register" }; @@ -1046,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 1c9fa88791..a72b4fc872 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs @@ -39,7 +39,7 @@ namespace SmartStore.Web.Controllers { - public partial class CustomerController : PublicControllerBase + public partial class CustomerController : PublicControllerBase { #region Fields @@ -352,37 +352,46 @@ protected void PrepareCustomerInfoModel(CustomerInfoModel model, Customer custom } [NonAction] - protected CustomerOrderListModel PrepareCustomerOrderListModel(Customer customer) + protected CustomerOrderListModel PrepareCustomerOrderListModel(Customer customer, int pageIndex) { if (customer == null) throw new ArgumentNullException("customer"); - var model = new CustomerOrderListModel(); + var storeScope = (_orderSettings.DisplayOrdersOfAllStores ? 0 : _storeContext.CurrentStore.Id); + + var model = new CustomerOrderListModel(); model.NavigationModel = GetCustomerNavigationModel(customer); model.NavigationModel.SelectedTab = CustomerNavigationEnum.Orders; - var orders = _orderService.SearchOrders(_storeContext.CurrentStore.Id, customer.Id, - null, null, null, null, null, null, null, null, 0, int.MaxValue); - foreach (var order in orders) - { - var orderModel = new CustomerOrderListModel.OrderDetailsModel() - { - Id = order.Id, - OrderNumber = order.GetOrderNumber(), - CreatedOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc), - OrderStatus = order.OrderStatus.GetLocalizedEnum(_localizationService, _workContext), - IsReturnRequestAllowed = _orderProcessingService.IsReturnRequestAllowed(order) - }; - var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); - orderModel.OrderTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, _workContext.WorkingLanguage); - model.Orders.Add(orderModel); - } + var orders = _orderService.SearchOrders(storeScope, customer.Id, null, null, null, null, null, null, null, null, pageIndex, _orderSettings.OrderListPageSize); + + var orderModels = orders + .Select(x => + { + var orderModel = new CustomerOrderListModel.OrderDetailsModel + { + Id = x.Id, + OrderNumber = x.GetOrderNumber(), + CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), + OrderStatus = x.OrderStatus.GetLocalizedEnum(_localizationService, _workContext), + IsReturnRequestAllowed = _orderProcessingService.IsReturnRequestAllowed(x) + }; + + var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(x.OrderTotal, x.CurrencyRate); + orderModel.OrderTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, x.CustomerCurrencyCode, false, _workContext.WorkingLanguage); + + return orderModel; + }) + .ToList(); + + model.Orders = new PagedList(orderModels, orders.PageIndex, orders.PageSize, orders.TotalCount); + var recurringPayments = _orderService.SearchRecurringPayments(_storeContext.CurrentStore.Id, customer.Id, 0, null); foreach (var recurringPayment in recurringPayments) { - var recurringPaymentModel = new CustomerOrderListModel.RecurringOrderModel() + var recurringPaymentModel = new CustomerOrderListModel.RecurringOrderModel { Id = recurringPayment.Id, StartDate = _dateTimeHelper.ConvertToUserTime(recurringPayment.StartDateUtc, DateTimeKind.Utc).ToString(), @@ -458,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 { @@ -575,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); @@ -591,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); @@ -632,10 +638,10 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha if (_customerSettings.CustomerNumberMethod == CustomerNumberMethod.AutomaticallySet && String.IsNullOrEmpty(customer.GetAttribute(SystemCustomerAttributeNames.CustomerNumber))) _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.CustomerNumber, customer.Id); - //newsletter + // newsletter if (_customerSettings.NewsletterEnabled) { - //save newsletter value + // save newsletter value var newsletter = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(model.Email, _storeContext.CurrentStore.Id); if (newsletter != null) { @@ -714,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: @@ -727,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)) @@ -824,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); } @@ -912,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); } @@ -975,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)) { @@ -1036,7 +1038,7 @@ public ActionResult Info(CustomerInfoModel model) if (model.CustomerNumber != currentCustomerNumber && customerNumbers.Where(x => x.Value == model.CustomerNumber).Any()) { - this.NotifyError("Common.CustomerNumberAlreadyExists"); + NotifyError("Common.CustomerNumberAlreadyExists"); } else { @@ -1079,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"); } @@ -1284,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); } @@ -1303,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) @@ -1318,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 @@ -1589,30 +1568,34 @@ public ActionResult UploadAvatar(CustomerAvatarModel model, HttpPostedFileBase u try { var customerAvatar = _pictureService.GetPictureById(customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId)); - if ((uploadedFile != null) && (!String.IsNullOrEmpty(uploadedFile.FileName))) - { - int avatarMaxSize = _customerSettings.AvatarMaximumSizeBytes; - if (uploadedFile.ContentLength > avatarMaxSize) - throw new SmartException(string.Format(_localizationService.GetResource("Account.Avatar.MaximumUploadedFileSize"), Prettifier.BytesToString(avatarMaxSize))); - byte[] customerPictureBinary = uploadedFile.InputStream.ToByteArray(); - if (customerAvatar != null) - customerAvatar = _pictureService.UpdatePicture(customerAvatar.Id, customerPictureBinary, uploadedFile.ContentType, null, true); - else - customerAvatar = _pictureService.InsertPicture(customerPictureBinary, uploadedFile.ContentType, null, true, false); - } + if ((uploadedFile != null) && (!String.IsNullOrEmpty(uploadedFile.FileName))) + { + var avatarMaxSize = _customerSettings.AvatarMaximumSizeBytes; + + if (uploadedFile.ContentLength > avatarMaxSize) + throw new SmartException(T("Account.Avatar.MaximumUploadedFileSize", Prettifier.BytesToString(avatarMaxSize))); - int customerAvatarId = 0; - if (customerAvatar != null) - customerAvatarId = customerAvatar.Id; + byte[] customerPictureBinary = uploadedFile.InputStream.ToByteArray(); + + if (customerAvatar != null) + customerAvatar = _pictureService.UpdatePicture(customerAvatar.Id, customerPictureBinary, uploadedFile.ContentType, null, true); + else + customerAvatar = _pictureService.InsertPicture(customerPictureBinary, uploadedFile.ContentType, null, true, false); + } + else if (customerAvatar != null) + { + _pictureService.DeletePicture(customerAvatar); + customerAvatar = null; + } + + var customerAvatarId = (customerAvatar != null ? customerAvatar.Id : 0); _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AvatarPictureId, customerAvatarId); - model.AvatarUrl = _pictureService.GetPictureUrl( - customerAvatarId, - _mediaSettings.AvatarPictureSize, - false); - return View(model); + model.AvatarUrl = _pictureService.GetPictureUrl(customerAvatarId, _mediaSettings.AvatarPictureSize, false); + + return View(model); } catch (Exception exc) { @@ -1620,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); } @@ -1648,6 +1627,7 @@ public ActionResult RemoveAvatar(CustomerAvatarModel model, HttpPostedFileBase u var customerAvatar = _pictureService.GetPictureById(customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId)); if (customerAvatar != null) _pictureService.DeletePicture(customerAvatar); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AvatarPictureId, 0); return RedirectToAction("Avatar"); diff --git a/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs b/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs index 99cc481d18..857b877aa3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs @@ -1,7 +1,11 @@ using System; +using System.Web; using System.Web.Mvc; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Html; using SmartStore.Services.Catalog; using SmartStore.Services.Media; using SmartStore.Services.Orders; @@ -9,7 +13,7 @@ namespace SmartStore.Web.Controllers { - public partial class DownloadController : PublicControllerBase + public partial class DownloadController : PublicControllerBase { private readonly IDownloadService _downloadService; private readonly IProductService _productService; @@ -18,8 +22,12 @@ public partial class DownloadController : PublicControllerBase private readonly CustomerSettings _customerSettings; - public DownloadController(IDownloadService downloadService, IProductService productService, - IOrderService orderService, IWorkContext workContext, CustomerSettings customerSettings) + public DownloadController( + IDownloadService downloadService, + IProductService productService, + IOrderService orderService, + IWorkContext workContext, + CustomerSettings customerSettings) { this._downloadService = downloadService; this._productService = productService; @@ -28,32 +36,40 @@ public DownloadController(IDownloadService downloadService, IProductService prod this._customerSettings = customerSettings; } + private ActionResult GetFileContentResultFor(Download download, Product product) + { + if (download.DownloadBinary == null) + { + return Content(T("Common.Download.NoDataAvailable")); + } + + var id = (product != null ? product.Id : download.Id); + var fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : id.ToString(); + var contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; + + return new FileContentResult(download.DownloadBinary, contentType) + { + FileDownloadName = fileName + download.Extension + }; + } + public ActionResult Sample(int id /* productId */) { var product = _productService.GetProductById(id); if (product == null) return HttpNotFound(); - if (!product.HasSampleDownload) - return Content("Product variant doesn't have a sample download."); + if (!product.HasSampleDownload) + return Content(T("Common.Download.HasNoSample")); var download = _downloadService.GetDownloadById(product.SampleDownloadId.GetValueOrDefault()); if (download == null) - return Content("Sample download is not available any more."); + return Content(T("Common.Download.SampleNotAvailable")); if (download.UseDownloadUrl) - { return new RedirectResult(download.DownloadUrl); - } - else - { - if (download.DownloadBinary == null) - return Content("Download data is not available any more."); - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : product.Id.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; - } + return GetFileContentResultFor(download, product); } public ActionResult GetDownload(Guid id /* orderItemId */, bool agree = false) @@ -68,7 +84,7 @@ public ActionResult GetDownload(Guid id /* orderItemId */, bool agree = false) var order = orderItem.Order; var product = orderItem.Product; if (!_downloadService.IsDownloadAllowed(orderItem)) - return Content("Downloads are not allowed"); + return Content(T("Common.Download.NotAllowed")); if (_customerSettings.DownloadableProductsValidateUser) { @@ -76,46 +92,35 @@ public ActionResult GetDownload(Guid id /* orderItemId */, bool agree = false) return new HttpUnauthorizedResult(); if (order.CustomerId != _workContext.CurrentCustomer.Id) - return Content("This is not your order"); + return Content(T("Account.CustomerOrders.NotYourOrder")); } var download = _downloadService.GetDownloadById(product.DownloadId); if (download == null) - return Content("Download is not available any more."); - - if (product.HasUserAgreement) - { - if (!agree) - return RedirectToAction("UserAgreement", "Customer", new { id = id }); - } + return Content(T("Common.Download.NoDataAvailable")); + if (product.HasUserAgreement && !agree) + return RedirectToAction("UserAgreement", "Customer", new { id = id }); if (!product.UnlimitedDownloads && orderItem.DownloadCount >= product.MaxNumberOfDownloads) - return Content(string.Format("You have reached maximum number of downloads {0}", product.MaxNumberOfDownloads)); + return Content(T("Common.Download.MaxNumberReached", product.MaxNumberOfDownloads)); - if (download.UseDownloadUrl) { - //increase download orderItem.DownloadCount++; _orderService.UpdateOrder(order); - //return result return new RedirectResult(download.DownloadUrl); } else { if (download.DownloadBinary == null) - return Content("Download data is not available any more."); + return Content(T("Common.Download.NoDataAvailable")); - //increase download orderItem.DownloadCount++; _orderService.UpdateOrder(order); - //return result - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : product.Id.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; + return GetFileContentResultFor(download, product); } } @@ -130,8 +135,9 @@ public ActionResult GetLicense(Guid id /* orderItemId */) var order = orderItem.Order; var product = orderItem.Product; + if (!_downloadService.IsLicenseDownloadAllowed(orderItem)) - return Content("Downloads are not allowed"); + return Content(T("Common.Download.NotAllowed")); if (_customerSettings.DownloadableProductsValidateUser) { @@ -139,52 +145,49 @@ public ActionResult GetLicense(Guid id /* orderItemId */) return new HttpUnauthorizedResult(); if (order.CustomerId != _workContext.CurrentCustomer.Id) - return Content("This is not your order"); + return Content(T("Account.CustomerOrders.NotYourOrder")); } var download = _downloadService.GetDownloadById(orderItem.LicenseDownloadId.HasValue ? orderItem.LicenseDownloadId.Value : 0); if (download == null) - return Content("Download is not available any more."); + return Content(T("Common.Download.NotAvailable")); if (download.UseDownloadUrl) - { - //return result return new RedirectResult(download.DownloadUrl); - } - else - { - if (download.DownloadBinary == null) - return Content("Download data is not available any more."); - - //return result - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : product.Id.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; - } - } + + return GetFileContentResultFor(download, product); + } public ActionResult GetFileUpload(Guid downloadId) { var download = _downloadService.GetDownloadByGuid(downloadId); if (download == null) - return Content("Download is not available any more."); + return Content(T("Common.Download.NotAvailable")); if (download.UseDownloadUrl) - { - //return result return new RedirectResult(download.DownloadUrl); - } - else - { - if (download.DownloadBinary == null) - return Content("Download data is not available any more."); - //return result - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : downloadId.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; - } - } + return GetFileContentResultFor(download, null); + } + + public ActionResult GetUserAgreement(int productId, bool? asPlainText) + { + var product = _productService.GetProductById(productId); + if (product == null) + return Content(T("Products.NotFound", productId)); + + if (!product.IsDownload || !product.HasUserAgreement || product.UserAgreementText.IsEmpty()) + return Content(T("DownloadableProducts.HasNoUserAgreement")); + + if (asPlainText ?? false) + { + var agreement = HtmlUtils.ConvertHtmlToPlainText(product.UserAgreementText); + agreement = HtmlUtils.StripTags(HttpUtility.HtmlDecode(agreement)); + + return Content(agreement); + } - } + return Content(product.UserAgreementText); + } + } } diff --git a/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs b/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs index f95f0ef4d2..3fc35cd8db 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs @@ -31,10 +31,10 @@ public ExternalAuthenticationController(IOpenAuthenticationService openAuthentic #region Methods - public RedirectResult RemoveParameterAssociation(string returnUrl) + public ActionResult RemoveParameterAssociation(string returnUrl) { ExternalAuthorizerHelper.RemoveParameters(); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [ChildActionOnly] diff --git a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs index 6b65cfaf2d..58e6eb4ace 100644 --- a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; @@ -41,6 +42,7 @@ public partial class HomeController : PublicControllerBase private readonly Lazy _sitemapGenerator; private readonly Lazy _captchaSettings; private readonly Lazy _commonSettings; + private readonly Lazy _customerSettings; #endregion @@ -56,7 +58,8 @@ public HomeController( Lazy emailAccountService, Lazy sitemapGenerator, Lazy captchaSettings, - Lazy commonSettings) + Lazy commonSettings, + Lazy customerSettings) { this._services = services; this._categoryService = categoryService; @@ -68,6 +71,7 @@ public HomeController( this._sitemapGenerator = sitemapGenerator; this._captchaSettings = captchaSettings; this._commonSettings = commonSettings; + this._customerSettings = customerSettings; } #endregion @@ -86,7 +90,7 @@ public ActionResult ContentSlider() var settings = _services.Settings.LoadSetting(); settings.BackgroundPictureUrl = pictureService.GetPictureUrl(settings.BackgroundPictureId, 0, false); - + var slides = settings.Slides .Where(s => s.LanguageCulture == _services.WorkContext.WorkingLanguage.LanguageCulture && @@ -119,7 +123,9 @@ public ActionResult ContactUs() { Email = _services.WorkContext.CurrentCustomer.Email, FullName = _services.WorkContext.CurrentCustomer.GetFullName(), - DisplayCaptcha = _captchaSettings.Value.Enabled && _captchaSettings.Value.ShowOnContactUsPage + DisplayCaptcha = _captchaSettings.Value.Enabled && _captchaSettings.Value.ShowOnContactUsPage, + DisplayPrivacyAgreement = _customerSettings.Value.DisplayPrivacyAgreementOnContactUs + }; return View(model); diff --git a/src/Presentation/SmartStore.Web/Controllers/InstallController.cs b/src/Presentation/SmartStore.Web/Controllers/InstallController.cs index e0bef97946..7c5a919eb0 100644 --- a/src/Presentation/SmartStore.Web/Controllers/InstallController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/InstallController.cs @@ -1,33 +1,28 @@ -using Autofac; -using System; +using System; using System.Collections.Generic; +using System.Data.Entity; using System.Data.SqlClient; using System.Linq; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; -using System.Web; -using System.Web.SessionState; -using System.Web.Caching; using System.Web.Hosting; using System.Web.Mvc; -using System.ComponentModel.Composition; +using System.Web.SessionState; +using Autofac; using SmartStore.Core; -using SmartStore.Core.Caching; +using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; +using SmartStore.Data; +using SmartStore.Data.Setup; using SmartStore.Services.Security; +using SmartStore.Utilities; using SmartStore.Web.Framework.Security; using SmartStore.Web.Infrastructure.Installation; using SmartStore.Web.Models.Install; -using SmartStore.Core.Async; -using System.Data.Entity; -using SmartStore.Data; -using SmartStore.Data.Setup; -using System.Configuration; -using SmartStore.Utilities; namespace SmartStore.Web.Controllers { @@ -182,8 +177,8 @@ public ActionResult Index() if (DataSettings.DatabaseIsInstalled()) return RedirectToRoute("HomePage"); - //set page timeout to 5 minutes - this.Server.ScriptTimeout = 300; + // set page timeout to 10 minutes + this.Server.ScriptTimeout = 600; var model = new InstallModel { @@ -480,7 +475,7 @@ protected virtual InstallationResult InstallCore(ILifetimeScope scope, InstallMo { return UpdateResult(x => { - x.Errors.Add(string.Format("The install language '{0}' is not registered", model.PrimaryLanguage)); + x.Errors.Add(_locService.GetResource("Install.LanguageNotRegistered").FormatInvariant(model.PrimaryLanguage)); x.Completed = true; x.Success = false; x.RedirectUrl = null; diff --git a/src/Presentation/SmartStore.Web/Controllers/NewsController.cs b/src/Presentation/SmartStore.Web/Controllers/NewsController.cs index fad060e373..b285065191 100644 --- a/src/Presentation/SmartStore.Web/Controllers/NewsController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/NewsController.cs @@ -319,13 +319,11 @@ public ActionResult NewsCommentAdd(int newsItemId, NewsItemModel model, bool cap //activity log _customerActivityService.InsertActivity("PublicStore.AddNewsComment", _localizationService.GetResource("ActivityLog.PublicStore.AddNewsComment")); - //The text boxes should be cleared after a comment has been posted - //That' why we reload the page - TempData["sm.news.addcomment.result"] = _localizationService.GetResource("News.Comments.SuccessfullyAdded"); + NotifySuccess(T("News.Comments.SuccessfullyAdded")); + return RedirectToRoute("NewsItem", new { SeName = newsItem.GetSeName(newsItem.LanguageId, ensureTwoPublishedLanguages: false) }); } - //If we got this far, something failed, redisplay form PrepareNewsItemModel(model, newsItem, true); return View(model); diff --git a/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs b/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs index ad148aeb33..d41e2a40b0 100644 --- a/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs @@ -3,16 +3,14 @@ using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Messages; -using SmartStore.Services.Localization; using SmartStore.Services.Messages; -using SmartStore.Web.Models.Newsletter; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Models.Newsletter; namespace SmartStore.Web.Controllers { - public partial class NewsletterController : PublicControllerBase + public partial class NewsletterController : PublicControllerBase { - private readonly ILocalizationService _localizationService; private readonly IWorkContext _workContext; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; private readonly IWorkflowMessageService _workflowMessageService; @@ -20,12 +18,13 @@ public partial class NewsletterController : PublicControllerBase private readonly CustomerSettings _customerSettings; - public NewsletterController(ILocalizationService localizationService, - IWorkContext workContext, INewsLetterSubscriptionService newsLetterSubscriptionService, - IWorkflowMessageService workflowMessageService, CustomerSettings customerSettings, + public NewsletterController( + IWorkContext workContext, + INewsLetterSubscriptionService newsLetterSubscriptionService, + IWorkflowMessageService workflowMessageService, + CustomerSettings customerSettings, IStoreContext storeContext) { - this._localizationService = localizationService; this._workContext = workContext; this._newsLetterSubscriptionService = newsLetterSubscriptionService; this._workflowMessageService = workflowMessageService; @@ -47,56 +46,59 @@ public ActionResult NewsletterBox() public ActionResult Subscribe(bool subscribe, string email) { string result; - bool success = false; + var success = false; if (!email.IsEmail()) - result = _localizationService.GetResource("Newsletter.Email.Wrong"); - else - { - //subscribe/unsubscribe - email = email.Trim(); - - var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(email, _storeContext.CurrentStore.Id); - if (subscription != null) - { - if (subscribe) - { - if (!subscription.Active) - { - _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); - } - result = _localizationService.GetResource("Newsletter.SubscribeEmailSent"); - } - else - { - if (subscription.Active) - { - _workflowMessageService.SendNewsLetterSubscriptionDeactivationMessage(subscription, _workContext.WorkingLanguage.Id); - } - result = _localizationService.GetResource("Newsletter.UnsubscribeEmailSent"); - } - } - else if (subscribe) - { - subscription = new NewsLetterSubscription() - { - NewsLetterSubscriptionGuid = Guid.NewGuid(), - Email = email, - Active = false, - CreatedOnUtc = DateTime.UtcNow, + { + result = T("Newsletter.Email.Wrong"); + } + else + { + //subscribe/unsubscribe + email = email.Trim(); + + var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(email, _storeContext.CurrentStore.Id); + if (subscription != null) + { + if (subscribe) + { + if (!subscription.Active) + { + _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); + } + result = T("Newsletter.SubscribeEmailSent"); + } + else + { + if (subscription.Active) + { + _workflowMessageService.SendNewsLetterSubscriptionDeactivationMessage(subscription, _workContext.WorkingLanguage.Id); + } + result = T("Newsletter.UnsubscribeEmailSent"); + } + } + else if (subscribe) + { + subscription = new NewsLetterSubscription + { + NewsLetterSubscriptionGuid = Guid.NewGuid(), + Email = email, + Active = false, + CreatedOnUtc = DateTime.UtcNow, StoreId = _storeContext.CurrentStore.Id - }; - _newsLetterSubscriptionService.InsertNewsLetterSubscription(subscription); - _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); - - result = _localizationService.GetResource("Newsletter.SubscribeEmailSent"); - } - else - { - result = _localizationService.GetResource("Newsletter.UnsubscribeEmailSent"); - } - success = true; - } + }; + + _newsLetterSubscriptionService.InsertNewsLetterSubscription(subscription); + _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); + + result = T("Newsletter.SubscribeEmailSent"); + } + else + { + result = T("Newsletter.UnsubscribeEmailSent"); + } + success = true; + } return Json(new { @@ -108,25 +110,26 @@ public ActionResult Subscribe(bool subscribe, string email) public ActionResult SubscriptionActivation(Guid token, bool active) { var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByGuid(token); - if (subscription == null) + if (subscription == null) + { return HttpNotFound(); + } var model = new SubscriptionActivationModel(); - if (active) - { - subscription.Active = active; - _newsLetterSubscriptionService.UpdateNewsLetterSubscription(subscription); - } - else - _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); - - if (active) - model.Result = _localizationService.GetResource("Newsletter.ResultActivated"); - else - model.Result = _localizationService.GetResource("Newsletter.ResultDeactivated"); - - return View(model); + if (active) + { + subscription.Active = active; + _newsLetterSubscriptionService.UpdateNewsLetterSubscription(subscription); + } + else + { + _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); + } + + model.Result = T(active ? "Newsletter.ResultActivated" : "Newsletter.ResultDeactivated"); + + return View(model); } } } diff --git a/src/Presentation/SmartStore.Web/Controllers/OrderController.cs b/src/Presentation/SmartStore.Web/Controllers/OrderController.cs index 8254820961..448a1983d3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/OrderController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/OrderController.cs @@ -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); @@ -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,7 +355,7 @@ protected ShipmentDetailsModel PrepareShipmentDetailsModel(Shipment shipment) var order = shipment.Order; if (order == null) - throw new Exception("order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); var store = _storeService.GetStoreById(order.StoreId) ?? _services.StoreContext.CurrentStore; var catalogSettings = _services.Settings.LoadSetting(store.Id); @@ -663,21 +680,29 @@ public ActionResult RePostPayment(int id) if (IsUnauthorizedOrder(order)) return new HttpUnauthorizedResult(); - if (_paymentService.CanRePostProcessPayment(order)) + try { - var postProcessPaymentRequest = new PostProcessPaymentRequest + if (_paymentService.CanRePostProcessPayment(order)) { - Order = order, - IsRePostProcessPayment = true - }; + var postProcessPaymentRequest = new PostProcessPaymentRequest + { + Order = order, + IsRePostProcessPayment = true + }; - _paymentService.PostProcessPayment(postProcessPaymentRequest); + _paymentService.PostProcessPayment(postProcessPaymentRequest); - if (postProcessPaymentRequest.RedirectUrl.HasValue()) - { - return Redirect(postProcessPaymentRequest.RedirectUrl); + if (postProcessPaymentRequest.RedirectUrl.HasValue()) + { + return Redirect(postProcessPaymentRequest.RedirectUrl); + } } } + catch (Exception exception) + { + NotifyError(exception); + } + return RedirectToAction("Details", "Order", new { id = order.Id }); } diff --git a/src/Presentation/SmartStore.Web/Controllers/PollController.cs b/src/Presentation/SmartStore.Web/Controllers/PollController.cs index 6b51058139..1919207834 100644 --- a/src/Presentation/SmartStore.Web/Controllers/PollController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/PollController.cs @@ -6,7 +6,6 @@ using SmartStore.Core.Caching; using SmartStore.Core.Domain.Polls; using SmartStore.Services.Customers; -using SmartStore.Services.Localization; using SmartStore.Services.Polls; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Infrastructure.Cache; @@ -14,11 +13,10 @@ namespace SmartStore.Web.Controllers { - public partial class PollController : PublicControllerBase + public partial class PollController : PublicControllerBase { #region Fields - private readonly ILocalizationService _localizationService; private readonly IWorkContext _workContext; private readonly IPollService _pollService; private readonly IWebHelper _webHelper; @@ -29,12 +27,13 @@ public partial class PollController : PublicControllerBase #region Constructors - public PollController(ILocalizationService localizationService, - IWorkContext workContext, IPollService pollService, - IWebHelper webHelper, ICacheManager cacheManager, + public PollController( + IWorkContext workContext, + IPollService pollService, + IWebHelper webHelper, + ICacheManager cacheManager, IStoreContext storeContext) { - this._localizationService = localizationService; this._workContext = workContext; this._pollService = pollService; this._webHelper = webHelper; @@ -49,18 +48,23 @@ public PollController(ILocalizationService localizationService, [NonAction] protected PollModel PreparePollModel(Poll poll, bool setAlreadyVotedProperty) { - var model = new PollModel() + var model = new PollModel { Id = poll.Id, AlreadyVoted = setAlreadyVotedProperty && _pollService.AlreadyVoted(poll.Id, _workContext.CurrentCustomer.Id), Name = poll.Name }; + var answers = poll.PollAnswers.OrderBy(x => x.DisplayOrder); - foreach (var answer in answers) - model.TotalVotes += answer.NumberOfVotes; + + foreach (var answer in answers) + { + model.TotalVotes += answer.NumberOfVotes; + } + foreach (var pa in answers) { - model.Answers.Add(new PollAnswerModel() + model.Answers.Add(new PollAnswerModel { Id = pa.Id, Name = pa.Name, @@ -88,10 +92,11 @@ public ActionResult PollBlock(string systemKeyword) var poll = _pollService.GetPollBySystemKeyword(systemKeyword, _workContext.WorkingLanguage.Id); if (poll == null) - return new PollModel() { Id = 0 }; //we do not cache nulls. that's why let's return an empty record (ID = 0) + return new PollModel { Id = 0 }; //we do not cache nulls. that's why let's return an empty record (ID = 0) return PreparePollModel(poll, false); }); + if (cachedModel == null || cachedModel.Id == 0) return Content(""); @@ -108,30 +113,29 @@ public ActionResult PollBlock(string systemKeyword) public ActionResult Vote(int pollAnswerId) { var pollAnswer = _pollService.GetPollAnswerById(pollAnswerId); - if (pollAnswer == null) - return Json(new - { - error = "No poll answer found with the specified id", - }); + + if (pollAnswer == null) + { + return Json(new { error = T("Polls.AnswerNotFound", pollAnswerId).Text }); + } var poll = pollAnswer.Poll; - if (!poll.Published) - return Json(new - { - error = "Poll is not available", - }); - if (_workContext.CurrentCustomer.IsGuest() && !poll.AllowGuestsToVote) - return Json(new - { - error = _localizationService.GetResource("Polls.OnlyRegisteredUsersVote"), - }); + if (!poll.Published) + { + return Json(new { error = T("Polls.NotAvailable").Text }); + } + + if (_workContext.CurrentCustomer.IsGuest() && !poll.AllowGuestsToVote) + { + return Json(new { error = T("Polls.OnlyRegisteredUsersVote").Text }); + } bool alreadyVoted = _pollService.AlreadyVoted(poll.Id, _workContext.CurrentCustomer.Id); if (!alreadyVoted) { //vote - pollAnswer.PollVotingRecords.Add(new PollVotingRecord() + pollAnswer.PollVotingRecords.Add(new PollVotingRecord { PollAnswerId = pollAnswer.Id, CustomerId = _workContext.CurrentCustomer.Id, @@ -140,8 +144,10 @@ public ActionResult Vote(int pollAnswerId) CreatedOnUtc = DateTime.UtcNow, UpdatedOnUtc = DateTime.UtcNow, }); + //update totals pollAnswer.NumberOfVotes = pollAnswer.PollVotingRecords.Count; + _pollService.UpdatePoll(poll); } @@ -161,12 +167,14 @@ public ActionResult HomePagePolls() .Select(x => PreparePollModel(x, false)) .ToList(); }); + //"AlreadyVoted" property of "PollModel" object depends on the current customer. Let's update it. //But first we need to clone the cached model (the updated one should not be cached) var model = new List(); + foreach (var p in cachedModel) { - var pollModel = (PollModel) p.Clone(); + var pollModel = (PollModel)p.Clone(); pollModel.AlreadyVoted = _pollService.AlreadyVoted(pollModel.Id, _workContext.CurrentCustomer.Id); model.Add(pollModel); } @@ -178,6 +186,5 @@ public ActionResult HomePagePolls() } #endregion - } } diff --git a/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs b/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs index 81ed0af2d8..8171bf6d4f 100644 --- a/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs @@ -4,20 +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; -using SmartStore.Web.Framework.Filters; namespace SmartStore.Web.Controllers { - [RequireHttpsByConfigAttribute(SslRequirement.Yes)] + [RequireHttpsByConfigAttribute(SslRequirement.Yes)] public partial class PrivateMessagesController : PublicControllerBase { #region Fields @@ -25,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; @@ -37,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; @@ -131,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, @@ -175,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, @@ -227,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) @@ -256,7 +255,7 @@ public ActionResult MarkUnread(FormCollection formCollection) } } } - return RedirectToRoute("PrivateMessages"); + return RedirectToAction("Index"); } //updates sent items (deletes PrivateMessages) @@ -306,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; @@ -319,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) { @@ -329,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] @@ -358,8 +357,8 @@ public ActionResult Send(SendPrivateMessageModel model) } else { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } } else { @@ -368,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(); @@ -408,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) { @@ -438,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) { @@ -449,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, @@ -497,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 6f52543374..79497418a9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ProductController.cs @@ -8,7 +8,6 @@ using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Localization; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -31,7 +30,7 @@ namespace SmartStore.Web.Controllers { - public partial class ProductController : PublicControllerBase + public partial class ProductController : PublicControllerBase { #region Fields @@ -168,14 +167,16 @@ public ActionResult ProductDetails(int productId, string attributes) } } - //prepare the model var selectedAttributes = new NameValueCollection(); - selectedAttributes.ConvertAttributeQueryData( + // get selected attributes from query string + selectedAttributes.GetSelectedAttributes( + Request.QueryString, _productAttributeParser.DeserializeQueryData(attributes), product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing ? 0 : product.Id); - var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes); + // prepare the view model + var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes, queryData: Request.QueryString); //save as recently viewed _recentlyViewedProductsService.AddProductToRecentlyViewedList(product.Id); @@ -320,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) @@ -329,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) { @@ -355,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() { @@ -372,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); @@ -403,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 @@ -522,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; @@ -551,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")); @@ -762,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, () => @@ -893,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) { @@ -1032,7 +1039,7 @@ public ActionResult AskQuestionSend(ProductAskQuestionModel model, bool captchaV } else { - ModelState.AddModelError("", "Fehler beim Versenden der Email. Bitte versuchen Sie es später erneut."); + ModelState.AddModelError("", T("Common.Error.SendMail")); } } diff --git a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs index 3cc16d6877..e4ff2cb6a7 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs @@ -93,6 +93,8 @@ public partial class ShoppingCartController : PublicControllerBase private readonly PluginMediator _pluginMediator; private readonly IQuantityUnitService _quantityUnitService; private readonly Lazy _topicService; + private readonly IMeasureService _measureService; + private readonly MeasureSettings _measureSettings; #endregion @@ -124,7 +126,8 @@ public ShoppingCartController(IProductService productService, CaptchaSettings captchaSettings, AddressSettings addressSettings, HttpContextBase httpContext, PluginMediator pluginMediator, IQuantityUnitService quantityUnitService, - Lazy topicService) + Lazy topicService, + IMeasureService measureService, MeasureSettings measureSettings) { this._productService = productService; this._workContext = workContext; @@ -171,6 +174,8 @@ public ShoppingCartController(IProductService productService, this._pluginMediator = pluginMediator; this._quantityUnitService = quantityUnitService; this._topicService = topicService; + this._measureService = measureService; + this._measureSettings = measureSettings; } #endregion @@ -178,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"); @@ -209,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), }; @@ -240,7 +245,10 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org ShortDesc = product.GetLocalized(x => x.ShortDescription), ProductType = product.ProductType, BasePrice = product.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, _workContext.WorkingCurrency), - Weight = product.Weight + Weight = product.Weight, + IsDownload = product.IsDownload, + HasUserAgreement = product.HasUserAgreement, + IsEsd = product.IsEsd }; model.ProductUrl = GetProductUrlWithAttributes(sci, model.ProductSeName); @@ -251,7 +259,7 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org model.BundlePerItemShoppingCart = item.BundleItem.BundleProduct.BundlePerItemShoppingCart; model.AttributeInfo = _productAttributeFormatter.FormatAttributes(product, item.AttributesXml, _workContext.CurrentCustomer, - renderPrices: false, renderGiftCardAttributes: true, allowHyperlinks: false); + renderPrices: false, renderGiftCardAttributes: true, allowHyperlinks: true); string bundleItemName = item.BundleItem.GetLocalized(x => x.Name); if (bundleItemName.HasValue()) @@ -389,17 +397,17 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org if (item.BundleItem != null) { if (_shoppingCartSettings.ShowProductBundleImagesOnShoppingCart) - model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbBundleItemPictureSize, true, model.ProductName, item.AttributesXml); + model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbBundleItemPictureSize, model.ProductName, item.AttributesXml); } else { if (_shoppingCartSettings.ShowProductImagesOnShoppingCart) - model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbPictureSize, true, model.ProductName, item.AttributesXml); + model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbPictureSize, model.ProductName, item.AttributesXml); } //item warnings var itemWarnings = _shoppingCartService.GetShoppingCartItemWarnings(_workContext.CurrentCustomer, item.ShoppingCartType, product, item.StoreId, - item.AttributesXml, item.CustomerEnteredPrice, item.Quantity, false, childItems: sci.ChildItems); + item.AttributesXml, item.CustomerEnteredPrice, item.Quantity, false, bundleItem: item.BundleItem, childItems: sci.ChildItems); foreach (var warning in itemWarnings) { @@ -537,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 @@ -635,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) { @@ -653,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 @@ -954,7 +985,7 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //1. "terms of services" are enabled (OBSOLETE now) //2. we have at least one checkout attribute //3. min order sub-total is OK - var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(); + var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(_storeContext.CurrentStore.Id); if (!cart.RequiresShipping()) { //remove attributes which require shippable products @@ -965,7 +996,6 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //products. sort descending (recently added products) foreach (var sci in cart - .OrderByDescending(x => x.Item.Id) .Take(_shoppingCartSettings.MiniShoppingCartProductNumber) .ToList()) { @@ -1031,7 +1061,7 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //picture if (_shoppingCartSettings.ShowProductImagesInMiniShoppingCart) { - cartItemModel.Picture = PrepareCartItemPictureModel(product, _mediaSettings.MiniCartThumbPictureSize, true, cartItemModel.ProductName, item.AttributesXml); + cartItemModel.Picture = PrepareCartItemPictureModel(product, _mediaSettings.MiniCartThumbPictureSize, cartItemModel.ProductName, item.AttributesXml); } model.Items.Add(cartItemModel); @@ -1045,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: @@ -1063,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]; @@ -1077,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: { @@ -1092,29 +1125,31 @@ protected void ParseAndSaveCheckoutAttributes(List ca if (!String.IsNullOrEmpty(txtAttribute)) { string enteredText = txtAttribute.Trim(); - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, enteredText); + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, enteredText); } } break; + case AttributeControlType.Datepicker: { var date = form[controlId + "_day"]; var month = form[controlId + "_month"]; var year = form[controlId + "_year"]; DateTime? selectedDate = null; + try { selectedDate = new DateTime(Int32.Parse(year), Int32.Parse(month), Int32.Parse(date)); } catch { } + if (selectedDate.HasValue) { - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, selectedDate.Value.ToString("D")); + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, selectedDate.Value.ToString("D")); } } break; + case AttributeControlType.FileUpload: { var postedFile = this.Request.Files[controlId].ToPostedFileResult(); @@ -1147,6 +1182,7 @@ protected void ParseAndSaveCheckoutAttributes(List ca } } break; + default: break; } @@ -1195,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) }); } @@ -1443,7 +1479,7 @@ public ActionResult UploadFileProductAttribute(int productId, int productAttribu var postedFile = Request.ToPostedFileResult(); if (postedFile == null) { - throw new ArgumentException("No file uploaded"); + throw new ArgumentException(T("Common.NoFileUploaded")); } int fileMaxSize = _catalogSettings.FileUploadMaximumSizeBytes; @@ -1712,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)] @@ -2603,7 +2632,7 @@ public ActionResult FlyoutWishlist() { Customer customer = _workContext.CurrentCustomer; - var cart = customer.GetCartItems(ShoppingCartType.Wishlist, _storeContext.CurrentStore.Id, true); + var cart = customer.GetCartItems(ShoppingCartType.Wishlist, _storeContext.CurrentStore.Id); var model = new WishlistModel(); PrepareWishlistModel(model, cart, true); diff --git a/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs b/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs index 9b7b6763c2..68c94b3fe7 100644 --- a/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs @@ -44,14 +44,14 @@ public ActionResult Sweep() var prevTaskStart = DateTime.UtcNow; var count = 0; - foreach (var task in pendingTasks) + for (var i = 0; i < pendingTasks.Count; i++) { - var elapsedSincePrevTask = DateTime.UtcNow - prevTaskStart; - if (elapsedSincePrevTask >= TimeSpan.FromMinutes(_taskScheduler.SweepIntervalMinutes)) + var task = pendingTasks[i]; + + if (i > 0) { - // the previous Task execution in this loop took longer - // than the scheduler's sweep interval. Most likely a subsequent - // Sweep call executed it already in another HTTP request. + // Maybe a subsequent Sweep call or another machine in a webfarm executed + // successive tasks already. // To be able to determine this, we need to reload the entity from the database. // The TaskExecutor will exit when the task should be in running state then. _services.DbContext.ReloadEntity(task); @@ -65,7 +65,7 @@ public ActionResult Sweep() } } - return Content("{0} tasks executed".FormatInvariant(count)); + return Content("{0} of {1} pending tasks executed".FormatInvariant(count, pendingTasks.Count)); } [HttpPost] @@ -83,5 +83,10 @@ public ActionResult Execute(int id /* taskId */) 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/TopicController.cs b/src/Presentation/SmartStore.Web/Controllers/TopicController.cs index 7e861fac2e..7c4be750a3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/TopicController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/TopicController.cs @@ -66,7 +66,8 @@ protected TopicModel PrepareTopicModel(string systemName) MetaDescription = topic.GetLocalized(x => x.MetaDescription), MetaTitle = topic.GetLocalized(x => x.MetaTitle), TitleTag = titleTag, - }; + RenderAsWidget = topic.RenderAsWidget + }; return model; } @@ -79,8 +80,9 @@ public ActionResult TopicDetails(string systemName) var cacheKey = string.Format(ModelCacheEventConsumer.TOPIC_MODEL_KEY, systemName, _workContext.WorkingLanguage.Id, _storeContext.CurrentStore.Id); var cacheModel = _cacheManager.Get(cacheKey, () => PrepareTopicModel(systemName)); - if (cacheModel == null) + if (cacheModel == null || (cacheModel.RenderAsWidget && !cacheModel.IncludeInSitemap)) return HttpNotFound(); + return View("TopicDetails", cacheModel); } diff --git a/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs b/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs index 67f1ddf752..74461e92ef 100644 --- a/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs @@ -14,5 +14,11 @@ public ActionResult WidgetsByZone(WidgetZoneModel zoneModel) return PartialView(zoneModel); } + [ChildActionOnly] + public ActionResult TabWidgets(object model, string viewDataKey) + { + var widgets = this.ControllerContext.ParentActionViewContext.ViewData[viewDataKey]; + return PartialView(widgets); + } } } diff --git a/src/Presentation/SmartStore.Web/Global.asax.cs b/src/Presentation/SmartStore.Web/Global.asax.cs index 6353e1e31a..4297e54186 100644 --- a/src/Presentation/SmartStore.Web/Global.asax.cs +++ b/src/Presentation/SmartStore.Web/Global.asax.cs @@ -1,18 +1,20 @@ -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.Bundling; -using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Modelling; @@ -21,15 +23,12 @@ 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(); @@ -162,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/Installation/DeDESeedData.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs index 2d75d6749c..3896e5c829 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs @@ -258,7 +258,7 @@ protected override void Alter(IList entities) .Alter("ReturnRequestStatusChanged.CustomerNotification", x => { x.Subject = "%Store.Name%. Rücksendung - Status-Änderung"; - x.Body = templateHeader + "

    %Store.Name% 

    Hallo %Customer.FullName%,

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

    Mit freundlichen Grüßen,

    Ihr %Store.Name% - Team

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

    %Store.Name% 

    Hallo %Customer.FullName%,

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

    Mit freundlichen Grüßen,

    Ihr %Store.Name% - Team

    " + templateFooter; }) .Alter("Service.EmailAFriend", x => { diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs index e9352a710e..04ba8b6e49 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs @@ -53,7 +53,7 @@ public virtual InstallationLanguage GetCurrentLanguage() if (cookie != null && !String.IsNullOrEmpty(cookie.Value)) cookieLanguageCode = cookie.Value; - // ensure it's available (it could be delete since the previous installation) + // ensure it's available (it could be deleted since the previous installation) var availableLanguages = GetAvailableLanguages(); var language = availableLanguages @@ -79,7 +79,7 @@ public virtual InstallationLanguage GetCurrentLanguage() private bool MatchLanguageByCurrentCulture(InstallationLanguage language) { - var curCulture = CultureInfo.GetCultureInfoByIetfLanguageTag("tr-TR"); // Thread.CurrentThread.CurrentCulture; + var curCulture = Thread.CurrentThread.CurrentUICulture; if (language.Code.IsCaseInsensitiveEqual(curCulture.IetfLanguageTag)) return true; diff --git a/src/Presentation/SmartStore.Web/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/Checkout/CheckoutConfirmModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs index 37236d4d19..2239a2e298 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs @@ -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/Common/AddressModel.cs b/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs index 2f81a1d44c..a393eb0873 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; @@ -7,7 +8,7 @@ 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/ContactUsModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs index 5b7f9464c8..db9ab838bc 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs @@ -1,4 +1,5 @@ -using System.Web.Mvc; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -6,12 +7,18 @@ 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/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/MenuModel.cs b/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs index d8bd94a134..c906dc18ea 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs @@ -13,7 +13,8 @@ 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; } diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs index b75ed065cc..80e9ddafef 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; @@ -7,7 +8,7 @@ namespace SmartStore.Web.Models.Customer { - [Validator(typeof(CustomerInfoValidator))] + [Validator(typeof(CustomerInfoValidator))] public partial class CustomerInfoModel : ModelBase { public CustomerInfoModel() @@ -20,7 +21,8 @@ public CustomerInfoModel() [SmartResourceDisplayName("Account.Fields.Email")] [AllowHtml] - public string Email { get; set; } + [DataType(DataType.EmailAddress)] + public string Email { get; set; } [SmartResourceDisplayName("Account.Fields.CustomerNumber")] [AllowHtml] @@ -100,13 +102,15 @@ public CustomerInfoModel() public bool PhoneRequired { get; set; } [SmartResourceDisplayName("Account.Fields.Phone")] [AllowHtml] - public string Phone { get; set; } + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } public bool FaxEnabled { get; set; } public bool FaxRequired { get; set; } [SmartResourceDisplayName("Account.Fields.Fax")] [AllowHtml] - public string Fax { get; set; } + [DataType(DataType.PhoneNumber)] + public string Fax { get; set; } public bool NewsletterEnabled { get; set; } [SmartResourceDisplayName("Account.Fields.Newsletter")] diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerOrderListModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerOrderListModel.cs index d81be762d6..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.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/PaswordRecoveryModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs index 6bb39f28be..b8581117d4 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs @@ -1,4 +1,5 @@ -using System.Web.Mvc; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -6,12 +7,13 @@ 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 daedf7b90b..4d1a445e2d 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/RegisterModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/RegisterModel.cs @@ -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")] diff --git a/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs b/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs index a5738aa264..c4b4773f3a 100644 --- a/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs @@ -126,7 +126,8 @@ public partial class GiftCard : ModelBase { public string CouponCode { get; set; } public string Amount { get; set; } - } + public string Remaining { get; set; } + } public partial class OrderNote : ModelBase { diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs index 7567b03e6c..e8c08f9810 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Orders; using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; using SmartStore.Web.Models.Media; @@ -50,10 +51,20 @@ public ShoppingCartModel() public bool DisplayCommentBox { get; set; } public string CustomerComment { get; set; } + public string MeasureUnitName { get; set; } + + public CheckoutNewsLetterSubscription NewsLetterSubscription { get; set; } + public bool? SubscribeToNewsLetter { get; set; } + + public CheckoutThirdPartyEmailHandOver ThirdPartyEmailHandOver { get; set; } + public string ThirdPartyEmailHandOverLabel { get; set; } + public bool? AcceptThirdPartyEmailHandOver { get; set; } + + public bool DisplayEsdRevocationWaiverBox { get; set; } #region Nested Classes - public partial class ShoppingCartItemModel : EntityModelBase + public partial class ShoppingCartItemModel : EntityModelBase { public ShoppingCartItemModel() { @@ -108,6 +119,11 @@ public ShoppingCartItemModel() public string BasePrice { get; set; } + public bool IsDownload { get; set; } + public bool HasUserAgreement { get; set; } + + public bool IsEsd { get; set; } + public bool BundlePerItemPricing { get; set; } public bool BundlePerItemShoppingCart { get; set; } public BundleItemModel BundleItem { get; set; } diff --git a/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs b/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs index a2806157c9..96548a4559 100644 --- a/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs @@ -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/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 b0683b8942..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); @@ -160,8 +170,6 @@ var self = this; var opts = this.options; - console.log(opts.galleryHeight); - gallery = $('#pd-gallery').smartGallery({ height: opts.galleryHeight, enableDescription: opts.showImageDescription, 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 2a4bd27a06..56fbff1fc5 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.smartgallery.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.smartgallery.js @@ -132,7 +132,9 @@ }; }; - this.loading(true); + if (this.images[startAt]) { + this.loading(true); + } this.showImage(startAt); @@ -184,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); }, @@ -224,10 +225,10 @@ loading: function(value) { if (value) { - this.loader.show(); + this.loader.addClass('active'); } else { - this.loader.hide(); + this.loader.removeClass('active'); }; }, diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js index a72c2f60da..0929bab9bd 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js @@ -17,7 +17,7 @@ opts = this.options = options, throbber = this.throbber = null, throbberContent = this.throbberContent = null; - + this.visible = false; this.init = function () { @@ -29,31 +29,21 @@ this.initialized = false; this.init(); - + } Throbber.prototype = { - - _reposition: function() { - var self = this, - size = { - left: (self.el.width() - self.throbberContent.outerWidth()) / 2, - top: (self.el.height() - self.throbberContent.outerHeight()) / 2 - } - self.throbberContent.css(size); - }, show: function (o) { - if (this.visible) return; - var self = this, - opts = $.extend( { }, this.options, o); - + var self = this, + opts = $.extend({}, this.options, o); + // create throbber if not avail if (!self.throbber) { - self.throbber = $('
    ') + self.throbber = $('
    ') .addClass(opts.cssClass) .addClass(opts.small ? "small" : "large") .appendTo(opts._global ? 'body' : self.el); @@ -63,44 +53,51 @@ if (opts._global) { self.throbber.addClass("global"); } - + else { + if (/static/.test(self.el.css("position"))) { + self.el.css("position", "relative"); + } + } + self.throbberContent = self.throbber.find(".throbber-content"); + var spinner = window.createCircularSpinner(opts.small ? 50 : 100, true, 3); + spinner.insertAfter(self.throbberContent); self.initialized = true; } - // set text and reposition + // set text self.throbber.css({ visibility: 'hidden', display: 'block' }); self.throbberContent.html(opts.message); - self._reposition(); + self.throbberContent.toggleClass('hide', !(_.isString(opts.message) && opts.message.trim().length > 0)); self.throbber.css({ visibility: 'visible', opacity: 0 }); - var show = function() { - if (_.isFunction(opts.callback)) { - opts.callback.apply(this); - } - if (!self.visible) { + var show = function () { + if (_.isFunction(opts.callback)) { + opts.callback.apply(this); + } + if (!self.visible) { // could have been set to false in 'hide'. // this can happen in very short running processes. self.hide(); - } + } } - + self.visible = true; - self.throbber.delay(opts.delay).transition({opacity: 1}, opts.speed || 0, "linear", show); + self.throbber.delay(opts.delay).transition({ opacity: 1 }, opts.speed || 0, "linear", show); - if (opts.timeout) { - var hideFn = _.bind(self.hide, this); - window.setTimeout(hideFn, opts.timeout + opts.delay); + if (opts.timeout) { + var hideFn = _.bind(self.hide, this); + window.setTimeout(hideFn, opts.timeout + opts.delay); } }, - hide: function(immediately) { + hide: function (immediately) { var self = this, opts = this.options; if (self.throbber && self.visible) { - var hide = function() { - self.throbber.css('display', 'none'); + var hide = function () { + self.throbber.css('display', 'none'); } self.visible = false; @@ -116,10 +113,9 @@ // A really lightweight plugin wrapper around the constructor, // preventing against multiple instantiations $.fn[pluginName] = function (options) { - return this.each(function () { if (!$.data(this, pluginName)) { - options = $.extend( {}, $[pluginName].defaults, options ); + options = $.extend({}, $[pluginName].defaults, options); $.data(this, pluginName, new Throbber(this, options)); } }); @@ -142,40 +138,30 @@ // internal _global: false }; - - // global resize event - $(window).on('resize.throbber', function() { - // resize all active/visible throbbers - $.each(throbbers, function(i, throbber) { - if (throbber.initialized && throbber.visible) { - throbber._reposition(); - } - }) - }); $[pluginName] = { - + // the global, default plugin options defaults: defaults, // options: a message string || options object - show: function(options) { - var opts = $.extend( defaults, _.isString(options) ? { message: options } : options, { show: false, _global: true } ); + show: function (options) { + var opts = $.extend(defaults, _.isString(options) ? { message: options } : options, { show: false, _global: true }); if (!globalThrobber) { globalThrobber = $(window).throbber(opts).data("throbber"); } globalThrobber.show(opts); - }, - hide: function() { + hide: function (immediately) { if (globalThrobber) { - globalThrobber.hide(); + globalThrobber.hide(immediately); } } } // $.throbber })(jQuery, window, document); + diff --git a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj index 66a4d657f2..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,7 +58,7 @@ prompt 4 false - AllFilesInTheProject + AllFilesInProjectFolder false false AnyCPU @@ -134,12 +135,9 @@ ..\..\packages\MsieJavaScriptEngine.1.5.6\lib\net40\MsieJavaScriptEngine.dll True - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll - - - ..\..\packages\recaptcha.1.0.5.0\lib\.NetFramework 4.0\Recaptcha.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -349,6 +347,7 @@ + @@ -646,267 +645,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -920,13 +658,6 @@ - - - - - - - @@ -1048,6 +779,7 @@ + @@ -1059,7 +791,6 @@ - @@ -1076,6 +807,8 @@ + + @@ -1193,6 +926,7 @@ + @@ -1216,7 +950,6 @@ - @@ -1541,12 +1274,6 @@ - - - - - - @@ -1555,7 +1282,6 @@ - @@ -1578,11 +1304,7 @@ - - - - @@ -1856,6 +1578,9 @@ + + + @@ -2133,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" + + @@ -2162,9 +1874,80 @@ if not exist "$(TargetDir)ClearScript.V8" md "$(TargetDir)ClearScript.V8" - + + + + false + + + + + **\*.cs; + **\*.orig; + **\*.bak; + **\*.log; + **\*.csproj; + **\*.csproj.user; + **\*.DotSettings.user; + **\packages.config; + **\*.Debug.config; + **\*.Release.config; + **\obj\**; + **\bin\*.xml; + **\bin\HostRestart\**; + Administration\bin\**; + App_Data\_Backup\**; + App_Data\_temp\**; + App_Data\ExportProfiles\**; + App_Data\ImportProfiles\**; + App_Data\Migrations\**; + App_Data\InstalledPlugins.txt; + App_Data\Licenses.lic; + App_Data\Settings.txt; + App_Data\SmartStore.Db.sdf; + Exchange\**; + Content\Files\ExportImport\**; + Content\Images\Thumbs\**; + Media\**\*.png; + Media\**\*.jpg; + Media\**\*.jpeg; + Media\**\*.gif; + Media\**\*.pdf; + Media\**\*.docx; + Properties\**; + Plugins\*\bin\**; + + + + + + + + + + + + + + + + + + + + + + - - + $(ProjectDir)Media\Thumbs\placeholder.txt + + + + + + + + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less index 1398710a14..d4c3a5acd4 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less @@ -63,6 +63,7 @@ @import "~/Content/bootstrap/carousel.less"; // (MC) extra 3rd party or own components +@import "~/Content/bootstrap/custom/spinner.less"; @import "~/Content/bootstrap/custom/throbber.less"; @import "~/Content/bootstrap/custom/select2.less"; @import "~/Content/bootstrap/custom/datetimepicker.less"; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less index 7fb08a94db..1db6a5e082 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less @@ -2,7 +2,6 @@ // ShoppingCart, OrderSummary & WishList Styles // -------------------------------------------------- - .order-summary-content .delivery-time { text-align: left; } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less index 067b66c9b2..60e01fb235 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less @@ -240,24 +240,14 @@ /* Terms of service ================================================ */ - -#terms-of-service-modal { - width: 650px; -} -#terms-of-service-modal .modal-body { - min-height: 300px; - overflow: hidden; -} -#iframe-terms-of-service { - min-height: 400px; - width: 100%; -} - .terms-of-service.alert { - padding-bottom: 4px !important; - a.read { - font-weight: bold; - } + padding-bottom: 4px !important; + a.read { + font-weight: bold; + &:hover { + cursor: pointer; + } + } } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less index 5de3a398a9..327e7c7ea5 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less @@ -130,16 +130,6 @@ nav ul, nav li { /* Misc ================================================ */ -.ajax-loader-small { - display: inline-block; - position: relative; - width: 16px; - height: 16px; - padding: 0; - margin: 0; - background: transparent url('images/ajax_loader_small.gif') 50% 50% no-repeat; -} - #popup-content { margin: 20px; } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/datalist.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/datalist.less index cd325675b3..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 { 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/pages.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less index 11a472e78c..4df8f7751d 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less @@ -67,6 +67,7 @@ } .compare-products-table tr.product-name { background-color: #e5e5e5; + font-weight: bold; } .compare-products-table tr.product-name a { color: #888; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/product.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/product.less index 486bc2dca5..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; } @@ -391,15 +395,15 @@ border-top: none !important; } +.attributes .image-has-value { + display: table-cell; + padding-top: 10px; +} + .attribute-value-image { margin-right: 5px; vertical-align: middle; } - -.attributes .image-has-value { - margin-left: 28px; -} - .mb-bundle-pictures .fa-plus { padding: 4px 10px 0 10px; } @@ -409,6 +413,10 @@ .attributes .attribute-value-image-checkbox { float: left; } +.attributes .attribute-value-image-checkbox { + margin-right: 10px; + padding-top: 10px; +} .mb-bundle-pictures img { max-width: 24px; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less index 13866fddad..eafccc84d9 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less @@ -14,7 +14,7 @@ position: relative; min-height: 32px; - .ajax-loader-small { + .spinner { position: absolute; left: 50%; top: 50%; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less index 8213eda3d6..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; } @@ -269,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/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/Views/Blog/BlogPost.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml index 15f3a51e38..3c49bafc7a 100644 --- a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml @@ -50,13 +50,6 @@ @Html.TextAreaFor(model => model.AddNewComment.CommentText) @Html.ValidationMessageFor(model => model.AddNewComment.CommentText)
    - string result = TempData["sm.blog.addcomment.result"] as string; - if (!String.IsNullOrEmpty(result)) - { -
    - @result -
    - }
    @Html.ValidationSummary(true)
    diff --git a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml index 7e3cc38336..02b64785ba 100644 --- a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml @@ -56,27 +56,13 @@ @using (Html.BeginForm()) {
    - @if (!ViewData.ModelState.IsValid) - { - @Html.ValidationSummary(true) - } - @{ - string result = TempData["sm.blog.addcomment.result"] as string; - } - @if (!String.IsNullOrEmpty(result)) - { -
    - - @result -
    - } + @Html.ValidationSummary()
    @Html.LabelFor(model => model.AddNewComment.CommentText)
    @Html.TextAreaFor(model => model.AddNewComment.CommentText, new { @class = "comment-text" })
    - @Html.ValidationMessageFor(model => model.AddNewComment.CommentText)
    @if (Model.AddNewComment.DisplayCaptcha) { diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml index f96a1c71c6..cbe8111e78 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml @@ -44,9 +44,12 @@ @item.Name
    - - @item.PictureModel.AlternateText - + @if (item.PictureModel.ImageUrl.HasValue()) + { + + @item.PictureModel.AlternateText + + }
    diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml index 77cb8c034f..69605ebe6e 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml @@ -1,13 +1,12 @@ -@model CompareProductsModel +@using SmartStore.Core; +@using SmartStore.Core.Infrastructure; +@using SmartStore.Web.Models.Catalog; +@model CompareProductsModel @{ Layout = "~/Views/Shared/_Root.cshtml"; - //title Html.AddTitleParts(T("PageTitle.CompareProducts").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Models.Catalog; @{ string columnWidth = ""; if (Model.Products.Count > 0) @@ -15,7 +14,6 @@ columnWidth = Math.Round((decimal)(90M / Model.Products.Count), 0).ToString() + "%"; } - var specificationAttributes = new List(); foreach (var product in Model.Products) { @@ -51,7 +49,15 @@ @product.Name
    - @T("Products.Compare.Price"): @product.ProductPrice.Price +
    + @T("Products.Compare.Price"): @product.ProductPrice.Price +
    + @if (product.BasePriceInfo.HasValue()) + { +
    + @product.BasePriceInfo +
    + }
    @foreach (var specificationAttribute in specificationAttributes) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml index c1c9f6bdf8..8202ddd22c 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml @@ -1,13 +1,12 @@ -@model CompareProductsModel +@using SmartStore.Core; +@using SmartStore.Core.Infrastructure; +@using SmartStore.Web.Models.Catalog; +@model CompareProductsModel @{ Layout = "~/Views/Shared/_ColumnsTwo.cshtml"; - //title Html.AddTitleParts(T("PageTitle.CompareProducts").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Models.Catalog; @{ string columnWidth = ""; if (Model.Products.Count > 0) @@ -15,7 +14,6 @@ columnWidth = Math.Round((decimal)(90M / Model.Products.Count), 0).ToString() + "%"; } - var specificationAttributes = new List(); foreach (var product in Model.Products) { @@ -52,9 +50,14 @@ {
    -

    - @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/FlyoutCompare.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml index 42d9889f80..f10c3ec1e2 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml @@ -19,9 +19,12 @@
  • - - @item.DefaultPictureModel.AlternateText - + @if (item.DefaultPictureModel.ImageUrl.HasValue()) + { + + @item.DefaultPictureModel.AlternateText + + }
    @item.Name diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml index 66d489bb87..92a35f55c8 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml @@ -8,14 +8,16 @@ @ )) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml index 9962eceb67..9e207b9861 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml @@ -1,13 +1,12 @@ -@model IList +@using SmartStore.Core; +@using SmartStore.Core.Infrastructure; +@using SmartStore.Web.Models.Catalog; +@model IList @{ Layout = "~/Views/Shared/_ColumnsThree.cshtml"; - //title Html.AddTitleParts(T("PageTitle.Manufacturers").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Models.Catalog;

    @T("Manufacturers.List")

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

    - - @item.Name + @item.Name

    - - @item.PictureModel.AlternateText + @if (item.PictureModel.ImageUrl.HasValue()) + { + + @item.PictureModel.AlternateText + }
    )) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml index e5d653f039..e94c1df66b 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml @@ -1,8 +1,8 @@ -@model ManufacturerNavigationModel -@using SmartStore.Core.Domain.Catalog; +@using SmartStore.Core.Domain.Catalog; @using SmartStore.Core.Infrastructure; @using SmartStore.Services.Catalog; @using SmartStore.Web.Models.Catalog; +@model ManufacturerNavigationModel @if (Model.Manufacturers.Count > 0) {
    @@ -15,7 +15,7 @@ {
  • - @if (Model.DisplayImages) + @if (Model.DisplayImages && manufacturer.PictureUrl.HasValue()) { @manufacturer.Name diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml index 978bb9c0c9..d374c9044b 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml @@ -26,8 +26,11 @@
  • - - @product.DefaultPictureModel.AlternateText + @if (product.DefaultPictureModel.ImageUrl.HasValue()) + { + + @product.DefaultPictureModel.AlternateText + }
    @product.Name diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml index 156c24a9d0..8809946c25 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml @@ -1,15 +1,11 @@ -@model SearchModel +@using SmartStore.Web.Models.Catalog; +@using SmartStore.Web.Framework.UI; +@model SearchModel @{ Layout = "~/Views/Shared/_ColumnsThree.cshtml"; - //title Html.AddTitleParts(T("PageTitle.Search").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web; -@using SmartStore.Web.Models.Catalog; -@using SmartStore.Web.Framework.UI; @functions{ private bool ShowListOptions() { @@ -32,7 +28,9 @@ diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml index fed6c42b8a..10c8f7d30f 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml @@ -8,8 +8,8 @@ //title Html.AddTitleParts(T("PageTitle.Checkout").Text); - string termsLink = ""; - string disclaimerLink = ""; + string termsLink = ""; + string disclaimerLink = ""; string terms = string.Format(T("Checkout.TermsOfService.IAccept"), termsLink, "", disclaimerLink); } @section orderProgress{ @@ -27,18 +27,22 @@ @Html.Raw(@T("Checkout.ConfirmHint"))
  • - + + + - if (Model.TermsOfServiceEnabled) - { + if (Model.TermsOfServiceEnabled) + {
    @@ -48,8 +52,8 @@ - @if (Model.ShowConfirmOrderLegalHint) - { - - } + { + + }
    @@ -109,26 +113,59 @@ $(function () { var checkoutButton = $(".confirm-order-next-step-button"); checkoutButton.click(function () { + var termOfServiceOk = true, + userAgreementsOk = true, + esdRevocationWaiverOk = true; - $("#customercommenthidden").val($("#CustomerComment").val()); + $("#customercommenthidden").val($("#CustomerComment").val()); + $('#SubscribeToNewsLetterHidden').val($('input[name=SubscribeToNewsLetter]').is(':checked')); + $('#AcceptThirdPartyEmailHandOverHidden').val($('input[name=AcceptThirdPartyEmailHandOver]').is(':checked')); - //terms of services - var termOfServiceOk = true; + // terms of services @if (Model.TermsOfServiceEnabled) { - + if (!$('#termsofservice').is(':checked')) { - displayNotification('@T("Checkout.TermsOfService.PleaseAccept").ToString().EncodeJsString()', "error"); + displayNotification('@T("Checkout.TermsOfService.PleaseAccept").ToString().EncodeJsString('"', false)', "error"); termOfServiceOk = false; $.scrollTo($('#termsofservice'), 800, { offset: -70 }); } else { termOfServiceOk = true; } - + } + + // agree user agreement for downloadable products + $('table.table-order-products').find('input[name^=AgreeUserAgreement]').each(function () { + if (!$(this).is(':checked')) { + userAgreementsOk = false; + displayNotification('@T("Checkout.DownloadUserAgreement.PleaseAgree").ToString().EncodeJsString('"', false)', 'error'); + if (termOfServiceOk) { + $.scrollTo($('table.table-order-products'), 800, { offset: -20 }); + } + return false; + } + }); + + // agree esd revocation waiver + @if(Model.ShowEsdRevocationWaiverBox) + { + + $('table.table-order-products').find('input[name^=AgreeEsdRevocationWaiver]').each(function () { + if (!$(this).is(':checked')) { + esdRevocationWaiverOk = false; + displayNotification('@T("Checkout.EsdRevocationWaiverConfirmation.PleaseAgree").ToString().EncodeJsString('"', false)', 'error'); + if (termOfServiceOk) { + $.scrollTo($('table.table-order-products'), 800, { offset: -20 }); + } + return false; + } + }); + + } - if (termOfServiceOk) { + if (termOfServiceOk && userAgreementsOk && esdRevocationWaiverOk) { var submitOrderEvent = jQuery.Event('submitOrder'); submitOrderEvent.isOrderValid = true; submitOrderEvent.isMobile = false; diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml index df97ab4bae..cab3cdbc8c 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml @@ -70,6 +70,7 @@ @Html.Action(infoRoute.Action, infoRoute.Controller, infoRoute.RouteValues) }
    +
    }
    } diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml index 6674bafc8e..07fc9cbf98 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml @@ -27,7 +27,7 @@
    - +
    @if (!String.IsNullOrEmpty(shippingMethod.Description)) { diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml index 54c1a51dce..68d6091639 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml @@ -42,7 +42,7 @@ - @shippingMethod.Name + @Html.Raw(HttpUtility.HtmlDecode(shippingMethod.Name))
    diff --git a/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml b/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml index 5384e9bb64..96b1274211 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml @@ -64,7 +64,8 @@
  • - @T("Wishlist") @Model.WishlistItems + @T("Wishlist") @Model.WishlistItems +
  • } @if (Model.ShoppingCartEnabled) @@ -72,14 +73,16 @@ }
  • - @T("Account.Logout") + @T("Account.Logout") +
  • @Html.Widget("account_dropdown_after") diff --git a/src/Presentation/SmartStore.Web/Views/Common/EntityPicker.cshtml b/src/Presentation/SmartStore.Web/Views/Common/EntityPicker.cshtml new file mode 100644 index 0000000000..f2ce8112b9 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Common/EntityPicker.cshtml @@ -0,0 +1,96 @@ +@model EntityPickerModel +@using SmartStore.Web.Models.Common; + + + +@helper FormButtons() +{ +
    + @Html.LabelFor(model => model.ProductName, new { @class = "control-label", @for = "ProductName" }) +
    + @Html.TextBoxFor(model => model.ProductName, new { @class = "entity-picker-searchterm" }) + + +
    +
    +} + +@helper ProductSearchForm() +{ +
    + @FormButtons() + +
    +
    + @Html.LabelFor(model => model.CategoryId, new { @class = "control-label", @for = "CategoryId" }) +
    + @Html.DropDownListFor(model => model.CategoryId, Model.AvailableCategories, Model.AllString, new { @class = "item" }) +
    +
    + +
    + @Html.LabelFor(model => model.ManufacturerId, new { @class = "control-label", @for = "ManufacturerId" }) +
    + @Html.DropDownListFor(model => model.ManufacturerId, Model.AvailableManufacturers, Model.AllString, new { @class = "item" }) +
    +
    + + @if (Model.AvailableStores.Count > 1) + { +
    + @Html.LabelFor(model => model.StoreId, new { @class = "control-label", @for = "StoreId" }) +
    + @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores, Model.AllString, new { @class = "item" }) +
    +
    + } + +
    + @Html.LabelFor(model => model.ProductTypeId, new { @class = "control-label", @for = "ProductTypeId" }) +
    + @Html.DropDownListFor(model => model.ProductTypeId, Model.AvailableProductTypes, Model.AllString, new { @class = "item" }) +
    +
    +
    +
    +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Common/EntityPickerList.cshtml b/src/Presentation/SmartStore.Web/Views/Common/EntityPickerList.cshtml new file mode 100644 index 0000000000..7537bfba4f --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Common/EntityPickerList.cshtml @@ -0,0 +1,56 @@ +@using SmartStore.Web.Models.Common; +@model EntityPickerModel +@helper HighlightSearchTermInTitle(EntityPickerModel.SearchResultModel item) +{ + if (Model.HighligtSearchTerm && Model.SearchTerm.HasValue() && item.Title.HasValue()) + { + @Html.Raw(item.Title.Replace(Model.SearchTerm, "{0}".FormatInvariant(Model.SearchTerm), StringComparison.OrdinalIgnoreCase)) + } + else + { + @item.Title + } +} +@foreach (var item in Model.SearchResult ?? Enumerable.Empty()) +{ +
    +
    +
    + @if (item.ImageUrl.HasValue()) + { + + } +
    +
    +
    + @if (item.LabelText.HasValue()) + { + @item.LabelText + } + @HighlightSearchTermInTitle(item) +
    +
    + @if (item.Published.HasValue) + { + + } + @item.Summary +
    +
    +
    +
    +} + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Common/InfoBlock.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Common/InfoBlock.Mobile.cshtml index ac87929f02..4085b4d143 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/InfoBlock.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/InfoBlock.Mobile.cshtml @@ -1,62 +1,74 @@ @model InfoBlockModel @using SmartStore.Web.Models.Common; @Html.Widget("mobile_infoblock_before") -
    - -
    -
    - -
    -
    - -
    -
    -
      - @if (Model.TopicPageUrls.ContainsKey("imprint")) - { -
    • @T("Imprint")
    • - } - @if (Model.TopicPageUrls.ContainsKey("disclaimer")) - { -
    • @T("Disclaimer")
    • - } -
    -
    +@if (Model.TopicPageUrls.ContainsKey("contactus") || Model.TopicPageUrls.ContainsKey("aboutus") || Model.BlogEnabled || Model.ForumEnabled) +{ +
    + +
    +} +@if (Model.TopicPageUrls.ContainsKey("paymentinfo") || Model.TopicPageUrls.ContainsKey("shippinginfo")) +{ +
    + +
    +} +@if (Model.TopicPageUrls.ContainsKey("privacyinfo") || Model.TopicPageUrls.ContainsKey("conditionsofuse")) +{ +
    + +
    +} +@if (Model.TopicPageUrls.ContainsKey("imprint") || Model.TopicPageUrls.ContainsKey("disclaimer")) +{ +
    +
      + @if (Model.TopicPageUrls.ContainsKey("imprint")) + { +
    • @T("Imprint")
    • + } + @if (Model.TopicPageUrls.ContainsKey("disclaimer")) + { +
    • @T("Disclaimer")
    • + } +
    +
    +} @if (Model.AllowPrivateMessages) {
    diff --git a/src/Presentation/SmartStore.Web/Views/Common/Menu.cshtml b/src/Presentation/SmartStore.Web/Views/Common/Menu.cshtml index adb9eb97f6..12b71e9959 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/Menu.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/Menu.cshtml @@ -50,7 +50,7 @@ @Html.Widget("header_menu_after") -@if (Model.NewsEnabled || Model.BlogEnabled || Model.ForumEnabled || Model.DisplayAdminLink || Model.DisplayAdminLink) +@if (Model.NewsEnabled || Model.BlogEnabled || Model.ForumEnabled || Model.DisplayAdminLink || Model.DisplayLoginLink) { } diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Info.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Info.cshtml index 22eca6cfcf..fbf29a96b4 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/Info.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/Info.cshtml @@ -52,11 +52,6 @@ { @Html.AntiForgeryToken() - if (!ViewData.ModelState.IsValid) - { - @Html.ValidationSummary(true) - } -
    @T("Account.YourPersonalDetails") diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Orders.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Orders.Mobile.cshtml index 554885a8c1..a0e67e4c08 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/Orders.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/Orders.Mobile.cshtml @@ -1,11 +1,10 @@ -@model CustomerOrderListModel +@using SmartStore.Web.Models.Customer; +@model CustomerOrderListModel @{ Layout = "~/Views/Shared/_Root.cshtml"; - //title Html.AddTitleParts(T("PageTitle.Account").Text); } -@using SmartStore.Web.Models.Customer;
    diff --git a/src/Presentation/SmartStore.Web/Views/Customer/Orders.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/Orders.cshtml index bc7a8f8606..94938bb554 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/Orders.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/Orders.cshtml @@ -1,11 +1,10 @@ -@model CustomerOrderListModel +@using SmartStore.Web.Models.Customer; +@model CustomerOrderListModel @{ Layout = "~/Views/Shared/_ColumnsTwo.cshtml"; - //title Html.AddTitleParts(T("PageTitle.Account").Text); } -@using SmartStore.Web.Models.Customer; @section left { @Html.Partial("MyAccountNavigation", Model.NavigationModel, new ViewDataDictionary()) @@ -19,7 +18,6 @@
    @if (Model.RecurringOrders.Count > 0) { - using (Html.BeginForm()) {
    @@ -106,46 +104,47 @@
    } } + + @if (Model.Orders.Count == 0) + { +
    + @T("Account.CustomerOrders.NoOrders") +
    + } +
    - @if (Model.Orders.Count > 0) - { - foreach (var order in Model.Orders) - { -
    - @T("Account.CustomerOrders.OrderNumber"): @order.OrderNumber -
    -
    -
    @T("Account.CustomerOrders.OrderStatus"):
    -
    @order.OrderStatus
    -
    @T("Account.CustomerOrders.OrderDate"):
    -
    @order.CreatedOn.ToString()
    -
    @T("Account.CustomerOrders.OrderTotal"):
    -
    @order.OrderTotal
    -
    -
    - +
    + }
    + + @Html.SmartStore().Pager(Model.Orders).Name("orders-paginator").Action("Orders", "Customer")
    diff --git a/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.Mobile.cshtml index 77782f2fa4..b6681691c6 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.Mobile.cshtml @@ -24,7 +24,7 @@
    @Html.LabelFor(model => model.Email) - @Html.TextBoxFor(model => model.Email, new { @class = "email" }) + @Html.TextBoxFor(model => model.Email, new { @class = "email", @type = "email" }) @Html.ValidationMessageFor(model => model.Email)
    diff --git a/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.cshtml index 3123bb9ffd..eaca87e410 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/PasswordRecovery.cshtml @@ -29,7 +29,7 @@
    @Html.LabelFor(model => model.Email, new { @class = "control-label required" })
    - @Html.TextBoxFor(model => model.Email, new { @class = "email" }) + @Html.TextBoxFor(model => model.Email, new { @class = "email", @type = "email" }) @Html.ValidationMessageFor(model => model.Email)
    diff --git a/src/Presentation/SmartStore.Web/Views/Customer/RegisterResult.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/RegisterResult.cshtml index a3a9ac9314..c4bcbfbfdb 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/RegisterResult.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/RegisterResult.cshtml @@ -4,21 +4,17 @@ @using SmartStore.Core.Infrastructure; @using SmartStore.Core.Infrastructure.DependencyManagement; @{ - Layout = "~/Views/Shared/_ColumnsThree.cshtml"; + Layout = "~/Views/Shared/_ColumnsThree.cshtml"; - //title - Html.AddTitleParts(T("PageTitle.Register").Text); + // title + Html.AddTitleParts(T("PageTitle.Register").Text); - //continue URL - var continueUrl = ""; - if (!String.IsNullOrEmpty(this.Context.Request.QueryString["returnUrl"])) - { - continueUrl = this.Context.Request.QueryString["returnUrl"]; - } - else - { - continueUrl = Url.RouteUrl("HomePage"); - } + // continue URL + var continueUrl = this.Context.Request.QueryString["returnUrl"]; + if (!continueUrl.HasValue() || (continueUrl.HasValue() && !Url.IsLocalUrl(continueUrl))) + { + continueUrl = Url.RouteUrl("HomePage"); + } }
    diff --git a/src/Presentation/SmartStore.Web/Views/Customer/UserAgreement.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/UserAgreement.cshtml index a352ee5bd2..40de7650cd 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/UserAgreement.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/UserAgreement.cshtml @@ -25,18 +25,21 @@

    @T("DownloadableProducts.UserAgreement")

    -

    +

    @Html.Raw(Model.UserAgreementText) -

    +
    -
    -
    - + +

    + +
    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Customer/_CheckUsernameAvailability.cshtml b/src/Presentation/SmartStore.Web/Views/Customer/_CheckUsernameAvailability.cshtml index b79106ebb5..fffbbe4a6b 100644 --- a/src/Presentation/SmartStore.Web/Views/Customer/_CheckUsernameAvailability.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Customer/_CheckUsernameAvailability.cshtml @@ -1,6 +1,6 @@  + + @@ -113,20 +115,57 @@ -
    - -
    - if (Model.Comments.Count > 0) diff --git a/src/Presentation/SmartStore.Web/Views/News/NewsItem.cshtml b/src/Presentation/SmartStore.Web/Views/News/NewsItem.cshtml index 988eb042d8..8b79db3ab8 100644 --- a/src/Presentation/SmartStore.Web/Views/News/NewsItem.cshtml +++ b/src/Presentation/SmartStore.Web/Views/News/NewsItem.cshtml @@ -40,20 +40,10 @@ @using (Html.BeginForm()) {
    - @if (!ViewData.ModelState.IsValid) - { + @if (ViewContext.ViewData.ModelState.ContainsKey("")) + { @Html.ValidationSummary(true) } - @{ - string result = TempData["sm.news.addcomment.result"] as string; - } - @if (!String.IsNullOrEmpty(result)) - { -
    - - @result -
    - }
    @Html.LabelFor(model => model.AddNewComment.CommentTitle) diff --git a/src/Presentation/SmartStore.Web/Views/Order/Details.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Order/Details.Mobile.cshtml index 80249d7776..289fb9fc47 100644 --- a/src/Presentation/SmartStore.Web/Views/Order/Details.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Order/Details.Mobile.cshtml @@ -439,8 +439,9 @@ { - - @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): + @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): +
    + @string.Format(T("ShoppingCart.Totals.GiftCardInfo.Remaining").Text, gc.Remaining) diff --git a/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml b/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml index 8e56cfc5a6..5a4fbfb5c6 100644 --- a/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml @@ -532,8 +532,9 @@ { - - @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): + @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): +
    + @string.Format(T("ShoppingCart.Totals.GiftCardInfo.Remaining").Text, gc.Remaining) @@ -541,8 +542,8 @@ - } - } + } + } @if (Model.RedeemedRewardPoints > 0) { diff --git a/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml b/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml index 5d0e9a084f..850a58a87e 100644 --- a/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml @@ -72,23 +72,21 @@ @Html.Action("ReviewOverview", new { id = Model.Id }) - @if (Model.Manufacturers != null && Model.Manufacturers.Count > 0) - { -
    - - @foreach (var manufacturer in Model.Manufacturers) - { -
    -
    - - @manufacturer.PictureModel.AlternateText - -
    -
    - } - -
    - } + @if (Model.Manufacturers != null && Model.Manufacturers.Any(x => x.PictureModel != null && x.PictureModel.ImageUrl.HasValue())) + { +
    + @foreach (var manufacturer in Model.Manufacturers.Where(x => x.PictureModel != null && x.PictureModel.ImageUrl.HasValue())) + { +
    +
    + + @manufacturer.PictureModel.AlternateText + +
    +
    + } +
    + }