Django #3

You can see the application deployed on heroku here.
You can get a source code here.

In previous posts, I described user management in the application, i.e. a new user can register – create an account on the site, a registered user can log in, log out or change the password.

Then I’ll describe the main functionality of the app – car and repair records.

For this purpose, in the project urls.py file, I will attach the mapping of addresses related to cars application.

After the changes, the project urls.py file will look like this:

from django.contrib import admin
from django.urls import path, include
from users import views as users_views
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('cars.urls')),
    path('register-user/', users_views.RegisterUser.as_view(
        template_name='users/register-user.html'), name='register_user'),
    path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='users/logout.html'), name='logout'),
    path('change-password/', users_views.ChangePassword.as_view(
        template_name='users/change-password.html'), name='change_password'),
]

The urls.py file created in the cars application also takes into account mapping addresses to appropriate views. The urls.py file from the cars application looks like this:

from django.urls import path
from .views import (
    CarsListView,
    AddCarView,
    RepairsListView,
    UpdateCarView,
    DeleteCarView,
    AddRepairView
)
urlpatterns = [
    path('', CarsListView.as_view(), name='cars'),
    path('add-car/', AddCarView.as_view(), name='add_car'),
    path('car/', RepairsListView.as_view(), name='car_detail'),
    path('car/<int:pk>/update/', UpdateCarView.as_view(), name='update_car'),
    path('car/<int:pk>/delete/', DeleteCarView.as_view(), name='delete_car'),
    path('car/<int:pk>/new-repair/', AddRepairView.as_view(), name='add_repair'),
]

The main view defined in the CarListView class displays all the user’s cars.

Remember to add the application in the project’s settings.py file in the INSTALLED_APPS list, e.g. for cars it will be an element: ‘cars.apps.CarsConfig’.

I used the pre-installed user model in the users application. In the cars application, I created models – objects mapping database tables.

These models will define: vehicle – Car model and repair – Repair model. The models.py file looks like this:

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator


class Car(models.Model):
    make = models.CharField(max_length=10)
    model = models.CharField(max_length=10)
    vrn = models.CharField(max_length=10)
    year = models.IntegerField(default=timezone.now().year,
                               validators=[MinValueValidator(timezone.now().year - 100),
                                           MaxValueValidator(timezone.now().year)])
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.make} {self.model}'


class Repair(models.Model):
    date = models.DateField(default=timezone.now)
    description = models.TextField()
    car = models.ForeignKey(Car, on_delete=models.CASCADE)

    def __str__(self):
        lead = self.description[:10] + '...'
        return lead

The Car model contains make, model, vrn, year, and user attributes that describe the vehicle make, model, vehicle registration number, and year of manufacture, as well as a foreign key containing the user object that created the vehicle entry.

The year field has validators that make it impossible to enter a car with a production year 100 years older than the current date and newer than the current year.

The on_delete option of the user attribute has the default value CASCADE, ie deleting a user will also delete all his cars.

The Repair model includes the attributes: date – repair date, description – repair details and a foreign key car – which car the repair concerns.

The __str__() method prints the header that is the beginning of the repair description (leading 10 characters).

To be able to edit entries of both models from the administration panel, you need to add both models to the admin.py file of the cars application, i.e.

from django.contrib import admin
from .models import Car, Repair

admin.site.register(Car)
admin.site.register(Repair)

In the next post, I will describe the views that define individual functionalities, i.e. displaying cars belonging to the user, adding repairs, and searching for cars using the search box on the website.

Django #2

The login and logout views are predefined as LoginView and LogoutView classes, so you only need to put the appropriate entries in the urls.py file (as described in part 1) and create template files, i.e.

login.html template:

{% extends 'cars/base.html' %}
{% load crispy_forms_tags %}


{% block content %}
  <div class="row justify-content-center">
    <div class="col-4">
      <form method="POST" class="form-control-sm">
       {% csrf_token %}
       <fieldset class="form-group">
         <legend class="border-bottom text-primary">
            Log In
          </legend>
          {{ form|crispy }}
        </fieldset>
        <button class="btn btn-secondary" type="submit">Login</button>
        <p class="text-muted text-small mt-5 ml-2">Need an Account? <a href="{% url 'register_user' %}">Sign up</a></p>
        </form>
      </div>
    </div>
{% endblock %}
login.html template

logout.html template:

{% extends 'cars/base.html' %}

{% block content %}
 <div class="row justify-content-center mb-5">
    <div class="col-4">
	   <h4 class="border-bottom text-primary mt-2">You're logged out now</h4>	
  <div class="text-muted text-small ml-2 mt-5">Back to: <a href="{% url 'login' %}">login page</a></div>
  </div>
 </div>
{% endblock %}

It still remains to create a view responsible for changing the password. The view class will inherit from the CreateView class as well as the LoginRequiredMixin class. The LoginRequiredMixin class adds the functionality that only the logged in user can change the password.

ChangePassword () view class code:

