<commit_before>"""Naming set for Korean"""

import copy

import basepart

class ComposerElementKorean(basepart.ComposerElementBase):
    """Korean name composer - inspired by 이강성, 『파이썬 3 바이블』"""
    character = ( \
    ('ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', \
    'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'), \
    ('ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', \
    'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'), \
    ('', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', \
    'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ') \
    )
    initial = basepart.ListBase()
    medial = basepart.ListBase()
    final = basepart.ListBase()
    def __init__(self, initial=None, medial=None, final=None):
        pass # See the metaclass
    def compose(self):
        """Compose the Korean name"""
        character = self.character
        list_original = [self.initial, self.medial, self.final]
        list_process = [basepart.ListBase(), basepart.ListBase(), basepart.ListBase()]
        ingredient = [basepart.ListBase(), basepart.ListBase(), basepart.ListBase()]

        # Check type and init list_process
        for (idx, elem) in enumerate(list_original):
            if isinstance(elem, basepart.ListBase):
                list_process[idx] = copy.deepcopy(elem)
            elif elem is None:
                list_process[idx] = basepart.IncludeList(character[idx])
            else:
                raise basepart.NamingLibException('Check composer input type')

        for (elem, characterset) in zip(list_process, (character[0], character[1], character[2])):
            # Change str to index
            elem.digitize(characterset)
            # Check index whether that is out of range
            elem.check_element_index(characterset)

        # Fill the ingredient
        for (idx, elem) in enumerate(list_process):
            ingredient[idx] = elem.choice(character[idx])

        result_int = 0xac00 + ((ingredient[0] * 21) + ingredient[1]) * 28 + ingredient[2]
        result_char = chr(result_int)

        self.result = result_char
        return result_char
<commit_msg>Add tenuis_consonant and multiple_final<commit_after>"""Naming set for Korean"""

import copy

import basepart

class ComposerElementKorean(basepart.ComposerElementBase):
    """Korean name composer - inspired by 이강성, 『파이썬 3 바이블』"""
    character = ( \
        tuple('ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ'), \
        tuple('ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ'), \
        tuple(' ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ') \
    )
    tenuis_consonant = tuple('ㄲㄸㅃㅆㅉ')
    multiple_final = tuple('ㄳㄵㄶㄺㄻㄼㄽㄾㄿㅀㅄ')
    initial = basepart.ListBase()
    medial = basepart.ListBase()
    final = basepart.ListBase()

    def __init__(self, initial=None, medial=None, final=None):
        pass # See the metaclass
    def compose(self):
        """Compose the Korean name"""
        character = self.character
        list_original = [self.initial, self.medial, self.final]
        list_process = [basepart.ListBase(), basepart.ListBase(), basepart.ListBase()]
        ingredient = [basepart.ListBase(), basepart.ListBase(), basepart.ListBase()]

        # Check type and init list_process
        for (idx, elem) in enumerate(list_original):
            if isinstance(elem, basepart.ListBase):
                list_process[idx] = copy.deepcopy(elem)
            elif elem is None:
                list_process[idx] = basepart.IncludeList(character[idx])
            else:
                raise basepart.NamingLibException('Check composer input type')

        for (elem, characterset) in zip(list_process, (character[0], character[1], character[2])):
            # Change str to index
            elem.digitize(characterset)
            # Check index whether that is out of range
            elem.check_element_index(characterset)

        # Fill the ingredient
        for (idx, elem) in enumerate(list_process):
            ingredient[idx] = elem.choice(character[idx])

        result_int = 0xac00 + ((ingredient[0] * 21) + ingredient[1]) * 28 + ingredient[2]
        result_char = chr(result_int)

        self.result = result_char
        return result_char
<|endoftext|>


<commit_before>"""The base experiment class."""

from wallace.models import Network, Node, Info, Transformation, Participant
from wallace.information import Gene, Meme, State
from wallace.nodes import Agent, Source, Environment
from wallace.transformations import Compression, Response
from wallace.transformations import Mutation, Replication
from sqlalchemy import and_
import random
import inspect
import sys
from collections import Counter
from operator import itemgetter

class Experiment(object):
    """Define the structure of an experiment."""

    def __init__(self, session):
        """Initialize an experiment object."""
        from recruiters import PsiTurkRecruiter
        self.verbose = True
        self.task = "Experiment title"
        self.session = session
        self.practice_repeats = 0
        self.experiment_repeats = 0
        self.recruiter = PsiTurkRecruiter
        self.initial_recruitment_size = 1
        self.agent = Agent
        self.known_classes = {
            "Agent": Agent,
            "Compression": Compression,
            "Environment": Environment,
            "Gene": Gene,
            "Info": Info,
            "Meme": Meme,
            "Mutation": Mutation,
            "Node": Node,
            "Replication": Replication,
            "Response": Response,
            "Source": Source,
            "State": State,
            "Transformation": Transformation,
        }

    def setup(self):
        """Create the networks if they don't already exist."""
        if not self.networks():
            for _ in range(self.practice_repeats):
                network = self.network()
                network.role = "practice"
                self.session.add(network)
            for _ in range(self.experiment_repeats):
                network = self.network()
                network.role = "experiment"
                self.session.add(network)
            self.session.commit()

    def networks(self, role="all", full="all"):
        """All the networks in the experiment."""
        if full not in ["all", True, False]:
            raise ValueError("full must be boolean or all, it cannot be {}"
                             .format(full))

        if full == "all":
            if role == "all":
                return Network.query.all()
            else:
                return Network\
                    .query\
                    .filter_by(role=role)\
                    .all()
        else:
            if role == "all":
                return Network.query.filter_by(full=full)\
                    .all()
            else:
                return Network\
                    .query\
                    .filter(and_(Network.role == role, Network.full == full))\
                    .all()

    def get_network_for_participant(self, participant):
        """Find a network for a participant.

        If no networks are available, None will be returned.
        """
        key = participant.id
        networks_with_space = Network.query.filter_by(full=False).all()
        networks_participated_in = [
            node.network_id for node in
            Node.query.with_entities(Node.network_id)
                .filter_by(participant_id=participant.id).all()
        ]

        legal_networks = [
            net for net in networks_with_space
            if net.id not in networks_participated_in
        ]

        if not legal_networks:
            self.log("No networks available, returning None", key)
            return None

        self.log("{} networks out of {} available"
                 .format(len(legal_networks),
                         (self.practice_repeats + self.experiment_repeats)),
                 key)

        legal_practice_networks = [net for net in legal_networks
                                   if net.role == "practice"]
        if legal_practice_networks:
            chosen_network = legal_practice_networks[0]
            self.log("Practice networks available.\
                     Assigning participant to practice network {}."
                     .format(chosen_network.id), key)
        else:
            chosen_network = random.choice(legal_networks)
            self.log("No practice networks available.\
                     Assigning participant to experiment network {}"
                     .format(chosen_network.id), key)
        return chosen_network

    def make_node_for_participant(self, participant, network):
        """Create a node for a participant."""
        if inspect.isclass(self.agent):
            if issubclass(self.agent, Node):
                node = self.agent(participant=participant, network=network)
            else:
                raise ValueError("{} is not a subclass of Node"
                                 .format(self.agent))
        else:
            node = self.agent(network=network)(participant=participant,
                                               network=network)
        return node

    def add_node_to_network(self, node, network):
        """Add a node to a network."""
        network.add_node(node)

    def receive_transmissions(self, transmissions):
        """Receive transmissions."""
        for t in transmissions:
            t.mark_received()

    def data_check(self, participant):
        """Check that the data are acceptable."""
        return True

    def bonus(self, participant):
        """The bonus to be awarded to the given participant."""
        return 0

    def bonus_reason(self):
        """The reason offered to the participant for giving the bonus."""
        return "Thank for participating! Here is your bonus."

    def attention_check(self, participant):
        """Check if participant performed adequately."""
        return True

    def submission_successful(self, participant):
        """Run when a participant submits successfully."""
        pass

    def recruit(self):
        """Recruit participants to the experiment as needed."""
        if self.networks(full=False):
            self.log("Network space available: recruiting 1 more participant",
                     "-----")
            self.recruiter().recruit_participants(n=1)
        else:
            self.log("All networks full: closing recruitment", "-----")
            self.recruiter().close_recruitment()

    def log(self, text, key="?????", force=False):
        """Print a string to the logs."""
        if force or self.verbose:
            print ">>>> {} {}".format(key, text)
            sys.stdout.flush()

    def log_summary(self):
        """Log a summary of all the participants' status codes."""
        participants = Participant.query\
            .with_entities(Participant.status).all()
        counts = Counter([p.status for p in participants])
        sorted_counts = sorted(counts.items(), key=itemgetter(0))
        self.log("Status summary: {}".format(str(sorted_counts)))
        return sorted_counts

    def save(self, *objects):
        """Add all the objects to the session and commit them."""
        if len(objects) > 0:
            self.session.add_all(objects)
        self.session.commit()

    def node_post_request(self, participant, node):
        """Run when a request to make a node is complete."""
        pass

    def node_get_request(self, node=None, nodes=None):
        """Run when a request to get nodes is complete."""
        pass

    def vector_post_request(self, node, vectors):
        """Run when a request to connect is complete."""
        pass

    def vector_get_request(self, node, vectors):
        """Run when a request to get vectors is complete."""
        pass

    def info_post_request(self, node, info):
        """Run when a request to create an info is complete."""
        pass

    def info_get_request(self, node, infos):
        """Run when a request to get infos is complete."""
        pass

    def transmission_post_request(self, node, transmissions):
        """Run when a request to transmit is complete."""
        pass

    def transmission_get_request(self, node, transmissions):
        """Run when a request to get transmissions is complete."""
        pass

    def transformation_post_request(self, node, transformation):
        """Run when a request to transform an info is complete."""
        pass

    def transformation_get_request(self, node, transformations):
        """Run when a request to get transformations is complete."""
        pass

    def fail_participant(self, participant):
        """Fail all the nodes of a participant."""
        participant_nodes = Node.query\
            .filter_by(participant_id=participant.id, failed=False)\
            .all()

        for node in participant_nodes:
            node.fail()

    def data_check_failed(self, participant):
        """What to do if a participant fails the data check."""
        self.fail_participant(participant)

    def attention_check_failed(self, participant):
        """What to do if a participant fails the attention check."""
        self.fail_participant(participant)

    def assignment_abandoned(self, participant):
        """What to do if a participant abandons the hit."""
        self.fail_participant(participant)

    def assignment_returned(self, participant):
        """What to do if a participant returns the hit."""
        self.fail_participant(participant)
<commit_msg>switch base experiment to node_type<commit_after>"""The base experiment class."""

from wallace.models import Network, Node, Info, Transformation, Participant
from wallace.information import Gene, Meme, State
from wallace.nodes import Agent, Source, Environment
from wallace.transformations import Compression, Response
from wallace.transformations import Mutation, Replication
from sqlalchemy import and_
import random
import inspect
import sys
from collections import Counter
from operator import itemgetter

class Experiment(object):
    """Define the structure of an experiment."""

    def __init__(self, session):
        """Initialize an experiment object."""
        from recruiters import PsiTurkRecruiter
        self.verbose = True
        self.task = "Experiment title"
        self.session = session
        self.practice_repeats = 0
        self.experiment_repeats = 0
        self.recruiter = PsiTurkRecruiter
        self.initial_recruitment_size = 1
        self.known_classes = {
            "Agent": Agent,
            "Compression": Compression,
            "Environment": Environment,
            "Gene": Gene,
            "Info": Info,
            "Meme": Meme,
            "Mutation": Mutation,
            "Node": Node,
            "Replication": Replication,
            "Response": Response,
            "Source": Source,
            "State": State,
            "Transformation": Transformation,
        }

    def setup(self):
        """Create the networks if they don't already exist."""
        if not self.networks():
            for _ in range(self.practice_repeats):
                network = self.network()
                network.role = "practice"
                self.session.add(network)
            for _ in range(self.experiment_repeats):
                network = self.network()
                network.role = "experiment"
                self.session.add(network)
            self.session.commit()

    def networks(self, role="all", full="all"):
        """All the networks in the experiment."""
        if full not in ["all", True, False]:
            raise ValueError("full must be boolean or all, it cannot be {}"
                             .format(full))

        if full == "all":
            if role == "all":
                return Network.query.all()
            else:
                return Network\
                    .query\
                    .filter_by(role=role)\
                    .all()
        else:
            if role == "all":
                return Network.query.filter_by(full=full)\
                    .all()
            else:
                return Network\
                    .query\
                    .filter(and_(Network.role == role, Network.full == full))\
                    .all()

    def get_network_for_participant(self, participant):
        """Find a network for a participant.

        If no networks are available, None will be returned.
        """
        key = participant.id
        networks_with_space = Network.query.filter_by(full=False).all()
        networks_participated_in = [
            node.network_id for node in
            Node.query.with_entities(Node.network_id)
                .filter_by(participant_id=participant.id).all()
        ]

        legal_networks = [
            net for net in networks_with_space
            if net.id not in networks_participated_in
        ]

        if not legal_networks:
            self.log("No networks available, returning None", key)
            return None

        self.log("{} networks out of {} available"
                 .format(len(legal_networks),
                         (self.practice_repeats + self.experiment_repeats)),
                 key)

        legal_practice_networks = [net for net in legal_networks
                                   if net.role == "practice"]
        if legal_practice_networks:
            chosen_network = legal_practice_networks[0]
            self.log("Practice networks available.\
                     Assigning participant to practice network {}."
                     .format(chosen_network.id), key)
        else:
            chosen_network = random.choice(legal_networks)
            self.log("No practice networks available.\
                     Assigning participant to experiment network {}"
                     .format(chosen_network.id), key)
        return chosen_network

    def make_node_for_participant(self, participant, network):
        """Create a node for a participant."""
        return self.node_type(network=network)(participant=participant,
                                               network=network)

    def node_type(self, network):
        """The type of node to make."""
        return Node

    def add_node_to_network(self, node, network):
        """Add a node to a network."""
        network.add_node(node)

    def receive_transmissions(self, transmissions):
        """Receive transmissions."""
        for t in transmissions:
            t.mark_received()

    def data_check(self, participant):
        """Check that the data are acceptable."""
        return True

    def bonus(self, participant):
        """The bonus to be awarded to the given participant."""
        return 0

    def bonus_reason(self):
        """The reason offered to the participant for giving the bonus."""
        return "Thank for participating! Here is your bonus."

    def attention_check(self, participant):
        """Check if participant performed adequately."""
        return True

    def submission_successful(self, participant):
        """Run when a participant submits successfully."""
        pass

    def recruit(self):
        """Recruit participants to the experiment as needed."""
        if self.networks(full=False):
            self.log("Network space available: recruiting 1 more participant",
                     "-----")
            self.recruiter().recruit_participants(n=1)
        else:
            self.log("All networks full: closing recruitment", "-----")
            self.recruiter().close_recruitment()

    def log(self, text, key="?????", force=False):
        """Print a string to the logs."""
        if force or self.verbose:
            print ">>>> {} {}".format(key, text)
            sys.stdout.flush()

    def log_summary(self):
        """Log a summary of all the participants' status codes."""
        participants = Participant.query\
            .with_entities(Participant.status).all()
        counts = Counter([p.status for p in participants])
        sorted_counts = sorted(counts.items(), key=itemgetter(0))
        self.log("Status summary: {}".format(str(sorted_counts)))
        return sorted_counts

    def save(self, *objects):
        """Add all the objects to the session and commit them."""
        if len(objects) > 0:
            self.session.add_all(objects)
        self.session.commit()

    def node_post_request(self, participant, node):
        """Run when a request to make a node is complete."""
        pass

    def node_get_request(self, node=None, nodes=None):
        """Run when a request to get nodes is complete."""
        pass

    def vector_post_request(self, node, vectors):
        """Run when a request to connect is complete."""
        pass

    def vector_get_request(self, node, vectors):
        """Run when a request to get vectors is complete."""
        pass

    def info_post_request(self, node, info):
        """Run when a request to create an info is complete."""
        pass

    def info_get_request(self, node, infos):
        """Run when a request to get infos is complete."""
        pass

    def transmission_post_request(self, node, transmissions):
        """Run when a request to transmit is complete."""
        pass

    def transmission_get_request(self, node, transmissions):
        """Run when a request to get transmissions is complete."""
        pass

    def transformation_post_request(self, node, transformation):
        """Run when a request to transform an info is complete."""
        pass

    def transformation_get_request(self, node, transformations):
        """Run when a request to get transformations is complete."""
        pass

    def fail_participant(self, participant):
        """Fail all the nodes of a participant."""
        participant_nodes = Node.query\
            .filter_by(participant_id=participant.id, failed=False)\
            .all()

        for node in participant_nodes:
            node.fail()

    def data_check_failed(self, participant):
        """What to do if a participant fails the data check."""
        self.fail_participant(participant)

    def attention_check_failed(self, participant):
        """What to do if a participant fails the attention check."""
        self.fail_participant(participant)

    def assignment_abandoned(self, participant):
        """What to do if a participant abandons the hit."""
        self.fail_participant(participant)

    def assignment_returned(self, participant):
        """What to do if a participant returns the hit."""
        self.fail_participant(participant)
<|endoftext|>


<commit_before>import functools
import inspect
import itertools
import queue
import threading
from typing import Any, Awaitable, Callable, Iterable, List, Union

from tornado import gen as tg, ioloop as ti
from wcpan.logger import DEBUG, EXCEPTION

RawTask = Callable[[], Any]
MaybeTask = Union['Task', RawTask]
AwaitCallback = Callable[[Any], None]

class AsyncWorker(object):

    def __init__(self) -> None:
        super(AsyncWorker, self).__init__()

        self._thread = None
        self._ready_lock = threading.Condition()
        self._loop = None
        # FIXME thread safe
        self._queue = queue.PriorityQueue()
        self._tail = {}
        self._tail_lock = threading.Lock()

    @property
    def is_alive(self) -> bool:
        return self._thread and self._thread.is_alive()

    def start(self) -> None:
        if not self.is_alive:
            self._thread = threading.Thread(target=self._run)
            self._thread.start()
        with self._ready_lock:
            if self._loop is None:
                if not self._ready_lock.wait_for(lambda: self._loop is not None, 1):
                    raise Exception('timeout')

    def stop(self) -> None:
        if self.is_alive:
            task = TerminalTask()
            self._queue.put(task)
            self._thread.join()
            self._thread = None

    async def do(self, task: MaybeTask) -> Awaitable[Any]:
        if not self.is_alive:
            raise WorkerError('worker is dead')

        task = ensure_task(task)
        fn = functools.partial(self.do_later, task)
        future = tg.Task(fn)
        id_ = task.id_
        self._update_tail(id_, future)

        rv = await future
        return rv

    def do_later(self, task: MaybeTask, callback: AwaitCallback = None) -> None:
        if not self.is_alive:
            raise WorkerError('worker is dead')

        task = ensure_task(task)

        id_ = task.id_
        self._make_tail(id_, callback)

        self._queue.put(task)

    async def flush(self, filter_: Callable[['Task'], bool]) -> Awaitable[None]:
        task = FlushTask(filter_)
        await self.do(task)

    def flush_later(self, filter_: Callable[['Task'], bool], callback: AwaitCallback = None) -> None:
        task = FlushTask(filter_)
        self.do_later(task, callback)

    def _make_tail(self, id_: int, callback: AwaitCallback) -> None:
        with self._tail_lock:
            self._tail[id_] = None, callback

    def _update_tail(self, id_: int, future: tg.Future) -> None:
        assert id_ in self._tail, 'invalid task id'

        with self._tail_lock:
            _, callback = self._tail[id_]
            assert _ is None, 'replacing existing future'

            self._tail[id_] = future, callback

    def _do_flush(self, task: 'FlushTask') -> None:
        with self._get_internal_mutex():
            q = self._get_internal_queue()
            nq = filter(lambda _: not task(_), q)
            nq = list(nq)
            DEBUG('wcpan.worker') << 'flush:' << 'before' << len(q) << 'after' << len(nq)
            self._set_internal_queue(nq)

    def _get_internal_queue(self) -> Iterable:
        return self._queue.queue

    def _set_internal_queue(self, nq: List) -> None:
        self._queue.queue = nq

    def _get_internal_mutex(self):
        return self._queue.mutex

    def _run(self) -> None:
        with self._ready_lock:
            self._loop = ti.IOLoop()
            self._loop.add_callback(self._process)
            self._ready_lock.notify()
        self._loop.start()
        self._loop.close()
        self._loop = None

    async def _process(self) -> Awaitable[None]:
        while True:
            task = self._queue.get()
            rv = None
            exception = None
            try:
                if isinstance(task, TerminalTask):
                    break
                elif isinstance(task, FlushTask):
                    rv = self._do_flush(task)
                else:
                    rv = await regular_call(task)
            except Exception as e:
                exception = e
            finally:
                self._queue.task_done()
                id_ = task.id_
                with self._tail_lock:
                    future, done = self._tail.get(id_, (None, None))
                    if id_ in self._tail:
                        del self._tail[id_]
                if exception:
                    if future:
                        future.set_exception(exception)
                    else:
                        EXCEPTION('wcpan.worker', exception) << 'uncought exception'
                elif done:
                    done(rv)
                else:
                    DEBUG('wcpan.worker') << 'unused return value:' << rv
        self._loop.stop()

@functools.total_ordering
class Task(object):

    _counter = itertools.count()

    def __init__(self, callable_: RawTask = None) -> None:
        super(Task, self).__init__()

        self._callable = callable_
        # FIXME atomic because GIL
        self._id = next(self._counter)

    def __eq__(self, that: 'Task') -> bool:
        return self.equal(that)

    def __lt__(self, that: 'Task') -> bool:
        return self.higher_then(that)

    def __call__(self) -> Any:
        if not self._callable:
            raise NotImplementedError()
        return self._callable()

    # highest first
    @property
    def priority(self) -> int:
        return 0

    @property
    def id_(self) -> int:
        return self._id

    def equal(self, that: 'Task') -> bool:
        if not isinstance(that, self.__class__):
            return NotImplemented
        return self.id_ == that.id_

    def higher_then(self, that: 'Task') -> bool:
        if not isinstance(that, self.__class__):
            return NotImplemented
        if self.priority > that.priority:
            return True
        if self.priority < that.priority:
            return False
        # lower ID was created earlier
        return self.id_ < that.id_

# ATTENTION DO NOT use this class
class InternalTask(Task):

    def __init__(self, *args, **kwargs) -> None:
        super(InternalTask, self).__init__(*args, **kwargs)

    def equal(self, that: Task) -> bool:
        # if not same type, always non-equal
        if self.__class__ is not that.__class__:
            return False
        return self.id_ == that.id_

    def higher_then(self, that: Task) -> bool:
        # if not internal task, it has lower priority
        if not isinstance(that, InternalTask):
            return True
        if self.priority > that.priority:
            return True
        if self.priority < that.priority:
            return False
        # lower ID was created earlier
        return self.id_ < that.id_

# ATTENTION DO NOT use this class
class FlushTask(InternalTask):

    def __init__(self, filter_: Callable[[Task], bool]) -> None:
        super(FlushTask, self).__init__(callable_=filter_)

    def __call__(self, task: Task) -> bool:
        return self._callable(task)

    @property
    def priority(self) -> int:
        return 1

# ATTENTION DO NOT use this class
class TerminalTask(InternalTask):

    def __init__(self) -> None:
        super(TerminalTask, self).__init__()

    @property
    def priority(self) -> int:
        return 2

class WorkerError(Exception):

    def __init__(self, message):
        super(WorkerError, self).__init__(message)

def ensure_task(maybe_task: MaybeTask) -> Task:
    if not isinstance(maybe_task, Task):
        maybe_task = Task(maybe_task)
    return maybe_task

async def regular_call(task: Task) -> Awaitable[Any]:
    rv = task()
    if inspect.isawaitable(rv):
        rv = await rv
    return rv
<commit_msg>change exception<commit_after>import functools
import inspect
import itertools
import queue
import threading
from typing import Any, Awaitable, Callable, Iterable, List, Union

from tornado import gen as tg, ioloop as ti
from wcpan.logger import DEBUG, EXCEPTION

RawTask = Callable[[], Any]
MaybeTask = Union['Task', RawTask]
AwaitCallback = Callable[[Any], None]

class AsyncWorker(object):

    def __init__(self) -> None:
        super(AsyncWorker, self).__init__()

        self._thread = None
        self._ready_lock = threading.Condition()
        self._loop = None
        # FIXME thread safe
        self._queue = queue.PriorityQueue()
        self._tail = {}
        self._tail_lock = threading.Lock()

    @property
    def is_alive(self) -> bool:
        return self._thread and self._thread.is_alive()

    def start(self) -> None:
        if not self.is_alive:
            self._thread = threading.Thread(target=self._run)
            self._thread.start()
        with self._ready_lock:
            if self._loop is None:
                if not self._ready_lock.wait_for(lambda: self._loop is not None, 1):
                    raise WorkerError('timeout start')
                    # FIXME cleanup

    def stop(self) -> None:
        if self.is_alive:
            task = TerminalTask()
            self._queue.put(task)
            self._thread.join()
            self._thread = None

    async def do(self, task: MaybeTask) -> Awaitable[Any]:
        if not self.is_alive:
            raise WorkerError('worker is dead')

        task = ensure_task(task)
        fn = functools.partial(self.do_later, task)
        future = tg.Task(fn)
        id_ = task.id_
        self._update_tail(id_, future)

        rv = await future
        return rv

    def do_later(self, task: MaybeTask, callback: AwaitCallback = None) -> None:
        if not self.is_alive:
            raise WorkerError('worker is dead')

        task = ensure_task(task)

        id_ = task.id_
        self._make_tail(id_, callback)

        self._queue.put(task)

    async def flush(self, filter_: Callable[['Task'], bool]) -> Awaitable[None]:
        task = FlushTask(filter_)
        await self.do(task)

    def flush_later(self, filter_: Callable[['Task'], bool], callback: AwaitCallback = None) -> None:
        task = FlushTask(filter_)
        self.do_later(task, callback)

    def _make_tail(self, id_: int, callback: AwaitCallback) -> None:
        with self._tail_lock:
            self._tail[id_] = None, callback

    def _update_tail(self, id_: int, future: tg.Future) -> None:
        assert id_ in self._tail, 'invalid task id'

        with self._tail_lock:
            _, callback = self._tail[id_]
            assert _ is None, 'replacing existing future'

            self._tail[id_] = future, callback

    def _do_flush(self, task: 'FlushTask') -> None:
        with self._get_internal_mutex():
            q = self._get_internal_queue()
            nq = filter(lambda _: not task(_), q)
            nq = list(nq)
            DEBUG('wcpan.worker') << 'flush:' << 'before' << len(q) << 'after' << len(nq)
            self._set_internal_queue(nq)

    def _get_internal_queue(self) -> Iterable:
        return self._queue.queue

    def _set_internal_queue(self, nq: List) -> None:
        self._queue.queue = nq

    def _get_internal_mutex(self):
        return self._queue.mutex

    def _run(self) -> None:
        with self._ready_lock:
            self._loop = ti.IOLoop()
            self._loop.add_callback(self._process)
            self._ready_lock.notify()
        self._loop.start()
        self._loop.close()
        self._loop = None

    async def _process(self) -> Awaitable[None]:
        while True:
            task = self._queue.get()
            rv = None
            exception = None
            try:
                if isinstance(task, TerminalTask):
                    break
                elif isinstance(task, FlushTask):
                    rv = self._do_flush(task)
                else:
                    rv = await regular_call(task)
            except Exception as e:
                exception = e
            finally:
                self._queue.task_done()
                id_ = task.id_
                with self._tail_lock:
                    future, done = self._tail.get(id_, (None, None))
                    if id_ in self._tail:
                        del self._tail[id_]
                if exception:
                    if future:
                        future.set_exception(exception)
                    else:
                        EXCEPTION('wcpan.worker', exception) << 'uncought exception'
                elif done:
                    done(rv)
                else:
                    DEBUG('wcpan.worker') << 'unused return value:' << rv
        self._loop.stop()

@functools.total_ordering
class Task(object):

    _counter = itertools.count()

    def __init__(self, callable_: RawTask = None) -> None:
        super(Task, self).__init__()

        self._callable = callable_
        # FIXME atomic because GIL
        self._id = next(self._counter)

    def __eq__(self, that: 'Task') -> bool:
        return self.equal(that)

    def __lt__(self, that: 'Task') -> bool:
        return self.higher_then(that)

    def __call__(self) -> Any:
        if not self._callable:
            raise NotImplementedError()
        return self._callable()

    # highest first
    @property
    def priority(self) -> int:
        return 0

    @property
    def id_(self) -> int:
        return self._id

    def equal(self, that: 'Task') -> bool:
        if not isinstance(that, self.__class__):
            return NotImplemented
        return self.id_ == that.id_

    def higher_then(self, that: 'Task') -> bool:
        if not isinstance(that, self.__class__):
            return NotImplemented
        if self.priority > that.priority:
            return True
        if self.priority < that.priority:
            return False
        # lower ID was created earlier
        return self.id_ < that.id_

# ATTENTION DO NOT use this class
class InternalTask(Task):

    def __init__(self, *args, **kwargs) -> None:
        super(InternalTask, self).__init__(*args, **kwargs)

    def equal(self, that: Task) -> bool:
        # if not same type, always non-equal
        if self.__class__ is not that.__class__:
            return False
        return self.id_ == that.id_

    def higher_then(self, that: Task) -> bool:
        # if not internal task, it has lower priority
        if not isinstance(that, InternalTask):
            return True
        if self.priority > that.priority:
            return True
        if self.priority < that.priority:
            return False
        # lower ID was created earlier
        return self.id_ < that.id_

# ATTENTION DO NOT use this class
class FlushTask(InternalTask):

    def __init__(self, filter_: Callable[[Task], bool]) -> None:
        super(FlushTask, self).__init__(callable_=filter_)

    def __call__(self, task: Task) -> bool:
        return self._callable(task)

    @property
    def priority(self) -> int:
        return 1

# ATTENTION DO NOT use this class
class TerminalTask(InternalTask):

    def __init__(self) -> None:
        super(TerminalTask, self).__init__()

    @property
    def priority(self) -> int:
        return 2

class WorkerError(Exception):

    def __init__(self, message):
        super(WorkerError, self).__init__(message)

def ensure_task(maybe_task: MaybeTask) -> Task:
    if not isinstance(maybe_task, Task):
        maybe_task = Task(maybe_task)
    return maybe_task

async def regular_call(task: Task) -> Awaitable[Any]:
    rv = task()
    if inspect.isawaitable(rv):
        rv = await rv
    return rv
<|endoftext|>


<commit_before># coding: utf-8
"""
    WeasyPrint
    ==========

    WeasyPrint converts web documents to PDF.

    The public API is what is accessible from this "root" packages
    without importing sub-modules.

    :copyright: Copyright 2011-2014 Simon Sapin and contributors, see AUTHORS.
    :license: BSD, see LICENSE for details.

"""

from __future__ import division, unicode_literals

import contextlib
import html5lib
import cssselect2
import tinycss2

VERSION = '0.39'
__version__ = VERSION

# Used for 'User-Agent' in HTTP and 'Creator' in PDF
VERSION_STRING = 'WeasyPrint %s (http://weasyprint.org/)' % VERSION

__all__ = ['HTML', 'CSS', 'Attachment', 'Document', 'Page',
           'default_url_fetcher', 'VERSION']

# Import after setting the version, as the version is used in other modules
from .urls import (fetch, default_url_fetcher, path2url, ensure_url,
                   url_is_absolute)  # noqa
from .compat import unicode  # noqa
from .logger import LOGGER  # noqa
# Some imports are at the end of the file (after the CSS class)
# to work around circular imports.

class HTML(cssselect2.ElementWrapper):
    """Represents an HTML document parsed by html5lib.

    You can just create an instance with a positional argument:
    ``doc = HTML(something)``
    The class will try to guess if the input is a filename, an absolute URL,
    or a file-like object.

    Alternatively, use **one** named argument so that no guessing is involved:

    :param filename: A filename, relative to the current directory, or
        absolute.
    :param url: An absolute, fully qualified URL.
    :param file_obj: A file-like: any object with a :meth:`~file.read` method.
    :param string: A string of HTML source. (This argument must be named.)

    Specifying multiple inputs is an error:
    ``HTML(filename="foo.html", url="localhost://bar.html")``
    will raise a TypeError.

    You can also pass optional named arguments:

    :param encoding: Force the source character encoding.
    :param base_url: The base used to resolve relative URLs
        (e.g. in ````). If not provided, try to use
        the input filename, URL, or ``name`` attribute of file-like objects.
    :param url_fetcher: A function or other callable
        with the same signature as :func:`default_url_fetcher` called to
        fetch external resources such as stylesheets and images.
        (See :ref:`url-fetchers`.)
    :param media_type: The media type to use for ``@media``.
        Defaults to ``'print'``. **Note:** In some cases like
        ``HTML(string=foo)`` relative URLs will be invalid if ``base_url``
        is not provided.

    """
    def __init__(self, guess=None, filename=None, url=None, file_obj=None,
                 string=None, encoding=None, base_url=None,
                 url_fetcher=default_url_fetcher, media_type='print'):
        result = _select_source(
            guess, filename, url, file_obj, string, base_url, url_fetcher)
        with result as (source_type, source, base_url, protocol_encoding):
            if source_type == 'tree':
                result = source
            else:
                if isinstance(source, unicode):
                    result = html5lib.parse(
                        source, namespaceHTMLElements=False)
                else:
                    result = html5lib.parse(
                        source, override_encoding=encoding,
                        transport_encoding=protocol_encoding,
                        namespaceHTMLElements=False)
                assert result
        base_url = find_base_url(result, base_url)
        super(HTML, self).__init__(
            result, parent=None, index=0, previous=None, in_html_document=True,
            content_language=None, base_url=base_url)
        self.url_fetcher = url_fetcher
        self.media_type = media_type

    def _ua_stylesheets(self):
        return [HTML5_UA_STYLESHEET]

    def _ph_stylesheets(self):
        return [HTML5_PH_STYLESHEET]

    def _get_metadata(self):
        return get_html_metadata(self)

    def iter_children(self):
        child = None
        for i, etree_child in enumerate(self.etree_children):
            child = cssselect2.ElementWrapper(
                etree_child, parent=self, index=i, previous=child,
                in_html_document=True)
            yield child

    def render(self, stylesheets=None, enable_hinting=False,
               presentational_hints=False):
        """Lay out and paginate the document, but do not (yet) export it
        to PDF or another format.

        This returns a :class:`~document.Document` object which provides
        access to individual pages and various meta-data.
        See :meth:`write_pdf` to get a PDF directly.

        .. versionadded:: 0.15

        :param stylesheets:
            An optional list of user stylesheets. List elements are
            :class:`CSS` objects, filenames, URLs, or file-like
            objects. (See :ref:`stylesheet-origins`.)
        :type enable_hinting: bool
        :param enable_hinting:
            Whether text, borders and background should be *hinted* to fall
            at device pixel boundaries. Should be enabled for pixel-based
            output (like PNG) but not for vector-based output (like PDF).
        :type presentational_hints: bool
        :param presentational_hints: Whether HTML presentational hints are
            followed.
        :returns: A :class:`~document.Document` object.

        """
        return Document._render(
            self, stylesheets, enable_hinting, presentational_hints)

    def write_pdf(self, target=None, stylesheets=None, zoom=1,
                  attachments=None, presentational_hints=False):
        """Render the document to a PDF file.

        This is a shortcut for calling :meth:`render`, then
        :meth:`Document.write_pdf() <document.Document.write_pdf>`.

        :param target:
            A filename, file-like object, or :obj:`None`.
        :param stylesheets:
            An optional list of user stylesheets. The list’s elements
            are :class:`CSS` objects, filenames, URLs, or file-like
            objects.  (See :ref:`stylesheet-origins`.)
        :type zoom: float
        :param zoom:
            The zoom factor in PDF units per CSS units.  **Warning**:
            All CSS units are affected, including physical units like
            ``cm`` and named sizes like ``A4``.  For values other than
            1, the physical CSS units will thus be “wrong”.
        :param attachments: A list of additional file attachments for the
            generated PDF document or :obj:`None`. The list's elements are
            :class:`Attachment` objects, filenames, URLs or file-like objects.
        :type presentational_hints: bool
        :param presentational_hints: Whether HTML presentational hints are
            followed.
        :returns:
            The PDF as byte string if :obj:`target` is not provided or
            :obj:`None`, otherwise :obj:`None` (the PDF is written to
            :obj:`target`).

        """
        return self.render(
            stylesheets, enable_hinting=False,
            presentational_hints=presentational_hints).write_pdf(
                target, zoom, attachments)

    def write_image_surface(self, stylesheets=None, resolution=96,
                            presentational_hints=False):
        surface, _width, _height = (
            self.render(stylesheets, enable_hinting=True,
                        presentational_hints=presentational_hints)
            .write_image_surface(resolution))
        return surface

    def write_png(self, target=None, stylesheets=None, resolution=96,
                  presentational_hints=False):
        """Paint the pages vertically to a single PNG image.

        There is no decoration around pages other than those specified in CSS
        with ``@page`` rules. The final image is as wide as the widest page.
        Each page is below the previous one, centered horizontally.

        This is a shortcut for calling :meth:`render`, then
        :meth:`Document.write_png() <document.Document.write_png>`.

        :param target:
            A filename, file-like object, or :obj:`None`.
        :param stylesheets:
            An optional list of user stylesheets.  The list’s elements
            are :class:`CSS` objects, filenames, URLs, or file-like
            objects. (See :ref:`stylesheet-origins`.)
        :type resolution: float
        :param resolution:
            The output resolution in PNG pixels per CSS inch. At 96 dpi
            (the default), PNG pixels match the CSS ``px`` unit.
        :type presentational_hints: bool
        :param presentational_hints: Whether HTML presentational hints are
            followed.
        :returns:
            The image as byte string if :obj:`target` is not provided or
            :obj:`None`, otherwise :obj:`None` (the image is written to
            :obj:`target`.)

        """
        png_bytes, _width, _height = (
            self.render(stylesheets, enable_hinting=True,
                        presentational_hints=presentational_hints)
            .write_png(target, resolution))
        return png_bytes

class CSS(object):
    """Represents a CSS stylesheet parsed by tinycss2.

    An instance is created in the same way as :class:`HTML`, except that
    the ``tree`` argument is not available. All other arguments are the same.

    An additional argument called ``font_config`` must be provided to handle
    ``@font-config`` rules. The same ``fonts.FontConfiguration`` object must be
    used for different ``CSS`` objects applied to the same document.

    ``CSS`` objects have no public attribute or method. They are only meant to
    be used in the :meth:`~HTML.write_pdf`, :meth:`~HTML.write_png` and
    :meth:`~HTML.render` methods of :class:`HTML` objects.

    """
    def __init__(self, guess=None, filename=None, url=None, file_obj=None,
                 string=None, encoding=None, base_url=None,
                 url_fetcher=default_url_fetcher, _check_mime_type=False,
                 media_type='print', font_config=None, matcher=None,
                 page_rules=None):
        result = _select_source(
            guess, filename, url, file_obj, string,
            base_url=base_url, url_fetcher=url_fetcher,
            check_css_mime_type=_check_mime_type)
        with result as (source_type, source, base_url, protocol_encoding):
            if source_type == 'string' and not isinstance(source, bytes):
                # unicode, no encoding
                stylesheet = tinycss2.parse_stylesheet(source)
            else:
                if source_type == 'file_obj':
                    source = source.read()
                stylesheet, encoding = tinycss2.parse_stylesheet_bytes(
                    source, environment_encoding=encoding,
                    protocol_encoding=protocol_encoding)
        self.base_url = base_url
        self.matcher = matcher or cssselect2.Matcher()
        self.page_rules = page_rules or []
        # TODO: fonts are stored here and should be cleaned after rendering
        self.fonts = []
        preprocess_stylesheet(
            media_type, base_url, stylesheet, url_fetcher, self.matcher,
            self.page_rules, self.fonts, font_config)

class Attachment(object):
    """Represents a file attachment for a PDF document.

    An instance is created in the same way as :class:`HTML`, except that
    the HTML specific arguments are not supported. An optional description can
    be provided with the ``description`` argument.

    :param description: A description of the attachment to be included in the
        PDF document. May be :obj:`None`

    """
    def __init__(self, guess=None, filename=None, url=None, file_obj=None,
                 string=None, base_url=None, url_fetcher=default_url_fetcher,
                 description=None):
        self.source = _select_source(
            guess, filename, url, file_obj, string,
            base_url=base_url, url_fetcher=url_fetcher)
        self.description = description

@contextlib.contextmanager
def _select_source(guess=None, filename=None, url=None, file_obj=None,
                   string=None, base_url=None, url_fetcher=default_url_fetcher,
                   check_css_mime_type=False):
    """
    Check that only one input is not None, and return it with the
    normalized ``base_url``.

    """
    if base_url is not None:
        base_url = ensure_url(base_url)

    nones = [
        param is None for param in (guess, filename, url, file_obj, string)]
    if nones == [False, True, True, True, True]:
        if hasattr(guess, 'read'):
            type_ = 'file_obj'
        elif url_is_absolute(guess):
            type_ = 'url'
        else:
            type_ = 'filename'
        result = _select_source(
            base_url=base_url, url_fetcher=url_fetcher,
            check_css_mime_type=check_css_mime_type,
            # Use str() to work around http://bugs.python.org/issue4978
            # See https://github.com/Kozea/WeasyPrint/issues/97
            **{str(type_): guess})
        with result as result:
            yield result
    elif nones == [True, False, True, True, True]:
        if base_url is None:
            base_url = path2url(filename)
        with open(filename, 'rb') as file_obj:
            yield 'file_obj', file_obj, base_url, None
    elif nones == [True, True, False, True, True]:
        with fetch(url_fetcher, url) as result:
            if check_css_mime_type and result['mime_type'] != 'text/css':
                LOGGER.warning(
                    'Unsupported stylesheet type %s for %s',
                    result['mime_type'], result['redirected_url'])
                yield 'string', '', base_url, None
            else:
                proto_encoding = result.get('encoding')
                if base_url is None:
                    base_url = result.get('redirected_url', url)
                if 'string' in result:
                    yield 'string', result['string'], base_url, proto_encoding
                else:
                    yield (
                        'file_obj', result['file_obj'], base_url,
                        proto_encoding)
    elif nones == [True, True, True, False, True]:
        if base_url is None:
            # filesystem file-like objects have a 'name' attribute.
            name = getattr(file_obj, 'name', None)
            # Some streams have a .name like '<stdin>', not a filename.
            if name and not name.startswith('<'):
                base_url = ensure_url(name)
        yield 'file_obj', file_obj, base_url, None
    elif nones == [True, True, True, True, False]:
        yield 'string', string, base_url, None
    else:
        raise TypeError('Expected exactly one source, got ' + (
            ', '.join(
                name for i, name in enumerate(
                    ('guess', 'filename', 'url', 'file_obj', 'string'))
                if not nones[i]
            ) or 'nothing'
        ))

# Work around circular imports.
from .css import preprocess_stylesheet  # noqa
from .html import (
    find_base_url, HTML5_UA_STYLESHEET, HTML5_PH_STYLESHEET,
    get_html_metadata)  # noqa
from .document import Document, Page  # noqa
<commit_msg>Don't test 'tree' as a possible HTML source<commit_after># coding: utf-8
"""
    WeasyPrint
    ==========

    WeasyPrint converts web documents to PDF.

    The public API is what is accessible from this "root" packages
    without importing sub-modules.

    :copyright: Copyright 2011-2014 Simon Sapin and contributors, see AUTHORS.
    :license: BSD, see LICENSE for details.

"""

from __future__ import division, unicode_literals

import contextlib
import html5lib
import cssselect2
import tinycss2

VERSION = '0.39'
__version__ = VERSION

# Used for 'User-Agent' in HTTP and 'Creator' in PDF
VERSION_STRING = 'WeasyPrint %s (http://weasyprint.org/)' % VERSION

__all__ = ['HTML', 'CSS', 'Attachment', 'Document', 'Page',
           'default_url_fetcher', 'VERSION']

# Import after setting the version, as the version is used in other modules
from .urls import (fetch, default_url_fetcher, path2url, ensure_url,
                   url_is_absolute)  # noqa
from .compat import unicode  # noqa
from .logger import LOGGER  # noqa
# Some imports are at the end of the file (after the CSS class)
# to work around circular imports.

class HTML(cssselect2.ElementWrapper):
    """Represents an HTML document parsed by html5lib.

    You can just create an instance with a positional argument:
    ``doc = HTML(something)``
    The class will try to guess if the input is a filename, an absolute URL,
    or a file-like object.

    Alternatively, use **one** named argument so that no guessing is involved:

    :param filename: A filename, relative to the current directory, or
        absolute.
    :param url: An absolute, fully qualified URL.
    :param file_obj: A file-like: any object with a :meth:`~file.read` method.
    :param string: A string of HTML source. (This argument must be named.)

    Specifying multiple inputs is an error:
    ``HTML(filename="foo.html", url="localhost://bar.html")``
    will raise a TypeError.

    You can also pass optional named arguments:

    :param encoding: Force the source character encoding.
    :param base_url: The base used to resolve relative URLs
        (e.g. in ````). If not provided, try to use
        the input filename, URL, or ``name`` attribute of file-like objects.
    :param url_fetcher: A function or other callable
        with the same signature as :func:`default_url_fetcher` called to
        fetch external resources such as stylesheets and images.
        (See :ref:`url-fetchers`.)
    :param media_type: The media type to use for ``@media``.
        Defaults to ``'print'``. **Note:** In some cases like
        ``HTML(string=foo)`` relative URLs will be invalid if ``base_url``
        is not provided.

    """
    def __init__(self, guess=None, filename=None, url=None, file_obj=None,
                 string=None, encoding=None, base_url=None,
                 url_fetcher=default_url_fetcher, media_type='print'):
        result = _select_source(
            guess, filename, url, file_obj, string, base_url, url_fetcher)
        with result as (source_type, source, base_url, protocol_encoding):
            if isinstance(source, unicode):
                result = html5lib.parse(
                    source, namespaceHTMLElements=False)
            else:
                result = html5lib.parse(
                    source, override_encoding=encoding,
                    transport_encoding=protocol_encoding,
                    namespaceHTMLElements=False)
            assert result
        base_url = find_base_url(result, base_url)
        super(HTML, self).__init__(
            result, parent=None, index=0, previous=None, in_html_document=True,
            content_language=None, base_url=base_url)
        self.url_fetcher = url_fetcher
        self.media_type = media_type

    def _ua_stylesheets(self):
        return [HTML5_UA_STYLESHEET]

    def _ph_stylesheets(self):
        return [HTML5_PH_STYLESHEET]

    def _get_metadata(self):
        return get_html_metadata(self)

    def iter_children(self):
        child = None
        for i, etree_child in enumerate(self.etree_children):
            child = cssselect2.ElementWrapper(
                etree_child, parent=self, index=i, previous=child,
                in_html_document=True)
            yield child

    def render(self, stylesheets=None, enable_hinting=False,
               presentational_hints=False):
        """Lay out and paginate the document, but do not (yet) export it
        to PDF or another format.

        This returns a :class:`~document.Document` object which provides
        access to individual pages and various meta-data.
        See :meth:`write_pdf` to get a PDF directly.

        .. versionadded:: 0.15

        :param stylesheets:
            An optional list of user stylesheets. List elements are
            :class:`CSS` objects, filenames, URLs, or file-like
            objects. (See :ref:`stylesheet-origins`.)
        :type enable_hinting: bool
        :param enable_hinting:
            Whether text, borders and background should be *hinted* to fall
            at device pixel boundaries. Should be enabled for pixel-based
            output (like PNG) but not for vector-based output (like PDF).
        :type presentational_hints: bool
        :param presentational_hints: Whether HTML presentational hints are
            followed.
        :returns: A :class:`~document.Document` object.

        """
        return Document._render(
            self, stylesheets, enable_hinting, presentational_hints)

    def write_pdf(self, target=None, stylesheets=None, zoom=1,
                  attachments=None, presentational_hints=False):
        """Render the document to a PDF file.

        This is a shortcut for calling :meth:`render`, then
        :meth:`Document.write_pdf() <document.Document.write_pdf>`.

        :param target:
            A filename, file-like object, or :obj:`None`.
        :param stylesheets:
            An optional list of user stylesheets. The list’s elements
            are :class:`CSS` objects, filenames, URLs, or file-like
            objects.  (See :ref:`stylesheet-origins`.)
        :type zoom: float
        :param zoom:
            The zoom factor in PDF units per CSS units.  **Warning**:
            All CSS units are affected, including physical units like
            ``cm`` and named sizes like ``A4``.  For values other than
            1, the physical CSS units will thus be “wrong”.
        :param attachments: A list of additional file attachments for the
            generated PDF document or :obj:`None`. The list's elements are
            :class:`Attachment` objects, filenames, URLs or file-like objects.
        :type presentational_hints: bool
        :param presentational_hints: Whether HTML presentational hints are
            followed.
        :returns:
            The PDF as byte string if :obj:`target` is not provided or
            :obj:`None`, otherwise :obj:`None` (the PDF is written to
            :obj:`target`).

        """
        return self.render(
            stylesheets, enable_hinting=False,
            presentational_hints=presentational_hints).write_pdf(
                target, zoom, attachments)

    def write_image_surface(self, stylesheets=None, resolution=96,
                            presentational_hints=False):
        surface, _width, _height = (
            self.render(stylesheets, enable_hinting=True,
                        presentational_hints=presentational_hints)
            .write_image_surface(resolution))
        return surface

    def write_png(self, target=None, stylesheets=None, resolution=96,
                  presentational_hints=False):
        """Paint the pages vertically to a single PNG image.

        There is no decoration around pages other than those specified in CSS
        with ``@page`` rules. The final image is as wide as the widest page.
        Each page is below the previous one, centered horizontally.

        This is a shortcut for calling :meth:`render`, then
        :meth:`Document.write_png() <document.Document.write_png>`.

        :param target:
            A filename, file-like object, or :obj:`None`.
        :param stylesheets:
            An optional list of user stylesheets.  The list’s elements
            are :class:`CSS` objects, filenames, URLs, or file-like
            objects. (See :ref:`stylesheet-origins`.)
        :type resolution: float
        :param resolution:
            The output resolution in PNG pixels per CSS inch. At 96 dpi
            (the default), PNG pixels match the CSS ``px`` unit.
        :type presentational_hints: bool
        :param presentational_hints: Whether HTML presentational hints are
            followed.
        :returns:
            The image as byte string if :obj:`target` is not provided or
            :obj:`None`, otherwise :obj:`None` (the image is written to
            :obj:`target`.)

        """
        png_bytes, _width, _height = (
            self.render(stylesheets, enable_hinting=True,
                        presentational_hints=presentational_hints)
            .write_png(target, resolution))
        return png_bytes

class CSS(object):
    """Represents a CSS stylesheet parsed by tinycss2.

    An instance is created in the same way as :class:`HTML`, except that
    the ``tree`` argument is not available. All other arguments are the same.

    An additional argument called ``font_config`` must be provided to handle
    ``@font-config`` rules. The same ``fonts.FontConfiguration`` object must be
    used for different ``CSS`` objects applied to the same document.

    ``CSS`` objects have no public attribute or method. They are only meant to
    be used in the :meth:`~HTML.write_pdf`, :meth:`~HTML.write_png` and
    :meth:`~HTML.render` methods of :class:`HTML` objects.

    """
    def __init__(self, guess=None, filename=None, url=None, file_obj=None,
                 string=None, encoding=None, base_url=None,
                 url_fetcher=default_url_fetcher, _check_mime_type=False,
                 media_type='print', font_config=None, matcher=None,
                 page_rules=None):
        result = _select_source(
            guess, filename, url, file_obj, string,
            base_url=base_url, url_fetcher=url_fetcher,
            check_css_mime_type=_check_mime_type)
        with result as (source_type, source, base_url, protocol_encoding):
            if source_type == 'string' and not isinstance(source, bytes):
                # unicode, no encoding
                stylesheet = tinycss2.parse_stylesheet(source)
            else:
                if source_type == 'file_obj':
                    source = source.read()
                stylesheet, encoding = tinycss2.parse_stylesheet_bytes(
                    source, environment_encoding=encoding,
                    protocol_encoding=protocol_encoding)
        self.base_url = base_url
        self.matcher = matcher or cssselect2.Matcher()
        self.page_rules = page_rules or []
        # TODO: fonts are stored here and should be cleaned after rendering
        self.fonts = []
        preprocess_stylesheet(
            media_type, base_url, stylesheet, url_fetcher, self.matcher,
            self.page_rules, self.fonts, font_config)

class Attachment(object):
    """Represents a file attachment for a PDF document.

    An instance is created in the same way as :class:`HTML`, except that
    the HTML specific arguments are not supported. An optional description can
    be provided with the ``description`` argument.

    :param description: A description of the attachment to be included in the
        PDF document. May be :obj:`None`

    """
    def __init__(self, guess=None, filename=None, url=None, file_obj=None,
                 string=None, base_url=None, url_fetcher=default_url_fetcher,
                 description=None):
        self.source = _select_source(
            guess, filename, url, file_obj, string,
            base_url=base_url, url_fetcher=url_fetcher)
        self.description = description

@contextlib.contextmanager
def _select_source(guess=None, filename=None, url=None, file_obj=None,
                   string=None, base_url=None, url_fetcher=default_url_fetcher,
                   check_css_mime_type=False):
    """
    Check that only one input is not None, and return it with the
    normalized ``base_url``.

    """
    if base_url is not None:
        base_url = ensure_url(base_url)

    nones = [
        param is None for param in (guess, filename, url, file_obj, string)]
    if nones == [False, True, True, True, True]:
        if hasattr(guess, 'read'):
            type_ = 'file_obj'
        elif url_is_absolute(guess):
            type_ = 'url'
        else:
            type_ = 'filename'
        result = _select_source(
            base_url=base_url, url_fetcher=url_fetcher,
            check_css_mime_type=check_css_mime_type,
            # Use str() to work around http://bugs.python.org/issue4978
            # See https://github.com/Kozea/WeasyPrint/issues/97
            **{str(type_): guess})
        with result as result:
            yield result
    elif nones == [True, False, True, True, True]:
        if base_url is None:
            base_url = path2url(filename)
        with open(filename, 'rb') as file_obj:
            yield 'file_obj', file_obj, base_url, None
    elif nones == [True, True, False, True, True]:
        with fetch(url_fetcher, url) as result:
            if check_css_mime_type and result['mime_type'] != 'text/css':
                LOGGER.warning(
                    'Unsupported stylesheet type %s for %s',
                    result['mime_type'], result['redirected_url'])
                yield 'string', '', base_url, None
            else:
                proto_encoding = result.get('encoding')
                if base_url is None:
                    base_url = result.get('redirected_url', url)
                if 'string' in result:
                    yield 'string', result['string'], base_url, proto_encoding
                else:
                    yield (
                        'file_obj', result['file_obj'], base_url,
                        proto_encoding)
    elif nones == [True, True, True, False, True]:
        if base_url is None:
            # filesystem file-like objects have a 'name' attribute.
            name = getattr(file_obj, 'name', None)
            # Some streams have a .name like '<stdin>', not a filename.
            if name and not name.startswith('<'):
                base_url = ensure_url(name)
        yield 'file_obj', file_obj, base_url, None
    elif nones == [True, True, True, True, False]:
        yield 'string', string, base_url, None
    else:
        raise TypeError('Expected exactly one source, got ' + (
            ', '.join(
                name for i, name in enumerate(
                    ('guess', 'filename', 'url', 'file_obj', 'string'))
                if not nones[i]
            ) or 'nothing'
        ))

# Work around circular imports.
from .css import preprocess_stylesheet  # noqa
from .html import (
    find_base_url, HTML5_UA_STYLESHEET, HTML5_PH_STYLESHEET,
    get_html_metadata)  # noqa
from .document import Document, Page  # noqa
<|endoftext|>


<commit_before># Copyright (c) 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest

from swift3 import utils

strs = [
    ('Owner', 'owner'),
    ('DisplayName', 'display_name'),
    ('AccessControlPolicy', 'access_control_policy'),
]

class TestSwift3Utils(unittest.TestCase):
    def test_camel_to_snake(self):
        for s1, s2 in strs:
            self.assertEquals(utils.camel_to_snake(s1), s2)

    def test_snake_to_camel(self):
        for s1, s2 in strs:
            self.assertEquals(s1, utils.snake_to_camel(s2))
<commit_msg>test/unit: allow running test directly<commit_after># Copyright (c) 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest

from swift3 import utils

strs = [
    ('Owner', 'owner'),
    ('DisplayName', 'display_name'),
    ('AccessControlPolicy', 'access_control_policy'),
]

class TestSwift3Utils(unittest.TestCase):
    def test_camel_to_snake(self):
        for s1, s2 in strs:
            self.assertEquals(utils.camel_to_snake(s1), s2)

    def test_snake_to_camel(self):
        for s1, s2 in strs:
            self.assertEquals(s1, utils.snake_to_camel(s2))

if __name__ == '__main__':
    unittest.main()
<|endoftext|>


<commit_before>from django.conf import settings
from django.contrib.auth.tokens import default_token_generator
from django.core.mail import EmailMessage
from django.template import loader, Template, Context
from enum import Enum
from html.parser import unescape
from common.models.tags import Tag
from common.helpers.date_helpers import DateTimeFormats, datetime_field_to_datetime, datetime_to_string
from democracylab.models import Contributor
from civictechprojects.models import VolunteerRelation
from common.helpers.constants import FrontEndSection
from common.helpers.front_end import section_url

class EmailSection(Enum):
    Header = 'Header'
    Button = 'Button'
    Paragraph = 'Paragraph'

class HtmlEmailTemplate:
    base_template = loader.get_template('html_email_frame.html')
    section_templates = {
        EmailSection.Header: loader.get_template('html_email_header.html'),
        EmailSection.Button: loader.get_template('html_email_button.html'),
        EmailSection.Paragraph: loader.get_template('html_email_paragraph.html')
    }

    def __init__(self):
        self.sections = []
        self.hydrated_template = None

    def add(self, section_type, section_content):
        self.sections.append(HtmlEmailTemplate.section_templates[section_type].render(section_content))
        return self

    def header(self, text):
        return self.add(EmailSection.Header, {'text': text})

    def paragraph(self, text):
        return self.add(EmailSection.Paragraph, {'text': text})

    def button(self, url, text):
        return self.add(EmailSection.Button, {'url': url, 'text': text})

    def render(self, email_msg, context=None):
        if self.hydrated_template is None:
            sections_text = ''.join(self.sections)
            self.hydrated_template = Template(HtmlEmailTemplate.base_template.render({"content": sections_text}))
        email_msg.content_subtype = "html"
        # For some reason xml markup characters in the template (<,>) get converted to entity codes (< and &rt;)
        # We unescape to convert the markup characters back
        email_msg.body = unescape(self.hydrated_template.render(Context(context or {})))
        return email_msg

def send_verification_email(contributor):
    # Get token
    user = Contributor.objects.get(id=contributor.id)
    verification_token = default_token_generator.make_token(user)
    verification_url = settings.PROTOCOL_DOMAIN + '/verify_user/' + str(contributor.id) + '/' + verification_token
    # Send email with token
    email_template = HtmlEmailTemplate()\
        .header("Hi {{first_name}}, we're glad you're here.")\
        .paragraph('Please confirm your email address by clicking the button below.')\
        .button(url=verification_url, text='VERIFY YOUR EMAIL')
    email_msg = EmailMessage(
        subject='Welcome to DemocracyLab',
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=[contributor.email]
    )
    email_msg = email_template.render(email_msg, {'first_name': user.first_name})
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_password_reset_email(contributor):
    # Get token
    user = Contributor.objects.get(id=contributor.id)
    reset_parameters = {
        'userId': contributor.id,
        'token': default_token_generator.make_token(user)
    }
    reset_url = section_url(FrontEndSection.ChangePassword, reset_parameters)
    # Send email with token
    email_template = HtmlEmailTemplate()\
        .header("Hi {{first_name}}.")\
        .paragraph('Please click below to reset your password.')\
        .button(url=reset_url, text='RESET PASSWORD')
    email_msg = EmailMessage(
        subject='DemocracyLab Password Reset',
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=[contributor.email]
    )
    email_msg = email_template.render(email_msg, {'first_name': user.first_name})
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_project_creation_notification(project):
    project_url = settings.PROTOCOL_DOMAIN + '/index/?section=AboutProject&id=' + str(project.id)

    verification_url = settings.PROTOCOL_DOMAIN + '/projects/approve/' + str(project.id)
    email_template = HtmlEmailTemplate() \
        .paragraph('{first_name} {last_name}({email}) has created the project "{project_name}": \n {project_url}'.format(
            first_name=project.project_creator.first_name,
            last_name=project.project_creator.last_name,
            email=project.project_creator.email,
            project_name=project.project_name,
            project_url=project_url
        )) \
        .button(url=verification_url, text='APPROVE')
    email_msg = EmailMessage(
        subject='New DemocracyLab Project: ' + project.project_name,
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=[settings.ADMIN_EMAIL]
    )
    email_msg = email_template.render(email_msg)
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_to_project_owners(project, sender, subject, template):
    project_volunteers = VolunteerRelation.objects.filter(project=project.id)
    co_owner_emails = list(map(lambda co: co.volunteer.email, list(filter(lambda v: v.is_co_owner, project_volunteers))))
    email_msg = EmailMessage(
        subject=subject,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=[project.project_creator.email] + co_owner_emails,
        reply_to=[sender.email]
    )
    email_msg = template.render(email_msg)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

def send_to_project_volunteer(volunteer_relation, subject, body):
    project_volunteers = VolunteerRelation.objects.filter(project=volunteer_relation.project.id)
    co_owner_emails = list(map(lambda co: co.volunteer.email, list(filter(lambda v: v.is_co_owner, project_volunteers))))
    email_msg = EmailMessage(
        subject=subject,
        body=body,
        from_email=settings.EMAIL_VOLUNTEER_ACCT['from_name'],
        to=[volunteer_relation.volunteer.email],
        reply_to=[volunteer_relation.project.project_creator.email] + co_owner_emails,
    )
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

def send_volunteer_application_email(volunteer_relation, is_reminder=False):
    project = volunteer_relation.project
    user = volunteer_relation.volunteer
    role_details = Tag.from_field(volunteer_relation.role)
    role_text = "{subcategory}: {name}".format(subcategory=role_details.subcategory, name=role_details.display_name)
    project_profile_url = settings.PROTOCOL_DOMAIN + '/index/?section=AboutProject&id=' + str(project.id)
    email_subject = '{is_reminder}{firstname} {lastname} would like to volunteer with {project} as {role}'.format(
        is_reminder='REMINDER: ' if is_reminder else '',
        firstname=user.first_name,
        lastname=user.last_name,
        project=project.project_name,
        role=role_text)
    email_template = HtmlEmailTemplate()\
        .header("You Have a New Volunteer!")\
        .paragraph('\"{message}\" -{firstname} {lastname}'.format(
            message=volunteer_relation.application_text,
            firstname=user.first_name,
            lastname=user.last_name))\
        .paragraph('Please click below to review this volunteer')\
        .button(url=project_profile_url, text='REVIEW VOLUNTEER')
    send_to_project_owners(project=project, sender=user, subject=email_subject, template=email_template)

volunteer_conclude_email_template = HtmlEmailTemplate() \
    .header("How has your experience been at {{project_name}}?") \
    .paragraph("Hi {{first_name}},") \
    .paragraph("We're always looking for ways to improve the connection between volunteers and tech-for-good projects.  "
               "We've developed this super-short survey and we'd love to hear from you.  It will take less than a minute "
               "and will help us make DemocracyLab even better.") \
    .button(url=settings.VOLUNTEER_CONCLUDE_SURVEY_URL, text='TAKE OUR SURVEY')

def send_volunteer_conclude_email(volunteer, project_name):
    context = {
        'first_name': volunteer.first_name,
        'project_name': project_name,
    }
    email_msg = EmailMessage(
        subject="Tell us about your experience with " + project_name,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=[volunteer.email],
    )
    email_msg = volunteer_conclude_email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

notify_volunteer_renewed_email_no_comment = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has renewed their volunteer commitment with {{project_name}} until {{projected_end_date}}.")

notify_volunteer_renewed_email_with_comments = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has renewed their volunteer commitment with {{project_name}} until {{projected_end_date}}, "
               "and would like to convey the following:") \
    .paragraph('"{{comments}}"')

def notify_project_owners_volunteer_renewed_email(volunteer_relation, comments):
    email_template = notify_volunteer_renewed_email_with_comments if comments else notify_volunteer_renewed_email_no_comment
    project_name = volunteer_relation.project.project_name
    projected_end_date = datetime_to_string(datetime_field_to_datetime(volunteer_relation.projected_end_date), DateTimeFormats.DATE_LOCALIZED)

    volunteer_name = volunteer_relation.volunteer.full_name()
    context = {
        'volunteer_name': volunteer_name,
        'project_name': project_name,
        'projected_end_date': projected_end_date,
        'comments': comments
    }
    email_msg = EmailMessage(
        subject=volunteer_name + " has renewed their volunteer commitment with " + project_name,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=_get_co_owner_emails(volunteer_relation.project)
    )
    email_msg = email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

notify_volunteer_concluded_email_no_comment = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has concluded their volunteer commitment with {{project_name}}.")

notify_volunteer_concluded_email_with_comments = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has concluded their volunteer commitment with {{project_name}}, "
               "and would like to convey the following:") \
    .paragraph('"{{comments}}"')

