Skip to content

Commit

Permalink
6164: add images to products (#184)
Browse files Browse the repository at this point in the history
* add image metadata

* migration

* Update serializers

* update model

* management command

* setting

* Add celery task

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ruff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ruff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* open api

* Mock product save api call

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add better description

* code review comments

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* better solution

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
cp-at-mit and pre-commit-ci[bot] authored Dec 18, 2024
1 parent e1b6e5b commit 469f918
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=

APISIX_SESSION_SECRET_KEY=must_be_at_least_16_chars

MITOL_LEARN_API_URL=https://api.rc.learn.mit.edu/api/v1/
28 changes: 26 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
"""Project conftest"""

# pylint: disable=wildcard-import, unused-wildcard-import
from unittest.mock import Mock, patch

import pytest

from fixtures.common import * # noqa: F403
from fixtures.users import * # noqa: F403
from unified_ecommerce.exceptions import DoNotUseRequestException


@pytest.fixture(autouse=True)
def mock_requests_get():
# Mock the response of requests.get
with patch("system_meta.tasks.requests.get") as mock_get:
# Create a mock response object
mock_response = Mock()
mock_response.raise_for_status = Mock() # Mock the raise_for_status method
mock_response.json.return_value = {
"results": [
{
"image": {
"url": "http://example.com/image.jpg",
"alt": "Image alt text",
"description": "Image description",
}
}
]
}
mock_get.return_value = (
mock_response # Set the mock response to be returned by requests.get
)
yield mock_get # This will be the mocked requests.get


@pytest.fixture(autouse=True)
def prevent_requests(mocker, request): # noqa: PT004
"""Patch requests to error on request by default"""
Expand Down
12 changes: 12 additions & 0 deletions openapi/specs/v0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,10 @@ components:
format: decimal
pattern: ^-?\d{0,5}(?:\.\d{0,2})?$
description: Price (decimal to two places)
image_metadata:
nullable: true
description: Image metadata including URL, alt text, and description (in
JSON).
PaymentTypeEnum:
enum:
- marketing
Expand Down Expand Up @@ -1762,6 +1766,10 @@ components:
deleted_by_cascade:
type: boolean
readOnly: true
image_metadata:
nullable: true
description: Image metadata including URL, alt text, and description (in
JSON).
required:
- deleted_by_cascade
- description
Expand Down Expand Up @@ -1799,6 +1807,10 @@ components:
format: decimal
pattern: ^-?\d{0,5}(?:\.\d{0,2})?$
description: Price (decimal to two places)
image_metadata:
nullable: true
description: Image metadata including URL, alt text, and description (in
JSON).
required:
- description
- name
Expand Down
5 changes: 5 additions & 0 deletions system_meta/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ class Meta:
description = Faker("text")
system = SubFactory(IntegratedSystemFactory)
system_data = Faker("json")
image_metadata = {
"image_url": "http://example.com/image.jpg",
"alt_text": "Image alt text",
"description": "Image description",
}


class ActiveProductFactory(ProductFactory):
Expand Down
58 changes: 58 additions & 0 deletions system_meta/management/commands/update_product_image_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.core.management.base import BaseCommand

from system_meta.models import Product
from system_meta.tasks import update_products


class Command(BaseCommand):
"""
A management command to update image_metadata for all Product objects
Example usage: python manage.py update_product_image_data --product_id 1
"""

help = "Update image_metadata for all Product objects"

def add_arguments(self, parser):
parser.add_argument(
"--product-id",
type=int,
help="The ID of the product to update",
)
parser.add_argument(
"--sku",
type=str,
help="The SKU of the product to update",
)
parser.add_argument(
"--name",
type=str,
help="The name of the product to update",
)
parser.add_argument(
"--system-name",
type=str,
help="The system name of the product to update",
)

def handle(self, *args, **kwargs): # noqa: ARG002
product_id = kwargs.get("product_id")
sku = kwargs.get("sku")
name = kwargs.get("name")
system_name = kwargs.get("system_name")

if product_id:
products = Product.objects.filter(id=product_id)
elif sku:
products = Product.objects.filter(sku=sku, system__name=system_name)
elif name:
products = Product.objects.filter(name=name)
else:
products = Product.objects.all()

for product in products:
update_products.delay(product.id)
self.stdout.write(
self.style.SUCCESS(
f"Successfully updated image metadata for product {product.id}"
)
)
21 changes: 21 additions & 0 deletions system_meta/migrations/0007_product_image_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.2.17 on 2024-12-10 16:10

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("system_meta", "0006_integratedsystemapikey"),
]