class ChangePassword(LoginRequiredMixin, CreateView):
    form_class = PasswordChangeForm

    def get(self, request, *args, **kwargs):
        form = PasswordChangeForm(request.user)
        return render(request, 'users/change-password.html', {'form': form})

    def post(self, request, *args, **kwargs):
        form = PasswordChangeForm(request.user, request.POST)
        if form.is_valid():
            user = form.save()
            update_session_auth_hash(request, user)
            messages.success(request, 'Password changed!')
            return redirect('cars')
        return render(request, 'users/change-password.html', {'form': form})

In the class above, I override the get() and post() methods.

When the page is displayed using the get() method, an empty password change form is displayed for the user.

When the page is displayed using the post () method, the submitted data is validated and, if correct, the changed password is saved, a password change message is displayed and redirected to a page named cars.

It remains to add the template file, ie change-password.html.

{% extends 'cars/base.html' %}
{% load crispy_forms_tags %}


{% block content %}
  <div class="row justify-content-center">
   <div class="col-4">
	  <form method="POST" class="form-control-sm">
      {% csrf_token %}
      <fieldset class="form-group">
        <legend class="border-bottom text-primary">
            New Password Settings
        </legend>
        {{ form|crispy }}
      </fieldset>
      <button class="btn btn-secondary" type="submit">Save</button>
    </form>
   </div>
  </div>
{% endblock %}
change-password.html template

to be continued….

Django #1

Below is a listing of the required packages for the project:

asgiref==3.2.5
Django==3.0.4
django-crispy-forms==1.9.0
pytz==2019.3
sqlparse==0.3.1

I recommend creating a separate virtual environment for the project (how to create a virtual environment I described here).

All packages except django-crispy-forms are installed when the django package is installed.

In the directory containing the venv subdirectory, create a project with the command: django-admin startproject project_name

Startproject command creates the project’s main directory and a manage.py file for managing the project.

You can then create a project administrator, i.e. a user with maximum administrative privileges, so that you can use the web admin panel. However, first you need to run a migrate: python manage.py migrate thanks to which the appropriate tables are created in the database, e.g. admin, auth etc.

After migration, you can create an administrative account with the command: python manage.py createsuperuser

The project directory contains the following files and directories:

├── project_name
│ ├── asgi.py
│ ├── init.py
│ ├── pycache
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
└── manage.py

By default, the data is placed in the sqlite3 database (db.sqlite3 file), which is a good solution when creating a project, and finally you can use a different database (Django includes support for PostgreSQL, MySQL and many others). Changing the default database only requires modifying the data from the DATABASES section of the project’s settings.py file.

In order to separate the individual components of the project, I made two components, i.e. users – responsible for user management and cars for vehicle management. In Django, these are called apps. To create them, type: python manage.py startapp users and python manage.py startapp cars

I will start with the users application, in which new users will be able to create accounts, log in, log out, and change their access password.

To define URL mapping to specific view classes, I modify the project urls.py file, i.e.

from django.contrib import admin
from django.urls import path
from users import views as users_views
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('register-user/', users_views.RegisterUser.as_view(
        template_name='users/register-user.html'), name='register_user'),
    path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='users/logout.html'), name='logout'),
    path('change-password/', users_views.ChangePassword.as_view(
        template_name='users/change-password.html'), name='change_password'),
]

For example, after entering the address ending with: register-user /, the class RegisterUser () is called from the views.py file and the assignment is given a name that we can use in templates.

For a class to be used as a view, its as_view () method must be called. As an argument I have given the template_name argument, in which I give the name of the template that displays the given page. You might as well pass this argument as a class attribute in a view, but for consistency with the rest of the views, I have included it here.

The code of the RegisterUser () class from the views.py file of the users application looks as follows (at the beginning I put the required imports):

from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm, PasswordChangeForm
from django.contrib.auth import login, authenticate
from django.views.generic import CreateView
from django.contrib import messages

class RegisterUser(CreateView):
    form_class = UserCreationForm

    def get(self, request, *args, **kwargs):
        return render(request, 'users/register-user.html', {'form': UserCreationForm()})

    def post(self, request, *args, **kwargs):
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password1')
            user = authenticate(username=username, password=password)
            login(request, user)
            messages.success(request, f'Welcome, {username}!')
            return redirect('cars')
        return render(request, 'users/register-user.html', {'form': form})

The RegisterUser () class inherits from the CreateView class, and the form is defined by the UserCreationForm class.

When the page is displayed using the GET method, an empty form appears, while if some data is already provided in the form, it is passed using the POST method.

To handle GET and POST, I override the get() and post() methods of RegisterUser(), respectively. In the post() method, the form is validated and if the form is correct, then the data from the form is saved, ie the user is created using the save() method. By default, this is a user with no administrative privileges. He is logged in using the data provided during registration. A new message with the tag Success will be displayed after redirecting to an address named cars. The message is displayed only once.

