How should I change his script so that it works for collections and not just one product in Shopify? - ruby

The script is a template that comes with the script editor app in Shopify. I need to make it work so that if you buy one product from a collection, you get another one free from that collection. This script works only for buying the same product. Here is the script:
PAID_ITEM_COUNT = 2
DISCOUNTED_ITEM_COUNT = 1
# Returns the integer amount of items that must be discounted next
# given the amount of items seen
#
def discounted_items_to_find(total_items_seen, discounted_items_seen)
Integer(total_items_seen / (PAID_ITEM_COUNT + DISCOUNTED_ITEM_COUNT) * DISCOUNTED_ITEM_COUNT) - discounted_items_seen
end
# Partitions the items and returns the items that are to be discounted.
#
# Arguments
# ---------
#
# * cart
# The cart to which split items will be added (typically Input.cart).
#
# * line_items
# The selected items that are applicable for the campaign.
#
def partition(cart, line_items)
# Sort the items by price from high to low
sorted_items = line_items.sort_by{|line_item| line_item.variant.price}.reverse
# Create an array of items to return
discounted_items = []
# Keep counters of items seen and discounted, to avoid having to recalculate on each iteration
total_items_seen = 0
discounted_items_seen = 0
# Loop over all the items and find those to be discounted
sorted_items.each do |line_item|
total_items_seen += line_item.quantity
# After incrementing total_items_seen, see if any items must be discounted
count = discounted_items_to_find(total_items_seen, discounted_items_seen)
# If there are none, skip to the next item
next if count <= 0
if count >= line_item.quantity
# If the full item quantity must be discounted, add it to the items to return
# and increment the count of discounted items
discounted_items.push(line_item)
discounted_items_seen += line_item.quantity
else
# If only part of the item must be discounted, split the item
discounted_item = line_item.split(take: count)
# Insert the newly-created item in the cart, right after the original item
position = cart.line_items.find_index(line_item)
cart.line_items.insert(position + 1, discounted_item)
# Add it to the list of items to return
discounted_items.push(discounted_item)
discounted_items_seen += discounted_item.quantity
end
end
# Return the items to be discounted
discounted_items
end
eligible_items = Input.cart.line_items.select do |line_item|
product = line_item.variant.product
!product.gift_card? && product.id == 11380899340
end
discounted_line_items = partition(Input.cart, eligible_items)
discounted_line_items.each do |line_item|
line_item.change_line_price(Money.zero, message: "Buy one Bolur, get one Bolur free")
end
Output.cart = Input.cart
I tried changing, what seems to be the relevant code:
eligible_items = Input.cart.line_items.select do |line_item|
product = line_item.variant.product
!product.gift_card? && product.id == 11380899340
end
to this:
eligible_items = Input.cart.line_items.select do |line_item|
product = line_item.variant.product
!product.gift_card? && **collection.id** == 123
end
but I get an error:
undefined method 'collection' for main (Your Cart)
undefined method'collection' for main (No Customer)

Two things here:
line_item.variant.product does not have the property collections. For that, you want to use line_item.product (docs) – which (should...see point two) expose all of the methods and properties of the product object.
However, in my attempt to do something similar to you (discount based on product) I tried iterating over line_item.variant – and am always hitting the error of undefined method 'product' for #<LineItem:0x7f9c97f9fff0>. Which I interpret as "line_items accessed in cart scripts can only be at the variant level".
So, I wonder if this is because the cart only contains variants (product/color/size) – so we aren't actually able to access the line_items by product, and only by variant.
I tired iterating over line_item.product_id, which also throws a similar error. I think we just have to try to do some hacky thing at the variant level.
I am going to see if I can access the product by the variant ID...back to the docs!

You actually can't do a collection, so you'd need to modify the script to work with a product type or tags. That script will need to be heavily modified to work for a number of products and not multiples of the same

Related

Sort shopping list based on previous shopping trips

