<commit_before># -*- coding: utf-8 -*-
"""Models for the Crowdsource application"""

# Django
from django.contrib.postgres.fields import JSONField
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe

# Standard Library
import json
from HTMLParser import HTMLParser
from random import choice

# Third Party
from bleach.sanitizer import Cleaner
from pyembed.core import PyEmbed
from pyembed.core.consumer import PyEmbedConsumerError
from taggit.managers import TaggableManager

# MuckRock
from muckrock.crowdsource import fields
from muckrock.crowdsource.constants import DOCUMENT_URL_RE
from muckrock.tags.models import TaggedItemBase

DOCCLOUD_EMBED = """

"""

class CrowdsourceQuerySet(models.QuerySet):
    """Object manager for crowdsources"""

    def get_viewable(self, user):
        """Get the viewable crowdsources for the user"""
        if user.is_staff:
            return self
        elif user.is_authenticated:
            return self.filter(
                Q(user=user) | Q(status='open', project_only=False) |
                Q(status='open', project_only=True, project__contributors=user)
            )
        else:
            return self.filter(status='open', project_only=False)

class Crowdsource(models.Model):
    """A Crowdsource"""

    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    user = models.ForeignKey('auth.User', related_name='crowdsources')
    project = models.ForeignKey(
        'project.Project',
        related_name='crowdsources',
        blank=True,
        null=True,
    )
    datetime_created = models.DateTimeField(default=timezone.now)
    datetime_opened = models.DateTimeField(blank=True, null=True)
    datetime_closed = models.DateTimeField(blank=True, null=True)
    status = models.CharField(
        max_length=9,
        default='draft',
        choices=(
            ('draft', 'Draft'),
            ('open', 'Opened'),
            ('close', 'Closed'),
        )
    )
    description = models.TextField(help_text='May use markdown')
    project_only = models.BooleanField(
        default=False,
        help_text='Only members of the project will be able to complete '
        'assignments for this crowdsource',
    )
    data_limit = models.PositiveSmallIntegerField(
        default=3,
        help_text='Number of times each data assignment will be completed '
        '(by different users) - only used if using data for this crowdsource',
        validators=[MinValueValidator(1)],
    )
    multiple_per_page = models.BooleanField(
        default=False,
        verbose_name='Allow multiple submissions per data item',
        help_text='This is useful for cases when there may be multiple '
        'records of interest per data source',
    )
    user_limit = models.BooleanField(
        default=True,
        help_text='Is the user limited to completing this form only once? '
        '(else, it is unlimited) - only used if not using data for this crowdsource',
    )
    submission_emails = models.ManyToManyField('communication.EmailAddress')

    objects = CrowdsourceQuerySet.as_manager()

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        """URL"""
        return reverse(
            'crowdsource-detail',
            kwargs={
                'slug': self.slug,
                'idx': self.pk,
            },
        )

    def get_data_to_show(self, user):
        """Get the crowdsource data to show"""
        options = self.data.get_choices(self.data_limit, user)
        if options:
            return choice(options)
        else:
            return None

    def create_form(self, form_json):
        """Create the crowdsource form from the form builder json"""
        # delete any old fields and re-create from the new JSON
        self.fields.all().delete()
        form_data = json.loads(form_json)
        seen_labels = set()
        cleaner = Cleaner(tags=[], attributes={}, styles=[], strip=True)
        htmlparser = HTMLParser()
        for order, field_data in enumerate(form_data):
            label = cleaner.clean(field_data['label'])[:255]
            label = htmlparser.unescape(label)
            label = self._uniqify_label_name(seen_labels, label)
            field = self.fields.create(
                label=label,
                type=field_data['type'],
                help_text=cleaner.clean(
                    field_data.get('description', ''),
                )[:255],
                min=field_data.get('min'),
                max=field_data.get('max'),
                required=field_data.get('required', False),
                gallery=field_data.get('gallery', False),
                order=order,
            )
            if 'values' in field_data and field.field.accepts_choices:
                for choice_order, value in enumerate(field_data['values']):
                    field.choices.create(
                        choice=cleaner.clean(value['label'])[:255],
                        value=cleaner.clean(value['value'])[:255],
                        order=choice_order,
                    )

    def _uniqify_label_name(self, seen_labels, label):
        """Ensure the label names are all unique"""
        new_label = label
        i = 0
        while new_label in seen_labels:
            i += 1
            postfix = str(i)
            new_label = u'{}-{}'.format(label[:254 - len(postfix)], postfix)
        seen_labels.add(new_label)
        return new_label

    def get_form_json(self):
        """Get the form JSON for editing the form"""
        return json.dumps([f.get_json() for f in self.fields.all()])

    def get_header_values(self, metadata_keys):
        """Get header values for CSV export"""
        values = ['user', 'datetime', 'skip']
        if self.multiple_per_page:
            values.append('number')
        if self.data.exists():
            values.append('datum')
            values.extend(metadata_keys)
        field_labels = list(self.fields.values_list('label', flat=True))
        return values + field_labels

    def get_metadata_keys(self):
        """Get the metadata keys for this crowdsource's data"""
        datum = self.data.first()
        if datum:
            return datum.metadata.keys()
        else:
            return []

    def total_assignments(self):
        """Total assignments to be completed"""
        if not self.data.all():
            return None
        return len(self.data.all()) * self.data_limit

    class Meta:
        verbose_name = 'assignment'

class CrowdsourceDataQuerySet(models.QuerySet):
    """Object manager for crowdsource data"""

    def get_choices(self, data_limit, user):
        """Get choices for data to show"""
        choices = (
            self.annotate(count=models.Count('responses__user', distinct=True))
            .filter(count__lt=data_limit)
        )
        if user is not None:
            choices = choices.exclude(responses__user=user)
        return choices

class CrowdsourceData(models.Model):
    """A source of data to show with the crowdsource questions"""

    crowdsource = models.ForeignKey(Crowdsource, related_name='data')
    url = models.URLField(max_length=255, verbose_name='Data URL')
    metadata = JSONField(default=dict, blank=True)

    objects = CrowdsourceDataQuerySet.as_manager()

    def __unicode__(self):
        return u'Crowdsource Data: {}'.format(self.url)

    def embed(self):
        """Get the html to embed into the crowdsource"""
        try:
            # first try to get embed code from oEmbed
            return mark_safe(PyEmbed().embed(self.url, max_height=400))
        except PyEmbedConsumerError:
            # if this is a private document cloud document, it will not have
            # an oEmbed, create the embed manually
            doc_match = DOCUMENT_URL_RE.match(self.url)
            if doc_match:
                return mark_safe(
                    DOCCLOUD_EMBED.format(doc_id=doc_match.group('doc_id'))
                )
            else:
                # fall back to a simple iframe
                return format_html(
                    '',
                    self.url,
                )

    class Meta:
        verbose_name = 'assignment data'

class CrowdsourceField(models.Model):
    """A field on a crowdsource form"""

    crowdsource = models.ForeignKey(Crowdsource, related_name='fields')
    label = models.CharField(max_length=255)
    type = models.CharField(
        max_length=10,
        choices=fields.FIELD_CHOICES,
    )
    help_text = models.CharField(max_length=255, blank=True)
    min = models.PositiveSmallIntegerField(blank=True, null=True)
    max = models.PositiveSmallIntegerField(blank=True, null=True)
    required = models.BooleanField(default=True)
    gallery = models.BooleanField(default=False)
    order = models.PositiveSmallIntegerField()

    def __unicode__(self):
        return self.label

    def get_form_field(self):
        """Return a form field appropriate for rendering this field"""
        return self.field().get_form_field(self)

    def get_json(self):
        """Get the JSON represenation for this field"""
        data = {
            'type': self.type,
            'label': self.label,
            'description': self.help_text,
            'required': self.required,
            'gallery': self.gallery,
        }
        if self.field.accepts_choices:
            data['values'] = [{
                'label': c.choice,
                'value': c.value
            } for c in self.choices.all()]
        if self.min is not None:
            data['min'] = self.min
        if self.max is not None:
            data['max'] = self.max
        return data

    @property
    def field(self):
        """Get the crowdsource field instance"""
        return fields.FIELD_DICT[self.type]

    class Meta:
        verbose_name = 'assignment field'
        ordering = ('order',)
        unique_together = (
            ('crowdsource', 'label'),
            ('crowdsource', 'order'),
        )

class CrowdsourceChoice(models.Model):
    """A choice presented to crowdsource users"""

    field = models.ForeignKey(CrowdsourceField, related_name='choices')
    choice = models.CharField(max_length=255)
    value = models.CharField(max_length=255)
    order = models.PositiveSmallIntegerField()

    def __unicode__(self):
        return self.choice

    class Meta:
        verbose_name = 'assignment choice'
        ordering = ('order',)
        unique_together = (
            ('field', 'choice'),
            ('field', 'order'),
        )