It remains to create the register-user.html template for RegisterUser(). For this purpose, I create a template directory in the users application according to the convention – in the templates directory, create another directory with the name consistent with the name of the application, i.e.

project_name/users/templates/users/register-user.html

The code of register-user.html is part of a larger whole, or more precisely, the base.html file from the cars application, which has not been created yet.

Applications can use their templates, so the look defined in cars in base.html is preserved in register-user.html

The template also enables the use of crispy_forms tags to make the form look better.

Each form must contain csrf_token, thanks to which the website is automatically immune to some forms of attacks (details: here).

Code of register-user.html file:

{% extends 'cars/base.html' %}
{% load crispy_forms_tags %}

{% block content %}
  <div class="row justify-content-center">
    <div class="col-4">
      <form method="POST" class="form-control-sm">
        {% csrf_token %}
        <fieldset class="form-group">
          <legend class="border-bottom text-primary">
            Registration
          </legend>
            {{ form|crispy }}
        </fieldset>
          <button class="btn btn-secondary" type="submit">Register</button>
      </form>
    </div>
  </div>
{% endblock %}

Django – web framework

In this series, I will describe the basics of the Django framework using the example of an application for recording repairs related to your car.

The application is similar to the one described in this posts, but the use of a web framework forced additional functionalities, e.g. user accounts support.

You can create your own account and log in to the application here.

You can download the project code here.

Tkinter GUI for CRUD app #3

Below I have described the code of the NewCarWindow class describing a new child window, created when the add_car_button button is clicked:

import tkinter as tk
from sql_helper import Helper


class NewCarWindow():
    def __init__(self, root, parent):
        self.top_level = tk.Toplevel(root)
        self.helper = Helper()
        self.parent = parent
        x = self.top_level.winfo_screenwidth()
        y = self.top_level.winfo_screenheight()
        geometry = '+{}+{}'.format(int((x / 2) - 100),
                                   int((y / 2) - 100))
        size = self.top_level.geometry(geometry)

First, I import the required classes, then the init () method sets the child window’s position relative to the main window.

        self.top_level.title('Add car')
        self.top_level.grab_set()
        label_make = tk.Label(self.top_level, text='Make: ',
                              font=12, padx=20, pady=10).grid(row=0, column=0)
        label_model = tk.Label(self.top_level, text='Model: ',
                               font=12, padx=20, pady=10).grid(row=1, column=0)
        label_year = tk.Label(self.top_level, text='Year: ',
                              font=12, padx=20, pady=10).grid(row=2, column=0)
        label_vrn = tk.Label(self.top_level, text='VRN: ',
                             font=12, padx=20, pady=10).grid(row=3, column=0)
        label_vin = tk.Label(self.top_level, text='VIN: ',
                             font=12, padx=20, pady=10).grid(row=4, column=0)

Then the title of the child window is set. The grab_set () method makes the main window inactive – e.g. you cannot press any button, etc.

The next step is to set the text labels describing the input fields, which I define below, together with variables of the StringVar type that store the values ​​entered into these fields:

    self.make_sv = tk.StringVar()
    self.model_sv = tk.StringVar()
    self.year_sv = tk.StringVar()
    self.vrn_sv = tk.StringVar()
    self.vin_sv = tk.StringVar()
    self.info_sv = tk.StringVar()

    entry_make = tk.Entry(
            self.top_level, text=self.make_sv)
    entry_make.focus_set()
    entry_make.grid(row=0, column=1)
    entry_model = tk.Entry(
            self.top_level, text=self.model_sv).grid(row=1, column=1)
    entry_year = tk.Entry(
            self.top_level, text=self.year_sv).grid(row=2, column=1)
    entry_vrn = tk.Entry(
            self.top_level, text=self.vrn_sv).grid(row=3, column=1)
    entry_vin = tk.Entry(
            self.top_level, text=self.vin_sv).grid(row=4, column=1)

    info_label = tk.Label(self.top_level, textvariable=self.info_sv,
                              font=12, padx=10, pady=10, fg='red').grid(row=5, column=0, columnspan=2)

The info_label label is used to display a message in the case of not filling all the required fields to create a new car.

        save_button = tk.Button(self.top_level, text='Save',
                                command=self.save_new_car)
        save_button.bind('<Return>', self.save_new_car)
        save_button.grid(row=6, column=1, sticky='W', padx=10)
        cancel_button = tk.Button(self.top_level, text='Cancel',
                                  command=self.top_level.destroy)
        cancel_button.grid(row=6, column=1, sticky='W', padx=70)
        cancel_button.bind('<Return>', self.top_level.destroy)

At the bottom of the window, the Save and Cancel buttons are displayed to save the data about the new car in the database or close the ‘Add car’ window.

def save_new_car(self, event=None):
    if self.make_sv.get() and self.model_sv.get() and self.year_sv.get() and self.vrn_sv.get() and self.vin_sv.get():
        self.helper.add_car(self.make_sv.get(),
                            self.model_sv.get(),
                            self.year_sv.get(),
                            self.vrn_sv.get(),
                            self.vin_sv.get())
        self.top_level.destroy()
        self.parent.show_cars()
    else:
        self.info_sv.set('Please fill in all entry fields')