I want to sort a shopping list based on the order items were checked off in previous shopping trips. For example I go to the store and shop Apples, Bananas and Eggs.
Next I go to the store I shop Avocados and Tomatos and Apples. For my next trip the application sorts Avocados, Tomatos and Apples all before Eggs f.e.
I found this post talking about topological sorting: How to sort a shopping list based on previous pick order?.
However I am not sure how this should work since in theory I could have cycles (A user could theoretically check off apples and then bananas and the next time bananas are checked off before apples).
Could you guide me on how to tackle this problem?
Kind regards
I assume:
Past item orderings should guide ordering the current order.
Any new items appear after any items ordered historically.
Orderings further back in time should have less impact than more recent orderings.
My idea is to assign weights to items seen in past orders based on:
Their position in a historic ordering.
How old that odering is.
The weightings might need adjusting, but, using data from that other question you link to, the Python code below does create orderings based on historic orderings:
from collections import defaultdict
shop = [['Toothpaste', 'Bread', 'Meat', 'Vegetables', 'Milk', 'Ice cream'], # last
['CDs', 'Bread', 'Fruit', 'Vegetables', 'Juice', 'Sugar', 'Chocolates'], # last-but-1
['Meat', 'Juice', 'Milk', 'Sugar']] # last-but-2
def weight_from_index(idx: int) -> float | int:
"Items to the left are of LOwer wt and will sort first."
return idx + 1
def historic_multiplier(idy: int) -> float:
"Older rows have larger multipliers and so are of lower overall weight."
return (idy + 1)**1
def shopping_weights(history: list[list[str]]) -> dict[str, int | float]:
"Weight for items from historic shops."
item2weight = defaultdict(float)
for y, hist in enumerate(history):
for x, item in enumerate(hist):
item2weight[item] += historic_multiplier(y) * weight_from_index(x)
return dict(item2weight)
def order_items(items: list[str], weights) -> list[str]:
wts = weights.copy()
new_items = set(items) - set(wts)
# New items last, but in given order otherwise
max_wt = max(wts.values())
for itm in new_items:
wts[itm] = max_wt + 1 + items.index(itm)
return sorted(items, key = lambda i: wts[i])
item_weights = shopping_weights(shop)
new_shop = ['Curry', 'Vegetables', 'Eggs', 'Milk', 'CDs', 'Meat']
new_order = order_items(new_shop, item_weights)
print(new_order)
# ['CDs', 'Meat', 'Vegetables', 'Milk', 'Curry', 'Eggs']
# Update the historic item orders
shop.insert(0, new_order)

Lua adding and filtering items in a table