def notify_project_owners_volunteer_concluded_email(volunteer_relation, comments=None):
    email_template = notify_volunteer_concluded_email_with_comments if comments else notify_volunteer_concluded_email_no_comment
    project_name = volunteer_relation.project.project_name

    volunteer_name = volunteer_relation.volunteer.full_name()
    context = {
        'volunteer_name': volunteer_name,
        'project_name': project_name,
        'comments': comments
    }
    email_msg = EmailMessage(
        subject=volunteer_name + " has concluded their volunteer commitment with " + project_name,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=_get_co_owner_emails(volunteer_relation.project)
    )
    email_msg = email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

def notify_project_owners_project_approved(project):
    email_template = HtmlEmailTemplate() \
        .paragraph('Your project "{{project_name}}" has been approved. You can see it at {{project_url}}')
    context = {
        'project_name': project.project_name,
        'project_url': settings.PROTOCOL_DOMAIN + '/index/?section=AboutProject&id=' + str(project.id)
    }
    email_msg = EmailMessage(
        subject=project.project_name + " has been approved",
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=_get_co_owner_emails(project)
    )
    email_msg = email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_email(email_msg, email_acct=None):
    if not settings.FAKE_EMAILS:
        email_msg.connection = email_acct['connection'] if email_acct is not None else settings.EMAIL_SUPPORT_ACCT['connection']
    else:
        test_email_subject = 'TEST EMAIL: ' + email_msg.subject
        test_email_body = '<!--\n Environment:{environment}\nTO: {to_line}\nREPLY-TO: {reply_to}\n -->\n{body}'.format(
            environment=settings.PROTOCOL_DOMAIN,
            to_line=email_msg.to,
            reply_to=email_msg.reply_to,
            body=email_msg.body
        )
        email_msg.subject = test_email_subject
        email_msg.body = test_email_body
        email_msg.to = [settings.ADMIN_EMAIL]
        if settings.EMAIL_SUPPORT_ACCT:
            email_msg.connection = settings.EMAIL_SUPPORT_ACCT['connection']
    email_msg.send()