The code for the save_new_car() method is activated if the Save button is pressed. If all the values ​​needed to create a new car have been filled, the add_car () method of the Helper class is run, then the child window is closed and the entries about cars in the main window are updated using the show_cars() method of the CarManager class.

The ‘Add car’ child window. The message is displayed when all input fields are not filled in. Focus is set on the first element.

***

The code of the RepairsWindow class, activated after pressing the main button repair_button:

import tkinter as tk
from tkinter import ttk
from sql_helper import Helper
from date_picker import DatePicker


class RepairsWindow():
    def __init__(self, root, car):
        self.top_level = tk.Toplevel(root)
        self.root = root
        self.top_level.title('Repairs')
        self.top_level.grab_set()
        self.car = car
        self.helper = Helper()

        x = self.top_level.winfo_screenwidth()
        y = self.top_level.winfo_screenheight()
        geometry = '+{}+{}'.format(int((x / 2) - 150),
                                   int((y / 2) - 150))
        size = self.top_level.geometry(geometry)

First, I import all the necessary classes. In the init () method, a new window is created – an instance of the TopLevel class, and the window title is set to ‘Repairs’. Then, using the grab_set () method, the possibility of starting widgets from the main window is blocked. The self.car and self.helper variables are set and the child window is positioned relative to the main window.

        toolbox_frame = tk.Frame(self.top_level)
        toolbox_frame.grid(column=0, row=0, sticky='W')
        self.add_repair_img = tk.PhotoImage(file='Resources/add_repair.gif')
        add_repair_button = tk.Button(
            toolbox_frame, image=self.add_repair_img, command=self.add_repair)
        add_repair_button.grid(column=0, row=0, sticky='W')
        add_repair_button.bind('<Return>', self.add_repair)
        add_repair_button.bind('<KP_Enter>', self.add_repair)
        if car.sold:
            add_repair_button.config(state='disabled')

A toolbox_frame is created in the child window, in which the add_repair_button button is placed. The add_repair () method is bound to the created button – activated with the mouse or keyboard after pressing Enter on the numeric or main keyboard. Then it is checked if the car is not marked as ‘sold’, then the button for adding new repairs is inactive.

        col_headers = ('No', 'Date', 'Description')
        self.repairs_tv = ttk.Treeview(self.top_level, columns=col_headers,
                                       show='headings', selectmode='none')
        self.repairs_tv.tag_configure('c1', background='ivory2')
        self.repairs_tv.tag_configure('c2', background='ivory3')
        for i, col in enumerate(col_headers):
            self.repairs_tv.heading(col, text=col)
            self.repairs_tv.column(col, anchor='center')
            if i == 0:
                self.repairs_tv.column(col, width=50, stretch='NO')
        self.repairs_tv.grid(column=0, row=2,  sticky='NSWE')

        scrollbar = tk.Scrollbar(self.top_level, command=self.repairs_tv.yview)
        scrollbar.grid(column=1, row=2, sticky='NS')
        
        self.show_repairs()

The next code snippet describes creating the Treeview widget that will display repair data. To improve readability, I created two tags that set the background colors of the entered widget elements. The init () method ends by calling the show_repairs () method, which adds repairs to the Treeview widget.

    def show_repairs(self):
        repairs = self.helper.show_repairs(self.car)
        self.repairs_tv.delete(*self.repairs_tv.get_children())
        for i, repair in enumerate(repairs, start=1):
            repair = (i, repair[0], repair[2])
            if i % 2:
                self.repairs_tv.insert('', 'end', values=repair, tag='c1')
            else:
                self.repairs_tv.insert('', 'end', values=repair, tag='c2')

The show_repairs () method calls the appropriate Helper method to retrieve the repair data from the database. Then the repairs_tv items are deleted and the updated data is set. To improve readability, the following lines are alternately colored, i.e.

Repairs window.

Code of the add_repair method, which is activated when the add_repair_button button is pressed with the mouse or the keyboard:

    def add_repair(self, event=None):
        self.add_repair_frame = tk.Frame(self.top_level)
        self.add_repair_frame.grid(
            column=0, row=1, pady=20, sticky='WE')

        date_label = tk.Label(self.add_repair_frame,
                              text='Date:').grid(column=0, row=2)
        self.date_sv = tk.StringVar()
        self.date_entry = tk.Entry(self.add_repair_frame,
                                   text=self.date_sv)
        self.date_entry.focus_set()
        self.date_entry.grid(column=1, row=2, sticky='W')

        self.cal_img = tk.PhotoImage(file='Resources/calendar.gif')
        show_cal_btn = tk.Button(self.add_repair_frame, image=self.cal_img,
                                 command=self.show_cal, relief='flat').grid(column=1, row=2, sticky='W', padx=170)

        description_label = tk.Label(self.add_repair_frame,
                                     text='Description:').grid(column=0, row=3)
        self.description_sv = tk.StringVar()
        self.description_entry = tk.Entry(self.add_repair_frame,
                                          text=self.description_sv)
        self.description_entry.grid(column=1, row=3, ipadx=200)
        save_button = tk.Button(self.add_repair_frame, text='Save',
                                command=self.save_repair)
        save_button.grid(column=1, row=4, pady=10, sticky='E')
        save_button.bind('<Return>', self.save_repair)
        save_button.bind('<KP_Enter>', self.save_repair)
        cancel_button = tk.Button(self.add_repair_frame, text='Cancel',
                                  command=self.cancel_repair)
        cancel_button.grid(column=1, row=4, sticky='E', padx=60)
        cancel_button.bind('<Return>', self.cancel_repair)
        cancel_button.bind('<KP_Enter>', self.cancel_repair)
