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()
1 Answer 1
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.