/// <summary> /// Gets a list of product variant attribute values from an attribute selection. /// Loads attribute values from <paramref name="attributes"/> and not from database. /// Typically used in conjunction with <see cref="ProductBatchContext"/>. /// Only returns values of list type attributes (<see cref="ProductVariantAttribute.IsListTypeAttribute()"/>). /// </summary> /// <param name="selection">Attributes selection.</param> /// <param name="attributes">Attributes from which the values are loaded.</param> /// <returns>List of product variant attribute values.</returns> public static IList <ProductVariantAttributeValue> MaterializeProductVariantAttributeValues( this ProductVariantAttributeSelection selection, IEnumerable <ProductVariantAttribute> attributes) { Guard.NotNull(attributes, nameof(attributes)); var result = new List <ProductVariantAttributeValue>(); if (selection?.AttributesMap?.Any() ?? false) { var listTypeAttributeIds = attributes .Where(x => x.IsListTypeAttribute()) .OrderBy(x => x.DisplayOrder) .Select(x => x.Id) .Distinct() .ToArray(); var valueIds = selection.AttributesMap .Where(x => listTypeAttributeIds.Contains(x.Key)) .SelectMany(x => x.Value) .Select(x => x.ToString()) .Where(x => x.HasValue()) // Avoid exception when string is empty. .Select(x => x.ToInt()) .Where(x => x != 0) .Distinct() .ToArray(); foreach (int valueId in valueIds) { foreach (var attribute in attributes) { var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == valueId); if (attributeValue != null) { result.Add(attributeValue); break; } } } } return(result); }
// TODO: (mg) (core) Check whether IProductAttributeMaterializer.PrefetchProductVariantAttributes is still required. // Looks like it can be done by MaterializeProductVariantAttributeValuesAsync. public virtual async Task <IList <ProductVariantAttribute> > MaterializeProductVariantAttributesAsync(ProductVariantAttributeSelection selection) { Guard.NotNull(selection, nameof(selection)); var ids = selection.AttributesMap .Select(x => x.Key) .ToArray(); if (!ids.Any()) { return(new List <ProductVariantAttribute>()); } var cacheKey = ATTRIBUTES_BY_IDS_KEY + string.Join(",", ids); var result = await _requestCache.GetAsync(cacheKey, async() => { var query = _db.ProductVariantAttributes .AsNoTracking() .Include(x => x.Product) .Include(x => x.ProductAttribute) .Include(x => x.ProductVariantAttributeValues) .Where(x => ids.Contains(x.Id)) .OrderBy(x => x.DisplayOrder); var attributes = await query.ToListAsync(); return(attributes.OrderBySequence(ids).ToList()); }); return(result); }
public virtual async Task <ProductVariantAttributeCombination> MergeWithCombinationAsync(Product product, ProductVariantAttributeSelection selection) { var combination = await FindAttributeCombinationAsync(product.Id, selection); if (combination != null && combination.IsActive) { product.MergeWithCombination(combination); } else if (product.MergedDataValues != null) { product.MergedDataValues.Clear(); } return(combination); }
public virtual async Task <ProductVariantAttributeCombination> FindAttributeCombinationAsync(int productId, ProductVariantAttributeSelection selection) { if (productId == 0 || !(selection?.AttributesMap?.Any() ?? false)) { return(null); } var cacheKey = ATTRIBUTECOMBINATION_BY_IDJSON_KEY.FormatInvariant(productId, selection.AsJson()); var combinations = await _requestCache.GetAsync(cacheKey, async() => { var combinations = await _db.ProductVariantAttributeCombinations .AsNoTracking() .Where(x => x.ProductId == productId) .Select(x => new { x.Id, x.RawAttributes }) .ToListAsync(); foreach (var combination in combinations) { if (selection.Equals(new ProductVariantAttributeSelection(combination.RawAttributes))) { return(await _db.ProductVariantAttributeCombinations.FindByIdAsync(combination.Id)); } } return(null); }); return(null); }
public virtual async Task <(ProductVariantAttributeSelection Selection, List <string> Warnings)> CreateAttributeSelectionAsync( ProductVariantQuery query, IEnumerable <ProductVariantAttribute> attributes, int productId, int bundleItemId, bool getFilesFromRequest = true) { Guard.NotNull(query, nameof(query)); Guard.NotNull(attributes, nameof(attributes)); var selection = new ProductVariantAttributeSelection(null); var warnings = new List <string>(); foreach (var pva in attributes) { var selectedItems = query.Variants.Where(x => x.ProductId == productId && x.BundleItemId == bundleItemId && x.AttributeId == pva.ProductAttributeId && x.VariantAttributeId == pva.Id); switch (pva.AttributeControlType) { case AttributeControlType.DropdownList: case AttributeControlType.RadioList: case AttributeControlType.Boxes: { var valueId = selectedItems.FirstOrDefault() ?.Value ?.SplitSafe(",") ?.FirstOrDefault() ?.ToInt() ?? 0; if (valueId > 0) { selection.AddAttributeValue(pva.Id, valueId); } } break; case AttributeControlType.Checkboxes: foreach (var item in selectedItems) { var valueId = item.Value.SplitSafe(",").FirstOrDefault()?.ToInt() ?? 0; if (valueId > 0) { selection.AddAttributeValue(pva.Id, valueId); } } break; case AttributeControlType.TextBox: case AttributeControlType.MultilineTextbox: { var value = string.Join(",", selectedItems.Select(x => x.Value)); if (value.HasValue()) { selection.AddAttributeValue(pva.Id, value); } } break; case AttributeControlType.Datepicker: var firstItemDate = selectedItems.FirstOrDefault()?.Date; if (firstItemDate.HasValue) { selection.AddAttributeValue(pva.Id, firstItemDate.Value.ToString("D")); } break; case AttributeControlType.FileUpload: if (getFilesFromRequest) { var files = _httpContextAccessor?.HttpContext?.Request?.Form?.Files; if (files?.Any() ?? false) { var postedFile = files[ProductVariantQueryItem.CreateKey(productId, bundleItemId, pva.ProductAttributeId, pva.Id)]; if (postedFile != null && postedFile.FileName.HasValue()) { if (postedFile.Length > _catalogSettings.Value.FileUploadMaximumSizeBytes) { warnings.Add(T("ShoppingCart.MaximumUploadedFileSize", (int)(_catalogSettings.Value.FileUploadMaximumSizeBytes / 1024))); } else { var download = new Download { DownloadGuid = Guid.NewGuid(), UseDownloadUrl = false, DownloadUrl = string.Empty, UpdatedOnUtc = DateTime.UtcNow, EntityId = productId, EntityName = "ProductAttribute" }; using var stream = postedFile.OpenReadStream(); await _downloadService.Value.InsertDownloadAsync(download, stream, postedFile.FileName); selection.AddAttributeValue(pva.Id, download.DownloadGuid.ToString()); } } } } else if (Guid.TryParse(selectedItems.FirstOrDefault()?.Value, out var downloadGuid) && downloadGuid != Guid.Empty) { var download = await _db.Downloads.Where(x => x.DownloadGuid == downloadGuid).FirstOrDefaultAsync(); if (download != null) { if (download.IsTransient) { download.IsTransient = false; await _db.SaveChangesAsync(); } selection.AddAttributeValue(pva.Id, download.DownloadGuid.ToString()); } } break; } } return(selection, warnings); }
// TODO: (mg) (core) Check caller's return value handling of MaterializeProductVariantAttributeValuesAsync (now returns IList instead of ICollection). public virtual async Task <IList <ProductVariantAttributeValue> > MaterializeProductVariantAttributeValuesAsync(ProductVariantAttributeSelection selection) { Guard.NotNull(selection, nameof(selection)); var ids = selection.GetAttributeValueIds(); if (!ids.Any()) { return(new List <ProductVariantAttributeValue>()); } var cacheKey = ATTRIBUTEVALUES_BY_IDS_KEY + string.Join(",", ids); var result = await _requestCache.GetAsync(cacheKey, async() => { // Only consider values of list control types. Otherwise for instance text entered in a text-box is misinterpreted as an attribute value id. var query = _db.ProductVariantAttributeValues .Include(x => x.ProductVariantAttribute) .ThenInclude(x => x.ProductAttribute) .AsNoTracking() .ApplyValueFilter(ids); return(await query.ToListAsync()); }); // That's what the old ported code did: //if (selection?.AttributesMap?.Any() ?? false) //{ // var pvaIds = selection.AttributesMap.Select(x => x.Key).ToArray(); // if (pvaIds.Any()) // { // var attributes = await _db.ProductVariantAttributes // .AsNoTracking() // .AsCaching(ProductAttributesCacheDuration) // .Where(x => pvaIds.Contains(x.Id)) // .OrderBy(x => x.DisplayOrder) // .ToListAsync(); // var valueIds = GetAttributeValueIds(attributes, selection).ToArray(); // var values = await _db.ProductVariantAttributeValues // .AsNoTracking() // .AsCaching(ProductAttributesCacheDuration) // .ApplyValueFilter(valueIds) // .ToListAsync(); // return values; // } //} return(result); }
public virtual async Task <string> FormatAttributesAsync( ProductVariantAttributeSelection selection, Product product, Customer customer = null, string separator = "<br />", bool htmlEncode = true, bool includePrices = true, bool includeProductAttributes = true, bool includeGiftCardAttributes = true, bool includeHyperlinks = true, ProductBatchContext batchContext = null) { Guard.NotNull(selection, nameof(selection)); Guard.NotNull(product, nameof(product)); customer ??= _workContext.CurrentCustomer; using var pool = StringBuilderPool.Instance.Get(out var result); if (includeProductAttributes) { var languageId = _workContext.WorkingLanguage.Id; var attributes = await _productAttributeMaterializer.MaterializeProductVariantAttributesAsync(selection); var attributesDic = attributes.ToDictionary(x => x.Id); // Key: ProductVariantAttributeValue.Id, value: calculated attribute price adjustment. var priceAdjustments = includePrices && _catalogSettings.ShowVariantCombinationPriceAdjustment ? await _priceCalculationService.CalculateAttributePriceAdjustmentsAsync(product, selection, 1, _priceCalculationService.CreateDefaultOptions(false, customer, null, batchContext)) : new Dictionary <int, CalculatedPriceAdjustment>(); foreach (var kvp in selection.AttributesMap) { if (!attributesDic.TryGetValue(kvp.Key, out var pva)) { continue; } foreach (var value in kvp.Value) { var valueStr = value.ToString().EmptyNull(); var pvaAttribute = string.Empty; if (pva.IsListTypeAttribute()) { var pvaValue = pva.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == valueStr.ToInt()); if (pvaValue != null) { pvaAttribute = "{0}: {1}".FormatInvariant( pva.ProductAttribute.GetLocalized(x => x.Name, languageId), pvaValue.GetLocalized(x => x.Name, languageId)); if (includePrices) { if (_shoppingCartSettings.ShowLinkedAttributeValueQuantity && pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage && pvaValue.Quantity > 1) { pvaAttribute = pvaAttribute + " × " + pvaValue.Quantity; } if (priceAdjustments.TryGetValue(pvaValue.Id, out var adjustment)) { if (adjustment.Price > 0) { pvaAttribute += $" (+{adjustment.Price})"; } else if (adjustment.Price < 0) { pvaAttribute += $" (-{adjustment.Price * -1})"; } } } if (htmlEncode) { pvaAttribute = pvaAttribute.HtmlEncode(); } } } else if (pva.AttributeControlType == AttributeControlType.MultilineTextbox) { string attributeName = pva.ProductAttribute.GetLocalized(x => x.Name, languageId); pvaAttribute = "{0}: {1}".FormatInvariant( htmlEncode ? attributeName.HtmlEncode() : attributeName, HtmlUtils.ConvertPlainTextToHtml(valueStr.HtmlEncode())); } else if (pva.AttributeControlType == AttributeControlType.FileUpload) { if (Guid.TryParse(valueStr, out var downloadGuid) && downloadGuid != Guid.Empty) { var download = await _db.Downloads .AsNoTracking() .Include(x => x.MediaFile) .Where(x => x.DownloadGuid == downloadGuid) .FirstOrDefaultAsync(); if (download?.MediaFile != null) { var attributeText = string.Empty; var fileName = htmlEncode ? download.MediaFile.Name.HtmlEncode() : download.MediaFile.Name; if (includeHyperlinks) { // TODO: (core) add a method for getting URL (use routing because it handles all SEO friendly URLs). var downloadLink = _webHelper.GetStoreLocation(false) + "download/getfileupload/?downloadId=" + download.DownloadGuid; attributeText = $"<a href=\"{downloadLink}\" class=\"fileuploadattribute\">{fileName}</a>"; } else { attributeText = fileName; } string attributeName = pva.ProductAttribute.GetLocalized(a => a.Name, languageId); pvaAttribute = "{0}: {1}".FormatInvariant( htmlEncode ? attributeName.HtmlEncode() : attributeName, attributeText); } } } else { // TextBox, Datepicker pvaAttribute = "{0}: {1}".FormatInvariant(pva.ProductAttribute.GetLocalized(x => x.Name, languageId), valueStr); if (htmlEncode) { pvaAttribute = pvaAttribute.HtmlEncode(); } } result.Grow(pvaAttribute, separator); } } } if (includeGiftCardAttributes && product.IsGiftCard) { var gci = selection.GiftCardInfo; if (gci != null) { // Sender. var giftCardFrom = product.GiftCardType == GiftCardType.Virtual ? (await _localizationService.GetResourceAsync("GiftCardAttribute.From.Virtual")).FormatInvariant(gci.SenderName, gci.SenderEmail) : (await _localizationService.GetResourceAsync("GiftCardAttribute.From.Physical")).FormatInvariant(gci.SenderName); // Recipient. var giftCardFor = product.GiftCardType == GiftCardType.Virtual ? (await _localizationService.GetResourceAsync("GiftCardAttribute.For.Virtual")).FormatInvariant(gci.RecipientName, gci.RecipientEmail) : (await _localizationService.GetResourceAsync("GiftCardAttribute.For.Physical")).FormatInvariant(gci.RecipientName); if (htmlEncode) { giftCardFrom = giftCardFrom.HtmlEncode(); giftCardFor = giftCardFor.HtmlEncode(); } result.Grow(giftCardFrom, separator); result.Grow(giftCardFor, separator); } } return(result.ToString()); }
// TODO: (mg) (core) Check whether IProductAttributeMaterializer.PrefetchProductVariantAttributes is still required. // Looks like it can be done by MaterializeProductVariantAttributeValuesAsync. public virtual async Task <IList <ProductVariantAttribute> > MaterializeProductVariantAttributesAsync(ProductVariantAttributeSelection selection) { Guard.NotNull(selection, nameof(selection)); var pvaIds = selection.AttributesMap .Select(x => x.Key) .ToArray(); if (pvaIds.Any()) { var query = _db.ProductVariantAttributes .Include(x => x.ProductAttribute) .Include(x => x.ProductVariantAttributeValues) .AsNoTracking() .AsCaching(ProductAttributesCacheDuration) .Where(x => pvaIds.Contains(x.Id)); var attributes = await query.ToListAsync(); return(attributes.OrderBySequence(pvaIds).ToList()); } return(new List <ProductVariantAttribute>()); }
public virtual async Task <CombinationAvailabilityInfo> IsCombinationAvailableAsync( Product product, IEnumerable <ProductVariantAttribute> attributes, IEnumerable <ProductVariantAttributeValue> selectedValues, ProductVariantAttributeValue currentValue) { if (product == null || _performanceSettings.MaxUnavailableAttributeCombinations <= 0 || !(selectedValues?.Any() ?? false)) { return(null); } // Get unavailable combinations. var unavailableCombinations = await _cache.GetAsync(UNAVAILABLE_COMBINATIONS_KEY.FormatInvariant(product.Id), async o => { o.ExpiresIn(TimeSpan.FromMinutes(10)); var data = new Dictionary <string, CombinationAvailabilityInfo>(); var query = _db.ProductVariantAttributeCombinations .AsNoTracking() .Where(x => x.ProductId == product.Id); if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) { query = query.Where(x => !x.IsActive || (x.StockQuantity <= 0 && !x.AllowOutOfStockOrders)); } else { query = query.Where(x => !x.IsActive); } // Do not proceed if there are too many unavailable combinations. var unavailableCombinationsCount = await query.CountAsync(); if (unavailableCombinationsCount <= _performanceSettings.MaxUnavailableAttributeCombinations) { var pager = query.ToFastPager(); while ((await pager.ReadNextPageAsync <ProductVariantAttributeCombination>()).Out(out var combinations)) { foreach (var combination in combinations) { var selection = new ProductVariantAttributeSelection(combination.RawAttributes); if (selection.AttributesMap.Any()) { // <ProductVariantAttribute.Id>:<ProductVariantAttributeValue.Id>[,...] var valuesKeys = selection.AttributesMap .OrderBy(x => x.Key) .Select(x => $"{x.Key}:{string.Join(",", x.Value.OrderBy(y => y))}"); data[string.Join("-", valuesKeys)] = new CombinationAvailabilityInfo { IsActive = combination.IsActive, IsOutOfStock = combination.StockQuantity <= 0 && !combination.AllowOutOfStockOrders }; } } } } return(data); }); if (!unavailableCombinations.Any()) { return(null); } using var pool = StringBuilderPool.Instance.Get(out var builder); var selectedValuesMap = selectedValues.ToMultimap(x => x.ProductVariantAttributeId, x => x); if (attributes == null || currentValue == null) { // Create key to test selectedValues. foreach (var kvp in selectedValuesMap.OrderBy(x => x.Key)) { Append(builder, kvp.Key, kvp.Value.Select(x => x.Id).Distinct()); } } else { // Create key to test currentValue. foreach (var attribute in attributes.OrderBy(x => x.Id)) { IEnumerable <int> valueIds; var selectedIds = selectedValuesMap.ContainsKey(attribute.Id) ? selectedValuesMap[attribute.Id].Select(x => x.Id) : null; if (attribute.Id == currentValue.ProductVariantAttributeId) { // Attribute to be tested. if (selectedIds != null && attribute.IsMultipleChoice) { // Take selected values and append current value. valueIds = selectedIds.Append(currentValue.Id).Distinct(); } else { // Single selection attribute -> take current value. valueIds = new[] { currentValue.Id }; } } else { // Other attribute. if (selectedIds != null) { // Take selected value(s). valueIds = selectedIds; } else { // No selected value -> no unavailable combination. return(null); } } Append(builder, attribute.Id, valueIds); } } var key = builder.ToString(); //$"{!unavailableCombinations.ContainsKey(key),-5} {currentValue.ProductVariantAttributeId}:{currentValue.Id} -> {key}".Dump(); if (unavailableCombinations.TryGetValue(key, out var availability)) { return(availability); } return(null);
public virtual async Task <ProductVariantAttributeCombination> FindAttributeCombinationAsync(int productId, ProductVariantAttributeSelection selection) { if (productId == 0 || !(selection?.AttributesMap?.Any() ?? false)) { return(null); } var combinations = await _db.ProductVariantAttributeCombinations .AsNoTracking() .AsCaching(ProductAttributesCacheDuration) .Where(x => x.ProductId == productId) .Select(x => new { x.Id, x.RawAttributes }) .ToListAsync(); foreach (var combination in combinations) { if (selection.Equals(new ProductVariantAttributeSelection(combination.RawAttributes))) { return(await _db.ProductVariantAttributeCombinations.FindByIdAsync(combination.Id)); } } return(null); }
public virtual async Task <ProductVariantAttributeCombination> FindAttributeCombinationAsync(int productId, ProductVariantAttributeSelection selection) { // TODO: (core) (important) Save combination hash in table and always lookup by hash instead of iterating thru local data to find a match. if (productId == 0 || !(selection?.AttributesMap?.Any() ?? false)) { return(null); } var cacheKey = ATTRIBUTECOMBINATION_BY_IDJSON_KEY.FormatInvariant(productId, selection.AsJson()); var combination = await _requestCache.GetAsync(cacheKey, async() => { var combinations = await _db.ProductVariantAttributeCombinations .AsNoTracking() .Where(x => x.ProductId == productId) .Select(x => new { x.Id, x.RawAttributes }) .ToListAsync(); foreach (var combination in combinations) { if (selection.Equals(new ProductVariantAttributeSelection(combination.RawAttributes))) { return(await _db.ProductVariantAttributeCombinations.FindByIdAsync(combination.Id)); } } return(null); }); return(combination); }