Repairs window after pressing the add_repair_button button.

Code of the cancel_repair () method that hides the repair_frame:

    def cancel_repair(self, event=None):
        self.add_repair_frame.grid_remove()

Save_repair () method code:

def save_repair(self, event=None):
        if self.date_sv.get() and self.description_sv.get():
            self.helper.add_repair(self.car, self.date_sv.get(),
                                   self.description_sv.get())
            self.show_repairs()
            self.add_repair_frame.grid_remove()

In the save_repair () method, basic form validation is performed, ie it is checked if all required fields to create a repair entry are filled in.

If so, the repair note is saved in the database (add_repair method of the Helper class), and then the entries about the repairs are updated and the repair_frame is hidden.

The remaining show_cal () method, which is activated after pressing the show_cal_btn button:

    def show_cal(self, event=None):
        date_picker = DatePicker(self.top_level, self.date_entry, '%d-%m-%Y')
        self.description_entry.focus_set()

The show_cal () method creates an instance of the DatePicker class that I created to get the date and insert it into the given Entry field. As an argument, this class allows you to insert the selected date formatting, so that, for example, in the application settings the user could choose how the date should be displayed.

You can find the code of the DatePicker class in this post.

Selecting a date using my DatePicker class

***

P.S. If the main window is to run as maximized, but with the resize option, then in the init () method of the CarManager class, add:

root.attributes('-zoomed', True)

P.P.S. If entries about cars are to be displayed with row formatting, the show_cars () method can be modified by adding tags that define row colors, i.e.

    def show_cars(self):
        all_cars = self.helper.show_all_cars()
        self.car_tview.delete(*self.car_tview.get_children())
        for i, car in enumerate(all_cars, start=1):
            car = list(car)
            car.insert(0, i)
            if i % 2:
                self.car_tview.insert('', 'end', values=car, tag='c2')
            else:
                self.car_tview.insert('', 'end', values=car, tag='c1')
            self.car_tview.tag_configure('c1', background='ivory3')
            self.car_tview.tag_configure('c2', background='ivory2')
Treeview widget with formatting tags

Tkinter GUI for CRUD app #2

In an earlier post, I created the main application window. Then I will complete the methods that handle events initiated by interface elements.

First, I will complete the show_cars () method, which is activated after loading widgets into the main window and each time when adding or removing a car from the database.

    def show_cars(self):
        all_cars = self.helper.show_all_cars()
        self.car_tview.delete(*self.car_tview.get_children())
        for i, car in enumerate(all_cars, start=1):
            car = list(car)
            car.insert(0, i)
            self.car_tview.insert('', 'end', values=car)


The show_all_cars () method of the Helper class runs first. It returns all cars in the database as a list of tuples. Then all items in the TreeView widget are removed to reload the car list after the changes.

To simplify handling of the del_car (), sell_car (), show_repairs () methods, I will extract a private method: __get_car_from_selection (), which returns an instance of the Car class based on the selected line, i.e.

    def __get_car_from_selection(self):
        selected_item = self.car_tview.focus()
        if selected_item == '':
            messagebox.showinfo('Information', 'No item selected')
        else:
            selection_dict = self.car_tview.item(selected_item)
            selection = selection_dict.get('values')
            i, *args = selection
            car = Car.from_list(args)
            return car

First, the above method checks if any line containing data about the car is selected. If so, the TreeView element’s dictionary is returned.

Data about the selected car is present as a value for the ‘values’ key. The first element that is the row ordinal is removed from the list.

The modified list is an argument of the from_list () method of the Car class.

Del_car () method code:

    def del_car(self):
        car = self.__get_car_from_selection()
        if car and messagebox.askyesno('Delete', 'Delete selected car?'):
            self.helper.del_car(car)
            self.show_cars()

The above method checks whether the variable car is not of the None type and whether the user has also confirmed the deletion of data about the car in the dialog box. If both of these conditions are met, the Helper class method is run, deleting the car from the database.

