class DocumentView(MediaView): """ Edit CMS documents. """ namespace = 'cubane.cms.documents' context = {'is_image': False} exclude_columns = ['size', 'image_size'] PATTERNS = [ view_url(r'download/', 'download', name='download'), view_url(r'preview/', 'preview', name='preview') ] def before(self, request, handler): request.is_image = False def _get_objects(self, request): return self.model.objects.select_related('parent').filter( is_image=False) def preview(self, request): return {'url': request.GET.get('url')}
def __init__(self, *args, **kwargs): self.patterns = copy.copy(self.PATTERNS) self.listing_actions = copy.copy(self.LISTING_ACTIONS) self.shortcut_actions = copy.copy(self.SHORTCUT_ACTIONS) # auto fit self.exclude_columns = [] if not settings.IMAGE_FITTING_ENABLED or not settings.IMAGE_FITTING_SHAPES: self.exclude_columns.append('auto_fit') # provide media sharing if CMS is used if 'cubane.cms' in settings.INSTALLED_APPS: self.patterns.append(view_url(r'share/', 'share', name='share')) self.listing_actions.append(('[/Share]', 'share', 'single')) self.shortcut_actions.append('share') else: self.exclude_columns.append('share_enabled') super(MediaView, self).__init__(*args, **kwargs)
def __init__(self, *args, **kwargs): self.patterns = [] self.listing_actions = [] self.shortcut_actions = [] # view order (previous order screen) self.patterns.append(view_url(r'view/$', 'view', name='view')) self.listing_actions.append(('View', 'view', 'single')) self.shortcut_actions.append('view') # support for pre-auth payments (approval) if settings.SHOP_PREAUTH: self.patterns.extend([ view_url(r'approve/$', 'approve', name='approve'), view_url(r'reject/$', 'reject', name='reject'), view_url(r'cancel/$', 'cancel', name='cancel') ]) self.listing_actions.extend([('[Approve]', 'approve', 'single'), ('[Reject]', 'reject', 'single'), ('[Cancel]', 'cancel', 'single')]) self.shortcut_actions.extend([ 'approve', 'reject', 'cancel', ]) else: self.exclude_columns = ['approval_status'] self.patterns.extend([ view_url(r'basket-editor/', 'basket_editor', name='basket_editor'), view_url(r'basket-editor-search/', 'basket_editor_search', name='basket_editor_search'), view_url(r'basket-editor-add-to-basket/', 'basket_editor_add_to_basket', name='basket_editor_add_to_basket'), ]) super(OrderView, self).__init__(*args, **kwargs)
class ProductView(ModelView): """ Editing categories (tree) """ template_path = 'cubane/ishop/merchant/products/' model = get_product_model() def __init__(self, namespace, with_folders): self.namespace = namespace self.with_folders = with_folders if with_folders: self.folder_model = get_category_model() self.sortable = self.with_folders # multiple categories if settings.SHOP_MULTIPLE_CATEGORIES: self.exclude_columns = ['category'] self.multiple_folders = True else: self.exclude_columns = ['categories_display'] self.multiple_folders = False super(ProductView, self).__init__() patterns = [ view_url(r'varieties/', 'varieties', name='varieties'), view_url(r'varieties/(?P<product_id>\d+)/edit/(?P<variety_id>\d+)/', 'varieties_edit', name='varieties.edit'), view_url(r'varieties/(?P<product_id>\d+)/delete/(?P<variety_id>\d+)', 'varieties_delete', name='varieties.delete'), view_url(r'sku/', 'sku', name='sku'), view_url(r'delivery/', 'delivery', name='delivery'), view_url(r'google-products-export/', 'google_products', name='google_products'), ] listing_actions = [('[SKUs]', 'sku', 'single'), ('[Varieties]', 'varieties', 'single'), ('[Delivery]', 'delivery', 'single'), ('Export To Google', 'google_products', 'any')] shortcut_actions = ['sku', 'varieties', 'delivery'] def _get_objects(self, request): if settings.SHOP_MULTIPLE_CATEGORIES: # multiple categories return self.model.objects.prefetch_related( Prefetch('categories', queryset=ProductCategory.objects.select_related( 'category').order_by('seq'))).distinct() else: # single category return self.model.objects.select_related('category').all() def _get_folders(self, request, parent): folders = self.folder_model.objects.all() if parent: folders = folders.filter(parent=parent) return folders def _folder_filter(self, request, objects, category_pks): """ Filter given object queryset by the given folder primary key. """ if category_pks: q = Q() if settings.SHOP_MULTIPLE_CATEGORIES: # multiple categories for pk in category_pks: q |= Q(categories__id=pk) | \ Q(categories__parent_id=pk) | \ Q(categories__parent__parent_id=pk) | \ Q(categories__parent__parent__parent_id=pk) | \ Q(categories__parent__parent__parent__parent_id=pk) | \ Q(categories__parent__parent__parent__parent__parent_id=pk) else: # single category for pk in category_pks: q |= Q(category_id=pk) | \ Q(category__parent_id=pk) | \ Q(category__parent__parent_id=pk) | \ Q(category__parent__parent__parent_id=pk) | \ Q(category__parent__parent__parent__parent_id=pk) | \ Q(category__parent__parent__parent__parent__parent_id=pk) # apply filter objects = objects.filter(q) return objects def _get_folder_assignment_name(self): """ Return the name of the field that is used to assign a folder to. """ if settings.SHOP_MULTIPLE_CATEGORIES: return 'categories' else: return 'category' def form_initial(self, request, initial, instance, edit): """ Setup gallery images (initial form data) """ initial['_gallery_images'] = load_media_gallery( instance.gallery_images) initial['_related_products_collection'] = RelatedModelCollection.load( instance, RelatedProducts) if settings.SHOP_MULTIPLE_CATEGORIES: initial['categories'] = RelatedModelCollection.load( instance, ProductCategory, sortable=False) def bulk_form_initial(self, request, initial, instance, edit): if settings.SHOP_MULTIPLE_CATEGORIES: initial['categories'] = RelatedModelCollection.load( instance, ProductCategory, sortable=False) def before_save(self, request, cleaned_data, instance, edit): """ Maintain SKU based on barcode. """ if request.settings.sku_is_barcode: instance.sku = cleaned_data.get('barcode') def after_save(self, request, d, instance, edit): """ Save gallery items (in seq.) """ save_media_gallery(request, instance, d.get('_gallery_images')) RelatedModelCollection.save(request, instance, d.get('_related_products_collection'), RelatedProducts) if settings.SHOP_MULTIPLE_CATEGORIES: RelatedModelCollection.save(request, instance, d.get('categories'), ProductCategory, allow_duplicates=False, sortable=False) def after_bulk_save(self, request, d, instance, edit): if settings.SHOP_MULTIPLE_CATEGORIES: RelatedModelCollection.save(request, instance, d.get('categories'), ProductCategory, allow_duplicates=False, sortable=False) def varieties(self, request): product_id = request.GET.get('pk') product = get_object_or_404(get_product_model(), pk=product_id) # load variety options variety_options = [ (a.variety_option.variety, a.variety_option) for a in VarietyAssignment.objects.select_related( 'variety_option', 'variety_option__variety').filter( product=product) ] assigned_variety_ids = [variety.pk for variety, _ in variety_options] # load all varieties and split them into assigned and unassigned varieties = Variety.objects.prefetch_related('options').exclude( options=None).order_by('title') if product.sku_enabled: varieties = varieties.filter(sku=False) # split into assigned and unassigned varieties = list(varieties) assigned = filter(lambda v: v.id in assigned_variety_ids, varieties) unassigned = filter(lambda v: v.id not in assigned_variety_ids, varieties) # inject list of assigned variety options for all assigned varieties for v in assigned: # collect assigned options assigned_options = [] for variety, option in variety_options: if v.pk == variety.pk: assigned_options.append(option) # sort by title and construct display text assigned_options = sorted(assigned_options, key=lambda o: o.title) v.assigned_options_display = ', '.join( option.title.strip() for option in assigned_options[:5]) + ( ', ...' if len(assigned_options) > 5 else '') return { 'product': product, 'assigned': assigned, 'unassigned': unassigned, 'ok_url': self._get_url(request, 'index') } def varieties_edit(self, request, product_id, variety_id): product = get_object_or_404(get_product_model(), pk=product_id) variety = get_object_or_404(Variety, pk=variety_id) options = list(variety.options.order_by('seq', 'id')) assignments = VarietyAssignment.objects.select_related( 'variety_option').filter(product=product, variety_option__variety=variety) assignment_list = list(assignments) # dataset based on available options initial = [{ 'option_id': option.id, 'title': option.title, 'enabled': False, 'offset_type': option.default_offset_type, 'offset_value': option.default_offset_value, 'text_label': option.text_label, 'seq': option.seq, 'option_enabled': option.enabled } for option in options] # update base dataset based on available assignments... for initial_option in initial: for assignment in assignment_list: if initial_option['option_id'] == assignment.variety_option.id: initial_option['enabled'] = True initial_option['option_enabled'] = True initial_option['offset_type'] = assignment.offset_type initial_option['offset_value'] = assignment.offset_value # remove options that are not currently assigned but disabled... initial = filter(lambda option: option.get('option_enabled'), initial) # sort by enabled state, then seq if we have a lot of varieties if len(initial) > 15: initial = sorted(initial, key=lambda x: (-x.get('enabled', x.get('seq')))) # determine form class if variety.is_attribute: form_class = VarietyAttributeAssignmentFormset else: form_class = VarietyAssignmentFormset # create form if request.method == 'POST': formset = form_class(request.POST) else: formset = form_class(initial=initial) # validation if formset.is_valid(): # delete existing assignments for assignment in assignments: request.changelog.delete(assignment) assignment.delete() # create new assignments for form in formset.forms: d = form.cleaned_data if d.get('enabled') == True: for option in options: if option.id == d.get('option_id'): assignment = VarietyAssignment() assignment.variety_option = option assignment.product = product if not variety.is_attribute: assignment.offset_type = d.get('offset_type') assignment.offset_value = d.get('offset_value') assignment.save() request.changelog.create(assignment) break request.changelog.commit( 'Variety Options for <em>%s</em> for product <em>%s</em> updated.' % (variety.title, product.title), product, flash=True) active_tab = request.POST.get('cubane_save_and_continue', '0') if not active_tab == '0': return self._redirect(request, 'varieties.edit', args=[product.id, variety.id]) else: return self._redirect(request, 'varieties', product) return { 'product': product, 'variety': variety, 'form': formset, } @view(require_POST) def varieties_delete(self, request, product_id, variety_id): product = get_object_or_404(get_product_model(), pk=product_id) variety = get_object_or_404(Variety, pk=variety_id) assignments = VarietyAssignment.objects.filter( product=product, variety_option__variety=variety) # delete assignments for assignment in assignments: request.changelog.delete(assignment) assignment.delete() request.changelog.commit( 'Variety <em>%s</em> removed.' % variety.title, product) return to_json_response({ 'success': True, }) def sku(self, request): # get product product_id = request.GET.get('pk') product = get_object_or_404(get_product_model(), pk=product_id) # get varieties _varieties = Variety.objects.prefetch_related( Prefetch('options', queryset=VarietyOption.objects.order_by('title')) ).filter(sku=True).exclude(options=None).exclude( style=Variety.STYLE_ATTRIBUTE).order_by('title').distinct() skus = ProductSKU.objects.filter(product=product) assigned_option_ids = [ a.variety_option.id for a in VarietyAssignment.objects.select_related('variety_option') .filter(product=product, variety_option__variety__sku=True) ] # initial dataset currently present initial = {} for sku in skus: initial[sku.pk] = sku initial[sku.pk].errors = [] # determine barcode system cms_settings = get_cms_settings() barcode_system = cms_settings.get_barcode_system(product) # create template form form_template = ProductSKUForm() form_template.configure(request, barcode_system) def has_var(prefix, name): return 'f-%s-%s' % (prefix, name) in request.POST def get_var(prefix, name, default=None): return request.POST.get('f-%s-%s' % (prefix, name), default) def get_int_var(prefix, name, default=None): return parse_int(get_var(prefix, name), default) # construct list of variety option names varieties = [] variety_index = {} for variety in _varieties: variety_index[variety.id] = { 'id': variety.id, 'title': variety.title, 'sku': variety.sku, 'options': {} } item = { 'id': variety.id, 'title': variety.title, 'sku': variety.sku, 'options': [], 'n_assigned_options': 0 } for option in variety.options.all(): variety_index[variety.id].get('options')[option.id] = { 'id': option.id, 'title': option.title, 'fullTitle': '%s: <em>%s</em>' % (variety.title, option.title) } item.get('options').append({ 'id': option.id, 'title': option.title, 'assigned': option.id in assigned_option_ids }) if option.pk in assigned_option_ids: item['n_assigned_options'] += 1 varieties.append(item) # sort varieties by number of assigned options, so that varieties that # have been assigned are at the top of the list. The rest remains sorted # alphabetically... varieties.sort(key=lambda x: -x.get('n_assigned_options', 0)) # validation is_valid = True if request.method == 'POST': # process sku records prefixes = request.POST.getlist('skus') assigned_option_ids = [] skus_to_save = [] sku_code_processed = [] barcodes_processed = [] for index, prefix in enumerate(prefixes): # extract relevant informatioin from post for # individual combination _id = get_var(prefix, '_id') d = { 'enabled': get_var(prefix, 'enabled') == 'on', 'sku': get_var(prefix, 'sku'), 'barcode': get_var(prefix, 'barcode'), 'price': get_var(prefix, 'price'), 'stocklevel': get_int_var(prefix, 'stocklevel', 0) } # parse assigned variety options from request data n_variety_option = 1 d['variety_options'] = [] while len(d['variety_options']) <= 16: _name = 'vo_%d' % n_variety_option if has_var(prefix, _name): d['variety_options'].append(get_int_var(prefix, _name)) n_variety_option += 1 else: break # make sure that sku, barcode and price are None # instead of empty if _id == '': _id = None if d.get('sku') == '': d['sku'] = None if d.get('barcode') == '': d['barcode'] = None if d.get('price') == '': d['price'] = None # construct form based on this data and validate form = ProductSKUForm(d) form.configure(request, barcode_system) # get variety options variety_options = VarietyOption.objects.filter( pk__in=d.get('variety_options')) # create or edit? sku = initial.get(_id, None) if sku is None: sku = ProductSKU.objects.get_by_variety_options( product, variety_options) # still not found? -> create new item if sku is None: sku = ProductSKU() sku.product = product # remember the sku record to be saved once we processed # everything. We will not save anything until everything # is considered to be valid. skus_to_save.append(sku) # mark any assigned variety options as selected, so that they # indeed remain selected, even if they have actually not been # properly assigned yet because if from errors for example for _variety in varieties: _options = _variety.get('options') for _option in _options: for _assigned_option in variety_options: if _option.get('id') == _assigned_option.pk: _option['assigned'] = True break # inject error information and keep track of error states sku.errors = [] if form.is_valid(): # update data from from d = form.cleaned_data else: for field, error in form.errors.items(): sku.errors.append({'field': field, 'error': error[0]}) is_valid = False # copy original data or cleaned data sku.enabled = d.get('enabled', False) sku.sku = d.get('sku') sku.barcode = d.get('barcode') sku.price = d.get('price') sku.stocklevel = d.get('stocklevel', 0) # keep track of variety options that should be assigned due to # SKU's that are enabled if sku.enabled: assigned_option_ids.extend( [option.pk for option in variety_options]) # set variety options (saved later) sku._variety_options = variety_options # verify uniqueness of the SKU code if not self._verify_sku(product, sku, sku_code_processed, initial): is_valid = False # verify uniqueness of barcode if not self._verify_barcode(product, sku, barcodes_processed, initial): is_valid = False # maintain changed data in initial data set, so that all # changes make theire way back into the view, even through # we might not have saved changes due to errors _id = ('idx_%d' % index) if sku.pk is None else sku.pk initial[_id] = sku # process if everything is valid if request.method == 'POST' and is_valid: # create missing option assignments assigned_option_ids = list( set(filter(lambda x: x is not None, assigned_option_ids))) for option_id in assigned_option_ids: try: assignment = VarietyAssignment.objects.get( product=product, variety_option__pk=option_id) except VarietyAssignment.DoesNotExist: VarietyAssignment.objects.create( product=product, variety_option_id=option_id) # remove deprecated option assignments deprecated_assignments = VarietyAssignment.objects.select_related( 'variety_option').filter( product=product, variety_option__variety__sku=True).exclude( variety_option__pk__in=assigned_option_ids) for deprecated_assignment in deprecated_assignments: deprecated_assignment.delete() # save changes to sku records. Null sku, so that we would not # collide when making updates sku_ids_saved = [] for sku in skus_to_save: # save product sku itself sku._sku = sku.sku sku.sku = None sku.save() sku_ids_saved.append(sku.id) # assign and save variety options sku.variety_options = sku._variety_options # remove all previous SKU deprecated_skus = ProductSKU.objects.filter( product=product).exclude(pk__in=sku_ids_saved) for deprecated_sku in deprecated_skus: deprecated_sku.delete() # apply new sku names, which is now safe to do for sku in skus_to_save: if request.settings.sku_is_barcode: sku.sku = sku.barcode else: sku.sku = sku._sku sku.save() # redirect and message if request.method == 'POST' and is_valid: messages.add_message( request, messages.SUCCESS, 'Product Varieties and SKUs saved for <em>%s</em>.' % product.title) if request.POST.get('cubane_save_and_continue') is None: return self._redirect(request, 'index') else: return self._redirect(request, 'sku', product) # materialise current initial data _initial = {} for _id, _sku in initial.items(): _initial[_id] = model_to_dict(_sku) if _sku.pk is None: _initial[_id]['variety_options'] = [ option.pk for option in _sku._variety_options ] else: _initial[_id]['variety_options'] = [ option.pk for option in _sku.variety_options.all() ] _initial[_id]['errors'] = _sku.errors if hasattr(_sku, 'errors') else [] # template context return { 'product': product, 'varieties': varieties, 'initial': to_json(_initial), 'variety_index_json': to_json(variety_index), 'form_template': form_template } def _verify_sku(self, product, sku, sku_code_processed, initial): """ Verify that the given SKU does not exist twice in the system. """ is_valid = True def add_error(errors, error): # already exists? for err in errors: if err.get('field') == error.get('field') and err.get( 'error') == error.get('error'): return errors.append(error) # empty SKU? if not sku.sku: return is_valid # make sure that the sku number does not conflict with # any other product in the system. products = get_product_model().objects.filter(sku=sku) if products.count() > 0: sku.errors.append({ 'field': 'sku', 'error': 'SKU number already in use by product \'%s\'.' % products[0].title }) is_valid = False # conflicts with any other record we processed so far? if sku.sku in sku_code_processed: for _, _sku in initial.items(): if _sku.sku == sku.sku: error = { 'field': 'sku', 'error': 'SKU number already in use for this product.' } add_error(sku.errors, error) add_error(_sku.errors, error) is_valid = False sku_code_processed.append(sku.sku) # conflict with any other SKU record for any other product? product_skus = ProductSKU.objects.exclude(product=product).filter( sku=sku.sku) if product_skus.count() > 0: sku.errors.append({ 'field': 'sku', 'error': 'SKU number already in use by product \'%s\'.' % product_skus[0].product.title }) is_valid = False return is_valid def _verify_barcode(self, product, sku, barcodes_processed, initial): """ Verify that any assigned barcode does not exist twice in the system. """ is_valid = True # empty barcode? if not sku.barcode: return is_valid # make sure that the barcode does not conflict with any product products = get_product_model().objects.filter(barcode=sku.barcode) if products.count() > 0: sku.errors.append({ 'field': 'barcode', 'error': 'Barcode already in use by product \'%s\'.' % products[0].title }) is_valid = False # conflicts with any other record we processed so far? if sku.barcode in barcodes_processed: for _, _sku in initial.items(): if _sku.barcode == sku.barcode: error = { 'field': 'barcode', 'error': 'Barcode already in use for this product.' } sku.errors.append(error) _sku.errors.append(error) is_valid = False barcodes_processed.append(sku.barcode) # conflict with any other SKU record for any other product? product_skus = ProductSKU.objects.exclude(product=product).filter( barcode=sku.barcode) if product_skus.count() > 0: sku.errors.append({ 'field': 'barcode', 'error': 'Barcode already in use by product \'%s\'.' % product_skus[0].product.title }) is_valid = False return is_valid def delivery(self, request): product_id = request.GET.get('pk') product = get_object_or_404(get_product_model(), pk=product_id) # get general delivery options that are available options = DeliveryOption.objects.filter(enabled=True) # get available delivery options delivery_options = list( ProductDeliveryOption.objects.select_related( 'delivery_option').filter(product=product)) delivery_options_ids = [ option.delivery_option.id for option in delivery_options ] # add missing options, so that each is convered for option in options: if option.id not in delivery_options_ids: assignment = ProductDeliveryOption() assignment.product = product assignment.delivery_option = option delivery_options.append(assignment) # dataset based on available options initial = [{ 'option_id': option.delivery_option.id, 'deliver_uk': option.delivery_option.deliver_uk, 'deliver_eu': option.delivery_option.deliver_eu, 'deliver_world': option.delivery_option.deliver_world, 'title': option.delivery_option.title, 'uk': option.uk, 'eu': option.eu, 'world': option.world } for option in delivery_options] if request.method == 'POST': formset = DeliveryOptionFormset(request.POST, initial=initial) else: formset = DeliveryOptionFormset(initial=initial) if request.method == 'POST': if formset.is_valid(): # delete all existing assignments assignments = ProductDeliveryOption.objects.filter( product=product) for assignment in assignments: request.changelog.delete(assignment) assignment.delete() # create new assignments for form in formset.forms: d = form.cleaned_data for option in options: if option.id == d.get('option_id'): assignment = ProductDeliveryOption() assignment.product = product assignment.delivery_option = option assignment.uk = d.get('uk') assignment.eu = d.get('eu') assignment.world = d.get('world') assignment.save() request.changelog.create(assignment) break # commit, message and redirect request.changelog.commit( 'Delivery options for product <em>%s</em> updated.' % product.title, product, flash=True) return self.redirect_to_index_or(request, 'delivery', product) else: print formset.errors return { 'product': product, 'delivery_options': delivery_options, 'form': formset } def google_products(self, request): def prettify_xml(elem): """ Return a pretty-printed XML string for the Element. """ rough_string = tostring(elem) reparsed = minidom.parseString(rough_string) return reparsed.toprettyxml(indent='\t').encode('utf-8', 'replace') products = get_product_model().objects.filter(feed_google=True) root = Element('rss') root.attrib['xmlns:g'] = 'http://base.google.com/ns/1.0' root.attrib['version'] = '2.0' channel = SubElement(root, 'channel') title = SubElement(channel, 'title') title.text = request.settings.name link = SubElement(channel, 'link') link.text = settings.DOMAIN_NAME description = SubElement(channel, 'description') for p in products: # availability if p.is_available and not p.pre_order: txt_availability = 'in stock' elif p.pre_order: txt_availability = 'preorder' else: txt_availability = 'out of stock' # determine delivery charge by placing the product onto the basket basket = Basket() basket.add_item(p, None, 1) delivery_charge = basket.delivery # determine feed item attributes txt_id = unicode(p.id) txt_title = clean_unicode(p.title).strip() txt_link = p.get_absolute_url() txt_description = text_from_html(p.description, 5000) txt_condition = 'new' txt_price = '%.2f GBP' % p.price txt_google_category = p.category.google_product_category if p.category and p.category.google_product_category else None txt_category = p.category.get_taxonomy_path( ) if p.category else None txt_country = 'GB' txt_delivery_price = '%s %s' % (delivery_charge, 'GBP') txt_barcode = p.barcode.strip() if p.barcode else None txt_part_number = p.part_number.strip() if p.part_number else None txt_brand = p.get_brand_title() # create item item = SubElement(channel, 'item') # id _id = SubElement(item, 'g:id') _id.text = txt_id # title title = SubElement(item, 'title') title.text = txt_title # link/url link = SubElement(item, 'link') link.text = txt_link # main text description = SubElement(item, 'description') description.text = txt_description # condition condition = SubElement(item, 'g:condition') condition.text = txt_condition # price price = SubElement(item, 'g:price') price.text = txt_price # availability availability = SubElement(item, 'g:availability') availability.text = txt_availability # google shopping category if txt_google_category: gcategory = SubElement(item, 'g:google_product_category') gcategory.text = txt_google_category # product type if txt_category: category = SubElement(item, 'g:product_type') category.text = txt_category # shipping shipping = SubElement(item, 'g:shipping') # country country = SubElement(shipping, 'g:country') country.text = txt_country # delivery price delivery_price = SubElement(shipping, 'g:price') delivery_price.text = txt_delivery_price # barcode, must be a valid UPC-A (GTIN-12), EAN/JAN (GTIN-13) # or GTIN-14, so we need to have at least 12 characters. if txt_barcode: gtin = SubElement(item, 'g:gtin') gtin.text = txt_barcode # part number if txt_part_number: _mpn = SubElement(item, 'g:mpn') _mpn.text = txt_part_number # brand if txt_brand: brand = SubElement(item, 'g:brand') brand.text = txt_brand # image if p.image: image = SubElement(item, 'g:image_link') image.text = p.image.large_url # additional images if len(p.gallery) > 0: for m in p.gallery[:10]: additional_image_link = SubElement( item, 'g:additional_image_link') additional_image_link.text = m.large_url # get temp. filename f = NamedTemporaryFile(delete=False) tmp_filename = f.name f.close() # create tmp file (utf-8) f = open(tmp_filename, 'w+b') f.write(prettify_xml(root)) f.seek(0) # send response filename = 'google_products_%s.xml' % datetime.date.today().strftime( '%d_%m_%Y') response = HttpResponse(FileWrapper(f), content_type='text/plain') response['Content-Disposition'] = 'attachment; filename=%s' % filename return response
class CustomerView(ModelView): template_path = 'cubane/ishop/merchant/customers/' namespace = 'cubane.ishop.customers' model = get_customer_model() patterns = [ view_url(r'change-password/$', view='change_password', name='change_password'), ] listing_actions = [('[Change Password]', 'change_password', 'single')] shortcut_actions = [ 'change_password', ] def __init__(self, *args, **kwargs): if not settings.SHOP_CHANGE_CUSTOMER_PASSWORD_ENABLED: self.shortcut_actions = [] self.listing_actions = [] super(CustomerView, self).__init__(*args, **kwargs) def _get_objects(self, request): return self.model.objects.all() @view(require_POST) @view(permission_required('delete')) def delete(self, request, pk=None): """ Delete existing model instance with given primary key pk or (if no primary key is given in the url) attempt to delete multiple entities that are given by ids post argument. """ # determine list of pks pks = [] if pk: pks = [pk] else: pks = request.POST.getlist('pks[]', []) if len(pks) == 0: pk = request.POST.get('pk') if pk: pks = [pk] # delete instance(s)... if len(pks) == 1: instance = self.get_object_or_404(request, pks[0]) label = instance.__unicode__() if not label: label = '<no label>' instance.delete() message = self._get_success_message(label, 'deleted') else: instances = self._get_objects(request).filter(pk__in=pks) for instance in instances: instance.delete() message = '%d %s deleted successfully.' % ( len(instances), self.model._meta.verbose_name_plural) # response if self._is_json(request): return to_json_response({'success': True, 'message': message}) else: messages.add_message(request, messages.SUCCESS, message) return self._redirect(request, 'index') def change_password(self, request): # feature enabled? if not settings.SHOP_CHANGE_CUSTOMER_PASSWORD_ENABLED: raise Http404('Feature is not enabled.') customer_id = request.GET.get('pk') try: customer = self.model.objects.get(pk=customer_id) user = customer.user except self.model.DoesNotExist: raise Http404('Unknown customer account id %s.' % customer_id) if request.method == 'POST': form = CustomerChangePasswordForm(request.POST) else: form = CustomerChangePasswordForm() if request.method == 'POST' and form.is_valid(): user.set_password(form.cleaned_data['password']) user.save() messages.add_message( request, messages.SUCCESS, 'Password changed for <em>%s</em>.' % customer.email) return self._redirect(request, 'index') return {'customer': customer, 'form': form} @view(csrf_exempt) def export(self, request): return _export(request)
class SitemapView(TemplateView): """ Edit cms content by navigating the main sitemap structure of the website. """ template_path = 'cubane/cms/sitemap/' namespace = 'cubane.cms.sitemap' patterns = [ view_url(r'^$', 'index', name='index'), view_url(r'^node/$', 'node', name='node'), ] def __init__(self, *args, **kwargs): super(SitemapView, self).__init__(*args, **kwargs) def index(self, request): return {} def node(self, request): # get cms from cubane.cms.views import get_cms cms = get_cms() # root level (pages and directory categories) if 'pk' not in request.GET and 'type' not in request.GET: return to_json_response({ 'success': True, 'items': [cms.get_sitemap_item(request, page) for page in cms.get_sitemap_root_pages()] }) # get pk argument if 'pk' not in request.GET: raise Http404('Missing argument: pk.') try: pk = int(request.GET.get('pk')) except ValueError: raise Http404('Invalid pk argument: Not a number.') # get type argument if 'type' not in request.GET: raise Http404('Missing argument: type.') type_name = request.GET.get('type') # resolve type by given name model = None for m in get_models(): if m.__name__ == type_name: model = m break if not model: raise Http404('Unknown model type name: \'%s\'.' % type_name) # get node by given pk node = model.objects.get(pk=pk) # generate child nodes items = [] children = cms.get_sitemap_children(node) if children: for child in children: item = cms.get_sitemap_item(request, child) if item: items.append(item) # return response (json) return to_json_response({ 'success': True, 'items': items })
class MediaView(ModelView): """ Base class for editing media assets. """ template_path = 'cubane/media/' model = Media folder_model = MediaFolder form = MediaForm PATTERNS = [view_url(r'download/', 'download', name='download')] LISTING_ACTIONS = [('Download', 'download', 'multiple')] SHORTCUT_ACTIONS = ['download'] def __init__(self, *args, **kwargs): self.patterns = copy.copy(self.PATTERNS) self.listing_actions = copy.copy(self.LISTING_ACTIONS) self.shortcut_actions = copy.copy(self.SHORTCUT_ACTIONS) # auto fit self.exclude_columns = [] if not settings.IMAGE_FITTING_ENABLED or not settings.IMAGE_FITTING_SHAPES: self.exclude_columns.append('auto_fit') # provide media sharing if CMS is used if 'cubane.cms' in settings.INSTALLED_APPS: self.patterns.append(view_url(r'share/', 'share', name='share')) self.listing_actions.append(('[/Share]', 'share', 'single')) self.shortcut_actions.append('share') else: self.exclude_columns.append('share_enabled') super(MediaView, self).__init__(*args, **kwargs) def before_save(self, request, cleaned_data, instance, edit): """ Detect changes of media instance regarding image-relevant properties, such as auto-fit or compression quality. """ auto_fit_changed = settings.IMAGE_FITTING_ENABLED and settings.IMAGE_FITTING_SHAPES and instance.auto_fit != cleaned_data.get( 'auto_fit') quality_changed = instance.jpeg_quality != cleaned_data.get( 'jpeg_quality') focal_point_changed = instance.focal_x != cleaned_data.get( 'focal_x') or instance.focal_y != cleaned_data.get('focal_y') if auto_fit_changed or quality_changed or focal_point_changed: instance._image_config_changed = True else: instance._image_config_changed = False def after_save(self, request, cleaned_data, instance, edit): changed = False if 'media' in request.FILES: instance.upload_from_stream(request.FILES['media'], request=request) changed = True elif instance._image_config_changed: # edit -> if we changed image-relevant configuration then we need # to re-generate image shapes... instance.upload(request=request) changed = True # image edited -> increase version number if edit and changed: instance.increase_version() def before_bulk_save(self, request, cleaned_data, instance, edit): """ Called before the given model instance is saved as part of bulk editing. """ self.before_save(request, cleaned_data, instance, edit) def after_bulk_save(self, request, cleaned_data, instance, edit): """ Called after the given model instance is saved as part of bulk editing. """ self.after_save(request, cleaned_data, instance, edit) def _get_folders(self, request, parent): folders = self.folder_model.objects.all() if parent: folders = folders.filter(parent=parent) return folders def _folder_filter(self, request, objects, folder_pks): if folder_pks: q = Q() for pk in folder_pks: q |= Q(parent_id=pk) | \ Q(parent__parent_id=pk) | \ Q(parent__parent__parent_id=pk) | \ Q(parent__parent__parent__parent_id=pk) | \ Q(parent__parent__parent__parent__parent_id=pk) | \ Q(parent__parent__parent__parent__parent__parent_id=pk) objects = objects.filter(q) return objects def after(self, request, handler, response): if isinstance(response, dict): response['multi_url'] = self.namespace + '.create_multi' return super(MediaView, self).after(request, handler, response) @property def listing_with_image(self): return True def _create_edit(self, request, pk=None, edit=False, duplicate=False): if not edit and len(request.FILES.getlist('media')) > 1: # cancel? if request.POST.get('cubane_form_cancel', '0') == '1': return self._redirect(request, 'index') kwargs = {} # create form if request.method == 'POST': form = MultiMediaForm(request.POST, request.FILES, **kwargs) else: form = MultiMediaForm(**kwargs) form.configure(request) # validate form if request.method == 'POST' and form.is_valid(): # update properties in model instance d = form.cleaned_data if 'media' in request.FILES: file_list = request.FILES.getlist('media') n = 0 Progress.start(request, len(file_list)) first_instance = None for f in file_list: instance = self.model() instance.parent = d.get('parent') if first_instance is None: first_instance = instance # auto fit if settings.IMAGE_FITTING_ENABLED and settings.IMAGE_FITTING_SHAPES: instance.auto_fit = d.get('auto_fit') # jpeg quality instance.jpeg_quality = d.get('jpeg_quality') # save instance.save() instance.upload_save_from_stream(f, request=request) n += 1 request.changelog.create(instance) # notify task runner to do the job if instance.is_blank: TaskRunner.notify() # report progress made Progress.set_progress(request, n) # commit changes message = '<em>%d</em> %s created.' % ( n, self.model._meta.verbose_name_plural) change = request.changelog.commit(message, model=self.model) Progress.stop(request) # ajax operation, simply return success and message # information if request.is_ajax(): return to_json_response({ 'success': True, 'message': message, 'change': change, 'next': self._get_url(request, 'index', namespace=True), 'instance_id': first_instance.pk, 'instance_title': unicode(first_instance), }) # dialog or screen? if request.GET.get('create', 'false') == 'true': return { 'dialog_created_id': instance.pk, 'dialog_created_title': unicode(instance) } else: return self._redirect(request, 'index') elif request.is_ajax(): return to_json_response({ 'success': False, 'errors': form.errors }) context = { 'form': form, 'permissions': { 'create': self.user_has_permission(request.user, 'add'), 'view': self.user_has_permission(request.user, 'view'), 'edit': self.user_has_permission(request.user, 'edit') }, 'verbose_name': 'Multiple Media' } else: if request.method == 'POST': Progress.start(request, 1) context = super(MediaView, self)._create_edit(request, pk, edit, duplicate) if isinstance(context, dict): context['is_images'] = isinstance(self, ImageView) return context def download(self, request): """ Download individual or multiple media assets. Multiple media assets are zipped. """ # get pks pks = get_pks(request.GET) # get list of media media = Media.objects.filter(pk__in=pks).order_by('caption') n_media = media.count() # no media? if n_media == 0: raise Http404( 'Unable to export media assets for empty list of media objects.' ) # single media item? if n_media == 1: item = media.first() response = FileResponse(open(item.original_path, 'rb')) response[ 'Content-Disposition'] = 'attachment; filename="%s"' % item.filename return response # multiple assets -> create zip file f = NamedTemporaryFile() zf = zipfile.ZipFile(f, 'w') # attach original files to zip, handle duplicated filenames filenames = {} for item in media: # determine unique filename filename = item.filename if filename not in filenames: filenames[filename] = 1 else: fn = filename base, ext = os.path.splitext(filename) while fn in filenames: filenames[fn] += 1 fn = '%s-%d%s' % (base, filenames[fn], ext) filename = fn filenames[filename] = 1 # attach file to zip... zf.write(item.original_path, filename) zf.close() f.seek(0) # determine site name from settings (CMS) if 'cubane.cms' in settings.INSTALLED_APPS: from cubane.cms.views import get_cms_settings site_name = get_cms_settings().name if site_name is None: site_name = '' site_name = slugify(site_name) if site_name != '': site_name += '_' else: site_name = '' # generate download filename for zip file today = datetime.date.today() filename = '{0}media_{1:02d}_{2:02d}_{3:04d}.zip'.format( site_name, today.day, today.month, today.year) # serve file response = FileResponse(f) response[ 'Content-Disposition'] = 'attachment; filename="%s"' % filename return response def share(self, request): # get media asset pk = request.GET.get('pk') media = get_object_or_404(Media, pk=pk) # create form if request.method == 'POST': form = MediaShareForm(request.POST) else: share_filename = media.share_filename if not share_filename: share_filename = media.filename form = MediaShareForm( initial={ 'share_enabled': media.share_enabled, 'share_filename': share_filename }) # configure form form.configure(request, instance=media, edit=True) # validate form if request.method == 'POST' and form.is_valid(): d = form.cleaned_data if media.share_enabled != d.get( 'share_enabled') or media.share_filename != d.get( 'share_filename'): previous_instance = request.changelog.get_changes(media) media.share_enabled = d.get('share_enabled') media.share_filename = d.get('share_filename') media.save() request.changelog.edit(media, previous_instance) request.changelog.commit( 'File sharing %s: <em>%s</em>.' % ('enabled' if media.share_enabled else 'disabled', media.caption), media, flash=True) return self._redirect(request, 'index') return { 'form': form, 'media': media, 'base_url': make_absolute_url(settings.MEDIA_DOWNLOAD_URL) }
class VarietyView(ModelView): """ Editing varieties. """ template_path = 'cubane/ishop/merchant/varieties/' namespace = 'cubane.ishop.varieties' folder_model = Variety model = Variety form = VarietyForm patterns = [ view_url(r'options/', 'options', name='options'), view_url(r'products/', 'products', name='products'), view_url(r'update\-seq/(?P<variety_id>\d+)/', 'update_seq', name='update_seq'), ] listing_actions = [ ('[Options]', 'options', 'single'), ('[Products]', 'products', 'single'), ] shortcut_actions = [ 'options', 'products' ] def _get_objects(self, request): return self.model.objects.prefetch_related( Prefetch('options', queryset=VarietyOption.objects.order_by('seq')) ).annotate( num_products=Count('options__assignments__product', distinct=True) ) def _get_objects_for_seq(self, request): return self.model.objects.all() def _get_folders(self, request, parent): folders = self.folder_model.objects.all() if parent: folders = folders.filter(parent=parent) return folders def options(self, request): # load variety variety_id = request.GET.get('pk') variety = get_object_or_404(Variety, pk=variety_id) # load import variety import_variety_id = request.GET.get('import_variety') import_variety = None if import_variety_id: try: import_variety = Variety.objects.get(pk=import_variety_id) except variety.DoesNotExist: pass # get current list of options, or if we are importing from the # given source if import_variety: options = list(import_variety.options.order_by('seq')) for option in options: option.id = None option.variety = None else: options = list(variety.options.order_by('seq', 'id')) initial = [{ 'title': option.title, 'enabled': option.enabled, 'image': option.image, 'color': option.color, 'offset_type': option.default_offset_type, 'offset_value': option.default_offset_value, 'text_label': option.text_label, 'seq': seq, '_id': option.id } for seq, option in enumerate(options, start=1)] # determine form (attribute or variety) if variety.is_attribute: form_class = VarietyAttributeFormset else: form_class = VarietyFormset # create main form if request.method == 'POST': formset = form_class(request.POST, initial=initial) else: formset = form_class(initial=initial) # form validation if request.method == 'POST': if request.POST.get('cubane_form_cancel', '0') == '1': return self._redirect(request, 'index') if formset.is_valid(): seq = 0 for form in formset.forms: d = form.cleaned_data try: option = filter(lambda o: o.id == d.get('_id'), options)[0] except IndexError: option = VarietyOption() if d.get('DELETE') == False and d.get('title'): option.title = d.get('title') option.enabled = d.get('enabled') option.image = d.get('image') option.color = d.get('color') option.seq = d.get('seq') option.text_label = d.get('text_label') option.variety = variety if option.seq is None: option.seq = seq + 1 seq += 1 if option.seq > seq: seq = option.seq if not variety.is_attribute: option.default_offset_type = d.get('offset_type') option.default_offset_value = d.get('offset_value') # get previous state if option.pk is not None: previous_instance = request.changelog.get_changes(option) else: previous_instance = None # save option option.save() # generate changelog if previous_instance: request.changelog.edit(option, previous_instance) else: request.changelog.create(option) elif d.get('DELETE') == True and option.id != None: # delete option request.changelog.delete(option) option.delete() request.changelog.commit( 'Variety Options for <em>%s</em> updated.' % variety.title, variety, flash=True ) return self.redirect_to_index_or(request, 'options', variety) # create import form if request.method == 'GET': import_form = VarietyOptionImportForm() import_form.configure(request, variety) else: import_form = None return { 'variety': variety, 'form': formset, 'import_form': import_form, 'kit_builder': settings.SHOP_ENABLE_KIT_BUILDER } def products(self, request): variety_id = request.GET.get('pk') variety = get_object_or_404(Variety, pk=variety_id) assignments = VarietyAssignment.objects.filter(variety_option__variety=variety).values('product_id').distinct() product_ids = [a.get('product_id') for a in assignments] # get products if settings.SHOP_MULTIPLE_CATEGORIES: products = get_product_model().objects.prefetch_related( Prefetch('categories', queryset=ProductCategory.objects.select_related('category').order_by('seq')) ).distinct() else: products = get_product_model().objects.select_related('category') products = products.filter(pk__in=product_ids).order_by('title') if request.method == 'POST': return self._redirect(request, 'index') return { 'variety': variety, 'products': products } def combine(self, request): variety_ids = request.GET.getlist('pks[]') # get target if len(variety_ids) > 0: target = Variety.objects.get(pk=variety_ids[0]) else: target = None # get sources sources = [] if len(variety_ids) > 1: varieties = Variety.objects.in_bulk(variety_ids[1:]) for vid in variety_ids[1:]: sources.append(varieties.get(int(vid))) # get combines options options = list(target.options.all()) for s in sources: options.extend(list(s.options.all())) if request.method == 'POST': if request.POST.get('cubane_form_cancel', '0') != '1': # build index for target labels = {} for option in target.options.all(): labels[option.title] = option # process all sources obsolete_options = [] for variety in sources: for option in variety.options.all().order_by('seq'): if option.title in labels: # already exists. Now we have to change all assignments over to point to the new one in target for assignment in VarietyAssignment.objects.filter(variety_option=option): assignment.variety_option = labels.get(option.title) assignment.save() obsolete_options.append(option) else: # does not exist yet, simply change the option over to point to the target variety option.variety = target option.save() labels[option.title] = option # remove obsolete options [option.delete() for option in obsolete_options] # remove source varieties, since they all have been combined into target. [variety.delete() for variety in sources] messages.add_message(request, messages.SUCCESS, 'Varieties Combined successfully into <em>%s</em>.' % target.title) return self._redirect(request, 'index') return { 'target': target, 'sources': sources, 'options': options } @view(require_POST) def update_seq(self, request, variety_id=None): variety = get_object_or_404(Variety, pk=variety_id) option_ids = [parse_int(x, 0) for x in request.POST.getlist('option[]')] if any(map(lambda x: x == 0, option_ids)): raise Http404('Unable to parse option id list.') options = list(variety.options.order_by('seq', 'id')) for i, oid in enumerate(option_ids, start = 1): o = [o for o in options if o.id == oid][0] o.seq = i o.save() return to_json_response({'success': True})
class InventoryView(ModelView): model = ProductSKU namespace = 'cubane.ishop.inventory' template_path = 'cubane/ishop/merchant/inventory/' patterns = [ view_url(r'inventory-import/', 'inventory_import', name='inventory_import'), view_url(r'inventory-export/', 'inventory_export', name='inventory_export'), ] listing_actions = [('Import / Export', 'inventory_import', 'any')] def _get_objects(self, request): return ProductSKU.objects.prefetch_related( Prefetch('variety_options', queryset=VarietyOption.objects.order_by('seq'))) def before_save(self, request, cleaned_data, instance, edit): """ Save barcode as SKU number """ if request.settings.sku_is_barcode: instance.sku = cleaned_data.get('barcode') def after_save(self, request, cleaned_data, instance, edit): """ Maintain variety assignments """ for variety_option in instance.variety_options.all(): try: assignment = VarietyAssignment.objects.get( product=instance.product, variety_option=variety_option) except VarietyAssignment.DoesNotExist: VarietyAssignment.objects.create(product=instance.product, variety_option=variety_option) def before_delete(self, request, instance): """ Delete variety assignments """ for variety_option in instance.variety_options.select_related( 'variety').all(): if variety_option.variety.sku: try: assignment = VarietyAssignment.objects.get( product=instance.product, variety_option=variety_option) assignment.delete() except VarietyAssignment.DoesNotExist: pass def inventory_import(self, request): """ Shop Inventory Import (CSV). """ # default encoding initial = {'encoding': get_cms_settings().default_encoding} if request.method == 'POST': form = ShopInventoryImportForm(request.POST, request.FILES) else: form = ShopInventoryImportForm(initial=initial) form.configure(request) export_form = ShopInventoryExportForm(initial=initial) export_form.configure(request) if request.method == 'POST' and form.is_valid(): d = form.cleaned_data # create importer and import date importer = ShopInventoryImporter() importer.import_from_stream(request, request.FILES['csvfile'], encoding=d.get('encoding')) # present information what happend during import if importer.has_errors: transaction.rollback() errors = importer.get_formatted_errors() messages.add_message( request, messages.ERROR, ('Import failed due to %s. No data ' + 'has been imported. Please correct all issues ' + 'and try again.') % pluralize(len(errors), 'error', tag='em')) for message in errors: messages.add_message(request, messages.ERROR, message) # redirect to itself if we have errors return HttpResponseRedirect( get_absolute_url( 'cubane.ishop.inventory.inventory_import')) else: # success message, render image processing page messages.add_message( request, messages.SUCCESS, pluralize(importer.num_records_processed, 'record', 'processed successfully', tag='em')) # redirect to itself if we have errors return HttpResponseRedirect( get_absolute_url( 'cubane.ishop.inventory.inventory_import')) return { 'form': form, 'export_form': export_form, 'export_form_action': reverse('cubane.ishop.inventory.inventory_export') } def inventory_export(self, request): """ Shop Inventory Export (CSV). """ # create exporter and export inventory data exporter = ShopInventoryExporter() f = exporter.export_to_temp_file(encoding=request.GET.get('encoding')) # generate filename based on today's date cms_settings = get_cms_settings() today = datetime.date.today() filename = '{0}__{1:02d}_{2:02d}_{3:04d}.csv'.format( slugify(cms_settings.name).replace('-', '_'), today.day, today.month, today.year) # serve file response = FileResponse(f) response['Content-Type'] = 'text/csv' response[ 'Content-Disposition'] = 'attachment; filename="%s"' % filename return response def old_data_importer(self, request): """ Import Form. """ if request.method == 'POST': form = ShopDataImportForm(request.POST, request.FILES) else: form = ShopDataImportForm() form.configure(request) export_form = ShopInventoryExportForm() export_form.configure(request) if request.method == 'POST' and form.is_valid(): d = form.cleaned_data # load data importer import_classname = settings.CUBANE_SHOP_IMPORT.get( 'importer', 'cubane.ishop.apps.merchant.dataimport.importer.ShopDataImporter' ) import_class = get_class_from_string(import_classname) # create importer and start importing... importer = import_class(import_images=d.get('import_images')) importer.import_from_stream(request, request.FILES['csvfile'], encoding=d.get('encoding')) # present information what happend during import if importer.has_errors: transaction.rollback() errors = importer.get_formatted_errors() messages.add_message( request, messages.ERROR, ('Import failed due to %s. No data ' + 'has been imported. Please correct all issues ' + 'and try again.') % pluralize(len(errors), 'error', tag='em')) for message in errors: messages.add_message(request, messages.ERROR, message) # redirect to itself if we have errors return HttpResponseRedirect( get_absolute_url('cubane.ishop.dataimport.index')) else: # success message, render image processing page messages.add_message( request, messages.SUCCESS, pluralize(importer.num_records_processed, 'record', 'processed successfully', tag='em')) # redirect to itself if we have errors return HttpResponseRedirect( get_absolute_url('cubane.ishop.dataimport.index')) return {'form': form, 'export_form': export_form}