<commit_before>import datetime
from django.conf import settings

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import SET_NULL
from django.db.models.signals import post_init, post_save, pre_save
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from django.contrib.auth.models import User
from multi_email_field.fields import MultiEmailField

from symposion.conference.models import Conference

from pycon.sponsorship import SPONSOR_COORDINATORS
from pycon.sponsorship.managers import SponsorManager
from symposion.utils.mail import send_email

# The benefits we track as individual fields on sponsors
# using separate SponsorBenefit records.
# Names are the names in the database as defined by PyCon organizers.
# Field names are the benefit names, lowercased, with
# spaces changed to _, and with "_benefit" appended.
# Column titles are arbitrary.

BENEFITS = [
    # Print logo not being used for 2017 but keep it in anyway
    {
        'name': 'Print logo',
        'field_name': 'print_logo_benefit',
        'column_title': _(u'Print Logo'),
    },
    {
        'name': 'Advertisement',
        'field_name': 'advertisement_benefit',
        'column_title': _(u'Ad'),
    }
]

class SponsorLevel(models.Model):

    conference = models.ForeignKey(Conference, verbose_name=_("conference"))
    name = models.CharField(_("name"), max_length=100)
    order = models.IntegerField(_("order"), default=0)
    cost = models.PositiveIntegerField(_("cost"))
    description = models.TextField(_("description"), blank=True, help_text=_("This is private."))

    class Meta:
        ordering = ["conference", "order"]
        verbose_name = _("sponsor level")
        verbose_name_plural = _("sponsor levels")

    def __unicode__(self):
        return u"%s %s" % (self.conference, self.name)

    def sponsors(self):
        return self.sponsor_set.filter(active=True).order_by("added")

class Sponsor(models.Model):

    applicant = models.ForeignKey(User, related_name="sponsorships", verbose_name=_("applicant"), null=True, on_delete=SET_NULL)

    name = models.CharField(_("Sponsor Name"), max_length=100)
    display_url = models.CharField(
        _("Link text - text to display on link to sponsor page, if different from the actual link"),
        max_length=200,
        default='',
        blank=True
    )
    external_url = models.URLField(
        _("Link to sponsor web page"),
        help_text=_("(Must include https:// or http://.)")
    )
    twitter_username = models.CharField(
        _("Twitter username"), blank=True, max_length=15,
    )
    annotation = models.TextField(_("annotation"), blank=True)
    contact_name = models.CharField(_("Contact Name"), max_length=100)
    contact_emails = MultiEmailField(
        _(u"Contact Emails"), default='',
        help_text=_(u"Please enter one email address per line.")
    )
    contact_phone = models.CharField(_(u"Contact Phone"), max_length=32)
    contact_address = models.TextField(_(u"Contact Address"))
    level = models.ForeignKey(SponsorLevel, verbose_name=_("level"))
    added = models.DateTimeField(_("added"), default=datetime.datetime.now)

    active = models.BooleanField(_("active"), default=False)
    approval_time = models.DateTimeField(null=True, blank=True, editable=False)

    wants_table = models.BooleanField(
        _(
            'Does your organization want a table at the job fair? '
            '(See Estimated Sponsor Fees '
            'for costs that might be involved.)'
        ), default=False)
    wants_booth = models.BooleanField(
        _(
            'Does your organization want a booth on the expo floor? '
            '(See Estimated Sponsor Fees '
            'for costs that might be involved.)'
        ), default=False)
    small_entity_discount = models.BooleanField(
        _(
            'Does your organization have fewer than 25 employees,'
            ' which qualifies you for our Small Entity Discount?'
        ), default=False)

    # Whether things are complete
    # True = complete, False = incomplate, Null = n/a for this sponsor level
    print_logo_benefit = models.NullBooleanField(help_text=_(u"Print logo benefit is complete"))
    advertisement_benefit = models.NullBooleanField(help_text=_(u"Advertisement benefit is complete"))

    registration_promo_codes = models.CharField(max_length=200, blank=True, default='')
    expo_promo_codes = models.CharField(max_length=200, blank=True, default='')
    booth_number = models.IntegerField(blank=True, null=True, default=None)
    job_fair_table_number = models.IntegerField(blank=True, null=True, default=None)

    web_description = models.TextField(
        _(u"Company description (to show on the web site)"),
    )
    web_logo = models.ImageField(
        _(u"Company logo (to show on the web site)"),
        upload_to="sponsor_files",
        null=True,  # This is nullable in case old data doesn't have a web logo
        # We enforce it on all new or edited sponsors though.
    )

    objects = SponsorManager()

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = _("sponsor")
        verbose_name_plural = _("sponsors")
        ordering = ['name']

    def save(self, *args, **kwargs):
        # Set fields related to benefits being complete
        for benefit in BENEFITS:
            field_name = benefit['field_name']
            benefit_name = benefit['name']
            setattr(self, field_name, self.benefit_is_complete(benefit_name))
        super(Sponsor, self).save(*args, **kwargs)

    def get_absolute_url(self):
        if self.active:
            return reverse("sponsor_detail", kwargs={"pk": self.pk})
        return reverse("sponsor_list")

    def get_display_url(self):
        """
        Return the text to display on the sponsor's link
        """
        if self.display_url:
            return self.display_url
        else:
            return self.external_url

    def render_email(self, text):
        """Replace special strings in text with values from the sponsor.

        %%NAME%% --> Sponsor name
        %%REGISTRATION_PROMO_CODES%% --> Registration promo codes, or empty string
        %%EXPO_PROMO_CODES%% --> Expo Hall only promo codes, or empty string
        %%BOOTH_NUMBER%% --> Booth number, or empty string if not set
        %%JOB_FAIR_TABLE_NUMBER%%" --> Job fair tabl number, or empty string if not set
        """
        text = text.replace("%%NAME%%", self.name)
        text = text.replace("%%REGISTRATION_PROMO_CODES%%", self.registration_promo_codes)
        text = text.replace("%%EXPO_PROMO_CODES%%", self.registration_promo_codes)

        # The next two are numbers, or if not set, None.  We don't want to
        # display "None" :-), but we might want to display "0".
        booth = str(self.booth_number) if self.booth_number is not None else ""
        text = text.replace("%%BOOTH_NUMBER%%", booth)
        table = str(self.job_fair_table_number) if self.job_fair_table_number is not None else ""
        text = text.replace("%%JOB_FAIR_TABLE_NUMBER%%", table)
        return text

    @cached_property
    def website_logo_url(self):
        if self.web_logo:
            return self.web_logo.url

    @property
    def joblisting_text(self):
        if not hasattr(self, "_joblisting_text"):
            self._joblisting_text = None
            benefits = self.sponsor_benefits.filter(benefit__id=8)
            if benefits.count():
                self._joblisting_text = benefits[0].text
        return self._joblisting_text

    @property
    def website_logo(self):
        return self.web_logo

    def reset_benefits(self):
        """
        Reset all benefits for this sponsor to the defaults for their
        sponsorship level.
        """
        level = None

        try:
            level = self.level
        except SponsorLevel.DoesNotExist:
            pass

        allowed_benefits = []
        if level:
            for benefit_level in level.benefit_levels.all():
                # Create all needed benefits if they don't exist already
                sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
                    sponsor=self, benefit=benefit_level.benefit)

                # and set to default limits for this level.
                sponsor_benefit.max_words = benefit_level.max_words
                sponsor_benefit.other_limits = benefit_level.other_limits

                # and set to active
                sponsor_benefit.active = True

                # @@@ We don't call sponsor_benefit.clean here. This means
                # that if the sponsorship level for a sponsor is adjusted
                # downwards, an existing too-long text entry can remain,
                # and won't raise a validation error until it's next
                # edited.
                sponsor_benefit.save()

                allowed_benefits.append(sponsor_benefit.pk)

        # Any remaining sponsor benefits that don't normally belong to
        # this level are set to inactive
        self.sponsor_benefits.exclude(pk__in=allowed_benefits).update(active=False, max_words=None, other_limits="")

    # @@@ should this just be done centrally?
    def send_coordinator_emails(self):
        for user in User.objects.filter(groups__name=SPONSOR_COORDINATORS):
            send_email(
                [user.email], "sponsor_signup",
                context={"sponsor": self}
            )

    def benefit_is_complete(self, name):
        """Return True - benefit is complete, False - benefit is not complete,
         or None - benefit not applicable for this sponsor's level """
        if BenefitLevel.objects.filter(level=self.level, benefit__name=name).exists():
            try:
                benefit = self.sponsor_benefits.get(benefit__name=name)
            except SponsorBenefit.DoesNotExist:
                return False
            else:
                return benefit.is_complete
        else:
            return None   # Not an applicable benefit for this sponsor's level

def _store_initial_level(sender, instance, **kwargs):
    if instance:
        instance._initial_level_id = instance.level_id
post_init.connect(_store_initial_level, sender=Sponsor)

def _check_level_change(sender, instance, created, **kwargs):
    if instance and (created or instance.level_id != instance._initial_level_id):
        instance.reset_benefits()
post_save.connect(_check_level_change, sender=Sponsor)

def _send_admin_email(sender, instance, created, **kwargs):
    """
    Send an email to the sponsors mailing list when a new application is
    submitted.
    """
    if created:
        send_email(
            to=[settings.SPONSORSHIP_EMAIL],
            kind='new_sponsor',
            context={
                'sponsor': instance,
            },
        )
post_save.connect(_send_admin_email, sender=Sponsor)

def _store_initial_active(sender, instance, **kwargs):
    if instance:
        instance._initial_active = instance.active
post_init.connect(_store_initial_active, sender=Sponsor)
post_save.connect(_store_initial_active, sender=Sponsor)

def _check_active_change(sender, instance, **kwargs):
    if instance:
        if instance.active:
            if not instance._initial_active or not instance.approval_time:
                # Instance is newly active.
                instance.approval_time = datetime.datetime.now()
        else:
            instance.approval_time = None
pre_save.connect(_check_active_change, sender=Sponsor)

def _send_sponsor_notification_emails(sender, instance, created, **kwargs):
    if instance and created:
        instance.send_coordinator_emails()
post_save.connect(_send_sponsor_notification_emails, sender=Sponsor)