Then the car entries in the database are updated using the show_cars () method of the CarManager class.

    def sell_car(self):
        car = self.__get_car_from_selection()
        if car and car.sold:
            messagebox.showinfo('Information', 'Already marked as sold')
        elif car and messagebox.askyesno('Sell', 'Mark car as sold?'):
            self.helper.set_sold(car)
            self.show_cars()

In the above method, it is checked whether the car has already been marked as sold. If not, a confirmation dialog is displayed.

If the user accepts it, the set_sold () method of the Helper class is called and then the show_cars () method of the CarManager object.

Show_repairs () method code:

    def show_repairs(self):
        car = self.__get_car_from_selection()
        if car:
            repairs = RepairsWindow(self.root, car)

In the show_repairs () method, when the variable car is not of type None, a new child window is created that is described by the RepairsWindow class.

The last method described in this post is the add_car () method, which is activated after pressing the add_car_button button.

def add_car(self):
        add_car = NewCarWindow(self.root, self)

The add_car () method creates a new child window that is described by the NewCarWindow class.

I will describe the codes of both classes in the next post.

The methods related to the search_entry input field still need to be added, i.e. on_entry_in (), on_entry_out () and on_entry_return (), i.e.

    def on_entry_in(self, event):
        self.search_entry.config(fg='black')
        self.search_variable.set('')

    def on_entry_out(self, event):
        self.search_entry.config(fg='grey')
        self.search_variable.set('Search car by VRN')

Gaining focus on the search_entry search box clears the search box and changes the font color. The loss of focus causes the display of gray text ‘Search car by VRN’ in the search_entry field.

def on_entry_return(self, event):
    vrn = self.search_variable.get()
    if vrn == '':
        self.show_cars()
    else:
        car = self.helper.search_by_vrn(vrn)
        self.car_tview.delete(*self.car_tview.get_children())
        if car:
            car = list(car)
            car.insert(0, 1)
            self.car_tview.insert('', 'end', values=car)

The on_entry_return method code above checks if the search field is empty after pressing Enter. If so, data on all available cars from the database are displayed.

If a phrase is entered in the search box, the search_by_vrn () method of the Helper class is run, followed by the updated car_tview widget to display the search results.

Tkinter GUI for CRUD app #1

In the >>previous post<<, I described a simple CRUD program that modifies a database (SQLite in this case).

For the program I created earlier, I will create GUI in the tkinter library. The interface will be developed gradually, making it easier to explain the methods used.

Program code can be found here.

I will slightly modify the files created in the previous post, i.e. for the class Car() I’ll add a class method that creates an instance when from a list. In addition, in the Helper () class, the del_car () method, apart from deleting the car from the database, also deletes all data about the entered repairs related to this car. The code of the new fragments looks like this:

class Car():
    @classmethod
    def from_list(cls, list):
        make, model, year, vrn, vin, sold = list
        return cls(make, model, year, vrn, vin, sold)

class Helper():
    def del_car(self, car):
        with self.conn:
            self.c.execute("SELECT ROWID FROM cars WHERE vin=:vin",
                           {'vin': car.vin})
            car_id = self.c.fetchone()
            self.c.execute("DELETE FROM repairs WHERE car=?",
                           (car_id))
            self.c.execute("DELETE FROM cars WHERE vin=:vin", {'vin': car.vin})

Here is my CarManager() class describing the graphical interface.

import tkinter as tk
from tkinter import messagebox, ttk
from sql_helper import Helper
from car import Car

class CarManager(tk.Frame):
    def __init__(self, root):
        super().__init__(root)
        self.root = root
        self.helper = Helper()
        root.title('Car manager')
        root.iconphoto(True, tk.PhotoImage(file='Resources/car_logo.png'))
        root.bind('<Control-x>', self.close_app)
        root.columnconfigure(1, weight=1)
        root.rowconfigure(2, weight=1)
        self.create_widgets()
        self.show_cars()


if __name__ == '__main__':
    root = tk.Tk()
    cm = CarManager(root)
    cm.mainloop()

When creating an instance of the CarManager class, I create instance of the Helper class, set the name of the created window to ‘Car Manager’ and add the program icon from the Resources directory. In addition, I set a hotkey to end the program (Ctrl + x). Then I set the way the widgets are to be scaled when the window is resized.

The last two lines of the init () method run the methods responsible for creating all widgets in the main window and for displaying data from the database about the saved cars.

Here is my close_app () method:

    def close_app(self, event):
        result = tk.messagebox.askyesno('Exit', 'Close application?')
        if result:
            self.root.destroy()

The result is True when selected the close app button, the main application window is closed.

Code of my create_widgets () method responsible for displaying interface elements:

