3
\$\begingroup\$

This is my first attempt at creating a simple Python tool that tracks the time of certain activities retrieved from a database. After stopping the timer, this tool writes the duration of the activity into the database.

Can you please provide me some tricks to make this code more Pythonic?

objects.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class Project:
    '''Class that holds the projects'''
    projects = []

    def __init__(self, name):
            self.name = name
            self.tasks = []
            type(self).projects.append(self)

    def __str__(self):
            return self.name

    @property
    def name(self):
            return self._name

    @name.setter
    def name(self, name):
            if not name:
                    raise ValueError(_('You need to specify a value!'))
            self._name = name

    def add_task(self, task):
            if (task not in self.tasks):
                    self.tasks.append(task)

    def get_tasks(self):
            return self.tasks

    @staticmethod
    def get_project_by_name(name):
            for project in Project.projects:
                    if name == project.name:
                            return project

class Task:
    '''A task for a project'''
    def __init__(self, id, name):
            self.id = id
            self.name = name

    def __str__(self):
            return '{0}:{1}'.format(self.id, self.name)

    def __eq__(self, other):
            return self.id == other.id and self.name == other.name

    @property
    def name(self):
            return self._name

    @name.setter
    def name(self, name):
            if not name:
                    raise ValueError(_('You need to specify a value!'))
            self._name = name


class Entry:
    '''A timesheet entry that contains the task, the date and the time spent'''
    def __init__(self, task, date, duration):
            self.task = task
            self.date = date
            self.time_in_minutes = self._get_duration_in_minutes(duration)

    def __str__(self):
            return '{0}|{1}|{2}'.format(self.date, self.task, self.time_in_minutes)

    def _get_duration_in_minutes(self, duration):
            hours, minutes = duration.split(':')
            return int(hours) * 60 + int(minutes)

database.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sqlite3
from objects import Project, Task

class KronosDatabase():
    _db_connection = None
    _db_cursor = None

    def __init__(self, filename):
        self._db_connection = sqlite3.connect(filename)
        self._db_cursor = self._db_connection.cursor()

    def __del__(self):
        self._db_connection.close()

    def insert_entry(self, entry):
        self._db_cursor.execute('''INSERT INTO Entry(task, time_in_minutes, date) VALUES (?, ?, ?)''', (entry.task, entry.time_in_minutes, entry.date) )
        self._db_connection.commit()

    def get_projects(self):
        query =  self._db_cursor.execute('''SELECT name FROM Project''').fetchall()

        for result in query:
            Project(result[0])

    def get_tasks_for_project(self, project_name):
        query = self._db_cursor.execute('''SELECT id, name FROM  Task WHERE project = ?''', (project_name,) ).fetchall()

        for result in query:
            task = Task(result[0], result[1])
            Project.get_project_by_name(project_name).add_task(task)

main.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from datetime import datetime
from tkinter import *
from tkinter import ttk
from objects import Project, Task, Entry
from database import KronosDatabase


