paint-brush
Create Dynamic List of Heterogeneous Propertiesby@djangostars
248 reads

Create Dynamic List of Heterogeneous Properties

by Django StarsMarch 24th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

From time to time you have to build some properties for an object and usually those are just a bunch of fields for a model. With <a href="https://github.com/django-stars/django-classifier">django-classifier</a> you may do that in another way. It allows you to create models at start, and set the properties needed from the admin panel at any&nbsp;time.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Create Dynamic List of Heterogeneous Properties
Django Stars HackerNoon profile picture

From time to time you have to build some properties for an object and usually those are just a bunch of fields for a model. With django-classifier you may do that in another way. It allows you to create models at start, and set the properties needed from the admin panel at any time.

Let me show you two demo projects. First one is the “user profile” I’ve built for our internal website. The second one shows the way to build online store products with dynamic properties and the option to apply filters for those.

Note: the code samples may be displayed improperly because of markdown. I recommend to continue reading the original article on our blog to make sure all the examples are displayed properly.

Demo Project #1

First example is pretty simple, however it shows the way to extend functionality that django-classifier provides.

I had a task to make the extension of the user profile possible at any time, while being able to add as much properties as possible. The most interesting part of this example is the division of the properties by categories/groups and fetching specific data from properties.

At first, I added three models as it’s described in the tutorial, but UserAttributeClassifier was extended with category field and UserAttributeClassifierLabel with slug field. We will need slug field in label model to fetch specific data from the properties in future.

Models’ listings:

class UserAttributeClassifier(ClassifierAbstract):  
    category = models.CharField(max_length=100)

    class Meta:
        ordering = ('category', )


class UserAttributeClassifierLabel(ClassifierLabelAbstract):  
    # have to be the same as in database to be able to fetch specific value
    GENERAL_BIRTHDAY = 'birthday'
    PHONE_MOBILE = 'mobile'
    IM_SKYPE = 'skype'

    classifier = models.ForeignKey(
        UserAttributeClassifier,
        related_name='labels'
    )
    slug = models.CharField(max_length=100, null=True, blank=True)

    class Meta:
        ordering = ('label', )


class UserAttribute(models.Model):  
    user = models.ForeignKey(User, related_name='attributes')
    label = models.ForeignKey(UserAttributeClassifierLabel, related_name='+')
    value = models.CharField(max_length=200)

All the user attributes/properties are editable from the admin panel.

  • Each group has validation on type of data, and you can add custom validation based on regex expression (check phone attribute in django-classifier-profile demo project)
  • Each attribute can have a bunch of labels. For example attribute “social link” may have: “facebook”, “twitter”, “Instagram”, etc.
  • You can set what labels are required or set that only one of the labels is required

User attributes are presented in the admin user edit form as general Django admin inlines.

On the frontend it works the same way with the help of Django formsets.

Django formsets listing

class UserClassifierFormSet(ClassifierFormSet):  
    """
    extended with one additional method to get forms grouped by category
    """

    @cached_property
    def grouped_forms(self):
        """
        grouped forms by category and filter options of kind field
        """
        self.empty_forms = {}
        forms = {}
        for kind in UserAttributeClassifier.objects.all():
            self.create_empty_form(kind)
            labels = list(kind.labels.values_list('pk', flat=True))
            forms[kind.category] = []
            for form in self.forms:
                label_id = None
                try:
                    label_id = form.cleaned_data['label'].pk
                except (AttributeError, KeyError):
                    label_id = form.data.get('label', form.initial.get('label'))

                if label_id in labels:
                    form.fields['label'].choices = kind.labels.values_list(
                        'pk', 'label'
                    )
                    forms[kind.category].append(form)

        return sorted(forms.items(), key=lambda i: i[0])

    def create_empty_form(self, kind):
        """
        returns empty form for specific attribute
        """
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True,
            use_required_attribute=False,
            **self.get_form_kwargs(None)
        )
        self.add_fields(form, None)
        form.fields['label'].choices = kind.labels.values_list('pk', 'label')
        form.fields['user'].initial = self.initial_extra[0]['user']

        self.empty_forms[kind.category] = form

Demo Project #2

My second demo project doesn’t extend classifier models. The main idea here is to show what possibilities it gives. It is a very basic online shop with the products that have properties built with the help of django-classifier.

Here I use attributes to build facet search for products, and I think that’s pretty cool. As soon as you add a new attribute to the product — it allows you to apply filters by that attribute without making any changes to the code.

All the “how to build it” logic is based on the two signal receivers. First one to reindex product, and the other one to update ElasticSearch mapping on attributes configuration changes.

Product app has a method to update mapping on ElasticSearch that sets keyword type for all product attributes, and in the view we use terms aggregations to build filters.

Outro

Django-classifier is a flexible constructor to create dynamic list of heterogeneous properties for various entities. This set of helpers is useful for creating contacts properties or attributes for descriptions of any kind of products. You’ll need to create your own models for each kind of entity, but it gives you more flexibility and allows you to easily change default behavior.

This manual originally posted in Django Stars Blog — join the discussion and ask if you have a questions