class Benefit(models.Model):

    name = models.CharField(_("name"), max_length=100, unique=True)
    description = models.TextField(_("description"), blank=True)
    type = models.CharField(
        _("type"),
        choices=[
            ("text", "Text"),
            ("richtext", "Rich Text"),
            ("file", "File"),
            ("weblogo", "Web Logo"),
            ("simple", "Simple")
        ],
        max_length=10,
        default="simple"
    )

    def __unicode__(self):
        return self.name

class BenefitLevel(models.Model):

    benefit = models.ForeignKey(
        Benefit,
        related_name="benefit_levels",
        verbose_name=_("benefit")
    )
    level = models.ForeignKey(
        SponsorLevel,
        related_name="benefit_levels",
        verbose_name=_("level")
    )
    max_words = models.PositiveIntegerField(_("max words"), blank=True, null=True)
    other_limits = models.CharField(_("other limits"), max_length=200, blank=True)

    class Meta:
        ordering = ["level"]

    def __unicode__(self):
        return u"%s - %s" % (self.level, self.benefit)

class SponsorBenefit(models.Model):

    sponsor = models.ForeignKey(
        Sponsor,
        related_name="sponsor_benefits",
        verbose_name=_("sponsor")
    )
    benefit = models.ForeignKey(
        Benefit,
        related_name="sponsor_benefits",
        verbose_name=_("benefit")
    )
    active = models.BooleanField(default=True)

    # Limits: will initially be set to defaults from corresponding BenefitLevel
    max_words = models.PositiveIntegerField(_("max words"), blank=True, null=True)
    other_limits = models.CharField(_("other limits"), max_length=200, blank=True)

    # Data: zero or one of these fields will be used, depending on the
    # type of the Benefit (text, file, or simple)
    text = models.TextField(_("text"), blank=True)
    upload = models.FileField(_("file"), blank=True, upload_to="sponsor_files")

    # Whether any assets required from the sponsor have been provided
    # (e.g. a logo file for a Web logo benefit).
    is_complete = models.NullBooleanField(help_text=_(u"True - benefit complete; False - benefit incomplete; Null - n/a"))

    class Meta:
        ordering = ['-active']

    def __unicode__(self):
        return u"%s - %s (%s)" % (self.sponsor, self.benefit,
                                  self.benefit.type)

    def save(self, *args, **kwargs):
        # Validate - save() doesn't clean your model by default, so call
        # it explicitly before saving
        self.full_clean()
        self.is_complete = self._is_complete()
        super(SponsorBenefit, self).save(*args, **kwargs)

    def clean(self):
        if self.max_words and len(self.text.split()) > self.max_words:
            raise ValidationError("Sponsorship level only allows for %s "
                                  "words." % self.max_words)
        editable_fields = self.data_fields()
        if bool(self.text) and 'text' not in editable_fields:
            raise ValidationError("Benefit type %s may not have text"
                                  % self.benefit.type)
        if bool(self.upload) and 'upload' not in editable_fields:
            raise ValidationError("Benefit type %s may not have an uploaded "
                                  "file (%s)" % (self.benefit.type,
                                                 self.upload))

    def data_fields(self):
        """
        Return list of data field names which should be editable for
        this ``SponsorBenefit``, depending on its ``Benefit`` type.
        """
        if self.benefit.type == "file" or self.benefit.type == "weblogo":
            return ["upload"]
        elif self.benefit.type in ("text", "richtext", "simple"):
            return ["text"]
        return []

    def _is_complete(self):
        return self.active and \
            ((self.benefit.type in ('text', 'richtext') and bool(self.text))
                or (self.benefit.type in ('file', 'weblogo') and bool(self.upload)))
<commit_msg>Fix interpolation of promo codes into emails<commit_after>import datetime
from django.conf import settings

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import SET_NULL
from django.db.models.signals import post_init, post_save, pre_save
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from django.contrib.auth.models import User
from multi_email_field.fields import MultiEmailField

from symposion.conference.models import Conference

from pycon.sponsorship import SPONSOR_COORDINATORS
from pycon.sponsorship.managers import SponsorManager
from symposion.utils.mail import send_email

# The benefits we track as individual fields on sponsors
# using separate SponsorBenefit records.
# Names are the names in the database as defined by PyCon organizers.
# Field names are the benefit names, lowercased, with
# spaces changed to _, and with "_benefit" appended.
# Column titles are arbitrary.

BENEFITS = [
    # Print logo not being used for 2017 but keep it in anyway
    {
        'name': 'Print logo',
        'field_name': 'print_logo_benefit',
        'column_title': _(u'Print Logo'),
    },
    {
        'name': 'Advertisement',
        'field_name': 'advertisement_benefit',
        'column_title': _(u'Ad'),
    }
]

class SponsorLevel(models.Model):

    conference = models.ForeignKey(Conference, verbose_name=_("conference"))
    name = models.CharField(_("name"), max_length=100)
    order = models.IntegerField(_("order"), default=0)
    cost = models.PositiveIntegerField(_("cost"))
    description = models.TextField(_("description"), blank=True, help_text=_("This is private."))

    class Meta:
        ordering = ["conference", "order"]
        verbose_name = _("sponsor level")
        verbose_name_plural = _("sponsor levels")

    def __unicode__(self):
        return u"%s %s" % (self.conference, self.name)

    def sponsors(self):
        return self.sponsor_set.filter(active=True).order_by("added")

class Sponsor(models.Model):

    applicant = models.ForeignKey(User, related_name="sponsorships", verbose_name=_("applicant"), null=True, on_delete=SET_NULL)

    name = models.CharField(_("Sponsor Name"), max_length=100)
    display_url = models.CharField(
        _("Link text - text to display on link to sponsor page, if different from the actual link"),
        max_length=200,
        default='',
        blank=True
    )
    external_url = models.URLField(
        _("Link to sponsor web page"),
        help_text=_("(Must include https:// or http://.)")
    )
    twitter_username = models.CharField(
        _("Twitter username"), blank=True, max_length=15,
    )
    annotation = models.TextField(_("annotation"), blank=True)
    contact_name = models.CharField(_("Contact Name"), max_length=100)
    contact_emails = MultiEmailField(
        _(u"Contact Emails"), default='',
        help_text=_(u"Please enter one email address per line.")
    )
    contact_phone = models.CharField(_(u"Contact Phone"), max_length=32)
    contact_address = models.TextField(_(u"Contact Address"))
    level = models.ForeignKey(SponsorLevel, verbose_name=_("level"))
    added = models.DateTimeField(_("added"), default=datetime.datetime.now)

    active = models.BooleanField(_("active"), default=False)
    approval_time = models.DateTimeField(null=True, blank=True, editable=False)

    wants_table = models.BooleanField(
        _(
            'Does your organization want a table at the job fair? '
            '(See Estimated Sponsor Fees '
            'for costs that might be involved.)'
        ), default=False)
    wants_booth = models.BooleanField(
        _(
            'Does your organization want a booth on the expo floor? '
            '(See Estimated Sponsor Fees '
            'for costs that might be involved.)'
        ), default=False)
    small_entity_discount = models.BooleanField(
        _(
            'Does your organization have fewer than 25 employees,'
            ' which qualifies you for our Small Entity Discount?'
        ), default=False)

    # Whether things are complete
    # True = complete, False = incomplate, Null = n/a for this sponsor level
    print_logo_benefit = models.NullBooleanField(help_text=_(u"Print logo benefit is complete"))
    advertisement_benefit = models.NullBooleanField(help_text=_(u"Advertisement benefit is complete"))

    registration_promo_codes = models.CharField(max_length=200, blank=True, default='')
    expo_promo_codes = models.CharField(max_length=200, blank=True, default='')
    booth_number = models.IntegerField(blank=True, null=True, default=None)
    job_fair_table_number = models.IntegerField(blank=True, null=True, default=None)

    web_description = models.TextField(
        _(u"Company description (to show on the web site)"),
    )
    web_logo = models.ImageField(
        _(u"Company logo (to show on the web site)"),
        upload_to="sponsor_files",
        null=True,  # This is nullable in case old data doesn't have a web logo
        # We enforce it on all new or edited sponsors though.
    )

    objects = SponsorManager()

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = _("sponsor")
        verbose_name_plural = _("sponsors")
        ordering = ['name']

    def save(self, *args, **kwargs):
        # Set fields related to benefits being complete
        for benefit in BENEFITS:
            field_name = benefit['field_name']
            benefit_name = benefit['name']
            setattr(self, field_name, self.benefit_is_complete(benefit_name))
        super(Sponsor, self).save(*args, **kwargs)

    def get_absolute_url(self):
        if self.active:
            return reverse("sponsor_detail", kwargs={"pk": self.pk})
        return reverse("sponsor_list")

    def get_display_url(self):
        """
        Return the text to display on the sponsor's link
        """
        if self.display_url:
            return self.display_url
        else:
            return self.external_url

    def render_email(self, text):
        """Replace special strings in text with values from the sponsor.

        %%NAME%% --> Sponsor name
        %%REGISTRATION_PROMO_CODES%% --> Registration promo codes, or empty string
        %%EXPO_PROMO_CODES%% --> Expo Hall only promo codes, or empty string
        %%BOOTH_NUMBER%% --> Booth number, or empty string if not set
        %%JOB_FAIR_TABLE_NUMBER%%" --> Job fair tabl number, or empty string if not set
        """
        text = text.replace("%%NAME%%", self.name)
        text = text.replace("%%REGISTRATION_PROMO_CODES%%", self.registration_promo_codes)
        text = text.replace("%%EXPO_PROMO_CODES%%", self.expo_promo_codes)

        # The next two are numbers, or if not set, None.  We don't want to
        # display "None" :-), but we might want to display "0".
        booth = str(self.booth_number) if self.booth_number is not None else ""
        text = text.replace("%%BOOTH_NUMBER%%", booth)
        table = str(self.job_fair_table_number) if self.job_fair_table_number is not None else ""
        text = text.replace("%%JOB_FAIR_TABLE_NUMBER%%", table)
        return text

    @cached_property
    def website_logo_url(self):
        if self.web_logo:
            return self.web_logo.url

    @property
    def joblisting_text(self):
        if not hasattr(self, "_joblisting_text"):
            self._joblisting_text = None
            benefits = self.sponsor_benefits.filter(benefit__id=8)
            if benefits.count():
                self._joblisting_text = benefits[0].text
        return self._joblisting_text

    @property
    def website_logo(self):
        return self.web_logo

    def reset_benefits(self):
        """
        Reset all benefits for this sponsor to the defaults for their
        sponsorship level.
        """
        level = None

        try:
            level = self.level
        except SponsorLevel.DoesNotExist:
            pass

        allowed_benefits = []
        if level:
            for benefit_level in level.benefit_levels.all():
                # Create all needed benefits if they don't exist already
                sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
                    sponsor=self, benefit=benefit_level.benefit)

                # and set to default limits for this level.
                sponsor_benefit.max_words = benefit_level.max_words
                sponsor_benefit.other_limits = benefit_level.other_limits

                # and set to active
                sponsor_benefit.active = True

                # @@@ We don't call sponsor_benefit.clean here. This means
                # that if the sponsorship level for a sponsor is adjusted
                # downwards, an existing too-long text entry can remain,
                # and won't raise a validation error until it's next
                # edited.
                sponsor_benefit.save()

                allowed_benefits.append(sponsor_benefit.pk)

        # Any remaining sponsor benefits that don't normally belong to
        # this level are set to inactive
        self.sponsor_benefits.exclude(pk__in=allowed_benefits).update(active=False, max_words=None, other_limits="")

    # @@@ should this just be done centrally?
    def send_coordinator_emails(self):
        for user in User.objects.filter(groups__name=SPONSOR_COORDINATORS):
            send_email(
                [user.email], "sponsor_signup",
                context={"sponsor": self}
            )

    def benefit_is_complete(self, name):
        """Return True - benefit is complete, False - benefit is not complete,
         or None - benefit not applicable for this sponsor's level """
        if BenefitLevel.objects.filter(level=self.level, benefit__name=name).exists():
            try:
                benefit = self.sponsor_benefits.get(benefit__name=name)
            except SponsorBenefit.DoesNotExist:
                return False
            else:
                return benefit.is_complete
        else:
            return None   # Not an applicable benefit for this sponsor's level

