class CustomDefinition(Item): visible_from_root = False members_order = [ "title", "identifier", "definition_type", "enabled", "content_types", "initialization" ] title = schema.String(required=True, indexed=True, unique=True, translated=True, descriptive=True) identifier = schema.String() definition_type = schema.String(required=True, default="dimension", enumeration=["dimension", "metric"]) enabled = schema.Boolean(required=True, default=True) content_types = schema.Collection( items=schema.Reference(class_family=Item), default=[Publishable], min=1) initialization = schema.CodeBlock(language="python") def applies(self, publishable, website=None): return isinstance(publishable, tuple(self.content_types)) def apply(self, publishable, values, index=None, env=None): if not self.initialization: return if index is None: from woost.models import Configuration defs = Configuration.instance.google_analytics_custom_definitions index = defs.index(self) context = { "publishable": publishable, "index": index, "value": schema.undefined, "undefined": schema.undefined, "env": {} if env is None else env } CustomDefinition.initialization.execute(self, context) index = context["index"] if index is not None: value = context["value"] if value is not schema.undefined: key = self.definition_type + str(index) value = get_ga_value(value) values[key] = value
class HTMLBlock(Block): instantiable = True type_group = "blocks.custom" view_class = "cocktail.html.Element" html = schema.CodeBlock(language="html", member_group="content") translated_html = schema.CodeBlock(translated=True, language="html", member_group="content") def init_view(self, view): Block.init_view(self, view) html = self.translated_html or self.html if html: view.append(html)
class CustomTriggerResponse(TriggerResponse): """A trigger response that allows the execution of arbitrary python code.""" instantiable = True code = schema.CodeBlock(language="python", required=True) def execute(self, items, user, batch=False, **context): context.update(items=items, user=user, batch=batch) code = line_separator_expr.sub("\n", self.code) exec code in context
class Attribute(Item): members_order = [ "title", "enabled", "content_types", "scope", "attribute_name", "code" ] title = schema.String( required = True, translated = True, unique = True, indexed = True, descriptive = True ) enabled = schema.Boolean( required = True, default = True, indexed = True ) content_types = schema.Collection( items = schema.Reference(class_family = Item), default = [Publishable], ui_form_control = "cocktail.ui.SplitSelector", min = 1 ) scope = schema.String( required = True, enumeration = ["any", "page", "ref"], default = "any", ui_form_control = "cocktail.ui.RadioSelector", indexed = True ) attribute_name = schema.String( required = True, unique = True, indexed = True ) code = schema.CodeBlock( language = "python", required = True )
class FacebookPublicationTarget(Item): members_order = [ "title", "graph_object_id", "administrator_id", "app_id", "app_secret", "auth_token", "languages", "targeting" ] visible_from_root = False title = schema.String(required=True, unique=True, indexed=True, normalized_index=True, descriptive=True, listed_by_default=False) graph_object_id = schema.String(required=True) administrator_id = schema.String(listed_by_default=False) app_id = schema.String(required=True, listed_by_default=False) app_secret = schema.String(required=True, listed_by_default=False, text_search=False) auth_token = schema.String( editable=False, text_search=False, translate_value=lambda value, language=None, **kwargs: translations( "FacebookPublicationTarget.auth_token-%s" % ("conceded" if value else "pending"), language, **kwargs)) languages = schema.Collection(items=schema.String( enumeration=lambda ctx: Configuration.instance.languages, translate_value=lambda value, language=None, **kwargs: "" if not value else translations(value))) targeting = schema.CodeBlock(language="python") def publish(self, publishable): if self.auth_token is None: raise ValueError( "Can't publish %s to %s: missing authorization token." % (publishable, self)) graph_url = "https://graph.facebook.com/%s/feed" % self.graph_object_id post_data = self._get_publication_parameters(publishable) encoded_post_data = dict( (k, v.encode("utf-8") if isinstance(v, unicode) else v) for k, v in post_data.iteritems()) response = urlopen(graph_url, urlencode(encoded_post_data)) status = response.getcode() if status < 200 or status > 299: raise FacebookPublicationError(response.read()) def _get_publication_parameters(self, publishable): og = OpenGraphExtension.instance og_properties = og.get_properties(publishable) post_data = { "access_token": self.auth_token, "name": og_properties.get("og:title") or translations(publishable), "link": og_properties.get("og:url") } # Disregard any default image; only show an image if the published # content defines one if "og:image" not in og_properties: post_data["picture"] = "" if self.targeting: context = { "language": get_language(), "og_properties": og_properties, "publishable": publishable, "locales": facebook_locales, "include_locales": (lambda included: [facebook_locales[loc_name] for loc_name in included]), "exclude_locales": (lambda excluded: [ loc_id for loc_name, loc_id in facebook_locales.iteritems() if loc_name not in excluded ]) } targeting_code = self.targeting.replace("\r\n", "\n") exec targeting_code in context targeting = context.get("targeting") if targeting: post_data["feed_targeting"] = dumps(targeting) return post_data def feed_posts(self): if self.auth_token is None: raise ValueError( "Can't read the posts in %s: missing authorization token." % self) graph_url = "https://graph.facebook.com/%s/feed/?access_token=%s" % ( self.graph_object_id, self.auth_token) response = urlopen(graph_url) status = response.getcode() body = response.read() if status < 200 or status > 299: raise FacebookPublicationError(body) feed_data = loads(body) return feed_data["data"] def find_post(self, publishable): uri = self._get_publication_parameters(publishable)["link"] for post in self.feed_posts(): if post.get("link") == uri: return post def publish_album(self, album_title, photos, album_description=None, photo_languages=None, generate_story=True): if self.auth_token is None: raise ValueError( "Can't publish album to %s: missing authorization token." % self) # Create the album response = urlopen( "https://graph.facebook.com/%s/albums" % self.graph_object_id, _encode_form({ "access_token": self.auth_token, "name": album_title, "message": album_description or "" })) status = response.getcode() body = response.read() if status < 200 or status > 299: raise FacebookPublicationError(body) album_id = loads(body)["id"] photos_url = "https://graph.facebook.com/%s/photos" % album_id # Upload photos for photo in photos: if photo_languages is None: photo_languages = photo.translations.keys() else: photo_languages = list( set(photo_languages) & set(photo.translations.keys())) if not photo_languages: photo_desc = "" elif len(photo_languages) == 1: photo_desc = translations(photo, photo_languages[0]) else: photo_desc = u"\n".join( u"%s: %s" % (translations(lang, lang), translations(photo, lang)) for lang in photo_languages) datagen, headers = multipart_encode({ "access_token": self.auth_token, "source": open(photo.file_path), "message": photo_desc, "no_story": ("0" if generate_story else "1") }) request = urllib2.Request(photos_url, datagen, headers) response = urllib2.urlopen(request) status = response.getcode() body = response.read() if status < 200 or status > 299: raise FacebookPublicationError(body) response = urlopen("https://graph.facebook.com/%s/?access_token=%s" % (album_id, self.auth_token)) status = response.getcode() body = response.read() if status < 200 or status > 299: raise FacebookPublicationError(body) return loads(body)
class Feed(Publishable): type_group = "setup" instantiable = True groups_order = ["meta", "feed_items"] members_order = [ "title", "ttl", "image", "description", "limit", "query_parameters", "item_title_expression", "item_link_expression", "item_publication_date_expression", "item_description_expression" ] default_mime_type = u"application/rss+xml" default_controller = schema.DynamicDefault( lambda: Controller.get_instance(qname="woost.feed_controller")) edit_controller = \ "woost.controllers.backoffice.feedfieldscontroller." \ "FeedFieldsController" edit_view = "woost.views.FeedFields" title = schema.String(indexed=True, normalized_index=True, full_text_indexed=True, descriptive=True, translated=True, member_group="meta") ttl = schema.Integer(listed_by_default=False, member_group="meta") image = schema.Reference( type=Publishable, related_end=schema.Collection(), relation_constraints=Publishable.resource_type.equal("image"), member_group="meta") description = schema.String(required=True, translated=True, listed_by_default=False, edit_control="cocktail.html.TextArea", member_group="meta") limit = schema.Integer(min=1, listed_by_default=False, member_group="feed_items") query_parameters = schema.Mapping(keys=schema.String(), required=True, listed_by_default=False, member_group="feed_items") item_title_expression = schema.CodeBlock(language="python", required=True, default="translations(item)", member_group="feed_items") item_link_expression = schema.CodeBlock(language="python", required=True, default="cms.uri(item)", member_group="feed_items") item_publication_date_expression = schema.CodeBlock( language="python", required=True, default="item.start_date or item.creation_time", member_group="feed_items") item_description_expression = schema.CodeBlock(language="python", required=True, default="item.description", member_group="feed_items") def select_items(self): user_collection = UserCollection(Publishable) user_collection.allow_paging = False user_collection.allow_member_selection = False user_collection.allow_language_selection = False user_collection.params.source = self.query_parameters.get user_collection.available_languages = \ Configuration.instance.get_enabled_languages() items = user_collection.subset if self.limit: items.range = (0, self.limit) return items
class Trigger(Item): """Describes an event.""" instantiable = False visible_from_root = False members_order = [ "title", "execution_point", "batch_execution", "matching_roles", "condition", "custom_context", "responses" ] title = schema.String( translated = True, descriptive = True ) execution_point = schema.String( required = True, enumeration = ("before", "after"), default = "after", edit_control = "cocktail.html.RadioSelector", translate_value = lambda value, language = None, **kwargs: u"" if not value else translations( "woost.models.Trigger.execution_point " + value, language, **kwargs ), text_search = False ) batch_execution = schema.Boolean( required = True ) responses = schema.Collection( items = "woost.models.TriggerResponse", bidirectional = True, related_key = "trigger" ) matching_roles = schema.Collection( items = schema.Reference( type = Role, required = True ), related_end = schema.Collection() ) condition = schema.CodeBlock( language = "python" ) custom_context = schema.CodeBlock( language = "python" ) def match(self, user, verbose = False, **context): # Check the user trigger_roles = self.matching_roles if trigger_roles: if user is None: print trigger_doesnt_match_style("user not specified") return False for role in user.iter_roles(): if role in trigger_roles: break else: print trigger_doesnt_match_style("user doesn't match") return False # Check the condition condition = self.condition if condition and not eval(condition, context): print trigger_doesnt_match_style("condition doesn't match") return False return True
class UserMember(Item): class __metaclass__(Item.__metaclass__): def __init__(cls, name, bases, members): Item.__metaclass__.__init__(cls, name, bases, members) # Automatically create polymorphic members if cls.member_class is not schema.Member \ and not issubclass( cls.member_class, (schema.RelationMember, schema.Schema) ): # Make sure the strings for the extension have been loaded from woost.extensions.usermodels import strings # Default if not cls.get_member("member_default"): cls.add_member(cls.create_member_default_member()) translations.copy_key("UserMember.member_default", name + ".member_default", overwrite=False) # Enumeration if not cls.get_member("member_enumeration"): cls.add_member(cls.create_member_enumeration_member()) translations.copy_key("UserMember.member_enumeration", name + ".member_enumeration", overwrite=False) instantiable = False visible_from_root = False member_class = schema.Member member_property_prefix = "member_" edit_controls = None search_controls = None edit_node_class = \ "woost.extensions.usermodels.usermembereditnode.UserMemberEditNode" edit_form = "woost.extensions.usermodels.UserMemberForm" groups_order = ["description", "definition", "constraints", "behavior" ] + (Item.groups_order or []) members_order = [ "label", "explanation", "member_name", "member_translated", "member_versioned", "member_descriptive", "member_indexed", "member_unique", "member_required", "member_member_group", "member_listed_by_default", "member_editable", "member_edit_control", "member_searchable", "member_search_control", "initialization" ] label = schema.String(required=True, translated=True, descriptive=True, member_group="description") explanation = schema.String(translated=True, edit_control="woost.views.RichTextEditor", listed_by_default=False, member_group="description") parent_schema = schema.Reference( type="woost.extensions.usermodels.usermembers.UserModel", bidirectional=True, visible=False) parent_collection = schema.Reference( type="woost.extensions.usermodels.usermembers.UserCollection", bidirectional=True, visible=False) member_name = schema.String(required=True, format=r"^[a-zA-Z][a-zA-Z0-9_]*$", listed_by_default=False, text_search=False, member_group="definition") member_translated = schema.Boolean(required=True, default=False, listed_by_default=False, member_group="definition") member_versioned = schema.Boolean(required=True, default=True, listed_by_default=False, member_group="definition") member_descriptive = schema.Boolean(required=True, default=False, listed_by_default=False, member_group="definition") member_indexed = schema.Boolean(required=True, default=False, listed_by_default=False, member_group="definition") initialization = schema.CodeBlock(language="python", member_group="code") member_unique = schema.Boolean(required=True, default=False, listed_by_default=False, member_group="constraints") member_required = schema.Boolean(default=False, required=True, listed_by_default=False, member_group="constraints") member_member_group = schema.String(listed_by_default=False, text_search=False, member_group="behavior") member_listed_by_default = schema.Boolean(required=True, default=True, listed_by_default=False, member_group="behavior") member_editable = schema.Boolean(required=True, default=True, listed_by_default=False, member_group="behavior") member_edit_control = schema.String( listed_by_default=False, member_group="behavior", text_search=False, translate_value=lambda value, language=None, **kwargs: translations( "woost.extensions.usermodels.auto-control", language) if not value else translations(value)) member_searchable = schema.Boolean(required=True, default=True, listed_by_default=False, member_group="behavior") member_search_control = schema.String( listed_by_default=False, member_group="behavior", text_search=False, translate_value=lambda value, language=None, **kwargs: translations( "woost.extensions.usermodels.auto-control", language) if not value else translations(value)) @classmethod def create_member_default_member(cls): return cls.member_class( "member_default", member_group="definition", ) @classmethod def create_member_enumeration_member(cls): return schema.Collection("member_enumeration", required=False, items=cls.member_class(required=True), member_group="constraints", edit_control="cocktail.html.TextArea") def update_member_definition(self): """Creates or updates the schema member described by the item.""" return self.produce_member(self.get_produced_member()) def produce_member(self, member=None): # Instantiate a new member if member is None: member = self.member_class() # Define translations if self.parent_schema: trans_key = self.parent_schema.member_name + "." + self.member_name translations.clear_key(trans_key) for lang in self.translations: translations[lang][trans_key] = self.get("label", lang) explanation = self.get("explanation", lang) if explanation: translations[lang][trans_key + "-explanation"] = \ explanation # Copy meta-properties, but remove the element from its schema first, # to allow it to be renamed try: parent_schema = member.schema if parent_schema: prev_schema_order = parent_schema.members_order parent_schema.remove_member(member) for key in self.__class__.members(): if key.startswith(self.member_property_prefix): property_name = key[len(self.member_property_prefix):] property_value = self.get(key) setattr(member, property_name, property_value) finally: if parent_schema: parent_schema.add_member(member) parent_schema.members_order = prev_schema_order # Apply user defined logic to the member if self.initialization: exec self.initialization in {"member": member, "user_member": self} return member def get_produced_member(self): """Returns a reference to the member produced and registered into the application by an earlier call to `produce_member`. """ if self.parent_schema is not None: parent_member = self.parent_schema.get_produced_member() if parent_member: return parent_member.get_member(self.member_name) elif self.parent_collection is not None: parent_member = self.parent_collection.get_produced_member() if parent_member: return parent_member.items def remove_produced_member(self): """Removes the member produced and registered by an earlier call to `produce_member`. """ member = self.get_produced_member() if member is None: return if self.parent_schema: parent_schema = self.parent_schema.get_produced_member() if parent_schema is not None: parent_schema.remove_member(self) elif self.parent_collection: parent_collection = self.parent_collection.get_produced_member() if parent_collection: parent_collection.items = None _v_after_commit_actions = None def _after_commit(self, action, *args): if self._v_after_commit_actions is None: self._v_after_commit_actions = WeakKeyDictionary() trans = datastore.connection.transaction_manager.get() transaction_actions = self._v_after_commit_actions.get(trans) if transaction_actions is None: self._v_after_commit_actions[trans] = transaction_actions = set() if action not in transaction_actions: transaction_actions.add(action) def commit_hook(successful, *args): if successful: try: action(*args) except Exception, ex: warn(str(ex)) trans.addAfterCommitHook(commit_hook, args)
class SiteMap(Publishable): type_group = "resource" default_hidden = True default_per_language_publication = False members_order = [ "title", "included_locales", "content_expression", "entries_expression" ] title = schema.String(translated=True, descriptive=True) included_locales = schema.Collection( items=LocaleMember(), edit_control="cocktail.html.SplitSelector", default_type=set) content_expression = schema.CodeBlock(language="python") entries_expression = schema.CodeBlock(language="python") def iter_entries(self): language_subset = app.website.get_published_languages( languages=self.included_locales or None) content = Publishable.select_accessible( Publishable.robots_should_index.equal(True), language=language_subset) if self.content_expression: context = {"site_map": self, "content": content} SiteMap.content_expression.execute(self, context) content = context["content"] for publishable in content: if not publishable.is_internal_content(): continue if publishable.per_language_publication: languages = language_subset & publishable.enabled_translations else: languages = (None, ) if not languages: continue properties = {} if publishable.x_sitemap_priority: properties["priority"] = publishable.x_sitemap_priority if publishable.x_sitemap_change_frequency: properties[ "changefreq"] = publishable.x_sitemap_change_frequency entries = [(properties, [(language, publishable.get_uri(host="!", language=language)) for language in languages])] if self.entries_expression: context = { "site_map": self, "publishable": publishable, "languages": languages, "entries": entries, "default_properties": properties } SiteMap.entries_expression.execute(self, context) entries = context["entries"] for entry in entries: yield entry def generate_sitemap(self): indent = " " * 4 yield '<?xml version="1.0" encoding="utf-8"?>\n' yield '<urlset\n' yield indent yield 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' yield indent yield 'xmlns:xhtml="http://www.w3.org/1999/xhtml">\n' for properties, urls in self.iter_entries(): # URLs for language, url in urls: yield indent yield '<url>\n' yield indent * 2 yield ('<loc>%s</loc>\n' % escape(str(url))) # Properties (priority, change frequency, etc) for key, value in properties.items(): yield indent * 2 yield '<%s>%s</%s>\n' % (key, escape(str(value)), key) yield indent yield '</url>\n' yield '</urlset>\n'
class CachingPolicy(Item): visible_from_root = False edit_form = "woost.views.CachingPolicyForm" groups_order = ["cache"] members_order = [ "description", "important", "cache_enabled", "server_side_cache", "expiration_expression", "cache_tags_expression", "cache_key_expression", "condition" ] description = schema.String( descriptive = True, translated = True, listed_by_default = False ) important = schema.Boolean( required = True, default = False ) cache_enabled = schema.Boolean( required = True, default = True ) server_side_cache = schema.Boolean( required = True, default = False, listed_by_default = False ) expiration_expression = schema.CodeBlock( language = "python" ) condition = schema.CodeBlock( language = "python" ) cache_key_expression = schema.CodeBlock( language = "python" ) cache_tags_expression = schema.CodeBlock( language = "python" ) def applies_to(self, publishable, **context): expression = self.condition if expression: expression = expression.replace("\r", "") context["publishable"] = publishable exec expression in context return context.get("applies", False) return True def get_content_cache_key(self, publishable, **context): user = get_current_user() cache_key = ( str(Location.get_current(relative = False)), None if user is None or user.anonymous else tuple(role.id for role in user.roles) ) key_qualifier = None expression = self.cache_key_expression if expression: expression = expression.replace("\r", "") context["publishable"] = publishable exec expression in context key_qualifier = context.get("cache_key") else: request = context.get("request") if request: key_qualifier = tuple(request.params.items()) if key_qualifier: cache_key = cache_key + (key_qualifier,) return cache_key def get_content_expiration(self, publishable, base = None, **context): expression = self.expiration_expression expiration = base if expression: expression = expression.replace("\r", "") context["expiration"] = expiration context["publishable"] = publishable context["datetime"] = datetime context["timedelta"] = timedelta exec expression in context expiration = context.get("expiration") return expiration def get_content_tags(self, publishable, base = None, **context): tags = publishable.get_cache_tags( language = context.get("language") or get_language() ) tags.add(self.main_cache_tag) if base: tags.update(base) expression = self.cache_tags_expression if expression: context["tags"] = tags exec expression in context tags = context.get("tags") return tags
class EmailTemplate(Item): type_group = "customization" encoding = "utf-8" members_order = [ "title", "mime_type", "sender", "receivers", "bcc", "template_engine", "subject", "body", "initialization_code" ] title = schema.String(listed_by_default=False, required=True, unique=True, indexed=True, normalized_index=True, full_text_indexed=True, descriptive=True, translated=True) mime_type = schema.String(required=True, default="html", listed_by_default=False, enumeration=["plain", "html"], translatable_enumeration=False, text_search=False) sender = schema.CodeBlock(language="python") receivers = schema.CodeBlock(language="python", required=True) bcc = schema.CodeBlock(language="python", listed_by_default=False) template_engine = schema.String( enumeration=buffet.available_engines.keys(), translatable_enumeration=False, text_search=False, listed_by_default=False) subject = schema.String(translated=True, edit_control="cocktail.html.TextArea") body = schema.String(translated=True, listed_by_default=False, edit_control="cocktail.html.TextArea") initialization_code = schema.CodeBlock(language="python") def send(self, context=None): if context is None: context = {} if context.get("attachments") is None: context["attachments"] = {} def eval_member(key): expr = self.get(key) return eval(expr, context.copy()) if expr else None # MIME block mime_type = self.mime_type pos = mime_type.find("/") if pos != -1: mime_type = mime_type[pos + 1:] # Custom initialization code init_code = self.initialization_code if init_code: exec init_code in context # Subject and body (templates) if self.template_engine: template_engine = buffet.available_engines[self.template_engine] engine = template_engine( options={"mako.output_encoding": self.encoding}) def render(field_name): markup = self.get(field_name) if markup: template = engine.load_template( "EmailTemplate." + field_name, self.get(field_name)) return engine.render(context, template=template) else: return u"" subject = render("subject").strip() body = render("body") else: subject = self.subject.encode(self.encoding) body = self.body.encode(self.encoding) message = MIMEText(body, _subtype=mime_type, _charset=self.encoding) # Attachments attachments = context.get("attachments") if attachments: attachments = dict((cid, attachment) for cid, attachment in attachments.iteritems() if attachment is not None) if attachments: message_text = message message = MIMEMultipart("related") message.attach(message_text) for cid, attachment in attachments.iteritems(): if isinstance(attachment, File): file_path = attachment.file_path file_name = attachment.file_name mime_type = attachment.mime_type else: file_path = attachment file_name = os.path.basename(file_path) mime_type_guess = guess_type(file_path) if mime_type_guess: mime_type = mime_type_guess[0] else: mime_type = "application/octet-stream" main_type, sub_type = mime_type.split('/', 1) message_attachment = MIMEBase(main_type, sub_type) message_attachment.set_payload(open(file_path).read()) Encoders.encode_base64(message_attachment) message_attachment.add_header("Content-ID", "<%s>" % cid) message_attachment.add_header( 'Content-Disposition', 'attachment; filename="%s"' % file_name) message.attach(message_attachment) def format_email_address(address, encoding): name, address = parseaddr(address) name = Header(name, encoding).encode() address = address.encode('ascii') return formataddr((name, address)) # Receivers (python expression) receivers = eval_member("receivers") if receivers: receivers = set(r.strip().encode(self.encoding) for r in receivers) if not receivers: return set() message["To"] = ", ".join([ format_email_address(receiver, self.encoding) for receiver in receivers ]) # Sender (python expression) sender = eval_member("sender") if sender: message['From'] = format_email_address(sender, self.encoding) # BCC (python expression) bcc = eval_member("bcc") if bcc: receivers.update(r.strip().encode(self.encoding) for r in bcc) if subject: message["Subject"] = Header(subject, self.encoding).encode() message["Date"] = formatdate() # Send the message smtp = Configuration.instance.connect_to_smtp() smtp.sendmail(sender, list(receivers), message.as_string()) smtp.quit() return receivers
class ECommerceBillingConcept(Item): members_order = [ "title", "enabled", "start_date", "end_date", "hidden", "scope", "eligible_countries", "eligible_products", "eligible_roles", "condition", "implementation" ] visible_from_root = False title = schema.String(translated=True, descriptive=True, required=True) enabled = schema.Boolean(required=True, default=True) start_date = schema.DateTime(indexed=True) end_date = schema.DateTime(indexed=True, min=start_date) hidden = schema.Boolean(default=False, required=True) scope = schema.String( required=True, enumeration=["order", "purchase"], default="order", edit_control="cocktail.html.RadioSelector", translate_value=lambda value, language=None, **kwargs: "" if not value else translations( "ECommerceBillingConcept.scope-" + value, language, **kwargs)) condition = schema.CodeBlock(language="python") eligible_countries = schema.Collection(items=schema.Reference( type=Location, relation_constraints=[Location.location_type.equal("country")]), related_end=schema.Collection()) eligible_products = schema.Collection( items=schema.Reference(type=ECommerceProduct), related_end=schema.Collection()) eligible_roles = schema.Collection(items=schema.Reference(type=Role), related_end=schema.Collection()) implementation = schema.CodeBlock(language="python") def is_current(self): return (self.start_date is None or self.start_date <= datetime.now()) \ and (self.end_date is None or self.end_date > datetime.now()) def applies_to(self, item, costs=None): if not self.enabled: return False if not self.is_current(): return False from woost.extensions.ecommerce.ecommerceproduct \ import ECommerceProduct order = None purchase = None product = None if isinstance(item, ECommerceProduct): if self.eligible_products and item not in self.eligible_products: return False product = item elif self.scope == "order": from woost.extensions.ecommerce.ecommerceorder \ import ECommerceOrder if not isinstance(item, ECommerceOrder): return False if self.eligible_products and not any( purchase.product in self.eligible_products for purchase in item.purchases): return False order = item elif self.scope == "purchase": from woost.extensions.ecommerce.ecommercepurchase \ import ECommercePurchase if not isinstance(item, ECommercePurchase): return False if self.eligible_products \ and item.product not in self.eligible_products: return False order = item.order purchase = item product = item.product if self.eligible_countries and ( order is None or order.country is None or not any( order.country.descends_from(region) for region in self.eligible_countries)): return False # Eligible roles if self.eligible_roles and ( order is None or order.customer is None or not any(role in self.eligible_roles for role in order.customer.iter_roles())): return False # Custom condition if self.condition: context = { "self": self, "order": order, "purchase": purchase, "product": product, "costs": costs, "applies": True } exec self.condition in context if not context["applies"]: return False return True def apply(self, item, costs): costs["concepts"].append(self) kind, value = self.parse_implementation() if kind == "override": applicable_concepts = [] for concept in costs["concepts"]: concept_kind, concept_value = concept.parse_implementation() if concept_kind not in ("add", "override") or concept is self: applicable_concepts.append(concept) costs["concepts"] = applicable_concepts costs["cost"] = value elif kind == "add": costs["cost"] += value elif kind == "override_percentage": applicable_concepts = [] for concept in costs["concepts"]: concept_kind, concept_value = concept.parse_implementation() if concept_kind not in ("add_percentage", "override_percentage" ) or concept is self: applicable_concepts.append(concept) costs["concepts"] = applicable_concepts costs["percentage"] = value elif kind == "add_percentage": costs["percentage"] += value elif kind == "free_units": delivered, paid = value q, r = divmod(item.quantity, delivered) costs["paid_units"] = q * paid + r elif kind == "custom": context = {"self": self, "item": item, "costs": costs} exec value in context def parse_implementation(self): value = self.implementation # Cost override match = override_regexp.match(value) if match: return ("override", Decimal(match.group(1))) # Additive cost match = add_regexp.match(value) if match: return ("add", Decimal(match.group(1))) # Override percentage match = override_percentage_regexp.match(value) if match: return ("override_percentage", Decimal(match.group(1))) # Override percentage match = add_percentage_regexp.match(value) if match: return ("add_percentage", Decimal(match.group(1))) # Free units discount ("3x2", "2x1", etc) match = free_units_regexp.match(value) if match: return ("free_units", (int(match.group(1)), int(match.group(2)))) return ("custom", value)