Kai Diefenbach avatar Kai Diefenbach committed 9a3386d Merge

Merged pluggable criteria branch.

Comments (0)

Files changed (40)

docs/developer/api.rst

 Plug-ins
 ========
 
+.. _api_criterion:
+
+Criterion
+---------
+
+.. py:class:: lfs.criteria.models.Criterion
+
+    Base class for all criteria.
+
+    **Attributes:**
+
+        cart
+            The current cart of the current customer.
+
+        content
+            The content object the criterion belongs to.
+
+        operator
+            The current selected operator for the criterion.
+
+        position
+            The position of the criterion within a list of criteria of the
+            content object.
+
+        product
+            The product, if the criterion is called from a product detail view.
+            Otherwise this is None.
+
+        request
+            The current request.
+
+    **Constants:**
+
+        EQUAL, LESS_THAN, LESS_THAN_EQUAL, GREATER_THAN, GREATER_THAN_EQUAL, IS_SELECTED, IS_NOT_SELECTED, IS_VALID, IS_NOT_VALID, CONTAINS
+            Integers which represents certain operators.
+
+        INPUT, SELECT, MULTIPLE_SELECT
+            Constants which represents the types of selectable values. One of
+            these must be returned from the ``get_value_type`` method.
+
+        NUMBER_OPERATORS
+            A list of operators which can be returned from the ``get_operators``
+            method.
+
+            .. code-block:: python
+
+                [
+                    [EQUAL, _(u"Equal to")],
+                    [LESS_THAN, _(u"Less than")],
+                    [LESS_THAN_EQUAL, _(u"Less than equal to")],
+                    [GREATER_THAN, _(u"Greater than")],
+                    [GREATER_THAN_EQUAL, _(u"Greater than equal to")],
+                ]
+
+
+        SELECTION_OPERATORS
+            A list of operators which can be returned from the ``get_operators``
+            method.
+
+            .. code-block:: python
+
+                [
+                    [IS_SELECTED, _(u"Is selected")],
+                    [IS_NOT_SELECTED, _(u"Is not selected")],
+                ]
+
+        VALID_OPERATORS
+            A list of operators which can be returned from the ``get_operators``
+            method.
+
+            .. code-block:: python
+
+                [
+                    [IS_VALID, _(u"Is valid")],
+                    [IS_NOT_VALID, _(u"Is not valid")],
+                ]
+
+        STRING_OPERATORS
+            A list of operators which can be returned from the ``get_operators``
+            method.
+
+            .. code-block:: python
+
+                [
+                    [EQUAL, _(u"Equal to")],
+                    [CONTAINS, _(u"Contains")],
+                ]
+
+    .. py:method:: lfs.criteria.models.Criterion.get_operators
+
+        Returns the selectable operators of the criterion which are displayed to
+        the shop manager. This is a list of list, whereas the the first value is
+        integer, which is stored within the criterion and the second value is
+        the string which is displayed to the shop manager, e.g.:
+
+        .. code-block:: python
+
+            [
+                [0, _(u"Equal to")],
+                [1, _(u"Less than")],
+                [2, _(u"Less than equal to")],
+                [3, _(u"Greater than")],
+                [4, _(u"Greater than equal to")],
+            ]
+
+        .. note::
+
+            You can use one of the provided class attributes, see above.
+
+            * NUMBER_OPERATORS
+            * SELECTION_OPERATORS
+            * VALID_OPERATORS
+            * STRING_OPERATORS
+
+    .. py:method:: lfs.criteria.models.Criterion.get_selectable_values(request)
+
+        Returns the selectable values as a list of dictionary, see below. This
+        is only called when ``get_value_type`` returns SELECT or
+        MULTIPLE_SELECT.
+
+        .. code-block:: python
+
+            [
+                {
+                    "id": 0,
+                    "name": "Name 0",
+                    "selected": False,
+                },
+                {
+                    "id": 1,
+                    "name": "Name 1",
+                    "selected": True,
+                },
+            ]
+
+    .. py:method:: lfs.criteria.models.Criterion.get_value_type
+
+        Returns the type of the selectable values field. Must return one of:
+
+        * self.INPUT
+        * self.SELECT
+        * self.MULTIPLE_SELECT
+
+    .. py:method:: lfs.criteria.models.Criterion.get_value
+
+        Returns the current value of the criterion.
+
+    .. py:method:: lfs.criteria.models.Criterion.is_valid(request, product=None)
+
+        Returns ``True`` if the criterion is valid otherwise ``False``.
+
+    .. py:method:: lfs.criteria.models.Criterion.update(value)
+
+        Updates the value of the criterion.
+
+        **Parameters:**
+
+        value
+            The value the shop user has entered for the criterion.
+
 .. _order_number_generator:
 
 OrderNumberGenerator

docs/developer/howtos/how_to_add_own_criteria/index.rst

+==========================
+How to add an own criteria
+==========================
+
+In this how-to you will learn how to add an own criterion.
+
+The goal is to create a criterion, in which the customer can enter a SKU and
+decide (via operators) whether the criterion is valid if the product, with the
+entered SKU, is in the cart or not.
+
+Please see also the example application  :download:`product_criterion
+<product_criterion.tar.gz>` or refer to the default implementation of LFS within
+``lfs.criteria.models``.
+
+Create an application
+=====================
+
+First you need to create a default Django application (or use an existing one),
+where  you can put in your plugin. If you do not know how to do this, please
+refer to the excellent `Django tutorial
+<http://docs.djangoproject.com/en/dev/intro/tutorial01/>`_.
+
+Implement the Criterion Model
+=============================
+
+The main part of the criterion consists of a model which must inherit from the
+``Criterion`` base class.
+
+Create the Class
+----------------
+
+.. code-block:: python
+
+    class ProductCriterion(Criterion):
+        value = models.CharField(max_length=100)
+
+The only attribute we need is the value the shop admin will save for the
+criterion. The attribute can have any type you need. In this example we use a
+simple character field. This example criterion will be valid, when the the
+product with the given SKU is within the card.
+
+Implement necessary Methods
+---------------------------
+
+In the next steps we implement all necessary methods which are needed to make
+the criterion work.
+
+The ``get_operators`` method needs to return the available operators for this
+criterion. It is a list of list, whereas the first value is an integer and the
+second value is the name of the operator.
+
+.. code-block:: python
+
+    def get_operators(self):
+        return [
+            [0, _(u"Is in cart")],
+            [1, _(u"Is not in cart")],
+        ]
+
+The ``is_valid`` method needs to return a boolean. If it returns ``True`` the
+criterion is considered valid, it returns ``False`` the criterion is considered
+not valid.
+
+.. code-block:: python
+
+    def is_valid(self):
+        if self.product:
+            return self.value == self.product.sku
+        elif self.cart:
+            result = any([self.value == item.product.sku for item in self.cart.get_items()])
+            return result if self.operator == 0 else not result
+        else:
+            return False
+
+.. note::
+
+    Within the ``is_valid`` method (as in all methods of the ``Criterion``
+    class) following attributes are available:
+
+        product
+            This is only set, when the criterion is called from the product
+            detail view otherwise it is ``None``.
+
+        cart
+            The current cart of the current user.
+
+        request
+            The current request.
+
+Please see also the example application :download:`product_criterion
+<product_criterion.tar.gz>` for the complete implementation of the model.
+
+Plug in the Criterion
+=====================
+
+Now as the code is ready, you can easily plugin your own criterion:
+
+#. Add your application to the PYTHONPATH
+
+#. Add your application to settings.INSTALLED_APPS and sync your database::
+
+    INSTALLED_APPS = (
+        ...
+        "product_criterion",
+    )
+
+#. Add the class to the :ref:`LFS_CRITERIA <settings_lfs_criteria>` setting::
+
+    LFS_CRITERIA = [
+        ...
+        ["product_criterion.models.CartPriceCriterion", _(u"Product Criterion")],
+    ]
+
+#. As all criteria are models, you have to synchronize your database::
+
+    $ bin/django syncdb
+
+#. Restart your instance and the criterion should be available for selection,
+   for instance within the discount criteria tab.
+
+And that's it
+=============
+
+You should now see your criterion within the criteria tab of ``Discounts`` for
+instance. You can enter a product SKU to it and select one of the above
+mentioned operators.
+
+Good to know
+============
+
+* You can also create criteria with select or multiple select fields. See the
+  :ref:`API <api_criterion>` or the default ``Country`` criterion within
+  ``lfs.criteria.models`` for more.
+
+* You can override more than the two mentioned methods above. See the
+  :ref:`Criterion API <api_criterion>` which methods are provided by the base
+  class.
+
+See Also
+========
+
+* :doc:`Criteria concept </user/concepts/criteria>`
+* :ref:`Criterion API <api_criterion>`
+* Look into the default criteria within ``lfs.criteria.models`` to see how these
+  are implemented
Add a comment to this file

