Package release

ocyan-plugin-oscar-odin

mandel/stable ยท Version 2.0.3

Efficiently map data objects. Used mostly for integrations with external systems.

Metadata

author Motolani Olaiya
author_email [email protected]
classifiers
  • License :: Other/Proprietary License
  • Framework :: Ocyan
  • Environment :: Plugins
description_content_type text/markdown
dynamic
  • author
  • author-email
  • classifier
  • description
  • description-content-type
  • home-page
  • provides-extra
  • requires-dist
  • requires-python
  • summary
provides_extras
  • test
requires_dist
  • ocyan.core<2,>=1.2.13
  • django-oscar-odin<1,>=0.1
  • ocyan.plugin.oscar<3,>=2.0.5
  • Django<6,>=5.2
  • django-oscar<5,>=4.1
  • empty_testproject; extra == "test"
  • ocyan.plugin.testing; extra == "test"
  • ocyan.plugin.oscar_checkout; extra == "test"
  • ocyan.plugin.oscar_catalogue; extra == "test"
  • oxyan.themes; extra == "test"
  • pylint-django; extra == "test"
  • ruff; extra == "test"
  • vdt.versionplugin.wheel; extra == "test"
  • coverage; extra == "test"
requires_python >=3.6

Release files

FileTest resultsHistory
ocyan_plugin_oscar_odin-2.0.3-py3-none-any.whl
Size
20 KB
Type
Python Wheel
Python
3
  • Uploaded to mandel/stable by Mandel-publish 2026-08-11 08:08:56
ocyan_plugin_oscar_odin-2.0.3.tar.gz
Size
17 KB
Type
Source
  • Uploaded to mandel/stable by Mandel-publish 2026-08-11 08:08:57

Ocyan plugin Oscar Odin

The oscar odin plugin comes with some tools to create dynamic mappings. A dynamic mapping can be edited from the wagtail admin, where rules can be added or overriden.

Installation

Add ocyan.plugin.oscar_odin to your projects dependencies.

Describe how to install the plugin.

About & Usage of Oscar Odin

How to make a dynamic mapping that can be edited in wagtail.

Dynamic mappings can be used to change the behaviour of existing mapping classes. This is most powerful if the mapping are not fully overwriting the models that contains the saved data. For this reason we need to be able to determine exactly which fields where impacted by the mapping.

Lets start with a basic mapping to the ProductResource. We have to make sure to map any required field on the ProductModel

import odin
from oscar.core.loading import get_model, get_classes

Product = get_model("catalogue", "Product")
ProductClass = get_model("catalogue", "ProductClass")

ProductResource, ProductClassResource = get_classes(
    "oscar_odin.resources.catalogue", ["ProductResource", "ProductClassResource"]
)


class ExampleToProductResourceMapping(odin.Mapping):
    from_obj = ExampleProduct
    to_obj = ProductResource
    # make sure to have this set to False or this will be the only mapping you can have
    # for the ExampleProduct
    register_mapping = False
    mappings = (
        ("Code", None, "upc"),
        ("ID", None, "code"),
        ("StandardSalesPrice", None, "price"),
        ("Description", None, "title"),
    )

    @odin.map_field(from_field="Description", to_field="slug")
    def slug(self, value):
        return slugify(value)

    @odin.assign_field(to_field="partner")
    def partner(self):
        return self.context["partner"]

    @odin.assign_field(to_field="product_class")
    def product_class(self):
        return ProductClassResource(slug=self.context["product_class"].slug)

    @odin.assign_field(to_field="structure")
    def structure(self):
        return Product.STANDALONE

Now we can use this mapping in combination with MappingEditorViewSet to make it editable.

from wagtail.snippets.views.snippets import SnippetViewSetGroup
from wagtail.snippets.models import register_snippet
from oscar.core.loading import get_model
from ocyan.plugin.oscar_odin.wagtailadmin import MappingEditorViewSet

from .mappings import ExampleToProductResourceMapping

Product = get_model("catalogue", "Product")

@register_snippet
class ExampleGroup(SnippetViewSetGroup):
    menu_label = _("Example")
    menu_icon = "tigre"
    menu_order = 900
    items = (
        MappingEditorViewSet(
            mapping_class=ExampleToProductResourceMapping,
            # not really sure why I introduced the associated model ...
            associated_model=Product,
        ),
        # .. whatever other items you need
    )

Now you can add mappings from ExampleProduct to ProductResource with different kinds of mappings from within wagtail.

To use the mapping with dynamic mapping rules in your code, it goes like this:

