Removed CleanerVersion layer [backwards-incompatible!]

This commit is contained in:
Raphael Michel
2015-12-12 13:08:33 +01:00
parent 0c9c9dd22c
commit d133d2abff
85 changed files with 712 additions and 1089 deletions

View File

@@ -1,5 +1,5 @@
from .auth import User
from .base import CachedFile, Versionable, cachedfile_name
from .base import CachedFile, cachedfile_name
from .event import Event, EventLock, EventPermission, EventSetting
from .items import (
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
@@ -12,7 +12,7 @@ from .orders import (
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
__all__ = [
'Versionable', 'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
'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',

View File

@@ -1,71 +1,12 @@
import copy
import uuid
from datetime import datetime
import six
from django.db import models
from django.db.models.signals import post_delete
from django.dispatch import receiver
from versions.models import Versionable as BaseVersionable, get_utc_now
class Versionable(BaseVersionable):
class Meta:
abstract = True
def clone_shallow(self, forced_version_date: datetime=None):
"""
This behaves like clone(), but misses all the Many2Many-relation-handling. This is
a performance optimization for cases in which we have to handle the Many2Many relations
by hand anyways.
"""
if not self.pk: # NOQA
raise ValueError('Instance must be saved before it can be cloned')
if self.version_end_date: # NOQA
raise ValueError('This is a historical item and can not be cloned.')
if forced_version_date: # NOQA
if not self.version_start_date <= forced_version_date <= get_utc_now():
raise ValueError('The clone date must be between the version start date and now.')
else:
forced_version_date = get_utc_now()
earlier_version = self
later_version = copy.copy(earlier_version)
later_version.version_end_date = None
later_version.version_start_date = forced_version_date
# set earlier_version's ID to a new UUID so the clone (later_version) can
# get the old one -- this allows 'head' to always have the original
# id allowing us to get at all historic foreign key relationships
earlier_version.id = six.u(str(uuid.uuid4()))
earlier_version.version_end_date = forced_version_date
earlier_version.save()
for field in earlier_version._meta.many_to_many:
earlier_version.clone_relations_shallow(later_version, field.attname, forced_version_date)
if hasattr(earlier_version._meta, 'many_to_many_related'):
for rel in earlier_version._meta.many_to_many_related:
earlier_version.clone_relations_shallow(later_version, rel.via_field_name, forced_version_date)
later_version.save()
return later_version
def clone_relations_shallow(self, clone, manager_field_name, forced_version_date):
# Source: the original object, where relations are currently pointing to
source = getattr(self, manager_field_name) # returns a VersionedRelatedManager instance
# Destination: the clone, where the cloned relations should point to
source.through.objects.filter(**{source.source_field.attname: clone.id}).update(**{
source.source_field.attname: self.id, 'version_end_date': forced_version_date
})
def cachedfile_name(instance, filename: str) -> str:
return 'cachedfiles/%s.%s' % (instance.id, filename.split('.')[-1])
return 'cachedfiles/%012d.%s' % (instance.id, filename.split('.')[-1])
class CachedFile(models.Model):

View File

@@ -9,17 +9,15 @@ from django.template.defaultfilters import date as _date
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from versions.models import VersionedForeignKey
from pretix.base.i18n import I18nCharField
from pretix.base.settings import SettingsProxy
from .auth import User
from .base import Versionable
from .organizer import Organizer
class Event(Versionable):
class Event(models.Model):
"""
This model represents an event. An event is anything you can buy
tickets for.
@@ -46,8 +44,7 @@ class Event(Versionable):
:type plugins: str
"""
organizer = VersionedForeignKey(Organizer, related_name="events",
on_delete=models.PROTECT)
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
name = I18nCharField(
max_length=200,
verbose_name=_("Name"),
@@ -184,7 +181,7 @@ class Event(Versionable):
return locking.LockManager(self)
class EventPermission(Versionable):
class EventPermission(models.Model):
"""
The relation between an Event and an User who has permissions to
access an event.
@@ -203,8 +200,8 @@ class EventPermission(Versionable):
:type can_change_orders: bool
"""
event = VersionedForeignKey(Event)
user = models.ForeignKey(User, related_name="event_perms")
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE)
can_change_settings = models.BooleanField(
default=True,
verbose_name=_("Can change event settings")
@@ -237,12 +234,12 @@ class EventPermission(Versionable):
}
class EventSetting(Versionable):
class EventSetting(models.Model):
"""
An event settings is a key-value setting which can be set for a
specific event
"""
object = VersionedForeignKey(Event, related_name='setting_objects')
object = models.ForeignKey(Event, related_name='setting_objects', on_delete=models.CASCADE)
key = models.CharField(max_length=255)
value = models.TextField()

View File

@@ -8,16 +8,14 @@ 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 versions.models import VersionedForeignKey, VersionedManyToManyField
from pretix.base.i18n import I18nCharField, I18nTextField
from ..types import VariationDict
from .base import Versionable
from .event import Event
class ItemCategory(Versionable):
class ItemCategory(models.Model):
"""
Items can be sorted into these categories.
@@ -28,7 +26,7 @@ class ItemCategory(Versionable):
:param position: An integer, used for sorting
:type position: int
"""
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name='categories',
@@ -44,7 +42,7 @@ class ItemCategory(Versionable):
class Meta:
verbose_name = _("Product category")
verbose_name_plural = _("Product categories")
ordering = ('position', 'version_birth_date')
ordering = ('position', 'id')
def __str__(self):
return str(self.name)
@@ -61,7 +59,7 @@ class ItemCategory(Versionable):
@property
def sortkey(self):
return self.position, self.version_birth_date
return self.position, self.id
def __lt__(self, other) -> bool:
return self.sortkey < other.sortkey
@@ -69,12 +67,12 @@ class ItemCategory(Versionable):
def itempicture_upload_to(instance, filename: str) -> str:
return '%s/%s/item-%s.%s' % (
instance.event.organizer.slug, instance.event.slug, instance.identity,
instance.event.organizer.slug, instance.event.slug, instance.id,
filename.split('.')[-1]
)
class Item(Versionable):
class Item(models.Model):
"""
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
Items are often also called 'products' but are named 'items' internally due to historic reasons.
@@ -104,13 +102,13 @@ class Item(Versionable):
"""
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
on_delete=models.PROTECT,
related_name="items",
verbose_name=_("Event"),
)
category = VersionedForeignKey(
category = models.ForeignKey(
ItemCategory,
on_delete=models.PROTECT,
related_name="items",
@@ -222,7 +220,7 @@ class Item(Versionable):
for var in all_variations:
key = []
for v in var.values.all():
key.append((v.prop_id, v.identity))
key.append((v.prop_id, v.id))
key = tuple(sorted(key))
variations_cache[key] = var
@@ -234,8 +232,8 @@ class Item(Versionable):
key = []
var = VariationDict()
for v in comb:
key.append((v.prop.identity, v.identity))
var[v.prop.identity] = v
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]
@@ -245,7 +243,7 @@ class Item(Versionable):
return result
def _get_all_generated_variations(self):
propids = set([p.identity for p in self.properties.all()])
propids = set([p.id for p in self.properties.all()])
if len(propids) == 0:
variations = [VariationDict()]
else:
@@ -261,11 +259,11 @@ class Item(Versionable):
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.identity for v in values]) != propids:
if set([v.prop.id for v in values]) != propids:
continue
vardict = VariationDict()
for v in values:
vardict[v.prop.identity] = v
vardict[v.prop.id] = v
vardict['variation'] = var
variations.append(vardict)
return variations
@@ -325,7 +323,7 @@ class Item(Versionable):
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
class Property(Versionable):
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'.
@@ -336,11 +334,11 @@ class Property(Versionable):
:type name: str
"""
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
related_name="properties"
)
item = VersionedForeignKey(
item = models.ForeignKey(
Item, related_name='properties', null=True, blank=True
)
name = I18nCharField(
@@ -366,7 +364,7 @@ class Property(Versionable):
self.event.get_cache().clear()
class PropertyValue(Versionable):
class PropertyValue(models.Model):
"""
A value of a property. If the property would be 'T-Shirt size',
this could be 'M' or 'L'.
@@ -379,7 +377,7 @@ class PropertyValue(Versionable):
:type position: int
"""
prop = VersionedForeignKey(
prop = models.ForeignKey(
Property,
on_delete=models.CASCADE,
related_name="values"
@@ -395,7 +393,7 @@ class PropertyValue(Versionable):
class Meta:
verbose_name = _("Property value")
verbose_name_plural = _("Property values")
ordering = ("position", "version_birth_date")
ordering = ("position", "id")
def __str__(self):
return "%s: %s" % (self.prop.name, self.value)
@@ -412,13 +410,13 @@ class PropertyValue(Versionable):
@property
def sortkey(self) -> Tuple[int, datetime]:
return self.position, self.version_birth_date
return self.position, self.id
def __lt__(self, other) -> bool:
return self.sortkey < other.sortkey
class ItemVariation(Versionable):
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'
@@ -444,11 +442,11 @@ class ItemVariation(Versionable):
:param default_price: This variation's default price
:type default_price: decimal.Decimal
"""
item = VersionedForeignKey(
item = models.ForeignKey(
Item,
related_name='variations'
)
values = VersionedManyToManyField(
values = models.ManyToManyField(
PropertyValue,
related_name='variations',
)
@@ -495,7 +493,7 @@ class ItemVariation(Versionable):
"""
vd = VariationDict()
for v in self.values.all():
vd[v.prop.identity] = v
vd[v.prop.id] = v
vd['variation'] = self
return vd
@@ -507,14 +505,14 @@ class ItemVariation(Versionable):
for pair in pk.split(","):
prop, value = pair.split(":")
self.values.add(
PropertyValue.objects.current.get(
identity=value,
PropertyValue.objects.get(
id=value,
prop_id=prop
)
)
class VariationsField(VersionedManyToManyField):
class VariationsField(models.ManyToManyField):
"""
This is a ManyToManyField using the pretixcontrol.views.forms.VariationsField
form field by default.
@@ -536,12 +534,12 @@ class VariationsField(VersionedManyToManyField):
initial = defaults['initial']
if callable(initial):
initial = initial()
defaults['initial'] = [i.identity for i in initial]
defaults['initial'] = [i.id for i in initial]
# Skip ManyToManyField in dependency chain
return super(RelatedField, self).formfield(**defaults)
class Question(Versionable):
class Question(models.Model):
"""
A question is an input field that can be used to extend a ticket
by custom information, e.g. "Attendee age". A question can allow one o several
@@ -573,7 +571,7 @@ class Question(Versionable):
(TYPE_BOOLEAN, _("Yes/No")),
)
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
related_name="questions"
)
@@ -589,7 +587,7 @@ class Question(Versionable):
default=False,
verbose_name=_("Required question")
)
items = VersionedManyToManyField(
items = models.ManyToManyField(
Item,
related_name='questions',
verbose_name=_("Products"),
@@ -615,7 +613,7 @@ class Question(Versionable):
self.event.get_cache().clear()
class Quota(Versionable):
class Quota(models.Model):
"""
A quota is a "pool of tickets". It is there to limit the number of items
of a certain type to be sold. For example, you could have a quota of 500
@@ -666,7 +664,7 @@ class Quota(Versionable):
AVAILABILITY_RESERVED = 20
AVAILABILITY_OK = 100
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name="quotas",
@@ -681,7 +679,7 @@ class Quota(Versionable):
null=True, blank=True,
help_text=_("Leave empty for an unlimited number of tickets.")
)
items = VersionedManyToManyField(
items = models.ManyToManyField(
Item,
verbose_name=_("Item"),
related_name="quotas",
@@ -745,7 +743,7 @@ class Quota(Versionable):
def count_in_cart(self) -> int:
from pretix.base.models import CartPosition
return CartPosition.objects.current.filter(
return CartPosition.objects.filter(
Q(expires__gte=now())
& self._position_lookup
).count()
@@ -753,7 +751,7 @@ class Quota(Versionable):
def count_orders(self) -> dict:
from pretix.base.models import Order, OrderPosition
o = OrderPosition.objects.current.filter(self._position_lookup).aggregate(
o = OrderPosition.objects.filter(self._position_lookup).aggregate(
paid=Sum(
Case(When(order__status=Order.STATUS_PAID, then=1),
output_field=models.IntegerField())

View File

@@ -6,9 +6,8 @@ from django.db import models
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from typing import List, Union
from versions.models import VersionedForeignKey
from .base import CachedFile, Versionable
from .base import CachedFile
from .event import Event
from .items import Item, ItemVariation, Question, Quota
@@ -17,7 +16,7 @@ def generate_secret():
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
class Order(Versionable):
class Order(models.Model):
"""
An order is created when a user clicks 'buy' on his cart. It holds
several OrderPositions and is connected to an user. It has an
@@ -83,7 +82,7 @@ class Order(Versionable):
choices=STATUS_CHOICE,
verbose_name=_("Status")
)
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
related_name="orders"
@@ -180,13 +179,11 @@ class Order(Versionable):
def mark_refunded(self):
"""
Mark this order as refunded. This clones the order object, sets the payment status and
returns the cloned order object.
Mark this order as refunded. This sets the payment status and returns the order object.
"""
order = self.clone()
order.status = Order.STATUS_REFUNDED
order.save()
return order
self.status = Order.STATUS_REFUNDED
self.save()
return self
def _can_be_paid(self) -> Union[bool, str]:
error_messages = {
@@ -223,12 +220,12 @@ class Order(Versionable):
for quota in quotas:
# Lock the quota, so no other thread is allowed to perform sales covered by this
# quota while we're doing so.
if quota.identity not in quota_cache:
quota_cache[quota.identity] = quota
if quota.id not in quota_cache:
quota_cache[quota.id] = quota
quota.cached_availability = quota.availability()[1]
else:
# Use cached version
quota = quota_cache[quota.identity]
quota = quota_cache[quota.id]
if quota.cached_availability is not None:
quota.cached_availability -= 1
if quota.cached_availability < 0:
@@ -240,12 +237,12 @@ class Order(Versionable):
class CachedTicket(models.Model):
order = VersionedForeignKey(Order, on_delete=models.CASCADE)
order = models.ForeignKey(Order, on_delete=models.CASCADE)
cachedfile = models.ForeignKey(CachedFile, on_delete=models.CASCADE)
provider = models.CharField(max_length=255)
class QuestionAnswer(Versionable):
class QuestionAnswer(models.Model):
"""
The answer to a Question, connected to an OrderPosition or CartPosition.
@@ -268,7 +265,7 @@ class QuestionAnswer(Versionable):
'CartPosition', null=True, blank=True,
related_name='answers'
)
question = VersionedForeignKey(
question = models.ForeignKey(
Question, related_name='answers'
)
answer = models.TextField()
@@ -286,14 +283,14 @@ class ObjectWithAnswers:
self.answ[a.question_id] = a.answer
self.questions = []
for q in self.item.questions.all():
if q.identity in self.answ:
q.answer = self.answ[q.identity]
if q.id in self.answ:
q.answer = self.answ[q.id]
else:
q.answer = ""
self.questions.append(q)
class OrderPosition(ObjectWithAnswers, Versionable):
class OrderPosition(ObjectWithAnswers, models.Model):
"""
An OrderPosition is one line of an order, representing one ordered items
of a specified type (or variation).
@@ -309,20 +306,23 @@ class OrderPosition(ObjectWithAnswers, Versionable):
:param attendee_name: The attendee's name, if entered.
:type attendee_name: str
"""
order = VersionedForeignKey(
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='positions'
related_name='positions',
on_delete=models.PROTECT
)
item = VersionedForeignKey(
item = models.ForeignKey(
Item,
verbose_name=_("Item"),
related_name='positions'
related_name='positions',
on_delete=models.PROTECT
)
variation = VersionedForeignKey(
variation = models.ForeignKey(
ItemVariation,
null=True, blank=True,
verbose_name=_("Variation")
verbose_name=_("Variation"),
on_delete=models.PROTECT
)
price = models.DecimalField(
decimal_places=2, max_digits=10,
@@ -347,17 +347,16 @@ class OrderPosition(ObjectWithAnswers, Versionable):
order=order, item=cartpos.item, variation=cartpos.variation,
price=cartpos.price, attendee_name=cartpos.attendee_name
)
op.save()
for answ in cartpos.answers.all():
answ = answ.clone()
answ.orderposition = op
answ.cartposition = None
answ.save()
op.save()
cartpos.delete()
ops.append(op)
class CartPosition(ObjectWithAnswers, Versionable):
class CartPosition(ObjectWithAnswers, models.Model):
"""
A cart position is similar to a order line, except that it is not
yet part of a binding order but just placed by some user in his or
@@ -383,7 +382,7 @@ class CartPosition(ObjectWithAnswers, Versionable):
:param attendee_name: The attendee's name, if entered.
:type attendee_name: str
"""
event = VersionedForeignKey(
event = models.ForeignKey(
Event,
verbose_name=_("Event")
)
@@ -391,14 +390,16 @@ class CartPosition(ObjectWithAnswers, Versionable):
max_length=255, null=True, blank=True,
verbose_name=_("Cart ID (e.g. session key)")
)
item = VersionedForeignKey(
item = models.ForeignKey(
Item,
verbose_name=_("Item")
verbose_name=_("Item"),
on_delete=models.CASCADE
)
variation = VersionedForeignKey(
variation = models.ForeignKey(
ItemVariation,
null=True, blank=True,
verbose_name=_("Variation")
verbose_name=_("Variation"),
on_delete=models.CASCADE
)
price = models.DecimalField(
decimal_places=2, max_digits=10,
@@ -421,3 +422,8 @@ class CartPosition(ObjectWithAnswers, Versionable):
class Meta:
verbose_name = _("Cart position")
verbose_name_plural = _("Cart positions")
def __str__(self):
return '<CartPosition: item %d, variation %d for cart %s>' % (
self.item.id, self.variation.id if self.variation else 0, self.cart_id
)

View File

@@ -2,15 +2,13 @@ from django.core.validators import RegexValidator
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from versions.models import VersionedForeignKey
from pretix.base.settings import SettingsProxy
from .auth import User
from .base import Versionable
class Organizer(Versionable):
class Organizer(models.Model):
"""
This model represents an entity organizing events, e.g. a company, institution,
charity, person, …
@@ -72,7 +70,7 @@ class Organizer(Versionable):
return ObjectRelatedCache(self)
class OrganizerPermission(Versionable):
class OrganizerPermission(models.Model):
"""
The relation between an Organizer and an User who has permissions to
access an organizer profile.
@@ -86,7 +84,7 @@ class OrganizerPermission(Versionable):
:type can_create_events: bool
"""
organizer = VersionedForeignKey(Organizer)
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name="organizer_perms")
can_create_events = models.BooleanField(
default=True,
@@ -104,11 +102,11 @@ class OrganizerPermission(Versionable):
}
class OrganizerSetting(Versionable):
class OrganizerSetting(models.Model):
"""
An event option is a key-value setting which can be set for an
organizer. It will be inherited by the events of this organizer
"""
object = VersionedForeignKey(Organizer, related_name='setting_objects')
object = models.ForeignKey(Organizer, related_name='setting_objects', on_delete=models.CASCADE)
key = models.CharField(max_length=255)
value = models.TextField()