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)
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', )
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})'
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', )
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')