コード例 #1
0
ファイル: event.py プロジェクト: GlitchyLabs/donation-tracker
class SpeedRun(models.Model):
    objects = SpeedRunManager()
    event = models.ForeignKey('Event',
                              on_delete=models.PROTECT,
                              default=LatestEvent)
    name = models.CharField(max_length=64)
    display_name = models.TextField(
        max_length=256,
        blank=True,
        verbose_name='Display Name',
        help_text='How to display this game on the stream.')
    # This field is now deprecated, we should eventually set up a way to migrate the old set-up to use the donor links
    deprecated_runners = models.CharField(max_length=1024,
                                          blank=True,
                                          verbose_name='*DEPRECATED* Runners',
                                          editable=False,
                                          validators=[runners_exists])
    console = models.CharField(max_length=32, blank=True)
    commentators = models.CharField(max_length=1024, blank=True)
    description = models.TextField(max_length=1024, blank=True)
    starttime = models.DateTimeField(verbose_name='Start Time',
                                     editable=False,
                                     null=True)
    endtime = models.DateTimeField(verbose_name='End Time',
                                   editable=False,
                                   null=True)
    # can be temporarily null when moving runs around, or null when they haven't been slotted in yet
    order = models.IntegerField(
        blank=True,
        null=True,
        help_text='Please note that using the schedule editor is much easier',
        validators=[positive])
    run_time = TimestampField(always_show_h=True)
    setup_time = TimestampField(always_show_h=True)
    runners = models.ManyToManyField('Runner')
    coop = models.BooleanField(
        default=False,
        help_text=
        'Cooperative runs should be marked with this for layout purposes')
    category = models.CharField(max_length=64,
                                blank=True,
                                null=True,
                                help_text='The type of run being performed')
    release_year = models.IntegerField(
        blank=True,
        null=True,
        verbose_name='Release Year',
        help_text='The year the game was released')
    giantbomb_id = models.IntegerField(
        blank=True,
        null=True,
        verbose_name='GiantBomb Database ID',
        help_text=
        'Identifies the game in the GiantBomb database, to allow auto-population of game data.'
    )
    tech_notes = models.TextField(blank=True,
                                  help_text='Notes for the tech crew')

    class Meta:
        app_label = 'tracker'
        verbose_name = 'Speed Run'
        unique_together = (('name', 'category', 'event'), ('event', 'order'))
        ordering = ['event__datetime', 'order']
        permissions = (('can_view_tech_notes', 'Can view tech notes'), )

    def natural_key(self):
        return (self.name, self.event.natural_key())

    def clean(self):
        if not self.name:
            raise ValidationError('Name cannot be blank')
        if not self.display_name:
            self.display_name = self.name
        if not self.order:
            self.order = None

    def save(self, fix_time=True, fix_runners=True, *args, **kwargs):
        i = TimestampField.time_string_to_int
        can_fix_time = self.order is not None and (i(self.run_time) != 0
                                                   or i(self.setup_time) != 0)

        # fix our own time
        if fix_time and can_fix_time:
            prev = SpeedRun.objects.filter(event=self.event,
                                           order__lt=self.order).last()
            if prev:
                self.starttime = prev.starttime + \
                    datetime.timedelta(milliseconds=i(
                        prev.run_time) + i(prev.setup_time))
            else:
                self.starttime = self.event.datetime
            self.endtime = self.starttime + \
                datetime.timedelta(milliseconds=i(
                    self.run_time) + i(self.setup_time))

        if fix_runners and self.id:
            if not self.runners.exists():
                try:
                    self.runners.add(*[
                        Runner.objects.get_by_natural_key(r)
                        for r in util.natural_list_parse(
                            self.deprecated_runners, symbol_only=True)
                    ])
                except Runner.DoesNotExist:
                    pass
            if self.runners.exists():
                self.deprecated_runners = u', '.join(
                    unicode(r) for r in self.runners.all())

        super(SpeedRun, self).save(*args, **kwargs)

        # fix up all the others if requested
        if fix_time:
            if can_fix_time:
                next = SpeedRun.objects.filter(event=self.event,
                                               order__gt=self.order).first()
                starttime = self.starttime + \
                    datetime.timedelta(milliseconds=i(
                        self.run_time) + i(self.setup_time))
                if next and next.starttime != starttime:
                    return [self] + next.save(*args, **kwargs)
            elif self.starttime:
                prev = SpeedRun.objects.filter(
                    event=self.event,
                    starttime__lte=self.starttime).exclude(order=None).last()
                if prev:
                    self.starttime = prev.starttime + datetime.timedelta(
                        milliseconds=i(prev.run_time) + i(prev.setup_time))
                else:
                    self.starttime = self.event.timezone.localize(
                        datetime.datetime.combine(self.event.date,
                                                  datetime.time(12)))
                next = SpeedRun.objects.filter(
                    event=self.event,
                    starttime__gte=self.starttime).exclude(order=None).first()
                if next and next.starttime != self.starttime:
                    return [self] + next.save(*args, **kwargs)
        return [self]

    def name_with_category(self):
        categoryString = ' ' + self.category if self.category else ''
        return u'{0}{1}'.format(self.name, categoryString)

    def __unicode__(self):
        return u'{0} ({1})'.format(self.name_with_category(), self.event)