def _get_co_owner_emails(project):
    return list(map(lambda co_owner: co_owner.email, project.all_owners()))

def _get_account_from_email(email_acct):
    return email_acct['from_name'] if email_acct is not None else 'DemocracyLab'
<commit_msg>Add review volunteer button to project owner email<commit_after>from django.conf import settings
from django.contrib.auth.tokens import default_token_generator
from django.core.mail import EmailMessage
from django.template import loader, Template, Context
from enum import Enum
from html.parser import unescape
from common.models.tags import Tag
from common.helpers.date_helpers import DateTimeFormats, datetime_field_to_datetime, datetime_to_string
from democracylab.models import Contributor
from civictechprojects.models import VolunteerRelation
from common.helpers.constants import FrontEndSection
from common.helpers.front_end import section_url

class EmailSection(Enum):
    Header = 'Header'
    Button = 'Button'
    Paragraph = 'Paragraph'

class HtmlEmailTemplate:
    base_template = loader.get_template('html_email_frame.html')
    section_templates = {
        EmailSection.Header: loader.get_template('html_email_header.html'),
        EmailSection.Button: loader.get_template('html_email_button.html'),
        EmailSection.Paragraph: loader.get_template('html_email_paragraph.html')
    }

    def __init__(self):
        self.sections = []
        self.hydrated_template = None

    def add(self, section_type, section_content):
        self.sections.append(HtmlEmailTemplate.section_templates[section_type].render(section_content))
        return self

    def header(self, text):
        return self.add(EmailSection.Header, {'text': text})

    def paragraph(self, text):
        return self.add(EmailSection.Paragraph, {'text': text})

    def button(self, url, text):
        return self.add(EmailSection.Button, {'url': url, 'text': text})

    def render(self, email_msg, context=None):
        if self.hydrated_template is None:
            sections_text = ''.join(self.sections)
            self.hydrated_template = Template(HtmlEmailTemplate.base_template.render({"content": sections_text}))
        email_msg.content_subtype = "html"
        # For some reason xml markup characters in the template (<,>) get converted to entity codes (< and &rt;)
        # We unescape to convert the markup characters back
        email_msg.body = unescape(self.hydrated_template.render(Context(context or {})))
        return email_msg