class CrowdsourceResponse(models.Model):
    """A response to a crowdsource question"""
    crowdsource = models.ForeignKey(Crowdsource, related_name='responses')
    user = models.ForeignKey('auth.User', related_name='crowdsource_responses')
    datetime = models.DateTimeField(default=timezone.now)
    data = models.ForeignKey(
        CrowdsourceData,
        blank=True,
        null=True,
        related_name='responses',
    )
    skip = models.BooleanField(default=False)
    # number is only used for multiple_per_page crowdsources,
    # keeping track of how many times a single user has submitted
    # per data item
    number = models.PositiveSmallIntegerField(default=1)
    flag = models.BooleanField(default=False)
    gallery = models.BooleanField(default=False)

    tags = TaggableManager(through=TaggedItemBase, blank=True)

    def __unicode__(self):
        return u'Response by {} on {}'.format(
            self.user,
            self.datetime,
        )

    def get_values(self, metadata_keys):
        """Get the values for this response for CSV export"""
        values = [
            self.user.username,
            self.datetime.strftime('%Y-%m-%d %H:%M:%S'),
            self.skip,
        ]
        if self.crowdsource.multiple_per_page:
            values.append(self.number)
        if self.data:
            values.append(self.data.url)
            values.extend(self.data.metadata.get(k, '') for k in metadata_keys)
        values += list(
            self.values.order_by('field__order').values_list(
                'value', flat=True
            )
        )
        return values

    def create_values(self, data):
        """Given the form data, create the values for this response"""
        # these values are passed in the form, but should not have
        # values created for them
        for key in ['data_id', 'full_name', 'email', 'newsletter']:
            data.pop(key, None)
        for label, value in data.iteritems():
            try:
                field = CrowdsourceField.objects.get(
                    crowdsource=self.crowdsource,
                    label=label,
                )
                self.values.create(
                    field=field,
                    value=value if value is not None else '',
                )
            except CrowdsourceField.DoesNotExist:
                pass

    def send_email(self, email):
        """Send an email of this response"""
        metadata = self.crowdsource.get_metadata_keys()
        text = u'\n'.join(
            u'{}: {}'.format(k, v) for k, v in zip(
                self.crowdsource.get_header_values(metadata),
                self.get_values(metadata),
            )
        )
        send_mail(
            '[Assignment Response] {} by {}'.format(
                self.crowdsource.title,
                self.user.username,
            ),
            text,
            'info@muckrock.com',
            [email],
        )

    class Meta:
        verbose_name = 'assignment response'

class CrowdsourceValue(models.Model):
    """A field value for a given response"""

    response = models.ForeignKey(CrowdsourceResponse, related_name='values')
    field = models.ForeignKey(CrowdsourceField, related_name='values')
    value = models.CharField(max_length=2000)

    def __unicode__(self):
        return self.value

    class Meta:
        verbose_name = 'assignment value'
<commit_msg>add flag, gallery and tags to assignment csv export<commit_after># -*- coding: utf-8 -*-
"""Models for the Crowdsource application"""

# Django
from django.contrib.postgres.fields import JSONField
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe

# Standard Library
import json
from HTMLParser import HTMLParser
from random import choice

# Third Party
from bleach.sanitizer import Cleaner
from pyembed.core import PyEmbed
from pyembed.core.consumer import PyEmbedConsumerError
from taggit.managers import TaggableManager

# MuckRock
from muckrock.crowdsource import fields
from muckrock.crowdsource.constants import DOCUMENT_URL_RE
from muckrock.tags.models import TaggedItemBase

DOCCLOUD_EMBED = """

"""

class CrowdsourceQuerySet(models.QuerySet):
    """Object manager for crowdsources"""

    def get_viewable(self, user):
        """Get the viewable crowdsources for the user"""
        if user.is_staff:
            return self
        elif user.is_authenticated:
            return self.filter(
                Q(user=user) | Q(status='open', project_only=False) |
                Q(status='open', project_only=True, project__contributors=user)
            )
        else:
            return self.filter(status='open', project_only=False)

class Crowdsource(models.Model):
    """A Crowdsource"""

    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    user = models.ForeignKey('auth.User', related_name='crowdsources')
    project = models.ForeignKey(
        'project.Project',
        related_name='crowdsources',
        blank=True,
        null=True,
    )
    datetime_created = models.DateTimeField(default=timezone.now)
    datetime_opened = models.DateTimeField(blank=True, null=True)
    datetime_closed = models.DateTimeField(blank=True, null=True)
    status = models.CharField(
        max_length=9,
        default='draft',
        choices=(
            ('draft', 'Draft'),
            ('open', 'Opened'),
            ('close', 'Closed'),
        )
    )
    description = models.TextField(help_text='May use markdown')
    project_only = models.BooleanField(
        default=False,
        help_text='Only members of the project will be able to complete '
        'assignments for this crowdsource',
    )
    data_limit = models.PositiveSmallIntegerField(
        default=3,
        help_text='Number of times each data assignment will be completed '
        '(by different users) - only used if using data for this crowdsource',
        validators=[MinValueValidator(1)],
    )
    multiple_per_page = models.BooleanField(
        default=False,
        verbose_name='Allow multiple submissions per data item',
        help_text='This is useful for cases when there may be multiple '
        'records of interest per data source',
    )
    user_limit = models.BooleanField(
        default=True,
        help_text='Is the user limited to completing this form only once? '
        '(else, it is unlimited) - only used if not using data for this crowdsource',
    )
    submission_emails = models.ManyToManyField('communication.EmailAddress')

    objects = CrowdsourceQuerySet.as_manager()

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        """URL"""
        return reverse(
            'crowdsource-detail',
            kwargs={
                'slug': self.slug,
                'idx': self.pk,
            },
        )

    def get_data_to_show(self, user):
        """Get the crowdsource data to show"""
        options = self.data.get_choices(self.data_limit, user)
        if options:
            return choice(options)
        else:
            return None

    def create_form(self, form_json):
        """Create the crowdsource form from the form builder json"""
        # delete any old fields and re-create from the new JSON
        self.fields.all().delete()
        form_data = json.loads(form_json)
        seen_labels = set()
        cleaner = Cleaner(tags=[], attributes={}, styles=[], strip=True)
        htmlparser = HTMLParser()
        for order, field_data in enumerate(form_data):
            label = cleaner.clean(field_data['label'])[:255]
            label = htmlparser.unescape(label)
            label = self._uniqify_label_name(seen_labels, label)
            field = self.fields.create(
                label=label,
                type=field_data['type'],
                help_text=cleaner.clean(
                    field_data.get('description', ''),
                )[:255],
                min=field_data.get('min'),
                max=field_data.get('max'),
                required=field_data.get('required', False),
                gallery=field_data.get('gallery', False),
                order=order,
            )
            if 'values' in field_data and field.field.accepts_choices:
                for choice_order, value in enumerate(field_data['values']):
                    field.choices.create(
                        choice=cleaner.clean(value['label'])[:255],
                        value=cleaner.clean(value['value'])[:255],
                        order=choice_order,
                    )

    def _uniqify_label_name(self, seen_labels, label):
        """Ensure the label names are all unique"""
        new_label = label
        i = 0
        while new_label in seen_labels:
            i += 1
            postfix = str(i)
            new_label = u'{}-{}'.format(label[:254 - len(postfix)], postfix)
        seen_labels.add(new_label)
        return new_label

    def get_form_json(self):
        """Get the form JSON for editing the form"""
        return json.dumps([f.get_json() for f in self.fields.all()])

    def get_header_values(self, metadata_keys):
        """Get header values for CSV export"""
        values = ['user', 'datetime', 'skip', 'flag', 'gallery', 'tags']
        if self.multiple_per_page:
            values.append('number')
        if self.data.exists():
            values.append('datum')
            values.extend(metadata_keys)
        field_labels = list(self.fields.values_list('label', flat=True))
        return values + field_labels

    def get_metadata_keys(self):
        """Get the metadata keys for this crowdsource's data"""
        datum = self.data.first()
        if datum:
            return datum.metadata.keys()
        else:
            return []

    def total_assignments(self):
        """Total assignments to be completed"""
        if not self.data.all():
            return None
        return len(self.data.all()) * self.data_limit

    class Meta:
        verbose_name = 'assignment'

class CrowdsourceDataQuerySet(models.QuerySet):
    """Object manager for crowdsource data"""

    def get_choices(self, data_limit, user):
        """Get choices for data to show"""
        choices = (
            self.annotate(count=models.Count('responses__user', distinct=True))
            .filter(count__lt=data_limit)
        )
        if user is not None:
            choices = choices.exclude(responses__user=user)
        return choices

class CrowdsourceData(models.Model):
    """A source of data to show with the crowdsource questions"""

    crowdsource = models.ForeignKey(Crowdsource, related_name='data')
    url = models.URLField(max_length=255, verbose_name='Data URL')
    metadata = JSONField(default=dict, blank=True)

    objects = CrowdsourceDataQuerySet.as_manager()

    def __unicode__(self):
        return u'Crowdsource Data: {}'.format(self.url)

    def embed(self):
        """Get the html to embed into the crowdsource"""
        try:
            # first try to get embed code from oEmbed
            return mark_safe(PyEmbed().embed(self.url, max_height=400))
        except PyEmbedConsumerError:
            # if this is a private document cloud document, it will not have
            # an oEmbed, create the embed manually
            doc_match = DOCUMENT_URL_RE.match(self.url)
            if doc_match:
                return mark_safe(
                    DOCCLOUD_EMBED.format(doc_id=doc_match.group('doc_id'))
                )
            else:
                # fall back to a simple iframe
                return format_html(
                    '',
                    self.url,
                )

    class Meta:
        verbose_name = 'assignment data'

