Django Expense Manager 1 - Creating the Project, Expenses App and Thinking About the User Model

New post, new project. This one will be a longer series where we will create an expense manager (together with user registration and authentication) with a Django backend and React frontend. We will start with the backend, create the models, write tests for it (using a factory), rewrite tests to use Pytest (my preference), create some very basic templates, delete those very basic templates, install the Django Rest Framework library, use that, write tests for that, use Postman to play around with our API, create a frontend with React, decide whether I like Vue or React better and which one to use, maybe change my mind, use fake data for the frontend to create our components, write tests for our components, connect the frontend to the backend, talk about deployment, deploy (and fail), talk about deployment again and then succeed in deploying it. This may or may not be the thought process in my head when I start a project. Also, I changed my mind. The React frontend will be create simultaneously. I will alternate between the two things. Why? So I make it more interesting for myself.

First thing first. Make sure you have Django installed. If not:

  1. Create a virtual environment (keep it with your project or outside, your preference)
  2. Install Django
  3. Install any other required modules.
  4. Create new project.
  5. Create new app.
  6. Create some models.

Ready? Let’s go. In your virtualenvs directory (my preference), let’s do:

python3 -m venv expensemanager
source expensemanager/bin/activate
pip install --upgrade pip
pip install django

Then, in whatever folder you have your projects it:

django-admin startproject expenses_backend
cd expenses_backend
python manage.py startapp expenses

OK we have the project and an app, now let’s add our app to INSTALLED_APPS in our project’s configuration file at expenses_backend/settings.py.

INSTALLED_APPS = [
    # personal apps
    'expenses.apps.ExpensesConfig',

    # django in-built apps
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Before we create any models, let’s decide on the database. Since I will put a version of this live, I will use PostgreSQL. Now, if you do the same and you want to follow with the development, there are a few options:

  1. Install PostgreSQL on your computer and configure it (if you know how to, perfect)
  2. Continue with the project in a Docker container with VSCode (really straightforward).

We will choose option 2. Please follow the instructions here. Once you’ve done that, open VSCode, open the folder of the project. Then click on the bottom-left icon and select Open folder in container and choose the Python3 & PostgreSQL option. Please note that Docker will then download quite a few things (might do a Docker tutorial series at some point and how easy it makes developing stuff without having to install everything on your computer and wondering which version you have and dealing with multiple versions). If it’s the first time, will be a few minutes. Once everything is loaded, the Docker image will contain a full PostgreSQL database you can use. By default, the information for it is as follows:

"server": "localhost",
"port": 5432,
"database": "postgres",
"username": "postgres",
"password": "postgres"

Consequently, let’s use that in our settings.py file. When we are preparing to deploy, we will change the way that is done. For now, since we are using a development environment, we will simply change the DATABASES dictionary in the settings.py to

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

PLEASE DO NOT PUT YOUR SECRETS INTO A REPOSITORY. THEY WILL BE FOUND.. I actually just did that in a private repository, and then quickly changed that. Keep in mind that people can see old commits so if you do accidentaly push, invalidate right away. You don’t want to be that guy. GitHub now works with some providers to automatically let you know if you’ve pushed some secrets into your repository (how? Magic probably). Also, note that in order for this to work we need a PostgreSQL adapter for Python to be able to connect to PostgreSQL. Open a terminal in the VSCode window (will be run in the container), and do the following:

pip install psycopg2

Actually, there is something we need to do again. What is it? We need to install Django. but Vlad, didn’t we do that already? Yes, we did in that virtual environment. What we will do now, however, is install it into our container and make it easier for us so if we have to install again, it’s one command for all of them.

pip install --upgrade pip
pip install django
pip freeze >requirements.txt

The last command creates a requirements.txt file that lists the Python packages we are using and their version. We will discuss about pinning versions at a later time if necessary. If you’ve worked on a few Django projects before you might be asking: why are we not running the initial makemigrations and migrate command? Well, I want to edit the User model. Why? I want to log in using emails by default. There’s a slightly older article, but still quite informative, about changing the Django user model. (lunch time walk break here) Also, I know I said this, but I will leave that as a separate post for a different small project. Instead, I will use django-allauth for this project. Let’s quickly install it:

pip install django-allauth

And make sure you remove and remake your requirements file. Now we have to change/add a few more things to our project settings file. So in expenses_backend/settings.py:

INSTALLED_APPS = [
    # personal apps
    'expenses.apps.ExpensesConfig',
    
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',


    # django in-built apps
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

AUTHENTICATION_BACKENDS = [

    # Needed to login by username in Django admin, regardless of `allauth`
    'django.contrib.auth.backends.ModelBackend',

    # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',

]


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.request',
            ],
        },
    },
]