def _store_initial_level(sender, instance, **kwargs):
    if instance:
        instance._initial_level_id = instance.level_id
post_init.connect(_store_initial_level, sender=Sponsor)

def _check_level_change(sender, instance, created, **kwargs):
    if instance and (created or instance.level_id != instance._initial_level_id):
        instance.reset_benefits()
post_save.connect(_check_level_change, sender=Sponsor)

def _send_admin_email(sender, instance, created, **kwargs):
    """
    Send an email to the sponsors mailing list when a new application is
    submitted.
    """
    if created:
        send_email(
            to=[settings.SPONSORSHIP_EMAIL],
            kind='new_sponsor',
            context={
                'sponsor': instance,
            },
        )
post_save.connect(_send_admin_email, sender=Sponsor)

def _store_initial_active(sender, instance, **kwargs):
    if instance:
        instance._initial_active = instance.active
post_init.connect(_store_initial_active, sender=Sponsor)
post_save.connect(_store_initial_active, sender=Sponsor)

def _check_active_change(sender, instance, **kwargs):
    if instance:
        if instance.active:
            if not instance._initial_active or not instance.approval_time:
                # Instance is newly active.
                instance.approval_time = datetime.datetime.now()
        else:
            instance.approval_time = None
pre_save.connect(_check_active_change, sender=Sponsor)

def _send_sponsor_notification_emails(sender, instance, created, **kwargs):
    if instance and created:
        instance.send_coordinator_emails()
post_save.connect(_send_sponsor_notification_emails, sender=Sponsor)

class Benefit(models.Model):

    name = models.CharField(_("name"), max_length=100, unique=True)
    description = models.TextField(_("description"), blank=True)
    type = models.CharField(
        _("type"),
        choices=[
            ("text", "Text"),
            ("richtext", "Rich Text"),
            ("file", "File"),
            ("weblogo", "Web Logo"),
            ("simple", "Simple")
        ],
        max_length=10,
        default="simple"
    )

    def __unicode__(self):
        return self.name

class BenefitLevel(models.Model):

    benefit = models.ForeignKey(
        Benefit,
        related_name="benefit_levels",
        verbose_name=_("benefit")
    )
    level = models.ForeignKey(
        SponsorLevel,
        related_name="benefit_levels",
        verbose_name=_("level")
    )
    max_words = models.PositiveIntegerField(_("max words"), blank=True, null=True)
    other_limits = models.CharField(_("other limits"), max_length=200, blank=True)

    class Meta:
        ordering = ["level"]

    def __unicode__(self):
        return u"%s - %s" % (self.level, self.benefit)

class SponsorBenefit(models.Model):

    sponsor = models.ForeignKey(
        Sponsor,
        related_name="sponsor_benefits",
        verbose_name=_("sponsor")
    )
    benefit = models.ForeignKey(
        Benefit,
        related_name="sponsor_benefits",
        verbose_name=_("benefit")
    )
    active = models.BooleanField(default=True)

    # Limits: will initially be set to defaults from corresponding BenefitLevel
    max_words = models.PositiveIntegerField(_("max words"), blank=True, null=True)
    other_limits = models.CharField(_("other limits"), max_length=200, blank=True)

    # Data: zero or one of these fields will be used, depending on the
    # type of the Benefit (text, file, or simple)
    text = models.TextField(_("text"), blank=True)
    upload = models.FileField(_("file"), blank=True, upload_to="sponsor_files")

    # Whether any assets required from the sponsor have been provided
    # (e.g. a logo file for a Web logo benefit).
    is_complete = models.NullBooleanField(help_text=_(u"True - benefit complete; False - benefit incomplete; Null - n/a"))

    class Meta:
        ordering = ['-active']

    def __unicode__(self):
        return u"%s - %s (%s)" % (self.sponsor, self.benefit,
                                  self.benefit.type)

    def save(self, *args, **kwargs):
        # Validate - save() doesn't clean your model by default, so call
        # it explicitly before saving
        self.full_clean()
        self.is_complete = self._is_complete()
        super(SponsorBenefit, self).save(*args, **kwargs)

    def clean(self):
        if self.max_words and len(self.text.split()) > self.max_words:
            raise ValidationError("Sponsorship level only allows for %s "
                                  "words." % self.max_words)
        editable_fields = self.data_fields()
        if bool(self.text) and 'text' not in editable_fields:
            raise ValidationError("Benefit type %s may not have text"
                                  % self.benefit.type)
        if bool(self.upload) and 'upload' not in editable_fields:
            raise ValidationError("Benefit type %s may not have an uploaded "
                                  "file (%s)" % (self.benefit.type,
                                                 self.upload))

    def data_fields(self):
        """
        Return list of data field names which should be editable for
        this ``SponsorBenefit``, depending on its ``Benefit`` type.
        """
        if self.benefit.type == "file" or self.benefit.type == "weblogo":
            return ["upload"]
        elif self.benefit.type in ("text", "richtext", "simple"):
            return ["text"]
        return []

    def _is_complete(self):
        return self.active and \
            ((self.benefit.type in ('text', 'richtext') and bool(self.text))
                or (self.benefit.type in ('file', 'weblogo') and bool(self.upload)))
<|endoftext|>


<commit_before>import contextlib
import errno
import fcntl
import os
import socket
import stat
import weakref
try:
    from cStringIO import StringIO
except ImportError: #pragma: no cover
    from StringIO import StringIO

from greenhouse import utils
from greenhouse._state import state

__all__ = ["Socket", "File", "monkeypatch", "unmonkeypatch", "pipe"]

_socket = socket.socket
_open = __builtins__['open']
_file = __builtins__['file']

SOCKET_CLOSED = set((errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN))

def monkeypatch():
    """replace functions in the standard library socket module
    with their non-blocking greenhouse equivalents"""
    socket.socket = Socket
    __builtins__['open'] = __builtins__['file'] = File

def unmonkeypatch():
    "undo a call to monkeypatch()"
    socket.socket = _socket
    __builtins__['open'] = _open
    __builtins__['file'] = _file

#@utils._debugger
class Socket(object):
    def __init__(self, *args, **kwargs):
        # wrap a basic socket or build our own
        self._sock = kwargs.pop('fromsock', None)
        if isinstance(self._sock, Socket):
            self._sock = self._sock._sock
        if not self._sock:
            self._sock = _socket(*args, **kwargs)

        # copy over attributes
        self.family = self._sock.family
        self.type = self._sock.type
        self.proto = self._sock.proto
        self._fileno = self._sock.fileno()

        # make the underlying socket non-blocking
        self.setblocking(False)

        # create events
        self._readable = utils.Event()
        self._writable = utils.Event()

        # some more housekeeping
        self._timeout = None
        self._closed = False

        # make sure we have a state.poller
        if not hasattr(state, 'poller'):
            import greenhouse.poller #pragma: no cover

        # allow for lookup by fileno
        state.descriptormap[self._fileno].append(weakref.ref(self))

    def __del__(self):
        try:
            state.poller.unregister(self)
        except:
            pass

    @contextlib.contextmanager
    def _registered(self):
        try:
            state.poller.register(self)
        except (IOError, OSError), error: #pragma: no cover
            if error.args and error.args[0] in errno.errorcode:
                raise socket.error(*error.args)
            raise

        yield

        try:
            state.poller.unregister(self)
        except (IOError, OSError), error: #pragma: no cover
            if error.args and error.args[0] in errno.errorcode:
                raise socket.error(*error.args)
            raise

    def accept(self):
        with self._registered():
            while 1:
                try:
                    client, addr = self._sock.accept()
                except socket.error, err:
                    if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
                        self._readable.wait()
                        continue
                    else:
                        raise #pragma: no cover
                return type(self)(fromsock=client), addr

    def bind(self, *args, **kwargs):
        return self._sock.bind(*args, **kwargs)

    def close(self):
        self._closed = True
        return self._sock.close()

    def connect(self, address):
        with self._registered():
            while True:
                err = self.connect_ex(address)
                if err in (errno.EINPROGRESS, errno.EALREADY,
                        errno.EWOULDBLOCK):
                    self._writable.wait()
                    continue
                if err not in (0, errno.EISCONN): #pragma: no cover
                    raise socket.error(err, errno.errorcode[err])
                return

    def connect_ex(self, address):
        return self._sock.connect_ex(address)

    def dup(self):
        return type(self)(fromsock=self._sock.dup())

    def fileno(self):
        return self._fileno

    def getpeername(self):
        return self._sock.getpeername()

    def getsockname(self):
        return self._sock.getsockname()

    def getsockopt(self, *args):
        return self._sock.getsockopt(*args)

    def gettimeout(self):
        return self._timeout

    def listen(self, backlog):
        return self._sock.listen(backlog)

    def makefile(self, mode='r', bufsize=None):
        return socket._fileobject(self)

    def recv(self, nbytes):
        with self._registered():
            while 1:
                if self._closed:
                    raise socket.error(errno.EBADF, "Bad file descriptor")
                try:
                    return self._sock.recv(nbytes)
                except socket.error, e:
                    if e[0] == errno.EWOULDBLOCK: #pragma: no cover
                        self._readable.wait()
                        continue
                    if e[0] in SOCKET_CLOSED:
                        self._closed = True
                        return ''
                    raise #pragma: no cover

    def recv_into(self, buffer, nbytes):
        with self._registered():
            self._readable.wait()
            return self._sock.recv_into(buffer, nbytes)

    def recvfrom(self, nbytes):
        with self._registered():
            self._readable.wait()
            return self._sock.recvfrom(nbytes)

    def recvfrom_into(self, buffer, nbytes):
        with self._registered():
            self._readable.wait()
            return self._sock.recvfrom_into(buffer, nbytes)

    def send(self, data):
        with self._registered():
            try:
                return self._sock.send(data)
            except socket.error, err: #pragma: no cover
                if err[0] in (errno.EWOULDBLOCK, errno.ENOTCONN):
                    return 0
                raise

    def sendall(self, data):
        sent = self.send(data)
        while sent < len(data): #pragma: no cover
            self._writable.wait()
            sent += self.send(data[sent:])

    def sendto(self, *args):
        try:
            return self._sock.sendto(*args)
        except socket.error, err: #pragma: no cover
            if err[0] in (errno.EWOULDBLOCK, errno.ENOTCONN):
                return 0
            raise

    def setblocking(self, flag):
        return self._sock.setblocking(flag)

    def setsockopt(self, level, option, value):
        return self._sock.setsockopt(level, option, value)

    def shutdown(self, flag):
        return self._sock.shutdown(flag)

    def settimeout(self, timeout):
        self._timeout = timeout