class CrowdsourceField(models.Model):
    """A field on a crowdsource form"""

    crowdsource = models.ForeignKey(Crowdsource, related_name='fields')
    label = models.CharField(max_length=255)
    type = models.CharField(
        max_length=10,
        choices=fields.FIELD_CHOICES,
    )
    help_text = models.CharField(max_length=255, blank=True)
    min = models.PositiveSmallIntegerField(blank=True, null=True)
    max = models.PositiveSmallIntegerField(blank=True, null=True)
    required = models.BooleanField(default=True)
    gallery = models.BooleanField(default=False)
    order = models.PositiveSmallIntegerField()

    def __unicode__(self):
        return self.label

    def get_form_field(self):
        """Return a form field appropriate for rendering this field"""
        return self.field().get_form_field(self)

    def get_json(self):
        """Get the JSON represenation for this field"""
        data = {
            'type': self.type,
            'label': self.label,
            'description': self.help_text,
            'required': self.required,
            'gallery': self.gallery,
        }
        if self.field.accepts_choices:
            data['values'] = [{
                'label': c.choice,
                'value': c.value
            } for c in self.choices.all()]
        if self.min is not None:
            data['min'] = self.min
        if self.max is not None:
            data['max'] = self.max
        return data

    @property
    def field(self):
        """Get the crowdsource field instance"""
        return fields.FIELD_DICT[self.type]

    class Meta:
        verbose_name = 'assignment field'
        ordering = ('order',)
        unique_together = (
            ('crowdsource', 'label'),
            ('crowdsource', 'order'),
        )

class CrowdsourceChoice(models.Model):
    """A choice presented to crowdsource users"""

    field = models.ForeignKey(CrowdsourceField, related_name='choices')
    choice = models.CharField(max_length=255)
    value = models.CharField(max_length=255)
    order = models.PositiveSmallIntegerField()

    def __unicode__(self):
        return self.choice

    class Meta:
        verbose_name = 'assignment choice'
        ordering = ('order',)
        unique_together = (
            ('field', 'choice'),
            ('field', 'order'),
        )

class CrowdsourceResponse(models.Model):
    """A response to a crowdsource question"""
    crowdsource = models.ForeignKey(Crowdsource, related_name='responses')
    user = models.ForeignKey('auth.User', related_name='crowdsource_responses')
    datetime = models.DateTimeField(default=timezone.now)
    data = models.ForeignKey(
        CrowdsourceData,
        blank=True,
        null=True,
        related_name='responses',
    )
    skip = models.BooleanField(default=False)
    # number is only used for multiple_per_page crowdsources,
    # keeping track of how many times a single user has submitted
    # per data item
    number = models.PositiveSmallIntegerField(default=1)
    flag = models.BooleanField(default=False)
    gallery = models.BooleanField(default=False)

    tags = TaggableManager(through=TaggedItemBase, blank=True)

    def __unicode__(self):
        return u'Response by {} on {}'.format(
            self.user,
            self.datetime,
        )

    def get_values(self, metadata_keys):
        """Get the values for this response for CSV export"""
        values = [
            self.user.username,
            self.datetime.strftime('%Y-%m-%d %H:%M:%S'),
            self.skip,
            self.flag,
            self.gallery,
            ', '.join(self.tags.values_list('name', flat=True)),
        ]
        if self.crowdsource.multiple_per_page:
            values.append(self.number)
        if self.data:
            values.append(self.data.url)
            values.extend(self.data.metadata.get(k, '') for k in metadata_keys)
        values += list(
            self.values.order_by('field__order').values_list(
                'value', flat=True
            )
        )
        return values

    def create_values(self, data):
        """Given the form data, create the values for this response"""
        # these values are passed in the form, but should not have
        # values created for them
        for key in ['data_id', 'full_name', 'email', 'newsletter']:
            data.pop(key, None)
        for label, value in data.iteritems():
            try:
                field = CrowdsourceField.objects.get(
                    crowdsource=self.crowdsource,
                    label=label,
                )
                self.values.create(
                    field=field,
                    value=value if value is not None else '',
                )
            except CrowdsourceField.DoesNotExist:
                pass

    def send_email(self, email):
        """Send an email of this response"""
        metadata = self.crowdsource.get_metadata_keys()
        text = u'\n'.join(
            u'{}: {}'.format(k, v) for k, v in zip(
                self.crowdsource.get_header_values(metadata),
                self.get_values(metadata),
            )
        )
        send_mail(
            '[Assignment Response] {} by {}'.format(
                self.crowdsource.title,
                self.user.username,
            ),
            text,
            'info@muckrock.com',
            [email],
        )

    class Meta:
        verbose_name = 'assignment response'

class CrowdsourceValue(models.Model):
    """A field value for a given response"""

    response = models.ForeignKey(CrowdsourceResponse, related_name='values')
    field = models.ForeignKey(CrowdsourceField, related_name='values')
    value = models.CharField(max_length=2000)

    def __unicode__(self):
        return self.value

    class Meta:
        verbose_name = 'assignment value'
<|endoftext|>


<commit_before>#!/usr/bin/env python

""" loggerator.py contains the loggerator utility to be used for logging debug,
warning, errors and other information from applications.

:author:    Jose Carlos Recuero
:version:   0.1
:since:     08/13/2014

"""

__docformat__ = 'restructuredtext en'

#------------------------------------------------------------------------------
#  _                            _
# (_)_ __ ___  _ __   ___  _ __| |_ ___
# | | '_ ` _ \| '_ \ / _ \| '__| __/ __|
# | | | | | | | |_) | (_) | |  | |_\__ \
# |_|_| |_| |_| .__/ \___/|_|   \__|___/
#             |_|
#------------------------------------------------------------------------------
#
# import std python modules
#
import os
import sys
import logging
import logging.config

#
# import dbase python modules
#

#------------------------------------------------------------------------------
#
#   ___ ___  _ __  ___| |_ __ _ _ __ | |_ ___
#  / __/ _ \| '_ \/ __| __/ _` | '_ \| __/ __|
# | (_| (_) | | | \__ \ || (_| | | | | |_\__ \
#  \___\___/|_| |_|___/\__\__,_|_| |_|\__|___/
#
#------------------------------------------------------------------------------
#

COL_RESET       = "\x1b[0m"
"""
    :type: string

    Clears all colors and styles (to white on black).
"""

BOLD_ON         = "\x1b[1m"
"""
    :type: string

    Bold on.
"""

ITALICS_ON      = "\x1b[3m"
"""
    :type: string

    Italics on.
"""

UNDERLINE_ON    = "\x1b[4m"
"""
    :type: string

    Underline on.
"""

INVERSE_ON      = "\x1b[7m"
"""
    :type: string

    Inverse on, reverses foreground & background colors.
"""

STRIKETHRGH_ON  = "\x1b[9m"
"""
    :type: string

    Strikethrough on.
"""

BOLD_OFF        = "\x1b[22m"
"""
    :type: string

    Bold off.
"""

ITALICS_OFF     = "\x1b[23m"
"""
    :type: string

    Italics off.
"""

UNDERLINE_OFF   = "\x1b[24m"
"""
    :type: string

    Underline off.
"""

INVERSE_OFF     = "\x1b[27m"
"""
    :type: string

    Inverse off.
"""

STRIKETHRGH_OFF = "\x1b[29m"
"""
    :type: string

    Strikethrough off.
"""

# Foreground colors are in form of 3x, background are 4x
FG_BLACK    = "\x1b[30m"
"""
    :type: string

    Set foreground color to black.
"""

FG_RED      = "\x1b[31m"
"""
    :type: string

    Set foreground color to red.
"""

FG_GREEN    = "\x1b[32m"
"""
    :type: string

    Set foreground color to green.
"""

FG_YELLOW   = "\x1b[33m"
"""
    :type: string

    Set foreground color to yellow.
"""

FG_BLUE     = "\x1b[34m"
"""
    :type: string

    Set foreground color to blue.
"""

FG_MAGENTA  = "\x1b[35m"
"""
    :type: string

    Set foreground color to magenta (purple).
"""

FG_CYAN     = "\x1b[36m"
"""
    :type: string

    Set foreground color to cyan.
"""

FG_WHITE    = "\x1b[37m"
"""
    :type: string

    Set foreground color to white.
"""

FG_DEFAULT  = "\x1b[39m"
"""
    :type: string

    Set foreground color to default (white).
"""

BG_BLACK    = "\x1b[40m"
"""
    :type: string

    Set background color to black.
"""

BG_RED      = "\x1b[41m"
"""
    :type: string

    Set background color to red.
"""

BG_GREEN    = "\x1b[42m"
"""
    :type: string

    Set background color to green.
"""

BG_YELLOW   = "\x1b[43m"
"""
    :type: string

    Set background color to yellow.
"""

BG_BLUE     = "\x1b[44m"
"""
    :type: string

    Set background color to blue.
"""

BG_MAGENTA  = "\x1b[45m"
"""
    :type: string

    Set background color to magenta (purple).
"""

BG_CYAN     = "\x1b[46m"
"""
    :type: string

    Set background color to cyan.
"""

BG_WHITE    = "\x1b[47m"
"""
    :type: string

    Set background color to white.
"""

BG_DEFAULT  = "\x1b[49m"
"""
    :type: string

    Set background color to default (black).
"""

