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:
To conveniently record, compare, and collaborate the statistics and experience of sim racing between friends (or anyone else).
The model list consists of the following:
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:
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:
To access this login site and any other non-“site” page, one needs to log in through the standard Django page:
This includes managing any
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.
Player shows their
RaceTime collection at a glance. Such a collection is accompanied by colour-coding for quick interpretation of their performance:
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:
A category isn’t an explicit model, but instead represents a unique combination of
Game. This is represented by the following model method:
1 2 3 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
The same goes for editing a single
RaceTime, bringing together all involved fields:
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:
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.
A model is stored such that race times can accumulate across combinations of
Track. This once again exercises the concept of a category:
These can be modified within the admin, accommodating optional photos of the model:
This site uses Django REST framework to expose an API for select operations:
1 2 3 4 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” 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.
Stopclutch is stored on GitHub. It’s built, tested, and linted through GitHub Actions:
It’s then deployed and served by 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.
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.
Here, I chose to use one app and split the views into multiple files in one module:
One file was then created for each view (or group of 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.
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:
1 2 3 4 5 6 7 8 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]
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.
I wrote a short FAQ for anyone who stumbled upon the site from the wild west of Google:
Theming was done using django-bootstrap4, using SASS (SCSS) for a more powerful and customisable method of styling:
The repository is closed-source, since I haven’t devoted time to ensuring that the repository history contains no accidental secrets, and so on.
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!