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()
200_success
146k22 gold badges190 silver badges478 bronze badges
asked Oct 20, 2016 at 6:40
\$\endgroup\$

1 Answer 1

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.

answered Oct 20, 2016 at 16:03
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.