_loggerDB = {}
"""
    :type: dict

    This module variable dictionary stores all Logger instance created, where
    the key for every instance is the component name. When the same component
    request a logger, it returns the already created instance.
"""

TRACE_LEVEL = 25
DISPLAY_LEVEL = 24

#------------------------------------------------------------------------------
#            _                     _   _
#  ___ _   _| |__  _ __ ___  _   _| |_(_)_ __   ___  ___
# / __| | | | '_ \| '__/ _ \| | | | __| | '_ \ / _ \/ __|
# \__ \ |_| | |_) | | | (_) | |_| | |_| | | | |  __/\__ \
# |___/\__,_|_.__/|_|  \___/ \__,_|\__|_|_| |_|\___||___/
#
#------------------------------------------------------------------------------
#

# ===========================================================================
def getLoggerator(name, color=(BOLD_ON + FG_BLACK)):
    """Returns the loggerator for a given component.

    It create a new loggerator to a component, if there is not any instance for
    that component. If there is an instance, it is returned.

    :type name: string
    :param name: Component name. It is used to assign a loggerator instance.

    :type color: string
    :param color: Color to display the component name.

    :rtype: loggerator instance.
    :return: Create a new loggerator instance if there is not anyone for the
        given component, or return the one previously created.
    """
    if name not in _loggerDB:
        _loggerDB[name] = Loggerator(name, color)
    return _loggerDB[name]

#------------------------------------------------------------------------------
#       _                     _       __ _       _ _   _
#   ___| | __ _ ___ ___    __| | ___ / _(_)_ __ (_) |_(_) ___  _ __  ___
#  / __| |/ _` / __/ __|  / _` |/ _ \ |_| | '_ \| | __| |/ _ \| '_ \/ __|
# | (__| | (_| \__ \__ \ | (_| |  __/  _| | | | | | |_| | (_) | | | \__ \
#  \___|_|\__,_|___/___/  \__,_|\___|_| |_|_| |_|_|\__|_|\___/|_| |_|___/
#
#------------------------------------------------------------------------------
#

#
# =============================================================================
#
class ContextFilter(logging.Filter):

    def filter(self, record):
        fr = sys._getframe(8)
        msg = '{0}::{1}::{2}'.format(os.path.basename(fr.f_code.co_filename),
                                     fr.f_code.co_name,
                                     fr.f_lineno)
        record.titular = msg
        if record.levelno == TRACE_LEVEL:
            record.levelname = 'TRACE'
        elif record.levelno == DISPLAY_LEVEL:
            record.levelname = 'DISPLAY'
        return True

#
# =============================================================================
#
class Loggerator(object):
    """Loggerator class is used to log information for a given component.

    Component name is given when Loggerator instance is created, and it will
    be reused.

    :type loggerator: logging instance
    :ivar loggerator: logging instance, which will be used to log messages.
    """

    # =========================================================================
    def __init__(self, name, color, out=sys.stdout, fname='cmd.log'):
        """Loggerator class constructor.

        Create a Loggerator instance for the component with the given name and
        using given color.

        :todo:
            New parameter with the log filename should be added. If the
            parameter is present, then log information will be sent to
            the logfile instead of to the display.

        :type name: string
        :param name: Name of the component for logging information.

        :type color: string
        :param color: String containing the color to display the component name
            in all logs.

        :type out: stdout
        :param out: standard output

        :type fname: string
        :param fname: filename for the log file
        """
        self.loggerator = logging.getLogger(name[0:15].center(16, '*'))
        self.loggerator.setLevel(logging.DEBUG)

        formatString = '%(asctime)s ' + color + '%(name)-16s ' +\
                       COL_RESET + '[%(levelname)-8s] [%(titular)-32s] %(message)s'
        formatter = logging.Formatter(formatString)

        fileHandler = logging.FileHandler(fname)
        fileHandler.setFormatter(formatter)
        self.loggerator.addHandler(fileHandler)
        self.loggerator.addFilter(ContextFilter())

        # consoleHandler = logging.StreamHandler()
        # consoleHandler.setFormatter(formatter)
        # self.loggerator.addHandler(consoleHandler)

        self.defaultColor = {}
        self.defaultColor['debug']   = (('FG', 'GREEN'), )
        self.defaultColor['info']    = (('FG', 'BLUE'), )
        self.defaultColor['trace']   = (('FG', 'MAGENTA'), )
        self.defaultColor['display']   = (('FG', 'YELLOW'), )
        self.defaultColor['warning'] = (('FG', 'RED'), )
        self.defaultColor['error']   = (('FG', 'WHITE'), ('BG', 'RED'))
        self.out = out

    # =========================================================================
    def display(self, message):
        """Display a message in the default standard output provided.

        Args:
            message (str) : string with the message to be displayed.

        Returns:
            None
        """
        self._extendedLog(message, 'display')
        self.out.write(str(message))
        self.out.write('\n')

    # =========================================================================
    def _filterLevel(self, level):
        if level in ['debug', 'info', 'warning', 'error']:
            return level
        elif level == 'trace':
            return TRACE_LEVEL
        elif level == 'display':
            return DISPLAY_LEVEL
        else:
            return logging.NOTSET

    # =========================================================================
    def _setColor(self, color):
        """ Set the color based on input list.

        It takes an input parameter, which could be a list or a string.
        If the parameter is a string, it is supposed to set as a foreground
        color.
        If the parameter is a list, it is supposed to set the foreground and/or
        background color.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :rtype: str
        :return: string to be used as color for log message.
        """
        if isinstance(color, str):
            color = (('FG', color), )
        return eval('+'.join(map(lambda x: '%s_%s' % (x[0], x[1]), color)))

    # =========================================================================
    def _log(self, message, level, color=None, *args, **kwargs):
        """ Log a message with the given color.

        It logs the given message with the given color.

        :type message: string
        :param message: Debug message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        color = self._setColor(color)
        formattedMessage = '%s%s%s' % (color, message, COL_RESET)
        function = getattr(self.loggerator, level, None)
        if function:
            function(formattedMessage, *args, **kwargs)
        else:
            level = self._filterLevel(level)
            self.loggerator.log(level, formattedMessage, *args, **kwargs)

    # =========================================================================
    def getLoggerator(self):
        """Return the loggerator.

        :rtype: logging
        :return: Return the logging instance used for the current loggerator.
        """
        return self.loggerator

    # =========================================================================
    def _extendedLog(self, message, level, **kwargs):
        """Debug log.

        It logs a debug message.

        :type message: string
        :param message: Debug message to be logged.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        color = kwargs.get('color', None)
        mode = kwargs.pop('mode', 'FG')
        extended = kwargs.pop('extended', None)
        if extended:
            useColor = extended
        elif color:
            useColor = ((mode, color), )
        else:
            useColor = self.defaultColor[level]
        kwargs['color'] = useColor
        self._log(message, level, **kwargs)

    # =========================================================================
    def debug(self, message, color=None, mode='FG', *args, **kwargs):
        """Debug log.

        It logs a debug message.

        :type message: string
        :param message: Debug message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'debug', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def info(self, message, color=None, mode='FG', *args, **kwargs):
        """Information log.

        It logs an information message.

        :type message: string
        :param message: Information message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'info', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def trace(self, message, color=None, mode='FG', *args, **kwargs):
        """Trace log.

        It logs a trace message.

        :type message: string
        :param message: Trace message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'trace', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def warning(self, message, color=None, mode='FG', *args, **kwargs):
        """Warning log.

        It logs a warning message.

        :type message: string
        :param message: Warning message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'warning', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def error(self, message, color=None, mode='FG', *args, **kwargs):
        """Error log.

        It logs an error message.

        :type message: string
        :param message: Error message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'error', color=color, mode=mode, *args, **kwargs)

#------------------------------------------------------------------------------
#                  _
#  _ __ ___   __ _(_)_ __
# | '_ ` _ \ / _` | | '_ \
# | | | | | | (_| | | | | |
# |_| |_| |_|\__,_|_|_| |_|
#
#------------------------------------------------------------------------------
#

if __name__ == "__main__":
    lg = Loggerator('test', FG_RED)
    lg.debug('testing')
<commit_msg>Create rotating logging files<commit_after>#!/usr/bin/env python

""" loggerator.py contains the loggerator utility to be used for logging debug,
warning, errors and other information from applications.

:author:    Jose Carlos Recuero
:version:   0.1
:since:     08/13/2014

"""

__docformat__ = 'restructuredtext en'

#------------------------------------------------------------------------------
#  _                            _
# (_)_ __ ___  _ __   ___  _ __| |_ ___
# | | '_ ` _ \| '_ \ / _ \| '__| __/ __|
# | | | | | | | |_) | (_) | |  | |_\__ \
# |_|_| |_| |_| .__/ \___/|_|   \__|___/
#             |_|
#------------------------------------------------------------------------------
#
# import std python modules
#
import os
import sys
import logging
import logging.handlers
import logging.config

#
# import dbase python modules
#

#------------------------------------------------------------------------------
#
#   ___ ___  _ __  ___| |_ __ _ _ __ | |_ ___
#  / __/ _ \| '_ \/ __| __/ _` | '_ \| __/ __|
# | (_| (_) | | | \__ \ || (_| | | | | |_\__ \
#  \___\___/|_| |_|___/\__\__,_|_| |_|\__|___/
#
#------------------------------------------------------------------------------
#

COL_RESET       = "\x1b[0m"
"""
    :type: string

    Clears all colors and styles (to white on black).