import requests
from django.contrib.contenttypes.models import ContentType
from oscar.core.loading import get_model, get_class
from ocyan.plugin.oscar_odin.models import MappingModel


Product = get_model("catalogue", "Product")

ct = ContentType.objects.get_for_model(Product)
mm, _ = MappingModel.objects.get_or_create(contenttype=ct)
mapper = mm.mapping_class(ExampleToProductResourceMapping)

Get the data you want mapped

data = requests.get("http://superapi.com/").json()

Convert the product data to a resource

from_resource = ExampleProduct.create_from_dict(data)

Map the resource to a ProductResource. If your mapping needs some context, import it

from .context import get_product_context

context = get_product_context()
to_resource = mapper.apply(from_resource, context=context)
to_resource.full_clean()

Finally map to ProductResource, which can easily be converted to Product.

ProductResource = get_class("oscar_odin.resources.catalogue", "ProductResource")

to_resource = from_resource.convert_to(
    ProductResource, context=context
)

To save the ProductResource to the database, if we want partial update, only to update the mapped fields, first we need to find out which fields where mapped by our dynamic mapper.

from oscar_odin.utils import get_mapped_fields

mapped_fields = get_mapped_fields(mapper)

Next we need to ask the mapper that maps to Product, what would be the fields_to_update for products_to_db so we can do the partial update

ProductToModel = get_class("oscar_odin.mappings.catalogue", "ProductToModel")

fields_to_update = ProductToModel.get_fields_impacted_by_mapping(
    *mapped_fields
)

result, errors = products_to_db(
    to_resource,
    fields_to_update=fields_to_update,
    clean_instances=False,
)

If you want you can have an alternative model_identifier_mapping

from oscar_odin.mappings.constants import MODEL_IDENTIFIERS_MAPPING

EXACT_MODEL_IDENTIFIERS_MAPPING = {**MODEL_IDENTIFIERS_MAPPING, Product: ("code",)}

result, errors = products_to_db(
    to_resource,
    fields_to_update=fields_to_update,
    identifier_mapping=EXACT_MODEL_IDENTIFIERS_MAPPING,
    clean_instances=False,
)

So this is what you can do with any resource, just use resource_to_db the details will be alike. But for Product there is one extra thing you can do in your mapping. If you want you can have to user map field to product attributes. For this to work you have to use a different baseclass for your mapping:

MappingModelProductAttributesMapping = get_class("oscar_odin.mappings.productattributes", "MappingModelProductAttributesMapping")

class ExampleToProductResourceMapping(MappingModelProductAttributesMapping):
    from_obj = ExampleProduct
    to_obj = ProductResource
    # make sure to have this set to False or this will be the only mapping you can have
    # for the ExampleProduct
    register_mapping = False
    ...

Next you must define the productclasses that you are using for your mapped models, so the available attributes can be determined. You can just define it as a class variable:

class ExampleToProductResourceMapping(MappingModelProductAttributesMapping):
    product_classes = ProductClass.objects.filter(slug="WHATEVER_FILTER_I_NEED")
    ...

Register extra actions

If you need more operations on you mapped fields, you can register them, and they will appear in the action dropdown in wagtail. You map action by their module path.

from ocyan.plugin.oscar_odin import mapping_action_registry

mapping_action_registry.register(ExampleToProductResourceMapping, "the.module.path_to_my_action.name_of_action", "Really nice action")

Alternatively you can define actions on you mapping class as a class variable.

class ExampleToProductResourceMapping(MappingModelProductAttributesMapping):
  action_choices = [
    ("the.module.path_to_my_action.name_of_action", "Really nice action"),
    ("the.module.path_to_my_action.name_of_anotgher_action", "Bad bad action")
  ]

providing clean for the wagtail forms

By defining a classmethod named clean_form on you mapping class you can provide a clean method to you wagtail mapping forms

MappingModelProductAttributesMapping allready has an implementation that can server as an example

class MappingModelProductAttributesMapping(MappingBase, metaclass=MappingMeta):
    "Baseclass for dynamic mappings that need to map to ProductResource.attributes"

    @classmethod
    def clean_form(cls, form):
        "Clean the FieldMappingModel form when aditing the mapping in wagtail"
        attributes = set(dict(cls.get_extra_to_obj_field_choices()))
        to_fields = form.cleaned_data["to_fields"]
        action = form.cleaned_data["action_importstring"]
        for to_obj in to_fields:
            if to_obj in attributes and action != cls.MAP_ATTRIBUTES_ACTION:
                raise ValidationError(
                    gettext(
                        "Action must be set to product attibute when selecting a product attribute"
                    )
                )