def create_widgets(self):
        # create menu
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label='Exit', command=exit, accelerator="Ctrl+x")
        menubar.add_cascade(label="File", menu=file_menu)

        # create toolbar
        tbar_frame = tk.Frame(root, height=10)
        tbar_frame.grid(row=0, column=0)

        self.add_car_img = tk.PhotoImage(file='Resources/add_car.gif')
        self.remove_car_img = tk.PhotoImage(file='Resources/remove_car.gif')
        self.repairs_img = tk.PhotoImage(file='Resources/repairs.gif')
        self.sold_img = tk.PhotoImage(file='Resources/sold.gif')

        add_car_button = tk.Button(tbar_frame, image=self.add_car_img,
                                   command=self.add_car).grid(row=0, column=0, sticky='W')
        del_car_button = tk.Button(tbar_frame, image=self.remove_car_img,
                                   command=self.del_car).grid(row=0, column=0, sticky='W', padx=30)
        repairs_button = tk.Button(tbar_frame, image=self.repairs_img,
                                   command=self.show_repairs).grid(row=0, column=0, sticky='W', padx=60)
        sold_button = tk.Button(tbar_frame, image=self.sold_img,
                                command=self.sell_car).grid(row=0, column=0, sticky='W', padx=90)

        # create search entry
        self.search_variable = tk.StringVar()
        self.search_entry = tk.Entry(
            root, textvariable=self.search_variable)
        self.search_entry.config(fg='grey')
        self.search_variable.set("Search car by VRN")
        self.search_entry.bind('<FocusIn>', self.on_entry_in)
        self.search_entry.bind('<FocusOut>', self.on_entry_out)
        self.search_entry.bind('<Return>', self.on_entry_return)
        self.search_entry.bind('<KP_Enter>', self.on_entry_return)
        self.search_entry.grid(row=0, column=1, sticky='E', ipadx=20)

        # create TreeView with Scrollbar
        col_headers = ('No', 'Make', 'Model', 'Year', 'VRN', 'VIN', 'Sold')
        self.car_tview = ttk.Treeview(self.root, columns=col_headers,
                                      show='headings', selectmode='browse')
        self.car_tview.columnconfigure(0, weight=1)
        self.car_tview.rowconfigure(0, weight=1)
        # set column headers
        for i, col in enumerate(col_headers):
            self.car_tview.heading(col, text=col)
            self.car_tview.column(col, anchor='center')
            if i == 0:
                self.car_tview.column(col, width=50, stretch='NO')
        self.car_tview.grid(row=2, column=0, columnspan=2, sticky='NSWE')

        scrollbar = tk.Scrollbar(self.root, command=self.car_tview.yview)
        scrollbar.grid(column=3, row=2, sticky='NS')
      

Successively, the program menu is created, then a toolbar with buttons for adding a car to the database, removing a car, adding data about car repairs and marking the sale of the car.

Then, a car search by the registration number field is created to which methods are attached to react to the events of obtaining / losing focus and confirming data by pressing the ENTER key on: numeric keypad (<KP_Enter>) or main keypad (<Return>).

The next few lines define a TreeView widget containing cars data and a scroll bar, if the data does not fit on the page.

After the changes, the main program window should look like this:

The initial window of the program. For now, no data …

Python and SQLite

Python includes the sqlite3 module in the standard library. To present the capabilities of the sqlite3 library, I’ll make a simple CRUD application for vehicle service and repair registration.

First, I’ll create two support files: car.py and cars_sql_scheme.py. The first one will define the Car class – describing the auto, and the second one will define the database schema (or in fact, queries creating two tables in the database).

car.py file listing

class Car():
    """Represents a sample car.

    Arguments:
    make - car make e.g. Honda
    model - car model e.g. Civic
    year - year of production
    vrn - vehicle registration number
    vin - VIN number
    sold - if car is still our property 

    """

    def __init__(self, make, model, year, vrn, vin, sold=False):
        self.make = make
        self.model = model
        self.year = year
        self.vrn = vrn
        self.vin = vin
        self.sold = sold

cars_sql_scheme.py file listing:

create_table_cars = """CREATE TABLE IF NOT EXISTS cars(
	make TEXT,
	model TEXT,
	year TEXT,
	vrn TEXT,
	vin TEXT,
	sold INTEGER
	)"""

create_table_repairs = """CREATE TABLE IF NOT EXISTS repairs(
	date TEXT,
	car INTEGER,
	description TEXT,
	FOREIGN KEY(car) REFERENCES cars(ROWID)
	)"""

In the third file, sql_helper.py, I define the Helper class, which will contain the methods:

  • add_car() – adding the car to the database
  • del_car() – removes the car from the database
  • search_by_vrn() – searches for a car by the registration number
  • show_all_cars() – returns all cars in the base
  • set_sold() – updates car data when the car has been sold
  • add_repair() – adds an annotation about the repair of the car
  • show_repairs() – displays all car repairs

The script imports the sqlite3 module, the Car class from car.py and the variables create_table_cars and create_table_repairs from cars_sql_scheme.py, i.e.

import sqlite3
from car import Car
from cars_sql_scheme import create_table_cars, create_table_repairs

class Helper():
    def __init__(self):
        self.conn = sqlite3.connect('cars.db')
        self.c = self.conn.cursor()
        self.c.execute(create_table_cars)
        self.c.execute(create_table_repairs)

