Skip to content

Commit 8f0e4b3

Browse files
committed
Added category importer
1 parent a930035 commit 8f0e4b3

8 files changed

Lines changed: 308 additions & 15 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* Admin > System > System Info shows used application memory (RAM)
3030
* Added option to make VATIN mandatory during customer registration
3131
* Customer import
32+
* Category import
3233

3334
### Improvements
3435
* (Perf) Implemented static caches for URL aliases and localized properties. Increases app startup and request speed by up to 30%.

src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public enum ImportEntityType
99
{
1010
Product = 0,
1111
Customer,
12-
NewsLetterSubscription
12+
NewsLetterSubscription,
13+
Category
1314
}
1415

1516
public enum ImportFileType

src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder)
102102

103103
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.Product", "Product", "Produkt");
104104
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.Customer", "Customer", "Kunde");
105-
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.NewsLetterSubscription", "Newsletter Subscribers", "Newsletter Abonnenten");
105+
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.NewsLetterSubscription", "Newsletter Subscriber", "Newsletter Abonnent");
106+
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.Category", "Category", "Warengruppe");
106107

107108
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportFileType.CSV", "Delimiter separated values (.csv)", "Trennzeichen getrennte Werte (.csv)");
108109
builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportFileType.XLSX", "Excel (.xlsx)", "Excel (.xlsx)");
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using SmartStore.Core.Data;
5+
using SmartStore.Core.Domain.Catalog;
6+
using SmartStore.Core.Domain.DataExchange;
7+
using SmartStore.Core.Domain.Media;
8+
using SmartStore.Core.Domain.Seo;
9+
using SmartStore.Core.Events;
10+
using SmartStore.Services.DataExchange.Import;
11+
using SmartStore.Services.Seo;
12+
using SmartStore.Services.Stores;
13+
14+
namespace SmartStore.Services.Catalog.Importer
15+
{
16+
public class CategoryImporter : IEntityImporter
17+
{
18+
private readonly IRepository<Category> _categoryRepository;
19+
private readonly IRepository<UrlRecord> _urlRecordRepository;
20+
private readonly IRepository<Picture> _pictureRepository;
21+
private readonly ICommonServices _services;
22+
private readonly ICategoryService _categoryService;
23+
private readonly IUrlRecordService _urlRecordService;
24+
private readonly ICategoryTemplateService _categoryTemplateService;
25+
private readonly IStoreMappingService _storeMappingService;
26+
private readonly SeoSettings _seoSettings;
27+
28+
public CategoryImporter(
29+
IRepository<Category> categoryRepository,
30+
IRepository<UrlRecord> urlRecordRepository,
31+
IRepository<Picture> pictureRepository,
32+
ICommonServices services,
33+
ICategoryService categoryService,
34+
IUrlRecordService urlRecordService,
35+
ICategoryTemplateService categoryTemplateService,
36+
IStoreMappingService storeMappingService,
37+
SeoSettings seoSettings)
38+
{
39+
_categoryRepository = categoryRepository;
40+
_urlRecordRepository = urlRecordRepository;
41+
_pictureRepository = pictureRepository;
42+
_services = services;
43+
_categoryService = categoryService;
44+
_urlRecordService = urlRecordService;
45+
_categoryTemplateService = categoryTemplateService;
46+
_storeMappingService = storeMappingService;
47+
_seoSettings = seoSettings;
48+
}
49+
50+
private int ProcessSlugs(ImportExecuteContext context, ImportRow<Category>[] batch)
51+
{
52+
var slugMap = new Dictionary<string, UrlRecord>(100);
53+
Func<string, UrlRecord> slugLookup = ((s) =>
54+
{
55+
if (slugMap.ContainsKey(s))
56+
{
57+
return slugMap[s];
58+
}
59+
return (UrlRecord)null;
60+
});
61+
62+
var entityName = typeof(Category).Name;
63+
64+
foreach (var row in batch)
65+
{
66+
if (row.IsNew || row.NameChanged || row.Segmenter.HasDataColumn("SeName"))
67+
{
68+
try
69+
{
70+
string seName = row.GetDataValue<string>("SeName");
71+
seName = row.Entity.ValidateSeName(seName, row.Entity.Name, true, _urlRecordService, _seoSettings, extraSlugLookup: slugLookup);
72+
73+
UrlRecord urlRecord = null;
74+
75+
if (row.IsNew)
76+
{
77+
// dont't bother validating SeName for new entities.
78+
urlRecord = new UrlRecord
79+
{
80+
EntityId = row.Entity.Id,
81+
EntityName = entityName,
82+
Slug = seName,
83+
LanguageId = 0,
84+
IsActive = true,
85+
};
86+
_urlRecordRepository.Insert(urlRecord);
87+
}
88+
else
89+
{
90+
urlRecord = _urlRecordService.SaveSlug(row.Entity, seName, 0);
91+
}
92+
93+
if (urlRecord != null)
94+
{
95+
// a new record was inserted to the store: keep track of it for this batch.
96+
slugMap[seName] = urlRecord;
97+
}
98+
}
99+
catch (Exception exception)
100+
{
101+
context.Result.AddWarning(exception.Message, row.GetRowInfo(), "SeName");
102+
}
103+
}
104+
}
105+
106+
// commit whole batch at once
107+
return _urlRecordRepository.Context.SaveChanges();
108+
}
109+
110+
private int ProcessCategories(ImportExecuteContext context,
111+
ImportRow<Category>[] batch,
112+
DateTime utcNow,
113+
List<int> allCategoryTemplateIds)
114+
{
115+
_categoryRepository.AutoCommitEnabled = true;
116+
117+
Category lastInserted = null;
118+
Category lastUpdated = null;
119+
var defaultTemplateId = allCategoryTemplateIds.First();
120+
121+
foreach (var row in batch)
122+
{
123+
Category category = null;
124+
object key;
125+
126+
// try get by int ID
127+
if (row.DataRow.TryGetValue("Id", out key) && key.ToString().ToInt() > 0)
128+
{
129+
category = _categoryService.GetCategoryById(key.ToString().ToInt());
130+
}
131+
132+
if (category == null)
133+
{
134+
// a Name is required with new categories
135+
if (!row.Segmenter.HasDataColumn("Name"))
136+
{
137+
context.Result.AddError("The 'Name' field is required for new categories. Skipping row.", row.GetRowInfo(), "Name");
138+
continue;
139+
}
140+
category = new Category
141+
{
142+
CategoryTemplateId = defaultTemplateId
143+
};
144+
}
145+
146+
var name = row.GetDataValue<string>("Name");
147+
148+
row.Initialize(category, name);
149+
150+
if (!row.IsNew && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
151+
{
152+
// Perf: use this later for SeName updates.
153+
row.NameChanged = true;
154+
}
155+
156+
row.SetProperty(context.Result, category, (x) => x.Name);
157+
row.SetProperty(context.Result, category, (x) => x.FullName);
158+
row.SetProperty(context.Result, category, (x) => x.Description);
159+
row.SetProperty(context.Result, category, (x) => x.BottomDescription);
160+
row.SetProperty(context.Result, category, (x) => x.MetaKeywords);
161+
row.SetProperty(context.Result, category, (x) => x.MetaDescription);
162+
row.SetProperty(context.Result, category, (x) => x.MetaTitle);
163+
row.SetProperty(context.Result, category, (x) => x.ParentCategoryId); // TODO: hierachical data in batch processing?
164+
row.SetProperty(context.Result, category, (x) => x.PageSize, 12);
165+
row.SetProperty(context.Result, category, (x) => x.AllowCustomersToSelectPageSize, true);
166+
row.SetProperty(context.Result, category, (x) => x.PageSizeOptions);
167+
row.SetProperty(context.Result, category, (x) => x.PriceRanges);
168+
row.SetProperty(context.Result, category, (x) => x.ShowOnHomePage);
169+
row.SetProperty(context.Result, category, (x) => x.HasDiscountsApplied);
170+
row.SetProperty(context.Result, category, (x) => x.Published, true);
171+
row.SetProperty(context.Result, category, (x) => x.DisplayOrder);
172+
row.SetProperty(context.Result, category, (x) => x.LimitedToStores);
173+
row.SetProperty(context.Result, category, (x) => x.Alias);
174+
row.SetProperty(context.Result, category, (x) => x.DefaultViewMode);
175+
176+
if (row.DataRow.TryGetValue("CategoryTemplateId", out key))
177+
{
178+
int templateId = key.ToString().ToInt();
179+
if (templateId > 0 && allCategoryTemplateIds.Contains(templateId))
180+
category.CategoryTemplateId = templateId;
181+
}
182+
183+
if (row.DataRow.TryGetValue("PictureId", out key))
184+
{
185+
int pictureId = key.ToString().ToInt();
186+
if (pictureId > 0 && _pictureRepository.TableUntracked.Any(x => x.Id == pictureId))
187+
category.PictureId = pictureId;
188+
}
189+
190+
var storeIds = row.GetDataValue<List<int>>("StoreIds");
191+
if (storeIds != null && storeIds.Any())
192+
{
193+
_storeMappingService.SaveStoreMappings(category, storeIds.ToArray());
194+
}
195+
196+
row.SetProperty(context.Result, category, (x) => x.CreatedOnUtc, utcNow);
197+
category.UpdatedOnUtc = utcNow;
198+
199+
if (row.IsTransient)
200+
{
201+
_categoryRepository.Insert(category);
202+
lastInserted = category;
203+
}
204+
else
205+
{
206+
_categoryRepository.Update(category);
207+
lastUpdated = category;
208+
}
209+
}
210+
211+
// commit whole batch at once
212+
var num = _categoryRepository.Context.SaveChanges();
213+
214+
// Perf: notify only about LAST insertion and update
215+
if (lastInserted != null)
216+
_services.EventPublisher.EntityInserted(lastInserted);
217+
if (lastUpdated != null)
218+
_services.EventPublisher.EntityUpdated(lastUpdated);
219+
220+
return num;
221+
}
222+
223+
public void Execute(ImportExecuteContext context)
224+
{
225+
var allCategoryTemplateIds = _categoryTemplateService.GetAllCategoryTemplates()
226+
.Select(x => x.Id)
227+
.ToList();
228+
229+
using (var scope = new DbContextScope(ctx: _categoryRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false))
230+
{
231+
var segmenter = new ImportDataSegmenter<Category>(context.DataTable);
232+
var utcNow = DateTime.UtcNow;
233+
234+
context.Result.TotalRecords = segmenter.TotalRows;
235+
236+
while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch())
237+
{
238+
var batch = segmenter.CurrentBatch;
239+
240+
// Perf: detach all entities
241+
_categoryRepository.Context.DetachAll(false);
242+
243+
context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows);
244+
245+
try
246+
{
247+
ProcessCategories(context, batch, utcNow, allCategoryTemplateIds);
248+
}
249+
catch (Exception exception)
250+
{
251+
context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories");
252+
}
253+
254+
// reduce batch to saved (valid) products.
255+
// No need to perform import operations on errored products.
256+
batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray();
257+
258+
// update result object
259+
context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient);
260+
context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient);
261+
262+
if (context.DataTable.HasColumn("SeName") || batch.Any(x => x.IsNew || x.NameChanged))
263+
{
264+
try
265+
{
266+
_categoryRepository.Context.AutoDetectChangesEnabled = true;
267+
ProcessSlugs(context, batch);
268+
}
269+
catch (Exception exception)
270+
{
271+
context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs");
272+
}
273+
finally
274+
{
275+
_categoryRepository.Context.AutoDetectChangesEnabled = false;
276+
}
277+
}
278+
}
279+
}
280+
}
281+
}
282+
}