コード例 #2
0
ファイル: event.py プロジェクト: GlitchyLabs/donation-tracker
class Event(models.Model):
    objects = EventManager()
    short = models.CharField(max_length=64, unique=True)
    name = models.CharField(max_length=128)
    receivername = models.CharField(max_length=128,
                                    blank=True,
                                    null=False,
                                    verbose_name='Receiver Name')
    targetamount = models.DecimalField(decimal_places=2,
                                       max_digits=20,
                                       validators=[positive, nonzero],
                                       verbose_name='Target Amount')
    minimumdonation = models.DecimalField(
        decimal_places=2,
        max_digits=20,
        validators=[positive, nonzero],
        verbose_name='Minimum Donation',
        help_text='Enforces a minimum donation amount on the donate page.',
        default=decimal.Decimal('1.00'))
    usepaypalsandbox = models.BooleanField(default=False,
                                           verbose_name='Use Paypal Sandbox')
    paypalemail = models.EmailField(max_length=128,
                                    null=False,
                                    blank=False,
                                    verbose_name='Receiver Paypal')
    paypalcurrency = models.CharField(max_length=8,
                                      null=False,
                                      blank=False,
                                      default=_currencyChoices[0][0],
                                      choices=_currencyChoices,
                                      verbose_name='Currency')
    donationemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        verbose_name='Donation Email Template',
        default=None,
        null=True,
        blank=True,
        on_delete=models.PROTECT,
        related_name='event_donation_templates')
    pendingdonationemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        verbose_name='Pending Donation Email Template',
        default=None,
        null=True,
        blank=True,
        on_delete=models.PROTECT,
        related_name='event_pending_donation_templates')
    donationemailsender = models.EmailField(
        max_length=128,
        null=True,
        blank=True,
        verbose_name='Donation Email Sender')
    scheduleid = models.CharField(max_length=128,
                                  unique=True,
                                  null=True,
                                  blank=True,
                                  verbose_name='Schedule ID (LEGACY)',
                                  editable=False)
    datetime = models.DateTimeField()
    timezone = TimeZoneField(default='US/Eastern')
    locked = models.BooleanField(
        default=False,
        help_text=
        'Requires special permission to edit this event or anything associated with it'
    )
    # Fields related to prize management
    prizecoordinator = models.ForeignKey(
        User,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Coordinator',
        help_text=
        'The person responsible for managing prize acceptance/distribution')
    allowed_prize_countries = models.ManyToManyField(
        'Country',
        blank=True,
        verbose_name="Allowed Prize Countries",
        help_text=
        "List of countries whose residents are allowed to receive prizes (leave blank to allow all countries)"
    )
    disallowed_prize_regions = models.ManyToManyField(
        'CountryRegion',
        blank=True,
        verbose_name='Disallowed Regions',
        help_text=
        'A blacklist of regions within allowed countries that are not allowed for drawings (e.g. Quebec in Canada)'
    )
    prize_accept_deadline_delta = models.IntegerField(
        default=14,
        null=False,
        blank=False,
        verbose_name='Prize Accept Deadline Delta',
        help_text=
        'The number of days a winner will be given to accept a prize before it is re-rolled.',
        validators=[positive, nonzero])
    prizecontributoremailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Contributor Accept/Deny Email Template',
        help_text=
        "Email template to use when responding to prize contributor's submission requests",
        related_name='event_prizecontributortemplates')
    prizewinneremailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Winner Email Template',
        help_text="Email template to use when someone wins a prize.",
        related_name='event_prizewinnertemplates')
    prizewinneracceptemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Accepted Email Template',
        help_text=
        "Email template to use when someone accepts a prize (and thus it needs to be shipped).",
        related_name='event_prizewinneraccepttemplates')
    prizeshippedemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Shipped Email Template',
        help_text=
        "Email template to use when the aprize has been shipped to its recipient).",
        related_name='event_prizeshippedtemplates')

    def __unicode__(self):
        return self.name

    def natural_key(self):
        return (self.short, )

    def save(self, *args, **kwargs):
        if self.datetime is not None:
            if self.datetime.tzinfo is None or self.datetime.tzinfo.utcoffset(
                    self.datetime) is None:
                self.datetime = self.timezone.localize(self.datetime)
        super(Event, self).save(*args, **kwargs)

    def clean(self):
        if self.id and self.id < 1:
            raise ValidationError('Event ID must be positive and non-zero')
        if not re.match('^\w+$', self.short):
            raise ValidationError('Event short name must be a url-safe string')
        if not self.scheduleid:
            self.scheduleid = None
        if self.donationemailtemplate != None or self.pendingdonationemailtemplate != None:
            if not self.donationemailsender:
                raise ValidationError(
                    'Must specify a donation email sender if automailing is used'
                )

    @property
    def date(self):
        return self.datetime.date()

    class Meta:
        app_label = 'tracker'
        get_latest_by = 'datetime'
        permissions = (('can_edit_locked_events', 'Can edit locked events'), )
        ordering = ('datetime', )