The init () method creates a connection to the database named cars.db, creates a query cursor, and creates the cars and repairs tables (if not already created).

The add_car () method takes the vehicle make, model, year of construction, registration number, and VIN as arguments. The function returns a new instance of the Car class. Adding a new car to the database is carried out using the context manager, so the transaction is automatically committed (no need to commit () after each database change), i.e.

def add_car(self, make, model, year, vrn, vin):
    	"""Adds new car to database.

    	Arguments:
    	make - car make e.g. Honda
    	model - car model e.g. Civic
    	year - year of production
    	vrn - vehicle registration number
    	vin - VIN number

    	Returns:
    	new Car instance
    	"""
        with self.conn:
            self.c.execute("INSERT INTO cars VALUES (:make, :model, :year, :vrn, :vin, :sold)", {
                'make': make, 'model': model, 'year': year, 'vrn': vrn, 'vin': vin, 'sold': False})
            return Car(make, model, year, vrn, vin)

Similarly, the del_car () method looks like this:

def del_car(self, car):
    	"""Deletes car from database.

    	Arguments:
    	car  - car instance

    	Returns:
    	None
    	"""
        with self.conn:
            self.c.execute("DELETE FROM cars WHERE vin=:vin", {'vin': car.vin})

The search function does not require to commit transaction, so I do not use the context manager, i.e.

def search_by_vrn(self, vrn):
        """Search car by vehicle registration number.

        Arguments:
        vrn  - vehicle registration number

        Returns:
        search result tuple
        """
        self.c.execute("SELECT * FROM CARS WHERE vrn=:vrn", {'vrn': vrn})
        return self.c.fetchone()

    def show_all_cars(self):
    	"""Search availale cars.

        Returns:
        search result  - list of tuples
        """
        self.c.execute("SELECT * FROM CARS")
        return self.c.fetchall()

The set_sold () function sets the sold column in the database to True, represented by a value of 1.

def set_sold(self, car):
    	"""Mark car as sold.

        Arguments:
        car  - Car instance

        Returns:
        None
        """
        car.sold = True
        with self.conn:
            self.c.execute("UPDATE cars SET sold=True WHERE 
              vin=:vin",'vin': car.vin})

The last two methods allow you to add a note about the car’s repair and display the repair list. ie.

def add_repair(self, car, date, description):
    	"""Adds repair note.

        Arguments:
        car  - Car instance
        date  - repair date
        description  - repair description

        Returns:
        None
        """
        self.c.execute("SELECT ROWID FROM cars WHERE vin=:vin",
                       {'vin': car.vin})
        car_id = self.c.fetchone()[0]
        with self.conn:
            self.c.execute("INSERT INTO repairs VALUES (:date, :car, :description)", {
                'date': date, 'car': car_id, 'description': description})
def show_repairs(self, car):
        """Shows car repairs notes.

        Arguments:
        car  - Car instance

        Returns:
        search result  - list of tuples
        """
        self.c.execute("SELECT ROWID FROM cars WHERE vin=:vin",
                       {'vin': car.vin})
        car_id = self.c.fetchone()
        self.c.execute("SELECT * FROM repairs WHERE car=?",
                       (car_id))
        return self.c.fetchall()

Venv – how to use

The venv library present as a built-in module starting with python 3.3 allows you to create virtual environments.

The virtual environment allows the use of specific versions of libraries, regardless of those that are installed in the default system locations. This prevents errors when the new, updated version of the library does not work well with the rest of the project (the project requires an earlier version).

To create a virtual environment for the current project, type:

python -m venv project_name/env_name

A env_name directory is created in the project directory project_name that contains a virtual environment.

The virtual environment directory contains several subdirectories, including / bin directory containing a link to the python interpreter, as well as an activate file to start the environment. In addition, the env_name directory also has a /lib directory containing installed modules (site-packages).

To run the created environment named env_name, enter in the shell:

source project_name/env_name/bin/activate

You can close the environment with the deactivate command.

When we have a virtual environment running, the newly installed libraries are available only in the selected virtual environment. You can save the library required for the project in a text file with:

pip freeze > requirements.txt

If you have a requirements.txt file describing the project requirements for libraries and their versions, you can install them with:

pip install -r requirements.txt

Bash prompt, venv and ArchLabs

If you want to change the command prompt displayed by bash in such a way as to display information about the currently used virtual environment created by venv, you need to modify the PS1 variable in the .bashrc file.

In the ArchLabs distribution, the PS1 variable settings are also modified in the hidden .bash directory in the user’s directory and therefore instead of directly modifying the PS1 variable, the PROMPT_USERFMT variable should be modified, i.e.

PROMPT_USERFMT='\nvenv:(${VIRTUAL_ENV##*/})\n\u@\h:'

Then, the created variable should be exported in the .bashrc file, i.e.

export $PROMPT_USERFMT

To reload bash settings, you need to reload the .bashrc file, i.e.

source .bashrc