docs/developer/howtos/how_to_add_own_criteria/product_criterion.tar.gz

Binary file added.

docs/developer/settings.rst

     product, which has been added to the cart. A reasonable alternative is
     ``lfs_checkout_dispatcher``, which redirects directly to the checkout view.
 
+.. _settings_lfs_criteria:
+
+LFS_CRITERIA
+    List of list of available criteria, whereas the first entry is the dotted
+    name to a criterion and the second entry is the name of the criterion, which
+    is displayed to the users. These criteria are provided for selection within
+    several places. LFS is shipped with following criteria::
+
+        LFS_CRITERIA = [
+            ["lfs.criteria.models.CartPriceCriterion", _(u"Cart Price")],
+            ["lfs.criteria.models.CombinedLengthAndGirthCriterion", _(u"Combined Length and Girth")],
+            ["lfs.criteria.models.CountryCriterion", _(u"Country")],
+            ["lfs.criteria.models.HeightCriterion", _(u"Height")],
+            ["lfs.criteria.models.LengthCriterion", _(u"Length")],
+            ["lfs.criteria.models.WidthCriterion", _(u"Width")],
+            ["lfs.criteria.models.WeightCriterion", _(u"Weight")],
+            ["lfs.criteria.models.ShippingMethodCriterion", _(u"Shipping Method")],
+            ["lfs.criteria.models.PaymentMethodCriterion", _(u"Payment Method")],
+        ]
+
+    See also :doc:`Criteria concept </user/concepts/criteria>` and
+    :doc:`/developer/howtos/how_to_add_own_criteria/index`.
+
 LFS_DOCS
     Base URL to the LFS docs. This is used for the context aware help link
     within the management interface. Defaults to
    developer/howtos/how_to_add_own_order_numbers/index.rst
    developer/howtos/how_to_shipping_price.rst
    developer/howtos/how_to_integrate_localized_version_of_tinymce.rst
+   developer/howtos/how_to_add_own_criteria/index.rst
 
 =========================
 Miscellaneous Information

lfs/core/management/commands/lfs_migrate.py

 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.db import connection
+from django.db import transaction
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
             print "You are up-to-date"
 
     def migrate_to_08(self, application, version):
+        from django.contrib.contenttypes.models import ContentType
+        from lfs.criteria.models import CartPriceCriterion
+        from lfs.criteria.models import CombinedLengthAndGirthCriterion
+        from lfs.criteria.models import CountryCriterion
+        from lfs.criteria.models import HeightCriterion
+        from lfs.criteria.models import LengthCriterion
+        from lfs.criteria.models import WidthCriterion
+        from lfs.criteria.models import WeightCriterion
+        from lfs.criteria.models import ShippingMethodCriterion
+        from lfs.criteria.models import PaymentMethodCriterion
+
+
+        # Delete locale from shop
         db.delete_column("core_shop", "default_locale")
