Stopclutch - a Django Race Manager
Tags: django, python, heroku, github
Published: 2021-05-08 11:30:53

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](https://simracingsetup.com/assetto-corsa/assetto-corsa-competizione-rating-explained/)), 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/home.jpg)

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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/g27.jpg)

## 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/game.jpg)

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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/admin.jpg)

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

![Stopclutch login](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/login.jpg)

This includes managing any `Game`:

![Stopclutch admin - games](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/admin-games.jpg)

### Player

![Stopclutch player list](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/player-list.jpg)

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](https://docs.djangoproject.com/en/3.2/topics/auth/customizing/#extending-the-existing-user-model). 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/player.jpg)

### 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/race-time.jpg)

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:

```python
def get_category_times(self):
    return RaceTime.objects.filter(track=self.track, vehicle_model=self.vehicle_model,
                                   game=self.game).order_by('race_duration')
```

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

![Stopclutch admin - race times](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/admin-race-times.jpg)

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

![Stopclutch admin - race time edit](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/admin-race-time-edit.jpg)

### 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/track.jpg)

### 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/vehicle-model.jpg)

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

![Stopclutch admin - vehicle model](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/admin-vehicle-model.jpg)

## API

This site uses [Django REST framework](https://www.django-rest-framework.org) to expose an API for select operations:

```python
urlpatterns = [
    path('players/', views.PlayerList.as_view()),
    path('assetto_times/', views.AssettoTimeCreate.as_view())
]
```

The idea behind this (which had a successful implementation in WPF at some point) was that the Assetto Corsa ["race result file"](https://www.assettocorsa.net/forum/index.php?threads/where-best-lap-times-are-located-in-game-folders.20516/#post-386223) 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/ci.jpg)

It's then deployed and served by Heroku:

![Stopclutch Heroku](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/heroku.jpg)

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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/git-root.jpg)

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

![Stopclutch Git views](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/git-views.jpg)

### 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:

```python
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]
```

### 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](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/faq.jpg)

Theming was done using [django-bootstrap4](https://django-bootstrap4.readthedocs.io/en/latest/), using SASS (SCSS) for a more powerful and customisable method of styling:

![Stopclutch SCSS](https://cdn.jsdelivr.net/gh/dylanjboyd/dylanjboyd.github.io@main/assets/img/stopclutch/scss.jpg)

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](https://sentry.io) for error monitoring, and [Codecov](https://codecov.io) for test coverage.

Until next time, all the best!