コード例 #3
0
class SpeedRun(models.Model):
    objects = SpeedRunManager()
    event = models.ForeignKey('Event',
                              on_delete=models.PROTECT,
                              default=LatestEvent)
    name = models.CharField(max_length=64)
    display_name = models.TextField(
        max_length=256,
        blank=True,
        verbose_name='Display Name',
        help_text='How to display this game on the stream.',
    )
    shortname = models.CharField(max_length=15, blank=True)
    twitch_name = models.TextField(
        max_length=256,
        blank=True,
        verbose_name='Twitch Name',
        help_text='What game name to use on Twitch',
    )
    # This field is now deprecated, we should eventually set up a way to migrate the old set-up to use the donor links
    deprecated_runners = models.CharField(
        max_length=1024,
        blank=True,
        verbose_name='*DEPRECATED* Runners',
        editable=False,
        validators=[runners_exists],
    )
    console = models.CharField(max_length=32, blank=True)
    commentators = models.CharField(max_length=1024, blank=True)
    description = models.TextField(max_length=1024, blank=True)
    starttime = models.DateTimeField(verbose_name='Start Time',
                                     editable=False,
                                     null=True)
    endtime = models.DateTimeField(verbose_name='End Time',
                                   editable=False,
                                   null=True)
    # can be temporarily null when moving runs around, or null when they haven't been slotted in yet
    order = models.IntegerField(
        blank=True,
        null=True,
        help_text='Please note that using the schedule editor is much easier',
        validators=[positive],
    )
    run_time = TimestampField(always_show_h=True)
    setup_time = TimestampField(always_show_h=True)
    runners = models.ManyToManyField('Runner')
    coop = models.BooleanField(
        default=False,
        help_text=
        'Cooperative runs should be marked with this for layout purposes',
    )
    category = models.CharField(
        max_length=64,
        blank=True,
        null=True,
        help_text='The type of run being performed',
    )
    release_year = models.IntegerField(
        blank=True,
        null=True,
        verbose_name='Release Year',
        help_text='The year the game was released',
    )
    giantbomb_id = models.IntegerField(
        blank=True,
        null=True,
        verbose_name='GiantBomb Database ID',
        help_text=
        'Identifies the game in the GiantBomb database, to allow auto-population of game data.',
    )
    tech_notes = models.TextField(blank=True, help_text='Layout to Use')
    layout_prefix = models.CharField(max_length=30, blank=True)
    discord = models.CharField(max_length=20, blank=True)
    coms_layout = models.CharField(max_length=20, blank=True)
    tracker_mode = models.CharField(max_length=20, blank=True)
    chat_group = models.CharField(max_length=30, blank=True, default='ZSRM')
    show_seeding = models.BooleanField(
        default=False,
        help_text='Tournament matches should show runner seedings',
    )
    twitch_game = models.CharField(max_length=50, blank=True)
    twitch_tags = models.CharField(
        max_length=100,
        blank=True,
        null=True,
        help_text='Array as text',
    )
    youtube = models.CharField(
        max_length=150,
        blank=True,
        null=True,
        help_text='JSON Object as text',
    )
    custom_bg = models.CharField(max_length=30, blank=True)
    custom_cta = models.CharField(max_length=20, blank=True)
    custom_channels = models.CharField(
        max_length=150,
        blank=True,
        null=True,
        help_text='JSON Object as text',
    )

    class Meta:
        app_label = 'tracker'
        verbose_name = 'Speed Run'
        unique_together = (('name', 'category', 'event'), ('event', 'order'))
        ordering = ['event__datetime', 'order']
        permissions = (('can_view_tech_notes', 'Can view tech notes'), )

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

    def natural_key(self):
        return self.name, self.event.natural_key()

    def clean(self):
        if not self.name:
            raise ValidationError('Name cannot be blank')
        if not self.display_name:
            self.display_name = self.name
        if not self.order:
            self.order = None

    def save(self, fix_time=True, fix_runners=True, *args, **kwargs):
        i = TimestampField.time_string_to_int
        can_fix_time = self.order is not None and (i(self.run_time) != 0
                                                   or i(self.setup_time) != 0)

        # fix our own time
        if fix_time and can_fix_time:
            prev = SpeedRun.objects.filter(event=self.event,
                                           order__lt=self.order).last()
            if prev:
                self.starttime = prev.starttime + datetime.timedelta(
                    milliseconds=i(prev.run_time) + i(prev.setup_time))
            else:
                self.starttime = self.event.datetime
            self.endtime = self.starttime + datetime.timedelta(
                milliseconds=i(self.run_time) + i(self.setup_time))

        if fix_runners and self.id:
            self.deprecated_runners = ', '.join(
                sorted(str(r) for r in self.runners.all()))

        # TODO: strip out force_insert and force_delete? causes issues if you try to insert a run in the middle
        # with #create with an order parameter, but nobody should be doing that outside of tests anyway?
        # maybe the admin lets you do it...
        super(SpeedRun, self).save(*args, **kwargs)

        # fix up all the others if requested
        if fix_time:
            if can_fix_time:
                next = SpeedRun.objects.filter(event=self.event,
                                               order__gt=self.order).first()
                starttime = self.starttime + datetime.timedelta(
                    milliseconds=i(self.run_time) + i(self.setup_time))
                if next and next.starttime != starttime:
                    return [self] + next.save(*args, **kwargs)
            elif self.starttime:
                prev = (SpeedRun.objects.filter(
                    event=self.event,
                    starttime__lte=self.starttime).exclude(order=None).last())
                if prev:
                    self.starttime = prev.starttime + datetime.timedelta(
                        milliseconds=i(prev.run_time) + i(prev.setup_time))
                else:
                    self.starttime = self.event.timezone.localize(
                        datetime.datetime.combine(self.event.date,
                                                  datetime.time(12)))
                next = (SpeedRun.objects.filter(
                    event=self.event,
                    starttime__gte=self.starttime).exclude(order=None).first())
                if next and next.starttime != self.starttime:
                    return [self] + next.save(*args, **kwargs)
        return [self]

    def name_with_category(self):
        category_string = f' {self.category}' if self.category else ''
        return f'{self.name}{category_string}'

    def __str__(self):
        return f'{self.name_with_category()} (event_id: {self.event_id})'
