In my previous RouteFlow post, I ended with a fairly simple goal: stop adding features and run the application from an empty account as if I were an actual climbing gym.
So that's what I did.
It didn't take very long to find the first problem.
Testing from an empty account
Until now, most of RouteFlow had been tested while individual features were being built.
This time I wanted to start from scratch and follow the actual process:
Register Gym Manager → Create Gym → Add Setters → Plan Setting Session → Assign Tasks → Complete Work → Publish Route
And I added one rule.
If I had to open Django Admin or manually fix something in the database to make the normal workflow possible, then v0.1 wasn't ready yet.
Simple enough.
The first test passed
I registered a completely new Gym Manager account.
The application logged me in correctly. The gym name appeared in the interface. The user had the correct role. There was no data leaking in from another gym.
Everything looked fine.
So initially, I marked the first step as passed.It wasn't.
When I checked the backend, I realised that registration had created the user, but it hadn't actually created the corresponding Gym.
The UI knew the name of the gym.
The database didn't know the gym existed.
gym_name is not gym
This came from a distinction I had deliberately made earlier in the project.
The user model contains both a textual gym name and an actual relationship to the Gym model:
gym = models.ForeignKey(
Gym,
null=True,
blank=True,
related_name='users',
on_delete=models.SET_NULL
)
gym_name = models.CharField(
max_length=150,
default="VertiGym Warszawska"
)
That distinction itself wasn't the problem.
The registration serializer was.
It could store gym_name, and it could accept an existing gym:
gym=validated_data.get('gym'),
gym_name=validated_data.get('gym_name', 'VertiGym'),
But it didn't create a Gym when a new manager registered one.
So I effectively had this:
User
├── gym_name = "RouteFlow Test Gym"
└── gym = NULL
when what I actually needed was:
Gym
└── name = "RouteFlow Test Gym"
↑
│
User
└── gym =
This was surprisingly easy to miss because nothing immediately broke.
Registration worked. Login worked. The frontend displayed the gym name. From the user's perspective, the account looked valid.
Only when I tested the application as a complete system did the difference between displaying a string and having an actual domain object become important.
The fix was small
The solution wasn't particularly complicated.
During registration, if no existing gym is supplied, the serializer now creates one using the submitted gym name:
gym_name = validated_data.get(
'gym_name',
'VertiGym Warszawska'
)
gym = validated_data.get('gym')
if not gym:
gym = Gym.objects.create(name=gym_name)
The newly created Gym is then assigned to the user:
user = User.objects.create_user(
email=validated_data['email'],
username=validated_data.get('username')
or validated_data['email'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', ''),
role=validated_data.get('role', 'gym_manager'),
gym=gym,
gym_name=gym_name,
password=validated_data['password'],
)
Then I repeated the test.
New user: created.
New gym: created.
User assigned to that gym: yes.
Problem solved.
Then the actual workflow worked
After fixing registration, I continued through the complete workflow: creating setters, planning a Setting Session, assigning tasks, logging in as a Route Setter and eventually publishing completed work as a Route.
That part worked.
So did the less visible parts I was particularly interested in testing: role-based permissions, persistence after refresh and data isolation between gyms.
A Route Setter could work with their assigned tasks without getting access to manager-level operations. A Head Setter could manage the setting workflow without getting access to gym-level user administration.
And a user from Gym A couldn't suddenly access Gym B's routes, setters, sessions or tasks.
Most importantly, the core relationship I've been building RouteFlow around finally worked as one continuous process:
Sector → Setting Session → Setter Task → Route
Then I hit the second problem.I still had to open Django Admin.Sectors.
The backend already understands them. Routes and Setting Sessions use them. Gym isolation applies to them.
But there currently isn't a way for a new Gym Manager to create the initial sectors from the frontend.
To continue the test, I had to create them manually through Django Admin.
And that breaks the rule I set for myself before starting this test.
In the previous post I wrote:
If it requires developer intervention along the way, v0.1 isn't ready yet.
Well.It did.So v0.1 isn't ready yet.Not quite.
Working features aren't necessarily a working product
This was probably the most useful result of the whole test.Most of RouteFlow worked.The task workflow worked. Route publishing worked. Authentication worked. RBAC worked. Gym isolation worked. Setting Sessions worked.
But a completely new customer still couldn't get from registration to actually using those features without me stepping in.
That's a very different kind of failure from an API returning 500 or a React component crashing.
And it's also much easier to miss when development consists of implementing one feature at a time.
While building a feature, the database already contains the objects you need. You already have test users. Sectors exist because you created them weeks ago. Your account is already attached to a gym. The application slowly accumulates the environment it needs to work. A new user gets none of that.
Starting again with an empty account removes all of those assumptions.
In this case, it exposed two of them almost immediately.
One was a bug: registration didn't create the actual Gym.
The other was a missing piece of the product: there was no frontend workflow for creating the first sectors.The first one is now fixed.
The second one is next. So, is the MVP ready? Almost.
And "almost" means something much more concrete now than it did before running this test.
The core workflow survived. The three user roles behave as expected. Data isolation between gyms works. A completed setting task can travel through the system and become a real Route without manual database work.
What didn't survive was the onboarding around that workflow.So I'm not adding another major feature yet.
The next job is much less exciting: let a Gym Manager create and manage sectors without Django Admin. Then I'll delete the test data, create another completely fresh account and do the whole thing again.
If I can get from registration to a published route without touching the backend manually, then I'll be much more comfortable calling RouteFlow v0.1 usable.
After that, I want the next bugs to come from people who actually work in climbing gyms. I'm fairly sure they'll find some. That's the point.
The code
RouteFlow is still under active development, and the source code is available on GitHub.
This is the second post about building RouteFlow. In the first one, I wrote about the architecture, gym-level data isolation, the setting workflow and why a completed Setter Task isn't automatically a Route.
This time I stopped building long enough to actually use the thing.
Turns out that was useful.


Top comments (0)