DEV Community

Cover image for Stopclutch - a Django Race Manager
Dylan Boyd
Dylan Boyd

Posted on • Originally published at thedylanboyd.com

Stopclutch - a Django Race Manager

For those that partake in sim racing, you likely have access to some auxiliary features that add context to your performance and progress within the game. That often includes record times and ghost laps. That said, what about when friends come over to play? This brings multiple challenges:

  • You don't want to overwrite your previous lap records to indicate the performance of a pool of friends.
  • In the case of more advanced monitoring of skills (such as ratings in Assetto Corsa Competizione), you don't want to spoil your records.
  • Though one could switch off configurable features that would adversely record such data, this takes time and can be disruptive when entertainment is of the essence.
  • You wish to record your friends' performance explicitly for future reference.
  • You want to share your racing records, and that of your friends, with ease.
  • You want a single platform to store racing records between multiple games.

Introductions

Introducing Stopclutch. It's a Django site to manage race times by you and your friends, colleagues, family, even the Queen:

Stopclutch home

I'm a long-time fan of Django; it's undoubtedly my favourite framework/platform to develop in for many reasons, but more on that later. This was the most prominent software project as of its inception, and one that I'm particularly proud of. Rather than being a pet project born out of curiosity and experimentation, this was born of a real need by my friends, all of whom came over to use my Logitech G27 at one point or another:

Stopclutch G27

The Objective in a Nutshell

To conveniently record, compare, and collaborate the statistics and experience of sim racing between friends (or anyone else).

Models

The model list consists of the following:

  • Game
  • Player
  • RaceTime
  • Track
  • VehicleMake
  • VehicleModel

Game

This model represents a Game that a racer would play, such as Assetto Corsa or Project CARS 2 (yes, I know, the original is supposedly better). Supporting more than one Game allows for specificity about RaceTime instances, and to compare similar setups between multiple Game instances if so desired:

Stopclutch game

Ordinarily, images would appear here to adorn vehicle models, but sadly a bug prevented that from working in the staging environment with which these screenshots were made.

Notable here is the cross-section of information given a particular object; this pattern will become more evident as more models are revealed below.

Every model in Stopclutch can be managed through the renowned admin site:

Stopclutch admin

To access this login site and any other non-"site" page, one needs to log in through the standard Django page:

Stopclutch login

This includes managing any Game:

Stopclutch admin - games

Player

Stopclutch player list

Every RaceTime is registered against a Player. Conventionally, a Player would have been one-to-one with a User, following Django's documentation on the matter. However, the main use case of Stopclutch is for friends to come over and race in my company; this allows me to log in with my credentials, allowing as few users registered on the site as possible. This reduces the site's attack surface area and upkeep effort.

Viewing a Player shows their RaceTime collection at a glance. Such a collection is accompanied by colour-coding for quick interpretation of their performance:

Stopclutch player

RaceTime

Given an instance of a recorded time, its details screen will show the properties of that RaceTime, as well as other times for the same category:

Stopclutch race time

A category isn't an explicit model, but instead represents a unique combination of Track, VehicleModel, and Game. This is represented by the following model method:

def get_category_times(self):
    return RaceTime.objects.filter(track=self.track, vehicle_model=self.vehicle_model,
                                   game=self.game).order_by('race_duration')
Enter fullscreen mode Exit fullscreen mode

As per the screenshot of the Game admin above, admin screens exist for managing any given RaceTime:

Stopclutch admin - race times

The same goes for editing a single RaceTime, bringing together all involved fields:

Stopclutch admin - race time edit

Track

When viewing a Track, the concept of a category is made more explicit. This will show the best combinations using all aforementioned model fields excluding the Track (obviously). A well-filled database will likely show lots of "1st" results here, so there may be some room for improvement which would become evident as the app is battle-tested further:

Stopclutch track

VehicleMake

