mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Removed multi-dimensional item variations [backwards-incompatible]
This commit is contained in:
@@ -2,8 +2,7 @@ from .auth import User
|
||||
from .base import CachedFile, cachedfile_name
|
||||
from .event import Event, EventLock, EventPermission, EventSetting
|
||||
from .items import (
|
||||
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
|
||||
Quota, VariationsField, itempicture_upload_to,
|
||||
Item, ItemCategory, ItemVariation, Question, Quota, itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .orders import (
|
||||
@@ -14,8 +13,7 @@ from .organizer import Organizer, OrganizerPermission, OrganizerSetting
|
||||
|
||||
__all__ = [
|
||||
'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
||||
'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question',
|
||||
'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition',
|
||||
'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to',
|
||||
'generate_secret', 'LogEntry'
|
||||
'ItemCategory', 'Item', 'ItemVariation', 'Question', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer',
|
||||
'ObjectWithAnswers', 'OrderPosition', 'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock',
|
||||
'cachedfile_name', 'itempicture_upload_to', 'generate_secret', 'LogEntry'
|
||||
]
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from itertools import product
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q, Case, Count, Sum, When
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from typing import List, Tuple
|
||||
from typing import Tuple
|
||||
|
||||
from pretix.base.i18n import I18nCharField, I18nTextField
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
from ..types import VariationDict
|
||||
from .event import Event
|
||||
|
||||
|
||||
@@ -196,117 +194,6 @@ class Item(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_all_variations(self, use_cache: bool=False) -> List[VariationDict]:
|
||||
"""
|
||||
This method returns a list containing all variations of this
|
||||
item. The list contains one VariationDict per variation, where
|
||||
the Proprty IDs are keys and the PropertyValue objects are
|
||||
values. If an ItemVariation object exists, it is available in
|
||||
the dictionary via the special key 'variation'.
|
||||
|
||||
VariationDicts differ from dicts only by specifying some extra
|
||||
methods.
|
||||
|
||||
:param use_cache: If this parameter is set to ``True``, a second call to this method
|
||||
on the same model instance won't query the database again but return
|
||||
the previous result again.
|
||||
:type use_cache: bool
|
||||
"""
|
||||
if use_cache and hasattr(self, '_get_all_variations_cache'):
|
||||
return self._get_all_variations_cache
|
||||
|
||||
all_variations = self.variations.all().prefetch_related("values")
|
||||
all_properties = self.properties.all().prefetch_related("values")
|
||||
variations_cache = {}
|
||||
for var in all_variations:
|
||||
key = []
|
||||
for v in var.values.all():
|
||||
key.append((v.prop_id, v.id))
|
||||
key = tuple(sorted(key))
|
||||
variations_cache[key] = var
|
||||
|
||||
result = []
|
||||
for comb in product(*[prop.values.all() for prop in all_properties]):
|
||||
if len(comb) == 0:
|
||||
result.append(VariationDict())
|
||||
continue
|
||||
key = []
|
||||
var = VariationDict()
|
||||
for v in comb:
|
||||
key.append((v.prop.id, v.id))
|
||||
var[v.prop.id] = v
|
||||
key = tuple(sorted(key))
|
||||
if key in variations_cache:
|
||||
var['variation'] = variations_cache[key]
|
||||
result.append(var)
|
||||
|
||||
self._get_all_variations_cache = result
|
||||
return result
|
||||
|
||||
def _get_all_generated_variations(self):
|
||||
propids = set([p.id for p in self.properties.all()])
|
||||
if len(propids) == 0:
|
||||
variations = [VariationDict()]
|
||||
else:
|
||||
all_variations = list(
|
||||
self.variations.annotate(
|
||||
qc=Count('quotas')
|
||||
).filter(qc__gt=0).prefetch_related(
|
||||
"values", "values__prop", "quotas__event"
|
||||
)
|
||||
)
|
||||
variations = []
|
||||
for var in all_variations:
|
||||
values = list(var.values.all())
|
||||
# Make sure we don't expose stale ItemVariation objects which are
|
||||
# still around altough they have an old set of properties
|
||||
if set([v.prop.id for v in values]) != propids:
|
||||
continue
|
||||
vardict = VariationDict()
|
||||
for v in values:
|
||||
vardict[v.prop.id] = v
|
||||
vardict['variation'] = var
|
||||
variations.append(vardict)
|
||||
return variations
|
||||
|
||||
def get_all_available_variations(self, use_cache: bool=False):
|
||||
"""
|
||||
This method returns a list of all variations which are theoretically
|
||||
possible for sale. It DOES only return variations which DO have an ItemVariation
|
||||
object, as all variations without one CAN NOT be part of a Quota and therefore can
|
||||
never be available for sale. The only exception is the empty variation
|
||||
for items without properties, which never has an ItemVariation object.
|
||||
|
||||
This DOES NOT take into account quotas itself. Use ``is_available`` on the
|
||||
ItemVariation objects (or the Item it self, if it does not have variations) to
|
||||
determine availability by the terms of quotas.
|
||||
|
||||
It is recommended to call::
|
||||
|
||||
.prefetch_related('properties', 'variations__values__prop')
|
||||
|
||||
when retrieving Item objects you are going to use this method on.
|
||||
"""
|
||||
if use_cache and hasattr(self, '_get_all_available_variations_cache'):
|
||||
return self._get_all_available_variations_cache
|
||||
|
||||
variations = self._get_all_generated_variations()
|
||||
|
||||
for i, var in enumerate(variations):
|
||||
var['available'] = var['variation'].active if 'variation' in var else True
|
||||
if 'variation' in var:
|
||||
if var['variation'].default_price is not None:
|
||||
var['price'] = var['variation'].default_price
|
||||
else:
|
||||
var['price'] = self.default_price
|
||||
else:
|
||||
var['price'] = self.default_price
|
||||
|
||||
variations = [var for var in variations if var['available']]
|
||||
|
||||
self._get_all_available_variations_cache = variations
|
||||
return variations
|
||||
|
||||
def check_quotas(self):
|
||||
"""
|
||||
This method is used to determine whether this Item is currently available
|
||||
@@ -314,130 +201,24 @@ class Item(LoggedModel):
|
||||
|
||||
:returns: any of the return codes of :py:meth:`Quota.availability()`.
|
||||
|
||||
:raises ValueError: if you call this on an item which has properties associated with it.
|
||||
:raises ValueError: if you call this on an item which has variations associated with it.
|
||||
Please use the method on the ItemVariation object you are interested in.
|
||||
"""
|
||||
if self.properties.count() > 0: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have properties '
|
||||
if self.variations.count() > 0: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have variations '
|
||||
'but call this on their ItemVariation objects')
|
||||
return min([q.availability() for q in self.quotas.all()],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
|
||||
|
||||
class Property(models.Model):
|
||||
"""
|
||||
A property is a modifier which can be applied to an Item. For example
|
||||
'Size' would be a property associated with the item 'T-Shirt'.
|
||||
|
||||
:param event: The event this belongs to
|
||||
:type event: Event
|
||||
:param name: The name of this property.
|
||||
:type name: str
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
related_name="properties"
|
||||
)
|
||||
item = models.ForeignKey(
|
||||
Item, related_name='properties', null=True, blank=True
|
||||
)
|
||||
name = I18nCharField(
|
||||
max_length=250,
|
||||
verbose_name=_("Property name")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Product property")
|
||||
verbose_name_plural = _("Product properties")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
|
||||
class PropertyValue(models.Model):
|
||||
"""
|
||||
A value of a property. If the property would be 'T-Shirt size',
|
||||
this could be 'M' or 'L'.
|
||||
|
||||
:param prop: The property this value is a valid option for.
|
||||
:type prop: Property
|
||||
:param value: The value, as a human-readable string
|
||||
:type value: str
|
||||
:param position: An integer, used for sorting
|
||||
:type position: int
|
||||
"""
|
||||
|
||||
prop = models.ForeignKey(
|
||||
Property,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="values"
|
||||
)
|
||||
value = I18nCharField(
|
||||
max_length=250,
|
||||
verbose_name=_("Value"),
|
||||
)
|
||||
position = models.IntegerField(
|
||||
default=0
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Property value")
|
||||
verbose_name_plural = _("Property values")
|
||||
ordering = ("position", "id")
|
||||
|
||||
def __str__(self):
|
||||
return "%s: %s" % (self.prop.name, self.value)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.prop:
|
||||
self.prop.event.get_cache().clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.prop:
|
||||
self.prop.event.get_cache().clear()
|
||||
|
||||
@property
|
||||
def sortkey(self) -> Tuple[int, datetime]:
|
||||
return self.position, self.id
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
return self.sortkey < other.sortkey
|
||||
|
||||
|
||||
class ItemVariation(models.Model):
|
||||
"""
|
||||
A variation is an item combined with values for all properties
|
||||
associated with the item. For example, if your item is 'T-Shirt'
|
||||
and your properties are 'Size' and 'Color', then an example for an
|
||||
variation would be 'T-Shirt XL read'.
|
||||
|
||||
Attention: _ALL_ combinations of PropertyValues _ALWAYS_ exist,
|
||||
even if there is no ItemVariation object for them! ItemVariation objects
|
||||
do NOT prove existance, they are only available to make it possible
|
||||
to override default values (like the price) for certain combinations
|
||||
of property values. However, appropriate ItemVariation objects will be
|
||||
created as soon as you add your variations to a quota.
|
||||
|
||||
They also allow to explicitly EXCLUDE certain combinations of property
|
||||
values by creating an ItemVariation object for them with active set to
|
||||
False.
|
||||
A variation of a product. For example, if your item is 'T-Shirt'
|
||||
then an example for a variation would be 'T-Shirt XL'.
|
||||
|
||||
:param item: The item this variation belongs to
|
||||
:type item: Item
|
||||
:param values: A set of ``PropertyValue`` objects defining this variation
|
||||
:param value: A string defining this variation
|
||||
:param active: Whether this value is to be sold.
|
||||
:type active: bool
|
||||
:param default_price: This variation's default price
|
||||
@@ -447,14 +228,18 @@ class ItemVariation(models.Model):
|
||||
Item,
|
||||
related_name='variations'
|
||||
)
|
||||
values = models.ManyToManyField(
|
||||
PropertyValue,
|
||||
related_name='variations',
|
||||
value = I18nCharField(
|
||||
max_length=255,
|
||||
verbose_name=_('Description')
|
||||
)
|
||||
active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Active"),
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
default_price = models.DecimalField(
|
||||
decimal_places=2, max_digits=7,
|
||||
null=True, blank=True,
|
||||
@@ -464,9 +249,10 @@ class ItemVariation(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Product variation")
|
||||
verbose_name_plural = _("Product variations")
|
||||
ordering = ("position", "id")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_variation_dict())
|
||||
return str(self.value)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -488,56 +274,10 @@ class ItemVariation(models.Model):
|
||||
return min([q.availability() for q in self.quotas.all()],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
|
||||
def to_variation_dict(self) -> VariationDict:
|
||||
"""
|
||||
:return: a :py:class:`VariationDict` representing this variation.
|
||||
"""
|
||||
vd = VariationDict()
|
||||
for v in self.values.all():
|
||||
vd[v.prop.id] = v
|
||||
vd['variation'] = self
|
||||
return vd
|
||||
|
||||
def add_values_from_string(self, pk):
|
||||
"""
|
||||
Add values to this ItemVariation using a serialized string of the form
|
||||
``property-id:value-id,ṗroperty-id:value-id``
|
||||
"""
|
||||
for pair in pk.split(","):
|
||||
prop, value = pair.split(":")
|
||||
self.values.add(
|
||||
PropertyValue.objects.get(
|
||||
id=value,
|
||||
prop_id=prop
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VariationsField(models.ManyToManyField):
|
||||
"""
|
||||
This is a ManyToManyField using the pretixcontrol.views.forms.VariationsField
|
||||
form field by default.
|
||||
"""
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
from pretix.control.forms import VariationsField as FVariationsField
|
||||
from django.db.models.fields.related import RelatedField
|
||||
|
||||
defaults = {
|
||||
'form_class': FVariationsField,
|
||||
# We don't need a queryset
|
||||
'queryset': ItemVariation.objects.none(),
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
# If initial is passed in, it's a list of related objects, but the
|
||||
# MultipleChoiceField takes a list of IDs.
|
||||
if defaults.get('initial') is not None:
|
||||
initial = defaults['initial']
|
||||
if callable(initial):
|
||||
initial = initial()
|
||||
defaults['initial'] = [i.id for i in initial]
|
||||
# Skip ManyToManyField in dependency chain
|
||||
return super(RelatedField, self).formfield(**defaults)
|
||||
def __lt__(self, other):
|
||||
if self.position == other.position:
|
||||
return self.id < other.id
|
||||
return self.position < other.position
|
||||
|
||||
|
||||
class Question(LoggedModel):
|
||||
@@ -686,7 +426,7 @@ class Quota(LoggedModel):
|
||||
related_name="quotas",
|
||||
blank=True
|
||||
)
|
||||
variations = VariationsField(
|
||||
variations = models.ManyToManyField(
|
||||
ItemVariation,
|
||||
related_name="quotas",
|
||||
blank=True,
|
||||
|
||||
Reference in New Issue
Block a user