[diary] add seeds stock support#16
Conversation
WalkthroughAdds a per-user SeedStock model with migration and admin, updates middleware to set missing user/profile to None, extends Telegram bot with seed-management commands (/seeds, /addseeds, /plant, planting) and welcome text, and adds a Makefile Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as Telegram User
participant Bot as Bot Handler
participant DB as Database
participant PlantSvc as Plant Creation Logic
User->>Bot: /plant <seed_stock_id>
Bot->>DB: SELECT SeedStock by id,user (select_related type,variety)
DB-->>Bot: SeedStock or not found
alt found & quantity > 0
Bot->>DB: UPDATE SeedStock SET quantity = F(quantity) - 1 WHERE id=... AND quantity>0
DB-->>Bot: rows_updated
Bot->>PlantSvc: create Plant (user,type,variety)
PlantSvc->>DB: INSERT Plant
DB-->>PlantSvc: Plant saved
Bot->>DB: SELECT SeedStock to refresh quantity
DB-->>Bot: updated quantity
Bot-->>User: confirmation with remaining seeds
else not found / empty
Bot-->>User: error message
end
sequenceDiagram
autonumber
actor User as Telegram User
participant Bot as Bot Handler
participant DB as Database
User->>Bot: /addseeds <type_slug> <quantity> <variety_slug>
Bot->>DB: SELECT PlantType WHERE slug=...
DB-->>Bot: PlantType or error
Bot->>DB: SELECT PlantVariety WHERE slug=... AND type=...
DB-->>Bot: PlantVariety or error
Bot->>DB: UPDATE SeedStock SET quantity = F(quantity) + amount WHERE user=... AND type=... AND variety=...
DB-->>Bot: rows_updated
alt rows_updated == 0
Bot->>DB: INSERT SeedStock (user,type,variety,quantity=amount)
DB-->>Bot: inserted or IntegrityError
alt IntegrityError
Bot->>DB: retry UPDATE SeedStock SET quantity = F(quantity) + amount ...
DB-->>Bot: rows_updated
end
end
Bot-->>User: confirmation with total quantity
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/bot/bot.py`:
- Around line 186-203: The seed consumption and plant creation must be made
atomic inside plant_from_seed_stock: replace the separate async
aget()/plant.asave()/stock.asave() sequence with a single transaction.atomic()
block that queries the SeedStock using select_for_update() (e.g.,
SeedStock.objects.select_for_update().select_related("type","variety").aget(...))
to lock the row, re-check stock.quantity > 0, create and save the Plant,
decrement and save stock.quantity (stock.asave(update_fields=["quantity"])) all
within the same transaction so concurrent /plant calls cannot over-consume or
leave inconsistent state.
In `@backend/diary/models.py`:
- Around line 226-230: The Meta.unique_together ("user", "type", "variety")
won't prevent duplicates when variety is NULL; update the model Meta to replace
unique_together with two explicit UniqueConstraint entries: one constrained to
rows where variety IS NOT NULL (enforce uniqueness on user+type+variety) and one
constrained to rows where variety IS NULL (enforce uniqueness on user+type
only), using django.db.models.UniqueConstraint with
condition=models.Q(variety__isnull=False) and
condition=models.Q(variety__isnull=True) respectively; modify the Meta class in
the model where unique_together is declared (class Meta) to remove
unique_together and add the two UniqueConstraint entries so the variety-null and
variety-non-null cases are enforced.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
backend/bot/bot.pybackend/diary/admin.pybackend/diary/migrations/0003_seedstock.pybackend/diary/models.py
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
268-284:⚠️ Potential issue | 🔴 CriticalMake
/plantconsume-and-create flow atomic.Line 280–281 and Line 283–284 are separate writes without transaction/row locking. Concurrent
/plantcalls can over-consume stock or leave partial state.#!/bin/bash set -euo pipefail # Show the critical section in plant_from_seed_stock. cat -n backend/bot/bot.py | sed -n '268,292p' # Confirm missing transaction/locking primitives in this handler. rg -n 'async def plant_from_seed_stock|transaction\.atomic|select_for_update|asave\(' backend/bot/bot.py -C4Expected verification result: the handler shows
Plant.asave()andSeedStock.asave()in sequence, withouttransaction.atomic/select_for_updatein that flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 268 - 284, The current consume-and-create sequence in the plant_from_seed_stock flow performs Plant.asave() and then decreases SeedStock.quantity with separate writes, which can race; wrap the lookup and both writes in an async database transaction and acquire a row lock on the SeedStock using select_for_update() (i.e., use transaction.atomic() around the block, change the initial query to use select_for_update().aget(...), create and save the Plant and decrement/save the SeedStock inside that transaction, and keep the existing update_fields=["quantity"] on stock.asave() to minimize write scope; also preserve the SeedStock.DoesNotExist handling before entering the transaction or re-check inside the lock to avoid partial state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/bot/bot.py`:
- Line 84: The welcome message in bot.py currently uses the inconsistent
placeholder "/plant <id>"; update that string to match the canonical command
format used elsewhere ("/plant <seed_stock_id>"). Locate the welcome message
literal in bot.py (the string that begins "Welcome, {user}! I'm your plant care
assistant bot...") and replace "/plant <id>" with "/plant <seed_stock_id>" so
all user-facing help text is consistent.
- Around line 223-238: The upsert for SeedStock has a race where two concurrent
requests can both see aupdate() == 0 and call acreate(), causing an
IntegrityError; to fix, wrap the SeedStock.objects.acreate(...) call used when
updated == 0 in a try/except catching django.db.IntegrityError, and in the
except block retry the update (call SeedStock.objects.filter(user=tg_user.user,
type=plant_type, variety=variety).aupdate(quantity=F("quantity") + quantity)) to
increment the existing row, then continue to load the stock via the same
select_related(...).aget(); ensure IntegrityError is imported and handled so the
second writer recovers instead of failing.
---
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 268-284: The current consume-and-create sequence in the
plant_from_seed_stock flow performs Plant.asave() and then decreases
SeedStock.quantity with separate writes, which can race; wrap the lookup and
both writes in an async database transaction and acquire a row lock on the
SeedStock using select_for_update() (i.e., use transaction.atomic() around the
block, change the initial query to use select_for_update().aget(...), create and
save the Plant and decrement/save the SeedStock inside that transaction, and
keep the existing update_fields=["quantity"] on stock.asave() to minimize write
scope; also preserve the SeedStock.DoesNotExist handling before entering the
transaction or re-check inside the lock to avoid partial state.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
268-284:⚠️ Potential issue | 🔴 CriticalMake
/plantconsume stock and create Plant in one transaction.This is still non-atomic: concurrent requests can both pass Line 276 and create multiple plants from the same remaining seed, and failures between Line 281 and Line 284 can leave inconsistent state.
🔒 Suggested fix (sync transactional unit + async wrapper)
+from asgiref.sync import sync_to_async +from django.db import transaction @@ `@dp.message`(Command("plant")) async def plant_from_seed_stock(message: Message): @@ - try: - stock = await SeedStock.objects.select_related("type", "variety").aget( - id=stock_id, user__telegramuser__id=message.from_user.id - ) - except SeedStock.DoesNotExist: - await message.answer(str(_("Seed stock not found."))) - return - - if stock.quantity <= 0: - await message.answer(str(_("This seed stock is empty."))) - return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() - - stock.quantity -= 1 - await stock.asave(update_fields=["quantity"]) + def _plant_from_stock_sync(): + with transaction.atomic(): + stock = ( + SeedStock.objects.select_for_update() + .select_related("type", "variety") + .get(id=stock_id, user__telegramuser__id=message.from_user.id) + ) + if stock.quantity <= 0: + return None, 0 + plant = Plant.objects.create( + user=stock.user, type=stock.type, variety=stock.variety + ) + stock.quantity -= 1 + stock.save(update_fields=["quantity"]) + return plant, stock.quantity + + try: + plant, remaining = await sync_to_async( + _plant_from_stock_sync, thread_sensitive=True + )() + except SeedStock.DoesNotExist: + await message.answer(str(_("Seed stock not found."))) + return + + if plant is None: + await message.answer(str(_("This seed stock is empty."))) + return @@ - plant_name=plant.name, quantity=stock.quantity + plant_name=plant.name, quantity=remaining ) ) )#!/bin/bash # Verify current implementation has no transactional lock around read/create/decrement. cat -n backend/bot/bot.py | sed -n '255,295p' rg -n 'select_for_update|transaction\.atomic|sync_to_async' backend/bot/bot.py -C2In Django 5.2 async ORM, are database transactions supported directly in async code, and what pattern is recommended for transactional sections?Reference: https://docs.djangoproject.com/en/5.2/topics/async/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 268 - 284, The read/create/decrement must be done atomically to prevent races: wrap the SeedStock fetch (using a SELECT ... FOR UPDATE) and the Plant creation/stock.quantity decrement/save inside a transactional synchronous block executed via sync_to_async; specifically, use Django's transaction.atomic plus select_for_update on the SeedStock query (the current SeedStock.objects...aget call) to lock the row, create Plant(user=stock.user, type=stock.type, variety=stock.variety) and save it, decrement stock.quantity and save with update_fields=["quantity"] all inside the atomic block, and surface SeedStock.DoesNotExist or other DB exceptions to the async caller while keeping the message.answer calls outside or after the transactional block as appropriate. Ensure you reference the existing symbols: SeedStock.objects.select_related(...).select_for_update(...), Plant(...).asave()/or .save() inside the transaction wrapper, and use sync_to_async to run the synchronous transaction.atomic section from the async handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/diary/models.py`:
- Around line 216-221: There is a contract mismatch between the /addseeds
command (bot.py parsing of variety_slug) and the SeedStock model
(SeedStock.variety FK) — choose one fix: either make SeedStock.variety nullable
by adding null=True, blank=True on the variety ForeignKey, create and apply the
migration, and replace the current unique_together ("user","type","variety")
with a DB-appropriate constraint (e.g. a Postgres UniqueConstraint with
condition to handle NULLs or adjust logic to avoid relying on uniqueness when
variety is NULL); or make variety_slug required in the /addseeds command by
changing the bot.py parsing/validation to require 4 args, remove the code path
that sets variety=None and update the command help text/usage to no longer mark
[variety_slug] as optional; reference SeedStock.variety, PlantVariety lookup
code in bot.py, and the Meta.unique_together on SeedStock when applying the
chosen change.
---
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 268-284: The read/create/decrement must be done atomically to
prevent races: wrap the SeedStock fetch (using a SELECT ... FOR UPDATE) and the
Plant creation/stock.quantity decrement/save inside a transactional synchronous
block executed via sync_to_async; specifically, use Django's transaction.atomic
plus select_for_update on the SeedStock query (the current
SeedStock.objects...aget call) to lock the row, create Plant(user=stock.user,
type=stock.type, variety=stock.variety) and save it, decrement stock.quantity
and save with update_fields=["quantity"] all inside the atomic block, and
surface SeedStock.DoesNotExist or other DB exceptions to the async caller while
keeping the message.answer calls outside or after the transactional block as
appropriate. Ensure you reference the existing symbols:
SeedStock.objects.select_related(...).select_for_update(...),
Plant(...).asave()/or .save() inside the transaction wrapper, and use
sync_to_async to run the synchronous transaction.atomic section from the async
handler.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
backend/bot/bot.pybackend/diary/migrations/0003_seedstock.pybackend/diary/models.py
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
264-281:⚠️ Potential issue | 🔴 CriticalMake
/plantstock consume + plant creation atomic.This is still split across separate async operations (read/check/create/decrement). Concurrent
/plantcalls can both pass the quantity check and over-consume stock; failures between Line 277 and Line 280 can also leave inconsistent data.Suggested fix (single transactional critical section)
+from asgiref.sync import sync_to_async +from django.db import transaction @@ +@sync_to_async +def _consume_seed_and_create_plant(stock_id: int, telegram_user_id: int): + with transaction.atomic(): + stock = ( + SeedStock.objects.select_for_update() + .select_related("type", "variety", "user") + .get(id=stock_id, user__telegramuser__id=telegram_user_id) + ) + if stock.quantity <= 0: + return None, None + + plant = Plant.objects.create(user=stock.user, type=stock.type, variety=stock.variety) + stock.quantity -= 1 + stock.save(update_fields=["quantity"]) + return plant.name, stock.quantity @@ - try: - stock = await SeedStock.objects.select_related("type", "variety").aget( - id=stock_id, user__telegramuser__id=message.from_user.id - ) - except SeedStock.DoesNotExist: + try: + plant_name, remaining = await _consume_seed_and_create_plant( + stock_id, message.from_user.id + ) + except SeedStock.DoesNotExist: await message.answer(str(_("Seed stock not found."))) return - - if stock.quantity <= 0: + if plant_name is None: await message.answer(str(_("This seed stock is empty."))) return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() - - stock.quantity -= 1 - await stock.asave(update_fields=["quantity"]) @@ _("Planted: {plant_name}. Remaining seeds: {quantity}.").format( - plant_name=plant.name, quantity=stock.quantity + plant_name=plant_name, quantity=remaining ) ) )#!/bin/bash # Verify whether /plant is still non-atomic in current branch. cat -n backend/bot/bot.py | sed -n '251,289p' rg -n 'transaction\.atomic|select_for_update' backend/bot/bot.py🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 264 - 281, The plant creation and stock decrement must be done inside a single DB transaction with a row lock to prevent race conditions: wrap the logic in an async transaction.atomic() context and fetch the SeedStock with select_for_update() (e.g., SeedStock.objects.select_related("type","variety").select_for_update().aget(id=stock_id, user__telegramuser__id=message.from_user.id)), then inside that transaction check stock.quantity, create and asave the Plant (Plant(user=stock.user, type=stock.type, variety=stock.variety).asave()), decrement stock.quantity and asave(update_fields=["quantity"]); keep the message.answer() responses for DoesNotExist and empty-stock cases as before but ensure they occur after acquiring the row lock or by catching DoesNotExist inside the transaction so the read/check/create/decrement are atomic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/bot/bot.py`:
- Around line 84-85: The welcome message currently lists /addseeds with an
optional [variety_slug] which contradicts the parser; update the help text in
the welcome string (the message built and formatted with .format(user=tg_user))
to require variety_slug (change "/addseeds <type_slug> <quantity>
[variety_slug]" to "/addseeds <type_slug> <quantity> <variety_slug>") so it
matches the addseeds parsing/handler contract.
---
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 264-281: The plant creation and stock decrement must be done
inside a single DB transaction with a row lock to prevent race conditions: wrap
the logic in an async transaction.atomic() context and fetch the SeedStock with
select_for_update() (e.g.,
SeedStock.objects.select_related("type","variety").select_for_update().aget(id=stock_id,
user__telegramuser__id=message.from_user.id)), then inside that transaction
check stock.quantity, create and asave the Plant (Plant(user=stock.user,
type=stock.type, variety=stock.variety).asave()), decrement stock.quantity and
asave(update_fields=["quantity"]); keep the message.answer() responses for
DoesNotExist and empty-stock cases as before but ensure they occur after
acquiring the row lock or by catching DoesNotExist inside the transaction so the
read/check/create/decrement are atomic.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
264-280:⚠️ Potential issue | 🔴 CriticalMake
/plantstock consumption + plant creation atomic.Line 265 reads stock, Line 277 creates the plant, and Line 280 decrements stock in separate operations. Concurrent requests can both pass Line 272 and over-consume seeds, and failures between writes can leave inconsistent state.
#!/bin/bash # Read-only verification: inspect the /plant flow for transactional locking. rg -n 'async def plant_from_seed_stock|stock\.quantity <= 0|plant\.asave|stock\.asave|transaction\.atomic|select_for_update' backend/bot/bot.py -C4Suggested fix
+from asgiref.sync import sync_to_async +from django.db import transaction ... +@sync_to_async +def _consume_seed_and_create_plant(stock_id: int, telegram_user_id: int) -> tuple[str, int]: + with transaction.atomic(): + stock = ( + SeedStock.objects.select_for_update() + .select_related("type", "variety", "user") + .get(id=stock_id, user__telegramuser__id=telegram_user_id) + ) + if stock.quantity <= 0: + raise ValueError("empty_stock") + + plant = Plant.objects.create( + user=stock.user, + type=stock.type, + variety=stock.variety, + ) + stock.quantity -= 1 + stock.save(update_fields=["quantity"]) + return plant.name, stock.quantity + `@dp.message`(Command("plant")) async def plant_from_seed_stock(message: Message): ... - try: - stock = await SeedStock.objects.select_related("type", "variety").aget( - id=stock_id, user__telegramuser__id=message.from_user.id - ) - except SeedStock.DoesNotExist: - await message.answer(str(_("Seed stock not found."))) - return - - if stock.quantity <= 0: - await message.answer(str(_("This seed stock is empty."))) - return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() - - stock.quantity -= 1 - await stock.asave(update_fields=["quantity"]) + try: + plant_name, remaining = await _consume_seed_and_create_plant( + stock_id=stock_id, + telegram_user_id=message.from_user.id, + ) + except SeedStock.DoesNotExist: + await message.answer(str(_("Seed stock not found."))) + return + except ValueError: + await message.answer(str(_("This seed stock is empty."))) + return ... _("Planted: {plant_name}. Remaining seeds: {quantity}.").format( - plant_name=plant.name, quantity=stock.quantity + plant_name=plant_name, quantity=remaining )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 264 - 280, Wrap the seed consumption + plant creation in a DB transaction and lock the SeedStock row with select_for_update to prevent races: fetch the stock using SeedStock.objects.select_for_update().select_related("type","variety").aget(... ) inside a transaction.atomic() block, re-check stock.quantity > 0 after locking, create and asave the Plant and decrement + asave the stock within the same transaction; keep the existing SeedStock.DoesNotExist handling but move the locked fetch/quantity check/plant creation/stock update into the transactional block so both operations are atomic and serializable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 264-280: Wrap the seed consumption + plant creation in a DB
transaction and lock the SeedStock row with select_for_update to prevent races:
fetch the stock using
SeedStock.objects.select_for_update().select_related("type","variety").aget(...
) inside a transaction.atomic() block, re-check stock.quantity > 0 after
locking, create and asave the Plant and decrement + asave the stock within the
same transaction; keep the existing SeedStock.DoesNotExist handling but move the
locked fetch/quantity check/plant creation/stock update into the transactional
block so both operations are atomic and serializable.
2f54ddd to
985326d
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
264-280:⚠️ Potential issue | 🔴 CriticalRace condition: concurrent
/plantcalls can over-consume seeds.The check-then-act pattern at lines 272-280 is not atomic. Two concurrent requests can both pass the
stock.quantity > 0check, create plants, and decrement the quantity, resulting in more plants than available seeds or negative stock values.Wrap the operation in
transaction.atomic()withselect_for_update()to serialize concurrent access:Suggested fix using atomic transaction
+from django.db import transaction +from asgiref.sync import sync_to_async `@dp.message`(Command("plant")) async def plant_from_seed_stock(message: Message): # ... argument parsing ... - try: - stock = await SeedStock.objects.select_related("type", "variety").aget( - id=stock_id, user__telegramuser__id=message.from_user.id - ) - except SeedStock.DoesNotExist: - await message.answer(str(_("Seed stock not found."))) - return - - if stock.quantity <= 0: - await message.answer(str(_("This seed stock is empty."))) - return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() - - stock.quantity -= 1 - await stock.asave(update_fields=["quantity"]) + `@sync_to_async` + def plant_from_stock_atomic(stock_id, telegram_user_id): + with transaction.atomic(): + try: + stock = SeedStock.objects.select_for_update().select_related( + "type", "variety" + ).get(id=stock_id, user__telegramuser__id=telegram_user_id) + except SeedStock.DoesNotExist: + return None, "not_found" + + if stock.quantity <= 0: + return None, "empty" + + plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) + plant.save() + + stock.quantity -= 1 + stock.save(update_fields=["quantity"]) + return (plant, stock), None + + result, error = await plant_from_stock_atomic(stock_id, message.from_user.id) + if error == "not_found": + await message.answer(str(_("Seed stock not found."))) + return + if error == "empty": + await message.answer(str(_("This seed stock is empty."))) + return + + plant, stock = result🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 264 - 280, The current non-atomic check-then-act on SeedStock (using SeedStock.objects.select_related(...).aget(...)) allows concurrent /plant calls to over-consume seeds; wrap the operation in a database transaction using transaction.atomic(), fetch the stock row with select_for_update() (replace the aget/select_related call with a select_for_update() query inside the transaction), re-check stock.quantity > 0 inside the transaction, create and save the Plant (Plant(user=stock.user, type=stock.type, variety=stock.variety).asave()) and decrement/save stock.quantity (stock.asave(update_fields=["quantity"])), and return appropriate messages if quantity is insufficient — this serializes access and prevents race conditions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 264-280: The current non-atomic check-then-act on SeedStock (using
SeedStock.objects.select_related(...).aget(...)) allows concurrent /plant calls
to over-consume seeds; wrap the operation in a database transaction using
transaction.atomic(), fetch the stock row with select_for_update() (replace the
aget/select_related call with a select_for_update() query inside the
transaction), re-check stock.quantity > 0 inside the transaction, create and
save the Plant (Plant(user=stock.user, type=stock.type,
variety=stock.variety).asave()) and decrement/save stock.quantity
(stock.asave(update_fields=["quantity"])), and return appropriate messages if
quantity is insufficient — this serializes access and prevents race conditions.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
backend/bot/bot.pybackend/diary/admin.pybackend/diary/migrations/0003_seedstock.pybackend/diary/models.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 985326d0c8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d3c205ca1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
278-290:⚠️ Potential issue | 🟠 Major
/plantstill has a consistency gap between decrement and plant creation.The stock decrement and
Plantcreation are separate writes; ifplant.asave()fails, quantity is already reduced and data diverges. This is the same root issue previously raised, just with the improved decrement path.Suggested fix
- updated = await stock_qs.aupdate(quantity=F("quantity") - 1) + updated = await stock_qs.aupdate(quantity=F("quantity") - 1) if updated == 0: await message.answer(str(_("This seed stock is empty."))) return - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() + try: + plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) + await plant.asave() + except Exception: + # rollback seed consumption if plant creation fails + await SeedStock.objects.filter(id=stock.id).aupdate(quantity=F("quantity") + 1) + raise🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 278 - 290, The decrement and Plant creation must be atomic to avoid divergence: wrap the stock decrement and Plant creation in a single database transaction (use the async transaction.atomic() context manager) and perform the conditional decrement and creation inside it; e.g., acquire the row with SeedStock.objects.select_for_update().filter(id=stock.id, quantity__gt=0) (async), perform the quantity decrement (either via F() and aupdate or by modifying the locked instance and asave()), check the update result (the existing updated == 0 check) and only then create and asave() the Plant instance (Plant(...) followed by plant.asave()) inside the same transaction so that if plant.asave() fails the decrement is rolled back.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/bot/bot.py`:
- Around line 259-260: Update the registration prompt string used in the bot
reply—replace the incorrect phrase "You must be registered in to use this
command. Use /start." (seen in the call str(_("You must be registered in to use
this command. Use /start."))) with the corrected wording "You must be registered
to use this command. Use /start." so the message reads grammatically correct;
adjust the NSLocalized/_ wrapper call accordingly where this string is
defined/emitted in bot.py.
- Around line 211-213: The TelegramUser lookup in bot.py (tg_user = await
TelegramUser.objects.select_related("user").aget(...)) can raise
TelegramUser.DoesNotExist and crash the /addseeds handler; wrap that await in a
try/except catching TelegramUser.DoesNotExist (or use an aget_or_none style
helper) inside the /addseeds command handler, and when missing respond to the
user with a short message instructing them to run /start (or otherwise register)
instead of letting the exception propagate; update any subsequent use of tg_user
in the handler to only run when the object exists.
---
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 278-290: The decrement and Plant creation must be atomic to avoid
divergence: wrap the stock decrement and Plant creation in a single database
transaction (use the async transaction.atomic() context manager) and perform the
conditional decrement and creation inside it; e.g., acquire the row with
SeedStock.objects.select_for_update().filter(id=stock.id, quantity__gt=0)
(async), perform the quantity decrement (either via F() and aupdate or by
modifying the locked instance and asave()), check the update result (the
existing updated == 0 check) and only then create and asave() the Plant instance
(Plant(...) followed by plant.asave()) inside the same transaction so that if
plant.asave() fails the decrement is rolled back.
1d3c205 to
bb2594c
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
292-307:⚠️ Potential issue | 🔴 CriticalMake
/plantstock decrement andPlantcreation one atomic unit.There is still a consistency gap: stock is decremented before
Plantpersistence is guaranteed. If creation fails after decrement, seeds are lost. Also, the write query on Line 295 drops the ownership predicate checked earlier. Please perform authorization check + decrement + plant creation in one locked transaction on the same scoped row.🔧 Suggested direction
- stock_qs = SeedStock.objects.filter(id=stock.id, quantity__gt=0) - updated = await stock_qs.aupdate(quantity=F("quantity") - 1) - if updated == 0: - await message.answer(html.quote(str(_("This seed stock is empty.")))) - return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() + # Move the flow into one transactional helper: + # 1) lock stock row scoped by id + requesting user + # 2) re-check quantity > 0 under lock + # 3) create Plant + # 4) decrement and save stock + # 5) return plant name + remaining quantityRun this read-only check to confirm current non-atomic flow and missing lock usage:
#!/bin/bash set -euo pipefail echo "=== plant_from_seed_stock implementation ===" sed -n '263,317p' backend/bot/bot.py echo echo "=== transaction/lock usage in bot handler file ===" rg -n 'transaction\.atomic|select_for_update|async def plant_from_seed_stock|quantity__gt=0' backend/bot/bot.py -n -C2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 292 - 307, The current flow decrements SeedStock via aconditional aupdate then creates Plant separately, risking lost seeds if Plant.asave() fails and dropping the earlier ownership predicate; change plant_from_seed_stock to run a single DB transaction that: fetches the target SeedStock with select_for_update() (ensuring the ownership check by including stock.user == request.user or equivalent), verify quantity > 0, decrement quantity and save the SeedStock row, then create and save the Plant inside the same transaction; use an async transaction.atomic context so any failure during Plant creation rolls back the decrement and preserves consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 292-307: The current flow decrements SeedStock via aconditional
aupdate then creates Plant separately, risking lost seeds if Plant.asave() fails
and dropping the earlier ownership predicate; change plant_from_seed_stock to
run a single DB transaction that: fetches the target SeedStock with
select_for_update() (ensuring the ownership check by including stock.user ==
request.user or equivalent), verify quantity > 0, decrement quantity and save
the SeedStock row, then create and save the Plant inside the same transaction;
use an async transaction.atomic context so any failure during Plant creation
rolls back the decrement and preserves consistency.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
Makefilebackend/bot/bot.pybackend/bot/middlewares.pybackend/diary/admin.pybackend/diary/migrations/0003_seedstock.pybackend/diary/models.py
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/diary/admin.py
bb2594c to
a22bc33
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
292-303:⚠️ Potential issue | 🟠 MajorStock decrement and plant creation must be transactionally coupled to prevent data loss.
The atomic
F("quantity") - 1update prevents read-modify-write races, but ifplant.asave()fails after the decrement succeeds, the user loses a seed without gaining a plant. Django 5.2 does not support async transactions directly; wrap both operations in a synchronous function and call it viasync_to_async(thread_sensitive=True):🔧 Suggested transactional wrapper
+ from django.db import transaction + from asgiref.sync import sync_to_async + + `@sync_to_async` + def decrement_and_create_plant(): + with transaction.atomic(): + updated = SeedStock.objects.filter(id=stock.id, quantity__gt=0).update( + quantity=F("quantity") - 1 + ) + if updated == 0: + return None + plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) + plant.save() + return plant + - # Atomic decrement with quantity check (prevents race conditions) - # Replaces non-atomic in-memory decrement to avoid concurrent request corruption - # Uses conditional filter + aupdate for database-level atomicity - stock_qs = SeedStock.objects.filter(id=stock.id, quantity__gt=0) - updated = await stock_qs.aupdate(quantity=F("quantity") - 1) - - if updated == 0: - await message.answer(html.quote(str(_("This seed stock is empty.")))) - return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() + plant = await decrement_and_create_plant() + if plant is None: + await message.answer(html.quote(str(_("This seed stock is empty.")))) + return🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 292 - 303, The decrement and Plant creation must be executed inside a DB transaction so the seed quantity decrement (SeedStock.objects.filter(...).aupdate(quantity=F("quantity") - 1)) and Plant.asave() either both succeed or both roll back; implement a synchronous helper that opens transaction.atomic(), performs the conditional update (using the same filter and F() expression), checks the update count, creates and saves the Plant, and returns a result, then call that helper via sync_to_async(thread_sensitive=True) from the async view; replace the current separate aupdate + Plant.asave() calls with this transactional wrapper and propagate the "out of stock" result to message.answer as before.
🧹 Nitpick comments (1)
backend/diary/models.py (1)
232-241: Defensive checks for non-nullablevarietyare unnecessary.Since the
varietyForeignKey is defined withoutnull=True, it's guaranteed to be non-null at the database level. Theif self.variety:checks insave()(line 233) and__str__(line 239) are therefore always truthy and can be simplified for clarity.♻️ Proposed simplification
def save(self, *args, **kwargs): - if self.variety: - self.type = self.variety.type + self.type = self.variety.type super().save(*args, **kwargs) def __str__(self): - name = self.type.name - if self.variety: - name += f" {self.variety.name}" + name = f"{self.type.name} {self.variety.name}" return f"{name}: {self.quantity}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/diary/models.py` around lines 232 - 241, Remove the unnecessary defensive checks around the non-nullable ForeignKey `variety` by directly using `self.variety` and `self.variety.type` instead of guarding with `if self.variety:`; update the `save` method to always assign `self.type = self.variety.type` and simplify `__str__` to build the name using `self.type.name` plus `self.variety.name` without conditional checks (references: save, __str__, attributes variety and type).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 292-303: The decrement and Plant creation must be executed inside
a DB transaction so the seed quantity decrement
(SeedStock.objects.filter(...).aupdate(quantity=F("quantity") - 1)) and
Plant.asave() either both succeed or both roll back; implement a synchronous
helper that opens transaction.atomic(), performs the conditional update (using
the same filter and F() expression), checks the update count, creates and saves
the Plant, and returns a result, then call that helper via
sync_to_async(thread_sensitive=True) from the async view; replace the current
separate aupdate + Plant.asave() calls with this transactional wrapper and
propagate the "out of stock" result to message.answer as before.
---
Nitpick comments:
In `@backend/diary/models.py`:
- Around line 232-241: Remove the unnecessary defensive checks around the
non-nullable ForeignKey `variety` by directly using `self.variety` and
`self.variety.type` instead of guarding with `if self.variety:`; update the
`save` method to always assign `self.type = self.variety.type` and simplify
`__str__` to build the name using `self.type.name` plus `self.variety.name`
without conditional checks (references: save, __str__, attributes variety and
type).
ℹ️ Review info
Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0239089e-db41-4ce7-a466-1e650671eacb
📒 Files selected for processing (6)
Makefilebackend/bot/bot.pybackend/bot/middlewares.pybackend/diary/admin.pybackend/diary/migrations/0003_seedstock.pybackend/diary/models.py
🚧 Files skipped from review as they are similar to previous changes (1)
- Makefile
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
ce91bce to
8a63fbc
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
backend/bot/bot.py (1)
292-303:⚠️ Potential issue | 🟠 MajorDecrement and plant creation are not transactional; a failure after decrement leaves inconsistent state.
The atomic
F("quantity") - 1update (line 296) prevents race conditions for the decrement, but ifplant.asave()(line 303) fails afterward, the stock is already decremented with no plant created—effectively losing a seed.Wrap both operations in a transaction to ensure consistency:
🔧 Suggested fix using transaction
+from django.db import transaction +from asgiref.sync import sync_to_async ... - # Atomic decrement with quantity check (prevents race conditions) - # Replaces non-atomic in-memory decrement to avoid concurrent request corruption - # Uses conditional filter + aupdate for database-level atomicity - stock_qs = SeedStock.objects.filter(id=stock.id, quantity__gt=0) - updated = await stock_qs.aupdate(quantity=F("quantity") - 1) - - if updated == 0: - await message.answer(html.quote(str(_("This seed stock is empty.")))) - return - - plant = Plant(user=stock.user, type=stock.type, variety=stock.variety) - await plant.asave() + `@sync_to_async` + def create_plant_from_stock(stock_id, user, plant_type, variety): + with transaction.atomic(): + updated = SeedStock.objects.filter(id=stock_id, quantity__gt=0).update( + quantity=F("quantity") - 1 + ) + if updated == 0: + return None + plant = Plant(user=user, type=plant_type, variety=variety) + plant.save() + return plant + + plant = await create_plant_from_stock(stock.id, stock.user, stock.type, stock.variety) + if plant is None: + await message.answer(html.quote(str(_("This seed stock is empty.")))) + return🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/bot/bot.py` around lines 292 - 303, Wrap the decrement and plant creation in a single async DB transaction so the decrement is rolled back if Plant.asave() fails: use "from django.db import transaction" and do "async with transaction.atomic():" around the logic that filters the SeedStock (SeedStock.objects.filter(id=stock.id, quantity__gt=0)), performs the conditional aupdate(quantity=F('quantity') - 1) and then creates/saves the Plant (Plant(...); await plant.asave()); keep the check on "updated == 0" inside the same transaction and call message.answer(...) and return if no rows updated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 292-303: Wrap the decrement and plant creation in a single async
DB transaction so the decrement is rolled back if Plant.asave() fails: use "from
django.db import transaction" and do "async with transaction.atomic():" around
the logic that filters the SeedStock (SeedStock.objects.filter(id=stock.id,
quantity__gt=0)), performs the conditional aupdate(quantity=F('quantity') - 1)
and then creates/saves the Plant (Plant(...); await plant.asave()); keep the
check on "updated == 0" inside the same transaction and call message.answer(...)
and return if no rows updated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a839a026-c449-459c-b1cd-f3790d81f343
📒 Files selected for processing (7)
Makefilebackend/bot/bot.pybackend/bot/middlewares.pybackend/diary/admin.pybackend/diary/migrations/0003_seedstock.pybackend/diary/models.pyscripts/docker-entrypoint.sh
💤 Files with no reviewable changes (1)
- scripts/docker-entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (2)
- Makefile
- backend/bot/middlewares.py
Summary
SeedStockmodel to track each user's seed inventory by plant type/variety and quantity/seedsto list available seeds with stock IDs/plant <seed_stock_id>to create aPlantfrom stock and decrement quantitydiary/0003_seedstock.pyfor schema changesDetails
SeedStockenforces uniqueness per(user, type, variety)and auto-alignstypefromvarietyon save/plantvalidates command usage, ownership, and non-empty stock before creating the plantValidation
python -m compileall backend/bot/bot.py backend/diary/models.py backend/diary/admin.py backend/diary/migrations/0003_seedstock.pypython backend/manage.py makemigrations diarycurrently fails in this environment because Django is not installedCodex Task
Summary by CodeRabbit
New Features
UX
Admin / Backoffice
Chores
Bug Fixes