Start documenting the models

This commit is contained in:
Raphael Michel
2015-03-19 12:17:19 +01:00
parent db6077eeb2
commit 43c44e917a
3 changed files with 321 additions and 41 deletions

View File

@@ -11,4 +11,8 @@ Contents:
setup setup
style style
structure structure
models
api/index api/index
.. TODO::
Document models, cache objects, settings objects, ItemVariation objects, form fields.

View File

@@ -0,0 +1,93 @@
.. highlight:: python
:linenothreshold: 5
Data models
===========
Pretix provides the following data(base) models. Every model and every model method or field that is not
documented here is considered private and should not be used by third-party plugins, as it may change
without advance notice.
.. IMPORTANT::
pretix's models are built with `cleanerversion`_, which extends the default Django ORM by adding versioning
information to the database. There are basically three things you absolutely need to know about cleanerversion:
* When querying the database, make sure you only get the current versions::
queryset = Model.objects.current.filter(…)
* Before you modify an object, clone it::
obj = Model.objects.current.get(identity=1) # Prefer identities over primary keys
obj = obj.clone() # Saves the old version to the database and creates the new one
obj.foo = 'bar'
obj.save()
* Beware of batch operations, use ``queryset.update()``, ``queryset.delete()`` etc. only if
you know what you're doing.
There is one exception: The ``User`` model is a classic Django model!
User model
----------
.. autoclass:: pretix.base.models.User
:members:
Organizers and events
---------------------
.. autoclass:: pretix.base.models.Organizer
:members:
.. autoclass:: pretix.base.models.OrganizerPermission
:members:
.. autoclass:: pretix.base.models.Event
:members:
.. autoclass:: pretix.base.models.EventPermission
:members:
Items
-----
.. autoclass:: pretix.base.models.Item
:members:
.. autoclass:: pretix.base.models.ItemCategory
:members:
.. autoclass:: pretix.base.models.Property
:members:
.. autoclass:: pretix.base.models.PropertyValue
:members:
.. autoclass:: pretix.base.models.Question
:members:
.. autoclass:: pretix.base.models.ItemVariation
:members:
:exclude-members: add_values_from_string
.. autoclass:: pretix.base.models.Quota
:members:
Carts and Orders
----------------
.. autoclass:: pretix.base.models.Order
:members:
.. autoclass:: pretix.base.models.OrderPosition
:members:
.. autoclass:: pretix.base.models.CartPosition
:members:
.. autoclass:: pretix.base.models.QuestionAnswer
:members:
.. _cleanerversion: https://github.com/swisscom/cleanerversion

View File

