Skip to content

Commit a07f95f

Browse files
Merge pull request #297 from AndreWohnsland/dev
Allow virgin only cocktail without all alcoholic ingredients
2 parents 963f33f + c041df9 commit a07f95f

File tree

13 files changed

+157
-57
lines changed

13 files changed

+157
-57
lines changed

src/api/internal/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def map_cocktail(cocktail: Optional[DBCocktail], scale: bool = True) -> Optional
2828
amount=cocktail.adjusted_amount,
2929
enabled=cocktail.enabled,
3030
virgin_available=cocktail.virgin_available,
31+
only_virgin=cocktail.only_virgin,
3132
# ingredient hand is if it is currently not on a bottle
3233
ingredients=[
3334
CocktailIngredient(

src/api/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Cocktail(BaseModel):
3939
amount: int
4040
enabled: bool
4141
virgin_available: bool
42+
only_virgin: bool
4243
ingredients: list[CocktailIngredient]
4344
image: str
4445
default_image: str

src/api/routers/cocktails.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,15 @@ async def prepare_cocktail(
6666
if cocktail is None:
6767
message = DH.get_translation("element_not_found", element_name=f"Cocktail (id={cocktail_id})")
6868
raise HTTPException(status_code=404, detail=message)
69+
cocktail.scale_cocktail(request.volume, factor)
6970
# need to check if the cocktail is possible
7071
# this can happen if there is no ui guidance, e.g. only a direct post of an id
72+
# the cocktail might only be possible in the virgin version, but the user requested a non-virgin version
7173
hand_ids = DBC.get_available_ids()
72-
if not cocktail.is_possible(hand_ids, cfg.MAKER_MAX_HAND_INGREDIENTS):
74+
if not cocktail.is_possible(hand_ids, cfg.MAKER_MAX_HAND_INGREDIENTS) or (
75+
cocktail.only_virgin and not cocktail.is_virgin
76+
):
7377
raise HTTPException(status_code=400, detail=DH.get_translation("cocktail_not_possible"))
74-
cocktail.scale_cocktail(request.volume, factor)
7578
result, msg, ingredient = maker.validate_cocktail(cocktail)
7679
if result != PrepareResult.VALIDATION_OK:
7780
# we need also provide the frontend with the ingredient id that caused the error

src/models.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class Cocktail:
6464
enabled: bool
6565
virgin_available: bool
6666
ingredients: list[Ingredient]
67+
only_virgin: bool = False
6768
adjusted_alcohol: float = 0
6869
adjusted_amount: int = 0
6970
adjusted_ingredients: list[Ingredient] = field(default_factory=list, init=False)
@@ -80,12 +81,22 @@ def __post_init__(self):
8081
@property
8182
def handadds(self):
8283
"""Returns a list of all handadd Ingredients."""
83-
return [x for x in self.adjusted_ingredients if x.bottle is None]
84+
return [x for x in self.adjusted_ingredients if x.bottle is None and x.amount > 0]
8485

8586
@property
8687
def machineadds(self):
8788
"""Returns a list of all machine Ingredients."""
88-
return [x for x in self.adjusted_ingredients if x.bottle is not None]
89+
return [x for x in self.adjusted_ingredients if x.bottle is not None and x.amount > 0]
90+
91+
@property
92+
def virgin_handadds(self):
93+
"""Returns a list of all non-alcoholic handadd Ingredients."""
94+
return [x for x in self.handadds if x.alcohol == 0]
95+
96+
@property
97+
def virgin_machineadds(self):
98+
"""Returns a list of all non-alcoholic machine Ingredients."""
99+
return [x for x in self.machineadds if x.alcohol == 0]
89100

90101
@property
91102
def is_virgin(self):
@@ -94,21 +105,53 @@ def is_virgin(self):
94105

95106
def is_possible(self, hand_available: list[int], max_hand_ingredients: int):
96107
"""Return if the recipe is possible with given additional hand add ingredients."""
97-
machine = self.machineadds
108+
self.only_virgin = False
109+
if self._is_normal_cocktail_possible(hand_available, max_hand_ingredients):
110+
return True
111+
if self.virgin_available and self._is_virgin_cocktail_possible(hand_available, max_hand_ingredients):
112+
self.only_virgin = True
113+
return True
114+
return False
115+
116+
def _has_all_ingredients(
117+
self,
118+
hand_available: list[int],
119+
max_hand_ingredients: int,
120+
machine_adds: list[Ingredient],
121+
hand_adds: list[Ingredient],
122+
) -> bool:
123+
"""Return if the recipe is possible with given additional hand add ingredients."""
98124
# If machine got not at least 1 add (=all handadd) return false
99125
# We don't want to let the user do all the work
100-
if len(machine) < 1:
126+
if len(machine_adds) < 1:
101127
return False
102-
hand = self.handadds
103128
# if the number of hand adds is higher than the allowed hand adds, return false
104-
if len(hand) > max_hand_ingredients:
129+
if len(hand_adds) > max_hand_ingredients:
105130
return False
106-
for ing in machine:
131+
for ing in machine_adds:
107132
if ing.bottle is None:
108133
return False
109-
hand_id = {x.id for x in hand}
134+
hand_id = {x.id for x in hand_adds}
110135
return not hand_id - set(hand_available)
111136

137+
def _is_normal_cocktail_possible(self, hand_available: list[int], max_hand_ingredients: int):
138+
"""Check if the normal (alcoholic) cocktail is possible."""
139+
return self._has_all_ingredients(
140+
hand_available,
141+
max_hand_ingredients,
142+
self.machineadds,
143+
self.handadds,
144+
)
145+
146+
def _is_virgin_cocktail_possible(self, hand_available: list[int], max_hand_ingredients: int):
147+
"""Check if the virgin cocktail is possible."""
148+
return self._has_all_ingredients(
149+
hand_available,
150+
max_hand_ingredients,
151+
self.virgin_machineadds,
152+
self.virgin_handadds,
153+
)
154+
112155
def enough_fill_level(self) -> Optional[Ingredient]:
113156
"""Check if the needed volume is there.
114157

src/ui/cocktail_view.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ def generate_image_block(cocktail: Cocktail | None, mainscreen: MainScreen):
4646
header_font_size = round(square_size / 15.8)
4747
header_height = round(square_size / 6.3)
4848
single_ingredient_label = UI_LANGUAGE.get_translation("label_single_ingredient", "main_window")
49-
name_label = cocktail.name if cocktail is not None else single_ingredient_label
49+
name_label = single_ingredient_label
50+
if cocktail is not None:
51+
prefix = "V. " if cocktail.only_virgin else ""
52+
name_label = f"{prefix}{cocktail.name}"
5053
button = create_button(
5154
name_label,
5255
font_size=header_font_size,

src/ui/setup_cocktail_selection.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def set_cocktail(self, cocktail: Cocktail):
7272
# and when it changes, the gui elements will not update
7373
db_cocktail = DB_COMMANDER.get_cocktail(cocktail.id)
7474
if db_cocktail is not None:
75+
# need to revaluate if there is only the virgin version available
76+
db_cocktail.is_possible(DB_COMMANDER.get_available_ids(), cfg.MAKER_MAX_HAND_INGREDIENTS)
7577
cocktail = db_cocktail
7678
self.cocktail = cocktail
7779
self._set_image()
@@ -108,21 +110,14 @@ def update_cocktail_data(self):
108110
self.prepare_button.setText(
109111
UI_LANGUAGE.get_translation("prepare_button", "cocktail_selection", amount=amount, unit=cfg.EXP_MAKER_UNIT)
110112
)
111-
self.LAlkoholname.setText(self.cocktail.name)
113+
virgin_prefix = "V. " if self.cocktail.is_virgin else ""
114+
self.LAlkoholname.setText(f"{virgin_prefix}{self.cocktail.name}")
112115
display_volume = self._decide_rounding(amount * cfg.EXP_MAKER_FACTOR, 20)
113116
self.LMenge.setText(f"{display_volume} {cfg.EXP_MAKER_UNIT}")
114117
self.LAlkoholgehalt.setText(f"{self.cocktail.adjusted_alcohol:.1f}%")
115118
display_data = self.cocktail.machineadds
116119
hand = self.cocktail.handadds
117-
# remove ingredients that have amount of 0
118-
hand = [ing for ing in hand if ing.amount > 0]
119-
display_data = [ing for ing in display_data if ing.amount > 0]
120-
# Activates or deactivates the virgin checkbox, depending on the virgin flag
121-
self.virgin_checkbox.setEnabled(self.cocktail.virgin_available)
122-
# Styles does not work on strikeout, so we use internal qt things
123-
# To be precise, they do work at start, but does not support dynamic changes
124-
set_strike_through(self.virgin_checkbox, not self.cocktail.virgin_available)
125-
# when there is handadd, also build some additional data
120+
self._apply_virgin_setting()
126121
if hand:
127122
display_data.extend([Ingredient(-1, "", 0, 0, 0, False, 100, 100), *hand])
128123
fields_ingredient = self.get_labels_maker_ingredients()
@@ -153,6 +148,16 @@ def update_cocktail_data(self):
153148
ingredient_name = ing.name
154149
field_ingredient.setText(f"{ingredient_name} ")
155150

151+
def _apply_virgin_setting(self):
152+
# hide the strong/weak buttons, since they are not needed
153+
self.increase_alcohol.setVisible(not self.cocktail.only_virgin)
154+
self.decrease_alcohol.setVisible(not self.cocktail.only_virgin)
155+
can_change_virgin = self.cocktail.virgin_available and not self.cocktail.only_virgin
156+
self.virgin_checkbox.setEnabled(can_change_virgin)
157+
# Styles does not work on strikeout, so we use internal qt things
158+
# To be precise, they do work at start, but does not support dynamic changes
159+
set_strike_through(self.virgin_checkbox, not can_change_virgin)
160+
156161
def _decide_rounding(self, val: float, threshold=8):
157162
"""Return the right rounding for numbers displayed to the user."""
158163
if val >= threshold:
@@ -165,14 +170,17 @@ def clear_recipe_data_maker(self):
165170
self.LAlkoholgehalt.setText("")
166171
self.LAlkoholname.setText(UI_LANGUAGE.get_cocktail_dummy())
167172
self.LMenge.setText("")
168-
self.virgin_checkbox.setChecked(False)
173+
self.virgin_checkbox.setChecked(self.cocktail.only_virgin)
169174
for field_ingredient, field_volume in zip(self.get_labels_maker_ingredients(), self.get_labels_maker_volume()):
170175
field_ingredient.setText("")
171176
field_volume.setText("")
172177

173178
def reset_alcohol_factor(self):
174179
"""Set the alcohol slider to default (100%) value."""
175-
shared.alcohol_factor = 1.0
180+
if self.cocktail.only_virgin:
181+
shared.alcohol_factor = 0.0
182+
else:
183+
shared.alcohol_factor = 1.0
176184

177185
def adjust_maker_label_size_cocktaildata(self):
178186
"""Adjust the font size for larger screens."""

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def db_commander():
1616
db_commander.insert_new_ingredient("Tequila", 38, 750, False, 100, 200, "ml") # id 5
1717
# This one will not be added to the machine / available
1818
db_commander.insert_new_ingredient("Fanta", 0, 1000, False, 100, 50, "ml") # id 6
19+
# Adding a new alcoholic ingredient that is NOT available (neither via machine nor hand)
20+
db_commander.insert_new_ingredient("Vodka", 40, 1000, False, 100, 150, "ml") # id 7
1921

2022
# create bottle 1-24
2123
with db_commander.session_scope() as session:
@@ -31,6 +33,8 @@ def db_commander():
3133
db_commander.insert_new_recipe("With Handadd", 20, 250, True, False, [(1, 50, 1), (4, 200, 2)]) # id 3
3234
# not all ingredients are available
3335
db_commander.insert_new_recipe("Not Available", 10, 250, True, False, [(4, 50, 1), (6, 200, 2)]) # id 4
36+
# This one should be possible only in its virgin form
37+
db_commander.insert_new_recipe("Virgin Only Possible", 12, 300, True, True, [(7, 100, 1), (2, 200, 2)]) # id 5
3438

3539
# Assign ingredients to bottles
3640
db_commander.set_bottle_at_slot("White Rum", 1)

tests/test_database_commander.py

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,52 @@ def test_get_cocktail(self, db_commander: DatabaseCommander):
3232
def test_get_all_cocktails(self, db_commander: DatabaseCommander):
3333
"""Test the get_all_cocktails method."""
3434
cocktails = db_commander.get_all_cocktails()
35-
assert len(cocktails) == 4
36-
cocktail = cocktails[0]
37-
assert cocktail.name == "Cuba Libre"
38-
assert cocktail.alcohol == 11
39-
assert cocktail.amount == 290
40-
assert cocktail.enabled is True
41-
assert cocktail.virgin_available is False
42-
assert len(cocktail.ingredients) == 2
35+
assert len(cocktails) == 5
36+
37+
cuba_libre = next((c for c in cocktails if c.name == "Cuba Libre"), None)
38+
assert cuba_libre is not None
39+
assert cuba_libre.alcohol == 11
40+
assert cuba_libre.amount == 290
41+
assert cuba_libre.enabled is True
42+
assert cuba_libre.virgin_available is False
43+
assert len(cuba_libre.ingredients) == 2
4344

4445
def test_get_possible_cocktails(self, db_commander: DatabaseCommander):
4546
"""Test the get_possible_cocktails method."""
4647
possible_cocktails = db_commander.get_possible_cocktails(max_hand_ingredients=1)
47-
assert len(possible_cocktails) == 2
48-
assert possible_cocktails[0].name == "Cuba Libre"
48+
# Now we should have 3 possible cocktails: Cuba Libre, With Handadd, and Virgin Only Possible
49+
assert len(possible_cocktails) == 3
50+
# Check that Cuba Libre is in the list
51+
cuba_libre = next((c for c in possible_cocktails if c.name == "Cuba Libre"), None)
52+
assert cuba_libre is not None
53+
assert cuba_libre.only_virgin is False
54+
55+
def test_get_possible_cocktail_virgin_only(self, db_commander: DatabaseCommander):
56+
"""Test that a cocktail that can only be made in virgin form is properly flagged."""
57+
possible_cocktails = db_commander.get_possible_cocktails(max_hand_ingredients=1)
58+
59+
virgin_only = next((c for c in possible_cocktails if c.name == "Virgin Only Possible"), None)
60+
assert virgin_only is not None
61+
assert virgin_only.only_virgin is True
62+
63+
# Verify that this cocktail has an alcoholic ingredient that's not available
64+
alcoholic_ingredients = [ing for ing in virgin_only.ingredients if ing.alcohol > 0]
65+
assert len(alcoholic_ingredients) > 0
66+
67+
# Check that the virgin ingredients are available either via machine or hand
68+
virgin_ingredients = [ing for ing in virgin_only.ingredients if ing.alcohol == 0]
69+
assert len(virgin_ingredients) > 0
70+
71+
for ing in virgin_ingredients:
72+
# Either the ingredient is connected to a bottle or it's in the available handadd list
73+
assert ing.bottle is not None or ing.id in db_commander.get_available_ids()
4974

5075
def test_get_disabled_cocktails(self, db_commander: DatabaseCommander):
5176
"""Test that we can get only not enabled cocktails."""
5277
disabled_cocktails = db_commander.get_all_cocktails(status="disabled")
5378
assert len(disabled_cocktails) == 1
54-
assert disabled_cocktails[0].name == "Tequila Sunrise"
79+
tequila_sunrise = next((c for c in disabled_cocktails if c.name == "Tequila Sunrise"), None)
80+
assert tequila_sunrise is not None
5581

5682
def test_increment_recipe_counter(self, db_commander: DatabaseCommander):
5783
"""Test the increment_recipe_counter method."""
@@ -157,28 +183,31 @@ def test_get_ingredient(self, db_commander: DatabaseCommander):
157183
def test_get_all_ingredients(self, db_commander: DatabaseCommander):
158184
"""Test the get_all_ingredients method."""
159185
ingredients = db_commander.get_all_ingredients()
160-
assert len(ingredients) == 6
161-
ingredient = ingredients[1]
162-
assert ingredient.name == "Cola"
163-
assert ingredient.alcohol == 0
164-
assert ingredient.bottle_volume == 1000
165-
assert ingredient.fill_level == 0
166-
assert ingredient.hand is False
167-
assert ingredient.pump_speed == 100
186+
assert len(ingredients) == 7
187+
188+
cola = next((i for i in ingredients if i.name == "Cola"), None)
189+
assert cola is not None
190+
assert cola.alcohol == 0
191+
assert cola.bottle_volume == 1000
192+
assert cola.fill_level == 0
193+
assert cola.hand is False
194+
assert cola.pump_speed == 100
168195

169196
def test_get_all_machine_ingredients(self, db_commander: DatabaseCommander):
170197
"""Test the get_all_machine_ingredients method."""
171198
ingredients = db_commander.get_all_ingredients(get_hand=False)
172-
assert len(ingredients) == 5
173-
ingredient = ingredients[1]
174-
assert ingredient.name == "Cola"
199+
assert len(ingredients) == 6
200+
201+
cola = next((i for i in ingredients if i.name == "Cola"), None)
202+
assert cola is not None
175203

176204
def test_get_all_hand_ingredients(self, db_commander: DatabaseCommander):
177205
"""Test the get_all_hand_ingredients method."""
178206
ingredients = db_commander.get_all_ingredients(get_machine=False)
179207
assert len(ingredients) == 1
180-
ingredient = ingredients[0]
181-
assert ingredient.name == "Blue Curacao"
208+
209+
blue_curacao = next((i for i in ingredients if i.name == "Blue Curacao"), None)
210+
assert blue_curacao is not None
182211

183212
def test_get_all_ingredients_return_empty_if_both_false(self, db_commander: DatabaseCommander):
184213
"""Test the get_all_ingredients method."""

web_client/src/api/cocktails.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,12 @@ export const prepareCocktail = async (
3838
cocktail: Cocktail,
3939
volume: number,
4040
alcohol_factor: number,
41-
is_virgin: boolean,
4241
teamName?: string,
4342
): Promise<CocktailStatus> => {
4443
return axiosInstance
4544
.post<CocktailStatus>(`${cocktail_url}/prepare/${cocktail.id}`, {
4645
volume,
4746
alcohol_factor,
48-
is_virgin,
4947
selected_team: teamName,
5048
})
5149
.then((res) => res.data);

web_client/src/components/cocktail/CocktailList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const CocktailList: React.FC = () => {
5555
>
5656
<h2 className='text-center py-1 flex items-center justify-center'>
5757
{cocktail.virgin_available && <MdNoDrinks className='mr-2' />}
58+
{cocktail.only_virgin && 'V. '}
5859
{cocktail.name}
5960
</h2>
6061
<div className='relative w-full' style={{ paddingTop: '100%' }}>

0 commit comments

Comments
 (0)