"""

BOLD_ON         = "\x1b[1m"
"""
    :type: string

    Bold on.
"""

ITALICS_ON      = "\x1b[3m"
"""
    :type: string

    Italics on.
"""

UNDERLINE_ON    = "\x1b[4m"
"""
    :type: string

    Underline on.
"""

INVERSE_ON      = "\x1b[7m"
"""
    :type: string

    Inverse on, reverses foreground & background colors.
"""

STRIKETHRGH_ON  = "\x1b[9m"
"""
    :type: string

    Strikethrough on.
"""

BOLD_OFF        = "\x1b[22m"
"""
    :type: string

    Bold off.
"""

ITALICS_OFF     = "\x1b[23m"
"""
    :type: string

    Italics off.
"""

UNDERLINE_OFF   = "\x1b[24m"
"""
    :type: string

    Underline off.
"""

INVERSE_OFF     = "\x1b[27m"
"""
    :type: string

    Inverse off.
"""

STRIKETHRGH_OFF = "\x1b[29m"
"""
    :type: string

    Strikethrough off.
"""

# Foreground colors are in form of 3x, background are 4x
FG_BLACK    = "\x1b[30m"
"""
    :type: string

    Set foreground color to black.
"""

FG_RED      = "\x1b[31m"
"""
    :type: string

    Set foreground color to red.
"""

FG_GREEN    = "\x1b[32m"
"""
    :type: string

    Set foreground color to green.
"""

FG_YELLOW   = "\x1b[33m"
"""
    :type: string

    Set foreground color to yellow.
"""

FG_BLUE     = "\x1b[34m"
"""
    :type: string

    Set foreground color to blue.
"""

FG_MAGENTA  = "\x1b[35m"
"""
    :type: string

    Set foreground color to magenta (purple).
"""

FG_CYAN     = "\x1b[36m"
"""
    :type: string

    Set foreground color to cyan.
"""

FG_WHITE    = "\x1b[37m"
"""
    :type: string

    Set foreground color to white.
"""

FG_DEFAULT  = "\x1b[39m"
"""
    :type: string

    Set foreground color to default (white).
"""

BG_BLACK    = "\x1b[40m"
"""
    :type: string

    Set background color to black.
"""

BG_RED      = "\x1b[41m"
"""
    :type: string

    Set background color to red.
"""

BG_GREEN    = "\x1b[42m"
"""
    :type: string

    Set background color to green.
"""

BG_YELLOW   = "\x1b[43m"
"""
    :type: string

    Set background color to yellow.
"""

BG_BLUE     = "\x1b[44m"
"""
    :type: string

    Set background color to blue.
"""

BG_MAGENTA  = "\x1b[45m"
"""
    :type: string

    Set background color to magenta (purple).
"""

BG_CYAN     = "\x1b[46m"
"""
    :type: string

    Set background color to cyan.
"""

BG_WHITE    = "\x1b[47m"
"""
    :type: string

    Set background color to white.
"""

BG_DEFAULT  = "\x1b[49m"
"""
    :type: string

    Set background color to default (black).
"""

_loggerDB = {}
"""
    :type: dict

    This module variable dictionary stores all Logger instance created, where
    the key for every instance is the component name. When the same component
    request a logger, it returns the already created instance.