#@utils._debugger
class File(object):
    CHUNKSIZE = 8192
    NEWLINE = "\n"

    @staticmethod
    def _mode_to_flags(mode):
        flags = os.O_RDONLY | os.O_NONBLOCK # always non-blocking
        if (('w' in mode or 'a' in mode) and 'r' in mode) or '+' in mode:
            # both read and write
            flags |= os.O_RDWR
        elif 'w' in mode or 'a' in mode:
            # else just write
            flags |= os.O_WRONLY

        if 'a' in mode:
            # append-write mode
            flags |= os.O_APPEND

        return flags

    def _set_up_waiting(self):
        if not hasattr(state, 'poller'):
            import greenhouse.poller #pragma: no cover
        try:
            state.poller.register(self)

            # if we got here, poller.register worked, so set up event-based IO
            self._wait = self._wait_event
            self._readable = utils.Event()
            self._writable = utils.Event()
            state.descriptormap[self._fileno].append(weakref.ref(self))
        except IOError:
            self._wait = self._wait_yield

    def __init__(self, name, mode='rb'):
        self.mode = mode
        self._buf = StringIO()

        # translate mode into the proper open flags
        flags = self._mode_to_flags(mode)

        # if write or append mode and the file doesn't exist, create it
        if flags & (os.O_WRONLY | os.O_RDWR) and not os.path.exists(name):
            os.mknod(name, 0644, stat.S_IFREG)

        # open the file, get a descriptor
        self._fileno = os.open(name, flags)

        # try to drive the asyncronous waiting off of the polling interface,
        # but epoll doesn't seem to support filesystem descriptors, so fall
        # back to a waiting with a simple yield
        self._set_up_waiting()

    def _wait_event(self, reading): #pragma: no cover
        "wait on our events"
        if reading:
            self._readable.wait()
        else:
            self._writable.wait()

    def _wait_yield(self, reading): #pragma: no cover
        "generic wait, for when polling won't work"
        scheduler.pause()

    @staticmethod
    def _add_flags(fd, flags):
        fdflags = fcntl.fcntl(fd, fcntl.F_GETFL)
        if fdflags & flags != flags:
            fcntl.fcntl(fd, fcntl.F_SETFL, flags | fdflags)

    @classmethod
    def fromfd(cls, fd, mode='rb'):
        fp = object.__new__(cls) # bypass __init__
        fp.mode = mode
        fp._fileno = fd
        fp._buf = StringIO()

        cls._add_flags(fd, cls._mode_to_flags(mode))
        fp._set_up_waiting()

        return fp

    def __iter__(self):
        line = self.readline()
        while line:
            yield line
            line = self.readline()

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    def __del__(self):
        try:
            state.poller.unregister(self)
        except:
            pass

    def close(self):
        os.close(self._fileno)
        state.poller.unregister(self)

    def fileno(self):
        return self._fileno

    def read(self, size=-1):
        chunksize = size < 0 and self.CHUNKSIZE or min(self.CHUNKSIZE, size)

        buf = self._buf
        buf.seek(0, os.SEEK_END)
        collected = buf.tell()

        while 1:
            if size >= 0 and collected >= size:
                # we have read enough already
                break

            try:
                output = os.read(self._fileno, chunksize)
            except (OSError, IOError), err: #pragma: no cover
                if err.args[0] in (errno.EAGAIN, errno.EINTR):
                    # would have blocked
                    self._wait(reading=True)
                    continue
                else:
                    raise

            if not output:
                # nothing more to read
                break

            collected += len(output)
            buf.write(output)

        # get rid of the old buffer
        rc = buf.getvalue()
        buf.seek(0)
        buf.truncate()

        if size >= 0:
            # leave the overflow in the buffer
            buf.write(rc[size:])
            return rc[:size]
        return rc

    def readline(self):
        buf = self._buf
        newline, chunksize = self.NEWLINE, self.CHUNKSIZE
        buf.seek(0)

        text = buf.read()
        while text.find(newline) < 0:
            try:
                text = os.read(self._fileno, chunksize)
            except (OSError, IOError), err: #pragma: no cover
                if err.args[0] in (errno.EAGAIN, errno.EINTR):
                    # would have blocked
                    self._wait(reading=True)
                    continue
                else:
                    raise
            if not text:
                break
            buf.write(text)
        else:
            # found a newline
            rc = buf.getvalue()
            index = rc.find(newline) + len(newline)

            buf.seek(0)
            buf.truncate()
            buf.write(rc[index:])
            return rc[:index]

        # hit the end of the file, no more newlines
        rc = buf.getvalue()
        buf.seek(0)
        buf.truncate()
        return rc

    def readlines(self):
        return list(self.__iter__())

    def seek(self, pos, modifier=0):
        os.lseek(self._fileno, pos, modifier)

        # clear out the buffer
        buf = self._buf
        buf.seek(0)
        buf.truncate()

    def tell(self):
        with os.fdopen(os.dup(self._fileno)) as fp:
            return fp.tell()

    def write(self, data):
        while data:
            try:
                went = os.write(self._fileno, data)
            except (OSError, IOError), err: #pragma: no cover
                if err.args[0] in (errno.EAGAIN, errno.EINTR):
                    self._wait(reading=False)
                    continue
                else:
                    raise

            data = data[went:]

    def writelines(self, lines):
        self.write("".join(lines))

def pipe():
    r, w = os.pipe()
    return File.fromfd(r, 'rb'), File.fromfd(w, 'wb')
<commit_msg>when we register socket descriptors with pollers, mask the events we don't need<commit_after>import contextlib
import errno
import fcntl
import os
import socket
import stat
import weakref
try:
    from cStringIO import StringIO
except ImportError: #pragma: no cover
    from StringIO import StringIO

from greenhouse import utils
from greenhouse._state import state

__all__ = ["Socket", "File", "monkeypatch", "unmonkeypatch", "pipe"]

_socket = socket.socket
_open = __builtins__['open']
_file = __builtins__['file']

SOCKET_CLOSED = set((errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN))

def monkeypatch():
    """replace functions in the standard library socket module
    with their non-blocking greenhouse equivalents"""
    socket.socket = Socket
    __builtins__['open'] = __builtins__['file'] = File

def unmonkeypatch():
    "undo a call to monkeypatch()"
    socket.socket = _socket
    __builtins__['open'] = _open
    __builtins__['file'] = _file