def send_verification_email(contributor):
    # Get token
    user = Contributor.objects.get(id=contributor.id)
    verification_token = default_token_generator.make_token(user)
    verification_url = settings.PROTOCOL_DOMAIN + '/verify_user/' + str(contributor.id) + '/' + verification_token
    # Send email with token
    email_template = HtmlEmailTemplate()\
        .header("Hi {{first_name}}, we're glad you're here.")\
        .paragraph('Please confirm your email address by clicking the button below.')\
        .button(url=verification_url, text='VERIFY YOUR EMAIL')
    email_msg = EmailMessage(
        subject='Welcome to DemocracyLab',
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=[contributor.email]
    )
    email_msg = email_template.render(email_msg, {'first_name': user.first_name})
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_password_reset_email(contributor):
    # Get token
    user = Contributor.objects.get(id=contributor.id)
    reset_parameters = {
        'userId': contributor.id,
        'token': default_token_generator.make_token(user)
    }
    reset_url = section_url(FrontEndSection.ChangePassword, reset_parameters)
    # Send email with token
    email_template = HtmlEmailTemplate()\
        .header("Hi {{first_name}}.")\
        .paragraph('Please click below to reset your password.')\
        .button(url=reset_url, text='RESET PASSWORD')
    email_msg = EmailMessage(
        subject='DemocracyLab Password Reset',
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=[contributor.email]
    )
    email_msg = email_template.render(email_msg, {'first_name': user.first_name})
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_project_creation_notification(project):
    project_url = settings.PROTOCOL_DOMAIN + '/index/?section=AboutProject&id=' + str(project.id)

    verification_url = settings.PROTOCOL_DOMAIN + '/projects/approve/' + str(project.id)
    email_template = HtmlEmailTemplate() \
        .paragraph('{first_name} {last_name}({email}) has created the project "{project_name}": \n {project_url}'.format(
            first_name=project.project_creator.first_name,
            last_name=project.project_creator.last_name,
            email=project.project_creator.email,
            project_name=project.project_name,
            project_url=project_url
        )) \
        .button(url=verification_url, text='APPROVE')
    email_msg = EmailMessage(
        subject='New DemocracyLab Project: ' + project.project_name,
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=[settings.ADMIN_EMAIL]
    )
    email_msg = email_template.render(email_msg)
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_to_project_owners(project, sender, subject, template):
    project_volunteers = VolunteerRelation.objects.filter(project=project.id)
    co_owner_emails = list(map(lambda co: co.volunteer.email, list(filter(lambda v: v.is_co_owner, project_volunteers))))
    email_msg = EmailMessage(
        subject=subject,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=[project.project_creator.email] + co_owner_emails,
        reply_to=[sender.email]
    )
    email_msg = template.render(email_msg)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