コード例 #4
0
class Event(models.Model):
    objects = EventManager()
    short = models.CharField(
        max_length=64,
        unique=True,
        help_text='This must be unique, as it is used for slugs.',
        validators=[validate_slug],
    )
    name = models.CharField(max_length=128)
    hashtag = models.CharField(
        max_length=32,
        help_text=
        'Normally you can use the short id for this, but this value can override it.',
        blank=True,
    )
    use_one_step_screening = models.BooleanField(
        default=True,
        verbose_name='Use One-Step Screening',
        help_text='Turn this off if you use the "Head Donations" flow',
    )
    receivername = models.CharField(max_length=128,
                                    blank=True,
                                    null=False,
                                    verbose_name='Receiver Name')
    targetamount = models.DecimalField(
        decimal_places=2,
        max_digits=20,
        validators=[positive, nonzero],
        verbose_name='Target Amount',
        default=0,
    )
    minimumdonation = models.DecimalField(
        decimal_places=2,
        max_digits=20,
        validators=[positive, nonzero],
        verbose_name='Minimum Donation',
        help_text='Enforces a minimum donation amount on the donate page.',
        default=decimal.Decimal('1.00'),
    )
    auto_approve_threshold = models.DecimalField(
        'Threshold amount to send to reader or ignore',
        decimal_places=2,
        max_digits=20,
        validators=[positive],
        blank=True,
        null=True,
        help_text=
        'Leave blank to turn off auto-approval behavior. If set, anonymous, no-comment donations at or above this amount get sent to the reader. Below this amount, they are ignored.',
    )
    paypalemail = models.EmailField(max_length=128,
                                    null=False,
                                    blank=False,
                                    verbose_name='Receiver Paypal')
    paypalcurrency = models.CharField(
        max_length=8,
        null=False,
        blank=False,
        default=_currencyChoices[0][0],
        choices=_currencyChoices,
        verbose_name='Currency',
    )
    paypalimgurl = models.CharField(
        max_length=1024,
        null=False,
        blank=True,
        verbose_name='Logo URL',
    )
    donationemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        verbose_name='Donation Email Template',
        default=None,
        null=True,
        blank=True,
        on_delete=models.PROTECT,
        related_name='event_donation_templates',
    )
    pendingdonationemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        verbose_name='Pending Donation Email Template',
        default=None,
        null=True,
        blank=True,
        on_delete=models.PROTECT,
        related_name='event_pending_donation_templates',
    )
    donationemailsender = models.EmailField(
        max_length=128,
        null=True,
        blank=True,
        verbose_name='Donation Email Sender')
    scheduleid = models.CharField(
        max_length=128,
        unique=True,
        null=True,
        blank=True,
        verbose_name='Schedule ID (LEGACY)',
        editable=False,
    )
    datetime = models.DateTimeField()
    timezone = TimeZoneField(default='US/Eastern')
    locked = models.BooleanField(
        default=False,
        help_text=
        'Requires special permission to edit this event or anything associated with it.',
    )
    allow_donations = models.BooleanField(
        default=True,
        help_text=
        'Whether or not donations are open for this event. A locked event will override this setting.',
    )
    # Fields related to prize management
    prizecoordinator = models.ForeignKey(
        User,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Coordinator',
        help_text=
        'The person responsible for managing prize acceptance/distribution',
        on_delete=models.PROTECT,
    )
    allowed_prize_countries = models.ManyToManyField(
        'Country',
        blank=True,
        verbose_name='Allowed Prize Countries',
        help_text=
        'List of countries whose residents are allowed to receive prizes (leave blank to allow all countries)',
    )
    disallowed_prize_regions = models.ManyToManyField(
        'CountryRegion',
        blank=True,
        verbose_name='Disallowed Regions',
        help_text=
        'A blacklist of regions within allowed countries that are not allowed for drawings (e.g. Quebec in Canada)',
    )
    prize_accept_deadline_delta = models.IntegerField(
        default=14,
        null=False,
        blank=False,
        verbose_name='Prize Accept Deadline Delta',
        help_text=
        'The number of days a winner will be given to accept a prize before it is re-rolled.',
        validators=[positive, nonzero],
    )
    prize_drawing_date = models.DateField(
        null=True,
        blank=True,
        verbose_name='Prize Drawing Date',
        help_text=
        'Prizes will be eligible for drawing on or after this date, otherwise they will be eligible for drawing immediately after their window closes.',
    )
    prizecontributoremailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Contributor Accept/Deny Email Template',
        help_text=
        "Email template to use when responding to prize contributor's submission requests",
        related_name='event_prizecontributortemplates',
        on_delete=models.SET_NULL,
    )
    prizewinneremailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Winner Email Template',
        help_text='Email template to use when someone wins a prize.',
        related_name='event_prizewinnertemplates',
        on_delete=models.SET_NULL,
    )
    prizewinneracceptemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Accepted Email Template',
        help_text=
        'Email template to use when someone accepts a prize (and thus it needs to be shipped).',
        related_name='event_prizewinneraccepttemplates',
        on_delete=models.SET_NULL,
    )
    prizeshippedemailtemplate = models.ForeignKey(
        post_office.models.EmailTemplate,
        default=None,
        null=True,
        blank=True,
        verbose_name='Prize Shipped Email Template',
        help_text=
        'Email template to use when the aprize has been shipped to its recipient).',
        related_name='event_prizeshippedtemplates',
        on_delete=models.SET_NULL,
    )

    def __str__(self):
        return self.name

    def next(self):
        return (Event.objects.filter(datetime__gte=self.datetime).exclude(
            pk=self.pk).first())

    def prev(self):
        return (Event.objects.filter(datetime__lte=self.datetime).exclude(
            pk=self.pk).last())

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

    def natural_key(self):
        return (self.short, )

    def save(self, *args, **kwargs):
        if self.datetime is not None:
            if (self.datetime.tzinfo is None
                    or self.datetime.tzinfo.utcoffset(self.datetime) is None):
                self.datetime = self.timezone.localize(self.datetime)
        super(Event, self).save(*args, **kwargs)

        # When an event's datetime moves later than the starttime of the first
        # run, we need to trigger a save on the run to update all runs' times
        # properly to begin after the event starts.
        first_run = self.speedrun_set.all().first()
        if first_run and first_run.starttime and first_run.starttime != self.datetime:
            first_run.save(fix_time=True)

    def clean(self):
        if self.id and self.id < 1:
            raise ValidationError('Event ID must be positive and non-zero')
        if not re.match(r'^\w+$', self.short):
            raise ValidationError('Event short name must be a url-safe string')
        if not self.scheduleid:
            self.scheduleid = None
        if (self.donationemailtemplate is not None
                or self.pendingdonationemailtemplate is not None):
            if not self.donationemailsender:
                raise ValidationError(
                    'Must specify a donation email sender if automailing is used'
                )
        if (self.prize_drawing_date and
                self.speedrun_set.last().end_time >= self.prize_drawing_date):
            raise ValidationError(
                {'prise_drawing_date': 'Draw date must be after the last run'})

    @property
    def date(self):
        return self.datetime.date()

    class Meta:
        app_label = 'tracker'
        get_latest_by = 'datetime'
        permissions = (('can_edit_locked_events', 'Can edit locked events'), )
        ordering = ('datetime', )