class MainWindow(Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent
        self.grid(row=0, column=0)

        self.menu_bar = Menu(parent)

        self.file_menu = Menu(self.menu_bar, tearoff=0)
        self.report_menu = Menu(self.menu_bar, tearoff=0)

        self.menu_bar.add_cascade(label='File', menu=self.file_menu)
        self.menu_bar.add_cascade(label='Reporting', menu=self.report_menu)

        parent.config(menu=self.menu_bar)

        self.project_label = ttk.Label(parent, text='Project')
        self.selected_project = StringVar()
        self.project_combobox = ttk.Combobox(parent, textvariable=self.selected_project, state='readonly')
        self.project_combobox['values'] = Project.projects
        self.project_combobox.bind('<<ComboboxSelected>>', self._project_combobox_selection_changed)

        self.task_label = ttk.Label(parent, text='Task')
        self.selected_task = StringVar()
        self.task_combobox = ttk.Combobox(parent, textvariable=self.selected_task, state='readonly')
        self.task_combobox.bind('<<ComboboxSelected>>', self._task_combobox_selection_changed)

        self.button = ttk.Button(parent, text='Start', command=self._button_click)
        self.button.state(['disabled'])

        self.time_label = ttk.Label(parent, text='00:00', font='Segoe 16')

        self.project_label.grid(row=0, column=0, padx=2, pady=2, sticky=W)
        self.project_combobox.grid(row=0, column=1, padx=2, pady=2, sticky=W)
        self.task_label.grid(row=1, column=0, padx=2, pady=2,sticky=W)
        self.task_combobox.grid(row=1, column=1,padx=2, pady=2, sticky=W)
        self.button.grid(row=2, column=0, columnspan=2, padx=2, pady=2,)
        self.time_label.grid(row=3, column=0, columnspan=2, padx=2, pady=2,)

    def _project_combobox_selection_changed(self, event):
        self.button.state(['disabled'])
        self.task_combobox.set('')
        self._get_tasks_for_project(self.selected_project.get())

    def _task_combobox_selection_changed(self, event):
        if self.task_combobox.get() != '':
            self.button.state(['!disabled'])

    def _button_click(self):
        if (state):
            self.button.configure(text='Start')
            self._stop_timer()
        else:
            self.button.configure(text='Stop')
            self._start_timer()

    def _get_tasks_for_project(self, project):
        selected_project = Project.get_project_by_name(project)
        db.get_tasks_for_project(self.selected_project.get())
        self.task_combobox['values'] = selected_project.get_tasks()

    def _start_timer(self):
        global state
        state = True
        self.project_combobox.configure(state='disabled')
        self.task_combobox.configure(state='disabled')

    def _stop_timer(self):
        global state
        state = False
        self.project_combobox.configure(state='readonly')
        self.task_combobox.configure(state='readonly')
        self._generate_entry()

    def _generate_entry(self):
        task_id = int(self.selected_task.get().split(':')[0])
        duration = self.time_label.cget('text')

        entry = Entry(task_id, datetime.now().date(), duration)
        db.insert_entry(entry)

    def update_time_label(self):
        if (state):
            global timer

            timer[1] += 1
            if (timer[1] >= 60):
                timer[1] = 0
                timer[0] += 1

            time_string = pattern.format(timer[0], timer[1])
            self.time_label.configure(text=time_string)
        app.after(60000, self.update_time_label)


db = KronosDatabase(u'C:\\Users\\dmolnar004\\Desktop\\kronos.db')
db.get_projects()
pattern = '{0:02d}:{1:02d}'
timer = [0, 0]
state = False

app = Tk()
app.title('Krono/Kairos')
window = MainWindow(app)
window.update_time_label()
app.mainloop()
\$\endgroup\$
2
\$\begingroup\$

Fix indentation in you objects.py, use 4 spaces instead of tabs+spaces

so it will look like this:

class Project:
'''Class that holds the projects'''
projects = []

def __init__(self, name):
    self.name = name
    self.tasks = []
    type(self).projects.append(self)
...

You don't need parenthesis here

if (task not in self.tasks):

Just write

if task not in self.tasks:

You might want to store projects as dict(project.name: project) instead of list so this:

@staticmethod
def get_project_by_name(name):
        for project in Project.projects:
                if name == project.name:
                        return project

will be just:

@staticmethod
def get_project_by_name(name):
    return Project.projects[name]

In case if you will have multiple projects with the same name you can make it dict(project.name: [projects_list])

in database.py

I would not keep db connection always open, I would connect to it each time I need it.

then

for result in query:
        task = Task(result[0], result[1])
        Project.get_project_by_name(project_name).add_task(task)

will be prettier if:

for _id, name in query:
    task = Task(_id, name)
    Project.get_project_by_name(project_name).add_task(task)

in main.py its not clear why you need state to be a global variable.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.