Skip to content

Commit

Permalink
Merge pull request #8 from a-musing-moose/store_stock
Browse files Browse the repository at this point in the history
Adding store stock models and template tag
  • Loading branch information
Sebastian Vetter committed Dec 6, 2012
2 parents 331d19a + 5cf0e62 commit 899a620
Show file tree
Hide file tree
Showing 17 changed files with 425 additions and 17 deletions.
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
.PHONY: sandbox geoip

geoip:
cd sandbox/geoip
wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz
gunzip GeoLiteCity.dat.gz
cd -
mv GeoLiteCity.dat sandbox/geoip

sandbox:
rm sandbox/sandbox/sandbox.sqlite3 || true
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Contents:
.. toctree::
:maxdepth: 2

settings


Indices and tables
Expand Down
9 changes: 9 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Settings
========

STORES_SRID
-----------

Default: ``4326`` (i.e. WGS 84)

This sets the SRID for the stores location field.
1 change: 1 addition & 0 deletions runtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def configure():
},
GEOIP_PATH = 'sandbox/geoip',
NOSE_ARGS=['-s', '-x', '--with-spec'],
STORES_SRID=32140, # Flat projection so spatialite can do distances
**OSCAR_SETTINGS
)

Expand Down
Binary file modified sandbox/sandbox/__init__.pyc
Binary file not shown.
2 changes: 2 additions & 0 deletions sandbox/sandbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,5 @@
},
}
}

STORES_SRID = 32140, # Flat projection so spatialite can do distances
Binary file modified sandbox/sandbox/wsgi.pyc
Binary file not shown.
6 changes: 5 additions & 1 deletion stores/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from stores import models


admin.site.register(models.Store)
class StoreAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}

admin.site.register(models.Store, StoreAdmin)
admin.site.register(models.StoreGroup)
admin.site.register(models.OpeningPeriod)
admin.site.register(models.StoreStock)
12 changes: 10 additions & 2 deletions stores/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ def get_urls(self):
urlpatterns = super(StoresApplication, self).get_urls()

urlpatterns += patterns('',
url(r'^$', self.list_view.as_view(), name='index'),
url(r'^(?P<slug>[\w-]+)/$', self.detail_view.as_view(), name='detail'),
url(
r'^$',
self.list_view.as_view(),
name='index'
),
url(
r'^(?P<slug>[\w-]+)/$',
self.detail_view.as_view(),
name='detail'
),
)
return self.post_process_urls(urlpatterns)

Expand Down
6 changes: 5 additions & 1 deletion stores/dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class StoresDashboardApplication(Application):