def send_to_project_volunteer(volunteer_relation, subject, body):
    project_volunteers = VolunteerRelation.objects.filter(project=volunteer_relation.project.id)
    co_owner_emails = list(map(lambda co: co.volunteer.email, list(filter(lambda v: v.is_co_owner, project_volunteers))))
    email_msg = EmailMessage(
        subject=subject,
        body=body,
        from_email=settings.EMAIL_VOLUNTEER_ACCT['from_name'],
        to=[volunteer_relation.volunteer.email],
        reply_to=[volunteer_relation.project.project_creator.email] + co_owner_emails,
    )
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

def send_volunteer_application_email(volunteer_relation, is_reminder=False):
    project = volunteer_relation.project
    user = volunteer_relation.volunteer
    role_details = Tag.from_field(volunteer_relation.role)
    role_text = "{subcategory}: {name}".format(subcategory=role_details.subcategory, name=role_details.display_name)
    project_profile_url = settings.PROTOCOL_DOMAIN + '/index/?section=AboutProject&id=' + str(project.id)
    approve_url = settings.PROTOCOL_DOMAIN + '/volunteer/approve/' + str(volunteer_relation.id) + '/'
    email_subject = '{is_reminder}{firstname} {lastname} would like to volunteer with {project} as {role}'.format(
        is_reminder='REMINDER: ' if is_reminder else '',
        firstname=user.first_name,
        lastname=user.last_name,
        project=project.project_name,
        role=role_text)
    email_template = HtmlEmailTemplate()\
        .header("You Have a New Volunteer!")\
        .paragraph('\"{message}\" -{firstname} {lastname}'.format(
            message=volunteer_relation.application_text,
            firstname=user.first_name,
            lastname=user.last_name))\
        .paragraph('Please click below to review this volunteer')\
        .button(url=project_profile_url, text='REVIEW VOLUNTEER')\
        .button(url=approve_url, text='APPROVE VOLUNTEER')
    send_to_project_owners(project=project, sender=user, subject=email_subject, template=email_template)

