Skip to content

Commit

Permalink
Attribute related changes
Browse files Browse the repository at this point in the history
- Admin: New type of attribute: CHOICES
- Admin: Selection of multiple choices from attribute for product
- Front: Product filter by attribute choices
  • Loading branch information
Ilya Chaban committed Apr 19, 2021
1 parent dc7a606 commit 0d8848a
Show file tree
Hide file tree
Showing 17 changed files with 502 additions and 63 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

List all changes after the last release here (newer on top). Each change on a separate bullet point line

### Added

- Admin: New type of attribute: CHOICES
- Admin: Selection of multiple choices from attribute for product
- Front: Product filter by attribute choices

## [2.6.3] - 2021-04-13

### Changed
Expand Down
5 changes: 5 additions & 0 deletions shuup/admin/__init__.py
Expand Up @@ -22,6 +22,11 @@ class ShuupAdminAppConfig(AppConfig):
"shuup.admin.modules.products.views.edit.ProductImageMediaFormPart",
"shuup.admin.modules.products.views.edit.ProductMediaFormPart",
],
"admin_attribute_form_part": [
"shuup.admin.modules.attributes.form_parts.AttributeBaseFormPart",
"shuup.admin.modules.attributes.form_parts.AttributeChoiceSettingsFormPart",
"shuup.admin.modules.attributes.form_parts.AttributeChoiceOptionsFormPart",
],
"admin_module": [
"shuup.admin.modules.system:SystemModule",
"shuup.admin.modules.products:ProductModule",
Expand Down
74 changes: 74 additions & 0 deletions shuup/admin/modules/attributes/form_parts.py
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from django.conf import settings

from shuup.admin.form_part import FormPart, TemplatedFormDef

from .forms import AttributeChoiceOptionFormSet, AttributeChoiceSettingsForm, AttributeForm


class AttributeBaseFormPart(FormPart):
priority = 1

def get_form_defs(self):
yield TemplatedFormDef(
"base",
AttributeForm,
template_name="shuup/admin/attributes/_edit_base_form.jinja",
required=True,
kwargs={
"instance": self.object,
"request": self.request,
"languages": settings.LANGUAGES,
},
)

def form_valid(self, form):
self.object = form["base"].save()


class AttributeChoiceSettingsFormPart(FormPart):
priority = 2

def get_form_defs(self):
yield TemplatedFormDef(
"choice_settings",
AttributeChoiceSettingsForm,
template_name="shuup/admin/attributes/_edit_choice_settings_form.jinja",
required=True,
kwargs={
"instance": self.object,
"request": self.request,
},
)

def form_valid(self, form):
self.object = form["base"].save()


class AttributeChoiceOptionsFormPart(FormPart):
name = "choice_options"
priority = 3
formset = AttributeChoiceOptionFormSet

def get_form_defs(self):
if not self.object.pk:
return

yield TemplatedFormDef(
self.name,
self.formset,
template_name="shuup/admin/attributes/_edit_choice_option_form.jinja",
required=False,
kwargs={"attribute": self.object, "languages": settings.LANGUAGES, "request": self.request},
)

def form_valid(self, form):
if self.name in form.forms:
frm = form.forms[self.name]
frm.save()
82 changes: 82 additions & 0 deletions shuup/admin/modules/attributes/forms.py
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from django import forms
from django.conf import settings
from django.forms import BaseModelFormSet

from shuup.admin.form_modifier import ModifiableFormMixin
from shuup.core.models import Attribute, AttributeChoiceOption
from shuup.utils.multilanguage_model_form import MultiLanguageModelForm, TranslatableModelForm


class AttributeForm(ModifiableFormMixin, MultiLanguageModelForm):
form_modifier_provide_key = "admin_extend_attribute_form"

class Meta:
model = Attribute
exclude = ("max_choices",)

def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)


class AttributeChoiceSettingsForm(forms.ModelForm):
class Meta:
model = Attribute
fields = ("max_choices",)

def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)


class AttributeChoiceOptionForm(MultiLanguageModelForm):
class Meta:
model = AttributeChoiceOption
fields = ("name",)

def __init__(self, *args, **kwargs):
self.attribute = kwargs.pop("attribute")
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)

def save(self, commit=True):
self.instance.attribute = self.attribute
super().save(commit)


class AttributeChoiceOptionFormSet(BaseModelFormSet):
model = AttributeChoiceOption
form_class = AttributeChoiceOptionForm