"""

TRACE_LEVEL = 25
DISPLAY_LEVEL = 24

#------------------------------------------------------------------------------
#            _                     _   _
#  ___ _   _| |__  _ __ ___  _   _| |_(_)_ __   ___  ___
# / __| | | | '_ \| '__/ _ \| | | | __| | '_ \ / _ \/ __|
# \__ \ |_| | |_) | | | (_) | |_| | |_| | | | |  __/\__ \
# |___/\__,_|_.__/|_|  \___/ \__,_|\__|_|_| |_|\___||___/
#
#------------------------------------------------------------------------------
#

# ===========================================================================
def getLoggerator(name, color=(BOLD_ON + FG_BLACK)):
    """Returns the loggerator for a given component.

    It create a new loggerator to a component, if there is not any instance for
    that component. If there is an instance, it is returned.

    :type name: string
    :param name: Component name. It is used to assign a loggerator instance.

    :type color: string
    :param color: Color to display the component name.

    :rtype: loggerator instance.
    :return: Create a new loggerator instance if there is not anyone for the
        given component, or return the one previously created.
    """
    if name not in _loggerDB:
        _loggerDB[name] = Loggerator(name, color)
    return _loggerDB[name]

#------------------------------------------------------------------------------
#       _                     _       __ _       _ _   _
#   ___| | __ _ ___ ___    __| | ___ / _(_)_ __ (_) |_(_) ___  _ __  ___
#  / __| |/ _` / __/ __|  / _` |/ _ \ |_| | '_ \| | __| |/ _ \| '_ \/ __|
# | (__| | (_| \__ \__ \ | (_| |  __/  _| | | | | | |_| | (_) | | | \__ \
#  \___|_|\__,_|___/___/  \__,_|\___|_| |_|_| |_|_|\__|_|\___/|_| |_|___/
#
#------------------------------------------------------------------------------
#

#
# =============================================================================
#
class ContextFilter(logging.Filter):

    def filter(self, record):
        fr = sys._getframe(8)
        msg = '{0}::{1}::{2}'.format(os.path.basename(fr.f_code.co_filename),
                                     fr.f_code.co_name,
                                     fr.f_lineno)
        record.titular = msg
        if record.levelno == TRACE_LEVEL:
            record.levelname = 'TRACE'
        elif record.levelno == DISPLAY_LEVEL:
            record.levelname = 'DISPLAY'
        return True

#
# =============================================================================
#
class Loggerator(object):
    """Loggerator class is used to log information for a given component.

    Component name is given when Loggerator instance is created, and it will
    be reused.

    :type loggerator: logging instance
    :ivar loggerator: logging instance, which will be used to log messages.
    """

    # =========================================================================
    def __init__(self, name, color, out=sys.stdout, fname='cmd.log'):
        """Loggerator class constructor.

        Create a Loggerator instance for the component with the given name and
        using given color.

        :todo:
            New parameter with the log filename should be added. If the
            parameter is present, then log information will be sent to
            the logfile instead of to the display.

        :type name: string
        :param name: Name of the component for logging information.

        :type color: string
        :param color: String containing the color to display the component name
            in all logs.

        :type out: stdout
        :param out: standard output

        :type fname: string
        :param fname: filename for the log file
        """
        self.loggerator = logging.getLogger(name[0:15].center(16, '*'))
        self.loggerator.setLevel(logging.DEBUG)

        formatString = '%(asctime)s ' + color + '%(name)-16s ' +\
                       COL_RESET + '[%(levelname)-8s] [%(titular)-32s] %(message)s'
        formatter = logging.Formatter(formatString)

        self._maxSize = 1024 * 1024 * 1024
        self._maxCount = 9
        fileHandler = logging.handlers.RotatingFileHandler(fname, maxBytes=self._maxSize, backupCount=self._maxCount)
        fileHandler.setFormatter(formatter)
        self.loggerator.addHandler(fileHandler)
        self.loggerator.addFilter(ContextFilter())

        # consoleHandler = logging.StreamHandler()
        # consoleHandler.setFormatter(formatter)
        # self.loggerator.addHandler(consoleHandler)

        self.defaultColor = {}
        self.defaultColor['debug']   = (('FG', 'GREEN'), )
        self.defaultColor['info']    = (('FG', 'BLUE'), )
        self.defaultColor['trace']   = (('FG', 'MAGENTA'), )
        self.defaultColor['display']   = (('FG', 'YELLOW'), )
        self.defaultColor['warning'] = (('FG', 'RED'), )
        self.defaultColor['error']   = (('FG', 'WHITE'), ('BG', 'RED'))
        self.out = out

    # =========================================================================
    def display(self, message):
        """Display a message in the default standard output provided.

        Args:
            message (str) : string with the message to be displayed.

        Returns:
            None
        """
        self._extendedLog(message, 'display')
        self.out.write(str(message))
        self.out.write('\n')

    # =========================================================================
    def _filterLevel(self, level):
        if level in ['debug', 'info', 'warning', 'error']:
            return level
        elif level == 'trace':
            return TRACE_LEVEL
        elif level == 'display':
            return DISPLAY_LEVEL
        else:
            return logging.NOTSET

    # =========================================================================
    def _setColor(self, color):
        """ Set the color based on input list.

        It takes an input parameter, which could be a list or a string.
        If the parameter is a string, it is supposed to set as a foreground
        color.
        If the parameter is a list, it is supposed to set the foreground and/or
        background color.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :rtype: str
        :return: string to be used as color for log message.
        """
        if isinstance(color, str):
            color = (('FG', color), )
        return eval('+'.join(map(lambda x: '%s_%s' % (x[0], x[1]), color)))

    # =========================================================================
    def _log(self, message, level, color=None, *args, **kwargs):
        """ Log a message with the given color.

        It logs the given message with the given color.

        :type message: string
        :param message: Debug message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        color = self._setColor(color)
        formattedMessage = '%s%s%s' % (color, message, COL_RESET)
        function = getattr(self.loggerator, level, None)
        if function:
            function(formattedMessage, *args, **kwargs)
        else:
            level = self._filterLevel(level)
            self.loggerator.log(level, formattedMessage, *args, **kwargs)

    # =========================================================================
    def getLoggerator(self):
        """Return the loggerator.

        :rtype: logging
        :return: Return the logging instance used for the current loggerator.
        """
        return self.loggerator

    # =========================================================================
    def _extendedLog(self, message, level, **kwargs):
        """Debug log.

        It logs a debug message.

        :type message: string
        :param message: Debug message to be logged.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        color = kwargs.get('color', None)
        mode = kwargs.pop('mode', 'FG')
        extended = kwargs.pop('extended', None)
        if extended:
            useColor = extended
        elif color:
            useColor = ((mode, color), )
        else:
            useColor = self.defaultColor[level]
        kwargs['color'] = useColor
        self._log(message, level, **kwargs)

    # =========================================================================
    def debug(self, message, color=None, mode='FG', *args, **kwargs):
        """Debug log.

        It logs a debug message.

        :type message: string
        :param message: Debug message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'debug', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def info(self, message, color=None, mode='FG', *args, **kwargs):
        """Information log.

        It logs an information message.

        :type message: string
        :param message: Information message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'info', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def trace(self, message, color=None, mode='FG', *args, **kwargs):
        """Trace log.

        It logs a trace message.

        :type message: string
        :param message: Trace message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'trace', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def warning(self, message, color=None, mode='FG', *args, **kwargs):
        """Warning log.

        It logs a warning message.

        :type message: string
        :param message: Warning message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'warning', color=color, mode=mode, *args, **kwargs)

    # =========================================================================
    def error(self, message, color=None, mode='FG', *args, **kwargs):
        """Error log.

        It logs an error message.

        :type message: string
        :param message: Error message to be logged.

        :type color: list, str
        :param color: foregorund color, or list with fore/background color.

        :type args: list
        :param args: List of parameters.

        :type kwargs: dict
        :param kwargs: Dictionary of parameters
        """
        self._extendedLog(message, 'error', color=color, mode=mode, *args, **kwargs)

#------------------------------------------------------------------------------
#                  _
#  _ __ ___   __ _(_)_ __
# | '_ ` _ \ / _` | | '_ \
# | | | | | | (_| | | | | |
# |_| |_| |_|\__,_|_|_| |_|
#
#------------------------------------------------------------------------------
#

if __name__ == "__main__":
    lg = Loggerator('test', FG_RED)
    lg.debug('testing')
<|endoftext|>


<commit_before>#
# This file is part of my.gpodder.org.
#
# my.gpodder.org is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# my.gpodder.org is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
#

from mygpo.api.basic_auth import require_valid_user, check_username
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404, HttpResponseNotAllowed
from mygpo.api.models import Device, Podcast, SubscriptionAction, Episode, EpisodeAction, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, EPISODE_ACTION_TYPES, DEVICE_TYPES, Subscription
from mygpo.api.httpresponse import JsonResponse
from mygpo.api.sanitizing import sanitize_url
from mygpo.api.advanced.directory import episode_data, podcast_data
from mygpo.api.backend import get_all_subscriptions
from django.core import serializers
from django.shortcuts import get_object_or_404
from time import mktime, gmtime, strftime
from datetime import datetime, timedelta
import dateutil.parser
from mygpo.log import log
from mygpo.utils import parse_time, parse_bool
from django.db import IntegrityError
import re
from django.views.decorators.csrf import csrf_exempt

try:
    #try to import the JSON module (if we are on Python 2.6)
    import json

    # Python 2.5 seems to have a different json module
    if not 'dumps' in dir(json):
        raise ImportError

except ImportError:
    # No JSON module available - fallback to simplejson (Python < 2.6)
    print "No JSON module available - fallback to simplejson (Python < 2.6)"
    import simplejson as json

@csrf_exempt
@require_valid_user
@check_username
def subscriptions(request, username, device_uid):

    now = datetime.now()
    now_ = int(mktime(now.timetuple()))

    if request.method == 'GET':
        try:
            d = Device.objects.get(user=request.user, uid=device_uid, deleted=False)
        except Device.DoesNotExist:
            raise Http404('device %s does not exist' % device_uid)

        try:
            since_ = request.GET['since']
        except KeyError:
            return HttpResponseBadRequest('parameter since missing')

        since = datetime.fromtimestamp(float(since_))

        changes = get_subscription_changes(request.user, d, since, now)

        return JsonResponse(changes)

    elif request.method == 'POST':
        d, created = Device.objects.get_or_create(user=request.user, uid=device_uid, defaults = {'type': 'other', 'name': 'New Device'})

        if d.deleted:
            d.deleted = False
            d.save()

        actions = json.loads(request.raw_post_data)
        add = actions['add'] if 'add' in actions else []
        rem = actions['remove'] if 'remove' in actions else []

        try:
            update_urls = update_subscriptions(request.user, d, add, rem)
        except IntegrityError, e:
            return HttpResponseBadRequest(e)

        return JsonResponse({
            'timestamp': now_,
            'update_urls': update_urls,
            })

    else:
        return HttpResponseNotAllowed(['GET', 'POST'])

def update_subscriptions(user, device, add, remove):
    updated_urls = []
    add_sanitized = []
    rem_sanitized = []

    for a in add:
        if a in remove:
           raise IntegrityError('can not add and remove %s at the same time' % a)

    for u in add:
        us = sanitize_url(u)
        if u != us:  updated_urls.append( (u, us) )
        if us != '': add_sanitized.append(us)

    for u in remove:
        us = sanitize_url(u)
        if u != us:  updated_urls.append( (u, us) )
        if us != '' and us not in add_sanitized:
            rem_sanitized.append(us)

    for a in add_sanitized:
        p, p_created = Podcast.objects.get_or_create(url=a)
        try:
            s = SubscriptionAction.objects.create(podcast=p,device=device,action=SUBSCRIBE_ACTION)
        except IntegrityError, e:
            log('can\'t add subscription %s for user %s: %s' % (a, user, e))

    for r in rem_sanitized:
        p, p_created = Podcast.objects.get_or_create(url=r)
        try:
            s = SubscriptionAction.objects.create(podcast=p,device=device,action=UNSUBSCRIBE_ACTION)
        except IntegrityError, e:
            log('can\'t remove subscription %s for user %s: %s' % (r, user, e))

    return updated_urls

def get_subscription_changes(user, device, since, until):
    actions = {}
    for a in SubscriptionAction.objects.filter(device=device, timestamp__gt=since, timestamp__lte=until).order_by('timestamp'):
        #ordered by ascending date; newer entries overwriter older ones
        actions[a.podcast] = a

    add = []
    remove = []

    for a in actions.values():
        if a.action == SUBSCRIBE_ACTION:
            add.append(a.podcast.url)
        elif a.action == UNSUBSCRIBE_ACTION:
            remove.append(a.podcast.url)

    until_ = int(mktime(until.timetuple()))
    return {'add': add, 'remove': remove, 'timestamp': until_}

@csrf_exempt
@require_valid_user
@check_username
def episodes(request, username, version=1):

    version = int(version)
    now = datetime.now()
    now_ = int(mktime(now.timetuple()))

    if request.method == 'POST':
        try:
            actions = json.loads(request.raw_post_data)
        except KeyError, e:
            log('could not parse episode update info for user %s: %s' % (username, e))
            return HttpResponseBadRequest()

        try:
            update_urls = update_episodes(request.user, actions)
        except Exception, e:
            log('could not update episodes for user %s: %s' % (username, e))
            return HttpResponseBadRequest(e)

        return JsonResponse({'timestamp': now_, 'update_urls': update_urls})

    elif request.method == 'GET':
        podcast_url= request.GET.get('podcast', None)
        device_uid = request.GET.get('device', None)
        since_     = request.GET.get('since', None)
        aggregated = parse_bool(request.GET.get('aggregated', False))

        since = datetime.fromtimestamp(float(since_)) if since_ else None

        try:
            podcast = Podcast.objects.get(url=podcast_url) if podcast_url else None
            device  = Device.objects.get(user=request.user,uid=device_uid, deleted=False) if device_uid else None
        except:
            raise Http404

        return JsonResponse(get_episode_changes(request.user, podcast, device, since, now, aggregated, version))

    else:
        return HttpResponseNotAllowed(['POST', 'GET'])

def get_episode_changes(user, podcast, device, since, until, aggregated, version):
    if aggregated:
        actions = {}
    else:
        actions = []
    eactions = EpisodeAction.objects.filter(user=user, timestamp__lte=until)

    if podcast:
        eactions = eactions.filter(episode__podcast=podcast)

    if device:
        eactions = eactions.filter(device=device)

    if since: # we can't use None with __gt
        eactions = eactions.filter(timestamp__gt=since)

    if aggregated:
        eactions = eactions.order_by('timestamp')

    for a in eactions:
        action = {
            'podcast': a.episode.podcast.url,
            'episode': a.episode.url,
            'action':  a.action,
            'timestamp': a.timestamp.strftime('%Y-%m-%dT%H:%M:%S') #2009-12-12T09:00:00
        }

        if a.action == 'play' and a.playmark:
            if version == 1:
                t = gmtime(a.playmark)
                action['position'] = strftime('%H:%M:%S', t)
            elif None in (a.playmark, a.started, a.total):
                log('Ignoring broken episode action in DB: %r' % (a,))
                continue
            else:
                action['position'] = int(a.playmark)
                action['started'] = int(a.started)
                action['total'] = int(a.total)

        if aggregated:
            actions[a.episode] = action
        else:
            actions.append(action)

    until_ = int(mktime(until.timetuple()))

    if aggregated:
        actions = list(actions.itervalues())

    return {'actions': actions, 'timestamp': until_}

def update_episodes(user, actions):
    update_urls = []

    for e in actions:
        u = e['podcast']
        us = sanitize_url(u)
        if u != us:  update_urls.append( (u, us) )
        if us == '': continue

        podcast, p_created = Podcast.objects.get_or_create(url=us)

        eu = e['episode']
        eus = sanitize_url(eu, podcast=False, episode=True)
        if eu != eus: update_urls.append( (eu, eus) )
        if eus == '': continue

        episode, e_created = Episode.objects.get_or_create(podcast=podcast, url=eus)
        action  = e['action']
        if not valid_episodeaction(action):
            raise Exception('invalid action %s' % action)

        if 'device' in e:
            device, created = Device.objects.get_or_create(user=user, uid=e['device'], defaults={'name': 'Unknown', 'type': 'other'})

            # undelete a previously deleted device
            if device.deleted:
                device.deleted = False
                device.save()

        else:
            device, created = None, False

        timestamp = dateutil.parser.parse(e['timestamp']) if 'timestamp' in e and e['timestamp'] else datetime.now()

        # Time values for play actions and their keys in JSON
        PLAY_ACTION_KEYS = ('position', 'started', 'total')
        time_values = {}

        for key in PLAY_ACTION_KEYS:
            if key in e:
                if action != 'play':
                    # Key found, but must not be supplied (no play action!)
                    return HttpResponseBadRequest('%s only allowed in play actions' % key)

                try:
                    time_values[key] = parse_time(e[key])
                except ValueError:
                    log('could not parse %s parameter (value: %s) for user %s' % (key, repr(e[key]), user))
                    return HttpResponseBadRequest('Wrong format for %s: %s' % (key, repr(e[key])))
            else:
                # Value not supplied by client
                time_values[key] = None

        # Sanity check: If started or total are given, require position
        if (time_values['started'] is not None or \
                time_values['total'] is not None) and \
                time_values['position'] is None:
            return HttpResponseBadRequest('started and total require position')

        try:
            EpisodeAction.objects.create(user=user, episode=episode, device=device, action=action, timestamp=timestamp,
                    playmark=time_values['position'], started=time_values['started'], total=time_values['total'])
        except Exception, e:
            log('error while adding episode action (user %s, episode %s, device %s, action %s, timestamp %s): %s' % (user, episode, device, action, timestamp, e))

    return update_urls

@csrf_exempt
@require_valid_user
@check_username
def device(request, username, device_uid):

    # Workaround for mygpoclient 1.0: It uses "PUT" requests
    # instead of "POST" requests for uploading device settings
    if request.method in ('POST', 'PUT'):
        d, created = Device.objects.get_or_create(user=request.user, uid=device_uid)

        #undelete a previously deleted device
        if d.deleted:
            d.deleted = False
            d.save()

        data = json.loads(request.raw_post_data)

        if 'caption' in data:
            d.name = data['caption']

        if 'type' in data:
            if not valid_devicetype(data['type']):
                return HttpResponseBadRequest('invalid device type %s' % data['type'])
            d.type = data['type']

        d.save()

        return HttpResponse()

    else:
        return HttpResponseNotAllowed(['POST'])

def valid_devicetype(type):
    for t in DEVICE_TYPES:
        if t[0] == type:
            return True
    return False

def valid_episodeaction(type):
    for t in EPISODE_ACTION_TYPES:
        if t[0] == type:
            return True
    return False

@csrf_exempt
@require_valid_user
@check_username
def devices(request, username):

    if request.method == 'GET':
        devices = []
        for d in Device.objects.filter(user=request.user, deleted=False):
            devices.append({
                'id': d.uid,
                'caption': d.name,
                'type': d.type,
                'subscriptions': Subscription.objects.filter(device=d).count()
            })

        return JsonResponse(devices)

    else:
        return HttpResponseNotAllowed(['GET'])

@csrf_exempt
@require_valid_user
@check_username
def updates(request, username, device_uid):
    now = datetime.now()
    now_ = int(mktime(now.timetuple()))

    device = get_object_or_404(Device, user=request.user, uid=device_uid)

    try:
        since_ = request.GET['since']
    except KeyError:
        return HttpResponseBadRequest('parameter since missing')

    since = datetime.fromtimestamp(float(since_))

    ret = get_subscription_changes(request.user, device, since, now)

    # replace added urls with details
    podcast_details = []
    for url in ret['add']:
        podcast = Podcast.objects.get(url=url)
        podcast_details.append(podcast_data(podcast))

    ret['add'] = podcast_details

    # add episode details
    subscriptions = get_all_subscriptions(request.user)
    episode_status = {}
    for e in Episode.objects.filter(podcast__in=subscriptions, timestamp__gte=since).order_by('timestamp'):
        episode_status[e] = 'new'
    for a in EpisodeAction.objects.filter(user=request.user, episode__podcast__in=subscriptions, timestamp__gte=since).order_by('timestamp'):
        episode_status[a.episode] = a.action

    updates = []
    for episode, status in episode_status.iteritems():
        t = episode_data(episode)
        t['released'] = e.timestamp.strftime('%Y-%m-%dT%H:%M:%S')
        t['status'] = status
        updates.append(t)

    ret['updates'] = updates

    return JsonResponse(ret)

<commit_msg>prevent episode playmark events without total time (bug 1136)<commit_after>#
# This file is part of my.gpodder.org.
#
# my.gpodder.org is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# my.gpodder.org is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with my.gpodder.org. If not, see <http://www.gnu.org/licenses/>.
#

from mygpo.api.basic_auth import require_valid_user, check_username
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404, HttpResponseNotAllowed
from mygpo.api.models import Device, Podcast, SubscriptionAction, Episode, EpisodeAction, SUBSCRIBE_ACTION, UNSUBSCRIBE_ACTION, EPISODE_ACTION_TYPES, DEVICE_TYPES, Subscription
from mygpo.api.httpresponse import JsonResponse
from mygpo.api.sanitizing import sanitize_url
from mygpo.api.advanced.directory import episode_data, podcast_data
from mygpo.api.backend import get_all_subscriptions
from django.core import serializers
from django.shortcuts import get_object_or_404
from time import mktime, gmtime, strftime
from datetime import datetime, timedelta
import dateutil.parser
from mygpo.log import log
from mygpo.utils import parse_time, parse_bool
from django.db import IntegrityError
import re
from django.views.decorators.csrf import csrf_exempt

try:
    #try to import the JSON module (if we are on Python 2.6)
    import json

    # Python 2.5 seems to have a different json module
    if not 'dumps' in dir(json):
        raise ImportError

except ImportError:
    # No JSON module available - fallback to simplejson (Python < 2.6)
    print "No JSON module available - fallback to simplejson (Python < 2.6)"
    import simplejson as json

@csrf_exempt
@require_valid_user
@check_username
def subscriptions(request, username, device_uid):

    now = datetime.now()
    now_ = int(mktime(now.timetuple()))

    if request.method == 'GET':
        try:
            d = Device.objects.get(user=request.user, uid=device_uid, deleted=False)
        except Device.DoesNotExist:
            raise Http404('device %s does not exist' % device_uid)

        try:
            since_ = request.GET['since']
        except KeyError:
            return HttpResponseBadRequest('parameter since missing')

        since = datetime.fromtimestamp(float(since_))

        changes = get_subscription_changes(request.user, d, since, now)

        return JsonResponse(changes)

    elif request.method == 'POST':
        d, created = Device.objects.get_or_create(user=request.user, uid=device_uid, defaults = {'type': 'other', 'name': 'New Device'})

        if d.deleted:
            d.deleted = False
            d.save()

        actions = json.loads(request.raw_post_data)
        add = actions['add'] if 'add' in actions else []
        rem = actions['remove'] if 'remove' in actions else []

        try:
            update_urls = update_subscriptions(request.user, d, add, rem)
        except IntegrityError, e:
            return HttpResponseBadRequest(e)

        return JsonResponse({
            'timestamp': now_,
            'update_urls': update_urls,
            })

    else:
        return HttpResponseNotAllowed(['GET', 'POST'])

def update_subscriptions(user, device, add, remove):
    updated_urls = []
    add_sanitized = []
    rem_sanitized = []

    for a in add:
        if a in remove:
           raise IntegrityError('can not add and remove %s at the same time' % a)

    for u in add:
        us = sanitize_url(u)
        if u != us:  updated_urls.append( (u, us) )
        if us != '': add_sanitized.append(us)

    for u in remove:
        us = sanitize_url(u)
        if u != us:  updated_urls.append( (u, us) )
        if us != '' and us not in add_sanitized:
            rem_sanitized.append(us)

    for a in add_sanitized:
        p, p_created = Podcast.objects.get_or_create(url=a)
        try:
            s = SubscriptionAction.objects.create(podcast=p,device=device,action=SUBSCRIBE_ACTION)
        except IntegrityError, e:
            log('can\'t add subscription %s for user %s: %s' % (a, user, e))

    for r in rem_sanitized:
        p, p_created = Podcast.objects.get_or_create(url=r)
        try:
            s = SubscriptionAction.objects.create(podcast=p,device=device,action=UNSUBSCRIBE_ACTION)
        except IntegrityError, e:
            log('can\'t remove subscription %s for user %s: %s' % (r, user, e))

    return updated_urls

def get_subscription_changes(user, device, since, until):
    actions = {}
    for a in SubscriptionAction.objects.filter(device=device, timestamp__gt=since, timestamp__lte=until).order_by('timestamp'):
        #ordered by ascending date; newer entries overwriter older ones
        actions[a.podcast] = a

    add = []
    remove = []

    for a in actions.values():
        if a.action == SUBSCRIBE_ACTION:
            add.append(a.podcast.url)
        elif a.action == UNSUBSCRIBE_ACTION:
            remove.append(a.podcast.url)

    until_ = int(mktime(until.timetuple()))
    return {'add': add, 'remove': remove, 'timestamp': until_}

@csrf_exempt
@require_valid_user
@check_username
def episodes(request, username, version=1):

    version = int(version)
    now = datetime.now()
    now_ = int(mktime(now.timetuple()))

    if request.method == 'POST':
        try:
            actions = json.loads(request.raw_post_data)
        except KeyError, e:
            log('could not parse episode update info for user %s: %s' % (username, e))
            return HttpResponseBadRequest()

        try:
            update_urls = update_episodes(request.user, actions)
        except Exception, e:
            log('could not update episodes for user %s: %s' % (username, e))
            return HttpResponseBadRequest(e)

        return JsonResponse({'timestamp': now_, 'update_urls': update_urls})

    elif request.method == 'GET':
        podcast_url= request.GET.get('podcast', None)
        device_uid = request.GET.get('device', None)
        since_     = request.GET.get('since', None)
        aggregated = parse_bool(request.GET.get('aggregated', False))

        since = datetime.fromtimestamp(float(since_)) if since_ else None

        try:
            podcast = Podcast.objects.get(url=podcast_url) if podcast_url else None
            device  = Device.objects.get(user=request.user,uid=device_uid, deleted=False) if device_uid else None
        except:
            raise Http404

        return JsonResponse(get_episode_changes(request.user, podcast, device, since, now, aggregated, version))

    else:
        return HttpResponseNotAllowed(['POST', 'GET'])

def get_episode_changes(user, podcast, device, since, until, aggregated, version):
    if aggregated:
        actions = {}
    else:
        actions = []
    eactions = EpisodeAction.objects.filter(user=user, timestamp__lte=until)

    if podcast:
        eactions = eactions.filter(episode__podcast=podcast)

    if device:
        eactions = eactions.filter(device=device)

    if since: # we can't use None with __gt
        eactions = eactions.filter(timestamp__gt=since)

    if aggregated:
        eactions = eactions.order_by('timestamp')

    for a in eactions:
        action = {
            'podcast': a.episode.podcast.url,
            'episode': a.episode.url,
            'action':  a.action,
            'timestamp': a.timestamp.strftime('%Y-%m-%dT%H:%M:%S') #2009-12-12T09:00:00
        }

        if a.action == 'play' and a.playmark:
            if version == 1:
                t = gmtime(a.playmark)
                action['position'] = strftime('%H:%M:%S', t)
            elif None in (a.playmark, a.started, a.total):
                log('Ignoring broken episode action in DB: %r' % (a,))
                continue
            else:
                action['position'] = int(a.playmark)
                action['started'] = int(a.started)
                action['total'] = int(a.total)

        if aggregated:
            actions[a.episode] = action
        else:
            actions.append(action)

    until_ = int(mktime(until.timetuple()))

    if aggregated:
        actions = list(actions.itervalues())

    return {'actions': actions, 'timestamp': until_}

def update_episodes(user, actions):
    update_urls = []

    for e in actions:
        u = e['podcast']
        us = sanitize_url(u)
        if u != us:  update_urls.append( (u, us) )
        if us == '': continue

        podcast, p_created = Podcast.objects.get_or_create(url=us)

        eu = e['episode']
        eus = sanitize_url(eu, podcast=False, episode=True)
        if eu != eus: update_urls.append( (eu, eus) )
        if eus == '': continue

        episode, e_created = Episode.objects.get_or_create(podcast=podcast, url=eus)
        action  = e['action']
        if not valid_episodeaction(action):
            raise Exception('invalid action %s' % action)

        if 'device' in e:
            device, created = Device.objects.get_or_create(user=user, uid=e['device'], defaults={'name': 'Unknown', 'type': 'other'})

            # undelete a previously deleted device
            if device.deleted:
                device.deleted = False
                device.save()

        else:
            device, created = None, False

        timestamp = dateutil.parser.parse(e['timestamp']) if 'timestamp' in e and e['timestamp'] else datetime.now()

        # Time values for play actions and their keys in JSON
        PLAY_ACTION_KEYS = ('position', 'started', 'total')
        time_values = {}

        for key in PLAY_ACTION_KEYS:
            if key in e:
                if action != 'play':
                    # Key found, but must not be supplied (no play action!)
                    return HttpResponseBadRequest('%s only allowed in play actions' % key)

                try:
                    time_values[key] = parse_time(e[key])
                except ValueError:
                    log('could not parse %s parameter (value: %s) for user %s' % (key, repr(e[key]), user))
                    return HttpResponseBadRequest('Wrong format for %s: %s' % (key, repr(e[key])))
            else:
                # Value not supplied by client
                time_values[key] = None

        # Sanity check: If started or total are given, require position
        if (time_values['started'] is not None or \
                time_values['total'] is not None) and \
                time_values['position'] is None:
            return HttpResponseBadRequest('started and total require position')

        # Sanity check: total and position can only appear together
        if (time_values['total'] or time_values['started']) and \
            not (time_values['total'] and time_values['started']):
            return HttpResponseBadRequest('total and started parameters can only appear together')

        try:
            EpisodeAction.objects.create(user=user, episode=episode, device=device, action=action, timestamp=timestamp,
                    playmark=time_values['position'], started=time_values['started'], total=time_values['total'])
        except Exception, e:
            log('error while adding episode action (user %s, episode %s, device %s, action %s, timestamp %s): %s' % (user, episode, device, action, timestamp, e))

    return update_urls

@csrf_exempt
@require_valid_user
@check_username
def device(request, username, device_uid):

    # Workaround for mygpoclient 1.0: It uses "PUT" requests
    # instead of "POST" requests for uploading device settings
    if request.method in ('POST', 'PUT'):
        d, created = Device.objects.get_or_create(user=request.user, uid=device_uid)

        #undelete a previously deleted device
        if d.deleted:
            d.deleted = False
            d.save()

        data = json.loads(request.raw_post_data)

        if 'caption' in data:
            d.name = data['caption']

        if 'type' in data:
            if not valid_devicetype(data['type']):
                return HttpResponseBadRequest('invalid device type %s' % data['type'])
            d.type = data['type']

        d.save()

        return HttpResponse()

    else:
        return HttpResponseNotAllowed(['POST'])

def valid_devicetype(type):
    for t in DEVICE_TYPES:
        if t[0] == type:
            return True
    return False

def valid_episodeaction(type):
    for t in EPISODE_ACTION_TYPES:
        if t[0] == type:
            return True
    return False

@csrf_exempt
@require_valid_user
@check_username
def devices(request, username):

    if request.method == 'GET':
        devices = []
        for d in Device.objects.filter(user=request.user, deleted=False):
            devices.append({
                'id': d.uid,
                'caption': d.name,
                'type': d.type,
                'subscriptions': Subscription.objects.filter(device=d).count()
            })

        return JsonResponse(devices)

    else:
        return HttpResponseNotAllowed(['GET'])

@csrf_exempt
@require_valid_user
@check_username
def updates(request, username, device_uid):
    now = datetime.now()
    now_ = int(mktime(now.timetuple()))

    device = get_object_or_404(Device, user=request.user, uid=device_uid)

    try:
        since_ = request.GET['since']
    except KeyError:
        return HttpResponseBadRequest('parameter since missing')

    since = datetime.fromtimestamp(float(since_))

    ret = get_subscription_changes(request.user, device, since, now)

    # replace added urls with details
    podcast_details = []
    for url in ret['add']:
        podcast = Podcast.objects.get(url=url)
        podcast_details.append(podcast_data(podcast))

    ret['add'] = podcast_details

    # add episode details
    subscriptions = get_all_subscriptions(request.user)
    episode_status = {}
    for e in Episode.objects.filter(podcast__in=subscriptions, timestamp__gte=since).order_by('timestamp'):
        episode_status[e] = 'new'
    for a in EpisodeAction.objects.filter(user=request.user, episode__podcast__in=subscriptions, timestamp__gte=since).order_by('timestamp'):
        episode_status[a.episode] = a.action

    updates = []
    for episode, status in episode_status.iteritems():
        t = episode_data(episode)
        t['released'] = e.timestamp.strftime('%Y-%m-%dT%H:%M:%S')
        t['status'] = status
        updates.append(t)

    ret['updates'] = updates

    return JsonResponse(ret)

<|endoftext|>