volunteer_conclude_email_template = HtmlEmailTemplate() \
    .header("How has your experience been at {{project_name}}?") \
    .paragraph("Hi {{first_name}},") \
    .paragraph("We're always looking for ways to improve the connection between volunteers and tech-for-good projects.  "
               "We've developed this super-short survey and we'd love to hear from you.  It will take less than a minute "
               "and will help us make DemocracyLab even better.") \
    .button(url=settings.VOLUNTEER_CONCLUDE_SURVEY_URL, text='TAKE OUR SURVEY')

def send_volunteer_conclude_email(volunteer, project_name):
    context = {
        'first_name': volunteer.first_name,
        'project_name': project_name,
    }
    email_msg = EmailMessage(
        subject="Tell us about your experience with " + project_name,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=[volunteer.email],
    )
    email_msg = volunteer_conclude_email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

notify_volunteer_renewed_email_no_comment = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has renewed their volunteer commitment with {{project_name}} until {{projected_end_date}}.")

notify_volunteer_renewed_email_with_comments = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has renewed their volunteer commitment with {{project_name}} until {{projected_end_date}}, "
               "and would like to convey the following:") \
    .paragraph('"{{comments}}"')

def notify_project_owners_volunteer_renewed_email(volunteer_relation, comments):
    email_template = notify_volunteer_renewed_email_with_comments if comments else notify_volunteer_renewed_email_no_comment
    project_name = volunteer_relation.project.project_name
    projected_end_date = datetime_to_string(datetime_field_to_datetime(volunteer_relation.projected_end_date), DateTimeFormats.DATE_LOCALIZED)

    volunteer_name = volunteer_relation.volunteer.full_name()
    context = {
        'volunteer_name': volunteer_name,
        'project_name': project_name,
        'projected_end_date': projected_end_date,
        'comments': comments
    }
    email_msg = EmailMessage(
        subject=volunteer_name + " has renewed their volunteer commitment with " + project_name,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=_get_co_owner_emails(volunteer_relation.project)
    )
    email_msg = email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