src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -371,24 +371,22 @@ private int ProcessProducts(ImportExecuteContext context, ImportRow<Product>[] b
371371
foreach (var row in batch)
372372
{
373373
Product product = null;
374-
375374
object key;
376-
var dataRow = row.DataRow;
377375

378376
// try get by int ID
379-
if (dataRow.TryGetValue("Id", out key) && key.ToString().ToInt() > 0)
377+
if (row.DataRow.TryGetValue("Id", out key) && key.ToString().ToInt() > 0)
380378
{
381379
product = _productService.GetProductById(key.ToString().ToInt());
382380
}
383381

384382
// try get by SKU
385-
if (product == null && dataRow.TryGetValue("SKU", out key))
383+
if (product == null && row.DataRow.TryGetValue("SKU", out key))
386384
{
387385
product = _productService.GetProductBySku(key.ToString());
388386
}
389387

390388
// try get by GTIN
391-
if (product == null && dataRow.TryGetValue("Gtin", out key))
389+
if (product == null && row.DataRow.TryGetValue("Gtin", out key))
392390
{
393391
product = _productService.GetProductByGtin(key.ToString());
394392
}
@@ -404,7 +402,7 @@ private int ProcessProducts(ImportExecuteContext context, ImportRow<Product>[] b
404402
product = new Product();
405403
}
406404

407-
string name = row.GetDataValue<string>("Name");
405+
var name = row.GetDataValue<string>("Name");
408406

409407
row.Initialize(product, name);
410408

@@ -508,7 +506,6 @@ private int ProcessProducts(ImportExecuteContext context, ImportRow<Product>[] b
508506
}
509507

510508
row.SetProperty(context.Result, product, (x) => x.CreatedOnUtc, utcNow);
511-
512509
product.UpdatedOnUtc = utcNow;
513510

514511
if (row.IsTransient)
@@ -587,7 +584,7 @@ public void Execute(ImportExecuteContext context)
587584
}
588585
catch (Exception exception)
589586
{
590-
context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSeoSlugs");
587+
context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs");
591588
}
592589
finally
593590
{

0 commit comments

Comments
 (0)