Makes are stored independently of models. While these could be combined for simplicity, they are combined here to allow for logos to be stored against them such that they display nicely on-screen, and in case additional fields are to be stored against them going forward.

VehicleModel

A model is stored such that race times can accumulate across combinations of Player, Game, and Track. This once again exercises the concept of a category:

Stopclutch vehicle model

These can be modified within the admin, accommodating optional photos of the model:

Stopclutch admin - vehicle model

API

This site uses Django REST framework to expose an API for select operations:

urlpatterns = [
    path('players/', views.PlayerList.as_view()),
    path('assetto_times/', views.AssettoTimeCreate.as_view())
]
Enter fullscreen mode Exit fullscreen mode

The idea behind this (which had a successful implementation in WPF at some point) was that the Assetto Corsa "race result file" would be watched and automatically processed, sending the results to this API. The application would load the list of players before running, allowing one to select who's racing before starting a race; the results would then be submitted for the selected player when Assetto Corsa updates race_out.json at the end of a race.

CI and Deployment

Stopclutch is stored on GitHub. It's built, tested, and linted through GitHub Actions:

Stopclutch CI

It's then deployed and served by Heroku:

Stopclutch Heroku

I was in the process of converting this project to run on Docker, which would enable it to easily use Redis for caching (and so on).

However, having attained a great amount of experience with Heroku since this site's inception, I realised I was spending (and dare I say wasting) a disproportionate amount of time on the finicky parts of build and deployment.

I ultimately found this to be detracting from the enjoyment of building any application, regardless of the benefits of doing so.

Criticism and Improvements

This was definitely more about learning than mastering. So much about Django and best practice became absurdly clear while implementing Stopclutch.

Part of this learning process involved apps and organising artifacts.

Choices Around Views

Here, I chose to use one app and split the views into multiple files in one module:

Stopclutch Git root

One file was then created for each view (or group of views):

Stopclutch Git views

Lessons Learned From Views

In retrospect, the way that I felt views needed to be split into individual files demonstrated that this might have warranted app splitting instead of lumping everything into multiple files within one app, or perhaps one massive file.

That said, I haven't had the best experiences around figuring out what to do when apps use each other in Django. Understanding how apps should communicate between each other when best practice indicates that they should be self-sufficient still has me puzzled.

Choices Around Managers

I found myself polluting the VehicleModel space with methods that I felt didn't belong there, namely finding a random object in the collection. As shown below, I instead found that Django has a specific construct to work around this, indicating that other developers have found this pattern to be common enough:

class VehicleModelManager(models.Manager):
    def random(self):
        if not self.count():
            return None

        count = self.aggregate(count=Count('id'))['count']
        random_index = randint(0, count - 1)
        return self.all()[random_index]
Enter fullscreen mode Exit fullscreen mode

Lessons Learned from Managers

Having "stumbled" upon this Django functionality indicated that it's worth reading through the Django documentation thoroughly.

Documentation can be a bore, but I thoroughly congratulate all contributors of the Django project for their wonderful documentation. It probably makes for light (or even fun) reading for some, and will undoubtedly have a positive return on investment for all Django developers.

Additional Bits

I wrote a short FAQ for anyone who stumbled upon the site from the wild west of Google:

Stopclutch FAQ

Theming was done using django-bootstrap4, using SASS (SCSS) for a more powerful and customisable method of styling:

Stopclutch SCSS

The repository is closed-source, since I haven't devoted time to ensuring that the repository history contains no accidental secrets, and so on.

Conclusion

This was a project born out of love of racing and cars, and directly served the needs of myself and my friends. It taught me a lot, and is quite possibly the only personal/pet project that will continue to have a sustained life going forward.

I do hope that this taught you something you didn't previously know, or motivates you to start a similar project of your own. I found it very fulfilling, and am excited at the prospect of improving it going forward and integrating it with services such as Sentry for error monitoring, and Codecov for test coverage.

Until next time, all the best!

Top comments (0)