validate_min = False
min_num = 0
validate_max = False
max_num = 100
absolute_max = 100
can_delete = True
can_order = False
extra = 3

def __init__(self, **kwargs):
self.attribute = kwargs.pop("attribute")
self.languages = kwargs.pop("languages")
self.request = kwargs.pop("request")
super().__init__(**kwargs)

def form(self, **kwargs):
kwargs.setdefault("attribute", self.attribute)
kwargs.setdefault("request", self.request)
if issubclass(self.form_class, TranslatableModelForm):
kwargs.setdefault("languages", settings.LANGUAGES)
kwargs.setdefault("default_language", settings.PARLER_DEFAULT_LANGUAGE_CODE)
return self.form_class(**kwargs)

def get_queryset(self):
return super().get_queryset().filter(attribute=self.attribute)
22 changes: 10 additions & 12 deletions shuup/admin/modules/attributes/views/edit.py
Expand Up @@ -6,38 +6,36 @@
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from django.contrib import messages
from django.db.transaction import atomic
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView

from shuup.admin.form_modifier import ModifiableFormMixin, ModifiableViewMixin
from shuup.admin.form_part import FormPartsViewMixin, SaveFormPartsMixin
from shuup.admin.toolbar import get_default_edit_toolbar
from shuup.admin.utils.views import CreateOrUpdateView
from shuup.core.models import Attribute
from shuup.utils.multilanguage_model_form import MultiLanguageModelForm


class AttributeForm(ModifiableFormMixin, MultiLanguageModelForm):
form_modifier_provide_key = "admin_extend_attribute_form"

class Meta:
model = Attribute
exclude = () # All the fields!


class AttributeEditView(ModifiableViewMixin, CreateOrUpdateView):
class AttributeEditView(SaveFormPartsMixin, FormPartsViewMixin, CreateOrUpdateView):
model = Attribute
form_class = AttributeForm
template_name = "shuup/admin/attributes/edit.jinja"
context_object_name = "attribute"
base_form_part_classes = []
form_part_class_provide_key = "admin_attribute_form_part"
add_form_errors_as_messages = True

def get_toolbar(self):
object = self.get_object()
delete_url = reverse_lazy("shuup_admin:attribute.delete", kwargs={"pk": object.pk}) if object.pk else None
return get_default_edit_toolbar(self, self.get_save_form_id(), delete_url=delete_url)

@atomic
def form_valid(self, form):
return self.save_form_parts(form)


class AttributeDeleteView(DetailView):
model = Attribute
Expand Down
14 changes: 10 additions & 4 deletions shuup/admin/modules/products/forms/base_forms.py
Expand Up @@ -334,13 +334,17 @@ def __init__(self, **kwargs):
self.trans_name_map = defaultdict(dict)
self.translated_field_names = []
super(ProductAttributesForm, self).__init__(**kwargs)
if self.product.pk:
self.applied_attrs = dict((pa.attribute_id, pa) for pa in self.product.attributes.all())
else:
self.applied_attrs = {}
self.applied_attrs = self._get_applied_attributes()
self._field_languages = {}
self._build_fields()

def _get_applied_attributes(self):
applied_attrs = {}
if self.product.pk:
for pa in self.product.attributes.select_related("attribute").prefetch_related("chosen_options"):
applied_attrs[pa.attribute_id] = pa
return applied_attrs

def _build_fields(self):
for attribute in self.attributes:
self._field_languages[attribute.identifier] = {}
Expand All @@ -352,6 +356,8 @@ def _build_fields(self):
if pa:
if attribute.type == AttributeType.TIMEDELTA: # Special case.
value = pa.numeric_value
elif attribute.type == AttributeType.CHOICES:
value = [choice.id for choice in pa.chosen_options.all()]
else:
value = pa.value
self.initial[attribute.identifier] = value
Expand Down
@@ -0,0 +1,9 @@
{% from "shuup/admin/macros/general.jinja" import content_block %}
{% from "shuup/admin/macros/multilanguage.jinja" import language_dependent_content_tabs, render_monolingual_fields %}
{% set base_form = form["base"] %}

{% call content_block(_("General Information"), icon="fa-info-circle") %}
{{ language_dependent_content_tabs(base_form) }}
<div class="form-divider"></div>
{{ render_monolingual_fields(base_form, exclude=["name"]) }}
{% endcall %}
@@ -0,0 +1,21 @@
{% from "shuup/admin/macros/general.jinja" import content_block %}
{% from "shuup/admin/macros/multilanguage.jinja" import language_dependent_content_tabs, render_monolingual_fields %}