-        application.version = "0.8"
-        application.save()
+
+        # Migrate Criteria #####################################################
+
+        cursor1 = connection.cursor()
+        cursor2 = connection.cursor()
+        cursor3 = connection.cursor()
+        cursor4 = connection.cursor()
+
+        db.add_column("criteria_cartpricecriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_combinedlengthandgirthcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_heightcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_lengthcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_widthcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_weightcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_countrycriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_shippingmethodcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+        db.add_column("criteria_paymentmethodcriterion", "criterion_ptr_id", models.IntegerField(null=True))
+
+        # CartPriceCriterion
+        db.add_column("criteria_cartpricecriterion", "value", models.FloatField(default=0.0))
+
+        cursor1.execute("""SELECT id FROM criteria_cartpricecriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(CartPriceCriterion)
+        cursor2.execute("""SELECT id, operator, price FROM criteria_cartpricecriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            CartPriceCriterion.objects.create(operator=row[1], value=row[2], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+        cursor1.execute("""DELETE FROM criteria_cartpricecriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_column("criteria_cartpricecriterion", "price")
+
+        # CombinedLengthAndGirthCriterion
+        db.add_column("criteria_combinedlengthandgirthcriterion", "value", models.FloatField(default=0.0))
+
+        cursor1.execute("""SELECT id FROM criteria_combinedlengthandgirthcriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(CombinedLengthAndGirthCriterion)
+        cursor2.execute("""SELECT id, operator, clag FROM criteria_combinedlengthandgirthcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            CombinedLengthAndGirthCriterion.objects.create(operator=row[1], value=row[2], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+        cursor1.execute("""DELETE FROM criteria_combinedlengthandgirthcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        # HeightCriterion
+        db.add_column("criteria_heightcriterion", "value", models.FloatField(default=0.0))
+
+        cursor1.execute("""SELECT id FROM criteria_heightcriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(HeightCriterion)
+        cursor2.execute("""SELECT id, operator, height FROM criteria_heightcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            HeightCriterion.objects.create(operator=row[1], value=row[2], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+        cursor1.execute("""DELETE FROM criteria_heightcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+        db.delete_column("criteria_heightcriterion", "height")
+
+        # LengthCriterion
+        db.add_column("criteria_lengthcriterion", "value", models.FloatField(default=0.0))
+
+        cursor1.execute("""SELECT id FROM criteria_lengthcriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(LengthCriterion)
+        cursor2.execute("""SELECT id, operator, length FROM criteria_lengthcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            LengthCriterion.objects.create(operator=row[1], value=row[2], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+        cursor1.execute("""DELETE FROM criteria_lengthcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_column("criteria_lengthcriterion", "length")
+
+        # WidthCriterion
+        db.add_column("criteria_widthcriterion", "value", models.FloatField(default=0.0))
+
+        cursor1.execute("""SELECT id FROM criteria_widthcriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(WidthCriterion)
+        cursor2.execute("""SELECT id, operator, width FROM criteria_widthcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            WidthCriterion.objects.create(operator=row[1], value=row[2], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+        cursor1.execute("""DELETE FROM criteria_widthcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_column("criteria_widthcriterion", "width")
+
+        # WeightCriterion
+        db.add_column("criteria_weightcriterion", "value", models.FloatField(default=0.0))
+
+        cursor1.execute("""SELECT id FROM criteria_weightcriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(WeightCriterion)
+        cursor2.execute("""SELECT id, operator, weight FROM criteria_weightcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            WeightCriterion.objects.create(operator=row[1], value=row[2], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+        cursor1.execute("""DELETE FROM criteria_weightcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_column("criteria_weightcriterion", "weight")
+
+        # CountryCriterion
+        from lfs.core.models import Country
+        db.create_table('criteria_countrycriterion_value', (
+            ('id', models.AutoField(primary_key=True)),
+            ('countrycriterion', models.ForeignKey(CountryCriterion)),
+            ('country', models.ForeignKey(Country)),
+        ))
+
+        cursor1.execute("""SELECT id FROM criteria_countrycriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(CountryCriterion)
+        cursor2.execute("""SELECT id, operator FROM criteria_countrycriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            cc = CountryCriterion.objects.create(operator=row[1], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+            cursor4.execute("""Select country_id FROM criteria_countrycriterion_countries WHERE id=%s""" %  row[0])
+            for row_2 in cursor4.fetchall():
+                cc.value.add(row_2[0])
+
+        cursor1.execute("""DELETE FROM criteria_countrycriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_table("criteria_countrycriterion_countries")
+
+        # PaymentMethodCriterion
+        from lfs.payment.models import PaymentMethod
+        db.create_table('criteria_paymentmethodcriterion_value', (
+            ('id', models.AutoField(primary_key=True)),
+            ('paymentmethodcriterion', models.ForeignKey(PaymentMethodCriterion)),
+            ('paymentmethod', models.ForeignKey(PaymentMethod)),
+        ))
+
+        cursor1.execute("""SELECT id FROM criteria_paymentmethodcriterion""")
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(PaymentMethodCriterion)
+        cursor2.execute("""SELECT id, operator FROM criteria_paymentmethodcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            pmc = PaymentMethodCriterion.objects.create(operator=row[1], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+            cursor4.execute("""Select paymentmethod_id FROM criteria_paymentmethodcriterion_payment_methods WHERE id=%s""" %  row[0])
+            for row_2 in cursor4.fetchall():
+                pmc.value.add(row_2[0])
+
+        cursor1.execute("""DELETE FROM criteria_paymentmethodcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_table("criteria_paymentmethodcriterion_payment_methods")
+
+        # ShippingMethodCriterion
+        from lfs.shipping.models import ShippingMethod
+        db.create_table('criteria_shippingmethodcriterion_value', (
+            ('id', models.AutoField(primary_key=True)),
+            ('shippingmethodcriterion', models.ForeignKey(PaymentMethodCriterion)),
+            ('shippingmethod', models.ForeignKey(ShippingMethod)),
+        ))
+
+        old_criteria = ", ".join([str(row[0]) for row in cursor1.fetchall()])
+
+        content_type = ContentType.objects.get_for_model(ShippingMethodCriterion)
+        cursor2.execute("""SELECT id, operator FROM criteria_shippingmethodcriterion""")
+        for row in cursor2.fetchall():
+            cursor3.execute("""Select content_type_id, content_id, position FROM criteria_criteriaobjects WHERE criterion_type_id=%s and criterion_id=%s""" % (content_type.id, row[0]))
+            criterion_object = cursor3.fetchone()
+            smc = ShippingMethodCriterion.objects.create(operator=row[1], content_type_id=criterion_object[0], content_id=criterion_object[1], position=criterion_object[2])
+
+            cursor4.execute("""Select shippingmethod_id FROM criteria_shippingmethodcriterion_shipping_methods WHERE id=%s""" %  row[0])
+            for row_2 in cursor4.fetchall():
+                smc.value.add(row_2[0])
+
+        cursor1.execute("""DELETE FROM criteria_shippingmethodcriterion WHERE id in (%s)""" % old_criteria)
+        transaction.commit_unless_managed()
+
+        db.delete_table("criteria_shippingmethodcriterion_shipping_methods")
 
     def migrate_to_07(self, application, version):
         from lfs.catalog.models import Product

lfs/criteria/admin.py

-# django imports
-from django.contrib import admin
-
-# lfs imports
-from lfs.criteria.models.criteria_objects import CriteriaObjects
-from lfs.criteria.models.criteria import CartPriceCriterion
-from lfs.criteria.models.criteria import WeightCriterion
-from lfs.criteria.models.criteria import CountryCriterion
-
-
-class CartPriceCriterionAdmin(admin.ModelAdmin):
-    """
-    """
-admin.site.register(CartPriceCriterion, CartPriceCriterionAdmin)
-
-
-class CountryCriterionAdmin(admin.ModelAdmin):
-    """
-    """
-admin.site.register(CountryCriterion, CountryCriterionAdmin)
-
-
-class WeightCriterionAdmin(admin.ModelAdmin):
-    """
-    """
-admin.site.register(WeightCriterion, WeightCriterionAdmin)
-
-
-class CriteriaObjectsAdmin(admin.ModelAdmin):
-    """
-    """
-admin.site.register(CriteriaObjects, CriteriaObjectsAdmin)

lfs/criteria/base.py

+# django imports
+from django.contrib.contenttypes.models import ContentType
+
+# lfs imports
+from lfs.core.utils import import_symbol
+
+
+class Criteria(object):
+    """
+    Base class for objects which have criteria.
+    """
+    def is_valid(self, request, product=None):
+        """
+        Returns ``True`` if the object is valid, otherwise ``False``.
+        """
+        for criterion in self.get_criteria():
+            criterion.request = request
+            criterion.product = product
+            if criterion.is_valid() == False:
+                return False
+        return True
+
+    def get_criteria(self):
+        """
+        Returns all criteria of the object.
+        """
+        content_type = ContentType.objects.get_for_model(self)
+
+        criteria = []
+        from lfs.criteria.models import Criterion
+        for criterion in Criterion.objects.filter(content_id=self.id, content_type=content_type):
+            criteria.append(criterion.get_content_object())
+        return criteria
+
+    def save_criteria(self, request):
+        """
+        Saves all passed criteria (via request.POST) to the object.
+        """
+        # First we delete all existing criteria objects for the given object.
+        for co in self.get_criteria():
+            co.delete()
+
+        # Then we add all passed criteria to the object.
+        for key, model in request.POST.items():
+            if key.startswith("type"):
+                try:
+                    id = key.split("-")[1]
+                except KeyError:
+                    continue
+
+                # Get the values for the criterion
+                operator = request.POST.get("operator-%s" % id)
+                position = request.POST.get("position-%s" % id)
+
+                criterion_class = import_symbol(model)
+                criterion = criterion_class.objects.create(content=self, operator=operator, position=position)
+
+                if criterion.get_value_type() == criterion.MULTIPLE_SELECT:
+                    value = request.POST.getlist("value-%s" % id)
+                else:
+                    value = request.POST.get("value-%s" % id)
+
+                criterion.update(value)

lfs/criteria/models.py

+# python imports
+import datetime
+
+# django imports
+from django.conf import settings
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.utils.translation import ugettext_lazy as _, ugettext
+from django.template import RequestContext
+from django.template.loader import render_to_string
+
+# lfs imports
+import lfs.cart.utils
+import lfs.core.utils
+from lfs import shipping
+from lfs.core.models import Country
+from lfs.payment.models import PaymentMethod
+from lfs.shipping.models import ShippingMethod
+
+
+class Criterion(models.Model):
+    """
+    Base class for all criteria.
+
+    **Attributes:**
+
+        cart
+            The current cart of the current customer.
+
+        content
+            The content object the criterion belongs to.
+
+        operator
+            The current selected operator for the criterion.
+
+        position
+            The position of the criterion within a list of criteria of the
+            content object.
+
+        product
+            The product, if the criterion is called from a product detail view.
+            Otherwise this is None.
+
+        request
+            The current request.
+
+    **Constants:**
+
+        EQUAL, LESS_THAN, LESS_THAN_EQUAL, GREATER_THAN, GREATER_THAN_EQUAL, IS_SELECTED, IS_NOT_SELECTED, IS_VALID, IS_NOT_VALID, CONTAINS
+            Integers which represents certain operators.
+
+        INPUT, SELECT, MULTIPLE_SELECT
+            Constants which represents the types of selectable values. One of
+            these must be returned from ``get_value_type``.
+
+        NUMBER_OPERATORS
+            A list of operators which can be returned from ``get_operators``.
+
+            .. code-block:: python
+
+                [
+                    [EQUAL, _(u"Equal to")],
+                    [LESS_THAN, _(u"Less than")],
+                    [LESS_THAN_EQUAL, _(u"Less than equal to")],
+                    [GREATER_THAN, _(u"Greater than")],
+                    [GREATER_THAN_EQUAL, _(u"Greater than equal to")],
+                ]
+
+
+        SELECTION_OPERATORS
+            A list of operators which can be returned from ``get_operators``.
+
+            .. code-block:: python
+
+                [
+                    [IS_SELECTED, _(u"Is selected")],
+                    [IS_NOT_SELECTED, _(u"Is not selected")],
+                ]
+
+        VALID_OPERATORS
+            A list of operators which can be returned from ``get_operators``.
+
+            .. code-block:: python
+
+                [
+                    [IS_VALID, _(u"Is valid")],
+                    [IS_NOT_VALID, _(u"Is not valid")],
+                ]
+
+        STRING_OPERATORS
+            A list of operators which can be return from ``get_operators``.
+
+            .. code-block:: python
+
+                [
+                    [EQUAL, _(u"Equal to")],
+                    [CONTAINS, _(u"Contains")],
+                ]
+    """
+    content_type = models.ForeignKey(ContentType, verbose_name=_(u"Content type"), related_name="content_type")
+    content_id = models.PositiveIntegerField(_(u"Content id"))
+    content = generic.GenericForeignKey(ct_field="content_type", fk_field="content_id")
+    sub_type = models.CharField(_(u"Sub type"), max_length=100, blank=True)
+
+    position = models.PositiveIntegerField(_(u"Position"), default=999)
+    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True)
+
+    class Meta:
+        ordering = ("position", )
+
+    EQUAL = 0
+    LESS_THAN = 1
+    LESS_THAN_EQUAL = 2
+    GREATER_THAN = 3
+    GREATER_THAN_EQUAL = 4
+    IS_SELECTED = 10
+    IS_NOT_SELECTED = 11
+    IS_VALID = 21
+    IS_NOT_VALID = 22
+    CONTAINS = 32
+
+    INPUT = 0
+    SELECT = 1
+    MULTIPLE_SELECT = 2
+
+    NUMBER_OPERATORS = [
+        [EQUAL, _(u"Equal to")],
+        [LESS_THAN, _(u"Less than")],
+        [LESS_THAN_EQUAL, _(u"Less than equal to")],
+        [GREATER_THAN, _(u"Greater than")],
+        [GREATER_THAN_EQUAL, _(u"Greater than equal to")],
+    ]
+
+    SELECTION_OPERATORS = [
+        [IS_SELECTED, _(u"Is selected")],
+        [IS_NOT_SELECTED, _(u"Is not selected")],
+    ]
+
+    VALID_OPERATORS = [
+        [IS_VALID, _(u"Is valid")],
+        [IS_NOT_VALID, _(u"Is not valid")],
+    ]
+
+    STRING_OPERATORS = [
+        [EQUAL, _(u"Equal to")],
+        [CONTAINS, _(u"Contains")],
+    ]
+
+    def __unicode__(self):
+        return ugettext("%(name)s: %(operator)s %(value)s") % {
+            'name': self.get_name(),
+            'operator': self.get_current_operator_as_string(),
+            'value': self.get_value_as_string()
+        }
+
+    def save(self, *args, **kwargs):
+        if self.sub_type == "":
+            self.sub_type = self.__class__.__name__.lower()
+        super(Criterion, self).save(*args, **kwargs)
+
+    @property
+    def cart(self):
+        """
+        Returns the current cart of the current customer.
+        """
+        return lfs.cart.utils.get_cart(self.request)
+
+    def get_content_object(self):
+        """
+        Returns the specific content object of the criterion.
+
+        This can be call on Criterion instances to get the specific criterion
+        instance.
+        """
+        if self.__class__.__name__.lower() == "criterion":
+            return getattr(self, self.sub_type)
+        else:
+            return self
+
+    def get_current_operator_as_string(self):
+        """
+        Returns the current operator as string.
+        """
+        for operator in self.get_operators():
+            if self.operator == operator[0]:
+                return operator[1]
+
+    def get_name(self):
+        """
+        Returns the name of the criterion as string.
+        """
+        klass = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
+        for x in settings.LFS_CRITERIA:
+            if x[0] == klass:
+                return x[1]
+        return self.__class__.__name__
+
+    def get_operators(self):
+        """
+        Returns the selectable operators of the criterion which are displayed to
+        the shop manager. This is a list of list, whereas the the first value is
+        integer, which is stored within the criterion and the second value is
+        the string which is displayed to the shop manager, e.g.:
+
+        .. code-block:: python
+
+            [
+                [0, _(u"Equal to")],
+                [1, _(u"Less than")],
+                [2, _(u"Less than equal to")],
+                [3, _(u"Greater than")],
+                [4, _(u"Greater than equal to")],
+            ]
+
+        .. note::
+
+            You can use one of the provided class attributes, see above.
+
+            * NUMBER_OPERATORS
+            * SELECTION_OPERATORS
+            * VALID_OPERATORS
+            * STRING_OPERATORS
+        """
+        raise NotImplementedError
+
+    def get_selectable_values(self, request):
+        """
+        Returns the selectable values as a list of dictionary:
+
+            [
+                {
+                    "id": 0,
+                    "name": "Name 0",
+                    "selected": False,
+                },
+                {
+                    "id": 1,
+                    "name": "Name 1",
+                    "selected": True,
+                },
+            ]
+
+        """
+        return []
+
+    def get_template(self, request):
+        """
+        Returns the template to render the criterion.
+        """
+        return "manage/criteria/base.html"
+
+    def get_value(self):
+        """
+        Returns the current value of the criterion.
+        """
+        return self.value
+
+    def get_value_type(self):
+        """
+        Returns the type of the selectable values field. Must return one of:
+
+            * self.INPUT
+            * self.SELECT
+            * self.MULTIPLE_SELECT
+        """
+        return self.INPUT
+
+    def get_value_as_string(self):
+        """
+        Returns the current value of the criterion as string.
+        """
+        value = self.get_value()
+
+        if value.__class__.__name__ == "ManyRelatedManager":
+            values = []
+            for value in self.get_value().all():
+                values.append(value.name)
+            return ", ".join(values)
+        else:
+            return value
+
+    def is_valid(self):
+        """
+        Returns ``True`` if the criterion is valid otherwise ``False``.
+        """
+        raise NotImplementedError
+
+    def render(self, request, position):
+        """
+        Renders the criterion as html in order to displayed it within the
+        management form.
+        """
+        operators = []
+        for operator in self.get_operators():
+            if self.operator == operator[0]:
+                selected = True
+            else:
+                selected = False
+
+            operators.append({
+                "id": operator[0],
+                "name": operator[1],
+                "selected": selected,
+            })
+
+        criteria = []
+        for criterion in settings.LFS_CRITERIA:
+            klass = criterion[0].split(".")[-1]
+            if self.__class__.__name__ == klass:
+                selected = True
+            else:
+                selected = False
+
+            criteria.append({
+                "module": criterion[0],
+                "name": criterion[1],
+                "selected": selected,
+            })
+
+        if self.id:
+            id = "ex%s" % self.id
+        else:
+            id = datetime.datetime.now().microsecond
+
+        return render_to_string(self.get_template(request), RequestContext(request, {
+            "id": id,
+            "operator": self.operator,
+            "value": self.get_value(),
+            "position": position,
+            "operators": operators,
+            "criteria": criteria,
+            "selectable_values": self.get_selectable_values(request),
+            "value_type": self.get_value_type(),
+            "criterion": self,
+        }))
+
+    def update(self, value):
+        """
+        Updates the value of the criterion.
+
+        **Parameters:**
+
+        value
+            The value the shop user has entered for the criterion.
+        """
+        if isinstance(self.value, float):
+            try:
+                value = float(value)
+            except (ValueError, TypeError):
+                value = 0.0
+            self.value = value
+        elif isinstance(self.value, int):
+            try:
+                value = int(value)
+            except (ValueError, TypeError):
+                value = 0
+            self.value = value
+        elif self.value.__class__.__name__ == "ManyRelatedManager":
+            for value_id in value:
+                self.value.add(value_id)
+        else:
+            self.value = value
+
+        self.save()
+
+
+class CartPriceCriterion(Criterion):
+    """
+    Criterion to check against cart/product price.
+    """
+    value = models.FloatField(_(u"Price"), default=0.0)
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.NUMBER_OPERATORS
+
+    def is_valid(self):
+        """
+        If product is given the weigth is taken from the product, otherwise it
+        is the total price of all products within the cart.
+        """
+        if self.product:
+            price = self.product.get_price(self.request)
+        elif self.cart:
+            price = self.cart.get_price_gross(self.request)
+        else:
+            price = 0
+
+        if (self.operator == self.EQUAL) and (price == self.value):
+            return True
+        elif (self.operator == self.LESS_THAN) and (price < self.value):
+            return True
+        elif (self.operator == self.LESS_THAN_EQUAL) and (price <= self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN) and (price > self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN_EQUAL) and (price >= self.value):
+            return True
+        else:
+            return False
+
+
+class CombinedLengthAndGirthCriterion(Criterion):
+    """
+    Criterion to check against combined length and girth.
+    """
+    value = models.FloatField(_(u"CLAG"), default=0.0)
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.NUMBER_OPERATORS
+
+    def is_valid(self):
+        """
+        If product is given the clag is taken from the product, otherwise it
+        is the clag of all products within the cart.
+        """
+        if self.product:
+            clag = (2 * self.product.get_width()) + (2 * self.product.get_height()) + self.product.get_length()
+        else:
+            if self.cart is None:
+                clag = 0
+            else:
+                max_width = 0
+                max_length = 0
+                total_height = 0
+                for item in cart.get_items():
+                    if max_length < item.product.get_length():
+                        max_length = item.product.get_length()
+
+                    if max_width < item.product.get_width():
+                        max_width = item.product.get_width()
+
+                    total_height += item.product.get_height()
+
+                clag = (2 * max_width) + (2 * total_height) + max_length
+
+        if (self.operator == self.EQUAL) and (clag == self.value):
+            return True
+        elif (self.operator == self.LESS_THAN) and (clag < self.value):
+            return True
+        elif (self.operator == self.LESS_THAN_EQUAL) and (clag <= self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN) and (clag > self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN_EQUAL) and (clag >= self.value):
+            return True
+        else:
+            return False
+
+
+class CountryCriterion(Criterion):
+    """
+    Criterion to check against shipping country.
+    """
+    value = models.ManyToManyField(Country, verbose_name=_(u"Countries"))
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.SELECTION_OPERATORS
+
+    def get_selectable_values(self, request):
+        shop = lfs.core.utils.get_default_shop(request)
+        countries = []
+        for country in shop.shipping_countries.all():
+            if country in self.value.all():
+                selected = True
+            else:
+                selected = False
+
+            countries.append({
+                "id": country.id,
+                "name": country.name,
+                "selected": selected,
+            })
+
+        return countries
+
+    def get_value_type(self):
+        return self.MULTIPLE_SELECT
+
+    def is_valid(self):
+        country = shipping.utils.get_selected_shipping_country(self.request)
+        if self.operator == self.IS_SELECTED:
+            return country in self.value.all()
+        else:
+            return country not in self.value.all()
+
+
+class HeightCriterion(Criterion):
+    """
+    Criterion to check against product's height / cart's total height.
+    """
+    value = models.FloatField(_(u"Height"), default=0.0)
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.NUMBER_OPERATORS
+
+    def is_valid(self):
+        """
+        If product is given the weigth is taken from the product, otherwise it
+        is the total height of all products within the cart.
+        """
+        if self.product:
+            height = self.product.get_height()
+        elif self.cart:
+            sum([item.product.get_height() * item.amount for item in cart.get_items()])
+        else:
+            height = 0
+
+        if (self.operator == self.EQUAL) and (height == self.value):
+            return True
+        elif (self.operator == self.LESS_THAN) and (height < self.value):
+            return True
+        elif (self.operator == self.LESS_THAN_EQUAL) and (height <= self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN) and (height > self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN_EQUAL) and (height >= self.value):
+            return True
+        else:
+            return False
+
+
+class LengthCriterion(Criterion):
+    """
+    Criterion to check against product's length / cart's max length.
+    """
+    value = models.FloatField(_(u"Length"), default=0.0)
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.NUMBER_OPERATORS
+
+    def is_valid(self):
+        """
+        If product is given the length is taken from the product otherwise it
+        is the max length of all products within the cart.
+        """
+        if self.product:
+            max_length = self.product.get_length()
+        elif self.cart:
+            max_length = max([item.product.get_length() for item in self.cart.get_items()])
+        else:
+            max_length = 0
+
+        if (self.operator == self.LESS_THAN) and (max_length < self.value):
+            return True
+        elif (self.operator == self.LESS_THAN_EQUAL) and (max_length <= self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN) and (max_length > self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN_EQUAL) and (max_length >= self.value):
+            return True
+        elif (self.operator == self.EQUAL) and (max_length == self.value):
+            return True
+        else:
+            return False
+
+
+class PaymentMethodCriterion(Criterion):
+    """
+    Criterion to check against payment methods.
+    """
+    value = models.ManyToManyField(PaymentMethod, verbose_name=_(u"Payment methods"))
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.SELECTION_OPERATORS + self.VALID_OPERATORS
+
+    def get_selectable_values(self, request):
+        selected_payment_methods = self.value.all()
+        payment_methods = []
+        for pm in PaymentMethod.objects.filter(active=True):
+            if pm in selected_payment_methods:
+                selected = True
+            else:
+                selected = False
+
+            payment_methods.append({
+                "id": pm.id,
+                "name": pm.name,
+                "selected": selected,
+            })
+
+        return payment_methods
+
+    def get_value_type(self):
+        return self.MULTIPLE_SELECT
+
+    def is_valid(self):
+        # see ShippingMethodCriterion for what's going on here
+        import lfs.shipping.utils
+        if isinstance(self.content, PaymentMethod):
+            is_payment_method = True
+        else:
+            is_payment_method = False
+
+        if (not is_payment_method) and (self.operator == self.IS_SELECTED):
+            payment_method = lfs.payment.utils.get_selected_payment_method(self.request)
+            return payment_method in self.value.all()
+        elif (not is_payment_method) and (self.operator == self.IS_NOT_SELECTED):
+            payment_method = lfs.payment.utils.get_selected_payment_method(self.request)
+            return payment_method not in self.value.all()
+        elif self.operator == self.IS_VALID:
+            for pm in self.value.all():
+                if not lfs.criteria.utils.is_valid(self.request, pm, self.product):
+                    return False
+            return True
+        elif self.operator == self.IS_NOT_VALID:
+            for pm in self.value.all():
+                if lfs.criteria.utils.is_valid(self.request, pm, self.product):
+                    return False
+            return True
+        else:
+            return False
+
+
+class ShippingMethodCriterion(Criterion):
+    """
+    Criterion to check against shipping methods.
+    """
+    value = models.ManyToManyField(ShippingMethod, verbose_name=_(u"Shipping methods"))
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.SELECTION_OPERATORS + self.VALID_OPERATORS
+
+    def get_selectable_values(self, request):
+        selected_shipping_methods = self.value.all()
+        shipping_methods = []
+        for sm in ShippingMethod.objects.filter(active=True):
+            if sm in selected_shipping_methods:
+                selected = True
+            else:
+                selected = False
+
+            shipping_methods.append({
+                "id": sm.id,
+                "name": sm.name,
+                "selected": selected,
+            })
+
+        return shipping_methods
+
+    def get_value_type(self):
+        return self.MULTIPLE_SELECT
+
+    def is_valid(self):
+        # Check whether the criteria is used of a shipping method. If so the
+        # operator IS_SELECTED and IS_NOT_SELECTED are not allowed. The reason
+        # why we have to check this is that the get_selected_shipping_method
+        # checks for valid shipping methods and call this method again, so that
+        # we get an infinte recursion.
+
+        import lfs.shipping.utils
+        if isinstance(self.content, ShippingMethod):
+            is_shipping_method = True
+        else:
+            is_shipping_method = False
+
+        if (not is_shipping_method) and (self.operator == self.IS_SELECTED):
+            shipping_method = lfs.shipping.utils.get_selected_shipping_method(self.request)
+            return shipping_method in self.value.all()
+        elif (not is_shipping_method) and (self.operator == self.IS_NOT_SELECTED):
+            shipping_method = lfs.shipping.utils.get_selected_shipping_method(self.request)
+            return shipping_method not in self.value.all()
+        elif self.operator == self.IS_VALID:
+            for sm in self.value.all():
+                if not lfs.criteria.utils.is_valid(self.request, sm, self.product):
+                    return False
+            return True
+        elif self.operator == self.IS_NOT_VALID:
+            for sm in self.value.all():
+                if lfs.criteria.utils.is_valid(self.request, sm, self.product):
+                    return False
+            return True
+        else:
+            return False
+
+
+class WeightCriterion(Criterion):
+    """
+    Criterion to check against product's weight / cart's total weight.
+    """
+    value = models.FloatField(_(u"Weight"), default=0.0)
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.NUMBER_OPERATORS
+
+    def is_valid(self):
+        """
+        If product is given the weigth is taken from the product, otherwise it
+        is the total weight of all products within the cart.
+        """
+        if self.product:
+            weight = self.product.get_weight()
+        elif self.cart:
+            weight = sum([item.product.get_weight() * item.amount for item in cart.get_items()])
+        else:
+            weight = 0
+
+        if (self.operator == self.LESS_THAN) and (weight < self.value):
+            return True
+        elif (self.operator == self.LESS_THAN_EQUAL) and (weight <= self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN) and (weight > self.value):
+            return True
+        elif (self.operator == self.GREATER_THAN_EQUAL) and (weight >= self.value):
+            return True
+        elif (self.operator == self.EQUAL) and (weight == self.value):
+            return True
+        else:
+            return False
+
+
+class WidthCriterion(Criterion):
+    """
+    Criterion to check against product's width / cart's max width.
+    """
+    value = models.FloatField(_(u"Width"), default=0.0)
+
+    def get_operators(self):
+        """
+        Returns the available operators for the criterion.
+        """
+        return self.NUMBER_OPERATORS
+
+    def is_valid(self):
+        """
+        If product is given the width is taken from the product, otherwise it
+        is the max width of all products within the cart.
+        """
+        if self.product:
+            max_width = self.product.get_width()
+        elif self.cart:
+            max_width = max([item.product.get_width() for item in self.cart.get_items()])
+        else:
+            max_width = 0
+
+        if self.operator == self.LESS_THAN and (max_width < self.width):
+            return True
+        if self.operator == self.LESS_THAN_EQUAL and (max_width <= self.width):
+            return True
+        if self.operator == self.GREATER_THAN and (max_width > self.width):
+            return True
+        if self.operator == self.GREATER_THAN_EQUAL and (max_width >= self.width):
+            return True
+        if self.operator == self.EQUAL and (max_width == self.width):
+            return True
+
+        return False

lfs/criteria/models/__init__.py

-# Separated criteria and criteria_objects to prevent cyclic import:
-# PaymentMethod -> CriteriaObjects | PaymentCriteria -> PaymentMethod
-from criteria import *
-from criteria_objects import *

lfs/criteria/models/criteria.py

-# django imports
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.contrib.contenttypes import generic
-from django.db import models
-from django.utils.translation import ugettext_lazy as _, ugettext
-from django.template import RequestContext
-from django.template.loader import render_to_string
-
-# lfs imports
-import lfs.core.utils
-from lfs import shipping
-from lfs.caching.utils import lfs_get_object_or_404
-from lfs.core.models import Shop, Country
-from lfs.criteria.models.criteria_objects import CriteriaObjects
-from lfs.criteria.settings import EQUAL
-from lfs.criteria.settings import LESS_THAN
-from lfs.criteria.settings import LESS_THAN_EQUAL
-from lfs.criteria.settings import GREATER_THAN
-from lfs.criteria.settings import GREATER_THAN_EQUAL
-from lfs.criteria.settings import NUMBER_OPERATORS
-from lfs.criteria.settings import SELECT_OPERATORS
-from lfs.criteria.settings import IS, IS_NOT, IS_VALID, IS_NOT_VALID
-from lfs.payment.models import PaymentMethod
-from lfs.shipping.models import ShippingMethod
-
-
-class Criterion(object):
-    """Base class for all lfs criteria.
-    """
-    class Meta:
-        app_label = "criteria"
-
-    def as_html(self, request, position):
-        """Renders the criterion as html in order to displayed it within several
-        forms.
-        """
-        template = "manage/criteria/%s_criterion.html" % self.content_type
-
-        return render_to_string(template, RequestContext(request, {
-            "id": "%s%s" % (self.content_type, self.id),
-            "operator": self.operator,
-            "value": self.value,
-            "position": position,
-        }))
-
-
-class CartPriceCriterion(models.Model, Criterion):
-    """A criterion for the cart price.
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=NUMBER_OPERATORS)
-    price = models.FloatField(_(u"Price"), default=0.0)
-
-    def __unicode__(self):
-        return ugettext("Cart Price: %(operator)s %(price)s") % {'operator': self.get_operator_display(), 'price': self.price}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"price"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Cart Price")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-
-        If product is given the price is taken from the product otherwise from
-        the cart.
-        """
-        if product is not None:
-            cart_price = product.get_price(request)
-        else:
-            from lfs.cart import utils as cart_utils
-            cart = cart_utils.get_cart(request)
-
-            if cart is None:
-                return False
-
-            cart_price = cart.get_price_gross(request)
-
-        if self.operator == LESS_THAN and (cart_price < self.price):
-            return True
-        if self.operator == LESS_THAN_EQUAL and (cart_price <= self.price):
-            return True
-        if self.operator == GREATER_THAN and (cart_price > self.price):
-            return True
-        if self.operator == GREATER_THAN_EQUAL and (cart_price >= self.price):
-            return True
-        if self.operator == EQUAL and (cart_price == self.price):
-            return True
-
-        return False
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.price
-
-
-class CombinedLengthAndGirthCriterion(models.Model, Criterion):
-    """A criterion for the combined length and girth.
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=NUMBER_OPERATORS)
-    clag = models.FloatField(_(u"Width"), default=0.0)
-
-    def __unicode__(self):
-        return ugettext("CLAG: %(operator)s %(clag)s") % {'operator': self.get_operator_display(),
-                                                          'clag': self.clag}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"combinedlengthandgirth"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Combined length and girth")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-
-        If product is given the combined length and girth is taken from the
-        product otherwise from the cart.
-        """
-        if product is not None:
-            clag = (2 * product.get_width()) + (2 * product.get_height()) + product.get_length()
-        else:
-            from lfs.cart import utils as cart_utils
-            cart = cart_utils.get_cart(request)
-            if cart is None:
-                return False
-
-            cart_clag = 0
-            max_width = 0
-            max_length = 0
-            total_height = 0
-            for item in cart.get_items():
-                if max_length < item.product.get_length():
-                    max_length = item.product.get_length()
-
-                if max_width < item.product.get_width():
-                    max_width = item.product.get_width()
-
-                total_height += item.product.get_height()
-
-            clag = (2 * max_width) + (2 * total_height) + max_length
-
-        if self.operator == LESS_THAN and (clag < self.clag):
-            return True
-        if self.operator == LESS_THAN_EQUAL and (clag <= self.clag):
-            return True
-        if self.operator == GREATER_THAN and (clag > self.clag):
-            return True
-        if self.operator == GREATER_THAN_EQUAL and (clag >= self.clag):
-            return True
-        if self.operator == EQUAL and (clag == self.clag):
-            return True
-
-        return False
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.clag
-
-
-class CountryCriterion(models.Model, Criterion):
-    """A criterion for the shipping country.
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=SELECT_OPERATORS)
-    countries = models.ManyToManyField(Country, verbose_name=_(u"Countries"))
-
-    def __unicode__(self):
-        values = []
-        for value in self.value.all():
-            values.append(value.name)
-
-        return ugettext("Country: %(operator)s %(countries)s") % {'operator': self.get_operator_display(),
-                                                                 'countries': ", ".join(values)}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"country"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Country")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-        """
-        country = shipping.utils.get_selected_shipping_country(request)
-        if self.operator == IS:
-            return country in self.countries.all()
-        else:
-            return country not in self.countries.all()
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.countries
-
-    def as_html(self, request, position):
-        """Renders the criterion as html in order to be displayed within several
-        forms.
-        """
-        shop = lfs_get_object_or_404(Shop, pk=1)
-
-        countries = []
-        for country in shop.shipping_countries.all():
-            if country in self.countries.all():
-                selected = True
-            else:
-                selected = False
-
-            countries.append({
-                "id": country.id,
-                "name": country.name,
-                "selected": selected,
-            })
-
-        return render_to_string("manage/criteria/country_criterion.html", RequestContext(request, {
-            "id": "%s%s" % (self.content_type, self.id),
-            "operator": self.operator,
-            "value": self.value,
-            "position": position,
-            "countries": countries,
-        }))
-
-
-class HeightCriterion(models.Model, Criterion):
-    """
-    """
-    operator = models.PositiveIntegerField(blank=True, null=True, choices=NUMBER_OPERATORS)
-    height = models.FloatField(_(u"Height"), default=0.0)
-
-    def __unicode__(self):
-        return ugettext("Height: %(operator)s %(height)s") % {'operator': self.get_operator_display(),
-                                                              'height': self.height}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"height"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Height")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-
-        If product is given the height is taken from the product otherwise from
-        the cart.
-        """
-        if product is not None:
-            cart_height = product.get_height()
-        else:
-            from lfs.cart import utils as cart_utils
-            cart = cart_utils.get_cart(request)
-            if cart is None:
-                return False
-
-            cart_height = 0
-            for item in cart.get_items():
-                cart_height += (item.product.get_height() * item.amount)
-
-        if self.operator == LESS_THAN and (cart_height < self.height):
-            return True
-        if self.operator == LESS_THAN_EQUAL and (cart_height <= self.height):
-            return True
-        if self.operator == GREATER_THAN and (cart_height > self.height):
-            return True
-        if self.operator == GREATER_THAN_EQUAL and (cart_height >= self.height):
-            return True
-        if self.operator == EQUAL and (cart_height == self.height):
-            return True
-
-        return False
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.height
-
-
-class LengthCriterion(models.Model, Criterion):
-    """A criterion for the length.
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=NUMBER_OPERATORS)
-    length = models.FloatField(_(u"Length"), default=0.0)
-
-    def __unicode__(self):
-        return ugettext("Length: %(operator)s %(length)s") % {'operator': self.get_operator_display(),
-                                                              'length': self.length}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"length"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Length")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-
-        If product is given the length is taken from the product otherwise from
-        the cart.
-        """
-        if product is not None:
-            max_length = product.get_length()
-        else:
-            from lfs.cart import utils as cart_utils
-            cart = cart_utils.get_cart(request)
-            if cart is None:
-                return False
-
-            max_length = 0
-            for item in cart.get_items():
-                if max_length < item.product.get_length():
-                    max_length = item.product.get_length()
-
-        if self.operator == LESS_THAN and (max_length < self.length):
-            return True
-        if self.operator == LESS_THAN_EQUAL and (max_length <= self.length):
-            return True
-        if self.operator == GREATER_THAN and (max_length > self.length):
-            return True
-        if self.operator == GREATER_THAN_EQUAL and (max_length >= self.length):
-            return True
-        if self.operator == EQUAL and (max_length == self.length):
-            return True
-
-        return False
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.length
-
-
-class PaymentMethodCriterion(models.Model, Criterion):
-    """A criterion for the payment method.
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=SELECT_OPERATORS)
-    payment_methods = models.ManyToManyField(PaymentMethod, verbose_name=_(u"Payment methods"))
-
-    criteria_objects = generic.GenericRelation(CriteriaObjects,
-        object_id_field="criterion_id", content_type_field="criterion_type")
-
-    def __unicode__(self):
-        values = []
-        for value in self.value.all():
-            values.append(value.name)
-
-        return ugettext("Payment: %(operator)s %(payments)s") % {'operator': self.get_operator_display(),
-                                                                'payments': ", ".join(values)}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"payment_method"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Payment method")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-        """
-        # see ShippingMethodCriterion for what's going on here
-        import lfs.shipping.utils
-        content_object = self.criteria_objects.filter()[0].content
-        if isinstance(content_object, PaymentMethod):
-            is_payment_method = True
-        else:
-            is_payment_method = False
-
-        if not is_payment_method and self.operator == IS:
-            payment_method = lfs.payment.utils.get_selected_payment_method(request)
-            return payment_method in self.payment_methods.all()
-        elif not is_payment_method and self.operator == IS_NOT:
-            payment_method = lfs.payment.utils.get_selected_payment_method(request)
-            return payment_method not in self.payment_methods.all()
-        elif self.operator == IS_VALID:
-            for pm in self.payment_methods.all():
-                if not lfs.criteria.utils.is_valid(request, pm, product):
-                    return False
-            return True
-        elif self.operator == IS_NOT_VALID:
-            for pm in self.payment_methods.all():
-                if lfs.criteria.utils.is_valid(request, pm, product):
-                    return False
-            return True
-        else:
-            return False
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.payment_methods
-
-    def as_html(self, request, position):
-        """Renders the criterion as html in order to be displayed within several
-        forms.
-        """
-        selected_payment_methods = self.payment_methods.all()
-        payment_methods = []
-        for pm in PaymentMethod.objects.filter(active=True):
-            if pm in selected_payment_methods:
-                selected = True
-            else:
-                selected = False
-
-            payment_methods.append({
-                "id": pm.id,
-                "name": pm.name,
-                "selected": selected,
-            })
-
-        return render_to_string("manage/criteria/payment_method_criterion.html", RequestContext(request, {
-            "id": "%s%s" % (self.content_type, self.id),
-            "operator": self.operator,
-            "value": self.value,
-            "position": position,
-            "payment_methods": payment_methods,
-        }))
-
-
-class ShippingMethodCriterion(models.Model, Criterion):
-    """A criterion for the shipping method.
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=SELECT_OPERATORS)
-    shipping_methods = models.ManyToManyField(ShippingMethod, verbose_name=_(u"Shipping methods"))
-
-    criteria_objects = generic.GenericRelation(CriteriaObjects,
-        object_id_field="criterion_id", content_type_field="criterion_type")
-
-    def __unicode__(self):
-        values = []
-        for value in self.value.all():
-            values.append(value.name)
-
-        return ugettext("Shipping: %(operator)s %(shipping)s") % {'operator': self.get_operator_display(),
-                                                                 'shipping': ", ".join(values)}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"shipping_method"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Shipping method")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-        """
-        # Check whether the criteria is part of a shipping method if so the
-        # operator IS and IS_NOT are not allowed. This will later exluded by the
-        # UID.
-
-        # The reason why we have to check this is that the get_selected_shipping_method
-        # checks for valid shipping methods and call this method again, so that
-        # we get an infinte recursion.
-
-        import lfs.shipping.utils
-        content_object = self.criteria_objects.filter()[0].content
-        if isinstance(content_object, ShippingMethod):
-            is_shipping_method = True
-        else:
-            is_shipping_method = False
-
-        if not is_shipping_method and self.operator == IS:
-            shipping_method = lfs.shipping.utils.get_selected_shipping_method(request)
-            return shipping_method in self.shipping_methods.all()
-        elif not is_shipping_method and self.operator == IS_NOT:
-            shipping_method = lfs.shipping.utils.get_selected_shipping_method(request)
-            return shipping_method not in self.shipping_methods.all()
-        elif self.operator == IS_VALID:
-            for sm in self.shipping_methods.all():
-                if not lfs.criteria.utils.is_valid(request, sm, product):
-                    return False
-            return True
-        elif self.operator == IS_NOT_VALID:
-            for sm in self.shipping_methods.all():
-                if lfs.criteria.utils.is_valid(request, sm, product):
-                    return False
-            return True
-        else:
-            return False
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.shipping_methods
-
-    def as_html(self, request, position):
-        """Renders the criterion as html in order to be displayed within several
-        forms.
-        """
-        selected_shipping_methods = self.shipping_methods.all()
-        shipping_methods = []
-        for sm in ShippingMethod.objects.filter(active=True):
-            if sm in selected_shipping_methods:
-                selected = True
-            else:
-                selected = False
-
-            shipping_methods.append({
-                "id": sm.id,
-                "name": sm.name,
-                "selected": selected,
-            })
-
-        return render_to_string("manage/criteria/shipping_method_criterion.html", RequestContext(request, {
-            "id": "%s%s" % (self.content_type, self.id),
-            "operator": self.operator,
-            "value": self.value,
-            "position": position,
-            "shipping_methods": shipping_methods,
-        }))
-
-
-class UserCriterion(models.Model, Criterion):
-    """A criterion for user content objects
-    """
-    users = models.ManyToManyField(User)
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"user"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"User")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-        """
-        return request.user in self.users.all()
-
-    @property
-    def value(self):
-        """Returns the value of the criterion.
-        """
-        return self.users
-
-
-class WeightCriterion(models.Model, Criterion):
-    """
-    """
-    operator = models.PositiveIntegerField(_(u"Operator"), blank=True, null=True, choices=NUMBER_OPERATORS)
-    weight = models.FloatField(_(u"Weight"), default=0.0)
-
-    def __unicode__(self):
-        return ugettext("Weight: %(operator)s %(weight)s") % {'operator': self.get_operator_display(),
-                                                              'weight': self.weight}
-
-    @property
-    def content_type(self):
-        """Returns the content_type of the criterion as lower string.
-
-        This is for instance used to select the appropriate form for the
-        criterion.
-        """
-        return u"weight"
-
-    @property
-    def name(self):
-        """Returns the descriptive name of the criterion.
-        """
-        return ugettext(u"Weight")
-
-    def is_valid(self, request, product=None):
-        """Returns True if the criterion is valid.
-
-        If product is given the weight is taken from the product otherwise from
-        the cart.
-        """
-        if product is not None:
-            cart_weight = product.get_weight()
-        else:
-            from lfs.cart import utils as cart_utils
-            cart = cart_utils.get_cart(request)
-
-            if cart is None:
-                return False
-