#@utils._debugger
class Socket(object):
    def __init__(self, *args, **kwargs):
        # wrap a basic socket or build our own
        self._sock = kwargs.pop('fromsock', None)
        if isinstance(self._sock, Socket):
            self._sock = self._sock._sock
        if not self._sock:
            self._sock = _socket(*args, **kwargs)

        # copy over attributes
        self.family = self._sock.family
        self.type = self._sock.type
        self.proto = self._sock.proto
        self._fileno = self._sock.fileno()

        # make the underlying socket non-blocking
        self.setblocking(False)

        # create events
        self._readable = utils.Event()
        self._writable = utils.Event()

        # some more housekeeping
        self._timeout = None
        self._closed = False

        # make sure we have a state.poller
        if not hasattr(state, 'poller'):
            import greenhouse.poller #pragma: no cover

        # allow for lookup by fileno
        state.descriptormap[self._fileno].append(weakref.ref(self))

    def __del__(self):
        try:
            state.poller.unregister(self)
        except:
            pass

    @contextlib.contextmanager
    def _registered(self, events=None):
        poller = state.poller
        mask = None
        if events:
            mask = 0
            if 'r' in events:
                mask |= poller.INMASK
            if 'w' in events:
                mask |= poller.OUTMASK
            if 'e' in events:
                mask |= poller.ERRMASK
        try:
            poller.register(self, mask)
        except (IOError, OSError), error: #pragma: no cover
            if error.args and error.args[0] in errno.errorcode:
                raise socket.error(*error.args)
            raise

        yield

        try:
            poller.unregister(self)
        except (IOError, OSError), error: #pragma: no cover
            if error.args and error.args[0] in errno.errorcode:
                raise socket.error(*error.args)
            raise

    def accept(self):
        with self._registered('r'):
            while 1:
                try:
                    client, addr = self._sock.accept()
                except socket.error, err:
                    if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
                        self._readable.wait()
                        continue
                    else:
                        raise #pragma: no cover
                return type(self)(fromsock=client), addr

    def bind(self, *args, **kwargs):
        return self._sock.bind(*args, **kwargs)

    def close(self):
        self._closed = True
        return self._sock.close()

    def connect(self, address):
        with self._registered('w'):
            while True:
                err = self.connect_ex(address)
                if err in (errno.EINPROGRESS, errno.EALREADY,
                        errno.EWOULDBLOCK):
                    self._writable.wait()
                    continue
                if err not in (0, errno.EISCONN): #pragma: no cover
                    raise socket.error(err, errno.errorcode[err])
                return

    def connect_ex(self, address):
        return self._sock.connect_ex(address)

    def dup(self):
        return type(self)(fromsock=self._sock.dup())

    def fileno(self):
        return self._fileno

    def getpeername(self):
        return self._sock.getpeername()

    def getsockname(self):
        return self._sock.getsockname()

    def getsockopt(self, *args):
        return self._sock.getsockopt(*args)

    def gettimeout(self):
        return self._timeout

    def listen(self, backlog):
        return self._sock.listen(backlog)

    def makefile(self, mode='r', bufsize=None):
        return socket._fileobject(self)

    def recv(self, nbytes):
        with self._registered('r'):
            while 1:
                if self._closed:
                    raise socket.error(errno.EBADF, "Bad file descriptor")
                try:
                    return self._sock.recv(nbytes)
                except socket.error, e:
                    if e[0] == errno.EWOULDBLOCK: #pragma: no cover
                        self._readable.wait()
                        continue
                    if e[0] in SOCKET_CLOSED:
                        self._closed = True
                        return ''
                    raise #pragma: no cover

    def recv_into(self, buffer, nbytes):
        with self._registered('r'):
            self._readable.wait()
            return self._sock.recv_into(buffer, nbytes)

    def recvfrom(self, nbytes):
        with self._registered('r'):
            self._readable.wait()
            return self._sock.recvfrom(nbytes)

    def recvfrom_into(self, buffer, nbytes):
        with self._registered('r'):
            self._readable.wait()
            return self._sock.recvfrom_into(buffer, nbytes)

    def send(self, data):
        try:
            return self._sock.send(data)
        except socket.error, err: #pragma: no cover
            if err[0] in (errno.EWOULDBLOCK, errno.ENOTCONN):
                return 0
            raise

    def sendall(self, data):
        with self._registered('w'):
            sent = self.send(data)
            while sent < len(data): #pragma: no cover
                self._writable.wait()
                sent += self.send(data[sent:])

    def sendto(self, *args):
        try:
            return self._sock.sendto(*args)
        except socket.error, err: #pragma: no cover
            if err[0] in (errno.EWOULDBLOCK, errno.ENOTCONN):
                return 0
            raise

    def setblocking(self, flag):
        return self._sock.setblocking(flag)

    def setsockopt(self, level, option, value):
        return self._sock.setsockopt(level, option, value)

    def shutdown(self, flag):
        return self._sock.shutdown(flag)

    def settimeout(self, timeout):
        self._timeout = timeout

#@utils._debugger
class File(object):
    CHUNKSIZE = 8192
    NEWLINE = "\n"

    @staticmethod
    def _mode_to_flags(mode):
        flags = os.O_RDONLY | os.O_NONBLOCK # always non-blocking
        if (('w' in mode or 'a' in mode) and 'r' in mode) or '+' in mode:
            # both read and write
            flags |= os.O_RDWR
        elif 'w' in mode or 'a' in mode:
            # else just write
            flags |= os.O_WRONLY

        if 'a' in mode:
            # append-write mode
            flags |= os.O_APPEND

        return flags

    def _set_up_waiting(self):
        if not hasattr(state, 'poller'):
            import greenhouse.poller #pragma: no cover
        try:
            state.poller.register(self)

            # if we got here, poller.register worked, so set up event-based IO
            self._wait = self._wait_event
            self._readable = utils.Event()
            self._writable = utils.Event()
            state.descriptormap[self._fileno].append(weakref.ref(self))
        except IOError:
            self._wait = self._wait_yield

    def __init__(self, name, mode='rb'):
        self.mode = mode
        self._buf = StringIO()

        # translate mode into the proper open flags
        flags = self._mode_to_flags(mode)

        # if write or append mode and the file doesn't exist, create it
        if flags & (os.O_WRONLY | os.O_RDWR) and not os.path.exists(name):
            os.mknod(name, 0644, stat.S_IFREG)

        # open the file, get a descriptor
        self._fileno = os.open(name, flags)

        # try to drive the asyncronous waiting off of the polling interface,
        # but epoll doesn't seem to support filesystem descriptors, so fall
        # back to a waiting with a simple yield
        self._set_up_waiting()

    def _wait_event(self, reading): #pragma: no cover
        "wait on our events"
        if reading:
            self._readable.wait()
        else:
            self._writable.wait()

    def _wait_yield(self, reading): #pragma: no cover
        "generic wait, for when polling won't work"
        scheduler.pause()

    @staticmethod
    def _add_flags(fd, flags):
        fdflags = fcntl.fcntl(fd, fcntl.F_GETFL)
        if fdflags & flags != flags:
            fcntl.fcntl(fd, fcntl.F_SETFL, flags | fdflags)

    @classmethod
    def fromfd(cls, fd, mode='rb'):
        fp = object.__new__(cls) # bypass __init__
        fp.mode = mode
        fp._fileno = fd
        fp._buf = StringIO()

        cls._add_flags(fd, cls._mode_to_flags(mode))
        fp._set_up_waiting()

        return fp

    def __iter__(self):
        line = self.readline()
        while line:
            yield line
            line = self.readline()

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    def __del__(self):
        try:
            state.poller.unregister(self)
        except:
            pass

    def close(self):
        os.close(self._fileno)
        state.poller.unregister(self)

    def fileno(self):
        return self._fileno

    def read(self, size=-1):
        chunksize = size < 0 and self.CHUNKSIZE or min(self.CHUNKSIZE, size)

        buf = self._buf
        buf.seek(0, os.SEEK_END)
        collected = buf.tell()

        while 1:
            if size >= 0 and collected >= size:
                # we have read enough already
                break

            try:
                output = os.read(self._fileno, chunksize)
            except (OSError, IOError), err: #pragma: no cover
                if err.args[0] in (errno.EAGAIN, errno.EINTR):
                    # would have blocked
                    self._wait(reading=True)
                    continue
                else:
                    raise

            if not output:
                # nothing more to read
                break

            collected += len(output)
            buf.write(output)

        # get rid of the old buffer
        rc = buf.getvalue()
        buf.seek(0)
        buf.truncate()

        if size >= 0:
            # leave the overflow in the buffer
            buf.write(rc[size:])
            return rc[:size]
        return rc

    def readline(self):
        buf = self._buf
        newline, chunksize = self.NEWLINE, self.CHUNKSIZE
        buf.seek(0)

        text = buf.read()
        while text.find(newline) < 0:
            try:
                text = os.read(self._fileno, chunksize)
            except (OSError, IOError), err: #pragma: no cover
                if err.args[0] in (errno.EAGAIN, errno.EINTR):
                    # would have blocked
                    self._wait(reading=True)
                    continue
                else:
                    raise
            if not text:
                break
            buf.write(text)
        else:
            # found a newline
            rc = buf.getvalue()
            index = rc.find(newline) + len(newline)

            buf.seek(0)
            buf.truncate()
            buf.write(rc[index:])
            return rc[:index]

        # hit the end of the file, no more newlines
        rc = buf.getvalue()
        buf.seek(0)
        buf.truncate()
        return rc

    def readlines(self):
        return list(self.__iter__())

    def seek(self, pos, modifier=0):
        os.lseek(self._fileno, pos, modifier)

        # clear out the buffer
        buf = self._buf
        buf.seek(0)
        buf.truncate()

    def tell(self):
        with os.fdopen(os.dup(self._fileno)) as fp:
            return fp.tell()

    def write(self, data):
        while data:
            try:
                went = os.write(self._fileno, data)
            except (OSError, IOError), err: #pragma: no cover
                if err.args[0] in (errno.EAGAIN, errno.EINTR):
                    self._wait(reading=False)
                    continue
                else:
                    raise

            data = data[went:]

    def writelines(self, lines):
        self.write("".join(lines))

def pipe():
    r, w = os.pipe()
    return File.fromfd(r, 'rb'), File.fromfd(w, 'wb')
<|endoftext|>


<commit_before>from __future__ import print_function, division
import os
from os.path import join
from nilmtk.utils import check_directory_exists
from nilm_metadata import convert_yaml_to_hdf5

home_dir='/Users/GJWood/nilm_gjw_data' # path to input data

def refresh_gjw_metadata(gjw_path, output_filename, format="HDF"):
    """
    Parameters
    ----------
    gjw_path : str
        The root path of the gjw dataset.
    output_filename : str
        The destination filename (including path and suffix), will default if not specified
    directory and file structure
    nilm_gjw_data
        building<1>
            elec
                4-POWER_REAL_FINE <date> Dump.csv
                5-POWER_REACTIVE_STANDARD <date> Dump.csv
                ...
        ...
        building<n>
        HDF5
            nilm_gjw_data.hdf5
        metadata
            building1.yaml
            dataset.yaml
            meter_devices.yaml
        other files
    """
    if gjw_path is None: gjw_path = home_dir
    check_directory_exists(gjw_path)
    os.chdir(gjw_path)
    gjw_path = os.getcwd()  # sort out potential issue with slashes or backslashes
    if output_filename is None:
        output_filename =join(home_dir,'HDF5','nilm_gjw_data.hdf5')
    convert_yaml_to_hdf5(join(gjw_path, 'metadata'),output_filename)
    print("Done refreshing metadata")

def main():
    refresh_gjw_metadata('c:/Users/GJWood/nilm_gjw_data', None)

if __name__ == '__main__':
    main()<commit_msg>removed format unused parameter to improve codehealth<commit_after>from __future__ import print_function, division
import os
from os.path import join
from nilmtk.utils import check_directory_exists
from nilm_metadata import convert_yaml_to_hdf5