operations = [
migrations.AddField(
model_name="product",
name="image_metadata",
field=models.JSONField(
blank=True,
help_text="Image metadata including URL, alt text, and description (in JSON).",
null=True,
),
),
]
13 changes: 13 additions & 0 deletions system_meta/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from safedelete.models import SafeDeleteModel
from slugify import slugify

from system_meta.tasks import update_products
from unified_ecommerce.utils import SoftDeleteActiveModel

User = get_user_model()
Expand Down Expand Up @@ -77,10 +78,22 @@ class Product(SafeDeleteModel, SoftDeleteActiveModel, TimestampedModel):
null=True,
help_text="System-specific data for the product (in JSON).",
)
image_metadata = models.JSONField(
blank=True,
null=True,
help_text="Image metadata including URL, alt text, and description (in JSON).",
)

objects = SafeDeleteManager()
all_objects = models.Manager()

def save(self, *args, **kwargs):
# Retrieve image data from the API
created = not self.pk
super().save(*args, **kwargs)
if created:
update_products.delay(self.id)

class Meta:
"""Meta class for Product"""

Expand Down
1 change: 1 addition & 0 deletions system_meta/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ class Meta:
"system",
"price",
"deleted_by_cascade",
"image_metadata",
]
40 changes: 40 additions & 0 deletions system_meta/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging
from typing import Optional

import requests
from celery import shared_task
from django.conf import settings


@shared_task
def update_products(product_id: Optional[int] = None):
"""
Update all product's image metadata. If product_id is provided, only update the
product with that ID. Otherwise, update all products.
"""
from .models import Product

log = logging.getLogger(__name__)
if product_id:
products = Product.objects.filter(id=product_id)
else:
products = Product.objects.all()
for product in products:
try:
response = requests.get(
f"{settings.MITOL_LEARN_API_URL}learning_resources/",
params={"platform": product.system.slug, "readable_id": product.sku},
timeout=10,
)
response.raise_for_status()
results_data = response.json()
course_data = results_data.get("results")[0]
image_data = course_data.get("image")
product.image_metadata = {
"image_url": image_data.get("url"),
"alt_text": image_data.get("alt"),
"description": image_data.get("description"),
}
product.save()
except requests.RequestException:
log.exception("Failed to retrieve image data for product %s", product.id)
2 changes: 2 additions & 0 deletions unified_ecommerce/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@
name="MITOL_UE_PAYMENT_BASKET_CHOOSER", default="/cart/"
)

MITOL_LEARN_API_URL = get_string(name="MITOL_LEARN_API_URL", default="")

import_settings_modules("mitol.payment_gateway.settings.cybersource")

# Keycloak API settings
Expand Down
9 changes: 8 additions & 1 deletion unified_ecommerce/settings_celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Django settings for celery.
"""

from celery.schedules import crontab

from unified_ecommerce.envs import get_bool, get_int, get_string

USE_CELERY = True
Expand All @@ -18,7 +20,12 @@
"CELERY_WORKER_MAX_MEMORY_PER_CHILD", 250_000
)

CELERY_BEAT_SCHEDULE = {}
CELERY_BEAT_SCHEDULE = {
"update-products-daily": {
"task": "system_meta.tasks.update_products",
"schedule": crontab(hour=0, minute=0), # Runs every day at midnight
},
}

CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
Expand Down

0 comments on commit 469f918

Please sign in to comment.