DEV Community

Cover image for Day 88 of #100DaysOfCode — DevCollab: Data Models and Database Schema
M Saad Ahmad
M Saad Ahmad

Posted on

Day 88 of #100DaysOfCode — DevCollab: Data Models and Database Schema

Yesterday, the authentication foundation was laid; custom user model, JWT endpoints, everything tested in Postman. Today was about completing the entire database layer before writing a single API view. Models, relationships, constraints, migrations, admin registration, and seeded test data. By the end of today, the database will be fully shaped and ready for the API to be built on top of it tomorrow.


The Reason Behind Finishing Models First

It's tempting to build features incrementally: write a model, write its serializer, write its views, wire up its URLs, then move to the next model. That approach feels productive because you get working endpoints faster. But it has a hidden cost: every time you go back and change a model to support a new feature, you run another migration, potentially break existing serializers, and create technical debt in the API layer.

Finishing all models in one dedicated day means the database schema is stable before any API code is written. Serializers and views built on a finalized schema don't need to change because of a model tweak. The investment of one clean model day pays off across every API day that follows.


The Profile Model

Yesterday's Profile model was intentionally minimal, just enough fields to support the auth flow. Today, I reviewed it with fresh eyes now that the full picture is clear. The skills field stores a comma-separated string rather than a separate Skills table. This is a deliberate simplification. A join table for skills would be more normalized, but it adds a migration, a serializer, and API complexity for something that doesn't need it at this scale. The get_skills_list() helper method handles the string-to-list conversion wherever it's needed.


The Project Model

The Project model is the heart of DevCollab. A project belongs to one owner, has all the descriptive fields a developer needs to explain what they're building, and two control fields: status and is_open, that give the owner control over how the project appears to the outside world.

status has three choices: active, completed, and on_hold. This lets owners mark a project as finished without deleting it, preserving the collaboration history.

is_open is a simple boolean; true means the project is accepting collaboration requests, false means it isn't. When is_open is false, the project still appears in search results, but the request button on the frontend is replaced with a "Not accepting requests" label. Owners can toggle this without changing the project's status.

tech_stack and roles_needed are both comma-separated text fields with corresponding helper methods that return them as lists. This pattern is consistent with how skills are stored on Profile: simple, no extra tables, easy to work with in serializers and templates.

The model's Meta class sets default ordering to newest first and defines a __str__ that shows both the project title and the owner's username, useful in the admin panel when reviewing data.


The CollaborationRequest Model

CollaborationRequest is the most relationship-heavy model in the project. It links a requester (a User) to a Project, carries the message the requester wrote, and tracks the status of the request.

The status field has three choices: pending, reviewed, and accepted. The status field has three: pending, accepted, and rejected. Default is pending when a request is first created.

The most important constraint is unique_together on the project and requester fields. This is enforced at the database level, not just in form validation or view logic, which means it's impossible to create two requests from the same user for the same project, regardless of how the API is called. This constraint prevents a whole class of bugs before they can happen.

The on_delete behavior on both ForeignKeys deserves deliberate thought. If a User is deleted, their sent requests should be deleted too; CASCADE makes sense. If a Project is deleted, all its collaboration requests should go with it, CASCADE again. Both ForeignKeys use CASCADE.

related_name values are set carefully so that accessing relationships from the other side is readable: project.requests.all() gives all requests for a project, user.sent_requests.all() gives all requests sent by a user.


Migrations

Four tables total after today's migrations: the custom User table, Profile, Project, and CollaborationRequest. The migrations for accounts already ran yesterday. Today's migration covers the two new models in the projects app.

Running showmigrations after applying them confirms all migrations are applied cleanly with no pending changes. This is a habit worth forming: checking migration state before calling a day done means you never start the next day debugging a missing column.


Admin Registration

The admin panel is the most underrated tool in Django. For a project in active development, it's invaluable; it lets you inspect data, create test records, and verify relationships without writing any API clients or fixtures.

Both apps get thorough admin registration today.

For the accounts app, User registration extends Django's built-in UserAdmin rather than the plain ModelAdmin. This matters because the custom User model changed the login field to email; using plain ModelAdmin breaks the admin's built-in user management forms. Extending UserAdmin keeps all the password management, permissions, and group management interfaces working correctly. Profile is registered separately with an inline display of the user's username.

For the projects app, Project gets list_display showing title, owner, status, is_open flag, and created date. list_filter on status and is_open makes it easy to find active open projects during testing. search_fields on title and description supports quick lookup.

CollaborationRequest gets list_display showing the requester, the project, the status, and the created date. list_filter on status is particularly useful here; during testing, you'll want to quickly find all pending requests to accept or reject them.


Seeding Test Data

Writing an API without test data is frustrating; every endpoint returns empty lists, and edge cases are invisible. Today I seeded the database through the admin panel with enough data to make tomorrow's API work feel real.

Two employer-style users with filled-in profiles. Three projects, two active and open, one on hold. Two collaboration requests on the first project, one pending and one accepted. One request on the second project is pending.

This gives the API endpoints something to return, gives the filtering and search endpoints something to filter, and gives the permission checks something to enforce. A request from user A on user B's project, so ownership checks can be verified.


What the Database Looks Like Now

Four tables, all related:

The User table drives everything. Every Profile, Project, and CollaborationRequest traces back to a User. Delete a User and the cascade removes their Profile, their Projects, and all requests they've sent or received on their projects.

The project sits in the middle, owned by a User, receiving CollaborationRequests from other Users.

CollaborationRequest is the bridge, linking two Users indirectly through a Project, with a status that both sides can read and the owner can change.


Where Things Stand After Day 88

The entire database schema is finalized. Every model is defined, every migration is applied, every relationship is set up correctly, and the admin panel shows real seeded data. There are no pending model changes, no uncertain field types, and no relationships still being figured out.

Thanks for reading. Feel free to share your thoughts!

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.