def get_urls(self):
urlpatterns = patterns('',
url(r'^$', self.store_list_view.as_view(), name='store-list'),
url(
r'^$',
self.store_list_view.as_view(),
name='store-list'
),
url(
r'^create/$',
self.store_create_view.as_view(),
Expand Down
8 changes: 6 additions & 2 deletions stores/dashboard/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class Meta:
'name': forms.TextInput(
attrs={'placeholder': _("e.g. Christmas")}
),
'start': forms.TextInput(attrs={'placeholder': _("e.g. 9am, noon, etc.")}),
'end': forms.TextInput(attrs={'placeholder': _("e.g. 5pm, late, etc.")}),
'start': forms.TextInput(
attrs={'placeholder': _("e.g. 9am, noon, etc.")}
),
'end': forms.TextInput(
attrs={'placeholder': _("e.g. 5pm, late, etc.")}
),
}
1 change: 0 additions & 1 deletion stores/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ def get_query_set(self):

def pickup_stores(self):
return self.get_query_set().pickup_stores()

216 changes: 216 additions & 0 deletions stores/migrations/0002_auto__add_storestock.py

Large diffs are not rendered by default.

96 changes: 88 additions & 8 deletions stores/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
from django.utils.translation import ugettext as _
from django.template.defaultfilters import slugify
from django.contrib.gis.db.models import PointField
from django.contrib.gis.db.models import GeoManager
from django.conf import settings


from oscar.apps.address.abstract_models import AbstractAddress

from stores.managers import StoreManager

STORES_SRID = getattr(settings, 'STORES_SRID', 4326)


class StoreAddress(AbstractAddress):
store = models.OneToOneField(
Expand Down Expand Up @@ -36,7 +41,7 @@ class StoreContact(models.Model):
phone = models.CharField(_('Phone'), max_length=20, blank=True, null=True)
email = models.CharField(_('Email'), max_length=100, blank=True, null=True)

store = models.OneToOneField('stores.Store', name=_("Store"),
store = models.OneToOneField('stores.Store', verbose_name=_("Store"),
related_name="contact_details")

def __unicode__(self):
Expand Down Expand Up @@ -64,10 +69,15 @@ class Store(models.Model):
blank=True, null=True
)

location = PointField(_("Location"), null=True, blank=True)
location = PointField(
_("Location"),
null=True,
blank=True,
srid=STORES_SRID
)

group = models.ForeignKey('stores.StoreGroup', related_name='stores',
name=_("Group"), null=True, blank=True)
verbose_name=_("Group"), null=True, blank=True)

is_pickup_store = models.BooleanField(_("Is pickup store"), default=True)
is_active = models.BooleanField(_("Is active"), default=True)
Expand Down Expand Up @@ -95,18 +105,26 @@ class OpeningPeriod(models.Model):
SATURDAY: _("Saturday"),
SUNDAY: _("Sunday"),
}
store = models.ForeignKey('stores.Store', name=_("Store"),
store = models.ForeignKey('stores.Store', verbose_name=_("Store"),
related_name='opening_periods')

weekday_choices = [(k, v) for k, v in WEEK_DAYS.items()]
weekday = models.PositiveIntegerField(_("Weekday"),
choices=weekday_choices)
weekday = models.PositiveIntegerField(
_("Weekday"),
choices=weekday_choices
)
start = models.CharField(
_("Start"), max_length=30, null=True, blank=True,
_("Start"),
max_length=30,
null=True,
blank=True,
help_text=_("Leaving start and end time empty is displayed as 'Closed'")
)
end = models.CharField(
_("End"), max_length=30, null=True, blank=True,
_("End"),
max_length=30,
null=True,
blank=True,
help_text=_("Leaving start and end time empty is displayed as 'Closed'")
)

Expand All @@ -127,3 +145,65 @@ class Meta:
ordering = ['weekday']
verbose_name = _("Opening period")
verbose_name_plural = _("Opening periods")


class StoreStock(models.Model):

store = models.ForeignKey(
'stores.Store',
verbose_name=_("Store"),
related_name='stock'
)
product = models.ForeignKey(
'catalogue.Product',
verbose_name=_("Product"),
related_name="store_stock"
)
# Stock level information
num_in_stock = models.PositiveIntegerField(
_("Number in stock"),
default=0,
blank=True,
null=True
)

# The amount of stock allocated in store but not fed back to the master
num_allocated = models.IntegerField(
_("Number allocated"),
default=0,
blank=True,
null=True
)

location = models.CharField(
_("In store location"),
max_length=50,
blank=True,
null=True
)

# Date information
date_created = models.DateTimeField(
_("Date Created"),
auto_now_add=True
)
date_updated = models.DateTimeField(
_("Date Updated"),
auto_now=True,
db_index=True
)

class Meta:
verbose_name = _("Store Stock Record")
verbose_name_plural = _("Store Stock Records")

objects = GeoManager() # Needed for distance queries against stores

def __unicode__(self):
if self.store and self.product:
return "%s @ %s" % (self.product.title, self.store.name)
return "Store Stock"

@property
def is_available_to_buy(self):
return self.num_in_stock > self.num_allocated
Empty file added stores/templatetags/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions stores/templatetags/store_stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django import template
from django.db.models import get_model

StoreStock = get_model('stores', 'StoreStock')

register = template.Library()


@register.assignment_tag
def store_stock_for_product(product, location=None, limit=20):
query_set = StoreStock.objects.filter(product=product)
if location:
query_set = query_set.distance(
location,
field_name='store__location'
).order_by('distance')
else:
query_set = query_set.order_by('store__name')
return query_set[0:limit]
62 changes: 62 additions & 0 deletions tests/unit/templatetag_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from django.test import TestCase
from django.contrib.gis.geos.point import Point
from django.template import Template, Context
from django_dynamic_fixture import get as G

from django.db.models import get_model

Product = get_model('catalogue', 'Product')
Store = get_model('stores', 'Store')
StoreStock = get_model('stores', 'StoreStock')


class StoreStockTest(TestCase):

def setUp(self):
self.product = G(Product)
self.store1_location = '{"type": "Point", "coordinates": [87.39,12.02]}'
self.store2_location = '{"type": "Point", "coordinates": [88.39,11.02]}'
self.store1 = G(Store, is_pickup_store=True, location=self.store1_location)
self.store2 = G(Store, is_pickup_store=True, location=self.store2_location)
self.store_stock1 = G(StoreStock, store=self.store1, product=self.product)
self.store_stock1 = G(StoreStock, store=self.store2, product=self.product)

def test_store_stock_loads(self):
rendered = Template(
'{% load store_stock %}'
).render(Context())

def test_store_stock_for_product_returns_stock_lines(self):
rendered = Template(
"""
{% load store_stock %} {% store_stock_for_product product as store_stock %}
{% for stock in store_stock %} {{ stock.store.name }} {% endfor %}
"""
).render(Context({
'product': self.product
}))
self.assertTrue(self.store1.name in rendered)
self.assertTrue(self.store2.name in rendered)

def test_store_stock_for_product_limits_when_asked(self):
rendered = Template(
"""
{% load store_stock %} {% store_stock_for_product product limit=1 as store_stock %}
{% for stock in store_stock %} {{ stock.store.name }} {% endfor %}
"""
).render(Context({
'product': self.product
}))
self.assertTrue(self.store1.name in rendered)

def test_store_stock_for_product_order_by_closed(self):
rendered = Template(
"""
{% load store_stock %} {% store_stock_for_product product location=loc as store_stock %}
{% for stock in store_stock %}{{ stock.store.name }}{% endfor %}
"""
).render(Context({
'product': self.product,
'loc': '{"type": "Point", "coordinates": [88.39,11.02]}'
}))
self.assertTrue("%s%s" % (self.store2.name, self.store1.name) in rendered)

0 comments on commit 899a620

Please sign in to comment.