home_dir='/Users/GJWood/nilm_gjw_data' # path to input data
# was def refresh_gjw_metadata(gjw_path, output_filename, format="HDF"):
def refresh_gjw_metadata(gjw_path, output_filename):
    """
    Parameters
    ----------
    gjw_path : str
        The root path of the gjw dataset.
    output_filename : str
        The destination filename (including path and suffix), will default if not specified
    directory and file structure
    nilm_gjw_data
        building<1>
            elec
                4-POWER_REAL_FINE <date> Dump.csv
                5-POWER_REACTIVE_STANDARD <date> Dump.csv
                ...
        ...
        building<n>
        HDF5
            nilm_gjw_data.hdf5
        metadata
            building1.yaml
            dataset.yaml
            meter_devices.yaml
        other files
    """
    if gjw_path is None: gjw_path = home_dir
    check_directory_exists(gjw_path)
    os.chdir(gjw_path)
    gjw_path = os.getcwd()  # sort out potential issue with slashes or backslashes
    if output_filename is None:
        output_filename =join(home_dir,'HDF5','nilm_gjw_data.hdf5')
    convert_yaml_to_hdf5(join(gjw_path, 'metadata'),output_filename)
    print("Done refreshing metadata")

def main():
    refresh_gjw_metadata('c:/Users/GJWood/nilm_gjw_data', None)

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


<commit_before>import csv
import datetime
from builtins import str

import six
from django.contrib import admin
from django.contrib.admin.models import CHANGE, LogEntry
from django.contrib.admin.views.main import ChangeList
from django.contrib.contenttypes.models import ContentType
from django.db.models.aggregates import Sum
from django.db.models.fields.files import FieldFile
from django.db.models.query import QuerySet
from django.http import HttpResponse
from django.utils.encoding import smart_str
from djmoney.money import Money
from parler.admin import TranslatableAdmin
from solo.admin import SingletonModelAdmin

from bluebottle.activities.models import Contributor
from bluebottle.clients import properties
from bluebottle.members.models import Member
from bluebottle.utils.exchange_rates import convert
from .models import Language, TranslationPlatformSettings
from ..segments.models import SegmentType

class LanguageAdmin(admin.ModelAdmin):
    model = Language
    list_display = ('code', 'language_name', 'native_name')

admin.site.register(Language, LanguageAdmin)

def prep_field(request, obj, field, manyToManySep=';'):
    """ Returns the field as a unicode string. If the field is a callable, it
    attempts to call it first, without arguments.
    """
    if '__' in field:
        bits = field.split('__')
        field = bits.pop()

        for bit in bits:
            obj = getattr(obj, bit, None)

            if obj is None:
                return ""

    attr = getattr(obj, field)

    if isinstance(attr, FieldFile):
        attr = request.build_absolute_uri(attr.url)

    if isinstance(attr, Money):
        attr = str(attr)

    if isinstance(attr, datetime.datetime):
        attr = attr.strftime('%d-%m-%y %H:%M')

    output = attr() if callable(attr) else attr

    if isinstance(output, (list, tuple, QuerySet)):
        output = manyToManySep.join([str(item) for item in output])
    return output if output else ""

def escape_csv_formulas(item):
    if item and isinstance(item, six.string_types):
        if item[0] in ['=', '+', '-', '@']:
            item = u"'" + item
        return smart_str(item)
    else:
        return item

def export_as_csv_action(description="Export as CSV", fields=None, exclude=None, header=True,
                         manyToManySep=';'):
    """ This function returns an export csv action. """

    def export_as_csv(modeladmin, request, queryset):
        """ Generic csv export admin action.
        Based on http://djangosnippets.org/snippets/2712/
        """
        opts = modeladmin.model._meta
        field_names = [field.name for field in opts.fields]
        labels = []

        if exclude:
            field_names = [f for f in field_names if f not in exclude]

        elif fields:
            try:
                field_names = [field for field, _ in fields]
                labels = [label for _, label in fields]
            except ValueError:
                field_names = [field for field in fields]
                labels = field_names

        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="%s.csv"' % (
            str(opts).replace('.', '_')
        )

        writer = csv.writer(response)

        if header:
            row = labels if labels else field_names
            if queryset.model is Member or issubclass(queryset.model, Contributor):
                for segment_type in SegmentType.objects.all():
                    labels.append(segment_type.name)
            writer.writerow([escape_csv_formulas(item) for item in row])

        if queryset.model is Member:
            queryset = queryset.prefetch_related('place')
            queryset = queryset.prefetch_related('segments')
            queryset = queryset.prefetch_related('contributor_set')

        for obj in queryset:
            row = [prep_field(request, obj, field, manyToManySep) for field in field_names]

            # Write extra field data
            if queryset.model is Member:
                for segment_type in SegmentType.objects.all():
                    segments = " | ".join(obj.segments.filter(
                        segment_type=segment_type).values_list('name', flat=True))
                    row.append(segments)
            if issubclass(queryset.model, Contributor):
                for segment_type in SegmentType.objects.all():
                    if obj.user:
                        segments = " | ".join(obj.user.segments.filter(
                            segment_type=segment_type).values_list('name', flat=True))
                    else:
                        segments = ''
                    row.append(segments)
            escaped_row = [escape_csv_formulas(item) for item in row]
            writer.writerow(escaped_row)
        return response

    export_as_csv.short_description = description
    export_as_csv.acts_on_all = True
    return export_as_csv

class TotalAmountAdminChangeList(ChangeList):
    def get_results(self, *args, **kwargs):
        self.model_admin.change_list_template = 'utils/admin/total_amount_change_list.html'
        super(TotalAmountAdminChangeList, self).get_results(*args, **kwargs)

        total_column = self.model_admin.total_column or 'amount'
        currency_column = '{}_currency'.format(total_column)

        totals = self.queryset.values(
            currency_column
        ).annotate(
            total=Sum(total_column)
        ).order_by()

        amounts = [Money(total['total'], total[currency_column]) for total in totals]
        amounts = [convert(amount, properties.DEFAULT_CURRENCY) for amount in amounts]
        self.total = sum(amounts) or Money(0, properties.DEFAULT_CURRENCY)

class BasePlatformSettingsAdmin(SingletonModelAdmin):
    pass

def log_action(obj, user, change_message='Changed', action_flag=CHANGE):
    LogEntry.objects.log_action(
        user_id=user.id,
        content_type_id=ContentType.objects.get_for_model(obj).pk,
        object_id=obj.pk,
        object_repr=str(obj),
        action_flag=action_flag,
        change_message=change_message
    )

@admin.register(TranslationPlatformSettings)
class TranslationPlatformSettingsAdmin(TranslatableAdmin, BasePlatformSettingsAdmin):
    pass
<commit_msg>Fix csv better<commit_after>import csv
import datetime
from builtins import str

import six
from django.contrib import admin
from django.contrib.admin.models import CHANGE, LogEntry
from django.contrib.admin.views.main import ChangeList
from django.contrib.contenttypes.models import ContentType
from django.db.models.aggregates import Sum
from django.db.models.fields.files import FieldFile
from django.db.models.query import QuerySet
from django.http import HttpResponse
from django.utils.encoding import smart_str
from djmoney.money import Money
from parler.admin import TranslatableAdmin
from solo.admin import SingletonModelAdmin

from bluebottle.activities.models import Contributor
from bluebottle.clients import properties
from bluebottle.members.models import Member
from bluebottle.utils.exchange_rates import convert
from .models import Language, TranslationPlatformSettings
from ..segments.models import SegmentType

class LanguageAdmin(admin.ModelAdmin):
    model = Language
    list_display = ('code', 'language_name', 'native_name')

admin.site.register(Language, LanguageAdmin)

def prep_field(request, obj, field, manyToManySep=';'):
    """ Returns the field as a unicode string. If the field is a callable, it
    attempts to call it first, without arguments.
    """
    if '__' in field:
        bits = field.split('__')
        field = bits.pop()

        for bit in bits:
            obj = getattr(obj, bit, None)

            if obj is None:
                return ""

    attr = getattr(obj, field)

    if isinstance(attr, FieldFile):
        attr = request.build_absolute_uri(attr.url)

    if isinstance(attr, Money):
        attr = str(attr)

    if isinstance(attr, datetime.datetime):
        attr = attr.strftime('%d-%m-%y %H:%M')

    output = attr() if callable(attr) else attr

    if isinstance(output, (list, tuple, QuerySet)):
        output = manyToManySep.join([str(item) for item in output])
    return output if output else ""

def escape_csv_formulas(item):
    if item and isinstance(item, six.string_types):
        if item[0] in ['=', '+', '-', '@']:
            item = u"'" + item
        return smart_str(item)
    else:
        return item

def export_as_csv_action(description="Export as CSV", fields=None, exclude=None, header=True,
                         manyToManySep=';'):
    """ This function returns an export csv action. """

    def export_as_csv(modeladmin, request, queryset):
        """ Generic csv export admin action.
        Based on http://djangosnippets.org/snippets/2712/
        """
        opts = modeladmin.model._meta
        field_names = [field.name for field in opts.fields]
        labels = []

        if exclude:
            field_names = [f for f in field_names if f not in exclude]

        elif fields:
            try:
                field_names = [field for field, _ in fields]
                labels = [label for _, label in fields]
            except ValueError:
                field_names = [field for field in fields]
                labels = field_names

        response = HttpResponse(content_type='text/csv; charset=utf-8')
        response['Content-Disposition'] = 'attachment; filename="%s.csv"' % (
            str(opts).replace('.', '_')
        )
        writer = csv.writer(response, delimiter=';', dialect='excel')

        if header:
            row = labels if labels else field_names
            if queryset.model is Member or issubclass(queryset.model, Contributor):
                for segment_type in SegmentType.objects.all():
                    labels.append(segment_type.name)
            writer.writerow([escape_csv_formulas(item) for item in row])

        if queryset.model is Member:
            queryset = queryset.prefetch_related('place')
            queryset = queryset.prefetch_related('segments')
            queryset = queryset.prefetch_related('contributor_set')

        for obj in queryset:
            row = [prep_field(request, obj, field, manyToManySep) for field in field_names]

            # Write extra field data
            if queryset.model is Member:
                for segment_type in SegmentType.objects.all():
                    segments = " | ".join(obj.segments.filter(
                        segment_type=segment_type).values_list('name', flat=True))
                    row.append(segments)
            if issubclass(queryset.model, Contributor):
                for segment_type in SegmentType.objects.all():
                    if obj.user:
                        segments = " | ".join(obj.user.segments.filter(
                            segment_type=segment_type).values_list('name', flat=True))
                    else:
                        segments = ''
                    row.append(segments)
            escaped_row = [escape_csv_formulas(item) for item in row]
            writer.writerow(escaped_row)
        return response

    export_as_csv.short_description = description
    export_as_csv.acts_on_all = True
    return export_as_csv

