Skip to content

Commit

Permalink
feat: add price.origins_tags field (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphael0202 committed Dec 29, 2023
1 parent b317014 commit 46e0c7f
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""add origins_tags field
Revision ID: 24d71d56d493
Revises: 1e60d73e79cd
Create Date: 2023-12-29 10:23:22.430506
"""
from typing import Sequence, Union

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "24d71d56d493"
down_revision: Union[str, None] = "1e60d73e79cd"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"prices",
sa.Column(
"origins_tags",
sa.JSON().with_variant(
postgresql.JSONB(astext_type=sa.Text()), "postgresql"
),
nullable=True,
),
)
op.create_index(
op.f("ix_prices_origins_tags"), "prices", ["origins_tags"], unique=False
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_prices_origins_tags"), table_name="prices")
op.drop_column("prices", "origins_tags")
# ### end Alembic commands ###
22 changes: 0 additions & 22 deletions app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from fastapi_filter import FilterDepends
from fastapi_pagination import Page, add_pagination
from fastapi_pagination.ext.sqlalchemy import paginate
from openfoodfacts.taxonomy import get_taxonomy
from openfoodfacts.utils import get_logger
from sqlalchemy.orm import Session

Expand Down Expand Up @@ -209,27 +208,6 @@ def create_price(
detail="Proof does not belong to current user",
)

if price.category_tag is not None:
# lowercase the category tag to perform the match
price.category_tag = price.category_tag.lower()
category_taxonomy = get_taxonomy("category")
if price.category_tag not in category_taxonomy:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid category tag: category '{price.category_tag}' does not exist in the taxonomy",
)

if price.labels_tags is not None:
# lowercase the labels tags to perform the match
price.labels_tags = [label_tag.lower() for label_tag in price.labels_tags]
labels_taxonomy = get_taxonomy("label")
for label_tag in price.labels_tags:
if label_tag not in labels_taxonomy:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid label tag: label '{label_tag}' does not exist in the taxonomy",
)

db_price = crud.create_price(db, price=price, user=current_user)
background_tasks.add_task(tasks.create_price_product, db, db_price)
background_tasks.add_task(tasks.create_price_location, db, db_price)
Expand Down
1 change: 1 addition & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class Price(Base):
product_name = Column(String, nullable=True)
category_tag = Column(String, nullable=True, index=True)
labels_tags = Column(JSONVariant, nullable=True, index=True)
origins_tags = Column(JSONVariant, nullable=True, index=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), nullable=True)
product: Mapped[Product] = relationship(back_populates="prices")

Expand Down
65 changes: 60 additions & 5 deletions app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from fastapi_filter.contrib.sqlalchemy import Filter
from openfoodfacts import Flavor
from openfoodfacts.taxonomy import get_taxonomy
from pydantic import (
AnyHttpUrl,
BaseModel,
Expand Down Expand Up @@ -123,10 +124,23 @@ class PriceCreate(BaseModel):
The most common labels are:
- `en:organic`: the product is organic
- `fr:ab-agriculture-biologique`: the product is organic, in France
- `en:fair-trade`: the product is fair-trade
Other labels can be provided if relevant.
""",
examples=["en:organic", "fr:ab-agriculture-biologique", "en:fair-trade"],
)
origins_tags: list[str] | None = Field(
default=None,
description="""origins of the product, only for products without barcode.
This field is a list as some products may be a mix of several origins,
but most products have only one origin.
The origins must be valid origins in the Open Food Facts taxonomy.
If one of the origins is not valid, the price will be rejected.""",
examples=["en:california", "en:france", "en:italy", "en:spain"],
)
price: float = Field(
gt=0,
Expand Down Expand Up @@ -162,20 +176,61 @@ class PriceCreate(BaseModel):
)

@field_validator("labels_tags")
def labels_tags_is_valid(cls, v):
def labels_tags_is_valid(cls, v: list[str] | None):
if v is not None:
if len(v) == 0:
raise ValueError("`labels_tags` cannot be empty")
v = [label_tag.lower() for label_tag in v]
labels_taxonomy = get_taxonomy("label")
for label_tag in v:
if label_tag not in labels_taxonomy:
raise ValueError(
f"Invalid label tag: label '{label_tag}' does not exist in the taxonomy",
)
return v

@field_validator("origins_tags")
def origins_tags_is_valid(cls, v: list[str] | None):
if v is not None:
if len(v) == 0:
raise ValueError("`origins_tags` cannot be empty")
v = [origin_tag.lower() for origin_tag in v]
origins_taxonomy = get_taxonomy("origin")
for origin_tag in v:
if origin_tag not in origins_taxonomy:
raise ValueError(
f"Invalid origin tag: origin '{origin_tag}' does not exist in the taxonomy",
)
return v

@field_validator("category_tag")
def category_tag_is_valid(cls, v: str | None):
if v is not None:
v = v.lower()
category_taxonomy = get_taxonomy("category")
if v not in category_taxonomy:
raise ValueError(
f"Invalid category tag: category '{v}' does not exist in the taxonomy"
)
return v

@model_validator(mode="after")
def product_code_and_category_tag_are_exclusive(self):
"""Validator that checks that `product_code` and `category_tag` are
exclusive, and that at least one of them is set."""
if self.product_code is not None and self.category_tag is not None:
raise ValueError(
"`product_code` and `category_tag` are exclusive, you can't set both"
)
if self.product_code is not None:
if self.category_tag is not None:
raise ValueError(
"`product_code` and `category_tag` are exclusive, you can't set both"
)
if self.labels_tags is not None:
raise ValueError(
"`labels_tags` can only be set for products without barcode"
)
if self.origins_tags is not None:
raise ValueError(
"`origins_tags` can only be set for products without barcode"
)
if self.product_code is None and self.category_tag is None:
raise ValueError("either `product_code` or `category_tag` must be set")
return self
Expand Down
Loading

0 comments on commit 46e0c7f

Please sign in to comment.