Import and export admin panel added using product_db code.
1. cd ..lib/python2.7/site-packages/cartridge/shop/templates/admin
2. mkdir import_export
3. copy the following attached files to above directory
3.1. base.html
3.2 change_list.html
3.3 change_list_import_export.html
3.4 export.html
3.5 import.html
4. replace ..lib/python2.7/site-packages/cartridge/shop/admin.py with the
attached admin.py file.5. replace ..lib/python2.7/site-packages/cartridge/shop/forms.py with the attached forms.py file. 6. python manage.py runserver 7. go to http://127.0.0.1:8000/admin/shop/product/ 8. Find import and export as part of admin panel 9. Import require parent dir of images to be copied for the product. 10. So place product images in the above directory before performing an import. -- You received this message because you are subscribed to the Google Groups "Mezzanine Users" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. For more options, visit https://groups.google.com/d/optout.
from __future__ import unicode_literals
from future.builtins import super, zip
"""
Admin classes for all the shop models.
Many attributes in here are controlled by the ``SHOP_USE_VARIATIONS``
setting which defaults to True. In this case, variations are managed in
the product change view, and are created given the ``ProductOption``
values selected.
A handful of fields (mostly those defined on the abstract ``Priced``
model) are duplicated across both the ``Product`` and
``ProductVariation`` models, with the latter being the definitive
source, and the former supporting denormalised data that can be
referenced when iterating through products, without having to
query the underlying variations.
When ``SHOP_USE_VARIATIONS`` is set to False, a single variation is
still stored against each product, to keep consistent with the overall
model design. Since from a user perspective there are no variations,
the inlines for variations provide a single inline for managing the
one variation per product, so in the product change view, a single set
of price fields are available via the one variation inline.
Also when ``SHOP_USE_VARIATIONS`` is set to False, the denormalised
price fields on the product model are presented as editable fields in
the product change list - if these form fields are used, the values
are then pushed back onto the one variation for the product.
"""
from copy import deepcopy
import csv
import os
import shutil
import sys
from datetime import datetime
import django
from django.conf.urls import url
from django.contrib import admin
from django.contrib import messages
from django.contrib.admin.templatetags.admin_static import static
from django.core.management.base import BaseCommand
from django.core.management.base import CommandError
from django.core.urlresolvers import reverse
from django.db.models import ImageField
from django.http import HttpResponseRedirect, HttpResponse
from django.utils.translation import ugettext_lazy as _
from django.template.response import TemplateResponse
from mezzanine.conf import settings
from mezzanine.core.admin import (DisplayableAdmin,
TabularDynamicInlineAdmin,
BaseTranslationModelAdmin)
from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.pages.admin import PageAdmin
from cartridge.shop.fields import MoneyField
from cartridge.shop.forms import ProductAdminForm, ProductVariationAdminForm
from cartridge.shop.forms import ProductVariationAdminFormset
from cartridge.shop.forms import DiscountAdminForm, ImageWidget, MoneyWidget
from cartridge.shop.models import Category, Product, ProductImage
from cartridge.shop.models import ProductVariation, ProductOption, Order
from cartridge.shop.models import OrderItem, Sale, DiscountCode
from cartridge.shop.views import HAS_PDF
from .forms import ExportForm, ImportForm
# images get copied from thie directory
#LOCAL_IMAGE_DIR = "/tmp/orig"
# images get copied to this directory under STATIC_ROOT
IMAGE_SUFFIXES = [".jpg", ".png", ".JPG", ".jpeg", ".JPEG", ".tif", ".gif", ".GIF"]
EMPTY_IMAGE_ENTRIES = ["Please add", "N/A", ""]
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M"
# Here we define what column headings are used in the csv.
TITLE = _("Title")
CONTENT = _("Content")
DESCRIPTION = _("Description")
SKU = _("SKU")
IMAGE = _("Image")
CATEGORY = _("Category")
SUB_CATEGORY = _("Sub-Category")
# SIZE = _("Size")
NUM_IN_STOCK = _("Number in Stock")
UNIT_PRICE = _("Unit Price")
SALE_PRICE = _("Sale Price")
SALE_START_DATE = _("Sale Start Date")
SALE_START_TIME = _("Sale Start Time")
SALE_END_DATE = _("Sale End Date")
SALE_END_TIME = _("Sale End Time")
DATETIME_FORMAT = "%s %s" % (DATE_FORMAT, TIME_FORMAT)
SITE_MEDIA_IMAGE_DIR = _("product")
SITE_MEDIA_IMAGE_NDIR = _("media/product")
PRODUCT_IMAGE_DIR = os.path.join(str(settings.STATIC_ROOT), str(SITE_MEDIA_IMAGE_NDIR))
TYPE_CHOICES = dict()
for id, choice in settings.SHOP_OPTION_TYPE_CHOICES:
TYPE_CHOICES[choice] = id
fieldnames = [TITLE, CONTENT, DESCRIPTION, CATEGORY, SUB_CATEGORY,
SKU, IMAGE, NUM_IN_STOCK, UNIT_PRICE,
SALE_PRICE, SALE_START_DATE, SALE_START_TIME, SALE_END_DATE, SALE_END_TIME]
# TODO: Make sure no options conflict with other fieldnames.
fieldnames += TYPE_CHOICES.keys()
# Lists of field names.
option_fields = [f.name for f in ProductVariation.option_fields()]
_flds = lambda s: [f.name for f in Order._meta.fields if f.name.startswith(s)]
billing_fields = _flds("billing_detail")
shipping_fields = _flds("shipping_detail")
################
# CATEGORIES #
################
# Categories fieldsets are extended from Page fieldsets, since
# categories are a Mezzanine Page type.
category_fieldsets = deepcopy(PageAdmin.fieldsets)
category_fieldsets[0][1]["fields"][3:3] = ["content", "products"]
category_fieldsets += ((_("Product filters"), {
"fields": ("sale", ("price_min", "price_max"), "combined"),
"classes": ("collapse-closed",)},),)
if settings.SHOP_CATEGORY_USE_FEATURED_IMAGE:
category_fieldsets[0][1]["fields"].insert(3, "featured_image")
# Options are only used when variations are in use, so only provide
# them as filters for dynamic categories when this is the case.
if settings.SHOP_USE_VARIATIONS:
category_fieldsets[-1][1]["fields"] = (("options",) +
category_fieldsets[-1][1]["fields"])
class CategoryAdmin(PageAdmin):
fieldsets = category_fieldsets
formfield_overrides = {ImageField: {"widget": ImageWidget}}
filter_horizontal = ("options", "products",)
################
# VARIATIONS #
################
# If variations aren't used, the variation inline should always
# provide a single inline for managing the single variation per
# product.
variation_fields = ["sku", "num_in_stock", "unit_price",
"sale_price", "sale_from", "sale_to", "image"]
if settings.SHOP_USE_VARIATIONS:
variation_fields.insert(1, "default")
variations_max_num = None
variations_extra = 0
else:
variations_max_num = 1
variations_extra = 1
class ProductVariationAdmin(admin.TabularInline):
verbose_name_plural = _("Current variations")
model = ProductVariation
fields = variation_fields
max_num = variations_max_num
extra = variations_extra
formfield_overrides = {MoneyField: {"widget": MoneyWidget}}
form = ProductVariationAdminForm
formset = ProductVariationAdminFormset
ordering = ["option%s" % i for i in settings.SHOP_OPTION_ADMIN_ORDER]
class ProductImageAdmin(TabularDynamicInlineAdmin):
model = ProductImage
formfield_overrides = {ImageField: {"widget": ImageWidget}}
##############
# PRODUCTS #
##############
product_fieldsets = deepcopy(DisplayableAdmin.fieldsets)
product_fieldsets[0][1]["fields"].insert(2, "available")
product_fieldsets[0][1]["fields"].extend(["content", "categories"])
product_fieldsets = list(product_fieldsets)
other_product_fields = []
if settings.SHOP_USE_RELATED_PRODUCTS:
other_product_fields.append("related_products")
if settings.SHOP_USE_UPSELL_PRODUCTS:
other_product_fields.append("upsell_products")
if len(other_product_fields) > 0:
product_fieldsets.append((_("Other products"), {
"classes": ("collapse-closed",),
"fields": tuple(other_product_fields)}))
product_list_display = ["admin_thumb", "title", "status", "available",
"admin_link"]
product_list_editable = ["status", "available"]
# If variations are used, set up the product option fields for managing
# variations. If not, expose the denormalised price fields for a product
# in the change list view.
if settings.SHOP_USE_VARIATIONS:
product_fieldsets.insert(1, (_("Create new variations"),
{"classes": ("create-variations",), "fields": option_fields}))
else:
extra_list_fields = ["sku", "unit_price", "sale_price", "num_in_stock"]
product_list_display[4:4] = extra_list_fields
product_list_editable.extend(extra_list_fields)
class ProductAdmin(DisplayableAdmin):
class Media:
js = (static("cartridge/js/admin/product_variations.js"),)
css = {"all": (static("cartridge/css/admin/product.css"),)}
list_display = product_list_display
list_display_links = ("admin_thumb", "title")
list_editable = product_list_editable
list_filter = ("status", "available", "categories")
filter_horizontal = ("categories",) + tuple(other_product_fields)
search_fields = ("title", "content", "categories__title",
"variations__sku")
inlines = (ProductImageAdmin, ProductVariationAdmin)
form = ProductAdminForm
fieldsets = product_fieldsets
#: template for export view
export_template_name = 'admin/import_export/export.html'
#: template for change_list view
change_list_template = 'admin/import_export/change_list_import_export.html'
#: template for import view
import_template_name = 'admin/import_export/import.html'
def get_model_info(self):
# module_name is renamed to model_name in Django 1.8
app_label = self.model._meta.app_label
try:
return (app_label, self.model._meta.model_name,)
except AttributeError:
return (app_label, self.model._meta.module_name,)
def get_urls(self):
info = self.get_model_info()
urls = super(ProductAdmin, self).get_urls()
my_urls = [
url(r'^import/$',
self.admin_site.admin_view(self.import_action),
name='%s_%s_import' % info),
url(r'^export/$',
self.admin_site.admin_view(self.export_action),
name='%s_%s_export' % self.get_model_info()),
]
return my_urls + urls
def _product_from_row(self, row):
product, created = Product.objects.get_or_create(title=row[TITLE])
product.content = row[CONTENT]
product.description = row[DESCRIPTION]
# TODO: set the 2 below from spreadsheet.
product.status = CONTENT_STATUS_PUBLISHED
product.available = True
# TODO: allow arbitrary level/number of categories.
base_cat, created = Category.objects.get_or_create(title=row[CATEGORY])
if row[SUB_CATEGORY]:
sub_cat, created = Category.objects.get_or_create(
title=row[SUB_CATEGORY],
parent=base_cat)
product.categories.add(sub_cat)
shop_cat, created = Category.objects.get_or_create(title="Shop")
product.categories.add(shop_cat)
return product
def _make_image(self, image_str, product, local_img_dir):
if image_str in EMPTY_IMAGE_ENTRIES:
return None
# try adding various image suffixes, if none given in original filename.
root, suffix = os.path.splitext(image_str)
if suffix not in IMAGE_SUFFIXES:
raise CommandError("INCORRECT SUFFIX: %s" % image_str)
image_path = os.path.join(local_img_dir , image_str)
if not os.path.exists(image_path):
raise CommandError("NO FILE %s" % image_path)
shutil.copy(image_path, PRODUCT_IMAGE_DIR)
# shutil.copy(image_path, os.path.join(PRODUCT_IMAGE_DIR, "orig"))
image, created = ProductImage.objects.get_or_create(
file="%s" % (image_str),
description=image_str, # TODO: handle column for this.
product=product)
return image
def _make_date(self, date_str, time_str):
date_string = '%s %s' % (date_str, time_str)
date = datetime.datetime.strptime(date_string, DATETIME_FORMAT)
return date
def import_action(self, request, *args, **kwargs):
'''
Perform a dry_run of the import to make sure the import will not
result in errors. If there where no error, save the user
uploaded file to a local temp file that will be used by
'process_import' for the actual import.
'''
print("############################PROCESS IMPORT ACTION#################")
context = {}
import_formats = ["csv",]
form = ImportForm(import_formats,
request.POST or None,
request.FILES or None)
if request.POST and form.is_valid():
print("############IMPORTING ######################")
input_format = ["csv",]
import_file = form.cleaned_data['import_file']
local_img_dir = form.cleaned_data['imgs_dirname']
print(_("Importing .."))
# More appropriate for testing.
# Product.objects.all().delete()
#reader = csv.DictReader(open(import_file))
reader = csv.DictReader(import_file)
for row in reader:
print(row)
product = self._product_from_row(row)
try:
variation = ProductVariation.objects.create(
# strip whitespace
sku=row[SKU].replace(" ", ""),
product=product,
)
except IntegrityError:
raise CommandError("Product with SKU exists! sku: %s" % row[SKU])
if row[NUM_IN_STOCK]:
variation.num_in_stock = row[NUM_IN_STOCK]
if row[UNIT_PRICE]:
variation.unit_price = row[UNIT_PRICE]
if row[SALE_PRICE]:
variation.sale_price = row[SALE_PRICE]
if row[SALE_START_DATE] and row[SALE_START_TIME]:
variation.sale_from = self._make_date(row[SALE_START_DATE],
row[SALE_START_TIME])
if row[SALE_END_DATE] and row[SALE_END_TIME]:
variation.sale_to = self._make_date(row[SALE_END_DATE],
row[SALE_END_TIME])
for option in TYPE_CHOICES:
if row[option]:
name = "option%s" % TYPE_CHOICES[option]
setattr(variation, name, row[option])
new_option, created = ProductOption.objects.get_or_create(
type=TYPE_CHOICES[option], # TODO: set dynamically
name=row[option])
variation.save()
image = self._make_image(row[IMAGE], product, local_img_dir )
if image:
variation.image = image
product.variations.manage_empty()
product.variations.set_default_images([])
product.copy_default_variation()
product.save()
print("Variations: %s" % ProductVariation.objects.all().count())
print("Products: %s" % Product.objects.all().count())
#context['confirm_form'] = ConfirmImportForm(initial={
# 'import_file_name': import_file.name,
# 'original_file_name': import_file.name,
# 'input_format': form.cleaned_data['input_format'],
#})
print("############################PROCESS IMPORT FINISHED #################")
success_message = u'Import finished'
messages.success(request, success_message)
#tmp_storage.remove()
url = reverse('admin:%s_%s_changelist' % self.get_model_info(),
current_app=self.admin_site.name)
return HttpResponseRedirect(url)
if django.VERSION >= (1, 8, 0):
context.update(self.admin_site.each_context(request))
elif django.VERSION >= (1, 7, 0):
context.update(self.admin_site.each_context())
context['form'] = form
context['opts'] = self.model._meta
#context['fields'] = [f.column_name for f in resource.get_fields()]
request.current_app = self.admin_site.name
return TemplateResponse(request, [self.import_template_name],
context)
def export_action(self, request, *args, **kwargs):
'''
Perform the actual export action (after the user has confirmed he
wishes to import)
'''
print("########################## EXPORT ACTION #######################################")
formats=("csv",)
form = ExportForm(formats, request.POST or None)
if form.is_valid():
print(_("Exporting .."))
#response = HttpResponse(content_type='text/csv')
#response['Content-Disposition'] = 'attachment; filename=hostedCustomerReport.csv'
filehandle = open("/tmp/tmpexport.csv", 'w')
writer = csv.DictWriter(filehandle, fieldnames=fieldnames)
#writer = csv.writer(response)
headers = dict()
for field in fieldnames:
headers[field] = field
writer.writerow(headers)
for pv in ProductVariation.objects.all():
row = dict()
row[TITLE] = pv.product.title
row[CONTENT] = pv.product.content
row[DESCRIPTION] = pv.product.description
row[SKU] = pv.sku
row[IMAGE] = pv.image
# TODO: handle multiple categories, and multiple levels of categories
cat = pv.product.categories.all()[0]
if cat.parent:
row[SUB_CATEGORY] = cat.title
row[CATEGORY] = cat.parent.title
else:
row[CATEGORY] = cat.title
row[SUB_CATEGORY] = ""
for option in TYPE_CHOICES:
row[option] = getattr(pv, "option%s" % TYPE_CHOICES[option])
row[NUM_IN_STOCK] = pv.num_in_stock
row[UNIT_PRICE] = pv.unit_price
row[SALE_PRICE] = pv.sale_price
try:
row[SALE_START_DATE] = pv.sale_from.strftime(DATE_FORMAT)
row[SALE_START_TIME] = pv.sale_from.strftime(TIME_FORMAT)
except AttributeError:
pass
try:
row[SALE_END_DATE] = pv.sale_to.strftime(DATE_FORMAT)
row[SALE_END_TIME] = pv.sale_to.strftime(TIME_FORMAT)
except AttributeError:
pass
writer.writerow(row)
filehandle.close()
f = open('/tmp/tmpexport.csv', 'r')
response = HttpResponse(f, content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=product-info.csv'
return response
context = {}
if django.VERSION >= (1, 8, 0):
context.update(self.admin_site.each_context(request))
elif django.VERSION >= (1, 7, 0):
context.update(self.admin_site.each_context())
context['form'] = form
context['opts'] = self.model._meta
request.current_app = self.admin_site.name
return TemplateResponse(request, [self.export_template_name],
context)
def save_model(self, request, obj, form, change):
"""
Store the product object for creating variations in save_formset.
"""
super(ProductAdmin, self).save_model(request, obj, form, change)
self._product = obj
def save_formset(self, request, form, formset, change):
"""
Here be dragons. We want to perform these steps sequentially:
- Save variations formset
- Run the required variation manager methods:
(create_from_options, manage_empty, etc)
- Save the images formset
The variations formset needs to be saved first for the manager
methods to have access to the correct variations. The images
formset needs to be run last, because if images are deleted
that are selected for variations, the variations formset will
raise errors when saving due to invalid image selections. This
gets addressed in the set_default_images method.
An additional problem is the actual ordering of the inlines,
which are in the reverse order for achieving the above. To
address this, we store the images formset as an attribute, and
then call save on it after the other required steps have
occurred.
"""
# Store the images formset for later saving, otherwise save the
# formset.
if formset.model == ProductImage:
self._images_formset = formset
else:
super(ProductAdmin, self).save_formset(request, form, formset,
change)
# Run each of the variation manager methods if we're saving
# the variations formset.
if formset.model == ProductVariation:
# Build up selected options for new variations.
options = dict([(f, request.POST.getlist(f)) for f in option_fields
if request.POST.getlist(f)])
# Create a list of image IDs that have been marked to delete.
deleted_images = [request.POST.get(f.replace("-DELETE", "-id"))
for f in request.POST
if f.startswith("images-") and f.endswith("-DELETE")]
# Create new variations for selected options.
self._product.variations.create_from_options(options)
# Create a default variation if there are none.
self._product.variations.manage_empty()
# Remove any images deleted just now from variations they're
# assigned to, and set an image for any variations without one.
self._product.variations.set_default_images(deleted_images)
# Save the images formset stored previously.
super(ProductAdmin, self).save_formset(request, form,
self._images_formset, change)
# Run again to allow for no images existing previously, with
# new images added which can be used as defaults for variations.
self._product.variations.set_default_images(deleted_images)
# Copy duplicate fields (``Priced`` fields) from the default
# variation to the product.
self._product.copy_default_variation()
# Save every translated fields from ``ProductOption`` into
# the required ``ProductVariation``
if settings.USE_MODELTRANSLATION:
from collections import OrderedDict
from modeltranslation.utils import (build_localized_fieldname
as _loc)
for opt_name in options:
for opt_value in options[opt_name]:
opt_obj = ProductOption.objects.get(type=opt_name[6:],
name=opt_value)
params = {opt_name: opt_value}
for var in self._product.variations.filter(**params):
for code in OrderedDict(settings.LANGUAGES):
setattr(var, _loc(opt_name, code),
getattr(opt_obj, _loc('name', code)))
var.save()
class ProductOptionAdmin(BaseTranslationModelAdmin):
ordering = ("type", "name")
list_display = ("type", "name")
list_display_links = ("type",)
list_editable = ("name",)
list_filter = ("type",)
search_fields = ("type", "name")
radio_fields = {"type": admin.HORIZONTAL}
class OrderItemInline(admin.TabularInline):
verbose_name_plural = _("Items")
model = OrderItem
extra = 0
formfield_overrides = {MoneyField: {"widget": MoneyWidget}}
def address_pairs(fields):
"""
Zips address fields into pairs, appending the last field if the
total is an odd number.
"""
pairs = list(zip(fields[::2], fields[1::2]))
if len(fields) % 2:
pairs.append(fields[-1])
return pairs
order_list_display = ("id", "billing_name", "total", "time", "status",
"transaction_id")
if HAS_PDF:
order_list_display += ("invoice",)
class OrderAdmin(admin.ModelAdmin):
class Media:
css = {"all": (static("cartridge/css/admin/order.css"),)}
ordering = ("status", "-id")
list_display = order_list_display
list_editable = ("status",)
list_filter = ("status", "time")
list_display_links = ("id", "billing_name",)
search_fields = (["id", "status", "transaction_id"] +
billing_fields + shipping_fields)
date_hierarchy = "time"
radio_fields = {"status": admin.HORIZONTAL}
inlines = (OrderItemInline,)
formfield_overrides = {MoneyField: {"widget": MoneyWidget}}
fieldsets = (
(_("Billing details"), {"fields": address_pairs(billing_fields)}),
(_("Shipping details"), {"fields": address_pairs(shipping_fields)}),
(None, {"fields": ("additional_instructions", ("shipping_total",
"shipping_type"), ('tax_total', 'tax_type'),
("discount_total", "discount_code"), "item_total",
("total", "status"), "transaction_id")}),
)
def change_view(self, *args, **kwargs):
if kwargs.get("extra_context", None) is None:
kwargs["extra_context"] = {}
kwargs["extra_context"]["has_pdf"] = HAS_PDF
return super(OrderAdmin, self).change_view(*args, **kwargs)
class SaleAdmin(admin.ModelAdmin):
list_display = ("title", "active", "discount_deduct", "discount_percent",
"discount_exact", "valid_from", "valid_to")
list_editable = ("active", "discount_deduct", "discount_percent",
"discount_exact", "valid_from", "valid_to")
filter_horizontal = ("categories", "products")
formfield_overrides = {MoneyField: {"widget": MoneyWidget}}
form = DiscountAdminForm
fieldsets = (
(None, {"fields": ("title", "active")}),
(_("Apply to product and/or products in categories"),
{"fields": ("products", "categories")}),
(_("Reduce unit price by"),
{"fields": (("discount_deduct", "discount_percent",
"discount_exact"),)}),
(_("Sale period"), {"fields": (("valid_from", "valid_to"),)}),
)
class DiscountCodeAdmin(admin.ModelAdmin):
list_display = ("title", "active", "code", "discount_deduct",
"discount_percent", "min_purchase", "free_shipping", "valid_from",
"valid_to")
list_editable = ("active", "code", "discount_deduct", "discount_percent",
"min_purchase", "free_shipping", "valid_from", "valid_to")
filter_horizontal = ("categories", "products")
formfield_overrides = {MoneyField: {"widget": MoneyWidget}}
form = DiscountAdminForm
fieldsets = (
(None, {"fields": ("title", "active", "code")}),
(_("Apply to product and/or products in categories"),
{"fields": ("products", "categories")}),
(_("Reduce unit price by"),
{"fields": (("discount_deduct", "discount_percent"),)}),
(None, {"fields": (("min_purchase", "free_shipping"),)}),
(_("Valid for"),
{"fields": (("valid_from", "valid_to", "uses_remaining"),)}),
)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Product, ProductAdmin)
if settings.SHOP_USE_VARIATIONS:
admin.site.register(ProductOption, ProductOptionAdmin)
admin.site.register(Order, OrderAdmin)
admin.site.register(Sale, SaleAdmin)
admin.site.register(DiscountCode, DiscountCodeAdmin)
*** admin.py.orig 2016-05-14 12:33:18.040113927 +0530
--- admin.py 2016-05-14 14:00:35.958529540 +0530
***************
*** 30,45 ****
--- 30,60 ----
"""
from copy import deepcopy
+ import csv
+ import os
+ import shutil
+ import sys
+ from datetime import datetime
+ import django
+ from django.conf.urls import url
from django.contrib import admin
from django.contrib.admin.templatetags.admin_static import static
+ from django.core.management.base import BaseCommand
+ from django.core.management.base import CommandError
+ from django.contrib import messages
+ from django.core.urlresolvers import reverse
from django.db.models import ImageField
+ from django.http import HttpResponseRedirect, HttpResponse
from django.utils.translation import ugettext_lazy as _
+ from django.template.response import TemplateResponse
+
from mezzanine.conf import settings
from mezzanine.core.admin import (DisplayableAdmin,
TabularDynamicInlineAdmin,
BaseTranslationModelAdmin)
+ from mezzanine.core.models import CONTENT_STATUS_PUBLISHED
from mezzanine.pages.admin import PageAdmin
from cartridge.shop.fields import MoneyField
***************
*** 51,63 ****
from cartridge.shop.models import OrderItem, Sale, DiscountCode
from cartridge.shop.views import HAS_PDF
! # Lists of field names.
! option_fields = [f.name for f in ProductVariation.option_fields()]
! _flds = lambda s: [f.name for f in Order._meta.fields if f.name.startswith(s)]
! billing_fields = _flds("billing_detail")
! shipping_fields = _flds("shipping_detail")
!
################
# CATEGORIES #
--- 66,112 ----
from cartridge.shop.models import OrderItem, Sale, DiscountCode
from cartridge.shop.views import HAS_PDF
+ from .forms import ExportForm, ImportForm
! #PRODUCT_DB VARIABLES
! # images get copied from thie directory
! #LOCAL_IMAGE_DIR = "/tmp/orig"
! # images get copied to this directory under STATIC_ROOT
! IMAGE_SUFFIXES = [".jpg", ".png", ".JPG", ".jpeg", ".JPEG", ".tif", ".gif", ".GIF"]
! EMPTY_IMAGE_ENTRIES = ["Please add", "N/A", ""]
! DATE_FORMAT = "%Y-%m-%d"
! TIME_FORMAT = "%H:%M"
!
! # Here we define what column headings are used in the csv.
! TITLE = _("Title")
! CONTENT = _("Content")
! DESCRIPTION = _("Description")
! SKU = _("SKU")
! IMAGE = _("Image")
! CATEGORY = _("Category")
! SUB_CATEGORY = _("Sub-Category")
! # SIZE = _("Size")
! NUM_IN_STOCK = _("Number in Stock")
! UNIT_PRICE = _("Unit Price")
! SALE_PRICE = _("Sale Price")
! SALE_START_DATE = _("Sale Start Date")
! SALE_START_TIME = _("Sale Start Time")
! SALE_END_DATE = _("Sale End Date")
! SALE_END_TIME = _("Sale End Time")
!
! DATETIME_FORMAT = "%s %s" % (DATE_FORMAT, TIME_FORMAT)
! SITE_MEDIA_IMAGE_DIR = _("product")
! SITE_MEDIA_IMAGE_NDIR = _("media/product")
! PRODUCT_IMAGE_DIR = os.path.join(str(settings.STATIC_ROOT), str(SITE_MEDIA_IMAGE_NDIR))
! TYPE_CHOICES = dict()
! for id, choice in settings.SHOP_OPTION_TYPE_CHOICES:
! TYPE_CHOICES[choice] = id
!
! fieldnames = [TITLE, CONTENT, DESCRIPTION, CATEGORY, SUB_CATEGORY,
! SKU, IMAGE, NUM_IN_STOCK, UNIT_PRICE,
! SALE_PRICE, SALE_START_DATE, SALE_START_TIME, SALE_END_DATE, SALE_END_TIME]
! # TODO: Make sure no options conflict with other fieldnames.
! fieldnames += TYPE_CHOICES.keys()
################
# CATEGORIES #
***************
*** 171,176 ****
--- 220,448 ----
form = ProductAdminForm
fieldsets = product_fieldsets
+ #: template for export view
+ export_template_name = 'admin/import_export/export.html'
+ #: template for change_list view
+ change_list_template = 'admin/import_export/change_list_import_export.html'
+ #: template for import view
+ import_template_name = 'admin/import_export/import.html'
+
+ def get_model_info(self):
+ # module_name is renamed to model_name in Django 1.8
+ app_label = self.model._meta.app_label
+ try:
+ return (app_label, self.model._meta.model_name,)
+ except AttributeError:
+ return (app_label, self.model._meta.module_name,)
+
+ def get_urls(self):
+ info = self.get_model_info()
+ urls = super(ProductAdmin, self).get_urls()
+ my_urls = [
+ url(r'^import/$',
+ self.admin_site.admin_view(self.import_action),
+ name='%s_%s_import' % info),
+ url(r'^export/$',
+ self.admin_site.admin_view(self.export_action),
+ name='%s_%s_export' % self.get_model_info()),
+ ]
+ return my_urls + urls
+
+ def _product_from_row(self, row):
+ product, created = Product.objects.get_or_create(title=row[TITLE])
+ product.content = row[CONTENT]
+ product.description = row[DESCRIPTION]
+ # TODO: set the 2 below from spreadsheet.
+ product.status = CONTENT_STATUS_PUBLISHED
+ product.available = True
+ # TODO: allow arbitrary level/number of categories.
+ base_cat, created = Category.objects.get_or_create(title=row[CATEGORY])
+ if row[SUB_CATEGORY]:
+ sub_cat, created = Category.objects.get_or_create(
+ title=row[SUB_CATEGORY],
+ parent=base_cat)
+ product.categories.add(sub_cat)
+ shop_cat, created = Category.objects.get_or_create(title="Shop")
+ product.categories.add(shop_cat)
+ return product
+
+
+ def _make_image(self, image_str, product, local_img_dir):
+ if image_str in EMPTY_IMAGE_ENTRIES:
+ return None
+ # try adding various image suffixes, if none given in original filename.
+ root, suffix = os.path.splitext(image_str)
+ if suffix not in IMAGE_SUFFIXES:
+ raise CommandError("INCORRECT SUFFIX: %s" % image_str)
+ image_path = os.path.join(local_img_dir , image_str)
+ if not os.path.exists(image_path):
+ raise CommandError("NO FILE %s" % image_path)
+ shutil.copy(image_path, PRODUCT_IMAGE_DIR)
+ # shutil.copy(image_path, os.path.join(PRODUCT_IMAGE_DIR, "orig"))
+ image, created = ProductImage.objects.get_or_create(
+ file="%s" % (image_str),
+ description=image_str, # TODO: handle column for this.
+ product=product)
+ return image
+
+
+ def _make_date(self, date_str, time_str):
+ date_string = '%s %s' % (date_str, time_str)
+ date = datetime.datetime.strptime(date_string, DATETIME_FORMAT)
+ return date
+
+ def import_action(self, request, *args, **kwargs):
+ '''
+ Perform a dry_run of the import to make sure the import will not
+ result in errors. If there where no error, save the user
+ uploaded file to a local temp file that will be used by
+ 'process_import' for the actual import.
+ '''
+ context = {}
+ import_formats = ["csv",]
+ form = ImportForm(import_formats,
+ request.POST or None,
+ request.FILES or None)
+ if request.POST and form.is_valid():
+ input_format = ["csv",]
+ import_file = form.cleaned_data['import_file']
+ local_img_dir = form.cleaned_data['imgs_dirname']
+ print(_("Importing .."))
+ # More appropriate for testing.
+ # Product.objects.all().delete()
+ #reader = csv.DictReader(open(import_file))
+ reader = csv.DictReader(import_file)
+ for row in reader:
+ print(row)
+ product = self._product_from_row(row)
+ try:
+ variation = ProductVariation.objects.create(
+ # strip whitespace
+ sku=row[SKU].replace(" ", ""),
+ product=product,
+ )
+ except IntegrityError:
+ raise CommandError("Product with SKU exists! sku: %s" % row[SKU])
+ if row[NUM_IN_STOCK]:
+ variation.num_in_stock = row[NUM_IN_STOCK]
+ if row[UNIT_PRICE]:
+ variation.unit_price = row[UNIT_PRICE]
+ if row[SALE_PRICE]:
+ variation.sale_price = row[SALE_PRICE]
+ if row[SALE_START_DATE] and row[SALE_START_TIME]:
+ variation.sale_from = self._make_date(row[SALE_START_DATE],
+ row[SALE_START_TIME])
+ if row[SALE_END_DATE] and row[SALE_END_TIME]:
+ variation.sale_to = self._make_date(row[SALE_END_DATE],
+ row[SALE_END_TIME])
+ for option in TYPE_CHOICES:
+ if row[option]:
+ name = "option%s" % TYPE_CHOICES[option]
+ setattr(variation, name, row[option])
+ new_option, created = ProductOption.objects.get_or_create(
+ type=TYPE_CHOICES[option], # TODO: set dynamically
+ name=row[option])
+ variation.save()
+ image = self._make_image(row[IMAGE], product, local_img_dir )
+ if image:
+ variation.image = image
+ product.variations.manage_empty()
+ product.variations.set_default_images([])
+ product.copy_default_variation()
+ product.save()
+
+ print("Variations: %s" % ProductVariation.objects.all().count())
+ print("Products: %s" % Product.objects.all().count())
+ success_message = u'Import finished'
+ messages.success(request, success_message)
+
+ url = reverse('admin:%s_%s_changelist' % self.get_model_info(),
+ current_app=self.admin_site.name)
+ return HttpResponseRedirect(url)
+
+ if django.VERSION >= (1, 8, 0):
+ context.update(self.admin_site.each_context(request))
+ elif django.VERSION >= (1, 7, 0):
+ context.update(self.admin_site.each_context())
+
+ context['form'] = form
+ context['opts'] = self.model._meta
+ #context['fields'] = [f.column_name for f in resource.get_fields()]
+
+ request.current_app = self.admin_site.name
+ return TemplateResponse(request, [self.import_template_name],
+ context)
+
+ def export_action(self, request, *args, **kwargs):
+ '''
+ Perform the actual export action (after the user has confirmed he
+ wishes to import)
+ '''
+ formats=("csv",)
+ form = ExportForm(formats, request.POST or None)
+ if form.is_valid():
+ print(_("Exporting .."))
+ #response = HttpResponse(content_type='text/csv')
+ #response['Content-Disposition'] = 'attachment; filename=hostedCustomerReport.csv'
+ filehandle = open("/tmp/tmpexport.csv", 'w')
+ writer = csv.DictWriter(filehandle, fieldnames=fieldnames)
+ #writer = csv.writer(response)
+ headers = dict()
+ for field in fieldnames:
+ headers[field] = field
+ writer.writerow(headers)
+ for pv in ProductVariation.objects.all():
+ row = dict()
+ row[TITLE] = pv.product.title
+ row[CONTENT] = pv.product.content
+ row[DESCRIPTION] = pv.product.description
+ row[SKU] = pv.sku
+ row[IMAGE] = pv.image
+ # TODO: handle multiple categories, and multiple levels of categories
+ cat = pv.product.categories.all()[0]
+ if cat.parent:
+ row[SUB_CATEGORY] = cat.title
+ row[CATEGORY] = cat.parent.title
+ else:
+ row[CATEGORY] = cat.title
+ row[SUB_CATEGORY] = ""
+
+ for option in TYPE_CHOICES:
+ row[option] = getattr(pv, "option%s" % TYPE_CHOICES[option])
+
+ row[NUM_IN_STOCK] = pv.num_in_stock
+ row[UNIT_PRICE] = pv.unit_price
+ row[SALE_PRICE] = pv.sale_price
+ try:
+ row[SALE_START_DATE] = pv.sale_from.strftime(DATE_FORMAT)
+ row[SALE_START_TIME] = pv.sale_from.strftime(TIME_FORMAT)
+ except AttributeError:
+ pass
+ try:
+ row[SALE_END_DATE] = pv.sale_to.strftime(DATE_FORMAT)
+ row[SALE_END_TIME] = pv.sale_to.strftime(TIME_FORMAT)
+ except AttributeError:
+ pass
+ writer.writerow(row)
+ filehandle.close()
+ f = open('/tmp/tmpexport.csv', 'r')
+ response = HttpResponse(f, content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename=product-info.csv'
+ return response
+
+ context = {}
+
+ if django.VERSION >= (1, 8, 0):
+ context.update(self.admin_site.each_context(request))
+ elif django.VERSION >= (1, 7, 0):
+ context.update(self.admin_site.each_context())
+
+ context['form'] = form
+ context['opts'] = self.model._meta
+ request.current_app = self.admin_site.name
+ return TemplateResponse(request, [self.export_template_name],
+ context)
+
def save_model(self, request, obj, form, change):
"""
Store the product object for creating variations in save_formset.
admin.py.orig
Description: Binary data
from __future__ import absolute_import, unicode_literals
from future.builtins import filter, int, range, str, super, zip
from future.utils import with_metaclass
from collections import OrderedDict
from copy import copy
from datetime import date
from itertools import dropwhile, takewhile
from locale import localeconv
from re import match
from django import forms
from django.forms.models import BaseInlineFormSet, ModelFormMetaclass
from django.forms.models import inlineformset_factory
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from mezzanine.conf import settings
from mezzanine.core.templatetags.mezzanine_tags import thumbnail
from cartridge.shop import checkout
from cartridge.shop.models import Product, ProductOption, ProductVariation
from cartridge.shop.models import Cart, CartItem, Order, DiscountCode
from cartridge.shop.utils import (make_choices, set_locale, set_shipping,
clear_session)
ADD_PRODUCT_ERRORS = {
"invalid_options": _("The selected options are currently unavailable."),
"no_stock": _("The selected options are currently not in stock."),
"no_stock_quantity": _("The selected quantity is currently unavailable."),
}
class AddProductForm(forms.Form):
"""
A form for adding the given product to the cart or the
wishlist.
"""
quantity = forms.IntegerField(label=_("Quantity"), min_value=1)
sku = forms.CharField(required=False, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
"""
Handles adding a variation to the cart or wishlist.
When adding from the product page, the product is provided
from the view and a set of choice fields for all the
product options for this product's variations are added to
the form. When the form is validated, the selected options
are used to determine the chosen variation.
A ``to_cart`` boolean keyword arg is also given specifying
whether the product is being added to a cart or wishlist.
If a product is being added to the cart, then its stock
level is also validated.
When adding to the cart from the wishlist page, a sku is
given for the variation, so the creation of choice fields
is skipped.
"""
self._product = kwargs.pop("product", None)
self._to_cart = kwargs.pop("to_cart")
super(AddProductForm, self).__init__(*args, **kwargs)
# Adding from the wishlist with a sku, bail out.
if args[0] is not None and args[0].get("sku", None):
return
# Adding from the product page, remove the sku field
# and build the choice fields for the variations.
del self.fields["sku"]
option_fields = ProductVariation.option_fields()
if not option_fields:
return
option_names, option_labels = list(zip(*[(f.name, f.verbose_name)
for f in option_fields]))
option_values = list(zip(*self._product.variations.filter(
unit_price__isnull=False).values_list(*option_names)))
if option_values:
for i, name in enumerate(option_names):
values = [_f for _f in set(option_values[i]) if _f]
if values:
field = forms.ChoiceField(label=option_labels[i],
choices=make_choices(values))
self.fields[name] = field
def clean(self):
"""
Determine the chosen variation, validate it and assign it as
an attribute to be used in views.
"""
if not self.is_valid():
return
# Posted data will either be a sku, or product options for
# a variation.
data = self.cleaned_data.copy()
quantity = data.pop("quantity")
# Ensure the product has a price if adding to cart.
if self._to_cart:
data["unit_price__isnull"] = False
error = None
if self._product is not None:
# Chosen options will be passed to the product's
# variations.
qs = self._product.variations
else:
# A product hasn't been given since we have a direct sku.
qs = ProductVariation.objects
try:
variation = qs.get(**data)
except ProductVariation.DoesNotExist:
error = "invalid_options"
else:
# Validate stock if adding to cart.
if self._to_cart:
if not variation.has_stock():
error = "no_stock"
elif not variation.has_stock(quantity):
error = "no_stock_quantity"
if error is not None:
raise forms.ValidationError(ADD_PRODUCT_ERRORS[error])
self.variation = variation
return self.cleaned_data
class CartItemForm(forms.ModelForm):
"""
Model form for each item in the cart - used for the
``CartItemFormSet`` below which controls editing the entire cart.
"""
class Meta:
model = CartItem
fields = ("quantity",)
def clean_quantity(self):
"""
Validate that the given quantity is available.
"""
variation = ProductVariation.objects.get(sku=self.instance.sku)
quantity = self.cleaned_data["quantity"]
if not variation.has_stock(quantity - self.instance.quantity):
error = ADD_PRODUCT_ERRORS["no_stock_quantity"].rstrip(".")
raise forms.ValidationError("%s: %s" % (error, quantity))
return quantity
CartItemFormSet = inlineformset_factory(Cart, CartItem, form=CartItemForm,
can_delete=True, extra=0)
class FormsetForm(object):
"""
Form mixin that provides template methods for iterating through
sets of fields by prefix, single fields and finally remaning
fields that haven't been, iterated with each fieldset made up from
a copy of the original form, giving access to as_* methods.
The use case for this is ``OrderForm`` below. It contains a
handful of fields named with the prefixes ``billing_detail_XXX``
and ``shipping_detail_XXX``. Using ``FormsetForm`` we can then
group these into fieldsets in our templates::
<!-- Fields prefixed with "billing_detail_" -->
<fieldset>{{ form.billing_detail_fields.as_p }}</fieldset>
<!-- Fields prefixed with "shipping_detail_" -->
<fieldset>{{ form.shipping_detail_fields.as_p }}</fieldset>
<!-- All remaining fields -->
<fieldset>{{ form.other_fields.as_p }}</fieldset>
Some other helpers exist for use with an individual field name:
- ``XXX_field`` returns a fieldset containing the field named XXX
- ``fields_before_XXX`` returns a fieldset with all fields before
the field named XXX
- ``fields_after_XXX`` returns a fieldset with all fields after
the field named XXX
"""
def _fieldset(self, field_names):
"""
Return a subset of fields by making a copy of the form
containing only the given field names.
"""
fieldset = copy(self)
if not hasattr(self, "_fields_done"):
self._fields_done = []
fieldset.non_field_errors = lambda *args: None
names = [f for f in field_names if f not in self._fields_done]
fieldset.fields = OrderedDict([(f, self.fields[f]) for f in names])
self._fields_done.extend(names)
return fieldset
def values(self):
"""
Return pairs of label and value for each field.
"""
for field in self.fields:
label = self.fields[field].label
if label is None:
label = field[0].upper() + field[1:].replace("_", " ")
yield (label, self.initial.get(field, self.data.get(field, "")))
def __getattr__(self, name):
"""
Dynamic fieldset caller - matches requested attribute name
against pattern for creating the list of field names to use
for the fieldset.
"""
if name == "errors":
return None
filters = (
("^other_fields$", lambda:
self.fields.keys()),
("^hidden_fields$", lambda:
[n for n, f in self.fields.items()
if isinstance(f.widget, forms.HiddenInput)]),
("^(\w*)_fields$", lambda name:
[f for f in self.fields.keys() if f.startswith(name)]),
("^(\w*)_field$", lambda name:
[f for f in self.fields.keys() if f == name]),
("^fields_before_(\w*)$", lambda name:
takewhile(lambda f: f != name, self.fields.keys())),
("^fields_after_(\w*)$", lambda name:
dropwhile(lambda f: f != name, self.fields.keys())[1:]),
)
for filter_exp, filter_func in filters:
filter_args = match(filter_exp, name)
if filter_args is not None:
return self._fieldset(filter_func(*filter_args.groups()))
raise AttributeError(name)
class DiscountForm(forms.ModelForm):
class Meta:
model = Order
fields = ("discount_code",)
def __init__(self, request, data=None, initial=None, **kwargs):
"""
Store the request so that it can be used to retrieve the cart
which is required to validate the discount code when entered.
"""
super(DiscountForm, self).__init__(
data=data, initial=initial, **kwargs)
self._request = request
def clean_discount_code(self):
"""
Validate the discount code if given, and attach the discount
instance to the form.
"""
code = self.cleaned_data.get("discount_code", "")
cart = self._request.cart
if code:
try:
discount = DiscountCode.objects.get_valid(code=code, cart=cart)
self._discount = discount
except DiscountCode.DoesNotExist:
error = _("The discount code entered is invalid.")
raise forms.ValidationError(error)
return code
def set_discount(self):
"""
Assigns the session variables for the discount.
"""
discount = getattr(self, "_discount", None)
if discount is not None:
# Clear out any previously defined discount code
# session vars.
names = ("free_shipping", "discount_code", "discount_total")
clear_session(self._request, *names)
total = self._request.cart.calculate_discount(discount)
if discount.free_shipping:
set_shipping(self._request, _("Free shipping"), 0)
else:
# A previously entered discount code providing free
# shipping may have been entered prior to this
# discount code beign entered, so clear out any
# previously set shipping vars.
clear_session(self._request, "shipping_type", "shipping_total")
self._request.session["free_shipping"] = discount.free_shipping
self._request.session["discount_code"] = discount.code
self._request.session["discount_total"] = str(total)
class OrderForm(FormsetForm, DiscountForm):
"""
Main Form for the checkout process - ModelForm for the Order Model
with extra fields for credit card. Used across each step of the
checkout process with fields being hidden where applicable.
"""
step = forms.IntegerField(widget=forms.HiddenInput())
same_billing_shipping = forms.BooleanField(required=False, initial=True,
label=_("My delivery details are the same as my billing details"))
remember = forms.BooleanField(required=False, initial=True,
label=_("Remember my address for next time"))
card_name = forms.CharField(label=_("Cardholder name"))
card_type = forms.ChoiceField(label=_("Card type"),
widget=forms.RadioSelect,
choices=make_choices(settings.SHOP_CARD_TYPES))
card_number = forms.CharField(label=_("Card number"))
card_expiry_month = forms.ChoiceField(label=_("Card expiry month"),
initial="%02d" % date.today().month,
choices=make_choices(["%02d" % i for i in range(1, 13)]))
card_expiry_year = forms.ChoiceField(label=_("Card expiry year"))
card_ccv = forms.CharField(label=_("CCV"), help_text=_("A security code, "
"usually the last 3 digits found on the back of your card."))
class Meta:
model = Order
fields = ([f.name for f in Order._meta.fields if
f.name.startswith("billing_detail") or
f.name.startswith("shipping_detail")] +
["additional_instructions", "discount_code"])
def __init__(
self, request, step, data=None, initial=None, errors=None,
**kwargs):
"""
Setup for each order form step which does a few things:
- Calls OrderForm.preprocess on posted data
- Sets up any custom checkout errors
- Hides the discount code field if applicable
- Hides sets of fields based on the checkout step
- Sets year choices for cc expiry field based on current date
"""
# ``data`` is usually the POST attribute of a Request object,
# which is an immutable QueryDict. We want to modify it, so we
# need to make a copy.
data = copy(data)
# Force the specified step in the posted data, which is
# required to allow moving backwards in steps. Also handle any
# data pre-processing, which subclasses may override.
if data is not None:
data["step"] = step
data = self.preprocess(data)
if initial is not None:
initial["step"] = step
super(OrderForm, self).__init__(
request, data=data, initial=initial, **kwargs)
self._checkout_errors = errors
# Hide discount code field if it shouldn't appear in checkout,
# or if no discount codes are active.
settings.use_editable()
if not (settings.SHOP_DISCOUNT_FIELD_IN_CHECKOUT and
DiscountCode.objects.active().exists()):
self.fields["discount_code"].widget = forms.HiddenInput()
# Determine which sets of fields to hide for each checkout step.
# A ``hidden_filter`` function is defined that's used for
# filtering out the fields to hide.
is_first_step = step == checkout.CHECKOUT_STEP_FIRST
is_last_step = step == checkout.CHECKOUT_STEP_LAST
is_payment_step = step == checkout.CHECKOUT_STEP_PAYMENT
hidden_filter = lambda f: False
if settings.SHOP_CHECKOUT_STEPS_SPLIT:
if is_first_step:
# Hide cc fields for billing/shipping if steps are split.
hidden_filter = lambda f: f.startswith("card_")
elif is_payment_step:
# Hide non-cc fields for payment if steps are split.
hidden_filter = lambda f: not f.startswith("card_")
elif not settings.SHOP_PAYMENT_STEP_ENABLED:
# Hide all cc fields if payment step is not enabled.
hidden_filter = lambda f: f.startswith("card_")
if settings.SHOP_CHECKOUT_STEPS_CONFIRMATION and is_last_step:
# Hide all fields for the confirmation step.
hidden_filter = lambda f: True
for field in filter(hidden_filter, self.fields):
self.fields[field].widget = forms.HiddenInput()
self.fields[field].required = False
# Set year choices for cc expiry, relative to the current year.
year = now().year
choices = make_choices(list(range(year, year + 21)))
self.fields["card_expiry_year"].choices = choices
@classmethod
def preprocess(cls, data):
"""
A preprocessor for the order form data that can be overridden
by custom form classes. The default preprocessor here handles
copying billing fields to shipping fields if "same" checked.
"""
if data.get("same_billing_shipping", "") == "on":
for field in data:
bill_field = field.replace("shipping_detail", "billing_detail")
if field.startswith("shipping_detail") and bill_field in data:
data[field] = data[bill_field]
return data
def clean_card_expiry_year(self):
"""
Ensure the card expiry doesn't occur in the past.
"""
try:
month = int(self.cleaned_data["card_expiry_month"])
year = int(self.cleaned_data["card_expiry_year"])
except ValueError:
# Haven't reached payment step yet.
return
n = now()
if year == n.year and month < n.month:
raise forms.ValidationError(_("A valid expiry date is required."))
return str(year)
def clean(self):
"""
Raise ``ValidationError`` if any errors have been assigned
externally, via one of the custom checkout step handlers.
"""
if self._checkout_errors:
raise forms.ValidationError(self._checkout_errors)
return super(OrderForm, self).clean()
#######################
# ADMIN WIDGETS #
#######################
class ImageWidget(forms.FileInput):
"""
Render a visible thumbnail for image fields.
"""
def render(self, name, value, attrs):
rendered = super(ImageWidget, self).render(name, value, attrs)
if value:
orig = u"%s%s" % (settings.MEDIA_URL, value)
thumb = u"%s%s" % (settings.MEDIA_URL, thumbnail(value, 48, 48))
rendered = (u"<a target='_blank' href='%s'>"
u"<img style='margin-right:6px;' src='%s'>"
u"</a>%s" % (orig, thumb, rendered))
return mark_safe(rendered)
class MoneyWidget(forms.TextInput):
"""
Render missing decimal places for money fields.
"""
def render(self, name, value, attrs):
try:
value = float(value)
except (TypeError, ValueError):
pass
else:
set_locale()
value = ("%%.%sf" % localeconv()["frac_digits"]) % value
attrs["style"] = "text-align:right;"
return super(MoneyWidget, self).render(name, value, attrs)
class ProductAdminFormMetaclass(ModelFormMetaclass):
"""
Metaclass for the Product Admin form that dynamically assigns each
of the types of product options as sets of checkboxes for selecting
which options to use when creating new product variations.
"""
def __new__(cls, name, bases, attrs):
for option in settings.SHOP_OPTION_TYPE_CHOICES:
field = forms.MultipleChoiceField(label=option[1],
required=False, widget=forms.CheckboxSelectMultiple)
attrs["option%s" % option[0]] = field
args = (cls, name, bases, attrs)
return super(ProductAdminFormMetaclass, cls).__new__(*args)
class ProductAdminForm(with_metaclass(ProductAdminFormMetaclass,
forms.ModelForm)):
"""
Admin form for the Product model.
"""
class Meta:
model = Product
exclude = []
def __init__(self, *args, **kwargs):
"""
Set the choices for each of the fields for product options.
Also remove the current instance from choices for related and
upsell products (if enabled).
"""
super(ProductAdminForm, self).__init__(*args, **kwargs)
for field, options in list(ProductOption.objects.as_fields().items()):
self.fields[field].choices = make_choices(options)
instance = kwargs.get("instance")
if instance:
queryset = Product.objects.exclude(id=instance.id)
if settings.SHOP_USE_RELATED_PRODUCTS:
self.fields["related_products"].queryset = queryset
if settings.SHOP_USE_UPSELL_PRODUCTS:
self.fields["upsell_products"].queryset = queryset
class ProductVariationAdminForm(forms.ModelForm):
"""
Ensure the list of images for the variation are specific to the
variation's product.
"""
def __init__(self, *args, **kwargs):
super(ProductVariationAdminForm, self).__init__(*args, **kwargs)
if "instance" in kwargs:
product = kwargs["instance"].product
qs = self.fields["image"].queryset.filter(product=product)
self.fields["image"].queryset = qs
class ProductVariationAdminFormset(BaseInlineFormSet):
"""
Ensure no more than one variation is checked as default.
"""
def clean(self):
super(ProductVariationAdminFormset, self).clean()
if len([f for f in self.forms if hasattr(f, "cleaned_data") and
f.cleaned_data.get("default", False)]) > 1:
error = _("Only one variation can be checked as the default.")
raise forms.ValidationError(error)
class DiscountAdminForm(forms.ModelForm):
"""
Ensure only one discount field is given a value and if not, assign
the error to the first discount field so that it displays correctly.
"""
def clean(self):
fields = [f for f in self.fields if f.startswith("discount_")]
reductions = [self.cleaned_data.get(f) for f in fields
if self.cleaned_data.get(f)]
if len(reductions) > 1:
error = _("Please enter a value for only one type of reduction.")
self._errors[fields[0]] = self.error_class([error])
return super(DiscountAdminForm, self).clean()
class ImportForm(forms.Form):
import_file = forms.FileField(
label=_('File to import')
)
input_format = forms.ChoiceField(
label=_('Format'),
choices=(),
)
imgs_dirname = forms.CharField(label=_("Imgs Parent Dirname Eg:/tmp/orig"))
def __init__(self, import_formats, *args, **kwargs):
super(ImportForm, self).__init__(*args, **kwargs)
choices = []
#for i, f in enumerate(import_formats):
# choices.append((str(i), f().get_title(),))
choices.append((str(0), "csv",))
if len(import_formats) > 1:
choices.insert(0, ('', '---'))
self.fields['input_format'].choices = choices
class ExportForm(forms.Form):
file_format = forms.ChoiceField(
label=_('Format'),
choices=(),
)
def __init__(self, formats, *args, **kwargs):
super(ExportForm, self).__init__(*args, **kwargs)
choices = []
#for i, f in enumerate(formats):
# choices.append((str(i), f().get_title(),))
choices.append((str(0), "csv",))
if len(formats) > 1:
choices.insert(0, ('', '---'))
self.fields['file_format'].choices = choices
*** forms.py.orig 2016-05-14 13:43:36.353569854 +0530
--- forms.py 2016-05-14 13:46:52.618308720 +0530
***************
*** 542,544 ****
--- 542,585 ----
error = _("Please enter a value for only one type of reduction.")
self._errors[fields[0]] = self.error_class([error])
return super(DiscountAdminForm, self).clean()
+
+
+ class ImportForm(forms.Form):
+ import_file = forms.FileField(
+ label=_('File to import')
+ )
+ input_format = forms.ChoiceField(
+ label=_('Format'),
+ choices=(),
+ )
+ imgs_dirname = forms.CharField(label=_("Imgs Parent Dirname Eg:/tmp/orig"))
+
+ def __init__(self, import_formats, *args, **kwargs):
+ super(ImportForm, self).__init__(*args, **kwargs)
+ choices = []
+ #for i, f in enumerate(import_formats):
+ # choices.append((str(i), f().get_title(),))
+ choices.append((str(0), "csv",))
+ if len(import_formats) > 1:
+ choices.insert(0, ('', '---'))
+
+ self.fields['input_format'].choices = choices
+
+
+ class ExportForm(forms.Form):
+ file_format = forms.ChoiceField(
+ label=_('Format'),
+ choices=(),
+ )
+
+ def __init__(self, formats, *args, **kwargs):
+ super(ExportForm, self).__init__(*args, **kwargs)
+ choices = []
+ #for i, f in enumerate(formats):
+ # choices.append((str(i), f().get_title(),))
+ choices.append((str(0), "csv",))
+ if len(formats) > 1:
+ choices.insert(0, ('', '---'))
+
+ self.fields['file_format'].choices = choices
+
forms.py.orig
Description: Binary data
-
{% block object-tools-items %}
{% if has_add_permission %}
{{ block.super }}
{% endif %}
{% endblock %}
{% trans "Export" %}
{% csrf_token %}{% trans "Import" %}
{% if confirm_form %} {% csrf_token %} {{ confirm_form.as_p }}{% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}
{% trans "This importer will import the following fields: " %}
{{ fields|join:", " }}
{% trans "Errors" %}
-
{% for error in result.base_errors %}
-
{{ error.error }}
{{ error.traceback|linebreaks }}
{% endfor %}
{% for line, errors in result.row_errors %}
{% for error in errors %}
-
{% trans "Line number" %}: {{ line }} - {{ error.error }}
{{ error.row.values|join:", " }}{{ error.traceback|linebreaks }}
{% endfor %}
{% endfor %}
{% trans "Preview" %}
| {% for field in result.diff_headers %} | {{ field }} | {% endfor %}
|---|---|
| {% if row.import_type == 'new' %} {% trans "New" %} {% elif row.import_type == 'skip' %} {% trans "Skipped" %} {% elif row.import_type == 'delete' %} {% trans "Delete" %} {% elif row.import_type == 'update' %} {% trans "Update" %} {% endif %} | {% for field in row.diff %}{{ field }} | {% endfor %}