{% call content_block(_("Choice Options"), "fa-users") %}
{% set formset = form["choice_options"] %}
{{ formset.management_form }}
{% for form in formset %}
<table>
<tr>
<td>
{% set form_prefix = "choice_options"+ "-" + (loop.index - 1)|string %}
{% set full_prefix = form_prefix + idx|string if idx == "__prefix__" else form_prefix %}
{{ language_dependent_content_tabs(form, tab_id_prefix=full_prefix) }}
<div class="form-divider"></div>
{{ render_monolingual_fields(form, exclude=["name"]) }}
</td>
</tr>
</table>
{% endfor %}
{% endcall %}
@@ -0,0 +1,7 @@
{% from "shuup/admin/macros/general.jinja" import content_block %}
{% from "shuup/admin/macros/multilanguage.jinja" import language_dependent_content_tabs, render_monolingual_fields %}
{% set choice_settings_form = form["choice_settings"] %}

{% call content_block(_("Choice Settings"), icon="fa-info-circle") %}
{{ render_monolingual_fields(choice_settings_form) }}
{% endcall %}
14 changes: 8 additions & 6 deletions shuup/admin/templates/shuup/admin/attributes/edit.jinja
@@ -1,12 +1,14 @@
{% extends "shuup/admin/base.jinja" %}
{% from "shuup/admin/macros/general.jinja" import single_section_form with context %}
{% from "shuup/admin/macros/multilanguage.jinja" import language_dependent_content_tabs, render_monolingual_fields %}
{% block title %}{{ attribute.name or _("New Attribute") }}{% endblock %}
{% from "shuup/admin/macros/general.jinja" import content_with_sidebar %}

{% block body_class %}shuup-details{% endblock %}
{% block content %}
{% call single_section_form("attribute_form", form) %}
{{ language_dependent_content_tabs(form) }}
{{ render_monolingual_fields(form) }}
{% call content_with_sidebar(content_id="attribute_form") %}
<form method="post" id="attribute_form" novalidate>
{% csrf_token %}
{% for form_def in form.form_defs.values() %}
{% include form_def.template_name with context %}
{% endfor %}
</form>
{% endcall %}
{% endblock %}
55 changes: 55 additions & 0 deletions shuup/core/migrations/0086_auto_20210415_0847.py
@@ -0,0 +1,55 @@
# Generated by Django 2.2.18 on 2021-04-15 08:47

import django.db.models.deletion
import parler.fields
import parler.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('shuup', '0085_longer_order_line_sku'),
]

operations = [
migrations.AddField(
model_name='attribute',
name='max_choices',
field=models.PositiveIntegerField(default=1, help_text='Amount of choices that user can choose from existing options.', verbose_name='max choices'),
),
migrations.CreateModel(
name='AttributeChoiceOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='shuup.Attribute', verbose_name='attribute')),
],
options={
'abstract': False,
},
bases=(parler.models.TranslatableModelMixin, models.Model),
),
migrations.AddField(
model_name='productattribute',
name='chosen_options',
field=models.ManyToManyField(to='shuup.AttributeChoiceOption', verbose_name='chosen options'),
),
migrations.CreateModel(
name='AttributeChoiceOptionTranslation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language_code', models.CharField(db_index=True, max_length=15, verbose_name='Language')),
('name', models.CharField(help_text='The attribute choice option name. ', max_length=256, verbose_name='name')),
('master', parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='shuup.AttributeChoiceOption')),
],
options={
'verbose_name': 'attribute choice option Translation',
'db_table': 'shuup_attributechoiceoption_translation',
'db_tablespace': '',
'managed': True,
'default_permissions': (),
'unique_together': {('language_code', 'master')},
},
bases=(parler.models.TranslatedFieldsModelMixin, models.Model),
),
]
18 changes: 18 additions & 0 deletions shuup/core/migrations/0087_attribute_ordering.py
@@ -0,0 +1,18 @@
# Generated by Django 2.2.18 on 2021-04-15 08:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('shuup', '0086_auto_20210415_0847'),
]

operations = [
migrations.AddField(
model_name='attribute',
name='ordering',
field=models.IntegerField(default=0, help_text='The ordering in which your attribute will be displayed.'),
),
]

0 comments on commit 0d8848a

Please sign in to comment.