@@ -123,11 +123,11 @@ class User(AbstractBaseUser, PermissionsMixin):
This is the user model used by pretix for authentication. This is the user model used by pretix for authentication.
Handling users is somehow complicated, as we try to have two Handling users is somehow complicated, as we try to have two
classes of users in one system: classes of users in one system:
(1) We want global users who can just login into pretix and (1) We want *global* users who can just login into pretix and
buy tickets for multiple events -- we also need those buy tickets for multiple events -- we also need those
global users for event organizers who should not need global users for event organizers who should not need
multiple users for managing multiple events. multiple users for managing multiple events.
(2) We want local users who exist only in the scope of a (2) We want *local* users who exist only in the scope of a
certain event certain event
The hard part is to find a primary key to identify all of these The hard part is to find a primary key to identify all of these
users. Letting the users choose usernames is a bad idea, as users. Letting the users choose usernames is a bad idea, as
@@ -148,7 +148,30 @@ class User(AbstractBaseUser, PermissionsMixin):
according to this scheme when it is empty. The __str__() method according to this scheme when it is empty. The __str__() method
returns the identifier. returns the identifier.
The is_staff field is only True for system operators. :param identifier: The identifier of the user, as described above
:type identifier: str
:param username: The username, null for global users.
:type username: str
:param event: The event the user belongs to, null for global users
:type event: Event
:param email: The user's e-mail address. May be empty or null for local users
:type email: str
:param givenname: The user's given name. May be empty or null.
:type givenname: str
:param familyname: The user's given name. May be empty or null.
:type familyname: str
:param givenname: The user's given name. May be empty or null.
:type givenname: str
:param is_active: Whether this user account is activated.
:type is_active: bool
:param is_staff: ``True`` for system operators.
:type is_staff: bool
:param date_joined: The datetime of the user's registration.
:type date_joined: datetime
:param locale: The user's preferred locale code.
:type locale: str
:param timezone: The user's preferred timezone.
:type timezone: str
""" """
USERNAME_FIELD = 'identifier' USERNAME_FIELD = 'identifier'
@@ -195,6 +218,10 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.identifier return self.identifier
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
Before passing the call to the default ``save()`` method, this will fill the ``identifier``
field if it is empty, according to the scheme descriped in the model docstring.
"""
if not self.identifier: if not self.identifier:
if self.event is None: if self.event is None:
self.identifier = self.email.lower() self.identifier = self.email.lower()
@@ -205,6 +232,13 @@ class User(AbstractBaseUser, PermissionsMixin):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_short_name(self) -> str: def get_short_name(self) -> str:
"""
Returns the first of the following user properties that is found to exist:
* Given name
* Family name
* User name
"""
if self.givenname: if self.givenname:
return self.givenname return self.givenname
elif self.familyname: elif self.familyname:
@@ -213,6 +247,14 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.username return self.username
def get_full_name(self) -> str: def get_full_name(self) -> str:
"""
Returns the first of the following user properties that is found to exist:
* A combination of given name and family name, depending on the locale
* Given name
* Family name
* User name
"""
if self.givenname and not self.familyname: if self.givenname and not self.familyname:
return self.givenname return self.givenname
elif not self.givenname and self.familyname: elif not self.givenname and self.familyname:
@@ -232,8 +274,10 @@ class Organizer(Versionable):
charity, person, … charity, person, …
:param name: The organizer's name :param name: The organizer's name
:type name: str
:param slug: A globally unique, short name for this organizer, to be used :param slug: A globally unique, short name for this organizer, to be used
in URLs and similar places. in URLs and similar places.
:type slug: str
""" """
name = models.CharField(max_length=200, name = models.CharField(max_length=200,
@@ -253,7 +297,7 @@ class Organizer(Versionable):
return self.name return self.name
@cached_property @cached_property
def settings(self): def settings(self) -> SettingsProxy:
""" """
Returns an object representing this organizer's settings Returns an object representing this organizer's settings
""" """
@@ -264,6 +308,14 @@ class OrganizerPermission(Versionable):
""" """
The relation between an Organizer and an User who has permissions to The relation between an Organizer and an User who has permissions to
access an organizer profile. access an organizer profile.
:param organizer: The organizer this relation refers to
:type organizer: Organizer
:param user: The user this set of permissions is valid for
:type user: User
:param can_create_events: Whether or not this user can create new events with this
organizer account.
:type can_create_events: bool
""" """
organizer = VersionedForeignKey(Organizer) organizer = VersionedForeignKey(Organizer)
@@ -290,26 +342,41 @@ class Event(Versionable):
tickets for. tickets for.
:param organizer: The organizer this event belongs to :param organizer: The organizer this event belongs to
:type organizer: Organizer
:param name: This events full title :param name: This events full title
:type name: str
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
be unique among the events of the same organizer. be unique among the events of the same organizer.
:type slug: str
:param locale: This events default locale :param locale: This events default locale
:type locale: str
:param timezone: The timezone this event takes place in (or is being described in) :param timezone: The timezone this event takes place in (or is being described in)
:type timezone: str
:param currency: The currency of all prices and payments of this event :param currency: The currency of all prices and payments of this event
:type currency: str
:param date_from: The datetime this event starts :param date_from: The datetime this event starts
:type date_from: datetime
:param date_to: The datetime this event ends :param date_to: The datetime this event ends
:type date_to: datetime
:param show_date_to: If ``False``, the ``date_to`` value will not be shown to the :param show_date_to: If ``False``, the ``date_to`` value will not be shown to the
user and the event will be listed only with it's start date. user and the event will be listed only with it's start date.
:type show_date_to: bool
:param show_times: If ``False``, ``date_from`` and ``date_to`` will only be displayed :param show_times: If ``False``, ``date_from`` and ``date_to`` will only be displayed
as dates, without a time of day. as dates, without a time of day.
:type show_times: bool
:param presale_start: No tickets will be sold before this date. :param presale_start: No tickets will be sold before this date.
:type presale_start: datetime
:param presale_end: No tickets will be sold before this date. :param presale_end: No tickets will be sold before this date.
:type presale_end: datetime
:param payment_term_days: The number of days in which an order has to be :param payment_term_days: The number of days in which an order has to be
paid after it has been submitted. paid after it has been submitted.
:type payment_term_days: int
:param payment_term_last: The day all orders must be paid by, no matter when :param payment_term_last: The day all orders must be paid by, no matter when
the order was placed. the order was placed.
:type payment_term_last: datetime
:param plugins: A comma-separated list of plugin names that are active for this :param plugins: A comma-separated list of plugin names that are active for this
event. event.
:type plugins: str
""" """
organizer = VersionedForeignKey(Organizer, related_name="events", organizer = VersionedForeignKey(Organizer, related_name="events",
@@ -394,17 +461,29 @@ class Event(Versionable):
return obj return obj
def get_plugins(self) -> "list[str]": def get_plugins(self) -> "list[str]":
"""
Get the names of the plugins activated for this event as a list.
"""
if self.plugins is None: if self.plugins is None:
return [] return []
return self.plugins.split(",") return self.plugins.split(",")
def get_date_from_display(self) -> str: def get_date_from_display(self) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting.
"""
return _date( return _date(
self.date_from, self.date_from,
"DATETIME_FORMAT" if self.show_times else "DATE_FORMAT" "DATETIME_FORMAT" if self.show_times else "DATE_FORMAT"
) )
def get_date_to_display(self) -> str: def get_date_to_display(self) -> str:
"""
Returns a formatted string containing the start date of the event with respect
to the current locale and to the ``show_times`` setting. Returns an empty string
if ``show_date_to`` is ``False``.
"""
if not self.show_date_to: if not self.show_date_to:
return "" return ""
return _date( return _date(
@@ -413,6 +492,12 @@ class Event(Versionable):
) )
def get_cache(self) -> "pretix.base.cache.EventRelatedCache": def get_cache(self) -> "pretix.base.cache.EventRelatedCache":
"""
Returns an :py:class:`EventRelatedCache` object. This behaves equivalent to
Django's built-in cache backends, but puts you into an isolated environment for
this event, so you don't have to prefix your cache keys. In addition, the cache
is being cleared every time the event or one of its related objects change.
"""
from pretix.base.cache import EventRelatedCache from pretix.base.cache import EventRelatedCache
return EventRelatedCache(self) return EventRelatedCache(self)
@@ -428,6 +513,15 @@ class EventPermission(Versionable):
""" """
The relation between an Event and an User who has permissions to The relation between an Event and an User who has permissions to
access an event. access an event.
:param event: The event this refers to
:type event: Event
:param user: The user these permission set applies to
:type user: User
:param can_change_settings: If ``True``, the user can change all basic settings for this event.
:type can_change_settings: bool
:param can_change_items: If ``True``, the user can change and add items and related objects for this event.
:type can_change_items: bool
""" """
event = VersionedForeignKey(Event) event = VersionedForeignKey(Event)
@@ -454,8 +548,14 @@ class EventPermission(Versionable):
class ItemCategory(Versionable): class ItemCategory(Versionable):
""" """
Items can be sorted into categories, which only have a name and a Items can be sorted into these categories.
configurable order
:param event: The event this belongs to
:type event: Event
:param name: The name of this category
:type name: str
:param position: An integer, used for sorting
:type position: int
""" """
event = VersionedForeignKey( event = VersionedForeignKey(
Event, Event,
@@ -500,6 +600,11 @@ class Property(Versionable):
""" """
A property is a modifier which can be applied to an Item. For example 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'. '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 = VersionedForeignKey( event = VersionedForeignKey(
@@ -532,7 +637,14 @@ class Property(Versionable):
class PropertyValue(Versionable): class PropertyValue(Versionable):
""" """
A value of a property. If the property would be 'T-Shirt size', A value of a property. If the property would be 'T-Shirt size',
this could be 'M' or 'L' 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 = VersionedForeignKey( prop = VersionedForeignKey(
@@ -577,7 +689,22 @@ class PropertyValue(Versionable):
class Question(Versionable): class Question(Versionable):
""" """
A question is an input field that can be used to extend a ticket A question is an input field that can be used to extend a ticket
by custom information, e.g. "Attendee name" or "Attendee age". by custom information, e.g. "Attendee age". A question can allow one o several
input types, currently:
* a number (``TYPE_NUMBER``)
* a one-line string (``TYPE_STRING``)
* a multi-line string (``TYPE_TEXT``)
* a boolean (``TYPE_BOOLEAN``)
:param event: The event this question belongs to
:type event: Event
:param question: The question text. This will be displayed next to the input field.
:type question: str
:param type: One of the above types
:param required: Whether answering this question is required for submiting an order including
items associated with this question.
:type required: bool
""" """
TYPE_NUMBER = "N" TYPE_NUMBER = "N"
TYPE_STRING = "S" TYPE_STRING = "S"
@@ -627,17 +754,32 @@ class Question(Versionable):
class Item(Versionable): class Item(Versionable):
""" """
An item is a thing which can be sold. It belongs to an An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
event and may or may not belong to a category. Items are often Items are often also called 'products' but are named 'items' internally due to historic reasons.
also called 'products' but are named 'items' internally due to
historic reasons.
It has a default price which might by overriden by It has a default price which might by overriden by restrictions.
restrictions.
:param event: The event this belongs to.
:type event: Event
:param category: The category this belongs to. May be null.
:type category: ItemCategory
:param name: The name of this item:
:type name: str
:param active: Whether this item is being sold
:type active: bool
:param short_description: A short description
:type short_description: str
:param long_description: A long description
:type long_description: str
:param default_price: The item's default price
:type default_price: decimal.Decimal
:param tax_rate: The VAT tax that is included in this item's price (in %)
:type tax_rate: decimal.Decimal
:param properties: A set of ``Property`` objects that should be applied to this item
:param questions: A set of ``Question`` objects that should be applied to this item
:param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise)
:type admission: bool
Items can not be deleted, as this would cause database
inconsistencies. Instead, they have an attribute "deleted".
Deleted items will not be shown anywhere.
""" """
event = VersionedForeignKey( event = VersionedForeignKey(
Event, Event,
@@ -736,6 +878,11 @@ class Item(Versionable):
VariationDicts differ from dicts only by specifying some extra VariationDicts differ from dicts only by specifying some extra
methods. 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'): if use_cache and hasattr(self, '_get_all_variations_cache'):
return self._get_all_variations_cache return self._get_all_variations_cache
@@ -773,16 +920,18 @@ class Item(Versionable):
This method returns a list of all variations which are theoretically This method returns a list of all variations which are theoretically
possible for sale. It DOES call all activated restriction plugins, and it possible for sale. It DOES call all activated restriction plugins, and it
DOES only return variations which DO have an ItemVariation object, as all 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 NOT variations without one CAN NOT be part of a Quota and therefore can never
ever be available for sale. The only exception is the empty variation be available for sale. The only exception is the empty variation
for items without properties, which never has an ItemVariation object. for items without properties, which never has an ItemVariation object.
This DOES NOT take into account quotas itself. Use is_available on the 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 ItemVariation objects (or the Item it self, if it does not have variations) to
determine availability by the terms of quotas. determine availability by the terms of quotas.
It is recommended to call It is recommended to call::
prefetch_related('properties', 'variations__values__prop')
.prefetch_related('properties', 'variations__values__prop')
when retrieving Item objects you are going to use this method on. when retrieving Item objects you are going to use this method on.
""" """
if use_cache and hasattr(self, '_get_all_available_variations_cache'): if use_cache and hasattr(self, '_get_all_available_variations_cache'):
@@ -842,7 +991,12 @@ class Item(Versionable):
def check_quotas(self): def check_quotas(self):
""" """
This method is used to determine whether this Item is currently available This method is used to determine whether this Item is currently available
for sale. It may return any of the return codes of Quota.availability() for sale.
: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.
Please use the method on the ItemVariation object you are interested in.
""" """
if self.properties.count() > 0: # NOQA if self.properties.count() > 0: # NOQA
raise ValueError('Do not call this directly on items which have properties ' raise ValueError('Do not call this directly on items which have properties '
@@ -853,8 +1007,14 @@ class Item(Versionable):
""" """
This method is used to determine whether this ItemVariation is restricted This method is used to determine whether this ItemVariation is restricted
in sale by any restriction plugins. in sale by any restriction plugins.
It returns False, if the item is unavailable or the item's price, if it is
available. :returns:
* ``False``, if the item is unavailable
* the item's price, otherwise
:raises ValueError: if you call this on an item which has properties associated with it.
Please use the method on the ItemVariation object you are interested in.
""" """
if self.properties.count() > 0: # NOQA if self.properties.count() > 0: # NOQA
raise ValueError('Do not call this directly on items which have properties ' raise ValueError('Do not call this directly on items which have properties '
@@ -879,20 +1039,29 @@ class ItemVariation(Versionable):
""" """
A variation is an item combined with values for all properties A variation is an item combined with values for all properties
associated with the item. For example, if your item is 'T-Shirt' associated with the item. For example, if your item is 'T-Shirt'
and your properties are 'Size' and 'Color', then an example for a and your properties are 'Size' and 'Color', then an example for an
variation would be 'T-Shirt XL read'. variation would be 'T-Shirt XL read'.
Attention: _ALL_ combinations of PropertyValues _ALWAYS_ exist, Attention: _ALL_ combinations of PropertyValues _ALWAYS_ exist,
even if there is no ItemVariation object for them! ItemVariation objects even if there is no ItemVariation object for them! ItemVariation objects
do NOT prove existance, they are only available to make it possible do NOT prove existance, they are only available to make it possible
to override default values (like the price) for certain combinations to override default values (like the price) for certain combinations
of property values. 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 They also allow to explicitly EXCLUDE certain combinations of property
values by creating an ItemVariation object for them with active set to values by creating an ItemVariation object for them with active set to
False. False.
Restrictions can be not only set to items but also directly to variations. Restrictions can be not only set to items but also directly to variations.
:param item: The item this variation belongs to
:type item: Item
:param values: A set of ``PropertyValue`` objects defining this variation
:param active: Whether this value is to be sold.
:type active: bool
:param default_price: This variation's default price
:type default_price: decimal.Decimal
""" """
item = VersionedForeignKey( item = VersionedForeignKey(
Item, Item,
@@ -932,12 +1101,16 @@ class ItemVariation(Versionable):
def check_quotas(self): def check_quotas(self):
""" """
This method is used to determine whether this ItemVariation is currently This method is used to determine whether this ItemVariation is currently
available for sale in terms of quotas. It may return any of the return codes available for sale in terms of quotas.
of Quota.availability()
:returns: any of the return codes of :py:meth:`Quota.availability()`.
""" """
return min([q.availability() for q in self.quotas.all()]) return min([q.availability() for q in self.quotas.all()])
def to_variation_dict(self): def to_variation_dict(self):
"""
:return: a :py:class:`VariationDict` representing this variation.
"""
vd = VariationDict() vd = VariationDict()
for v in self.values.all(): for v in self.values.all():
vd[v.prop.identity] = v vd[v.prop.identity] = v
@@ -948,8 +1121,11 @@ class ItemVariation(Versionable):
""" """
This method is used to determine whether this ItemVariation is restricted This method is used to determine whether this ItemVariation is restricted
in sale by any restriction plugins. in sale by any restriction plugins.
It returns False, if the item is unavailable or the item's price, if it is
available. :returns:
* ``False``, if the item is unavailable
* the item's price, otherwise
""" """
from .signals import determine_availability from .signals import determine_availability
responses = determine_availability.send( responses = determine_availability.send(
@@ -1058,9 +1234,7 @@ class Quota(Versionable):
VIP tickets will be fine). VIP tickets will be fine).
As always, a quota can not only be tied to an item, but also to specific As always, a quota can not only be tied to an item, but also to specific
variations. We follow the general rule here: If there are no variations variations.
speficied, the quota applies to all of them, and if there are variations
specified, the quota applies to those.
Please read the documentation section on quotas carefully before doing Please read the documentation section on quotas carefully before doing
anything with quotas. This might confuse you otherwise. anything with quotas. This might confuse you otherwise.
@@ -1084,6 +1258,15 @@ class Quota(Versionable):
AVAILABILITY_GONE AVAILABILITY_GONE
This item is completely sold out. This item is completely sold out.
:param event: The event this belongs to
:type event: Event
:param name: This quota's name
:type str:
:param size: The number of items in this quota
:type size: int
:param items: The set of :py:class:`Item` objects this quota applies to
:param variations: The set of :py:class:`ItemVariation` objects this quota applies to
""" """
AVAILABILITY_GONE = 0 AVAILABILITY_GONE = 0
@@ -1141,9 +1324,10 @@ class Quota(Versionable):
def availability(self): def availability(self):
""" """
This method is used to determine whether Items or ItemVariations belonging This method is used to determine whether Items or ItemVariations belonging
to this quota should currently be available for sale. It returns a tuple where to this quota should currently be available for sale.
the first entry is one of the Quota.AVAILABILITY_ constants and the second
is the number of available tickets. :returns: a tuple where the first entry is one of the Quota.AVAILABILITY_ constants and the second
is the number of available tickets.
""" """
# TODO: These lookups are highly inefficient. However, we'll wait with optimizing # TODO: These lookups are highly inefficient. However, we'll wait with optimizing
# until Django 1.8 is released, as the following feature might make it a # until Django 1.8 is released, as the following feature might make it a
@@ -1192,10 +1376,9 @@ class Quota(Versionable):
def lock(self): def lock(self):
""" """
Issue a lock on this quota so nobody can take tickets from this quota until Issue a lock on this quota so nobody can take tickets from this quota until
you release the lock. you release the lock. Will retry 5 times on failure.
Raises an Quota.LockTimeoutException if the quota is locked every time we :raises Quota.LockTimeoutException: if the quota is locked every time we try to obtain the lock
try to obtain a lock.
""" """
retries = 5 retries = 5
for i in range(retries): for i in range(retries):
@@ -1215,7 +1398,7 @@ class Quota(Versionable):
def release(self, force=False): def release(self, force=False):
""" """
Release a lock placed by lock(). If the parameter force is not set, Release a lock placed by :py:meth:`lock()`. If the parameter force is not set to ``True``,
the lock will only be released if it was issued in _this_ python the lock will only be released if it was issued in _this_ python
representation of the database object. representation of the database object.
""" """