notify_volunteer_concluded_email_no_comment = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has concluded their volunteer commitment with {{project_name}}.")

notify_volunteer_concluded_email_with_comments = HtmlEmailTemplate() \
    .paragraph("{{volunteer_name}} has concluded their volunteer commitment with {{project_name}}, "
               "and would like to convey the following:") \
    .paragraph('"{{comments}}"')

def notify_project_owners_volunteer_concluded_email(volunteer_relation, comments=None):
    email_template = notify_volunteer_concluded_email_with_comments if comments else notify_volunteer_concluded_email_no_comment
    project_name = volunteer_relation.project.project_name

    volunteer_name = volunteer_relation.volunteer.full_name()
    context = {
        'volunteer_name': volunteer_name,
        'project_name': project_name,
        'comments': comments
    }
    email_msg = EmailMessage(
        subject=volunteer_name + " has concluded their volunteer commitment with " + project_name,
        from_email=_get_account_from_email(settings.EMAIL_VOLUNTEER_ACCT),
        to=_get_co_owner_emails(volunteer_relation.project)
    )
    email_msg = email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_VOLUNTEER_ACCT)

def notify_project_owners_project_approved(project):
    email_template = HtmlEmailTemplate() \
        .paragraph('Your project "{{project_name}}" has been approved. You can see it at {{project_url}}')
    context = {
        'project_name': project.project_name,
        'project_url': settings.PROTOCOL_DOMAIN + '/index/?section=AboutProject&id=' + str(project.id)
    }
    email_msg = EmailMessage(
        subject=project.project_name + " has been approved",
        from_email=_get_account_from_email(settings.EMAIL_SUPPORT_ACCT),
        to=_get_co_owner_emails(project)
    )
    email_msg = email_template.render(email_msg, context)
    send_email(email_msg, settings.EMAIL_SUPPORT_ACCT)

def send_email(email_msg, email_acct=None):
    if not settings.FAKE_EMAILS:
        email_msg.connection = email_acct['connection'] if email_acct is not None else settings.EMAIL_SUPPORT_ACCT['connection']
    else:
        test_email_subject = 'TEST EMAIL: ' + email_msg.subject
        test_email_body = '<!--\n Environment:{environment}\nTO: {to_line}\nREPLY-TO: {reply_to}\n -->\n{body}'.format(
            environment=settings.PROTOCOL_DOMAIN,
            to_line=email_msg.to,
            reply_to=email_msg.reply_to,
            body=email_msg.body
        )
        email_msg.subject = test_email_subject
        email_msg.body = test_email_body
        email_msg.to = [settings.ADMIN_EMAIL]
        if settings.EMAIL_SUPPORT_ACCT:
            email_msg.connection = settings.EMAIL_SUPPORT_ACCT['connection']
    email_msg.send()

def _get_co_owner_emails(project):
    return list(map(lambda co_owner: co_owner.email, project.all_owners()))

def _get_account_from_email(email_acct):
    return email_acct['from_name'] if email_acct is not None else 'DemocracyLab'
<|endoftext|>