class TotalAmountAdminChangeList(ChangeList):
    def get_results(self, *args, **kwargs):
        self.model_admin.change_list_template = 'utils/admin/total_amount_change_list.html'
        super(TotalAmountAdminChangeList, self).get_results(*args, **kwargs)

        total_column = self.model_admin.total_column or 'amount'
        currency_column = '{}_currency'.format(total_column)

        totals = self.queryset.values(
            currency_column
        ).annotate(
            total=Sum(total_column)
        ).order_by()

        amounts = [Money(total['total'], total[currency_column]) for total in totals]
        amounts = [convert(amount, properties.DEFAULT_CURRENCY) for amount in amounts]
        self.total = sum(amounts) or Money(0, properties.DEFAULT_CURRENCY)

class BasePlatformSettingsAdmin(SingletonModelAdmin):
    pass

def log_action(obj, user, change_message='Changed', action_flag=CHANGE):
    LogEntry.objects.log_action(
        user_id=user.id,
        content_type_id=ContentType.objects.get_for_model(obj).pk,
        object_id=obj.pk,
        object_repr=str(obj),
        action_flag=action_flag,
        change_message=change_message
    )

@admin.register(TranslationPlatformSettings)
class TranslationPlatformSettingsAdmin(TranslatableAdmin, BasePlatformSettingsAdmin):
    pass
<|endoftext|>


<commit_before>import unittest
import datetime

import pytz
from pyiem.nws import ugc

STR1 = "DCZ001-170200-"
STR2 = "DCZ001-MDZ004>007-009>011-013-014-016>018-VAZ036>042-050>057-170200-"

class TestObservation(unittest.TestCase):

    def test_totextstr(self):
        """ See if we can generate a proper string from a UGCS """
        ugcs = [ugc.UGC("DC", "Z", "001"), ugc.UGC("IA", "C", "001"),
                ugc.UGC("IA", "C", "002")]
        self.assertEquals(ugc.ugcs_to_text(ugcs),
                          "((DCZ001)) [DC] and ((IAC001)), ((IAC002)) [IA]")

    def test_str1(self):
        """ check ugc.parse of STR1 parsing """
        valid = datetime.datetime(2008, 12, 17, 3, 0)
        valid = valid.replace(tzinfo=pytz.timezone('UTC'))
        (ugcs, expire) = ugc.parse(STR1, valid)

        expire_answer = valid.replace(hour=2)
        ugcs_answer = [ugc.UGC("DC", "Z", "001"), ]

        self.assertEqual(ugcs, ugcs_answer)
        self.assertEqual(expire, expire_answer)

    def test_str2(self):
        """ check ugc.parse of STR2 parsing """
        valid = datetime.datetime(2008, 12, 17, 3, 0)
        valid = valid.replace(tzinfo=pytz.timezone('UTC'))
        (ugcs, expire) = ugc.parse(STR2, valid)

        expire_answer = valid.replace(hour=2)
        ugcs_answer = [ugc.UGC("DC", "Z", "001"), ugc.UGC("MD", "Z", "004"),
                       ugc.UGC("MD", "Z", "005"), ugc.UGC("MD", "Z", "006"),
                       ugc.UGC("MD", "Z", "007"), ugc.UGC("MD", "Z", "009"),
                       ugc.UGC("MD", "Z", "010"), ugc.UGC("MD", "Z", "011"),
                       ugc.UGC("MD", "Z", "013"), ugc.UGC("MD", "Z", "014"),
                       ugc.UGC("MD", "Z", "016"), ugc.UGC("MD", "Z", "017"),
                       ugc.UGC("MD", "Z", "018"), ugc.UGC("VA", "Z", "036"),
                       ugc.UGC("VA", "Z", "037"), ugc.UGC("VA", "Z", "038"),
                       ugc.UGC("VA", "Z", "039"), ugc.UGC("VA", "Z", "040"),
                       ugc.UGC("VA", "Z", "041"), ugc.UGC("VA", "Z", "042"),
                       ugc.UGC("VA", "Z", "050"), ugc.UGC("VA", "Z", "051"),
                       ugc.UGC("VA", "Z", "052"), ugc.UGC("VA", "Z", "053"),
                       ugc.UGC("VA", "Z", "054"), ugc.UGC("VA", "Z", "055"),
                       ugc.UGC("VA", "Z", "056"), ugc.UGC("VA", "Z", "057")]

        self.assertEqual(ugcs, ugcs_answer)
        self.assertEqual(expire, expire_answer)
<commit_msg>pylint the ugc test<commit_after>"""Can we parse UGC strings"""

from pyiem.util import utc
from pyiem.nws import ugc

STR1 = "DCZ001-170200-"
STR2 = "DCZ001-MDZ004>007-009>011-013-014-016>018-VAZ036>042-050>057-170200-"

def test_missed_ugc():
    """Invalid encoded county string, check that NMC006 was not included"""
    text = ("NMC001-005>007-011-019-027-028-031-033-039-041-043-045-"
            "047-049-053-055-057-061-040300-")
    valid = utc(2018, 6, 4, 3, 0)
    (ugcs, expire) = ugc.parse(text, valid)
    assert expire == valid
    assert ugcs[3] != ugc.UGC("NM", "C", "006")

def test_totextstr():
    """ See if we can generate a proper string from a UGCS """
    ugcs = [ugc.UGC("DC", "Z", "001"), ugc.UGC("IA", "C", "001"),
            ugc.UGC("IA", "C", "002")]
    assert (ugc.ugcs_to_text(ugcs) ==
            "((DCZ001)) [DC] and ((IAC001)), ((IAC002)) [IA]")

def test_str1():
    """ check ugc.parse of STR1 parsing """
    valid = utc(2008, 12, 17, 3, 0)
    (ugcs, expire) = ugc.parse(STR1, valid)

    expire_answer = valid.replace(hour=2)
    ugcs_answer = [ugc.UGC("DC", "Z", "001"), ]

    assert ugcs == ugcs_answer
    assert expire == expire_answer

def test_str2():
    """ check ugc.parse of STR2 parsing """
    valid = utc(2008, 12, 17, 3, 0)
    (ugcs, expire) = ugc.parse(STR2, valid)

    expire_answer = valid.replace(hour=2)
    ugcs_answer = [ugc.UGC("DC", "Z", "001"), ugc.UGC("MD", "Z", "004"),
                   ugc.UGC("MD", "Z", "005"), ugc.UGC("MD", "Z", "006"),
                   ugc.UGC("MD", "Z", "007"), ugc.UGC("MD", "Z", "009"),
                   ugc.UGC("MD", "Z", "010"), ugc.UGC("MD", "Z", "011"),
                   ugc.UGC("MD", "Z", "013"), ugc.UGC("MD", "Z", "014"),
                   ugc.UGC("MD", "Z", "016"), ugc.UGC("MD", "Z", "017"),
                   ugc.UGC("MD", "Z", "018"), ugc.UGC("VA", "Z", "036"),
                   ugc.UGC("VA", "Z", "037"), ugc.UGC("VA", "Z", "038"),
                   ugc.UGC("VA", "Z", "039"), ugc.UGC("VA", "Z", "040"),
                   ugc.UGC("VA", "Z", "041"), ugc.UGC("VA", "Z", "042"),
                   ugc.UGC("VA", "Z", "050"), ugc.UGC("VA", "Z", "051"),
                   ugc.UGC("VA", "Z", "052"), ugc.UGC("VA", "Z", "053"),
                   ugc.UGC("VA", "Z", "054"), ugc.UGC("VA", "Z", "055"),
                   ugc.UGC("VA", "Z", "056"), ugc.UGC("VA", "Z", "057")]

    assert ugcs == ugcs_answer
    assert expire == expire_answer
<|endoftext|>


<commit_before>from math import ceil
from webcolors import html5_parse_simple_color
from django.contrib.gis.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from busstops.models import Operator, Service, DataSource, SIRISource

def get_css(colours, direction=None, horizontal=False):
    if len(colours) == 1:
        return colours[0]
    else:
        if direction is None:
            direction = 180
        background = 'linear-gradient('
        if horizontal:
            background += 'to top'
        elif direction < 180:
            background += 'to left'
        else:
            background += 'to right'
        percentage = 100 / len(colours)
        for i, colour in enumerate(colours):
            if i != 0:
                background += ',{} {}%'.format(colour, ceil(percentage * i))
            if i != len(colours) - 1 and colour != colours[i + 1]:
                background += ',{} {}%'.format(colour, ceil(percentage * (i + 1)))
        background += ')'

        return background

class VehicleType(models.Model):
    name = models.CharField(max_length=255, unique=True)
    double_decker = models.NullBooleanField()
    coach = models.NullBooleanField()

    class Meta():
        ordering = ('name',)

    def __str__(self):
        return self.name

