Beispiel #1
0
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
Beispiel #2
0
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)
Beispiel #3
0
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
Beispiel #4
0
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)
Beispiel #6
0
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
Beispiel #7
0
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
Beispiel #8
0
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)
Beispiel #9
0
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'
Beispiel #10
0
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
Beispiel #11
0
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)