You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

abstract_models.py 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import zlib
  2. from django.db import models
  3. from django.utils.translation import ugettext_lazy as _
  4. class AbstractAddress(models.Model):
  5. """
  6. Superclass address object
  7. This is subclassed and extended to provide models for
  8. user, shipping and billing addresses.
  9. """
  10. # @todo: Need a way of making these choice lists configurable
  11. # per project
  12. MR, MISS, MRS, MS, DR = ('Mr', 'Miss', 'Mrs', 'Ms', 'Dr')
  13. TITLE_CHOICES = (
  14. (MR, _("Mr")),
  15. (MISS, _("Miss")),
  16. (MRS, _("Mrs")),
  17. (MS, _("Ms")),
  18. (DR, _("Dr")),
  19. )
  20. title = models.CharField(_("Title"), max_length=64, choices=TITLE_CHOICES,
  21. blank=True, null=True)
  22. first_name = models.CharField(_("First name"), max_length=255, blank=True,
  23. null=True)
  24. last_name = models.CharField(_("Last name"), max_length=255, blank=True)
  25. # We use quite a few lines of an address as they are often quite long and
  26. # it's easier to just hide the unnecessary ones than add extra ones.
  27. line1 = models.CharField(_("First line of address"), max_length=255)
  28. line2 = models.CharField(_("Second line of address"), max_length=255,
  29. blank=True, null=True)
  30. line3 = models.CharField(_("Third line of address"), max_length=255,
  31. blank=True, null=True)
  32. line4 = models.CharField(_("City"), max_length=255, blank=True, null=True)
  33. state = models.CharField(_("State/County"), max_length=255, blank=True,
  34. null=True)
  35. postcode = models.CharField(_("Post/Zip-code"), max_length=64, blank=True, null=True)
  36. country = models.ForeignKey('address.Country', verbose_name=_("Country"))
  37. #: A field only used for searching addresses - this contains all the
  38. #: relevant fields
  39. search_text = models.CharField(
  40. _("Search text - used only for searching addresses"),
  41. max_length=1000)
  42. class Meta:
  43. abstract = True
  44. verbose_name = _('Address')
  45. verbose_name_plural = _('Addresses')
  46. def save(self, *args, **kwargs):
  47. self._clean_fields()
  48. self._update_search_text()
  49. super(AbstractAddress, self).save(*args, **kwargs)
  50. @property
  51. def city(self):
  52. return self.line4
  53. def _clean_fields(self):
  54. """
  55. Clean up fields
  56. """
  57. for field in ['first_name', 'last_name', 'line1', 'line2', 'line3',
  58. 'line4', 'state', 'postcode']:
  59. if self.__dict__[field]:
  60. self.__dict__[field] = self.__dict__[field].strip()
  61. # Ensure postcodes are always uppercase
  62. if self.postcode:
  63. self.postcode = self.postcode.upper()
  64. def _update_search_text(self):
  65. search_fields = filter(
  66. bool, [self.first_name, self.last_name,
  67. self.line1, self.line2, self.line3, self.line4,
  68. self.state, self.postcode, self.country.name])
  69. self.search_text = ' '.join(search_fields)
  70. @property
  71. def summary(self):
  72. """
  73. Returns a single string summary of the address,
  74. separating fields using commas.
  75. """
  76. return u", ".join(self.active_address_fields())
  77. def populate_alternative_model(self, address_model):
  78. """
  79. For populating an address model using the matching fields
  80. from this one.
  81. This is used to convert a user address to a shipping address
  82. as part of the checkout process.
  83. """
  84. destination_field_names = [
  85. field.name for field in address_model._meta.fields]
  86. for field_name in [field.name for field in self._meta.fields]:
  87. if field_name in destination_field_names and field_name != 'id':
  88. setattr(address_model, field_name, getattr(self, field_name))
  89. def active_address_fields(self):
  90. """
  91. Returns the non-empty components of the address, but merging the
  92. title, first_name and last_name into a single line.
  93. """
  94. self._clean_fields()
  95. fields = filter(
  96. lambda x: x, [self.salutation(), self.line1, self.line2,
  97. self.line3, self.line4, self.state, self.postcode])
  98. if self.country:
  99. fields.append(self.country.name)
  100. return fields
  101. def salutation(self):
  102. """
  103. Return the salutation
  104. """
  105. return u" ".join([part for part in [
  106. self.title, self.first_name, self.last_name] if part])
  107. def name(self):
  108. """
  109. Return the full name
  110. """
  111. return u" ".join([part for part in [
  112. self.first_name, self.last_name] if part])
  113. def __unicode__(self):
  114. return self.summary
  115. class AbstractCountry(models.Model):
  116. """
  117. International Organization for Standardization (ISO) 3166-1 Country list.
  118. """
  119. iso_3166_1_a2 = models.CharField(_('ISO 3166-1 alpha-2'), max_length=2,
  120. primary_key=True)
  121. iso_3166_1_a3 = models.CharField(_('ISO 3166-1 alpha-3'), max_length=3,
  122. null=True, db_index=True)
  123. iso_3166_1_numeric = models.PositiveSmallIntegerField(
  124. _('ISO 3166-1 numeric'), null=True, db_index=True)
  125. name = models.CharField(_('Official name (CAPS)'), max_length=128)
  126. printable_name = models.CharField(_('Country name'), max_length=128)
  127. is_highlighted = models.BooleanField(_("Is Highlighted"), default=False,
  128. db_index=True)
  129. is_shipping_country = models.BooleanField(_("Is Shipping Country"),
  130. default=False, db_index=True)
  131. class Meta:
  132. abstract = True
  133. verbose_name = _('Country')
  134. verbose_name_plural = _('Countries')
  135. ordering = ('-is_highlighted', 'name',)
  136. def __unicode__(self):
  137. return self.printable_name
  138. class AbstractShippingAddress(AbstractAddress):
  139. """
  140. A shipping address.
  141. A shipping address should not be edited once the order has been placed -
  142. it should be read-only after that.
  143. """
  144. phone_number = models.CharField(_("Phone number"), max_length=32,
  145. blank=True, null=True)
  146. notes = models.TextField(
  147. blank=True, null=True,
  148. verbose_name=_('Courier instructions'),
  149. help_text=_("For example, leave the parcel in the wheelie bin "
  150. "if I'm not in."))
  151. class Meta:
  152. abstract = True
  153. verbose_name = _("Shipping address")
  154. verbose_name_plural = _("Shipping addresses")
  155. @property
  156. def order(self):
  157. """
  158. Return the order linked to this shipping address
  159. """
  160. orders = self.order_set.all()
  161. if not orders:
  162. return None
  163. return orders[0]
  164. class AbstractUserAddress(AbstractShippingAddress):
  165. """
  166. A user's address. A user can have many of these and together they form an
  167. 'address book' of sorts for the user.
  168. We use a separate model for shipping and billing (even though there will be
  169. some data duplication) because we don't want shipping/billing addresses
  170. changed or deleted once an order has been placed. By having a separate
  171. model, we allow users the ability to add/edit/delete from their address
  172. book without affecting orders already placed.
  173. """
  174. user = models.ForeignKey('auth.User', related_name='addresses',
  175. verbose_name=_("User"))
  176. # Customers can set defaults
  177. is_default_for_shipping = models.BooleanField(
  178. _("Default shipping address?"), default=False)
  179. #: Whether this address should be the default for billing.
  180. is_default_for_billing = models.BooleanField(
  181. _("Default billing address?"), default=False)
  182. #: We keep track of the number of times an address has been used
  183. #: as a shipping address so we can show the most popular ones
  184. #: first at the checkout.
  185. num_orders = models.PositiveIntegerField(_("Number of Orders"), default=0)
  186. #: A hash is kept to try and avoid duplicate addresses being added
  187. #: to the address book.
  188. hash = models.CharField(_("Address Hash"), max_length=255, db_index=True)
  189. date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
  190. def generate_hash(self):
  191. """
  192. Returns a hash of the address summary
  193. """
  194. # We use an upper-case version of the summary
  195. return zlib.crc32(self.summary.strip().upper().encode('UTF8'))
  196. def save(self, *args, **kwargs):
  197. """
  198. Save a hash of the address fields
  199. """
  200. # Save a hash of the address fields so we can check whether two
  201. # addresses are the same to avoid saving duplicates
  202. self.hash = self.generate_hash()
  203. # Ensure that each user only has one default shipping address
  204. # and billing address
  205. self._ensure_defaults_integrity()
  206. super(AbstractUserAddress, self).save(*args, **kwargs)
  207. def _ensure_defaults_integrity(self):
  208. if self.is_default_for_shipping:
  209. self.__class__._default_manager.filter(
  210. user=self.user,
  211. is_default_for_shipping=True).update(
  212. is_default_for_shipping=False)
  213. if self.is_default_for_billing:
  214. self.__class__._default_manager.filter(
  215. user=self.user,
  216. is_default_for_billing=True).update(
  217. is_default_for_billing=False)
  218. class Meta:
  219. abstract = True
  220. verbose_name = _("User address")
  221. verbose_name_plural = _("User addresses")
  222. ordering = ['-num_orders']
  223. class AbstractBillingAddress(AbstractAddress):
  224. class Meta:
  225. abstract = True
  226. verbose_name_plural = _("Billing address")
  227. verbose_name_plural = _("Billing addresses")
  228. @property
  229. def order(self):
  230. """
  231. Return the order linked to this shipping address
  232. """
  233. orders = self.order_set.all()
  234. if not orders:
  235. return None
  236. return orders[0]