def render_js_extra(context, **pageargs): __M_caller = context.caller_stack._push_frame() try: def js_extra(): return render_js_extra(context) settings = context.get("settings", UNDEFINED) __M_writer = context.writer() # SOURCE LINE 57 __M_writer( u"\n <script type=\"text/javascript\">\n $(function() {\n\n var view_name = 'view-login';\n\n // adding js class for styling with accessibility in mind\n $('body').addClass('js').addClass(view_name);\n\n // new window/tab opening\n $('a[rel=\"external\"], a[class=\"new-vp\"]')\n .click( function() {\n window.open( $(this).attr('href') );\n return false;\n });\n\n // form field label styling on focus\n $(\"form :input\").focus(function() {\n $(\"label[for='\" + this.id + \"']\").parent().addClass(\"is-focused\");\n }).blur(function() {\n $(\"label\").parent().removeClass(\"is-focused\");\n });\n });\n\n (function() {\n var urlParams = {};\n var searchString = window.location.search.substring(1);\n if (searchString !== undefined) {\n var pairs = searchString.split('&');\n for (var i = 0; i < pairs.length; i++) {\n var values = pairs[i].split('=');\n urlParams[values[0]] = values[1] === undefined ? false : decodeURIComponent(values[1]);\n }\n }\n\n if (localStorage.pepperpd_login_email)\n {\n $(\"#email\").val(localStorage.pepperpd_login_email);\n }\n toggleSubmitButton(true);\n\n $('#login-form').on('submit', function() {\n toggleSubmitButton(false);\n });\n\n $('#login-form').on('ajax:error', function() {\n toggleSubmitButton(true);\n });\n\n $('#login-form').on('ajax:success', function(event, json, xhr) {\n if (json.success) {\n if ($('#remember-yes').attr(\"checked\"))\n {\n localStorage.pepperpd_login_email=$(\"#email\").val();\n }\n\n if (urlParams['next'] !== undefined && !isExternal(urlParams['next']) && urlParams['next'].match(/^course.+courseware/)) {\n var next = urlParams['next'];\n if (next.substring(0,1) != '/') {\n next = '/' + next;\n }\n location.href = next;\n } else if (urlParams['next'] !== undefined && !isExternal(urlParams['next']) && urlParams['next'].match(/^\\/sso\\/idp\\/auth/)) {\n location.href = urlParams['next'];\n } else {\n location.href = \"" ) # SOURCE LINE 122 __M_writer(filters.decode.utf8(reverse("dashboard"))) __M_writer( u"\";\n }\n\n } else {\n toggleSubmitButton(true);\n $('.message.submission-error').addClass('is-shown').focus();\n $('.message.submission-error .message-copy').html(json.value);\n }\n });\n })(this);\n\n function toggleSubmitButton(enable) {\n var $submitButton = $('form .form-actions #submit');\n\n\n if(enable) {\n $submitButton.\n removeClass('is-disabled').\n removeProp('disabled').\n\n <!--@begin:modify LogIn -> Log In-->\n <!--@date:2013-11-02-->\n html(\"" ) # SOURCE LINE 144 __M_writer(filters.decode.utf8(_("Log In").format(platform_name=settings.PLATFORM_NAME))) __M_writer(u" <span class='orn-plus'>&</span> ") __M_writer(filters.decode.utf8(_("Access My Courses"))) __M_writer( u"\");\n <!--@end-->\n } else {\n $submitButton.\n addClass('is-disabled').\n prop('disabled', true).\n html(gettext('Processing your account information …'));\n }\n\n }\n\n//------------- Clear time storage-------------------//\n\n function clearTimeSessionStorage(){\n var user_id = sessionStorage.getItem('user_id');\n if(user_id != null){\n sessionStorage.removeItem(user_id + '_course_id');\n sessionStorage.removeItem(user_id + '_vertical_id');\n sessionStorage.removeItem(user_id + '_type');\n sessionStorage.removeItem(user_id + '_time');\n sessionStorage.removeItem('user_id');\n }\n }\n\n clearTimeSessionStorage();\n\n </script>\n" ) return "" finally: context.caller_stack._pop_frame()
def create_saved_search(request, database_name=settings.MONGO_DB_NAME, collection_name=settings.MONGO_MASTER_COLLECTION, skip=0, limit=200, return_keys=()): name = _("Create a Saved Search") if request.method == 'POST': form = SavedSearchForm(request.POST) if form.is_valid(): ss = form.save(commit = False) ss.user = request.user ss.save() return HttpResponseRedirect(reverse('saved_searches')) else: #The form is invalid messages.error(request,_("Please correct the errors in the form.")) return render_to_response('generic/bootstrapform.html', {'form': form, 'name':name, }, RequestContext(request)) #this is a GET idata ={'database_name': database_name, 'collection_name': collection_name} context= {'name':name, 'form': SavedSearchForm(initial=idata) } return render_to_response('generic/bootstrapform.html', RequestContext(request, context,))
def handle(self, *args, **options): message = options.get('message') verbosity = options.get('verbosity') get_template('templated_email/notice.email') if not message: raise CommandError(_('message must be included in options')) # get all users users = User.objects.all() for user in users: name = user.get_full_name() if not name or len(name) == 0: name = user.email if verbosity: self.stdout.write(_( 'Emailing name: %(name)s, email: %(email)s') % {'name': name, 'email': user.email}) # send each email separately so users cannot see eachother send_templated_mail( template_name='notice', from_email='*****@*****.**', recipient_list=[user.email], context={ 'username': user.username, 'full_name': name, 'message': message }, )
def form_valid(self, form): cg = self.contactgroup request = self.request if not cg.userperms & perms.WRITE_FILES: raise PermissionDenied upfile = request.FILES['file_to_upload'] # name has already been sanitized by UploadedFile._set_name path = self.kwargs['path'] fullfilename = cg.get_fullfilename(os.path.join(path, upfile.name)) destination = None try: # We're not using "with" because we want to show errors destination = open(force_str(fullfilename), 'wb') for chunk in upfile.chunks(): destination.write(chunk) messages.add_message( request, messages.SUCCESS, _('File {} has been uploaded successfully.') .format(upfile.name)) except IOError as err: messages.add_message( request, messages.ERROR, _('Could not upload file {filename}: {error}').format( filename=upfile.name, error=err)) finally: if destination: destination.close() return self.get(self.request)
def clean(self): self.handle_deleted_and_added_contributions() super().clean() found_contributor = set() count_responsible = 0 for form in self.forms: if not form.cleaned_data or form.cleaned_data.get('DELETE'): continue contributor = form.cleaned_data.get('contributor') if contributor is None: raise forms.ValidationError(_('Please select the name of each added contributor. Remove empty rows if necessary.')) if contributor and contributor in found_contributor: raise forms.ValidationError(_('Duplicate contributor found. Each contributor should only be used once.')) elif contributor: found_contributor.add(contributor) if form.cleaned_data.get('responsibility') == 'RESPONSIBLE': count_responsible += 1 if count_responsible < 1: raise forms.ValidationError(_('No responsible contributor found. Each course must have exactly one responsible contributor.')) elif count_responsible > 1: raise forms.ValidationError(_('Too many responsible contributors found. Each course must have exactly one responsible contributor.'))
def crop_and_upload_image_to_popit(self, image_filename, crop_bounds, moderator_why_allowed, make_primary): original = PillowImage.open(image_filename) # Some uploaded images are CYMK, which gives you an error when # you try to write them as PNG, so convert to RGBA (this is # RGBA rather than RGB so that any alpha channel (transparency) # is preserved). person_id = self.queued_image.person.id person_extra = PersonExtra.objects.get(base__id=person_id) original = original.convert('RGBA') cropped = original.crop(crop_bounds) ntf = NamedTemporaryFile(delete=False) cropped.save(ntf.name, 'PNG') md5sum = get_file_md5sum(ntf.name) filename = str(person_id) + '.png' source = _( 'Uploaded by {uploaded_by}: Approved from photo moderation queue' ).format(uploaded_by=self.queued_image.user.username) ImageExtra.objects.create_from_file( ntf.name, join('images', filename), base_kwargs={ 'source': source, 'is_primary': make_primary, 'content_object': person_extra, }, extra_kwargs={ 'md5sum': md5sum, 'uploading_user': self.queued_image.user, 'user_notes': self.queued_image.justification_for_use, 'copyright': moderator_why_allowed, 'user_copyright': self.queued_image.why_allowed, 'notes': _('Approved from photo moderation queue'), }, )
def get_data(self): # Gather our instances try: instances = api.server_list(self.request) except: instances = [] exceptions.handle(self.request, _('Unable to retrieve instances.')) # Gather our flavors and correlate our instances to them if instances: try: flavors = api.flavor_list(self.request) except: flavors = [] exceptions.handle(self.request, ignore=True) full_flavors = SortedDict([(str(flavor.id), flavor) for flavor in flavors]) # Loop through instances to get flavor info. for instance in instances: try: flavor_id = instance.flavor["id"] if flavor_id in full_flavors: instance.full_flavor = full_flavors[flavor_id] else: # If the flavor_id is not in full_flavors list, # get it via nova api. instance.full_flavor = api.flavor_get(self.request, flavor_id) except: msg = _('Unable to retrieve instance size information.') exceptions.handle(self.request, msg) return instances
def dump_student_extensions(course, student): """ Dumps data about the due date extensions granted for a particular student in a particular course. """ data = [] header = [_("Unit"), _("Extended Due Date")] units = get_units_with_due_date(course) units = {u.location: u for u in units} query = StudentFieldOverride.objects.filter( course_id=course.id, student=student, field='due') for override in query: location = override.location.replace(course_key=course.id) if location not in units: continue due = DATE_FIELD.from_json(json.loads(override.value)) due = due.strftime("%Y-%m-%d %H:%M") title = title_or_url(units[location]) data.append(dict(zip(header, (title, due)))) return { "header": header, "title": _("Due date extensions for {0} {1} ({2})").format( student.first_name, student.last_name, student.username), "data": data}
def handle(self, request, data): domain = api.keystone.get_default_domain(self.request) try: LOG.info('Creating user with name "%s"' % data['name']) if "email" in data: data['email'] = data['email'] or None new_user = api.keystone.user_create(request, name=data['name'], email=data['email'], password=data['password'], project=data['project'], enabled=True, domain=domain.id) messages.success(request, _('User "%s" was successfully created.') % data['name']) if data['project'] and data['role_id']: roles = api.keystone.roles_for_user(request, new_user.id, data['project']) or [] assigned = [role for role in roles if role.id == str( data['role_id'])] if not assigned: try: api.keystone.add_tenant_user_role(request, data['project'], new_user.id, data['role_id']) except Exception: exceptions.handle(request, _('Unable to add user ' 'to primary project.')) return new_user except Exception: exceptions.handle(request, _('Unable to create user.'))
def export_result(request): response = {'status': -1, 'message': _('Exporting result failed.')} # Passed by check_document_access_permission but unused by APIs notebook = json.loads(request.POST.get('notebook', '{}')) snippet = json.loads(request.POST.get('snippet', '{}')) data_format = json.loads(request.POST.get('format', 'hdfs-file')) destination = json.loads(request.POST.get('destination', '')) overwrite = json.loads(request.POST.get('overwrite', False)) api = get_api(request, snippet) if data_format == 'hdfs-file': if overwrite and request.fs.exists(destination): if request.fs.isfile(destination): request.fs.do_as_user(request.user.username, request.fs.rmtree, destination) else: raise ValidationError(_("The target path is a directory")) response['watch_url'] = api.export_data_as_hdfs_file(snippet, destination, overwrite) response['status'] = 0 elif data_format == 'hive-table': notebook_id = notebook['id'] or request.GET.get('editor', request.GET.get('notebook')) response['watch_url'] = reverse('notebook:execute_and_watch') + '?action=save_as_table¬ebook=' + str(notebook_id) + '&snippet=0&destination=' + destination response['status'] = 0 elif data_format == 'hdfs-directory': notebook_id = notebook['id'] or request.GET.get('editor', request.GET.get('notebook')) response['watch_url'] = reverse('notebook:execute_and_watch') + '?action=insert_as_query¬ebook=' + str(notebook_id) + '&snippet=0&destination=' + destination response['status'] = 0 return JsonResponse(response)
def dump_module_extensions(course, unit): """ Dumps data about students with due date extensions for a particular module, specified by 'url', in a particular course. """ data = [] header = [_("Username"), _("Full Name"), _("Extended Due Date")] query = StudentFieldOverride.objects.filter( course_id=course.id, location=unit.location, field='due') for override in query: due = DATE_FIELD.from_json(json.loads(override.value)) due = due.strftime("%Y-%m-%d %H:%M") fullname = override.student.profile.name data.append(dict(zip( header, (override.student.username, fullname, due)))) data.sort(key=lambda x: x[header[0]]) return { "header": header, "title": _("Users with due date extensions for {0}").format( title_or_url(unit)), "data": data }
def clean_shipping_percentage(self): if self.value > 100: raise ValidationError(_("Percentage discount cannot be greater than 100")) if self.range: raise ValidationError(_("No range should be selected as this benefit does not " "apply to products")) if self.max_affected_items: raise ValidationError(_("Shipping discounts don't require a 'max affected items' " "attribute"))
def merge_path(request): """ Path merging view """ response = {} if request.method == 'POST': try: ids_path_merge = request.POST.getlist('path[]') if len(ids_path_merge) == 2: path_a = Path.objects.get(pk=ids_path_merge[0]) path_b = Path.objects.get(pk=ids_path_merge[1]) if not path_a.same_structure(request.user) or not path_b.same_structure(request.user): response = {'error': _(u"You don't have the right to change these paths")} elif path_a.merge_path(path_b): response = {'success': _(u"Paths merged successfully")} messages.success(request, _(u"Paths merged successfully")) else: response = {'error': _(u"No matching points to merge paths found")} else: raise except Exception as exc: response = {'error': exc, } return HttpResponse(json.dumps(response), mimetype="application/json")
def clean_multibuy(self): if not self.range: raise ValidationError(_("Multibuy benefits require a product range")) if self.value: raise ValidationError(_("Multibuy benefits don't require a value")) if self.max_affected_items: raise ValidationError(_("Multibuy benefits don't require a 'max affected items' " "attribute"))
def clean_shipping_absolute(self): if not self.value: raise ValidationError(_("A discount value is required")) if self.range: raise ValidationError(_("No range should be selected as this benefit does not " "apply to products")) if self.max_affected_items: raise ValidationError(_("Shipping discounts don't require a 'max affected items' " "attribute"))
def clean_destination(self): create = self.cleaned_data.get("create") dest = self.cleaned_data.get("destination") dest = os.path.abspath(dest.strip().replace("..", "")) if not self.jail: jail = self.cleaned_data.get("jail") if jail: self.jail = Jails.objects.get(jail_host=jail) if not self.jail: raise forms.ValidationError( _("This shouldn't happen, but the jail could not be found") ) full = "%s/%s%s" % (self.jc.jc_path, self.jail.jail_host, dest) if len(full) > 88: raise forms.ValidationError( _("The full path cannot exceed 88 characters") ) if not os.path.exists(full): os.makedirs(full) return dest
def get_visibility_errors(self, customer): if self.product.deleted: yield ValidationError(_('This product has been deleted.'), code="product_deleted") if customer and customer.is_all_seeing: # None of the further conditions matter for omniscient customers. return if not self.visible: yield ValidationError(_('This product is not visible.'), code="product_not_visible") is_logged_in = (bool(customer) and not customer.is_anonymous) if not is_logged_in and self.visibility_limit != ProductVisibility.VISIBLE_TO_ALL: yield ValidationError( _('The Product is invisible to users not logged in.'), code="product_not_visible_to_anonymous") if is_logged_in and self.visibility_limit == ProductVisibility.VISIBLE_TO_GROUPS: # TODO: Optimization user_groups = set(customer.groups.all().values_list("pk", flat=True)) my_groups = set(self.visibility_groups.values_list("pk", flat=True)) if not bool(user_groups & my_groups): yield ValidationError( _('This product is not visible to your group.'), code="product_not_visible_to_group" ) for receiver, response in get_visibility_errors.send(ShopProduct, shop_product=self, customer=customer): for error in response: yield error
def check_server_disallowed(self): """ Check if server domain name or IP is disallowed in settings.py. """ hostname = self.netloc_parts[2].lower() if (hasattr(settings, 'DISALLOWED_DOMAIN_LIST') and settings.DISALLOWED_DOMAIN_LIST): for domain in settings.DISALLOWED_DOMAIN_LIST: if hostname == domain or hostname.endswith('.' + domain): raise ValidationError(unicode( _("Domain name %(domain)s is disallowed.") % locals())) try: ip = socket.gethostbyname(hostname) except socket.error: raise ValidationError(unicode( _("Could not resolve IP address for %(hostname)s.") % locals())) if (not hasattr(settings, 'DISALLOWED_SERVER_IP_LIST') or not settings.DISALLOWED_SERVER_IP_LIST): return server = long_ip(ip) # print 'server', server, dotted_ip(server), ip for disallowed in settings.DISALLOWED_SERVER_IP_LIST: disallowed = disallowed.strip() if disallowed == '' or disallowed.startswith('#'): continue mask = bit_mask(32) if '/' in disallowed: disallowed, bits = disallowed.split('/', 1) mask = slash_mask(int(bits)) identifier = long_ip(disallowed) & mask masked = server & mask if masked == identifier: raise ValidationError(unicode( _("Server IP address %(ip)s is disallowed.") % locals()))
def __init__(self, *args, **kwargs): super(ReservationCrispyFormModelForm,self).__init__(*args, **kwargs) self.helper = FormHelper(self) self.helper.form_action = "" self.helper.form_method = "POST" self.helper.form_class = 'form-horizontal' self.helper.label_class = 'col-sm-3' self.helper.field_class = 'col-sm-4' self.fields["acceptation"].widget = forms.RadioSelect() # delete empty choice for the type del self.fields["acceptation"].choices[0] self.helper.layout = layout.Layout( layout.Fieldset( _("Main data"), Field("acceptation", css_class="input-block-level"), _("Renseignement"), Field("firstname", css_class="input-block-level"), Field("lastname", css_class="input-block-level"), Field("telephone", css_class="input-block-level"), Field("accompagnement", css_class="input-block-level"), Field("notes", css_class="input-block-level", rows="3"), ), FormActions( Submit("submit", _("Save")), Button('cancel', 'Cancelar', onclick="window.location.href='/';") ) )
def __init__(self, *args, **kwargs): super(SummaryField, self).__init__(*args, **kwargs) self.required = False self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) self.max_length = 300 self.label = _('update summary:') self.help_text = _('enter a brief summary of your revision (e.g. fixed spelling, grammar, improved style, this field is optional)')
def get_size(instance): if hasattr(instance, "full_flavor"): size_string = _("%(name)s | %(RAM)s RAM") vals = {'name': instance.full_flavor.name, 'RAM': sizeformat.mbformat(instance.full_flavor.ram)} return size_string % vals return _("Not available")
def __init__(self, *args, **kwargs): from servo.lib.utils import empty super(GsxRepairForm, self).__init__(*args, **kwargs) repair = self.instance techs = User.techies.filter(location=repair.order.location) c = [(u.tech_id, u.get_full_name()) for u in techs] c.insert(0, ('', '-------------------',)) self.fields['tech_id'] = forms.ChoiceField(choices=c, required=False, label=_('Technician')) self.fields['parts'].initial = repair.order.get_parts() if repair.can_mark_complete is False: del(self.fields['mark_complete']) del(self.fields['replacement_sn']) choices = Template.templates() for f in ('notes', 'symptom', 'diagnosis',): self.fields[f].widget = AutocompleteTextarea(choices=choices) symptom_codes = self.instance.get_symptom_code_choices() self.fields['symptom_code'] = forms.ChoiceField(choices=symptom_codes, label=_('Symptom group')) if empty(self.instance.symptom_code): # default to the first choice self.instance.symptom_code = symptom_codes[0][0] issue_codes = self.instance.get_issue_code_choices() self.fields['issue_code'] = forms.ChoiceField(choices=issue_codes, label=_('Issue code'))
def confirm(self, ok_func, *msgs): """Execute the specified callable `ok_func` after the user has confirmed the specified message. The confirmation message may be specified as a series of positional arguments which will be concatenated to a single prompt. The callable will be called with a single positional argument which will be the action request that confirmed the message. In a web context this will be another object than this one. In a non-interactive environment the `ok_func` function is called directly (i.e. we don't ask any confirmation and act as confirmation had been given). """ cb = self.add_callback(*msgs) def noop(ar): return ar.success(_("Aborted")) cb.add_choice('yes', ok_func, _("Yes")) cb.add_choice('no', noop, _("No")) self.set_callback(cb) if not self.renderer.is_interactive: if self._confirm_answer: ok_func(self)
def get_human_hour(self): if self.scrub_hour == '*': return _(u'Every hour') elif self.scrub_hour.startswith('*/'): return _(u'Every {0} hour(s)').format(self.scrub_hour.split('*/')[1]) else: return self.scrub_hour
def get_human_daymonth(self): if self.scrub_daymonth == '*': return _(u'Everyday') elif self.scrub_daymonth.startswith('*/'): return _(u'Every {0} days').format(self.scrub_daymonth.split('*/')[1]) else: return self.scrub_daymonth
def __init__(self, path, raw_file_url=None, username=None, password=None, local_site_name=None): if not is_exe_in_path('git'): # This is technically not the right kind of error, but it's the # pattern we use with all the other tools. raise ImportError self.path = self._normalize_git_url(path) self.raw_file_url = raw_file_url self.username = username self.password = password self.local_site_name = local_site_name self.git_dir = None url_parts = urlparse.urlparse(self.path) if url_parts[0] == 'file': self.git_dir = url_parts[2] p = self._run_git(['--git-dir=%s' % self.git_dir, 'config', 'core.repositoryformatversion']) failure = p.wait() if failure: # See if we have a permissions error if not os.access(self.git_dir, os.R_OK): raise SCMError(_("Permission denied accessing the local " "Git repository '%s'") % self.git_dir) else: raise SCMError(_('Unable to retrieve information from ' 'local Git repository'))
def get_human_minute(self): if self.scrub_minute == '*': return _(u'Every minute') elif self.scrub_minute.startswith('*/'): return _(u'Every {0} minute(s)').format(self.scrub_minute.split('*/')[1]) else: return self.scrub_minute
def select_variant_from_properties(request): """ This is called via an ajax call if the combination of properties are changed. """ product_id = request.POST.get("product_id") try: variant = Product.objects.get(pk=product_id) except Product.DoesNotExist: return HttpResponse("") else: product = variant.parent options = lfs_utils.parse_properties(request) variant = product.get_variant(options) if variant is None: msg = _(u"The choosen combination of properties is not deliverable.") variant = product.get_default_variant() else: msg = _(u"The product has been changed according to your selection.") result = simplejson.dumps({ "product": product_inline(request, variant), "message": msg, }, cls=LazyEncoder) return HttpResponse(result, mimetype='application/json')
def video_handler(request, video, format="mp4", prev=None, next=None): if not video["available"]: if request.is_admin: # TODO(bcipolli): add a link, with querystring args that auto-checks this video in the topic tree messages.warning(request, _("This video was not found! You can download it by going to the Update page.")) elif request.is_logged_in: messages.warning(request, _("This video was not found! Please contact your teacher or an admin to have it downloaded.")) elif not request.is_logged_in: messages.warning(request, _("This video was not found! You must login as an admin/teacher to download the video.")) # Fallback mechanism available_urls = dict([(lang, avail) for lang, avail in video["availability"].iteritems() if avail["on_disk"]]) if video["available"] and not available_urls: vid_lang = "en" messages.success(request, "Got video content from %s" % video["availability"]["en"]["stream"]) else: vid_lang = select_best_available_language(request.language, available_codes=available_urls.keys()) context = { "video": video, "title": video["title"], "selected_language": vid_lang, "video_urls": video["availability"].get(vid_lang), "subtitle_urls": video["availability"].get(vid_lang, {}).get("subtitles"), "prev": prev, "next": next, "backup_vids_available": bool(settings.BACKUP_VIDEO_SOURCE), "use_mplayer": settings.USE_MPLAYER and is_loopback_connection(request), } return context
def object_action(self, form, instance): try: if instance.is_superuser or instance.is_staff: messages.error( self.request, _( 'Super user and staff user password ' 'reseting is not allowed, use the admin ' 'interface for these cases.' ) ) else: instance.set_password(form.cleaned_data['new_password_1']) instance.save() messages.success( self.request, _( 'Successfull password reset for user: %s.' ) % instance ) except Exception as exception: messages.error( self.request, _( 'Error reseting password for user "%(user)s": %(error)s' ) % { 'user': instance, 'error': exception } )
class Meta: abstract = True app_label = 'order' verbose_name = _("Order Discount") verbose_name_plural = _("Order Discounts")
def __str__(self): return _("'%(type)s' event for order #%(number)s") \ % {'type': self.event_type.name, 'number': self.order.number}
class Meta: abstract = True app_label = 'order' verbose_name = _("Communication Event") verbose_name_plural = _("Communication Events") ordering = ['-date_created']
class Meta: abstract = True app_label = 'order' verbose_name = _("Order Note") verbose_name_plural = _("Order Notes")
class Meta: abstract = True app_label = 'order' ordering = ['-date_placed'] verbose_name = _("Order") verbose_name_plural = _("Orders")
class AbstractOrder(models.Model): """ The main order model """ number = models.CharField(_("Order number"), max_length=128, db_index=True, unique=True) # We track the site that each order is placed within site = models.ForeignKey('sites.Site', verbose_name=_("Site"), null=True, on_delete=models.SET_NULL) basket = models.ForeignKey('basket.Basket', verbose_name=_("Basket"), null=True, blank=True, on_delete=models.SET_NULL) # Orders can be placed without the user authenticating so we don't always # have a customer ID. user = models.ForeignKey(AUTH_USER_MODEL, related_name='orders', null=True, blank=True, verbose_name=_("User"), on_delete=models.SET_NULL) # Billing address is not always required (eg paying by gift card) billing_address = models.ForeignKey('order.BillingAddress', null=True, blank=True, verbose_name=_("Billing Address"), on_delete=models.SET_NULL) # Total price looks like it could be calculated by adding up the # prices of the associated lines, but in some circumstances extra # order-level charges are added and so we need to store it separately currency = models.CharField(_("Currency"), max_length=12, default=get_default_currency) total_incl_tax = models.DecimalField(_("Order total (inc. tax)"), decimal_places=2, max_digits=12) total_excl_tax = models.DecimalField(_("Order total (excl. tax)"), decimal_places=2, max_digits=12) # Shipping charges shipping_incl_tax = models.DecimalField(_("Shipping charge (inc. tax)"), decimal_places=2, max_digits=12, default=0) shipping_excl_tax = models.DecimalField(_("Shipping charge (excl. tax)"), decimal_places=2, max_digits=12, default=0) # Not all lines are actually shipped (such as downloads), hence shipping # address is not mandatory. shipping_address = models.ForeignKey('order.ShippingAddress', null=True, blank=True, verbose_name=_("Shipping Address"), on_delete=models.SET_NULL) shipping_method = models.CharField(_("Shipping method"), max_length=128, blank=True) # Identifies shipping code shipping_code = models.CharField(blank=True, max_length=128, default="") # Use this field to indicate that an order is on hold / awaiting payment status = models.CharField(_("Status"), max_length=100, blank=True) guest_email = models.EmailField(_("Guest email address"), blank=True) # Index added to this field for reporting date_placed = models.DateTimeField(db_index=True) #: Order status pipeline. This should be a dict where each (key, value) #: #: corresponds to a status and a list of possible statuses that can follow #: that one. pipeline = getattr(settings, 'OSCAR_ORDER_STATUS_PIPELINE', {}) #: Order status cascade pipeline. This should be a dict where each (key, #: value) pair corresponds to an *order* status and the corresponding #: *line* status that needs to be set when the order is set to the new #: status cascade = getattr(settings, 'OSCAR_ORDER_STATUS_CASCADE', {}) @classmethod def all_statuses(cls): """ Return all possible statuses for an order """ return list(cls.pipeline.keys()) def available_statuses(self): """ Return all possible statuses that this order can move to """ return self.pipeline.get(self.status, ()) def set_status(self, new_status): """ Set a new status for this order. If the requested status is not valid, then ``InvalidOrderStatus`` is raised. """ if new_status == self.status: return old_status = self.status if new_status not in self.available_statuses(): raise exceptions.InvalidOrderStatus( _("'%(new_status)s' is not a valid status for order %(number)s" " (current status: '%(status)s')") % { 'new_status': new_status, 'number': self.number, 'status': self.status }) self.status = new_status if new_status in self.cascade: for line in self.lines.all(): line.status = self.cascade[self.status] line.save() self.save() # Send signal for handling status changed order_status_changed.send( sender=self, order=self, old_status=old_status, new_status=new_status, ) set_status.alters_data = True @property def is_anonymous(self): # It's possible for an order to be placed by a customer who then # deletes their profile. Hence, we need to check that a guest email is # set. return self.user is None and bool(self.guest_email) @property def basket_total_before_discounts_incl_tax(self): """ Return basket total including tax but before discounts are applied """ total = D('0.00') for line in self.lines.all(): total += line.line_price_before_discounts_incl_tax return total @property def basket_total_before_discounts_excl_tax(self): """ Return basket total excluding tax but before discounts are applied """ total = D('0.00') for line in self.lines.all(): total += line.line_price_before_discounts_excl_tax return total @property def basket_total_incl_tax(self): """ Return basket total including tax """ return self.total_incl_tax - self.shipping_incl_tax @property def basket_total_excl_tax(self): """ Return basket total excluding tax """ return self.total_excl_tax - self.shipping_excl_tax @property def total_before_discounts_incl_tax(self): return (self.basket_total_before_discounts_incl_tax + self.shipping_incl_tax) @property def total_before_discounts_excl_tax(self): return (self.basket_total_before_discounts_excl_tax + self.shipping_excl_tax) @property def total_discount_incl_tax(self): """ The amount of discount this order received """ discount = D('0.00') for line in self.lines.all(): discount += line.discount_incl_tax return discount @property def total_discount_excl_tax(self): discount = D('0.00') for line in self.lines.all(): discount += line.discount_excl_tax return discount @property def total_tax(self): return self.total_incl_tax - self.total_excl_tax @property def num_lines(self): return self.lines.count() @property def num_items(self): """ Returns the number of items in this order. """ num_items = 0 for line in self.lines.all(): num_items += line.quantity return num_items @property def shipping_tax(self): return self.shipping_incl_tax - self.shipping_excl_tax @property def shipping_status(self): """Return the last complete shipping event for this order.""" # As safeguard against identical timestamps, also sort by the primary # key. It's not recommended to rely on this behaviour, but in practice # reasonably safe if PKs are not manually set. events = self.shipping_events.order_by('-date_created', '-pk').all() if not len(events): return '' # Collect all events by event-type event_map = OrderedDict() for event in events: event_name = event.event_type.name if event_name not in event_map: event_map[event_name] = [] event_map[event_name].extend(list(event.line_quantities.all())) # Determine last complete event status = _("In progress") for event_name, event_line_quantities in event_map.items(): if self._is_event_complete(event_line_quantities): return event_name return status @property def has_shipping_discounts(self): return len(self.shipping_discounts) > 0 @property def shipping_before_discounts_incl_tax(self): # We can construct what shipping would have been before discounts by # adding the discounts back onto the final shipping charge. total = D('0.00') for discount in self.shipping_discounts: total += discount.amount return self.shipping_incl_tax + total def _is_event_complete(self, event_quantities): # Form map of line to quantity event_map = {} for event_quantity in event_quantities: line_id = event_quantity.line_id event_map.setdefault(line_id, 0) event_map[line_id] += event_quantity.quantity for line in self.lines.all(): if event_map.get(line.pk, 0) != line.quantity: return False return True class Meta: abstract = True app_label = 'order' ordering = ['-date_placed'] verbose_name = _("Order") verbose_name_plural = _("Orders") def __str__(self): return u"#%s" % (self.number, ) def verification_hash(self): key = '%s%s' % (self.number, settings.SECRET_KEY) hash = hashlib.md5(key.encode('utf8')) return hash.hexdigest() @property def email(self): if not self.user: return self.guest_email return self.user.email @property def basket_discounts(self): # This includes both offer- and voucher- discounts. For orders we # don't need to treat them differently like we do for baskets. return self.discounts.filter(category=AbstractOrderDiscount.BASKET) @property def shipping_discounts(self): return self.discounts.filter(category=AbstractOrderDiscount.SHIPPING) @property def post_order_actions(self): return self.discounts.filter(category=AbstractOrderDiscount.DEFERRED) def set_date_placed_default(self): if self.date_placed is None: self.date_placed = now() def save(self, *args, **kwargs): # Ensure the date_placed field works as it auto_now_add was set. But # this gives us the ability to set the date_placed explicitly (which is # useful when importing orders from another system). self.set_date_placed_default() super(AbstractOrder, self).save(*args, **kwargs)
def __str__(self): return _("Discount of %(amount)r from order %(order)s") % { 'amount': self.amount, 'order': self.order }
class Meta: abstract = True app_label = 'order' verbose_name = _("Shipping Event Type") verbose_name_plural = _("Shipping Event Types") ordering = ('name', )
class Meta: app_label = 'order' verbose_name = _("Shipping Event Quantity") verbose_name_plural = _("Shipping Event Quantities") unique_together = ('event', 'line')
class AbstractOrderDiscount(models.Model): """ A discount against an order. Normally only used for display purposes so an order can be listed with discounts displayed separately even though in reality, the discounts are applied at the line level. This has evolved to be a slightly misleading class name as this really track benefit applications which aren't necessarily discounts. """ order = models.ForeignKey('order.Order', on_delete=models.CASCADE, related_name="discounts", verbose_name=_("Order")) # We need to distinguish between basket discounts, shipping discounts and # 'deferred' discounts. BASKET, SHIPPING, DEFERRED = "Basket", "Shipping", "Deferred" CATEGORY_CHOICES = ( (BASKET, _(BASKET)), (SHIPPING, _(SHIPPING)), (DEFERRED, _(DEFERRED)), ) category = models.CharField(_("Discount category"), default=BASKET, max_length=64, choices=CATEGORY_CHOICES) offer_id = models.PositiveIntegerField(_("Offer ID"), blank=True, null=True) offer_name = models.CharField(_("Offer name"), max_length=128, db_index=True, blank=True) voucher_id = models.PositiveIntegerField(_("Voucher ID"), blank=True, null=True) voucher_code = models.CharField(_("Code"), max_length=128, db_index=True, blank=True) frequency = models.PositiveIntegerField(_("Frequency"), null=True) amount = models.DecimalField(_("Amount"), decimal_places=2, max_digits=12, default=0) # Post-order offer applications can return a message to indicate what # action was taken after the order was placed. message = models.TextField(blank=True) @property def is_basket_discount(self): return self.category == self.BASKET @property def is_shipping_discount(self): return self.category == self.SHIPPING @property def is_post_order_action(self): return self.category == self.DEFERRED class Meta: abstract = True app_label = 'order' verbose_name = _("Order Discount") verbose_name_plural = _("Order Discounts") def save(self, **kwargs): if self.offer_id and not self.offer_name: offer = self.offer if offer: self.offer_name = offer.name if self.voucher_id and not self.voucher_code: voucher = self.voucher if voucher: self.voucher_code = voucher.code super(AbstractOrderDiscount, self).save(**kwargs) def __str__(self): return _("Discount of %(amount)r from order %(order)s") % { 'amount': self.amount, 'order': self.order } @property def offer(self): Offer = get_model('offer', 'ConditionalOffer') try: return Offer.objects.get(id=self.offer_id) except Offer.DoesNotExist: return None @property def voucher(self): Voucher = get_model('voucher', 'Voucher') try: return Voucher.objects.get(id=self.voucher_id) except Voucher.DoesNotExist: return None def description(self): if self.voucher_code: return self.voucher_code return self.offer_name or u""
class User(AbstractBaseUser, PermissionsMixin): email = models.EmailField( verbose_name=_('email address'), max_length=255, unique=True, db_index=True, ) speaker_name = models.CharField( verbose_name=_('speaker name'), max_length=100, ) bio = EAWTextField( verbose_name=_('biography'), max_length=1000, help_text=_( "Describe yourself with 500 characters or less. " "There will be no formatting." ), ) photo = models.ImageField( verbose_name=_('photo'), blank=True, default='', upload_to=photo_upload_to, ) facebook_profile_url = models.URLField( verbose_name=_('Facebook'), blank=True, help_text=format_html_lazy(_( "Link to your Facebook profile page. This will be shown when " "we display your public information. If you do not know what your " "profile page link is, click on " "<a href='https://www.facebook.com/me' target='_blank'>" "this link</a>, and copy-paste the URL of the page opened. " "Remember to log in to Facebook first!" )), ) twitter_id = models.CharField( verbose_name=_('Twitter'), blank=True, max_length=100, validators=[ RegexValidator(r'^[0-9a-zA-Z_]*$', 'Not a valid Twitter handle'), ], help_text=_( "Your Twitter handle, without the \"@\" sign. This will be " "shown when we display your public information." ), ) github_id = models.CharField( verbose_name=_('GitHub'), blank=True, max_length=100, validators=[ RegexValidator(r'^[0-9a-zA-Z_-]*$', 'Not a valid GitHub account'), ], help_text=_( "Your GitHub account, without the \"@\" sign. This will be " "shown when we display your public information." ), ) verified = models.BooleanField( verbose_name=_('verified'), default=False, help_text=_( "Designates whether the user has verified email ownership." ), ) is_staff = models.BooleanField( verbose_name=_('staff status'), default=False, help_text=_( "Designates whether the user can log into this admin site." ), ) is_active = models.BooleanField( verbose_name=_('active'), default=True, help_text=_( "Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts." ), ) date_joined = models.DateTimeField( verbose_name=_('date joined'), default=timezone.now, ) objects = UserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] class Meta: verbose_name = _('user') verbose_name_plural = _('users') swappable = 'AUTH_USER_MODEL' def __str__(self): return self.email def as_hash(self): """Return the user's hash representation. Pipeline: - pk -> md5 -> first 2 bytes -> b16 -> str. """ return '#{hash_user}'.format( hash_user=( base64.b16encode( # Picking the first 2 bytes may still run into hash # collisions if you take the whole users list into account, # but as this is used for representing reviewers which is a # comparatively small subset, it's not a big deal. hashlib.md5(str(self.pk).encode('utf-8')).digest()[:2], ).decode('utf-8') ) ) as_hash.short_description = _('Reviewer ID') def get_full_name(self): return self.speaker_name def get_short_name(self): return self.speaker_name def get_photo_url(self): """Return the photo URL if set, otherwise a default image. """ if self.photo: return self.photo.url return static('images/default_head.png') def is_valid_speaker(self): """Whether the user is a valid speaker. :seealso: ``UserQuerySet.get_valid_speakers`` """ return ( self.verified and self.is_active and self.speaker_name and self.bio ) def is_reviewer(self): return self.has_perm('reviews.add_review') @property def cospeaking_info_set(self): return self.additionalspeaker_set.filter( cancelled=False, conference=settings.CONFERENCE_DEFAULT_SLUG, ) @property def twitter_profile_url(self): return 'https://twitter.com/{}'.format(self.twitter_id) @property def github_profile_url(self): return 'https://github.com/{}'.format(self.github_id) def get_verification_key(self): key = signing.dumps( obj=getattr(self, self.USERNAME_FIELD), salt=settings.SECRET_KEY, ) return key def email_user(self, subject, message, from_email=None, **kwargs): """Send an email to this user. """ send_mail(subject, message, from_email, [self.email], **kwargs) def send_verification_email(self, request): verification_key = self.get_verification_key() verification_url = request.build_absolute_uri( reverse('user_verify', kwargs={ 'verification_key': verification_key, }), ) context = { 'user': self, 'host': request.get_host(), 'verification_key': verification_key, 'verification_url': verification_url, } message = render_to_string( 'registration/verification_email.txt', context, ) self.email_user( subject=ugettext('Verify your email address on {host}').format( **context ), message=message, fail_silently=False, )
def __str__(self): return _("%(product)s - quantity %(qty)d") % { 'product': self.line.product, 'qty': self.quantity }
class Meta: db_table = 'quotes' ordering = ('-publish', ) verbose_name = _('quote') verbose_name_plural = _('quotes')
class Meta: verbose_name = _('user') verbose_name_plural = _('users') swappable = 'AUTH_USER_MODEL'
def import_documents(request): def is_reserved_directory(doc): return doc['fields']['type'] == 'directory' and doc['fields'][ 'name'] in (Document2.HOME_DIR, Document2.TRASH_DIR) try: if request.FILES.get('documents'): documents = request.FILES['documents'].read() else: documents = json.loads(request.POST.get('documents')) documents = json.loads(documents) except ValueError as e: raise PopupException( _('Failed to import documents, the file does not contain valid JSON.' )) # Validate documents if not _is_import_valid(documents): raise PopupException( _('Failed to import documents, the file does not contain the expected JSON schema for Hue documents.' )) docs = [] uuids_map = dict((doc['fields']['uuid'], None) for doc in documents if not is_reserved_directory(doc)) for doc in documents: # Filter docs to import, ignoring reserved directories (home and Trash) and history docs if not is_reserved_directory(doc): # Remove any deprecated fields if 'tags' in doc['fields']: doc['fields'].pop('tags') # If doc is not owned by current user, make a copy of the document with current user as owner if doc['fields']['owner'][0] != request.user.username: doc = _copy_document_with_owner(doc, request.user, uuids_map) else: # Update existing doc or create new doc = _create_or_update_document_with_owner( doc, request.user, uuids_map) # For oozie docs replace dependent uuids with the newly created ones if doc['fields']['type'].startswith('oozie-'): doc = _update_imported_oozie_document(doc, uuids_map) # If the doc contains any history dependencies, ignore them # NOTE: this assumes that each dependency is exported as an array using the natural PK [uuid, version, is_history] deps_minus_history = [ dep for dep in doc['fields'].get('dependencies', []) if len(dep) >= 3 and not dep[2] ] doc['fields']['dependencies'] = deps_minus_history # Replace illegal characters if '/' in doc['fields']['name']: new_name = doc['fields']['name'].replace('/', '-') LOG.warn( "Found illegal slash in document named: %s, renaming to: %s." % (doc['fields']['name'], new_name)) doc['fields']['name'] = new_name # Set last modified date to now doc['fields']['last_modified'] = datetime.now().replace( microsecond=0).isoformat() docs.append(doc) f = tempfile.NamedTemporaryFile(mode='w+', suffix='.json') f.write(json.dumps(docs)) f.flush() stdout = string_io() try: with transaction.atomic( ): # We wrap both commands to commit loaddata & sync management.call_command('loaddata', f.name, verbosity=3, traceback=True, stdout=stdout) Document.objects.sync() if request.POST.get('redirect'): return redirect(request.POST.get('redirect')) else: return JsonResponse({ 'status': 0, 'message': stdout.getvalue(), 'count': len(documents), 'created_count': len([doc for doc in documents if doc['pk'] is None]), 'updated_count': len([doc for doc in documents if doc['pk'] is not None]), 'username': request.user.username, 'documents': [ dict([('name', doc['fields']['name']), ('uuid', doc['fields']['uuid']), ('type', doc['fields']['type']), ('owner', doc['fields']['owner'][0])]) for doc in docs ] }) except Exception as e: LOG.error('Failed to run loaddata command in import_documents:\n %s' % stdout.getvalue()) return JsonResponse({'status': -1, 'message': smart_str(e)}) finally: stdout.close()
def formdata(request, type, parent_id, delete_id=None): model = None results = None response = {} # define types handled if type == "rightsnote": model = models.RightsStatementRightsGrantedNote parent_model = models.RightsStatementRightsGranted model_parent_field = "rightsgranted" model_value_fields = ["rightsgrantednote"] results = model.objects.filter(rightsgranted=parent_id) if type == "rightsrestriction": model = models.RightsStatementRightsGrantedRestriction parent_model = models.RightsStatementRightsGranted model_parent_field = "rightsgranted" model_value_fields = ["restriction"] results = model.objects.filter(rightsgranted=parent_id) if type == "licensenote": model = models.RightsStatementLicenseNote parent_model = models.RightsStatementLicense model_parent_field = "rightsstatementlicense" model_value_fields = ["licensenote"] results = model.objects.filter(rightsstatementlicense=parent_id) if type == "statutenote": model = models.RightsStatementStatuteInformationNote parent_model = models.RightsStatementStatuteInformation model_parent_field = "rightsstatementstatute" model_value_fields = ["statutenote"] results = model.objects.filter(rightsstatementstatute=parent_id) if type == "copyrightnote": model = models.RightsStatementCopyrightNote parent_model = models.RightsStatementCopyright model_parent_field = "rightscopyright" model_value_fields = ["copyrightnote"] results = model.objects.filter(rightscopyright=parent_id) if type == "copyrightdocumentationidentifier": model = models.RightsStatementCopyrightDocumentationIdentifier parent_model = models.RightsStatementCopyright model_parent_field = "rightscopyright" model_value_fields = [ "copyrightdocumentationidentifiertype", "copyrightdocumentationidentifiervalue", "copyrightdocumentationidentifierrole", ] results = model.objects.filter(rightscopyright=parent_id) if type == "statutedocumentationidentifier": model = models.RightsStatementStatuteDocumentationIdentifier parent_model = models.RightsStatementStatuteInformation model_parent_field = "rightsstatementstatute" model_value_fields = [ "statutedocumentationidentifiertype", "statutedocumentationidentifiervalue", "statutedocumentationidentifierrole", ] results = model.objects.filter(rightsstatementstatute=parent_id) if type == "licensedocumentationidentifier": model = models.RightsStatementLicenseDocumentationIdentifier parent_model = models.RightsStatementLicense model_parent_field = "rightsstatementlicense" model_value_fields = [ "licensedocumentationidentifiertype", "licensedocumentationidentifiervalue", "licensedocumentationidentifierrole", ] results = model.objects.filter(rightsstatementlicense=parent_id) if type == "otherrightsdocumentationidentifier": model = models.RightsStatementOtherRightsDocumentationIdentifier parent_model = models.RightsStatementOtherRightsInformation model_parent_field = "rightsstatementotherrights" model_value_fields = [ "otherrightsdocumentationidentifiertype", "otherrightsdocumentationidentifiervalue", "otherrightsdocumentationidentifierrole", ] results = model.objects.filter(rightsstatementotherrights=parent_id) if type == "otherrightsnote": model = models.RightsStatementOtherRightsInformationNote parent_model = models.RightsStatementOtherRightsInformation model_parent_field = "rightsstatementotherrights" model_value_fields = ["otherrightsnote"] results = model.objects.filter(rightsstatementotherrights=parent_id) # handle creation if request.method == "POST": # load or initiate model instance id = request.POST.get("id", 0) if id > 0: instance = model.objects.get(pk=id) else: instance = model() # set instance parent parent = parent_model.objects.filter(pk=parent_id) setattr(instance, model_parent_field, parent[0]) # set instance field values using request data for field in model_value_fields: value = request.POST.get(field, "") setattr(instance, field, value) instance.save() if id == 0: response["new_id"] = instance.pk response["message"] = _("Added.") # handle deletion if request.method == "DELETE": if delete_id is None: response["message"] = _("Error: no delete ID supplied.") else: model.objects.filter(pk=delete_id).delete() response["message"] = _("Deleted.") # send back revised data if results is not None: response["results"] = [] for result in results: values = {} for field in model_value_fields: values[field] = result.__dict__[field] response["results"].append({"id": result.pk, "values": values}) if model is None: response["message"] = _("Incorrect type.") return helpers.json_response(response)
absolute_import, division, print_function, unicode_literals) import re from django.utils.translation import ugettext_lazy as _ THEME_DEFAULT = 'default' THEME_SPHINX = 'sphinxdoc' THEME_SCROLLS = 'scrolls' THEME_AGOGO = 'agogo' THEME_TRADITIONAL = 'traditional' THEME_NATURE = 'nature' THEME_HAIKU = 'haiku' DOCUMENTATION_CHOICES = ( ('auto', _('Automatically Choose')), ('sphinx', _('Sphinx Html')), ('mkdocs', _('Mkdocs (Markdown)')), ('sphinx_htmldir', _('Sphinx HtmlDir')), ('sphinx_singlehtml', _('Sphinx Single Page HTML')), ) DEFAULT_THEME_CHOICES = ( # Translators: This is a name of a Sphinx theme. (THEME_DEFAULT, _('Default')), # Translators: This is a name of a Sphinx theme. (THEME_SPHINX, _('Sphinx Docs')), # (THEME_SCROLLS, 'Scrolls'), # (THEME_AGOGO, 'Agogo'), # Translators: This is a name of a Sphinx theme. (THEME_TRADITIONAL, _('Traditional')),
class Meta: db_table = 'stories' ordering = ('-publish', ) verbose_name = _('story') verbose_name_plural = _('stories')
class AbstractConditionalOffer(models.Model): """ A conditional offer (eg buy 1, get 10% off) """ name = models.CharField( _("Name"), max_length=128, unique=True, help_text=_("This is displayed within the customer's basket")) slug = fields.AutoSlugField( _("Slug"), max_length=128, unique=True, populate_from='name') description = models.TextField(_("Description"), blank=True, help_text=_("This is displayed on the offer" " browsing page")) # Offers come in a few different types: # (a) Offers that are available to all customers on the site. Eg a # 3-for-2 offer. # (b) Offers that are linked to a voucher, and only become available once # that voucher has been applied to the basket # (c) Offers that are linked to a user. Eg, all students get 10% off. The # code to apply this offer needs to be coded # (d) Session offers - these are temporarily available to a user after some # trigger event. Eg, users coming from some affiliate site get 10% # off. SITE, VOUCHER, USER, SESSION = ("Site", "Voucher", "User", "Session") TYPE_CHOICES = ( (SITE, _("Site offer - available to all users")), (VOUCHER, _("Voucher offer - only available after entering " "the appropriate voucher code")), (USER, _("User offer - available to certain types of user")), (SESSION, _("Session offer - temporary offer, available for " "a user for the duration of their session")), ) offer_type = models.CharField( _("Type"), choices=TYPE_CHOICES, default=SITE, max_length=128) exclusive = models.BooleanField( _("Exclusive offer"), help_text=_("Exclusive offers cannot be combined on the same items"), default=True ) # We track a status variable so it's easier to load offers that are # 'available' in some sense. OPEN, SUSPENDED, CONSUMED = "Open", "Suspended", "Consumed" status = models.CharField(_("Status"), max_length=64, default=OPEN) condition = models.ForeignKey( 'offer.Condition', on_delete=models.CASCADE, related_name='offers', verbose_name=_("Condition")) benefit = models.ForeignKey( 'offer.Benefit', on_delete=models.CASCADE, related_name='offers', verbose_name=_("Benefit")) # Some complicated situations require offers to be applied in a set order. priority = models.IntegerField( _("Priority"), default=0, help_text=_("The highest priority offers are applied first")) # AVAILABILITY # Range of availability. Note that if this is a voucher offer, then these # dates are ignored and only the dates from the voucher are used to # determine availability. start_datetime = models.DateTimeField( _("Start date"), blank=True, null=True, help_text=_("Offers are active from the start date. " "Leave this empty if the offer has no start date.")) end_datetime = models.DateTimeField( _("End date"), blank=True, null=True, help_text=_("Offers are active until the end date. " "Leave this empty if the offer has no expiry date.")) # Use this field to limit the number of times this offer can be applied in # total. Note that a single order can apply an offer multiple times so # this is not necessarily the same as the number of orders that can use it. # Also see max_basket_applications. max_global_applications = models.PositiveIntegerField( _("Max global applications"), help_text=_("The number of times this offer can be used before it " "is unavailable"), blank=True, null=True) # Use this field to limit the number of times this offer can be used by a # single user. This only works for signed-in users - it doesn't really # make sense for sites that allow anonymous checkout. max_user_applications = models.PositiveIntegerField( _("Max user applications"), help_text=_("The number of times a single user can use this offer"), blank=True, null=True) # Use this field to limit the number of times this offer can be applied to # a basket (and hence a single order). Often, an offer should only be # usable once per basket/order, so this field will commonly be set to 1. max_basket_applications = models.PositiveIntegerField( _("Max basket applications"), blank=True, null=True, help_text=_("The number of times this offer can be applied to a " "basket (and order)")) # Use this field to limit the amount of discount an offer can lead to. # This can be helpful with budgeting. max_discount = models.DecimalField( _("Max discount"), decimal_places=2, max_digits=12, null=True, blank=True, help_text=_("When an offer has given more discount to orders " "than this threshold, then the offer becomes " "unavailable")) # TRACKING # These fields are used to enforce the limits set by the # max_* fields above. total_discount = models.DecimalField( _("Total Discount"), decimal_places=2, max_digits=12, default=D('0.00')) num_applications = models.PositiveIntegerField( _("Number of applications"), default=0) num_orders = models.PositiveIntegerField( _("Number of Orders"), default=0) redirect_url = fields.ExtendedURLField( _("URL redirect (optional)"), blank=True) date_created = models.DateTimeField(_("Date Created"), auto_now_add=True) objects = models.Manager() active = ActiveOfferManager() # We need to track the voucher that this offer came from (if it is a # voucher offer) _voucher = None class Meta: abstract = True app_label = 'offer' ordering = ['-priority', 'pk'] verbose_name = _("Conditional offer") verbose_name_plural = _("Conditional offers") def save(self, *args, **kwargs): # Check to see if consumption thresholds have been broken if not self.is_suspended: if self.get_max_applications() == 0: self.status = self.CONSUMED else: self.status = self.OPEN return super().save(*args, **kwargs) def get_absolute_url(self): return reverse('offer:detail', kwargs={'slug': self.slug}) def __str__(self): return self.name def clean(self): if (self.start_datetime and self.end_datetime and self.start_datetime > self.end_datetime): raise exceptions.ValidationError( _('End date should be later than start date')) @property def is_open(self): return self.status == self.OPEN @property def is_suspended(self): return self.status == self.SUSPENDED def suspend(self): self.status = self.SUSPENDED self.save() suspend.alters_data = True def unsuspend(self): self.status = self.OPEN self.save() unsuspend.alters_data = True def is_available(self, user=None, test_date=None): """ Test whether this offer is available to be used """ if self.is_suspended: return False if test_date is None: test_date = now() predicates = [] if self.start_datetime: predicates.append(self.start_datetime > test_date) if self.end_datetime: predicates.append(test_date > self.end_datetime) if any(predicates): return False return self.get_max_applications(user) > 0 def is_condition_satisfied(self, basket): return self.condition.proxy().is_satisfied(self, basket) def is_condition_partially_satisfied(self, basket): return self.condition.proxy().is_partially_satisfied(self, basket) def get_upsell_message(self, basket): return self.condition.proxy().get_upsell_message(self, basket) def apply_benefit(self, basket): """ Applies the benefit to the given basket and returns the discount. """ if not self.is_condition_satisfied(basket): return ZERO_DISCOUNT return self.benefit.proxy().apply( basket, self.condition.proxy(), self) def apply_deferred_benefit(self, basket, order, application): """ Applies any deferred benefits. These are things like adding loyalty points to someone's account. """ return self.benefit.proxy().apply_deferred(basket, order, application) def set_voucher(self, voucher): self._voucher = voucher def get_voucher(self): return self._voucher def get_max_applications(self, user=None): """ Return the number of times this offer can be applied to a basket for a given user. """ if self.max_discount and self.total_discount >= self.max_discount: return 0 # Hard-code a maximum value as we need some sensible upper limit for # when there are not other caps. limits = [10000] if self.max_user_applications and user: limits.append(max(0, self.max_user_applications - self.get_num_user_applications(user))) if self.max_basket_applications: limits.append(self.max_basket_applications) if self.max_global_applications: limits.append( max(0, self.max_global_applications - self.num_applications)) return min(limits) def get_num_user_applications(self, user): OrderDiscount = get_model('order', 'OrderDiscount') aggregates = OrderDiscount.objects.filter(offer_id=self.id, order__user=user)\ .aggregate(total=models.Sum('frequency')) return aggregates['total'] if aggregates['total'] is not None else 0 def shipping_discount(self, charge): return self.benefit.proxy().shipping_discount(charge) def record_usage(self, discount): self.num_applications += discount['freq'] self.total_discount += discount['discount'] self.num_orders += 1 self.save() record_usage.alters_data = True def availability_description(self): """ Return a description of when this offer is available """ restrictions = self.availability_restrictions() descriptions = [r['description'] for r in restrictions] return "<br/>".join(descriptions) def availability_restrictions(self): # noqa (too complex (15)) restrictions = [] if self.is_suspended: restrictions.append({ 'description': _("Offer is suspended"), 'is_satisfied': False}) if self.max_global_applications: remaining = self.max_global_applications - self.num_applications desc = _("Limited to %(total)d uses (%(remainder)d remaining)") \ % {'total': self.max_global_applications, 'remainder': remaining} restrictions.append({'description': desc, 'is_satisfied': remaining > 0}) if self.max_user_applications: if self.max_user_applications == 1: desc = _("Limited to 1 use per user") else: desc = _("Limited to %(total)d uses per user") \ % {'total': self.max_user_applications} restrictions.append({'description': desc, 'is_satisfied': True}) if self.max_basket_applications: if self.max_user_applications == 1: desc = _("Limited to 1 use per basket") else: desc = _("Limited to %(total)d uses per basket") \ % {'total': self.max_basket_applications} restrictions.append({ 'description': desc, 'is_satisfied': True}) def hide_time_if_zero(dt): # Only show hours/minutes if they have been specified if dt.tzinfo: localtime = dt.astimezone(get_current_timezone()) else: localtime = dt if localtime.hour == 0 and localtime.minute == 0: return date_filter(localtime, settings.DATE_FORMAT) return date_filter(localtime, settings.DATETIME_FORMAT) if self.start_datetime or self.end_datetime: today = now() if self.start_datetime and self.end_datetime: desc = _("Available between %(start)s and %(end)s") \ % {'start': hide_time_if_zero(self.start_datetime), 'end': hide_time_if_zero(self.end_datetime)} is_satisfied \ = self.start_datetime <= today <= self.end_datetime elif self.start_datetime: desc = _("Available from %(start)s") % { 'start': hide_time_if_zero(self.start_datetime)} is_satisfied = today >= self.start_datetime elif self.end_datetime: desc = _("Available until %(end)s") % { 'end': hide_time_if_zero(self.end_datetime)} is_satisfied = today <= self.end_datetime restrictions.append({ 'description': desc, 'is_satisfied': is_satisfied}) if self.max_discount: desc = _("Limited to a cost of %(max)s") % { 'max': currency(self.max_discount)} restrictions.append({ 'description': desc, 'is_satisfied': self.total_discount < self.max_discount}) return restrictions @property def has_products(self): return self.condition.range is not None def products(self): """ Return a queryset of products in this offer """ Product = get_model('catalogue', 'Product') if not self.has_products: return Product.objects.none() queryset = self.condition.range.all_products() return queryset.filter(is_discountable=True).exclude( structure=Product.CHILD)
def copy_document(request): uuid = json.loads(request.POST.get('uuid', '""')) if not uuid: raise PopupException(_('copy_document requires uuid')) # Document2 and Document model objects are linked and both are saved when saving document = Document2.objects.get_by_uuid(user=request.user, uuid=uuid) # Document model object document1 = document.doc.get() if document.type == 'directory': raise PopupException(_('Directory copy is not supported')) name = document.name + '-copy' # Make the copy of the Document2 model object copy_document = document.copy(name=name, owner=request.user) # Make the copy of Document model object too document1.copy(content_object=copy_document, name=name, owner=request.user) # Import workspace for all oozie jobs if document.type == 'oozie-workflow2' or document.type == 'oozie-bundle2' or document.type == 'oozie-coordinator2': from oozie.models2 import Workflow, Coordinator, Bundle, _import_workspace # Update the name field in the json 'data' field if document.type == 'oozie-workflow2': workflow = Workflow(document=document) workflow.update_name(name) workflow.update_uuid(copy_document.uuid) _import_workspace(request.fs, request.user, workflow) copy_document.update_data( {'workflow': workflow.get_data()['workflow']}) copy_document.save() if document.type == 'oozie-bundle2' or document.type == 'oozie-coordinator2': if document.type == 'oozie-bundle2': bundle_or_coordinator = Bundle(document=document) else: bundle_or_coordinator = Coordinator(document=document) json_data = bundle_or_coordinator.get_data_for_json() json_data['name'] = name json_data['uuid'] = copy_document.uuid copy_document.update_data(json_data) copy_document.save() _import_workspace(request.fs, request.user, bundle_or_coordinator) elif document.type == 'search-dashboard': from dashboard.models import Collection2 collection = Collection2(request.user, document=document) collection.data['collection']['label'] = name collection.data['collection']['uuid'] = copy_document.uuid copy_document.update_data( {'collection': collection.data['collection']}) copy_document.save() # Keep the document and data in sync else: copy_data = copy_document.data_dict if 'name' in copy_data: copy_data['name'] = name if 'uuid' in copy_data: copy_data['uuid'] = copy_document.uuid copy_document.update_data(copy_data) copy_document.save() return JsonResponse({'status': 0, 'document': copy_document.to_dict()})
class AbstractRange(models.Model): """ Represents a range of products that can be used within an offer. Ranges only support adding parent or stand-alone products. Offers will consider child products automatically. """ name = models.CharField(_("Name"), max_length=128, unique=True) slug = fields.AutoSlugField( _("Slug"), max_length=128, unique=True, populate_from="name") description = models.TextField(blank=True) # Whether this range is public is_public = models.BooleanField( _('Is public?'), default=False, help_text=_("Public ranges have a customer-facing page")) includes_all_products = models.BooleanField( _('Includes all products?'), default=False) included_products = models.ManyToManyField( 'catalogue.Product', related_name='includes', blank=True, verbose_name=_("Included Products"), through='offer.RangeProduct') excluded_products = models.ManyToManyField( 'catalogue.Product', related_name='excludes', blank=True, verbose_name=_("Excluded Products")) classes = models.ManyToManyField( 'catalogue.ProductClass', related_name='classes', blank=True, verbose_name=_("Product Types")) included_categories = models.ManyToManyField( 'catalogue.Category', related_name='includes', blank=True, verbose_name=_("Included Categories")) # Allow a custom range instance to be specified proxy_class = fields.NullCharField( _("Custom class"), max_length=255, default=None, unique=True) date_created = models.DateTimeField(_("Date Created"), auto_now_add=True) __included_product_ids = None __excluded_product_ids = None __included_categories = None __class_ids = None __category_ids = None objects = models.Manager() browsable = BrowsableRangeManager() class Meta: abstract = True app_label = 'offer' verbose_name = _("Range") verbose_name_plural = _("Ranges") def __str__(self): return self.name def get_absolute_url(self): return reverse( 'catalogue:range', kwargs={'slug': self.slug}) @cached_property def proxy(self): if self.proxy_class: return load_proxy(self.proxy_class)() def add_product(self, product, display_order=None): """ Add product to the range When adding product that is already in the range, prevent re-adding it. If display_order is specified, update it. Default display_order for a new product in the range is 0; this puts the product at the top of the list. """ initial_order = display_order or 0 RangeProduct = get_model('offer', 'RangeProduct') relation, __ = RangeProduct.objects.get_or_create( range=self, product=product, defaults={'display_order': initial_order}) if (display_order is not None and relation.display_order != display_order): relation.display_order = display_order relation.save() # Remove product from excluded products if it was removed earlier and # re-added again, thus it returns back to the range product list. if product.id in self._excluded_product_ids(): self.excluded_products.remove(product) self.invalidate_cached_ids() def remove_product(self, product): """ Remove product from range. To save on queries, this function does not check if the product is in fact in the range. """ RangeProduct = get_model('offer', 'RangeProduct') RangeProduct.objects.filter(range=self, product=product).delete() # Making sure product will be excluded from range products list by adding to # respective field. Otherwise, it could be included as a product from included # category or etc. self.excluded_products.add(product) # Invalidating cached property value with list of IDs of already excluded products. self.invalidate_cached_ids() def contains_product(self, product): # noqa (too complex (12)) """ Check whether the passed product is part of this range. """ # Delegate to a proxy class if one is provided if self.proxy: return self.proxy.contains_product(product) excluded_product_ids = self._excluded_product_ids() if product.id in excluded_product_ids: return False if self.includes_all_products: return True class_ids = self._class_ids() if class_ids and product.get_product_class().id in class_ids: return True included_product_ids = self._included_product_ids() # If the product's parent is in the range, the child is automatically included as well if product.is_child and product.parent.id in included_product_ids: return True if product.id in included_product_ids: return True test_categories = self._included_categories() if test_categories: for category in product.get_categories().only( *self._category_comparison_fields): for test_category in test_categories: if category == test_category \ or category.is_descendant_of(test_category): return True return False # Deprecated alias @deprecated def contains(self, product): return self.contains_product(product) def __get_pks_and_child_pks(self, queryset): """ Expects a product queryset; gets the primary keys of the passed products and their children. Verbose, but database and memory friendly. """ # One query to get parent and children; [(4, None), (5, 10), (5, 11)] pk_tuples_iterable = queryset.values_list('pk', 'children__pk') # Flatten list without unpacking; [4, None, 5, 10, 5, 11] flat_iterable = itertools.chain.from_iterable(pk_tuples_iterable) # Ensure uniqueness and remove None; {4, 5, 10, 11} return set(flat_iterable) - {None} @cached_property def _category_comparison_fields(self): # Overwritten Category models could contain a lot of data, e.g CMS # content. Hence, this avoids fetching unneeded data in the costly # range comparison queries. Note that using .only() with an empty list # is a no-op essentially, so nothing breaks when the field is missing. Category = get_model('catalogue', 'Category') return getattr(Category, 'COMPARISON_FIELDS', ()) def _included_categories(self): if not self.id: return self.included_categories.none() if self.__included_categories is None: self.__included_categories = self.included_categories.only( *self._category_comparison_fields) return self.__included_categories def _included_product_ids(self): if not self.id: return [] if self.__included_product_ids is None: self.__included_product_ids = self.__get_pks_and_child_pks( self.included_products) return self.__included_product_ids def _excluded_product_ids(self): if not self.id: return [] if self.__excluded_product_ids is None: self.__excluded_product_ids = self.__get_pks_and_child_pks( self.excluded_products) return self.__excluded_product_ids def _class_ids(self): if self.__class_ids is None: self.__class_ids = self.classes.values_list('pk', flat=True) return self.__class_ids def _category_ids(self): if self.__category_ids is None: ids = [] for category in self._included_categories(): children_ids = category.get_descendants().values_list( 'pk', flat=True) ids.append(category.pk) ids.extend(list(children_ids)) self.__category_ids = ids return self.__category_ids def invalidate_cached_ids(self): self.__category_ids = None self.__included_categories = None self.__included_product_ids = None self.__excluded_product_ids = None def num_products(self): # Delegate to a proxy class if one is provided if self.proxy: return self.proxy.num_products() if self.includes_all_products: return None return self.all_products().count() def all_products(self): """ Return a queryset containing all the products in the range This includes included_products plus the products contained in the included classes and categories, minus the products in excluded_products. """ if self.proxy: return self.proxy.all_products() Product = get_model("catalogue", "Product") if self.includes_all_products: # Filter out child products and blacklisted products return Product.objects.browsable().exclude( id__in=self._excluded_product_ids()) return Product.objects.filter( Q(id__in=self._included_product_ids()) | Q(product_class_id__in=self._class_ids()) | Q(productcategory__category_id__in=self._category_ids()) ).exclude(id__in=self._excluded_product_ids()).distinct() @property def is_editable(self): """ Test whether this range can be edited in the dashboard. """ return not self.proxy_class @property def is_reorderable(self): """ Test whether products for the range can be re-ordered. """ return len(self._class_ids()) == 0 and len(self._included_categories()) == 0
class EMail(Auditable, models.Model): to = models.CharField( max_length=1000, verbose_name=_('To'), help_text=_('One email address or several addresses separated by commas.'), ) reply_to = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_('Reply-To'), ) cc = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_('CC'), help_text=_('One email address or several addresses separated by commas.'), ) bcc = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_('BCC'), help_text=_('One email address or several addresses separated by commas.'), ) subject = models.CharField( max_length=200, verbose_name=_('Subject'), ) members = models.ManyToManyField( to='members.Member', related_name='emails', ) text = models.TextField(verbose_name=_('Text')) sent = models.DateTimeField(null=True, blank=True, verbose_name=_('Sent at')) template = models.ForeignKey(to=MailTemplate, null=True, blank=True, on_delete=models.SET_NULL) attachments = models.ManyToManyField( to='documents.Document', related_name='mails', ) @property def attachment_ids(self): if hasattr(self, 'attachments'): return list(self.attachments.all().values_list('pk', flat=True)) return [] def send(self): if self.sent: raise Exception('This mail has been sent already. It cannot be sent again.') from byro.mails.send import mail_send_task mail_send_task( to=self.to.split(','), subject=self.subject, body=self.text, sender=self.reply_to, cc=(self.cc or '').split(','), bcc=(self.bcc or '').split(','), attachments=self.attachment_ids, ) self.sent = now() self.save(update_fields=['sent']) def copy_to_draft(self): new_mail = deepcopy(self) new_mail.pk = None new_mail.sent = None new_mail.save() return new_mail
class AbstractCondition(BaseOfferMixin, models.Model): """ A condition for an offer to be applied. You can either specify a custom proxy class, or need to specify a type, range and value. """ COUNT, VALUE, COVERAGE = ("Count", "Value", "Coverage") TYPE_CHOICES = ( (COUNT, _("Depends on number of items in basket that are in " "condition range")), (VALUE, _("Depends on value of items in basket that are in " "condition range")), (COVERAGE, _("Needs to contain a set number of DISTINCT items " "from the condition range"))) range = models.ForeignKey( 'offer.Range', blank=True, null=True, on_delete=models.CASCADE, verbose_name=_("Range")) type = models.CharField(_('Type'), max_length=128, choices=TYPE_CHOICES, blank=True) value = fields.PositiveDecimalField( _('Value'), decimal_places=2, max_digits=12, null=True, blank=True) proxy_class = fields.NullCharField( _("Custom class"), max_length=255, default=None) class Meta: abstract = True app_label = 'offer' verbose_name = _("Condition") verbose_name_plural = _("Conditions") @property def proxy_map(self): return { self.COUNT: get_class( 'offer.conditions', 'CountCondition'), self.VALUE: get_class( 'offer.conditions', 'ValueCondition'), self.COVERAGE: get_class( 'offer.conditions', 'CoverageCondition'), } def consume_items(self, offer, basket, affected_lines): pass def is_satisfied(self, offer, basket): """ Determines whether a given basket meets this condition. This is stubbed in this top-class object. The subclassing proxies are responsible for implementing it correctly. """ return False def is_partially_satisfied(self, offer, basket): """ Determine if the basket partially meets the condition. This is useful for up-selling messages to entice customers to buy something more in order to qualify for an offer. """ return False def get_upsell_message(self, offer, basket): return None def can_apply_condition(self, line): """ Determines whether the condition can be applied to a given basket line """ if not line.stockrecord_id: return False product = line.product return (self.range.contains_product(product) and product.get_is_discountable()) def get_applicable_lines(self, offer, basket, most_expensive_first=True): """ Return line data for the lines that can be consumed by this condition """ line_tuples = [] for line in basket.all_lines(): if not self.can_apply_condition(line): continue price = unit_price(offer, line) if not price: continue line_tuples.append((price, line)) key = operator.itemgetter(0) if most_expensive_first: return sorted(line_tuples, reverse=True, key=key) return sorted(line_tuples, key=key)
class Meta: abstract = True app_label = 'offer' verbose_name = _("Range") verbose_name_plural = _("Ranges")
class Meta: abstract = True app_label = 'offer' verbose_name = _("Benefit") verbose_name_plural = _("Benefits")
class Meta: abstract = True app_label = 'offer' verbose_name = _("Condition") verbose_name_plural = _("Conditions")
def availability_restrictions(self): # noqa (too complex (15)) restrictions = [] if self.is_suspended: restrictions.append({ 'description': _("Offer is suspended"), 'is_satisfied': False}) if self.max_global_applications: remaining = self.max_global_applications - self.num_applications desc = _("Limited to %(total)d uses (%(remainder)d remaining)") \ % {'total': self.max_global_applications, 'remainder': remaining} restrictions.append({'description': desc, 'is_satisfied': remaining > 0}) if self.max_user_applications: if self.max_user_applications == 1: desc = _("Limited to 1 use per user") else: desc = _("Limited to %(total)d uses per user") \ % {'total': self.max_user_applications} restrictions.append({'description': desc, 'is_satisfied': True}) if self.max_basket_applications: if self.max_user_applications == 1: desc = _("Limited to 1 use per basket") else: desc = _("Limited to %(total)d uses per basket") \ % {'total': self.max_basket_applications} restrictions.append({ 'description': desc, 'is_satisfied': True}) def hide_time_if_zero(dt): # Only show hours/minutes if they have been specified if dt.tzinfo: localtime = dt.astimezone(get_current_timezone()) else: localtime = dt if localtime.hour == 0 and localtime.minute == 0: return date_filter(localtime, settings.DATE_FORMAT) return date_filter(localtime, settings.DATETIME_FORMAT) if self.start_datetime or self.end_datetime: today = now() if self.start_datetime and self.end_datetime: desc = _("Available between %(start)s and %(end)s") \ % {'start': hide_time_if_zero(self.start_datetime), 'end': hide_time_if_zero(self.end_datetime)} is_satisfied \ = self.start_datetime <= today <= self.end_datetime elif self.start_datetime: desc = _("Available from %(start)s") % { 'start': hide_time_if_zero(self.start_datetime)} is_satisfied = today >= self.start_datetime elif self.end_datetime: desc = _("Available until %(end)s") % { 'end': hide_time_if_zero(self.end_datetime)} is_satisfied = today <= self.end_datetime restrictions.append({ 'description': desc, 'is_satisfied': is_satisfied}) if self.max_discount: desc = _("Limited to a cost of %(max)s") % { 'max': currency(self.max_discount)} restrictions.append({ 'description': desc, 'is_satisfied': self.total_discount < self.max_discount}) return restrictions
def clean_fixed_price(self): if self.range: raise exceptions.ValidationError( _("No range should be selected as the condition range will " "be used instead."))
def clean(self): if (self.start_datetime and self.end_datetime and self.start_datetime > self.end_datetime): raise exceptions.ValidationError( _('End date should be later than start date'))
class AbstractBenefit(BaseOfferMixin, models.Model): range = models.ForeignKey( 'offer.Range', blank=True, null=True, on_delete=models.CASCADE, verbose_name=_("Range")) # Benefit types PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = ( "Percentage", "Absolute", "Multibuy", "Fixed price") SHIPPING_PERCENTAGE, SHIPPING_ABSOLUTE, SHIPPING_FIXED_PRICE = ( 'Shipping percentage', 'Shipping absolute', 'Shipping fixed price') TYPE_CHOICES = ( (PERCENTAGE, _("Discount is a percentage off of the product's value")), (FIXED, _("Discount is a fixed amount off of the product's value")), (MULTIBUY, _("Discount is to give the cheapest product for free")), (FIXED_PRICE, _("Get the products that meet the condition for a fixed price")), (SHIPPING_ABSOLUTE, _("Discount is a fixed amount of the shipping cost")), (SHIPPING_FIXED_PRICE, _("Get shipping for a fixed price")), (SHIPPING_PERCENTAGE, _("Discount is a percentage off of the shipping" " cost")), ) type = models.CharField( _("Type"), max_length=128, choices=TYPE_CHOICES, blank=True) # The value to use with the designated type. This can be either an integer # (eg for multibuy) or a decimal (eg an amount) which is slightly # confusing. value = fields.PositiveDecimalField( _("Value"), decimal_places=2, max_digits=12, null=True, blank=True) # If this is not set, then there is no upper limit on how many products # can be discounted by this benefit. max_affected_items = models.PositiveIntegerField( _("Max Affected Items"), blank=True, null=True, help_text=_("Set this to prevent the discount consuming all items " "within the range that are in the basket.")) # A custom benefit class can be used instead. This means the # type/value/max_affected_items fields should all be None. proxy_class = fields.NullCharField( _("Custom class"), max_length=255, default=None) class Meta: abstract = True app_label = 'offer' verbose_name = _("Benefit") verbose_name_plural = _("Benefits") @property def proxy_map(self): return { self.PERCENTAGE: get_class( 'offer.benefits', 'PercentageDiscountBenefit'), self.FIXED: get_class( 'offer.benefits', 'AbsoluteDiscountBenefit'), self.MULTIBUY: get_class( 'offer.benefits', 'MultibuyDiscountBenefit'), self.FIXED_PRICE: get_class( 'offer.benefits', 'FixedPriceBenefit'), self.SHIPPING_ABSOLUTE: get_class( 'offer.benefits', 'ShippingAbsoluteDiscountBenefit'), self.SHIPPING_FIXED_PRICE: get_class( 'offer.benefits', 'ShippingFixedPriceBenefit'), self.SHIPPING_PERCENTAGE: get_class( 'offer.benefits', 'ShippingPercentageDiscountBenefit') } def apply(self, basket, condition, offer): return ZERO_DISCOUNT def apply_deferred(self, basket, order, application): return None def clean(self): if not self.type: return method_name = 'clean_%s' % self.type.lower().replace(' ', '_') if hasattr(self, method_name): getattr(self, method_name)() def clean_multibuy(self): errors = [] if not self.range: errors.append(_("Multibuy benefits require a product range")) if self.value: errors.append(_("Multibuy benefits don't require a value")) if self.max_affected_items: errors.append(_("Multibuy benefits don't require a " "'max affected items' attribute")) if errors: raise exceptions.ValidationError(errors) def clean_percentage(self): errors = [] if not self.range: errors.append(_("Percentage benefits require a product range")) if not self.value: errors.append(_("Percentage discount benefits require a value")) elif self.value > 100: errors.append(_("Percentage discount cannot be greater than 100")) if errors: raise exceptions.ValidationError(errors) def clean_shipping_absolute(self): errors = [] if not self.value: errors.append(_("A discount value is required")) if self.range: errors.append(_("No range should be selected as this benefit does " "not apply to products")) if self.max_affected_items: errors.append(_("Shipping discounts don't require a " "'max affected items' attribute")) if errors: raise exceptions.ValidationError(errors) def clean_shipping_percentage(self): errors = [] if not self.value: errors.append(_("Percentage discount benefits require a value")) elif self.value > 100: errors.append(_("Percentage discount cannot be greater than 100")) if self.range: errors.append(_("No range should be selected as this benefit does " "not apply to products")) if self.max_affected_items: errors.append(_("Shipping discounts don't require a " "'max affected items' attribute")) if errors: raise exceptions.ValidationError(errors) def clean_shipping_fixed_price(self): errors = [] if self.range: errors.append(_("No range should be selected as this benefit does " "not apply to products")) if self.max_affected_items: errors.append(_("Shipping discounts don't require a " "'max affected items' attribute")) if errors: raise exceptions.ValidationError(errors) def clean_fixed_price(self): if self.range: raise exceptions.ValidationError( _("No range should be selected as the condition range will " "be used instead.")) def clean_absolute(self): errors = [] if not self.range: errors.append(_("Fixed discount benefits require a product range")) if not self.value: errors.append(_("Fixed discount benefits require a value")) if errors: raise exceptions.ValidationError(errors) def round(self, amount): """ Apply rounding to discount amount """ if hasattr(settings, 'OSCAR_OFFER_ROUNDING_FUNCTION'): return settings.OSCAR_OFFER_ROUNDING_FUNCTION(amount) return amount.quantize(D('.01'), ROUND_DOWN) def _effective_max_affected_items(self): """ Return the maximum number of items that can have a discount applied during the application of this benefit """ return self.max_affected_items if self.max_affected_items else 10000 def can_apply_benefit(self, line): """ Determines whether the benefit can be applied to a given basket line """ return line.stockrecord and line.product.is_discountable def get_applicable_lines(self, offer, basket, range=None): """ Return the basket lines that are available to be discounted :basket: The basket :range: The range of products to use for filtering. The fixed-price benefit ignores its range and uses the condition range """ if range is None: range = self.range line_tuples = [] for line in basket.all_lines(): product = line.product if (not range.contains_product(product) or not self.can_apply_benefit(line)): continue price = unit_price(offer, line) if not price: # Avoid zero price products continue if line.quantity_without_offer_discount(offer) == 0: continue line_tuples.append((price, line)) # We sort lines to be cheapest first to ensure consistent applications return sorted(line_tuples, key=operator.itemgetter(0)) def shipping_discount(self, charge): return D('0.00')