コード例 #5
0
class Event(models.Model):
    objects = EventManager()
    short = models.CharField(max_length=64, unique=True)
    name = models.CharField(max_length=128)
    receivername = models.CharField(max_length=128, blank=True, null=False, verbose_name='Receiver Name')
    targetamount = models.DecimalField(decimal_places=2, max_digits=20, validators=[positive, nonzero],
                                       verbose_name='Target Amount')
    minimumdonation = models.DecimalField(decimal_places=2, max_digits=20, validators=[positive, nonzero],
                                          verbose_name='Minimum Donation',
                                          help_text='Enforces a minimum donation amount on the donate page.',
                                          default=decimal.Decimal('1.00'))
    usepaypalsandbox = models.BooleanField(default=False, verbose_name='Use Paypal Sandbox')
    paypalemail = models.EmailField(max_length=128, null=False, blank=False, verbose_name='Receiver Paypal')
    paypalcurrency = models.CharField(max_length=8, null=False, blank=False, default=_currencyChoices[0][0],
                                      choices=_currencyChoices, verbose_name='Currency')
    donationemailtemplate = models.ForeignKey(post_office.models.EmailTemplate, verbose_name='Donation Email Template',
                                              default=None, null=True, blank=True, on_delete=models.PROTECT,
                                              related_name='event_donation_templates')
    pendingdonationemailtemplate = models.ForeignKey(post_office.models.EmailTemplate,
                                                     verbose_name='Pending Donation Email Template', default=None,
                                                     null=True, blank=True, on_delete=models.PROTECT,
                                                     related_name='event_pending_donation_templates')
    donationemailsender = models.CharField(max_length=128, null=True, blank=True, verbose_name='Donation Email Sender')
    scheduleid = models.CharField(max_length=128, unique=True, null=True, blank=True,
                                  verbose_name='Schedule ID (LEGACY)', editable=False)
    datetime = models.DateTimeField()
    timezone = TimeZoneField(default='US/Eastern')
    locked = models.BooleanField(default=False,
                                 help_text='Requires special permission to edit this event or anything associated with it')
    # Fields related to prize management
    prizecoordinator = models.ForeignKey(User, default=None, null=True, blank=True, verbose_name='Prize Coordinator',
                                         help_text='The person responsible for managing prize acceptance/distribution',
                                         on_delete=models.PROTECT)
    allowed_prize_countries = models.ManyToManyField('Country', blank=True, verbose_name="Allowed Prize Countries",
                                                     help_text="List of countries whose residents are allowed to receive prizes (leave blank to allow all countries)")
    disallowed_prize_regions = models.ManyToManyField('CountryRegion', blank=True, verbose_name='Disallowed Regions',
                                                      help_text='A blacklist of regions within allowed countries that are not allowed for drawings (e.g. Quebec in Canada)')
    prize_accept_deadline_delta = models.IntegerField(default=14, null=False, blank=False,
                                                      verbose_name='Prize Accept Deadline Delta',
                                                      help_text='The number of days a winner will be given to accept a prize before it is re-rolled.',
                                                      validators=[positive, nonzero])
    prizecontributoremailtemplate = models.ForeignKey(post_office.models.EmailTemplate, default=None, null=True,
                                                      blank=True,
                                                      verbose_name='Prize Contributor Accept/Deny Email Template',
                                                      help_text="Email template to use when responding to prize contributor's submission requests",
                                                      related_name='event_prizecontributortemplates',
                                                      on_delete=models.PROTECT)
    prizewinneremailtemplate = models.ForeignKey(post_office.models.EmailTemplate, default=None, null=True, blank=True,
                                                 verbose_name='Prize Winner Email Template',
                                                 help_text="Email template to use when someone wins a prize.",
                                                 related_name='event_prizewinnertemplates', on_delete=models.PROTECT)
    prizewinneracceptemailtemplate = models.ForeignKey(post_office.models.EmailTemplate, default=None, null=True,
                                                       blank=True, verbose_name='Prize Accepted Email Template',
                                                       help_text="Email template to use when someone accepts a prize (and thus it needs to be shipped).",
                                                       related_name='event_prizewinneraccepttemplates',
                                                       on_delete=models.PROTECT)
    prizeshippedemailtemplate = models.ForeignKey(post_office.models.EmailTemplate, default=None, null=True, blank=True,
                                                  verbose_name='Prize Shipped Email Template',
                                                  help_text="Email template to use when the aprize has been shipped to its recipient).",
                                                  related_name='event_prizeshippedtemplates', on_delete=models.PROTECT)
    # Fields for Horaro schedule import
    horaro_id = models.CharField(max_length=100, verbose_name='Event ID', blank=True, default='',
                                 help_text='ID or slug for Horaro event')
    horaro_game_col = models.IntegerField(verbose_name='Game Column', blank=True, null=True,
                                          help_text='Column index for game info (start at 0)')
    horaro_category_col = models.IntegerField(verbose_name='Category Column', blank=True, null=True,
                                              help_text='Column index for category info (start at 0)')
    horaro_runners_col = models.IntegerField(verbose_name='Runners Column', blank=True, null=True,
                                             help_text='Column index for runner info (start at 0)')
    horaro_commentators_col = models.IntegerField(verbose_name='Commentators Column', blank=True, null=True,
                                                  help_text='Column index for commentator info (start at 0)')

    # Fields for Tiltify donation import
    tiltify_enable_sync = models.BooleanField(default=False, verbose_name='Enable Tiltify Sync',
                                              help_text='Sync donations for this event via the Tiltify API')
    tiltify_api_key = models.CharField(max_length=100, verbose_name='Tiltify Campaign API Key', blank=True, default='')

    # Fields for Twitch chat announcements
    twitch_channel = models.CharField(max_length=100, verbose_name='Channel Name', blank=True, default='',
                                      help_text='Announcements will be made to this channel')
    twitch_login = models.CharField(max_length=100, verbose_name='Username', blank=True, default='',
                                    help_text='Username to use for chat announcements')
    twitch_oauth = models.CharField(max_length=200, verbose_name='OAuth Password', blank=True, default='',
                                    help_text='Get one here: http://www.twitchapps.com/tmi')

    def __str__(self):
        return self.name

    def natural_key(self):
        return (self.short,)

    def save(self, *args, **kwargs):
        if self.datetime is not None:
            if self.datetime.tzinfo is None or self.datetime.tzinfo.utcoffset(self.datetime) is None:
                self.datetime = self.timezone.localize(self.datetime)
        super(Event, self).save(*args, **kwargs)

    def clean(self):
        errors = {}

        if self.id and self.id < 1:
            raise ValidationError('Event ID must be positive and non-zero')
        if not re.match('^\w+$', self.short):
            errors['short'] = 'Event short name must be a url-safe string'
        if not self.scheduleid:
            self.scheduleid = None
        if self.donationemailtemplate != None or self.pendingdonationemailtemplate != None:
            if not self.donationemailsender:
                errors['donationemailsender'] = 'Must specify a donation email sender if automailing is used'

        # If Tiltify sync is enabled, the API key must be populated.
        if self.tiltify_enable_sync and not self.tiltify_api_key:
            errors['tiltify_api_key'] = 'Must be populated if Tiltify sync is enabled'

        # If any Twitch chat fields are filled in, they all must be.
        if self.twitch_channel or self.twitch_login or self.twitch_oauth:
            if not self.twitch_channel:
                errors['twitch_channel'] = 'Must be filled if enabling Twitch chat announcements'
            if not self.twitch_login:
                errors['twitch_login'] = '******'
            if not self.twitch_oauth:
                errors['twitch_oauth'] = 'Must be filled if enabling Twitch chat announcements'

        # Don't put the "oauth:" starting part on the token.  IRC code will add this automatically.
        if self.twitch_oauth.startswith("oauth:"):
            self.twitch_oauth = self.twitch_oauth[6:]

        if errors:
            raise ValidationError(errors)

    @property
    def date(self):
        return self.datetime.date()

    # Extra field for displaying Horaro columns on admin UI.
    def admin_horaro_check_cols(self):
        return format_html('<span id="horaro_cols"></span>')

    admin_horaro_check_cols.allow_tags = True
    admin_horaro_check_cols.short_description = "Schedule Columns"

    class Meta:
        app_label = 'tracker'
        get_latest_by = 'datetime'
        permissions = (
            ('can_edit_locked_events', 'Can edit locked events'),
        )
        ordering = ('datetime', 'name')