class Livery(models.Model):
    name = models.CharField(max_length=255, unique=True)
    colours = models.CharField(max_length=255, blank=True)
    css = models.CharField(max_length=255, blank=True)
    horizontal = models.BooleanField(default=False)

    class Meta():
        ordering = ('name',)
        verbose_name_plural = 'liveries'

    def __str__(self):
        return self.name

    def preview(self):
        if not self.colours:
            return
        background = get_css(self.colours.split(), None, self.horizontal)
        return mark_safe('

'.format(background))

class Vehicle(models.Model):
    code = models.CharField(max_length=255)
    fleet_number = models.PositiveIntegerField(null=True, blank=True)
    reg = models.CharField(max_length=24, blank=True)
    source = models.ForeignKey(DataSource, models.CASCADE, null=True, blank=True)
    operator = models.ForeignKey(Operator, models.SET_NULL, null=True, blank=True)
    vehicle_type = models.ForeignKey(VehicleType, models.SET_NULL, null=True, blank=True)
    colours = models.CharField(max_length=255, blank=True)
    livery = models.ForeignKey(Livery, models.SET_NULL, null=True, blank=True)
    notes = models.CharField(max_length=255, blank=True)
    latest_location = models.ForeignKey('VehicleLocation', models.SET_NULL, null=True, blank=True,
                                        related_name='latest_vehicle', editable=False)
    wifi = models.NullBooleanField()
    usb = models.NullBooleanField()

    class Meta():
        unique_together = ('code', 'operator')

    def __str__(self):
        if len(self.reg) > 3:
            reg = self.get_reg()
            if self.fleet_number:
                return '{} - {}'.format(self.fleet_number, reg)
            return reg
        if self.fleet_number:
            return str(self.fleet_number)
        return self.code.replace('_', ' ')

    @cached_property
    def latest_journey(self):
        return self.vehiclejourney_set.select_related('service').latest('datetime')

    def get_reg(self):
        if self.reg[-3:].isalpha():
            return self.reg[:-3] + ' ' + self.reg[-3:]
        if self.reg[:3].isalpha():
            return self.reg[:3] + ' ' + self.reg[3:]
        if self.reg[-2:].isalpha():
            return self.reg[:-2] + ' ' + self.reg[-2:]
        return self.reg

    def get_text_colour(self):
        colours = self.livery and self.livery.colours or self.colours
        if colours:
            colours = colours.split()
            parsed_colours = [html5_parse_simple_color(colour) for colour in colours]
            luminences = [c.red * .299 + c.blue * .587 + c.green * .144 for c in parsed_colours]
            luminence = sum(luminences) / len(luminences) / 255
            if luminence < .5:
                return '#fff'

    def get_livery(self, direction=None):
        colours = self.livery and self.livery.colours or self.colours
        if colours:
            colours = colours.split()
            return get_css(colours, direction, self.livery and self.livery.horizontal)

    def get_absolute_url(self):
        return reverse('vehicle_detail', args=(self.id,))

class VehicleJourney(models.Model):
    datetime = models.DateTimeField()
    service = models.ForeignKey(Service, models.SET_NULL, null=True, blank=True)
    route_name = models.CharField(max_length=64, blank=True)
    source = models.ForeignKey(DataSource, models.CASCADE)
    vehicle = models.ForeignKey(Vehicle, models.CASCADE)
    code = models.CharField(max_length=255, blank=True)
    destination = models.CharField(max_length=255, blank=True)
    direction = models.CharField(max_length=8, blank=True)

    class Meta():
        ordering = ('id',)

class JourneyCode(models.Model):
    code = models.CharField(max_length=64, blank=True)
    service = models.ForeignKey(Service, models.SET_NULL, null=True, blank=True)
    data_source = models.ForeignKey(DataSource, models.SET_NULL, null=True, blank=True)
    siri_source = models.ForeignKey(SIRISource, models.SET_NULL, null=True, blank=True)
    destination = models.CharField(max_length=255, blank=True)

    class Meta():
        unique_together = ('code', 'service', 'siri_source')

class VehicleLocation(models.Model):
    datetime = models.DateTimeField()
    latlong = models.PointField()
    journey = models.ForeignKey(VehicleJourney, models.CASCADE)
    heading = models.PositiveIntegerField(null=True, blank=True)
    early = models.IntegerField(null=True, blank=True)
    current = models.BooleanField(default=False, db_index=True)

    class Meta():
        ordering = ('id',)
        index_together = (
            ('current', 'datetime')
        )

    def get_json(self, extended=False):
        journey = self.journey
        vehicle = journey.vehicle
        json = {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': tuple(self.latlong),
            },
            'properties': {
                'vehicle': {
                    'url': vehicle.get_absolute_url(),
                    'name': str(vehicle),
                    'text_colour': vehicle.get_text_colour(),
                    'livery': vehicle.get_livery(self.heading),
                    'notes': vehicle.notes
                },
                'delta': self.early,
                'direction': self.heading,
                'datetime': self.datetime,
                'destination': journey.destination,
                'source': journey.source_id
            }
        }
        if extended:
            if vehicle.vehicle_type:
                json['properties']['vehicle']['type'] = str(vehicle.vehicle_type)
            if journey.service:
                json['properties']['service'] = {
                    'line_name': journey.service.line_name,
                    'url': journey.service.get_absolute_url()
                }
            else:
                json['properties']['service'] = {
                    'line_name': journey.route_name
                }
            if vehicle.operator:
                json['properties']['operator'] = str(vehicle.operator)
        return json
<commit_msg>Correct brightness formula. Blue and green were mixed up<commit_after>from math import ceil
from webcolors import html5_parse_simple_color
from django.contrib.gis.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from busstops.models import Operator, Service, DataSource, SIRISource

def get_css(colours, direction=None, horizontal=False):
    if len(colours) == 1:
        return colours[0]
    else:
        if direction is None:
            direction = 180
        background = 'linear-gradient('
        if horizontal:
            background += 'to top'
        elif direction < 180:
            background += 'to left'
        else:
            background += 'to right'
        percentage = 100 / len(colours)
        for i, colour in enumerate(colours):
            if i != 0:
                background += ',{} {}%'.format(colour, ceil(percentage * i))
            if i != len(colours) - 1 and colour != colours[i + 1]:
                background += ',{} {}%'.format(colour, ceil(percentage * (i + 1)))
        background += ')'

        return background

def get_brightness(colour):
    return (0.299 * colour.red + 0.587 * colour.green + 0.114 * colour.blue) / 255

class VehicleType(models.Model):
    name = models.CharField(max_length=255, unique=True)
    double_decker = models.NullBooleanField()
    coach = models.NullBooleanField()

    class Meta():
        ordering = ('name',)

    def __str__(self):
        return self.name

class Livery(models.Model):
    name = models.CharField(max_length=255, unique=True)
    colours = models.CharField(max_length=255, blank=True)
    css = models.CharField(max_length=255, blank=True)
    horizontal = models.BooleanField(default=False)

    class Meta():
        ordering = ('name',)
        verbose_name_plural = 'liveries'

    def __str__(self):
        return self.name

    def preview(self):
        if not self.colours:
            return
        background = get_css(self.colours.split(), None, self.horizontal)
        return mark_safe('

'.format(background))

class Vehicle(models.Model):
    code = models.CharField(max_length=255)
    fleet_number = models.PositiveIntegerField(null=True, blank=True)
    reg = models.CharField(max_length=24, blank=True)
    source = models.ForeignKey(DataSource, models.CASCADE, null=True, blank=True)
    operator = models.ForeignKey(Operator, models.SET_NULL, null=True, blank=True)
    vehicle_type = models.ForeignKey(VehicleType, models.SET_NULL, null=True, blank=True)
    colours = models.CharField(max_length=255, blank=True)
    livery = models.ForeignKey(Livery, models.SET_NULL, null=True, blank=True)
    notes = models.CharField(max_length=255, blank=True)
    latest_location = models.ForeignKey('VehicleLocation', models.SET_NULL, null=True, blank=True,
                                        related_name='latest_vehicle', editable=False)
    wifi = models.NullBooleanField()
    usb = models.NullBooleanField()

    class Meta():
        unique_together = ('code', 'operator')

    def __str__(self):
        if len(self.reg) > 3:
            reg = self.get_reg()
            if self.fleet_number:
                return '{} - {}'.format(self.fleet_number, reg)
            return reg
        if self.fleet_number:
            return str(self.fleet_number)
        return self.code.replace('_', ' ')

    @cached_property
    def latest_journey(self):
        return self.vehiclejourney_set.select_related('service').latest('datetime')

    def get_reg(self):
        if self.reg[-3:].isalpha():
            return self.reg[:-3] + ' ' + self.reg[-3:]
        if self.reg[:3].isalpha():
            return self.reg[:3] + ' ' + self.reg[3:]
        if self.reg[-2:].isalpha():
            return self.reg[:-2] + ' ' + self.reg[-2:]
        return self.reg

    def get_text_colour(self):
        colours = self.livery and self.livery.colours or self.colours
        if colours:
            colours = colours.split()
            parsed_colours = [html5_parse_simple_color(colour) for colour in colours]
            brightness = sum(get_brightness(colour) for colour in parsed_colours) / len(colours)
            if brightness < .5:
                return '#fff'

    def get_livery(self, direction=None):
        colours = self.livery and self.livery.colours or self.colours
        if colours:
            colours = colours.split()
            return get_css(colours, direction, self.livery and self.livery.horizontal)

    def get_absolute_url(self):
        return reverse('vehicle_detail', args=(self.id,))

class VehicleJourney(models.Model):
    datetime = models.DateTimeField()
    service = models.ForeignKey(Service, models.SET_NULL, null=True, blank=True)
    route_name = models.CharField(max_length=64, blank=True)
    source = models.ForeignKey(DataSource, models.CASCADE)
    vehicle = models.ForeignKey(Vehicle, models.CASCADE)
    code = models.CharField(max_length=255, blank=True)
    destination = models.CharField(max_length=255, blank=True)
    direction = models.CharField(max_length=8, blank=True)

    class Meta():
        ordering = ('id',)

class JourneyCode(models.Model):
    code = models.CharField(max_length=64, blank=True)
    service = models.ForeignKey(Service, models.SET_NULL, null=True, blank=True)
    data_source = models.ForeignKey(DataSource, models.SET_NULL, null=True, blank=True)
    siri_source = models.ForeignKey(SIRISource, models.SET_NULL, null=True, blank=True)
    destination = models.CharField(max_length=255, blank=True)

    class Meta():
        unique_together = ('code', 'service', 'siri_source')

class VehicleLocation(models.Model):
    datetime = models.DateTimeField()
    latlong = models.PointField()
    journey = models.ForeignKey(VehicleJourney, models.CASCADE)
    heading = models.PositiveIntegerField(null=True, blank=True)
    early = models.IntegerField(null=True, blank=True)
    current = models.BooleanField(default=False, db_index=True)

    class Meta():
        ordering = ('id',)
        index_together = (
            ('current', 'datetime')
        )

    def get_json(self, extended=False):
        journey = self.journey
        vehicle = journey.vehicle
        json = {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': tuple(self.latlong),
            },
            'properties': {
                'vehicle': {
                    'url': vehicle.get_absolute_url(),
                    'name': str(vehicle),
                    'text_colour': vehicle.get_text_colour(),
                    'livery': vehicle.get_livery(self.heading),
                    'notes': vehicle.notes
                },
                'delta': self.early,
                'direction': self.heading,
                'datetime': self.datetime,
                'destination': journey.destination,
                'source': journey.source_id
            }
        }
        if extended:
            if vehicle.vehicle_type:
                json['properties']['vehicle']['type'] = str(vehicle.vehicle_type)
            if journey.service:
                json['properties']['service'] = {
                    'line_name': journey.service.line_name,
                    'url': journey.service.get_absolute_url()
                }
            else:
                json['properties']['service'] = {
                    'line_name': journey.route_name
                }
            if vehicle.operator:
                json['properties']['operator'] = str(vehicle.operator)
        return json
<|endoftext|>