<commit_before>from datetime import datetime
from celery.schedules import crontab
from celery.task import periodic_task, task
from celery.utils.log import get_task_logger
from corehq.apps.domain.calculations import CALC_FNS, _all_domain_stats
from corehq.apps.hqadmin.escheck import check_cluster_health, check_case_index_by_view, CLUSTER_HEALTH, check_xform_index_by_view, check_case_index_by_doc, check_xform_index_by_doc
from corehq.apps.reports.export import save_metadata_export_to_tempfile
from corehq.apps.reports.models import (ReportNotification,
    UnsupportedScheduledReportError, HQGroupExportConfiguration)
from corehq.elastic import get_es
from corehq.pillows.mappings.domain_mapping import DOMAIN_INDEX
from couchexport.groupexports import export_for_group
from dimagi.utils.logging import notify_exception
from couchexport.tasks import cache_file_to_be_served
from django.conf import settings

logging = get_task_logger(__name__)

@periodic_task(run_every=crontab(hour=[8,14], minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def check_es_index():
    """
    Verify that the Case and soon to be added XForm Elastic indices are up to date with what's in couch

    This code is also called in the HQ admin page as well
    """

    es_status = {}
    es_status.update(check_cluster_health())

    es_check_case_doc = getattr(settings, 'ES_CASE_CHECK_DIRECT_DOC_ID', None)
    if es_check_case_doc:
        es_status.update(check_case_index_by_doc(es_check_case_doc))
    else:
        es_status.update(check_case_index_by_view())

    es_check_xform_doc = getattr(settings, 'ES_XFORM_CHECK_DIRECT_DOC_ID', None)
    if es_check_xform_doc:
        es_status.update(check_xform_index_by_doc(es_check_xform_doc))
    else:
        es_status.update(check_xform_index_by_view())

    do_notify = False
    message = []
    if es_status[CLUSTER_HEALTH] == 'red':
        do_notify=True
        message.append("Cluster health is red - something is up with the ES machine")

    for prefix in ['hqcases', 'xforms']:
        if es_status.get('%s_status' % prefix, False) == False:
            do_notify=True
            message.append("Elasticsearch %s Index Issue: %s" % (prefix, es_status['%s_message' % prefix]))

    if do_notify:
        notify_exception(None, message='\n'.join(message))

@task
def send_report(notification_id):
    notification = ReportNotification.get(notification_id)
    try:
        notification.send()
    except UnsupportedScheduledReportError:
        pass

@task
def create_metadata_export(download_id, domain, format, filename, datespan=None, user_ids=None):
    tmp_path = save_metadata_export_to_tempfile(domain, datespan, user_ids)

    class FakeCheckpoint(object):
        # for some silly reason the export cache function wants an object that looks like this
        # so just hack around it with this stub class rather than do a larger rewrite

        def __init__(self, domain):
            self.domain = domain

        @property
        def get_id(self):
            return '%s-form-metadata' % self.domain

    return cache_file_to_be_served(tmp_path, FakeCheckpoint(domain), download_id, format, filename)

@periodic_task(run_every=crontab(hour="*", minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def daily_reports():
    # this should get called every hour by celery
    reps = ReportNotification.view("reportconfig/all_notifications",
                                   key=["daily", datetime.utcnow().hour, None],
                                   include_docs=True).all()
    for rep in reps:
        send_report.delay(rep._id)

@periodic_task(run_every=crontab(hour="*", minute="1", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def weekly_reports():
    # this should get called every hour by celery
    now = datetime.utcnow()
    reps = ReportNotification.view("reportconfig/all_notifications",
                                   key=["weekly", now.hour, now.weekday()],
                                   include_docs=True).all()
    for rep in reps:
        send_report.delay(rep._id)

@periodic_task(run_every=crontab(hour="*", minute="1", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def monthly_reports():
    now = datetime.utcnow()
    reps = ReportNotification.view("reportconfig/all_notifications",
                                   key=["monthly", now.hour, now.day],
                                   include_docs=True).all()
    for rep in reps:
        send_report.delay(rep._id)

@periodic_task(run_every=crontab(hour=[0,12], minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def saved_exports():
    for row in HQGroupExportConfiguration.view("groupexport/by_domain", reduce=False).all():
        export_for_group(row["id"], "couch")

@periodic_task(run_every=crontab(hour="12, 22", minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def update_calculated_properties():
    es = get_es()

    #todo: use some sort of ES scrolling/paginating
    results = es.get(DOMAIN_INDEX + "/hqdomain/_search", data={"size": 99999})['hits']['hits']
    all_stats = _all_domain_stats()
    for r in results:
        dom = r["_source"]["name"]
        calced_props = {
            "cp_n_web_users": int(all_stats["web_users"][dom]),
            "cp_n_active_cc_users": int(CALC_FNS["mobile_users"](dom)),
            "cp_n_cc_users": int(all_stats["commcare_users"][dom]),
            "cp_n_active_cases": int(CALC_FNS["cases_in_last"](dom, 120)),
            "cp_n_inactive_cases": int(CALC_FNS["inactive_cases_in_last"](dom, 120)),
            "cp_n_60_day_cases": int(CALC_FNS["cases_in_last"](dom, 60)),
            "cp_n_cases": int(all_stats["cases"][dom]),
            "cp_n_forms": int(all_stats["forms"][dom]),
            "cp_first_form": CALC_FNS["first_form_submission"](dom, False),
            "cp_last_form": CALC_FNS["last_form_submission"](dom, False),
            "cp_is_active": CALC_FNS["active"](dom),
            "cp_has_app": CALC_FNS["has_app"](dom),
        }
        if calced_props['cp_first_form'] == 'No forms':
            del calced_props['cp_first_form']
            del calced_props['cp_last_form']
        es.post("%s/hqdomain/%s/_update" % (DOMAIN_INDEX, r["_id"]), data={"doc": calced_props})
<commit_msg>hotfix bad couch queries with additions of reports views<commit_after>from datetime import datetime
from celery.schedules import crontab
from celery.task import periodic_task, task
from celery.utils.log import get_task_logger
from corehq.apps.domain.calculations import CALC_FNS, _all_domain_stats
from corehq.apps.hqadmin.escheck import check_cluster_health, check_case_index_by_view, CLUSTER_HEALTH, check_xform_index_by_view, check_case_index_by_doc, check_xform_index_by_doc
from corehq.apps.reports.export import save_metadata_export_to_tempfile
from corehq.apps.reports.models import (ReportNotification,
    UnsupportedScheduledReportError, HQGroupExportConfiguration)
from corehq.elastic import get_es
from corehq.pillows.mappings.domain_mapping import DOMAIN_INDEX
from couchexport.groupexports import export_for_group
from dimagi.utils.logging import notify_exception
from couchexport.tasks import cache_file_to_be_served
from django.conf import settings

logging = get_task_logger(__name__)

@periodic_task(run_every=crontab(hour=[8,14], minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def check_es_index():
    """
    Verify that the Case and soon to be added XForm Elastic indices are up to date with what's in couch

    This code is also called in the HQ admin page as well
    """

    es_status = {}
    es_status.update(check_cluster_health())

    es_check_case_doc = getattr(settings, 'ES_CASE_CHECK_DIRECT_DOC_ID', None)
    if es_check_case_doc:
        es_status.update(check_case_index_by_doc(es_check_case_doc))
    else:
        es_status.update(check_case_index_by_view())

    es_check_xform_doc = getattr(settings, 'ES_XFORM_CHECK_DIRECT_DOC_ID', None)
    if es_check_xform_doc:
        es_status.update(check_xform_index_by_doc(es_check_xform_doc))
    else:
        es_status.update(check_xform_index_by_view())

    do_notify = False
    message = []
    if es_status[CLUSTER_HEALTH] == 'red':
        do_notify=True
        message.append("Cluster health is red - something is up with the ES machine")

    for prefix in ['hqcases', 'xforms']:
        if es_status.get('%s_status' % prefix, False) == False:
            do_notify=True
            message.append("Elasticsearch %s Index Issue: %s" % (prefix, es_status['%s_message' % prefix]))

    if do_notify:
        notify_exception(None, message='\n'.join(message))

@task
def send_report(notification_id):
    notification = ReportNotification.get(notification_id)
    try:
        notification.send()
    except UnsupportedScheduledReportError:
        pass

@task
def create_metadata_export(download_id, domain, format, filename, datespan=None, user_ids=None):
    tmp_path = save_metadata_export_to_tempfile(domain, datespan, user_ids)

    class FakeCheckpoint(object):
        # for some silly reason the export cache function wants an object that looks like this
        # so just hack around it with this stub class rather than do a larger rewrite

        def __init__(self, domain):
            self.domain = domain

        @property
        def get_id(self):
            return '%s-form-metadata' % self.domain

    return cache_file_to_be_served(tmp_path, FakeCheckpoint(domain), download_id, format, filename)

@periodic_task(run_every=crontab(hour="*", minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def daily_reports():
    # this should get called every hour by celery
    reps = ReportNotification.view("reportconfig/all_notifications",
                                   key=["daily", datetime.utcnow().hour, None],
                                   reduce=False,
                                   include_docs=True).all()

    for rep in reps:
        send_report.delay(rep._id)

@periodic_task(run_every=crontab(hour="*", minute="1", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def weekly_reports():
    # this should get called every hour by celery
    now = datetime.utcnow()
    reps = ReportNotification.view("reportconfig/all_notifications",
                                   key=["weekly", now.hour, now.weekday()],
                                   reduce=False,
                                   include_docs=True).all()
    for rep in reps:
        send_report.delay(rep._id)

@periodic_task(run_every=crontab(hour="*", minute="1", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def monthly_reports():
    now = datetime.utcnow()
    reps = ReportNotification.view("reportconfig/all_notifications",
                                   key=["monthly", now.hour, now.day],
                                   reduce=False,
                                   include_docs=True).all()
    for rep in reps:
        send_report.delay(rep._id)

@periodic_task(run_every=crontab(hour=[0,12], minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def saved_exports():
    for row in HQGroupExportConfiguration.view("groupexport/by_domain", reduce=False).all():
        export_for_group(row["id"], "couch")

@periodic_task(run_every=crontab(hour="12, 22", minute="0", day_of_week="*"), queue=getattr(settings, 'CELERY_PERIODIC_QUEUE','celery'))
def update_calculated_properties():
    es = get_es()

    #todo: use some sort of ES scrolling/paginating
    results = es.get(DOMAIN_INDEX + "/hqdomain/_search", data={"size": 99999})['hits']['hits']
    all_stats = _all_domain_stats()
    for r in results:
        dom = r["_source"]["name"]
        calced_props = {
            "cp_n_web_users": int(all_stats["web_users"][dom]),
            "cp_n_active_cc_users": int(CALC_FNS["mobile_users"](dom)),
            "cp_n_cc_users": int(all_stats["commcare_users"][dom]),
            "cp_n_active_cases": int(CALC_FNS["cases_in_last"](dom, 120)),
            "cp_n_inactive_cases": int(CALC_FNS["inactive_cases_in_last"](dom, 120)),
            "cp_n_60_day_cases": int(CALC_FNS["cases_in_last"](dom, 60)),
            "cp_n_cases": int(all_stats["cases"][dom]),
            "cp_n_forms": int(all_stats["forms"][dom]),
            "cp_first_form": CALC_FNS["first_form_submission"](dom, False),
            "cp_last_form": CALC_FNS["last_form_submission"](dom, False),
            "cp_is_active": CALC_FNS["active"](dom),
            "cp_has_app": CALC_FNS["has_app"](dom),
        }
        if calced_props['cp_first_form'] == 'No forms':
            del calced_props['cp_first_form']
            del calced_props['cp_last_form']
        es.post("%s/hqdomain/%s/_update" % (DOMAIN_INDEX, r["_id"]), data={"doc": calced_props})
<|endoftext|>


<commit_before>"""
The Zeroless module API.

.. data:: log

A global Logger object. To use it, just add an Handler object
and set an appropriate logging level.
"""

import zmq
import logging

from time import sleep
from copy import deepcopy
from warnings import warn
from functools import partial

log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())

def _check_valid_port_range(port):
    if port < 1024 or port > 65535:
        error = 'Port {0} is invalid, choose one between 1024 and 65535'
        error = error.format(port)
        log.error(error)
        raise ValueError(error)

def _check_valid_num_connections(socket_type, num_connections):
    if socket_type == zmq.PAIR and num_connections > 1:
        error = 'A client cannot connect more than once in the PAIR pattern'
        log.error(error)
        raise RuntimeError(error)

def _connect_zmq_sock(sock, ip, port):
    log.info('Connecting to {0} on port {1}'.format(ip, port))
    sock.connect('tcp://' + ip + ':' + str(port))

def _disconnect_zmq_sock(sock, ip, port):
    log.info('Disconnecting from {0} on port {1}'.format(ip, port))

    try:
        sock.disconnect('tcp://' + ip + ':' + str(port))
    except zmq.ZMQError:
        error = 'There was no connection to {0} on port {1}'.format(ip, port)
        log.exception(error)
        raise ValueError(error)

def _bind_zmq_sock(sock, port):
    log.info('Binding on port {0}'.format(port))

    try:
        sock.bind('tcp://*:' + str(port))
    except zmq.ZMQError:
        error = 'Port {0} is already in use'.format(port)
        log.exception(error)
        raise ValueError(error)

def _recv(sock):
    while True:
        frames = sock.recv_multipart()
        log.debug('Receiving: {0}'.format(frames))
        yield frames if len(frames) > 1 else frames[0]

class Sock:
    def __init__(self):
        pass

    def __sock(self, pattern):
        sock = zmq.Context().instance().socket(pattern)
        self._setup(sock)

        log.info('Ready...')

        return sock

    def __send_function(self, sock, topic=None, embed_topic=False):
        def _send(*data):
            log.debug('Sending: {0}'.format(data))

            try:
                sock.send_multipart(data)
            except TypeError:
                error = 'Data must be bytes, so try again'
                log.exception(error)
                raise TypeError(error)

        if sock.socket_type == zmq.PUB:
            if embed_topic:
                return partial(_send, topic)

            def _send_and_enforce_topic(*data):
                if not data[0].startswith(topic):
                    error = 'If embed_topic argument is not set, then the '
                    error += 'topic must be at the beginning of the first '
                    error += 'part (i.e. frame) of every published message'
                    log.exception(error)
                    raise ValueError(error)

                return _send(*data)

            if not topic == b'':
                return _send_and_enforce_topic

        return _send

    def __recv_generator(self, sock):
        return _recv(sock)

    # PubSub pattern
    def pub(self, topic=b'', embed_topic=False):
        """
        Returns a callable that can be used to transmit a message, with a given
        ``topic``, in a publisher-subscriber fashion. Note that the sender
        function has a ``print`` like signature, with an infinite number of
        arguments. Each one being a part of the complete message.

        By default, no topic will be included into published messages. Being up
        to developers to include the topic, at the beginning of the first part
        (i.e. frame) of every published message, so that subscribers are able
        to receive them. For a different behaviour, check the embed_topic
        argument.

        :param topic: the topic that will be published to (default=b'')
        :type topic: bytes
        :param embed_topic: set for the topic to be automatically sent as the
                            first part (i.e. frame) of every published message
                            (default=False)
        :type embed_topic bool
        :rtype: function
        """
        if not isinstance(topic, bytes):
            error = 'Topic must be bytes'
            log.error(error)
            raise TypeError(error)

        sock = self.__sock(zmq.PUB)
        return self.__send_function(sock, topic, embed_topic)

    def sub(self, topics=(b'',)):
        """
        Returns an iterable that can be used to iterate over incoming messages,
        that were published with one of the topics specified in ``topics``. Note
        that the iterable returns as many parts as sent by subscribed publishers.

        :param topics: a list of topics to subscribe to (default=b'')
        :type topics: list of bytes
        :rtype: generator
        """
        sock = self.__sock(zmq.SUB)

        for topic in topics:
            if not isinstance(topic, bytes):
                error = 'Topics must be a list of bytes'
                log.error(error)
                raise TypeError(error)
            sock.setsockopt(zmq.SUBSCRIBE, topic)

        return self.__recv_generator(sock)

    # PushPull pattern
    def push(self):
        """
        Returns a callable that can be used to transmit a message in a push-pull
        fashion. Note that the sender function has a ``print`` like signature,
        with an infinite number of arguments. Each one being a part of the
        complete message.

        :rtype: function
        """
        sock = self.__sock(zmq.PUSH)
        return self.__send_function(sock)

    def pull(self):
        """
        Returns an iterable that can be used to iterate over incoming messages,
        that were pushed by a push socket. Note that the iterable returns as
        many parts as sent by pushers.

        :rtype: generator
        """
        sock = self.__sock(zmq.PULL)
        return self.__recv_generator(sock)

    # ReqRep pattern
    def request(self):
        """
        Returns a callable and an iterable respectively. Those can be used to
        both transmit a message and/or iterate over incoming messages,
        that were replied by a reply socket. Note that the iterable returns
        as many parts as sent by repliers. Also, the sender function has a
        ``print`` like signature, with an infinite number of arguments. Each one
        being a part of the complete message.

        :rtype: (function, generator)
        """
        sock = self.__sock(zmq.REQ)
        return self.__send_function(sock), self.__recv_generator(sock)

    def reply(self):
        """
        Returns a callable and an iterable respectively. Those can be used to
        both transmit a message and/or iterate over incoming messages,
        that were requested by a request socket. Note that the iterable returns
        as many parts as sent by requesters. Also, the sender function has a
        ``print`` like signature, with an infinite number of arguments. Each one
        being a part of the complete message.

        :rtype: (function, generator)
        """
        sock = self.__sock(zmq.REP)
        return self.__send_function(sock), self.__recv_generator(sock)

    # Pair pattern
    def pair(self):
        """
        Returns a callable and an iterable respectively. Those can be used to
        both transmit a message and/or iterate over incoming messages, that were
        sent by a pair socket. Note that the iterable returns as many parts as
        sent by a pair. Also, the sender function has a ``print`` like signature,
        with an infinite number of arguments. Each one being a part of the
        complete message.

        :rtype: (function, generator)
        """
        sock = self.__sock(zmq.PAIR)
        return self.__send_function(sock), self.__recv_generator(sock)

    def _setup(self, sock):
        raise NotImplementedError()

class Client(Sock):
    """
    A client that can connect to a set of servers.
    """
    def __init__(self):
        """
        Constructor of the Client.
        """
        self._sock = None
        self._is_ready = False
        self._addresses = []

        Sock.__init__(self)

    def _setup(self, sock):
        self._sock = sock
        self._is_ready = True

        _check_valid_num_connections(self._sock.socket_type,
                                     len(self._addresses))

        for ip, port in self._addresses:
            _connect_zmq_sock(self._sock, ip, port)

    @property
    def addresses(self):
        """
        Returns a tuple containing all the connected addresses. Each address
        is a tuple with an ip address and a port.

        :rtype: (addresses)
        """
        return tuple(self._addresses)

    def connect(self, ip, port):
        """
        Connects to a server at the specified ip and port.

        :param ip: an IP address
        :type ip: str or unicode
        :param port: port number from 1024 up to 65535
        :type port: int
        """
        _check_valid_port_range(port)

        address = (ip, port)

        if address in self._addresses:
            error = 'Already connected to {0} on port {1}'.format(ip, port)
            log.exception(error)
            raise ValueError(error)

        self._addresses.append(address)

        if self._is_ready:
            _check_valid_num_connections(self._sock.socket_type,
                                         len(self._addresses))

            _connect_zmq_sock(self._sock, ip, port)

    def connect_local(self, port):
        """
        Connects to a server in localhost at the specified port.

        :param port: port number from 1024 up to 65535
        :type port: int
        """
        self.connect('127.0.0.1', port)

    def disconnect(self, ip, port):
        """
        Disconnects from a server at the specified ip and port.

        :param ip: an IP address
        :type ip: str or unicode
        :param port: port number from 1024 up to 65535
        :type port: int
        """
        _check_valid_port_range(port)
        address = (ip, port)

        try:
            self._addresses.remove(address)
        except ValueError:
            error = 'There was no connection to {0} on port {1}'.format(ip, port)
            log.exception(error)
            raise ValueError(error)

        if self._is_ready:
            _disconnect_zmq_sock(self._sock, ip, port)

    def disconnect_local(self, port):
        """
        Disconnects from a server in localhost at the specified port.

        :param port: port number from 1024 up to 65535
        :type port: int
        """
        self.disconnect('127.0.0.1', port)

    def disconnect_all(self):
        """
        Disconnects from all connected servers.
        """
        addresses = deepcopy(self._addresses)

        for ip, port in addresses:
            self.disconnect(ip, port)

class Server(Sock):
    """
    A server that clients can connect to.
    """
    def __init__(self, port):
        """
        Constructor of the Server.

        :param port: port number from 1024 up to 65535
        :type port: int
        """
        _check_valid_port_range(port)
        self._port = port

        Sock.__init__(self)

    def _setup(self, sock):
        if sock.socket_type == zmq.SUB:
            warning = 'SUB sockets that bind will not get any message before '
            warning += 'they first ask for via the provided generator, so '
            warning += 'prefer to bind PUB sockets if missing some messages '
            warning += 'is not an option'
            warn(warning)

        _bind_zmq_sock(sock, self._port)

    @property
    def port(self):
        """
        Returns the port.

        :rtype: int
        """
        return self._port
<commit_msg>Made all connect and disconnect methods in Client return itself, so that users can cascade connection/disconnection calls.<commit_after>"""
The Zeroless module API.

.. data:: log

A global Logger object. To use it, just add an Handler object
and set an appropriate logging level.
"""

import zmq
import logging

from time import sleep
from copy import deepcopy
from warnings import warn
from functools import partial

log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())

def _check_valid_port_range(port):
    if port < 1024 or port > 65535:
        error = 'Port {0} is invalid, choose one between 1024 and 65535'
        error = error.format(port)
        log.error(error)
        raise ValueError(error)

def _check_valid_num_connections(socket_type, num_connections):
    if socket_type == zmq.PAIR and num_connections > 1:
        error = 'A client cannot connect more than once in the PAIR pattern'
        log.error(error)
        raise RuntimeError(error)

def _connect_zmq_sock(sock, ip, port):
    log.info('Connecting to {0} on port {1}'.format(ip, port))
    sock.connect('tcp://' + ip + ':' + str(port))

def _disconnect_zmq_sock(sock, ip, port):
    log.info('Disconnecting from {0} on port {1}'.format(ip, port))

    try:
        sock.disconnect('tcp://' + ip + ':' + str(port))
    except zmq.ZMQError:
        error = 'There was no connection to {0} on port {1}'.format(ip, port)
        log.exception(error)
        raise ValueError(error)

def _bind_zmq_sock(sock, port):
    log.info('Binding on port {0}'.format(port))

    try:
        sock.bind('tcp://*:' + str(port))
    except zmq.ZMQError:
        error = 'Port {0} is already in use'.format(port)
        log.exception(error)
        raise ValueError(error)

def _recv(sock):
    while True:
        frames = sock.recv_multipart()
        log.debug('Receiving: {0}'.format(frames))
        yield frames if len(frames) > 1 else frames[0]

class Sock:
    def __init__(self):
        pass

    def __sock(self, pattern):
        sock = zmq.Context().instance().socket(pattern)
        self._setup(sock)

        log.info('Ready...')

        return sock

    def __send_function(self, sock, topic=None, embed_topic=False):
        def _send(*data):
            log.debug('Sending: {0}'.format(data))

            try:
                sock.send_multipart(data)
            except TypeError:
                error = 'Data must be bytes, so try again'
                log.exception(error)
                raise TypeError(error)

        if sock.socket_type == zmq.PUB:
            if embed_topic:
                return partial(_send, topic)

            def _send_and_enforce_topic(*data):
                if not data[0].startswith(topic):
                    error = 'If embed_topic argument is not set, then the '
                    error += 'topic must be at the beginning of the first '
                    error += 'part (i.e. frame) of every published message'
                    log.exception(error)
                    raise ValueError(error)

                return _send(*data)

            if not topic == b'':
                return _send_and_enforce_topic

        return _send

    def __recv_generator(self, sock):
        return _recv(sock)

    # PubSub pattern
    def pub(self, topic=b'', embed_topic=False):
        """
        Returns a callable that can be used to transmit a message, with a given
        ``topic``, in a publisher-subscriber fashion. Note that the sender
        function has a ``print`` like signature, with an infinite number of
        arguments. Each one being a part of the complete message.

        By default, no topic will be included into published messages. Being up
        to developers to include the topic, at the beginning of the first part
        (i.e. frame) of every published message, so that subscribers are able
        to receive them. For a different behaviour, check the embed_topic
        argument.

        :param topic: the topic that will be published to (default=b'')
        :type topic: bytes
        :param embed_topic: set for the topic to be automatically sent as the
                            first part (i.e. frame) of every published message
                            (default=False)
        :type embed_topic bool
        :rtype: function
        """
        if not isinstance(topic, bytes):
            error = 'Topic must be bytes'
            log.error(error)
            raise TypeError(error)

        sock = self.__sock(zmq.PUB)
        return self.__send_function(sock, topic, embed_topic)

    def sub(self, topics=(b'',)):
        """
        Returns an iterable that can be used to iterate over incoming messages,
        that were published with one of the topics specified in ``topics``. Note
        that the iterable returns as many parts as sent by subscribed publishers.

        :param topics: a list of topics to subscribe to (default=b'')
        :type topics: list of bytes
        :rtype: generator
        """
        sock = self.__sock(zmq.SUB)

        for topic in topics:
            if not isinstance(topic, bytes):
                error = 'Topics must be a list of bytes'
                log.error(error)
                raise TypeError(error)
            sock.setsockopt(zmq.SUBSCRIBE, topic)

        return self.__recv_generator(sock)

    # PushPull pattern
    def push(self):
        """
        Returns a callable that can be used to transmit a message in a push-pull
        fashion. Note that the sender function has a ``print`` like signature,
        with an infinite number of arguments. Each one being a part of the
        complete message.

        :rtype: function
        """
        sock = self.__sock(zmq.PUSH)
        return self.__send_function(sock)

    def pull(self):
        """
        Returns an iterable that can be used to iterate over incoming messages,
        that were pushed by a push socket. Note that the iterable returns as
        many parts as sent by pushers.

        :rtype: generator
        """
        sock = self.__sock(zmq.PULL)
        return self.__recv_generator(sock)

    # ReqRep pattern
    def request(self):
        """
        Returns a callable and an iterable respectively. Those can be used to
        both transmit a message and/or iterate over incoming messages,
        that were replied by a reply socket. Note that the iterable returns
        as many parts as sent by repliers. Also, the sender function has a
        ``print`` like signature, with an infinite number of arguments. Each one
        being a part of the complete message.

        :rtype: (function, generator)
        """
        sock = self.__sock(zmq.REQ)
        return self.__send_function(sock), self.__recv_generator(sock)

    def reply(self):
        """
        Returns a callable and an iterable respectively. Those can be used to
        both transmit a message and/or iterate over incoming messages,
        that were requested by a request socket. Note that the iterable returns
        as many parts as sent by requesters. Also, the sender function has a
        ``print`` like signature, with an infinite number of arguments. Each one
        being a part of the complete message.

        :rtype: (function, generator)
        """
        sock = self.__sock(zmq.REP)
        return self.__send_function(sock), self.__recv_generator(sock)

    # Pair pattern
    def pair(self):
        """
        Returns a callable and an iterable respectively. Those can be used to
        both transmit a message and/or iterate over incoming messages, that were
        sent by a pair socket. Note that the iterable returns as many parts as
        sent by a pair. Also, the sender function has a ``print`` like signature,
        with an infinite number of arguments. Each one being a part of the
        complete message.

        :rtype: (function, generator)
        """
        sock = self.__sock(zmq.PAIR)
        return self.__send_function(sock), self.__recv_generator(sock)

    def _setup(self, sock):
        raise NotImplementedError()

class Client(Sock):
    """
    A client that can connect to a set of servers.
    """
    def __init__(self):
        """
        Constructor of the Client.
        """
        self._sock = None
        self._is_ready = False
        self._addresses = []

        Sock.__init__(self)

    def _setup(self, sock):
        self._sock = sock
        self._is_ready = True

        _check_valid_num_connections(self._sock.socket_type,
                                     len(self._addresses))

        for ip, port in self._addresses:
            _connect_zmq_sock(self._sock, ip, port)

    @property
    def addresses(self):
        """
        Returns a tuple containing all the connected addresses. Each address
        is a tuple with an ip address and a port.

        :rtype: (addresses)
        """
        return tuple(self._addresses)

    def connect(self, ip, port):
        """
        Connects to a server at the specified ip and port.

        :param ip: an IP address
        :type ip: str or unicode
        :param port: port number from 1024 up to 65535
        :type port: int
        :rtype: self
        """
        _check_valid_port_range(port)

        address = (ip, port)

        if address in self._addresses:
            error = 'Already connected to {0} on port {1}'.format(ip, port)
            log.exception(error)
            raise ValueError(error)

        self._addresses.append(address)

        if self._is_ready:
            _check_valid_num_connections(self._sock.socket_type,
                                         len(self._addresses))

            _connect_zmq_sock(self._sock, ip, port)

        return self

    def connect_local(self, port):
        """
        Connects to a server in localhost at the specified port.

        :param port: port number from 1024 up to 65535
        :type port: int
        :rtype: self
        """
        return self.connect('127.0.0.1', port)

    def disconnect(self, ip, port):
        """
        Disconnects from a server at the specified ip and port.

        :param ip: an IP address
        :type ip: str or unicode
        :param port: port number from 1024 up to 65535
        :type port: int
        :rtype: self
        """
        _check_valid_port_range(port)
        address = (ip, port)

        try:
            self._addresses.remove(address)
        except ValueError:
            error = 'There was no connection to {0} on port {1}'.format(ip, port)
            log.exception(error)
            raise ValueError(error)

        if self._is_ready:
            _disconnect_zmq_sock(self._sock, ip, port)

        return self

    def disconnect_local(self, port):
        """
        Disconnects from a server in localhost at the specified port.

        :param port: port number from 1024 up to 65535
        :type port: int
        :rtype: self
        """
        return self.disconnect('127.0.0.1', port)

    def disconnect_all(self):
        """
        Disconnects from all connected servers.
        :rtype: self
        """
        addresses = deepcopy(self._addresses)

        for ip, port in addresses:
            self.disconnect(ip, port)

        return self

class Server(Sock):
    """
    A server that clients can connect to.
    """
    def __init__(self, port):
        """
        Constructor of the Server.

        :param port: port number from 1024 up to 65535
        :type port: int
        """
        _check_valid_port_range(port)
        self._port = port

        Sock.__init__(self)

    def _setup(self, sock):
        if sock.socket_type == zmq.SUB:
            warning = 'SUB sockets that bind will not get any message before '
            warning += 'they first ask for via the provided generator, so '
            warning += 'prefer to bind PUB sockets if missing some messages '
            warning += 'is not an option'
            warn(warning)

        _bind_zmq_sock(sock, self._port)

    @property
    def port(self):
        """
        Returns the port.

        :rtype: int
        """
        return self._port
<|endoftext|>


<commit_before>"""
The ggame :class:`App` class encapsulates functionality required for
initiating a graphics window in the browser, executing code using Javascript
animate, routing browser UI events (e.g. mouse and keyboard input) to
user Python code, and managing graphics elements (instances of
:class:`Sprite`, for example).
"""

# app.py

import traceback
from ggame.sysdeps import GFX_Window
from ggame.event import MouseEvent, KeyEvent

class App:
    """
    The :class:`App` class is a (typically subclassed) class that encapsulates
    handling of the display system, and processing user events. The :class:`App`
    class also manages lists of all :class:`Sprite` instances in the
    application.

    When subclassing :class:`App` you may elect to instantiate most of your
    sprite objects in the initialization section.

    Processing that must occur on a per-frame basis may be included by
    overriding the :meth:`~App.run` method. This is also an appropriate
    location to call similar 'step' methods for your various customized sprite
    classes.

    Once your application class has been instantiated, begin the frame drawing
    process by calling its :meth:`~App.run` method.

    The :class:`App` class is instantiated either by specifying the desired app
    window size in pixels, as two parameters:::

        myapp = App(640,480)

    or by providing no size parameters at all:::

        myapp = App()

    in which case, the full browser window size is used.

    NOTE: Only **one** instance of an :class:`App` class or subclass may be
    instantiated at a time.
    """

    spritelist = []
    """
    List of all sprites currently active in the application.
    """
    _eventdict = {}
    _spritesdict = {}
    _spritesadded = False
    win = None

    def __init__(self, *args):
        if App.win is None and (not args or len(args) == 2):
            x = y = 0
            if len(args) == 2:
                x = args[0]
                y = args[1]
            App.win = GFX_Window(x, y, type(self).destroy)
            App.width = App.win.width
            App.height = App.win.height
            # Add existing sprites to the window
            if not App._spritesadded and App.spritelist:
                App._spritesadded = True
                for sprite in App.spritelist:
                    App.win.add(sprite.gfx)
            App.win.bind(KeyEvent.keydown, type(self)._keyEvent)
            App.win.bind(KeyEvent.keyup, type(self)._keyEvent)
            App.win.bind(KeyEvent.keypress, type(self)._keyEvent)
            App.win.bind(MouseEvent.mousewheel, type(self)._mouseEvent)
            App.win.bind(MouseEvent.mousemove, type(self)._mouseEvent)
            App.win.bind(MouseEvent.mousedown, type(self)._mouseEvent)
            App.win.bind(MouseEvent.mouseup, type(self)._mouseEvent)
            App.win.bind(MouseEvent.click, type(self)._mouseEvent)
            App.win.bind(MouseEvent.dblclick, type(self)._mouseEvent)
        self.userfunc = None

    @classmethod
    def _routeEvent(cls, event, evtlist):
        for callback in reversed(evtlist):
            if not event.consumed:
                try:
                    callback(event)
                except BaseException:
                    traceback.print_exc()
                    raise

    @classmethod
    def _keyEvent(cls, hwevent):
        evtlist = App._eventdict.get(
            (hwevent.type, KeyEvent.keys.get(hwevent.keyCode, 0)), []
        )
        evtlist.extend(App._eventdict.get((hwevent.type, "*"), []))
        if evtlist:
            evt = KeyEvent(hwevent)
            cls._routeEvent(evt, evtlist)
        return False

    @classmethod
    def _mouseEvent(cls, hwevent):
        evtlist = App._eventdict.get(hwevent.type, [])
        if evtlist:
            evt = MouseEvent(cls, hwevent)
            cls._routeEvent(evt, evtlist)
        return False

    @classmethod
    def add(cls, obj):
        """
        Add a sprite object to the global lists.

        :param Sprite obj: The sprite reference to add.
        :returns: None
        """
        if App.win is not None:
            App.win.add(obj.gfx)
        App.spritelist.append(obj)
        # find out if sprite type is in dictionary without triggering pylint
        if not App._spritesdict.get(type(obj), False):
            App._spritesdict[type(obj)] = []
        App._spritesdict[type(obj)].append(obj)

    @classmethod
    def remove(cls, obj):
        """
        Remove a sprite object from the global lists.

        :param Sprite obj: The sprite reference to remove.
        :returns: None
        """
        App.spritelist.remove(obj)
        # remove from underlying layer only if existed in ours
        if App.win is not None:
            App.win.remove(obj.gfx)
        App._spritesdict[type(obj)].remove(obj)

    def _animate(self, _dummy):
        print("animate")
        if App.win:
            try:
                if self.userfunc:
                    self.userfunc()
                else:
                    self.step()
            except BaseException:
                traceback.print_exc()
                raise
            App.win.animate(self._animate)

    @classmethod
    def destroy(cls):
        """
        This will close the display window/tab, remove all references to
        sprites and place the `App` class in a state in which a new
        application could be instantiated.
        """
        print("destroying")
        if App.win:
            App.win.unbind(KeyEvent.keydown)
            App.win.unbind(KeyEvent.keyup)
            App.win.unbind(KeyEvent.keypress)
            App.win.unbind(MouseEvent.mousewheel)
            App.win.unbind(MouseEvent.mousemove)
            App.win.unbind(MouseEvent.mousedown)
            App.win.unbind(MouseEvent.mouseup)
            App.win.unbind(MouseEvent.click)
            App.win.unbind(MouseEvent.dblclick)
        for s in list(App.spritelist):
            s.destroy()
        App.win.destroy()
        App.win = None
        App.spritelist = []
        App._spritesdict = {}
        App._eventdict = {}
        App._spritesadded = False

    @classmethod
    def listenKeyEvent(cls, eventtype, key, callback):
        """
        Register to receive keyboard events.

        :param str eventtype:  The type of key event to
            receive (value is one of: `'keydown'`, `'keyup'` or `'keypress'`).

        :param str key:  Identify the keyboard key (e.g. `'space'`,
            `'left arrow'`, etc.) to receive events for.

        :param function callback:  The function or method that will be
            called with the :class:`~ggame.event.KeyEvent` object when the
            event occurs.

        :returns: Nothing

        See the source for :class:`~ggame.event.KeyEvent` for a list of key
        names
        to use with the `key` paramter.
        """
        evtlist = App._eventdict.get((eventtype, key), [])
        if callback not in evtlist:
            evtlist.append(callback)
        App._eventdict[(eventtype, key)] = evtlist

    @classmethod
    def listenMouseEvent(cls, eventtype, callback):
        """
        Register to receive mouse events.

        :param str eventtype: The type of mouse event to
            receive (value is one of: `'mousemove'`, `'mousedown'`, `'mouseup'`,
            `'click'`, `'dblclick'` or `'mousewheel'`).

        :param function callback: The function or method that will be
            called with the :class:`ggame.event.MouseEvent` object when the
            event occurs.

        :returns: Nothing
        """
        evtlist = App._eventdict.get(eventtype, [])
        if callback not in evtlist:
            evtlist.append(callback)
        App._eventdict[eventtype] = evtlist

    @classmethod
    def unlistenKeyEvent(cls, eventtype, key, callback):
        """
        Use this method to remove a registration to receive a particular
        keyboard event. Arguments must exactly match those used when
        registering for the event.

        :param str eventtype:  The type of key event to stop
            receiving (value is one of: `'keydown'`, `'keyup'` or `'keypress'`).

        :param str key:  The keyboard key (e.g. `'space'`,
            `'left arrow'`, etc.) to stop receiving events for.

        :param function callback:  The function or method that will no longer
            be called with the :class:`~ggame.event.KeyEvent` object when the
            event occurs.

        :returns: Nothing

        See the source for :class:`~ggame.event.KeyEvent` for a list of key
        names to use with the `key` paramter.

        """
        App._eventdict[(eventtype, key)].remove(callback)

    @classmethod
    def unlistenMouseEvent(cls, eventtype, callback):
        """
        Use this method to remove a registration to receive a particular
        mouse event. Arguments must exactly match those used when
        registering for the event.

        :param str eventtype: The type of mouse event to stop receiving events
            for (value is one of: `'mousemove'`, `'mousedown'`, `'mouseup'`,
            `'click'`, `'dblclick'` or `'mousewheel'`).

        :param function callback: The function or method that will no longer be
            called with the :class:`ggame.event.MouseEvent` object when the
            event occurs.

        :returns: Nothing
        """
        App._eventdict[eventtype].remove(callback)

    @classmethod
    def getSpritesbyClass(cls, sclass):
        """
        Returns a list of all active sprites of a given class.

        :param class sclass: A class name (e.g. 'Sprite') or subclass.

        :returns: A (potentially empty) list of sprite references.
        """
        return App._spritesdict.get(sclass, [])[:]

    def step(self):
        """
        The :meth:`~App.step` method is called once per animation frame.
        Override this method in your own subclass of :class:`App` to perform
        periodic calculations, such as checking for sprite collisions, or
        calling 'step' functions in your own customized sprite classes.

        The base class :meth:`~App.step` method is empty and is intended to be
        overriden.

        :returns: Nothing

        """

    def run(self, userfunc=None):
        """
        Calling the :meth:`~App.run` method begins the animation process
        whereby the :meth:`~App.step` method is called once per animation frame.

        :param function userfunc: Any function or method which shall be
            called once per animation frame.

        :returns: Nothing
        """
        self.userfunc = userfunc
        App.win.animate(self._animate)
<commit_msg>debugging<commit_after>"""
The ggame :class:`App` class encapsulates functionality required for
initiating a graphics window in the browser, executing code using Javascript
animate, routing browser UI events (e.g. mouse and keyboard input) to
user Python code, and managing graphics elements (instances of
:class:`Sprite`, for example).
"""

# app.py

import traceback
from ggame.sysdeps import GFX_Window
from ggame.event import MouseEvent, KeyEvent

class App:
    """
    The :class:`App` class is a (typically subclassed) class that encapsulates
    handling of the display system, and processing user events. The :class:`App`
    class also manages lists of all :class:`Sprite` instances in the
    application.

    When subclassing :class:`App` you may elect to instantiate most of your
    sprite objects in the initialization section.

    Processing that must occur on a per-frame basis may be included by
    overriding the :meth:`~App.run` method. This is also an appropriate
    location to call similar 'step' methods for your various customized sprite
    classes.

    Once your application class has been instantiated, begin the frame drawing
    process by calling its :meth:`~App.run` method.

    The :class:`App` class is instantiated either by specifying the desired app
    window size in pixels, as two parameters:::

        myapp = App(640,480)

    or by providing no size parameters at all:::

        myapp = App()

    in which case, the full browser window size is used.

    NOTE: Only **one** instance of an :class:`App` class or subclass may be
    instantiated at a time.
    """

    spritelist = []
    """
    List of all sprites currently active in the application.
    """
    _eventdict = {}
    _spritesdict = {}
    _spritesadded = False
    win = None

    def __init__(self, *args):
        if App.win is None and (not args or len(args) == 2):
            x = y = 0
            if len(args) == 2:
                x = args[0]
                y = args[1]
            App.win = GFX_Window(x, y, type(self).destroy)
            App.width = App.win.width
            App.height = App.win.height
            # Add existing sprites to the window
            if not App._spritesadded and App.spritelist:
                App._spritesadded = True
                for sprite in App.spritelist:
                    App.win.add(sprite.gfx)
            App.win.bind(KeyEvent.keydown, type(self)._keyEvent)
            App.win.bind(KeyEvent.keyup, type(self)._keyEvent)
            App.win.bind(KeyEvent.keypress, type(self)._keyEvent)
            App.win.bind(MouseEvent.mousewheel, type(self)._mouseEvent)
            App.win.bind(MouseEvent.mousemove, type(self)._mouseEvent)
            App.win.bind(MouseEvent.mousedown, type(self)._mouseEvent)
            App.win.bind(MouseEvent.mouseup, type(self)._mouseEvent)
            App.win.bind(MouseEvent.click, type(self)._mouseEvent)
            App.win.bind(MouseEvent.dblclick, type(self)._mouseEvent)
        self.userfunc = None

    @classmethod
    def _routeEvent(cls, event, evtlist):
        for callback in reversed(evtlist):
            if not event.consumed:
                try:
                    callback(event)
                except BaseException:
                    traceback.print_exc()
                    raise

    @classmethod
    def _keyEvent(cls, hwevent):
        evtlist = App._eventdict.get(
            (hwevent.type, KeyEvent.keys.get(hwevent.keyCode, 0)), []
        )
        evtlist.extend(App._eventdict.get((hwevent.type, "*"), []))
        if evtlist:
            evt = KeyEvent(hwevent)
            cls._routeEvent(evt, evtlist)
        return False

    @classmethod
    def _mouseEvent(cls, hwevent):
        evtlist = App._eventdict.get(hwevent.type, [])
        if evtlist:
            evt = MouseEvent(cls, hwevent)
            cls._routeEvent(evt, evtlist)
        return False

    @classmethod
    def add(cls, obj):
        """
        Add a sprite object to the global lists.

        :param Sprite obj: The sprite reference to add.
        :returns: None
        """
        if App.win is not None:
            App.win.add(obj.gfx)
        App.spritelist.append(obj)
        # find out if sprite type is in dictionary without triggering pylint
        if not App._spritesdict.get(type(obj), False):
            App._spritesdict[type(obj)] = []
        App._spritesdict[type(obj)].append(obj)

    @classmethod
    def remove(cls, obj):
        """
        Remove a sprite object from the global lists.

        :param Sprite obj: The sprite reference to remove.
        :returns: None
        """
        App.spritelist.remove(obj)
        # remove from underlying layer only if existed in ours
        if App.win is not None:
            App.win.remove(obj.gfx)
        App._spritesdict[type(obj)].remove(obj)

    def _animate(self, _dummy):
        if App.win:
            print("animate")
            try:
                if self.userfunc:
                    self.userfunc()
                else:
                    self.step()
            except BaseException:
                traceback.print_exc()
                raise
            App.win.animate(self._animate)

    @classmethod
    def destroy(cls):
        """
        This will close the display window/tab, remove all references to
        sprites and place the `App` class in a state in which a new
        application could be instantiated.
        """
        print("destroying")
        if App.win:
            App.win.unbind(KeyEvent.keydown)
            App.win.unbind(KeyEvent.keyup)
            App.win.unbind(KeyEvent.keypress)
            App.win.unbind(MouseEvent.mousewheel)
            App.win.unbind(MouseEvent.mousemove)
            App.win.unbind(MouseEvent.mousedown)
            App.win.unbind(MouseEvent.mouseup)
            App.win.unbind(MouseEvent.click)
            App.win.unbind(MouseEvent.dblclick)
        for s in list(App.spritelist):
            s.destroy()
        App.win.destroy()
        App.win = None
        App.spritelist = []
        App._spritesdict = {}
        App._eventdict = {}
        App._spritesadded = False

    @classmethod
    def listenKeyEvent(cls, eventtype, key, callback):
        """
        Register to receive keyboard events.

        :param str eventtype:  The type of key event to
            receive (value is one of: `'keydown'`, `'keyup'` or `'keypress'`).

        :param str key:  Identify the keyboard key (e.g. `'space'`,
            `'left arrow'`, etc.) to receive events for.

        :param function callback:  The function or method that will be
            called with the :class:`~ggame.event.KeyEvent` object when the
            event occurs.

        :returns: Nothing

        See the source for :class:`~ggame.event.KeyEvent` for a list of key
        names
        to use with the `key` paramter.
        """
        evtlist = App._eventdict.get((eventtype, key), [])
        if callback not in evtlist:
            evtlist.append(callback)
        App._eventdict[(eventtype, key)] = evtlist

    @classmethod
    def listenMouseEvent(cls, eventtype, callback):
        """
        Register to receive mouse events.

        :param str eventtype: The type of mouse event to
            receive (value is one of: `'mousemove'`, `'mousedown'`, `'mouseup'`,
            `'click'`, `'dblclick'` or `'mousewheel'`).

        :param function callback: The function or method that will be
            called with the :class:`ggame.event.MouseEvent` object when the
            event occurs.

        :returns: Nothing
        """
        evtlist = App._eventdict.get(eventtype, [])
        if callback not in evtlist:
            evtlist.append(callback)
        App._eventdict[eventtype] = evtlist

    @classmethod
    def unlistenKeyEvent(cls, eventtype, key, callback):
        """
        Use this method to remove a registration to receive a particular
        keyboard event. Arguments must exactly match those used when
        registering for the event.

        :param str eventtype:  The type of key event to stop
            receiving (value is one of: `'keydown'`, `'keyup'` or `'keypress'`).

        :param str key:  The keyboard key (e.g. `'space'`,
            `'left arrow'`, etc.) to stop receiving events for.

        :param function callback:  The function or method that will no longer
            be called with the :class:`~ggame.event.KeyEvent` object when the
            event occurs.

        :returns: Nothing

        See the source for :class:`~ggame.event.KeyEvent` for a list of key
        names to use with the `key` paramter.

        """
        App._eventdict[(eventtype, key)].remove(callback)

    @classmethod
    def unlistenMouseEvent(cls, eventtype, callback):
        """
        Use this method to remove a registration to receive a particular
        mouse event. Arguments must exactly match those used when
        registering for the event.

        :param str eventtype: The type of mouse event to stop receiving events
            for (value is one of: `'mousemove'`, `'mousedown'`, `'mouseup'`,
            `'click'`, `'dblclick'` or `'mousewheel'`).

        :param function callback: The function or method that will no longer be
            called with the :class:`ggame.event.MouseEvent` object when the
            event occurs.

        :returns: Nothing
        """
        App._eventdict[eventtype].remove(callback)

    @classmethod
    def getSpritesbyClass(cls, sclass):
        """
        Returns a list of all active sprites of a given class.

        :param class sclass: A class name (e.g. 'Sprite') or subclass.

        :returns: A (potentially empty) list of sprite references.
        """
        return App._spritesdict.get(sclass, [])[:]

    def step(self):
        """
        The :meth:`~App.step` method is called once per animation frame.
        Override this method in your own subclass of :class:`App` to perform
        periodic calculations, such as checking for sprite collisions, or
        calling 'step' functions in your own customized sprite classes.

        The base class :meth:`~App.step` method is empty and is intended to be
        overriden.

        :returns: Nothing

        """

    def run(self, userfunc=None):
        """
        Calling the :meth:`~App.run` method begins the animation process
        whereby the :meth:`~App.step` method is called once per animation frame.

        :param function userfunc: Any function or method which shall be
            called once per animation frame.

        :returns: Nothing
        """
        self.userfunc = userfunc
        App.win.animate(self._animate)
<|endoftext|>


<commit_before>import datetime as DT
from couchdbkit.exceptions import ResourceNotFound
import dateutil.parser
from dimagi.utils.web import render_to_response
from dimagi.utils.parsing import string_to_datetime
from dimagi.utils.couch.database import get_db
from collections import defaultdict
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from .googlecharts import get_punchcard_url
from .calc import punchcard
from corehq.apps.domain.decorators import login_and_domain_required
from corehq.apps.reports.templatetags.report_tags import render_user_inline
from dimagi.utils.couch.pagination import CouchPaginator

from couchexport.export import export_excel
from StringIO import StringIO

iso_format = '%Y-%m-%dT%H:%M:%SZ'

#def report_list(request, domain):
#    template = "reports/report_list.html"
#    return render_to_response(request, template, {'domain': domain})

def user_id_to_username(user_id):

    try:
        login = get_db().get(user_id)
    except:
        login = None
    if not login:
        return None
    else:
        return login['django_user']['username']

@login_and_domain_required
def default(request, domain):
    return HttpResponseRedirect(reverse("individual_summary_report", args=[domain]))

@login_and_domain_required
def export_data(req, domain):
    """
    Download all data for a couchdbkit model
    """
    export_tag = req.GET.get("export_tag", "")
    if not export_tag:
        raise Exception("You must specify a model to download!")
    tmp = StringIO()
    if export_excel([domain, export_tag], tmp):
        response = HttpResponse(mimetype='application/vnd.ms-excel')
        response['Content-Disposition'] = 'attachment; filename=%s.xls' % export_tag
        response.write(tmp.getvalue())
        tmp.close()
        return response
    else:
        return HttpResponse("Sorry, there was no data found for the tag '%s'." % export_tag)

@login_and_domain_required
def submit_history(request, domain, template="reports/partials/couch_report_partial.html"):
    individual = request.GET.get('individual', '')
    rows = []

    if individual:
        headings = ["Submit Time", "Form"]
    else:
        headings = ["Username", "Submit Time", "XMLNS"]
    return render_to_response(request, template, {
        'headings': headings,
        'rows': rows,
        'ajax_source': reverse('paging_submit_history', args=[domain, individual]),
    })
def paging_submit_history(request, domain, individual):
    def xmlns_to_name(xmlns):
        try:
            form = get_db().view('reports/forms_by_xmlns', key=[domain, xmlns], group=True).one()['value']
            lang = form['app']['langs'][0]
        except:
            form = None

        if form:
            name = "%s > %s > %s" % (
                reverse("corehq.apps.app_manager.views.view_app", args=[domain, form['app']['id']])
                + "?m=%s&f=%s" % (form['module']['id'], form['form']['id']),
                form['app']['name'],
                form['module']['name'][lang],
                form['form']['name'][lang]
            )
        else:
            name = xmlns
        return name
    def format_time(time):
        "time is an ISO timestamp"
        return time.replace('T', ' ').replace('Z', '')

    if individual:
        def view_to_table(row):
            time = row['value'].get('time')
            xmlns = row['value'].get('xmlns')

            #time = DT.datetime.strptime(time, iso_format)
            time = format_time(time)
            xmlns = xmlns_to_name(xmlns)
            return [time, xmlns]
        paginator = CouchPaginator('reports/submit_history', view_to_table, search=False, view_args=dict(
            endkey=[domain, individual],
            startkey=[domain, individual, {}],
            #descending=True,
        ))
    else:
        def view_to_table(row):
            time = row['value'].get('time')
            xmlns = row['value'].get('xmlns')
            user_id = row['value'].get('user_id')

            #time = DT.datetime.strptime(time, iso_format)
            time = format_time(time)
            xmlns = xmlns_to_name(xmlns)
            username = user_id_to_username(user_id)
            if username:
                return [username, time, xmlns]
        paginator = CouchPaginator('reports/all_submissions', view_to_table, search=False, view_args=dict(
            endkey=[domain],
            startkey=[domain, {}],
            #descending=True,
        ))
    return paginator.get_ajax_response(request)

@login_and_domain_required
def active_cases(request, domain, template="reports/partials/couch_report_partial.html"):

    individual = request.GET.get('individual', '')
    rows = []

    if individual:
        headings = ["Submit Time", "Form"]
    else:
        headings = ["Username", "Active Cases"]
    return render_to_response(request, template, {
        'headings': headings,
        'rows': rows,
        "ajax_source": reverse('paging_active_cases', args=[domain, individual]),
    })

@login_and_domain_required
def paging_active_cases(request, domain, individual):

    def view_to_table(row):
        r = row['value']
        username = user_id_to_username(r['user_id'])
        if username:
            return [username, r['active_cases']]

    paginator = CouchPaginator(
        "reports/active_cases",
        view_to_table,
        search=False,
        view_args=dict(
            group=True,
            endkey=[domain],
            startkey=[domain, {}],
        )
    )
    return paginator.get_ajax_response(request)

@login_and_domain_required
def submit_time_punchcard(request, domain):
    individual = request.GET.get("individual", '')
    data = punchcard.get_data(domain, individual)
    url = get_punchcard_url(data)
    #user_data = punchcard.get_users(domain)
#    if individual:
#        selected_user = [user for user, _ in user_data if user["_id"] == user_id][0]
#        name = "Punchcard Report for %s at %s" % (render_user_inline(selected_user))
    return render_to_response(request, "reports/punchcard.html", {
        "chart_url": url,
        #"user_data": user_data,
        #"clinic_id": clinic_id,
        #"user_id": user_id
    })

@login_and_domain_required
def user_summary(request, domain, template="reports/user_summary.html"):
    report_name = "User Summary Report (number of forms filled in by person)"

    return render_to_response(request, template, {
        "domain": domain,
        "show_dates": False,
        "report": {
            "name": report_name
        },
        "ajax_source": reverse('paging_user_summary', args=[domain]),
    })

@login_and_domain_required
def paging_user_summary(request, domain):

    def view_to_table(row):
        row['last_submission_date'] = dateutil.parser.parse(row['last_submission_date'])
        return row
    paginator = CouchPaginator(
        "reports/user_summary",
        view_to_table,
        search=False,
        view_args=dict(
            group=True,
            startkey=[domain],
            endkey=[domain, {}],
        )
    )
    return paginator.get_ajax_response(request)

@login_and_domain_required
def individual_summary(request, domain):
    individual = request.GET.get('individual', '')
    if individual:
        pass

    user_ids = get_db().view('reports/all_users', startkey=[domain], endkey=[domain, {}], group=True)
    user_ids = [result['key'][1] for result in user_ids]
    users = []
    for user_id in user_ids:
        username = user_id_to_username(user_id)
        if username:
            users.append({'id': user_id, 'username': username})

    return render_to_response(request, "reports/individual_summary.html", {
        "domain": domain,
        "show_users": True,
        "report": {
            "name": "Individual Summary",
            "header": [],
            "rows": [],
        },
        "users": users,
        "individual": individual,
    })

@login_and_domain_required
def daily_submissions(request, domain, view_name, title):
    start_date = request.GET.get('startdate')
    end_date = request.GET.get('enddate')
    if end_date:
        end_date = DT.date(*map(int, end_date.split('-')))
    else:
        end_date = DT.datetime.utcnow().date()

    if start_date:
        start_date = DT.date(*map(int, start_date.split('-')))
    else:
        start_date = (end_date- DT.timedelta(days=6))
    results = get_db().view(
        view_name,
        group=True,
        startkey=[domain, start_date.isoformat()],
        endkey=[domain, end_date.isoformat(), {}]
    ).all()
    print results
    all_users_results = get_db().view("reports/all_users", startkey=[domain], endkey=[domain, {}], group=True).all()
    user_ids = [result['key'][1] for result in all_users_results]

    dates = [start_date]
    while dates[-1] < end_date:
        dates.append(dates[-1] + DT.timedelta(days=1))
    date_map = dict([(date.isoformat(), i+1) for (i,date) in enumerate(dates)])
    user_map = dict([(user_id, i) for (i, user_id) in enumerate(user_ids)])
    rows = [[0]*(1+len(date_map)) for _ in user_ids]
    for result in results:
        _, date, user_id = result['key']
        val = result['value']
        rows[user_map[user_id]][date_map[date]] = val
    for i,user_id in enumerate(user_ids):
        rows[i][0] = user_id_to_username(user_id)

    valid_rows = []
    for row in rows:
        if row[0]:
            valid_rows.append(row)
    rows = valid_rows
    headers = ["Username"] + dates
    return render_to_response(request, "reports/daily_submissions.html", {
        "domain": domain,
        "show_dates": True,
        "start_date": start_date,
        "end_date": end_date,
        "report": {
            "name": title,
            "headers": headers,
            "rows": rows,
        }
    })

@login_and_domain_required
def excel_export_data(request, domain, template="reports/excel_export_data.html"):
    forms = get_db().view('reports/forms_by_xmlns', startkey=[domain], endkey=[domain, {}], group=True)
    forms = [x['value'] for x in forms]

    forms = sorted(forms, key=lambda form: \
        (0, form['app']['name'], form['module']['id'], form['form']['id']) \
        if 'app' in form else \
        (1, form['xmlns'])
    )

    apps = []
    unknown_forms = []

    # organize forms into apps, modules, forms:
    #        apps = [
    #            {
    #                "name": "App",
    #                "modules": [
    #                    {
    #                        "name": "Module 1",
    #                        "id": 1,
    #                        "forms": [
    #                            {...}
    #                        ]
    #                    }
    #                ]
    #
    #            }
    #        ]

    for f in forms:
        if 'app' in f:
            if apps and f['app']['name'] == apps[-1]['name']:
                if f['module']['id'] == apps[-1]['modules'][-1]['id']:
                    apps[-1]['modules'][-1]['forms'].append(f)
                else:
                    module = f['module'].copy()
                    module.update(forms=[f])
                    apps[-1]['modules'].append(module)
            else:
                app = f['app'].copy()
                module = f['module'].copy()
                module.update(forms=[f])
                app.update(modules=[module])
                apps.append(app)

        else:
            unknown_forms.append(f)

    return render_to_response(request, template, {
        "domain": domain,
        "forms": forms,
        "forms_by_app": apps,
        "unknown_forms": unknown_forms,
        "report": {
            "name": "Export Data to Excel"
        }
    })

    <commit_msg>importing ResourceNotFound crashed the build server; wasn't using it so for now just deleting that line<commit_after>import datetime as DT
import dateutil.parser
from dimagi.utils.web import render_to_response
from dimagi.utils.parsing import string_to_datetime
from dimagi.utils.couch.database import get_db
from collections import defaultdict
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from .googlecharts import get_punchcard_url
from .calc import punchcard
from corehq.apps.domain.decorators import login_and_domain_required
from corehq.apps.reports.templatetags.report_tags import render_user_inline
from dimagi.utils.couch.pagination import CouchPaginator

from couchexport.export import export_excel
from StringIO import StringIO

iso_format = '%Y-%m-%dT%H:%M:%SZ'

#def report_list(request, domain):
#    template = "reports/report_list.html"
#    return render_to_response(request, template, {'domain': domain})

def user_id_to_username(user_id):

    try:
        login = get_db().get(user_id)
    except:
        login = None
    if not login:
        return None
    else:
        return login['django_user']['username']

@login_and_domain_required
def default(request, domain):
    return HttpResponseRedirect(reverse("individual_summary_report", args=[domain]))

@login_and_domain_required
def export_data(req, domain):
    """
    Download all data for a couchdbkit model
    """
    export_tag = req.GET.get("export_tag", "")
    if not export_tag:
        raise Exception("You must specify a model to download!")
    tmp = StringIO()
    if export_excel([domain, export_tag], tmp):
        response = HttpResponse(mimetype='application/vnd.ms-excel')
        response['Content-Disposition'] = 'attachment; filename=%s.xls' % export_tag
        response.write(tmp.getvalue())
        tmp.close()
        return response
    else:
        return HttpResponse("Sorry, there was no data found for the tag '%s'." % export_tag)

@login_and_domain_required
def submit_history(request, domain, template="reports/partials/couch_report_partial.html"):
    individual = request.GET.get('individual', '')
    rows = []

    if individual:
        headings = ["Submit Time", "Form"]
    else:
        headings = ["Username", "Submit Time", "XMLNS"]
    return render_to_response(request, template, {
        'headings': headings,
        'rows': rows,
        'ajax_source': reverse('paging_submit_history', args=[domain, individual]),
    })
def paging_submit_history(request, domain, individual):
    def xmlns_to_name(xmlns):
        try:
            form = get_db().view('reports/forms_by_xmlns', key=[domain, xmlns], group=True).one()['value']
            lang = form['app']['langs'][0]
        except:
            form = None

        if form:
            name = "%s > %s > %s" % (
                reverse("corehq.apps.app_manager.views.view_app", args=[domain, form['app']['id']])
                + "?m=%s&f=%s" % (form['module']['id'], form['form']['id']),
                form['app']['name'],
                form['module']['name'][lang],
                form['form']['name'][lang]
            )
        else:
            name = xmlns
        return name
    def format_time(time):
        "time is an ISO timestamp"
        return time.replace('T', ' ').replace('Z', '')

    if individual:
        def view_to_table(row):
            time = row['value'].get('time')
            xmlns = row['value'].get('xmlns')

            #time = DT.datetime.strptime(time, iso_format)
            time = format_time(time)
            xmlns = xmlns_to_name(xmlns)
            return [time, xmlns]
        paginator = CouchPaginator('reports/submit_history', view_to_table, search=False, view_args=dict(
            endkey=[domain, individual],
            startkey=[domain, individual, {}],
            #descending=True,
        ))
    else:
        def view_to_table(row):
            time = row['value'].get('time')
            xmlns = row['value'].get('xmlns')
            user_id = row['value'].get('user_id')

            #time = DT.datetime.strptime(time, iso_format)
            time = format_time(time)
            xmlns = xmlns_to_name(xmlns)
            username = user_id_to_username(user_id)
            if username:
                return [username, time, xmlns]
        paginator = CouchPaginator('reports/all_submissions', view_to_table, search=False, view_args=dict(
            endkey=[domain],
            startkey=[domain, {}],
            #descending=True,
        ))
    return paginator.get_ajax_response(request)

@login_and_domain_required
def active_cases(request, domain, template="reports/partials/couch_report_partial.html"):

    individual = request.GET.get('individual', '')
    rows = []

    if individual:
        headings = ["Submit Time", "Form"]
    else:
        headings = ["Username", "Active Cases"]
    return render_to_response(request, template, {
        'headings': headings,
        'rows': rows,
        "ajax_source": reverse('paging_active_cases', args=[domain, individual]),
    })

@login_and_domain_required
def paging_active_cases(request, domain, individual):

    def view_to_table(row):
        r = row['value']
        username = user_id_to_username(r['user_id'])
        if username:
            return [username, r['active_cases']]

    paginator = CouchPaginator(
        "reports/active_cases",
        view_to_table,
        search=False,
        view_args=dict(
            group=True,
            endkey=[domain],
            startkey=[domain, {}],
        )
    )
    return paginator.get_ajax_response(request)

@login_and_domain_required
def submit_time_punchcard(request, domain):
    individual = request.GET.get("individual", '')
    data = punchcard.get_data(domain, individual)
    url = get_punchcard_url(data)
    #user_data = punchcard.get_users(domain)
#    if individual:
#        selected_user = [user for user, _ in user_data if user["_id"] == user_id][0]
#        name = "Punchcard Report for %s at %s" % (render_user_inline(selected_user))
    return render_to_response(request, "reports/punchcard.html", {
        "chart_url": url,
        #"user_data": user_data,
        #"clinic_id": clinic_id,
        #"user_id": user_id
    })

@login_and_domain_required
def user_summary(request, domain, template="reports/user_summary.html"):
    report_name = "User Summary Report (number of forms filled in by person)"

    return render_to_response(request, template, {
        "domain": domain,
        "show_dates": False,
        "report": {
            "name": report_name
        },
        "ajax_source": reverse('paging_user_summary', args=[domain]),
    })

@login_and_domain_required
def paging_user_summary(request, domain):

    def view_to_table(row):
        row['last_submission_date'] = dateutil.parser.parse(row['last_submission_date'])
        return row
    paginator = CouchPaginator(
        "reports/user_summary",
        view_to_table,
        search=False,
        view_args=dict(
            group=True,
            startkey=[domain],
            endkey=[domain, {}],
        )
    )
    return paginator.get_ajax_response(request)

@login_and_domain_required
def individual_summary(request, domain):
    individual = request.GET.get('individual', '')
    if individual:
        pass

    user_ids = get_db().view('reports/all_users', startkey=[domain], endkey=[domain, {}], group=True)
    user_ids = [result['key'][1] for result in user_ids]
    users = []
    for user_id in user_ids:
        username = user_id_to_username(user_id)
        if username:
            users.append({'id': user_id, 'username': username})

    return render_to_response(request, "reports/individual_summary.html", {
        "domain": domain,
        "show_users": True,
        "report": {
            "name": "Individual Summary",
            "header": [],
            "rows": [],
        },
        "users": users,
        "individual": individual,
    })

@login_and_domain_required
def daily_submissions(request, domain, view_name, title):
    start_date = request.GET.get('startdate')
    end_date = request.GET.get('enddate')
    if end_date:
        end_date = DT.date(*map(int, end_date.split('-')))
    else:
        end_date = DT.datetime.utcnow().date()

    if start_date:
        start_date = DT.date(*map(int, start_date.split('-')))
    else:
        start_date = (end_date- DT.timedelta(days=6))
    results = get_db().view(
        view_name,
        group=True,
        startkey=[domain, start_date.isoformat()],
        endkey=[domain, end_date.isoformat(), {}]
    ).all()
    print results
    all_users_results = get_db().view("reports/all_users", startkey=[domain], endkey=[domain, {}], group=True).all()
    user_ids = [result['key'][1] for result in all_users_results]

    dates = [start_date]
    while dates[-1] < end_date:
        dates.append(dates[-1] + DT.timedelta(days=1))
    date_map = dict([(date.isoformat(), i+1) for (i,date) in enumerate(dates)])
    user_map = dict([(user_id, i) for (i, user_id) in enumerate(user_ids)])
    rows = [[0]*(1+len(date_map)) for _ in user_ids]
    for result in results:
        _, date, user_id = result['key']
        val = result['value']
        rows[user_map[user_id]][date_map[date]] = val
    for i,user_id in enumerate(user_ids):
        rows[i][0] = user_id_to_username(user_id)

    valid_rows = []
    for row in rows:
        if row[0]:
            valid_rows.append(row)
    rows = valid_rows
    headers = ["Username"] + dates
    return render_to_response(request, "reports/daily_submissions.html", {
        "domain": domain,
        "show_dates": True,
        "start_date": start_date,
        "end_date": end_date,
        "report": {
            "name": title,
            "headers": headers,
            "rows": rows,
        }
    })

@login_and_domain_required
def excel_export_data(request, domain, template="reports/excel_export_data.html"):
    forms = get_db().view('reports/forms_by_xmlns', startkey=[domain], endkey=[domain, {}], group=True)
    forms = [x['value'] for x in forms]

    forms = sorted(forms, key=lambda form: \
        (0, form['app']['name'], form['module']['id'], form['form']['id']) \
        if 'app' in form else \
        (1, form['xmlns'])
    )

    apps = []
    unknown_forms = []

    # organize forms into apps, modules, forms:
    #        apps = [
    #            {
    #                "name": "App",
    #                "modules": [
    #                    {
    #                        "name": "Module 1",
    #                        "id": 1,
    #                        "forms": [
    #                            {...}
    #                        ]
    #                    }
    #                ]
    #
    #            }
    #        ]

    for f in forms:
        if 'app' in f:
            if apps and f['app']['name'] == apps[-1]['name']:
                if f['module']['id'] == apps[-1]['modules'][-1]['id']:
                    apps[-1]['modules'][-1]['forms'].append(f)
                else:
                    module = f['module'].copy()
                    module.update(forms=[f])
                    apps[-1]['modules'].append(module)
            else:
                app = f['app'].copy()
                module = f['module'].copy()
                module.update(forms=[f])
                app.update(modules=[module])
                apps.append(app)

        else:
            unknown_forms.append(f)

    return render_to_response(request, template, {
        "domain": domain,
        "forms": forms,
        "forms_by_app": apps,
        "unknown_forms": unknown_forms,
        "report": {
            "name": "Export Data to Excel"
        }
    })

    <|endoftext|>