diff --git a/cartridge/shop/defaults.py b/cartridge/shop/defaults.py index 31e20ef2c..43661c63e 100644 --- a/cartridge/shop/defaults.py +++ b/cartridge/shop/defaults.py @@ -293,3 +293,15 @@ editable=False, default=True, ) + +register_setting( + name="SHOP_WISHLIST_NOTIFICATIONS", + label=_( + "Sends email notifications if an item in user's wishlist is in stock" + ), + description=_( + "Sends email notifications if an item in user's wishlist is in stock" + ), + editable=True, + default=True, +) diff --git a/cartridge/shop/managers.py b/cartridge/shop/managers.py index 0433aca98..38d074066 100644 --- a/cartridge/shop/managers.py +++ b/cartridge/shop/managers.py @@ -202,3 +202,36 @@ def get_valid(self, code, cart): if products.filter(variations__sku__in=cart.skus()).count() == 0: raise self.model.DoesNotExist return discount + + +class WishlistManager(Manager): + + def from_request(self, request): + """ + Gets current user's wishlist. Authenticated users' wishlists are stored + in the database, while unauthenticated users' are stored in a cookie. + Note that this method returns a `list` of SKUs in both cases, not a + `QuerySet` of `ProductVariation` objects. + """ + if request.user.is_authenticated(): + wishlist = [] + skus = self.filter(user=request.user).values_list("sku", flat=True) + for sku in skus: + wishlist.append(sku) + else: + wishlist = request.COOKIES.get("wishlist", "").split(",") + if not wishlist[0]: + wishlist = [] + return wishlist + + def delete_for_request(self, sku_to_delete, request): + """ + Delete item from user's wishlist. Authenticated users' wishlists are + stored in the database, while unauthenticated users' are stored in a + cookie. In the latter case, the SKU is removed directly from + `request.wishlist`, which is provided by `ShopMiddleware`. + """ + if request.user.is_authenticated(): + self.get(user=request.user, sku=sku_to_delete).delete() + else: + request.wishlist.remove(sku_to_delete) diff --git a/cartridge/shop/middleware.py b/cartridge/shop/middleware.py index 4e595bd9a..85567906e 100644 --- a/cartridge/shop/middleware.py +++ b/cartridge/shop/middleware.py @@ -1,7 +1,7 @@ from mezzanine.conf import settings -from cartridge.shop.models import Cart +from cartridge.shop.models import Cart, Wishlist class SSLRedirect(object): @@ -29,7 +29,4 @@ class ShopMiddleware(SSLRedirect): """ def process_request(self, request): request.cart = Cart.objects.from_request(request) - wishlist = request.COOKIES.get("wishlist", "").split(",") - if not wishlist[0]: - wishlist = [] - request.wishlist = wishlist + request.wishlist = Wishlist.objects.from_request(request) diff --git a/cartridge/shop/migrations/0021_auto__add_wishlist__add_unique_wishlist_user_sku.py b/cartridge/shop/migrations/0021_auto__add_wishlist__add_unique_wishlist_user_sku.py new file mode 100644 index 000000000..12576525c --- /dev/null +++ b/cartridge/shop/migrations/0021_auto__add_wishlist__add_unique_wishlist_user_sku.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Wishlist' + db.create_table('shop_wishlist', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('sku', self.gf('cartridge.shop.fields.SKUField')(max_length=20)), + )) + db.send_create_signal('shop', ['Wishlist']) + + # Adding unique constraint on 'Wishlist', fields ['user', 'sku'] + db.create_unique('shop_wishlist', ['user_id', 'sku']) + + + def backwards(self, orm): + # Removing unique constraint on 'Wishlist', fields ['user', 'sku'] + db.delete_unique('shop_wishlist', ['user_id', 'sku']) + + # Deleting model 'Wishlist' + db.delete_table('shop_wishlist') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'generic.assignedkeyword': { + 'Meta': {'ordering': "('_order',)", 'object_name': 'AssignedKeyword'}, + '_order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'keyword': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'assignments'", 'to': "orm['generic.Keyword']"}), + 'object_pk': ('django.db.models.fields.IntegerField', [], {}) + }, + 'generic.keyword': { + 'Meta': {'object_name': 'Keyword'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '2000', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '500'}) + }, + 'generic.rating': { + 'Meta': {'object_name': 'Rating'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_pk': ('django.db.models.fields.IntegerField', [], {}), + 'value': ('django.db.models.fields.IntegerField', [], {}) + }, + 'pages.page': { + 'Meta': {'ordering': "('titles',)", 'object_name': 'Page'}, + '_meta_title': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + '_order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'content_model': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'expiry_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'gen_description': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'in_menus': ('mezzanine.pages.fields.MenusField', [], {'default': '[1, 2, 3]', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'keywords': ('mezzanine.generic.fields.KeywordsField', [], {'object_id_field': "'object_pk'", 'to': "orm['generic.AssignedKeyword']", 'frozen_by_south': 'True'}), + 'keywords_string': ('django.db.models.fields.CharField', [], {'max_length': '500', 'blank': 'True'}), + 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['pages.Page']"}), + 'publish_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'short_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '2000', 'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '2'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'titles': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True'}) + }, + 'shop.cart': { + 'Meta': {'object_name': 'Cart'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}) + }, + 'shop.cartitem': { + 'Meta': {'object_name': 'CartItem'}, + 'cart': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['shop.Cart']"}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'quantity': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'sku': ('cartridge.shop.fields.SKUField', [], {'max_length': '20'}), + 'total_price': ('cartridge.shop.fields.MoneyField', [], {'default': "'0'", 'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'unit_price': ('cartridge.shop.fields.MoneyField', [], {'default': "'0'", 'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'shop.category': { + 'Meta': {'ordering': "('_order',)", 'object_name': 'Category', '_ormbases': ['pages.Page']}, + 'combined': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'content': ('mezzanine.core.fields.RichTextField', [], {}), + 'options': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'product_options'", 'blank': 'True', 'to': "orm['shop.ProductOption']"}), + 'page_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['pages.Page']", 'unique': 'True', 'primary_key': 'True'}), + 'price_max': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'price_min': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['shop.Product']", 'symmetrical': 'False', 'blank': 'True'}), + 'sale': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.Sale']", 'null': 'True', 'blank': 'True'}) + }, + 'shop.discountcode': { + 'Meta': {'object_name': 'DiscountCode'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'discountcode_related'", 'blank': 'True', 'to': "orm['shop.Category']"}), + 'code': ('cartridge.shop.fields.DiscountCodeField', [], {'unique': 'True', 'max_length': '20'}), + 'discount_deduct': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'discount_exact': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'discount_percent': ('cartridge.shop.fields.PercentageField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '2', 'blank': 'True'}), + 'free_shipping': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'min_purchase': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['shop.Product']", 'symmetrical': 'False', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'uses_remaining': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'valid_from': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'valid_to': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'shop.order': { + 'Meta': {'ordering': "('-id',)", 'object_name': 'Order'}, + 'additional_instructions': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'billing_detail_city': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'billing_detail_country': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'billing_detail_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'billing_detail_first_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'billing_detail_last_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'billing_detail_phone': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'billing_detail_postcode': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'billing_detail_state': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'billing_detail_street': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'discount_code': ('cartridge.shop.fields.DiscountCodeField', [], {'max_length': '20', 'blank': 'True'}), + 'discount_total': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item_total': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'shipping_detail_city': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'shipping_detail_country': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'shipping_detail_first_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'shipping_detail_last_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'shipping_detail_phone': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'shipping_detail_postcode': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'shipping_detail_state': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'shipping_detail_street': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'shipping_total': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'shipping_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'total': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'transaction_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'user_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'shop.orderitem': { + 'Meta': {'object_name': 'OrderItem'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['shop.Order']"}), + 'quantity': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'sku': ('cartridge.shop.fields.SKUField', [], {'max_length': '20'}), + 'total_price': ('cartridge.shop.fields.MoneyField', [], {'default': "'0'", 'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'unit_price': ('cartridge.shop.fields.MoneyField', [], {'default': "'0'", 'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}) + }, + 'shop.product': { + 'Meta': {'object_name': 'Product'}, + '_meta_title': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True', 'blank': 'True'}), + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['shop.Category']", 'symmetrical': 'False', 'blank': 'True'}), + 'content': ('mezzanine.core.fields.RichTextField', [], {}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'expiry_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'gen_description': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'keywords': ('mezzanine.generic.fields.KeywordsField', [], {'object_id_field': "'object_pk'", 'to': "orm['generic.AssignedKeyword']", 'frozen_by_south': 'True'}), + 'keywords_string': ('django.db.models.fields.CharField', [], {'max_length': '500', 'blank': 'True'}), + 'num_in_stock': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'publish_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'rating': ('mezzanine.generic.fields.RatingField', [], {'object_id_field': "'object_pk'", 'to': "orm['generic.Rating']", 'frozen_by_south': 'True'}), + 'rating_average': ('django.db.models.fields.FloatField', [], {'default': '0'}), + 'rating_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'related_products_rel_+'", 'blank': 'True', 'to': "orm['shop.Product']"}), + 'sale_from': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'sale_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'sale_price': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'sale_to': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'short_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), + 'sku': ('cartridge.shop.fields.SKUField', [], {'max_length': '20', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '2000', 'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '2'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'unit_price': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'upsell_products': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'upsell_products_rel_+'", 'blank': 'True', 'to': "orm['shop.Product']"}) + }, + 'shop.productaction': { + 'Meta': {'unique_together': "(('product', 'timestamp'),)", 'object_name': 'ProductAction'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'actions'", 'to': "orm['shop.Product']"}), + 'timestamp': ('django.db.models.fields.IntegerField', [], {}), + 'total_cart': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'total_purchase': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'shop.productimage': { + 'Meta': {'ordering': "('_order',)", 'object_name': 'ProductImage'}, + '_order': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'images'", 'to': "orm['shop.Product']"}) + }, + 'shop.productoption': { + 'Meta': {'object_name': 'ProductOption'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('cartridge.shop.fields.OptionField', [], {'max_length': '50', 'null': 'True'}), + 'type': ('django.db.models.fields.IntegerField', [], {}) + }, + 'shop.productvariation': { + 'Meta': {'ordering': "('-default',)", 'object_name': 'ProductVariation'}, + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.ProductImage']", 'null': 'True', 'blank': 'True'}), + 'num_in_stock': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'option1': ('cartridge.shop.fields.OptionField', [], {'max_length': '50', 'null': 'True'}), + 'option2': ('cartridge.shop.fields.OptionField', [], {'max_length': '50', 'null': 'True'}), + 'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variations'", 'to': "orm['shop.Product']"}), + 'sale_from': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'sale_id': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'sale_price': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'sale_to': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'sku': ('cartridge.shop.fields.SKUField', [], {'max_length': '20', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'unit_price': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}) + }, + 'shop.sale': { + 'Meta': {'object_name': 'Sale'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'sale_related'", 'blank': 'True', 'to': "orm['shop.Category']"}), + 'discount_deduct': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'discount_exact': ('cartridge.shop.fields.MoneyField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '2', 'blank': 'True'}), + 'discount_percent': ('cartridge.shop.fields.PercentageField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '2', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['shop.Product']", 'symmetrical': 'False', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'valid_from': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'valid_to': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'shop.wishlist': { + 'Meta': {'unique_together': "(('user', 'sku'),)", 'object_name': 'Wishlist'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sku': ('cartridge.shop.fields.SKUField', [], {'max_length': '20'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['shop'] \ No newline at end of file diff --git a/cartridge/shop/models.py b/cartridge/shop/models.py index e9e877821..abe9e8816 100644 --- a/cartridge/shop/models.py +++ b/cartridge/shop/models.py @@ -1,9 +1,11 @@ from decimal import Decimal from operator import iand, ior +from django.contrib.auth.models import User +from django.contrib.auth.signals import user_logged_in from django.core.urlresolvers import reverse from django.db import models -from django.db.models.signals import m2m_changed +from django.db.models.signals import m2m_changed, pre_save from django.db.models import CharField, F, Q from django.db.models.base import ModelBase from django.db.utils import DatabaseError @@ -18,6 +20,7 @@ from mezzanine.generic.fields import RatingField from mezzanine.pages.models import Page from mezzanine.utils.models import AdminThumbMixin, upload_to +from mezzanine.utils.email import send_mail_template from cartridge.shop import fields, managers @@ -305,6 +308,30 @@ def update_stock(self, quantity): self.product.save() +@receiver(pre_save, sender=ProductVariation, + dispatch_uid="wishlist_notifications_on_save") +def wishlist_notifications_on_save(sender, instance, **kwargs): + if settings.SHOP_WISHLIST_NOTIFICATIONS is not True: + return + if instance.id: + productvariation = ProductVariation.objects.get(pk=instance.id) + old_stock = productvariation.num_in_stock + if (old_stock == 0) and (instance.num_in_stock > old_stock): + # Send email + email_from = settings.DEFAULT_FROM_EMAIL + wishlist = Wishlist.objects.filter(sku=productvariation.sku) + for item in wishlist: + email_to = item.user.email + subject = _("Notification from wishlist") + context = { + "productvariation": productvariation, + "user": item.user, + } + send_mail_template(subject, "email/wishlist_notification", + email_from, email_to, context, + fail_silently=settings.DEBUG) + + class Category(Page, RichText): """ A category of products on the website. @@ -843,3 +870,31 @@ def calculate(self, amount): class Meta: verbose_name = _("Discount code") verbose_name_plural = _("Discount codes") + + +class Wishlist(models.Model): + + user = models.ForeignKey(User) + sku = fields.SKUField() + + objects = managers.WishlistManager() + + class Meta: + unique_together = ('user', 'sku') + verbose_name = _("Wishlist item") + verbose_name_plural = _("Wishlist items") + + +@receiver(user_logged_in, sender=User) +def transfer_wishlist_data(sender, request, user, *args, **kwargs): + skus = getattr(request, "wishlist", []) + if not skus: + return + existed = (Wishlist.objects.filter(user=user, sku__in=skus) + .values_list("sku", flat=True)) + for sku in skus: + if sku not in existed: + wishlist = Wishlist() + wishlist.user = user + wishlist.sku = sku + wishlist.save() diff --git a/cartridge/shop/templates/email/wishlist_notification.txt b/cartridge/shop/templates/email/wishlist_notification.txt new file mode 100644 index 000000000..8e7868d5d --- /dev/null +++ b/cartridge/shop/templates/email/wishlist_notification.txt @@ -0,0 +1,11 @@ +{% extends "email/base.txt" %} +{% load shop_tags i18n %} + +{% block main %} +{% trans "Dear" %} {{ user }}, + +{% trans "This product is available for order" %}: + +{{ productvariation }} + +{% endblock %} diff --git a/cartridge/shop/templates/shop/wishlist.html b/cartridge/shop/templates/shop/wishlist.html index 3c4dc97f5..6ce72816b 100644 --- a/cartridge/shop/templates/shop/wishlist.html +++ b/cartridge/shop/templates/shop/wishlist.html @@ -11,7 +11,7 @@ {% block main %} {% if error %}{{ error }}{% endif %} -{% if request.wishlist %} +{% if wishlist_items %} {% for item in wishlist_items %} diff --git a/cartridge/shop/utils.py b/cartridge/shop/utils.py index 2c73ae3b4..da4604d36 100644 --- a/cartridge/shop/utils.py +++ b/cartridge/shop/utils.py @@ -48,6 +48,20 @@ def add_item(self, *args, **kwargs): self._request.session["cart"] = cart.id +class CookieBackedWishlist(object): + """ + A dummy wishlist object used for unauthenticated users, backed by cookie + storage. + """ + def __init__(self, request): + super(CookieBackedWishlist, self).__init__() + self.request = request + + def save(self, *args, **kwargs): + if self.sku not in self.request.wishlist: + self.request.wishlist.append(self.sku) + + def make_choices(choices): """ Zips a list with itself for field choices. @@ -113,3 +127,12 @@ def set_locale(): "configure the SHOP_CURRENCY_LOCALE setting in your settings " "module.") raise ImproperlyConfigured(msg % currency_locale) + + +def get_wishlist(request): + if request.user.is_authenticated(): + from cartridge.shop.models import Wishlist + wishlist = Wishlist() + else: + wishlist = CookieBackedWishlist(request) + return wishlist diff --git a/cartridge/shop/views.py b/cartridge/shop/views.py index 7926893fb..21886bf46 100644 --- a/cartridge/shop/views.py +++ b/cartridge/shop/views.py @@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.messages import info from django.core.urlresolvers import get_callable, reverse +from django.db import IntegrityError from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.template import RequestContext @@ -19,8 +20,8 @@ from cartridge.shop import checkout from cartridge.shop.forms import AddProductForm, DiscountForm, CartItemFormSet from cartridge.shop.models import Product, ProductVariation, Order, OrderItem -from cartridge.shop.models import DiscountCode -from cartridge.shop.utils import recalculate_discount, sign +from cartridge.shop.models import DiscountCode, Wishlist +from cartridge.shop.utils import recalculate_discount, sign, get_wishlist # Set up checkout handlers. @@ -60,13 +61,16 @@ def product(request, slug, template="shop/product.html"): info(request, _("Item added to cart")) return redirect("shop_cart") else: - skus = request.wishlist - sku = add_product_form.variation.sku - if sku not in skus: - skus.append(sku) - info(request, _("Item added to wishlist")) response = redirect("shop_wishlist") - set_cookie(response, "wishlist", ",".join(skus)) + wishlist = get_wishlist(request) + wishlist.user = request.user + wishlist.sku = add_product_form.variation.sku + try: + wishlist.save() + except IntegrityError: + pass + set_cookie(response, "wishlist", ",".join(request.wishlist)) + info(request, _("Item added to wishlist")) return response context = { "product": product, @@ -76,7 +80,8 @@ def product(request, slug, template="shop/product.html"): "variations_json": variations_json, "has_available_variations": any([v.has_price() for v in variations]), "related_products": product.related_products.published( - for_user=request.user), + for_user=request.user + ), "add_product_form": add_product_form } return render(request, template, context) @@ -110,15 +115,16 @@ def wishlist(request, template="shop/wishlist.html"): message = _("Item removed from wishlist") url = "shop_wishlist" sku = request.POST.get("sku") - if sku in skus: - skus.remove(sku) + if sku in request.wishlist: + Wishlist.objects.delete_for_request(sku, request) if not error: info(request, message) response = redirect(url) - set_cookie(response, "wishlist", ",".join(skus)) + set_cookie(response, "wishlist", ",".join(request.wishlist)) return response # Remove skus from the cookie that no longer exist. + skus = request.wishlist published_products = Product.objects.published(for_user=request.user) f = {"product__in": published_products, "sku__in": skus} wishlist = ProductVariation.objects.filter(**f).select_related(depth=1)