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