I am writing a WoW addon that will track and filter a list of items.
Items that are not already on the list are added; items that are already listed should not be added.
The issue I am getting is my check function does not consistently prevent duplicate items from being added to the list.
Adding item A the first time works fine, trying to re-add it again if it was the last thing added is correctly not re-added.
Adding item B the first time works fine, trying to re-add it again if it was the last thing added is correctly not re-added.
The problem is that if I try to re-add item A when it was not the last thing added it incorrectly re-adds the item to the list; so essentially I can re-add items as long as they were not the last item to be added.
Here is a gif that shows you what is happening;
And here are my two functions.
I have also linked my full lua code here and the toc here.
-- check if item is already listed
local function isItemOnList(incomingItemID)
for k, v in pairs(AAAGlobalItemLinkList) do
if v.itemID == incomingItemID then
print(v.itemID, ':matched:', incomingItemID)
return true
end
print('no match', incomingItemID)
return false
end
end
-- add item link to list function
local function addItemLinkToList()
local cursorItemType, cursorItemID, cursorItemLink = GetCursorInfo()
print(cursorItemID)
if not isItemOnList(cursorItemID) then
local itemName,
itemLink,
itemRarity,
itemLevel,
itemMinLevel,
itemType,
itemSubType,
itemStackCount,
itemEquipLoc,
iconFileDataID,
itemSellPrice,
itemClassID,
itemSubClassID,
bindType,
expacID,
itemSetID,
isCraftingReagent = GetItemInfo(cursorItemID)
tempItemList = {
itemID = cursorItemID,
itemName = itemName,
itemLink = itemLink,
itemRarity = itemRarity,
itemLevel = itemLevel,
itemMinLevel = itemMinLevel,
itemType = itemType,
itemSubType = itemSubType,
itemStackCount = itemStackCount,
itemEquipLoc = itemEquipLoc,
iconFileDataID = iconFileDataID,
itemSellPrice = itemSellPrice,
itemClassID = itemClassID,
itemSubClassID = itemSubClassID,
bindType = bindType,
expacID = expacID,
itemSetID = itemSetID,
isCraftingReagent = isCraftingReagent
}
table.insert(AAAGlobalItemLinkList, 1, tempItemList)
print(cursorItemLink, 'added to list')
else
print(cursorItemLink, 'already listed')
end
updateScrollFrame()
ClearCursor()
end
-- update scroll frames function
local function updateScrollFrame()
wipe(listItems)
for index = 1, #AAAGlobalItemLinkList do
--if testItemRarity(AAAGlobalItemLinkList[index]) then
table.insert(listItems, AAAGlobalItemLinkList[index]['itemLink'])
--end
end
FauxScrollFrame_Update(testingScrollFrame, #listItems, NumberOfButtons, HeightOfButtons)
for index = 1, NumberOfButtons do
local offset = index + FauxScrollFrame_GetOffset(testingScrollFrame)
local button = testingScrollFrame.buttons[index]
if index > #listItems then
button:SetText('')a
button.index = nil
else
button.index = offset
--local itemName, itemLink = GetItemInfo(listItems[offset])
button:SetText(listItems[offset])
end
end
end
I am not getting any errors at all.
I have also linked my full lua code here and the toc here.
Hopefully someone can explain how I have messed up and can also point me in the right direction to fix it.
in your function isItemOnList you return false inside for loop, so loop cant be executed for more than 1 iteration. You should put return false outside of for loop:
-- check if item is already listed
local function isItemOnList(incomingItemID)
for k, v in pairs(AAAGlobalItemLinkList) do
if v.itemID == incomingItemID then
print(v.itemID, ':matched:', incomingItemID)
return true
end
end
print('no match', incomingItemID)
return false
end
also you can do without return , sonil will be returned by default , and for if checks nil is the same as false

Kaminari combine two collections and place heading in between

I have a page with two collections of items.
def index
#collection1 = Model.collection1
#collection2 = Model.collection2
end
I'm aware of how to combine them together to paginate with Kaminari, however I need to place a <h1>collection 2</h1> header before the items of #collection2 starts (if any).
The page would be something like this:
..Item from collection 1..
..Item from collection 1..
..Item from collection 1..
<h1>collection 2</h1>
..Item from collection 2..
..Item from collection 2..
Im having a hard time figuring out a way to do this. Any idea?
I have come up with the following solution.
def index
#collection1 = Model.collection1
#collection2 = Model.collection2
#collection1_paginated = #collection1.page(params[:page])
#collection2_paginated = Kaminari.paginate_array(#collection1 + #collection2).page(params[:page])
.padding(#collection1_paginated.size)
.take(#collection1_paginated.limit_value - #collection1_paginated.size)
#collection1_or_collection2 = Kaminari.paginate_array(#collection1 + #collection2).page(params[:page])
end
And in the views you can do:
= render partial: 'model', collection: #collection1_paginated
- display_collection_2_heading?
h1 collection 2
= render partial: 'model', collection: #collection2_paginated
= paginate #collection1_or_collection2
To display the Collection 2 heading create the following helper.
def display_collection_2_heading?
#collection1.present? && ((#collection1.count / #collection1_paginated.limit_value)+1) == #collection1_paginated.current_page
end
This helpers makes sure the heading only goes one only time after the last item from #collection1.

Custom filter in rails ActiveAdmin using ransack

I have created an activeadmin filter that has the following choices on filtering the table data in its drop-down menu.
Choice A
Choice B
Choice C
Choice D
I want to add a fifth choice F that would either choice B or C (that is the results of both B and C).
Choice A
Choice B
Choice C
Choice D
Choice F = either B or C
The choices are stored in a table called Coupons in its title field (i created a row having the choice F). So far, I tried to employ a ransack to do the trick, without being quite sure.
//app/admin/user.rb
filter :has_right_choice, label: 'Filter by the right choice', collection: Coupon.all, as: :select
//app/models/user.rb
ransacker :has_right_choice, :formatter => ->(title) {
if (title == 'Choice F')
Coupon.all.where("title = 'Choice B' OR title = 'Choice C'")
else
Coupon.all
end
} do |parent|
parent.table[:id]
end
But my solution doesn't work. Is there a better approach instead of ransack ? If not, any suggestion ?
==========================================================================
EDIT: solution
Choice A # lets say that this choice has id = 1 in Coupons table
Choice B # id = 2
Choice C # id = 3
Choice D # id = 4
Choice F # id = 5
Also lets say that Users table has a field choice_id that refers to the Choices table.
We modify the faulty code as follows:
//app/admin/user.rb
filter :choice_id_in, label: 'Filter by the right choice', collection: Coupon.all, as: :select
//app/models/user.rb
ransacker :choice_id,
:formatter => -> (coupon_id) {
if (coupon_id == "5")
ids = User.all.where("choice_id = 3 OR choice_id = 4").map(&:id)
else
ids = User.where("choice_id = ?", coupon_id).map(&:id)
end
(ids.empty?) ? ids << 0: ids #activeadmin translates the queries into IN operator, may get syntax error if empty
# id = 0 is non-existent in Users as id >= 1
ids #maybe is not needed
} do |parent|
parent.table[:id]
end
Suggestions for improvement are welcome. Thanks #berker for the guidance.

Django Forms, ModelChoiceField using RadioSelect widget, Grouped by FK

The challenge, output a radio select in nested <ul></ul>, grouped by the task fk.
ie.
class Category(models.Model):
# ...
class Task(models.Model):
# ...
category = models.ForeignKey(Category)
# ...
forms.py
class ActivityForm(forms.ModelForm):
# ...
task = forms.ModelChoiceField(
queryset = Task.objects.all(),
widget = RadioSelectGroupedByFK
)
widgets.py
class RadioFieldRendererGroupedByFK(RadioFieldRenderer):
"""
An object used by RadioSelect to enable customization of radio widgets.
"""
#def __init__(self, attrs=None):
# Need a radio select for each?? Just an Idea.
#widgets = (RadioSelect(attrs=attrs), RadioSelect(attrs=attrs))
#super(RadioFieldRendererGroupedByFK, self).__init__(widgets, attrs)
def render(self):
"""Outputs nested <ul> for this set of radio fields."""
return mark_safe(
#### Somehow the crux of the work happens here? but how to get the
#### right context??
u'<ul>\n%s\n</ul>' % u'\n'.join(
[u'<li>%s</li>' % force_unicode(w) for w in self]
)
)
class RadioSelectGroupedByFK(forms.RadioSelect):
renderer = RadioFieldRendererGroupedByFK
Best thanks!!
itertools.groupby() is perfect for this. I'll assume that Task and Category each have a name attribute you want them sorted by. I don't know the Django APIs, so you'll probably want to move sorting into the db query, and you'll need to look at the widget's attributes to figure out how to access the task objects, but here's the basic formula:
from itertools import groupby
# sort first since groupby only groups adjacent items
tasks.sort(key=lambda task: (task.category.name, task.name))
for category, category_tasks in groupby(tasks, key=lambda task: task.category):
print '%s:' % category.name
for task in category_tasks:
print '* %s' % task.name
This should print a list like:
Breakfast foods:
* eggs
* spam
Dinner foods:
* spam
* spam
* spam
HTH

Resources