Skip to content

Commit

Permalink
Merge branch 'master' into fix/1411_fedissues
Browse files Browse the repository at this point in the history
Conflicts:
	oscar/templates/oscar/catalogue/reviews/review_form.html
  • Loading branch information
andysellick committed Jul 21, 2014
2 parents fe2d198 + 30b4e4f commit 820012e
Show file tree
Hide file tree
Showing 45 changed files with 371 additions and 214 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ sandbox: install
sites/sandbox/manage.py syncdb --noinput
sites/sandbox/manage.py migrate
# Import some fixtures. Order is important as JSON fixtures include primary keys
sites/sandbox/manage.py loaddata sites/sandbox/fixtures/variants.json
sites/sandbox/manage.py loaddata sites/sandbox/fixtures/child_products.json
sites/sandbox/manage.py oscar_import_catalogue sites/sandbox/fixtures/*.csv
sites/sandbox/manage.py oscar_import_catalogue_images sites/sandbox/fixtures/images.tar.gz
sites/sandbox/manage.py oscar_populate_countries
Expand Down
25 changes: 13 additions & 12 deletions docs/source/howto/how_to_change_a_url.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ How to add views or change URLs or permissions
Oscar has many views and associated URLs. Often you want to customise these
URLs for your domain, or add additional views to an app.

This How-to describes how to do just that.
This how-to describes how to do just that.
It builds upon the steps described in :doc:`/topics/customisation`. Please
read it first and ensure that you've:

Expand All @@ -19,8 +19,8 @@ The application class
Each Oscar app comes with an application instance which inherits from
:class:`oscar.core.application.Application`. They're mainly used to gather
URLs (with the correct permissions) for each Oscar app. This structure makes
Oscar apps more modular as each app is responsible for it's own URLs. And as
it is a class, it can be overridden as any other Oscar class; hence making
Oscar apps more modular as each app is responsible for its own URLs. And as
it is a class, it can be overridden like any other Oscar class; hence making
it straightforward to change URLs or add new views.
Each app instance exposes a ``urls`` property, which is used to access the
list of URLs of an app.
Expand All @@ -37,23 +37,24 @@ illustrates this nicely::

catalogue_app = get_class('catalogue.app', 'application')
basket_app = get_class('basket.app', 'application')
...
# ...

def get_urls(self):
urls = [
url(r'^catalogue/', include(self.catalogue_app.urls)),
url(r'^basket/', include(self.basket_app.urls)),
...
# ...
]

The root app pulls in the URLs from it's children. That means to add
The root app pulls in the URLs from its children. That means to add
all Oscar URLs to your Django project, you only need to include the ``urls``
property from the root app::

# urls.py
from oscar.app import application

urlpatterns = [
... # Your other URLs
# Your other URLs
url(r'', include(application.urls)),
]

Expand All @@ -69,7 +70,7 @@ other classes in Oscar::

catalogue_app = get_class('catalogue.app', 'application')
customer_app = get_class('customer.app', 'application')
...
# ...

That means you just need to create another
``application`` instance. It will usually inherit from Oscar's version. Say
Expand Down Expand Up @@ -103,8 +104,8 @@ instead of Oscar's default instance. Hence, create a subclass of Oscar's main
def get_urls(self):
urlpatterns = [
url(r'^catalog/', include(self.catalogue_app.urls)),

... # all the remaining URLs, removed for simplicity
# all the remaining URLs, removed for simplicity
# ...
]
return urlpatterns

Expand All @@ -117,8 +118,8 @@ it to use your new application instance instead of Oscar's default::
from myproject.app import application

urlpatterns = [
... # Your other URLs
# Your other URLs
url(r'', include(application.urls)),
]

All URLs containing ``catalogue`` previously are now displayed as ``catalog``.
All URLs containing ``catalogue`` previously are now displayed as ``catalog``.
2 changes: 1 addition & 1 deletion docs/source/howto/how_to_create_a_custom_range.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A custom range must:

Example::

class ExclamatoryProducts(object)
class ExclamatoryProducts(object):
name = "Products including a '!'"

def contains_product(self, product):
Expand Down
4 changes: 2 additions & 2 deletions docs/source/howto/how_to_setup_solr.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Haystack provides an abstraction layer on top of different search backends and
integrates with Django. Your Haystack connection settings in your
``settings.py`` for the config above should look like this:

.. code-block:: django
.. code-block:: python
HAYSTACK_CONNECTIONS = {
'default': {
Expand All @@ -65,4 +65,4 @@ If all is well, you should now be able to rebuild the search index.
The products being indexed twice is caused by a low-priority bug in Oscar and
can be safely ignored.
If the indexing succeeded, search in Oscar will be working. Search for any
term in the search box on your Oscar site, and you should get results.
term in the search box on your Oscar site, and you should get results.
15 changes: 4 additions & 11 deletions docs/source/ref/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ partner. The setting should be a dict from parter name to a path to a "wrapper"
class. For example::

OSCAR_PARTNER_WRAPPERS = {
'Acme': 'myproject.partners.AcmeWrapper'
'Omnicorp': 'myproject.partners.OmnicorpWrapper'
'Acme': 'myproject.partners.AcmeWrapper',
'Omnicorp': 'myproject.partners.OmnicorpWrapper',
}

The wrapper class should subclass ``oscar.apps.partner.wrappers.DefaultWrapper``
Expand Down Expand Up @@ -364,13 +364,6 @@ Default: ``'oscar_open_basket'``

The name of the cookie for the open basket.

``OSCAR_BASKET_COOKIE_SAVED``
-----------------------------

Default: ``'oscar_saved_basket'``

The name of the cookie for the saved basket.

Currency settings
=================

Expand Down Expand Up @@ -471,8 +464,8 @@ backwards-compatibility.
Example::

# in myproject.utils
def some_slugify(value)
pass
def some_slugify(value):
return value

# in settings.py
OSCAR_SLUG_FUNCTION = 'myproject.utils.some_slugify'
Expand Down
28 changes: 27 additions & 1 deletion docs/source/releases/v0.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ Backwards incompatible changes in 0.8
Product structure
~~~~~~~~~~~~~~~~~

Generally, backwards compatibility has been preserved. Two changes are
Generally, backwards compatibility has been preserved. Those changes are
unavoidable:

* You now need to explicitly set product structure when creating a product;
Expand All @@ -224,6 +224,7 @@ unavoidable:
deprecation warning), but if you used the old related name in a query lookup
(e.g. ``products.filter(variants__title='foo')``, you will have to change it
to ``children``.
* Template blocks and CSS classes have been renamed.

The following methods and properties have been deprecated:

Expand All @@ -236,6 +237,13 @@ The following methods and properties have been deprecated:
* ``Strategy.select_variant_stockrecords`` - Use
``select_children_stockrecords`` instead.

Furthermore, CSS classes and template blocks have been updated. Please follow
the following renaming pattern:
* ``variant-product`` becomes ``child-product``
* ``product_variants`` becomes ``child_products``
* ``variants`` becomes ``children``
* ``variant`` becomes ``child``

Shipping
~~~~~~~~

Expand Down Expand Up @@ -341,6 +349,24 @@ Other potentially breaking changes related to shipping include:
* ``oscar.apps.shipping.Scales`` has been renamed and moved to
``oscar.apps.shipping.scales.Scale``, and is now overridable.

Email address handling
----------------------

In theory, the local part of an email is case-sensitive. In practice, many
users don't know about this and most email servers don't consider the
capitalisation. Because of this, Oscar now disregards capitalisation when
looking up emails (e.g. when a user logs in).
Storing behaviour is unaltered: When a user's email address is stored (e.g.
when registering or checking out), the local part is unaltered and
the host portion is lowercased.

.. warning::

Those changes mean you might now have multiple users with email addresses
that Oscar considers identical. Please use the new
``oscar_find_duplicate_emails`` management command to check your database
and deal with any conflicts accordingly.

Misc
~~~~

Expand Down
23 changes: 16 additions & 7 deletions oscar/apps/basket/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,32 @@ def process_response(self, request, response):
and request.basket._wrapped is empty):
return response

cookie_key = self.get_cookie_key(request)
# Check if we need to set a cookie. If the cookies is already available
# but is set in the cookies_to_delete list then we need to re-set it.
has_basket_cookie = (
settings.OSCAR_BASKET_COOKIE_OPEN in request.COOKIES
and settings.OSCAR_BASKET_COOKIE_OPEN not in cookies_to_delete)
cookie_key in request.COOKIES
and cookie_key not in cookies_to_delete)

# If a basket has had products added to it, but the user is anonymous
# then we need to assign it to a cookie
if (request.basket.id and not request.user.is_authenticated()
and not has_basket_cookie):
cookie = self.get_basket_hash(request.basket.id)
response.set_cookie(
settings.OSCAR_BASKET_COOKIE_OPEN, cookie,
cookie_key, cookie,
max_age=settings.OSCAR_BASKET_COOKIE_LIFETIME, httponly=True)
return response

def get_cookie_key(self, request):
"""
Returns the cookie name to use for storing a cookie basket.
The method serves as a useful hook in multi-site scenarios where
different baskets might be needed.
"""
return settings.OSCAR_BASKET_COOKIE_OPEN

def process_template_response(self, request, response):
if hasattr(response, 'context_data'):
if response.context_data is None:
Expand Down Expand Up @@ -114,8 +124,8 @@ def get_basket(self, request):
return request._basket_cache

manager = Basket.open
cookie_basket = self.get_cookie_basket(
settings.OSCAR_BASKET_COOKIE_OPEN, request, manager)
cookie_key = self.get_cookie_key(request)
cookie_basket = self.get_cookie_basket(cookie_key, request, manager)

if hasattr(request, 'user') and request.user.is_authenticated():
# Signed-in user: if they have a cookie basket too, it means
Expand All @@ -137,8 +147,7 @@ def get_basket(self, request):

if cookie_basket:
self.merge_baskets(basket, cookie_basket)
request.cookies_to_delete.append(
settings.OSCAR_BASKET_COOKIE_OPEN)
request.cookies_to_delete.append(cookie_key)

elif cookie_basket:
# Anonymous user with a basket tied to the cookie
Expand Down
64 changes: 50 additions & 14 deletions oscar/apps/catalogue/abstract_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,26 @@ def get_absolute_url(self):
kwargs={'product_slug': self.slug, 'pk': self.id})

def clean(self):
# call clean method for product structure
"""
Validate a product. Those are the rules:
+---------------+-------------+--------------+-----------+
| | stand alone | parent | child |
+---------------+-------------+--------------+-----------+
| title | required | required | optional |
+---------------+-------------+--------------+-----------+
| product class | required | must be None | required |
+---------------+-------------+--------------+-----------+
| parent | forbidden | forbidden | required |
+---------------+-------------+--------------+-----------+
| stockrecords | 0 or more | forbidden | required |
+---------------+-------------+--------------+-----------+
| categories | 1 or more | 1 or more | forbidden |
+---------------+-------------+--------------+-----------+
Because the validation logic is quite complex, validation is delegated
to the sub method appropriate for the product's structure.
"""
getattr(self, '_clean_%s' % self.structure)()
if not self.is_parent:
self.attr.validate_attributes()
Expand Down Expand Up @@ -399,33 +418,33 @@ def attribute_summary(self):
return ", ".join(pairs)

@property
def min_variant_price_incl_tax(self):
def min_child_price_incl_tax(self):
"""
Return minimum variant price including tax
Return minimum child product price including tax
"""
return self._min_variant_price('price_incl_tax')
return self._min_child_price('price_incl_tax')

@property
def min_variant_price_excl_tax(self):
def min_child_price_excl_tax(self):
"""
Return minimum variant price excluding tax
Return minimum child product price excluding tax
"""
return self._min_variant_price('price_excl_tax')
return self._min_child_price('price_excl_tax')

def _min_variant_price(self, property):
def _min_child_price(self, property):
"""
Return minimum variant price
Return minimum child product price
"""
prices = []
for variant in self.variants.all():
if variant.has_stockrecords:
prices.append(getattr(variant.stockrecord, property))
for child in self.children.all():
if child.has_stockrecords:
prices.append(getattr(child.stockrecord, property))
if not prices:
return None
prices.sort()
return prices[0]

# Deprecated properties
# The properties below are based on deprecated naming conventions

@property
@deprecated
Expand All @@ -447,15 +466,32 @@ def is_top_level(self):
@deprecated
def is_group(self):
"""
Test if this is a top level product and has more than 0 variants
Test if this is a parent product
"""
return self.is_parent

@property
@deprecated
def is_variant(self):
"""Return True if a product is not a top level product"""
return self.is_child

@property
@deprecated
def min_variant_price_incl_tax(self):
"""
Return minimum variant price including tax
"""
return self._min_child_price('price_incl_tax')

@property
@deprecated
def min_variant_price_excl_tax(self):
"""
Return minimum variant price excluding tax
"""
return self._min_child_price('price_excl_tax')

# Wrappers

def get_title(self):
Expand Down
2 changes: 1 addition & 1 deletion oscar/apps/catalogue/reviews/abstract_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def can_user_vote(self, user):
review
"""
if not user.is_authenticated():
return False, u"Only signed in users can vote"
return False, _(u"Only signed in users can vote")
vote = self.votes.model(review=self, user=user, delta=1)
try:
vote.full_clean()
Expand Down
7 changes: 5 additions & 2 deletions oscar/apps/catalogue/reviews/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.conf.urls import url
from django.contrib.auth.decorators import login_required

from oscar.core.application import Application
from oscar.core.loading import get_class
Expand All @@ -17,8 +18,10 @@ def get_urls(self):
urls = [
url(r'^(?P<pk>\d+)/$', self.detail_view.as_view(),
name='reviews-detail'),
url(r'^add/$', self.create_view.as_view(), name='reviews-add'),
url(r'^(?P<pk>\d+)/vote/$', self.vote_view.as_view(),
url(r'^add/$', self.create_view.as_view(),
name='reviews-add'),
url(r'^(?P<pk>\d+)/vote/$',
login_required(self.vote_view.as_view()),
name='reviews-vote'),
url(r'^$', self.list_view.as_view(), name='reviews-list'),
]
Expand Down

0 comments on commit 820012e

Please sign in to comment.