I am not actually adding any social account provider login, so I am not adding any provider specific settings (more information can be found on the django-allauth documentation page). Now let’s add the urls for all the account management to our project’s urls.py file. So in my expenses_backend/urls.py file, I have:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('mysupersecretnotadmin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
]

Now we can do the migrations:

python manage.py migrate

Let’s also create a superuser.

python manage.py createsuperuser

A few more settings to add:

ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SITE_ID = 1

I am requiring emails and no usernames, requiring email verification and finally using the console as the email backend for now. (Please note we will be adding more to handle authentication through the API endpoints we will create and we will use the dj-rest-auth package ). Also please note that sometimes the please note are for myself so I don’t forget what I said :)

Oooookay. Now, let’s actually work with our Transactions. So let’s head to the expenses/models.py file and think about what we want to have. For that, I will also have this open on the side. The Django documentation is quite comprehensive. So what fields do we want it to have? The front-end is created at the same time as the backend, so you can see of the fields that we want there as well. (I wonder how Hugo will handle these circular references between these two posts.).

In the other post I defined a JS object with a transaction as:

const transaction = {
  id: 1,
  amount: 25.00,
  name: "Rent",
  retailer: "Nile Web Services",
  category: "Housing and Utilities",
  date: "2021-03-24 13:42:15",
  currency: "GBP",
  type: "expense",
  recurring: false
}

Those are the fields I want to have on the Transaction model:

  1. ID (created automatically unless you want it overridden)
  2. Amount
  3. Name
  4. Retailer
  5. Category
  6. Date
  7. Currency
  8. Type
  9. Recurring

Looking at those, we see Retailer and Category. Hmmm sounds like we have to create those Models too. What do we want the Retailer to have?

  1. ID
  2. Name
  3. Location (maybe? in case you’re wondering where this retailer is.oooh actually would be quite interesting if we could get automatic location of a retailer and then somehow plot these on a map…Nosebook knows that about you probably, so maybe you should too? haha this will be added in the future)
  4. Online (whether it’s a store online or not)

What about the Category (like Groceries, Takeaway, Clothes, etc)?

  1. ID
  2. Name (obviously)
  3. Type (tangible, intangible -> this is just something I’m curious about my own buying habits. After all, don’t YOU want to know just how many Steam games you bought and never played?)

Ok those are the fields I want to have. But what types will they be? Let’s go through the list again. (Should we have done that all at once? Yes and no. Yes because it seems like we are repeating ourselves but no because I want the logic to come after the ideas. Otherwise I start thinking too much about how to implement something.) Oh, all of these should have a user associated with them of course. :) isn’t that why we did all the work earlier?

The Transaction model will have:

  1. ID (will be the primary key) - created automatically by Django, not our problem
  2. Amount - we want it to have two decimal places. Let’s use a Decimal Field. What options does it have? It has a maximum number of digits and the number of decimal places. Let’s make the first one 12 and the second one 2. That way we can record our daily expenses of billions but to two decimal places accuracy, yay
  3. Name - we want to write text. We will use a Char Field with a maximum length of 255. (in case any of you decides to use MySQL, I don’t want there to be any surprises so I’ll use that as the maximum length)
  4. Retailer - ok so this one we want to be able to refer to an existing retailer. How do we do that? We use a Foreign Key (linking one database table to another) to the Retailer model. We’ll discuss this in more detail when looking at the code.
  5. Category - same as above
  6. Date - DateTime Field
  7. Currency - ok this one I need to think of. We want to let the user decide, and they should be able to choose it. But then there are so many currencies out there. There must be a better way. Django Money comes to the rescue. We will use a MoneyField for the amount that will also include a currency
  8. Type (i.e. Inflow/outflow or Expense/Income). Will be a CharField that selects from these choices or maybe a Boolean Field (I will think about it)
  9. Recurring (yes, no). We will think about how to deal with subscriptions when we get to it. Boolean Field
  10. User - ForeignKey to the User model

The Retailer model will have:

  1. ID
  2. Name - CharField (255 characters max)
  3. Location - if implemented, will be done as a CharField
  4. Online (whether it’s a store online or not) - BooleanField
  5. User - ForeignKey to the User model

The Category model will have:

  1. ID
  2. Name - CharField (255 characters max)
  3. Type (tangible/intangible) -> Either CharField with choices or BooleanField
  4. User - ForeignKey to the User model

Ok it’s a pretty long post. Let’s leave the models code for the next one :)

Also, the git commit for this post is here