From f0f3b410b2207ce11748f359ab241af8d1882168 Mon Sep 17 00:00:00 2001
From: anima
Date: Tue, 29 Jun 2021 05:47:15 +0200
Subject: [PATCH] release version 2.2.0a
---
README.md | 19 +--
timeTrack.py | 373 +++++++++++++++++++++++++++++++++------------------
2 files changed, 252 insertions(+), 140 deletions(-)
diff --git a/README.md b/README.md
index 5071328..427e403 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Simple time tracking
## Requirements
-- Python 3.9 or higher
+- Python 3.7 or higher
## Usage
*as of v.2.1.0a*
@@ -10,15 +10,16 @@ Start the script and follow the menu.
Currently no further usability.
## Agenda
-- [ ] Reports
+- [ ] [Reports](https://git.4nima.net/anima/timetrack/issues/13)
- [x] by day
- [ ] by week
- [ ] by month
- [ ] by user
-- [ ] Manual time entries
-- [ ] Break in time entries
-- [ ] User management
-- [ ] Categories
-- [ ] Clients
-- [ ] References
-- [ ] GUI
+- [ ] [Manual time entries](https://git.4nima.net/anima/timetrack/issues/6)
+- [ ] [Break in time entries](https://git.4nima.net/anima/timetrack/issues/5)
+ - [ ] [Short side tasks](https://git.4nima.net/anima/timetrack/issues/12)
+- [x] User management
+- [ ] [Categories](https://git.4nima.net/anima/timetrack/issues/7)
+- [ ] [Clients](https://git.4nima.net/anima/timetrack/issues/8)
+- [ ] [References](https://git.4nima.net/anima/timetrack/issues/9)
+- [ ] [GUI](https://git.4nima.net/anima/timetrack/issues/10)
diff --git a/timeTrack.py b/timeTrack.py
index 8b1425d..6ca9f72 100644
--- a/timeTrack.py
+++ b/timeTrack.py
@@ -3,7 +3,7 @@
#
# timeTrack.py
# by 4nima
-# v.2.1.0
+# v.2.2.0a
#
#########################
# simple time tracking with database
@@ -23,28 +23,32 @@ class TimeTrack:
self.USERID = 0
self.USERNAME = ''
self.OLDEVENT = 2
+ self.CLIENTS = False
+ self.CATEGORIES = False
+ self.REFERENCES = False
self.LOGFILE = 'timetrack.log'
- self.DBCON = sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
logging.basicConfig(
filename=self.LOGFILE,
level=logging.DEBUG,
format='%(asctime)s - %(process)d-%(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
+ if os.name == 'posix':
+ logging.debug('Unix/Linux system detected')
+ self.LINUX = True
+ else:
+ logging.debug('Winwos System detected')
+
self.db_setup()
self.load_config()
- def __del__(self):
- self.DBCON.close()
-
## Prepartation
+ #==============
### Check OS and clear screen
def clear_screen(self):
- if os.name == 'posix':
- logging.debug('Unix/Linux system detected')
+ if self.LINUX:
_ = os.system('clear')
else:
- logging.debug('Winwos System detected')
_ = os.system('cls')
### Loads or creates a config file
@@ -60,17 +64,23 @@ class TimeTrack:
quit()
else:
logging.info('Config file was loaded successfully')
- self.USERID = data['user']
- self.OLDEVENT = data['oldevent']
- logging.debug('UserID {} was used'.format(data['user']))
+ self.USERID = data['userid']
+ self.OLDEVENT = data['oldevent']
+ self.CLIENTS = data['clients']
+ self.CATEGORIES = data['categories']
+ self.REFERENCES = data['references']
+ logging.debug('UserID {} was used'.format(data['userid']))
self.set_user()
else:
logging.warning('Config file not found')
config = {
'default' : 'interactive',
- 'user' : 1,
- 'oldevent' : 2
+ 'userid' : 1,
+ 'oldevent' : 2,
+ 'clients' : False,
+ 'categories' : False,
+ 'references' : False
}
with open(self.CONFIG, "w") as outfile:
json.dump(config, outfile, indent=4, sort_keys=True)
@@ -92,6 +102,7 @@ class TimeTrack:
id INTEGER PRIMARY KEY AUTOINCREMENT,
starttime TIMESTAMP NOT NULL,
endtime TIMESTAMP NOT NULL,
+ breaktime1 TIMESTAMP,
user_id INTEGER NOT NULL,
activity TEXT,
reference TEXT,
@@ -105,6 +116,7 @@ class TimeTrack:
CREATE TABLE IF NOT EXISTS active_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
starttime TIMESTAMP NOT NULL,
+ breaktime TIMESTAMP,
user_id INT NOT NULL
)
""")
@@ -134,7 +146,7 @@ class TimeTrack:
logging.debug('Create initial database tables')
try:
- with self.DBCON as con:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
for SQL in sql:
con.execute(SQL)
except sqlite3.Error as err:
@@ -261,6 +273,7 @@ class TimeTrack:
return timedata
## user handling
+ #===============
### Creates a user who does not yet exist
def create_user(self, USER=''):
if USER == '':
@@ -277,8 +290,9 @@ class TimeTrack:
logging.debug('Accepted username: {}'.format(username))
sql = "INSERT INTO users ( name ) values ( ? )"
+
try:
- with self.DBCON as con:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
con.execute(sql, [username])
except sqlite3.Error as err:
logging.error('User could not be saved in database')
@@ -303,7 +317,7 @@ class TimeTrack:
sql = "SELECT * FROM users"
try:
- with self.DBCON as con:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
cur = con.cursor()
if data:
cur.execute(sql, [data])
@@ -335,7 +349,25 @@ class TimeTrack:
else:
self.USERNAME = data[0][1]
+ def delete_user(self, UID=False):
+ if not UID:
+ logging.error('no userid passed for deletion')
+ else:
+ logging.debug('Userid {} will be delete'.format(UID))
+ sql = "DELETE FROM users WHERE id = ?"
+
+ try:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
+ con.execute(sql, [UID])
+ except sqlite3.Error as err:
+ logging.error('User could not be delete from database')
+ logging.debug(sql)
+ logging.error('SQLError: {}'.format(err))
+ else:
+ logging.info('User was delete successfully')
+
## Time handling
+ #===============
### Creates an active event if none exists for the user
def save_event(self, TIME=datetime.datetime.now()):
if not self.get_active_event(USERID=self.USERID):
@@ -343,7 +375,7 @@ class TimeTrack:
sql = "INSERT INTO active_events ( starttime, user_id ) VALUES ( ?, ? )"
try:
- with self.DBCON as con:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
con.execute(sql, [TIME, self.USERID])
except sqlite3.Error as err:
logging.error('Event could not be created')
@@ -376,7 +408,7 @@ class TimeTrack:
return 0
try:
- with self.DBCON as con:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
con.execute(sql, [data])
except sqlite3.Error as err:
logging.error('Event could not be deleted')
@@ -386,7 +418,7 @@ class TimeTrack:
else:
logging.debug('Event was successfully deleted')
- # Checks for existing time entries and creates a new one if none is available.
+ ### Checks for existing time entries and creates a new one if none is available.
def time_start(self, AUTOFORWARD=True):
self.clear_screen()
starttime = datetime.datetime.now()
@@ -414,58 +446,45 @@ class TimeTrack:
else:
logging.debug('Event younger than 1 day')
print('Vergangene Zeit: >{} Stunden'.format(int(elapsed.seconds/3600)))
+
+ printtext = [
+ 'Wie soll mit dem Event verfahren werden?',
+ '[1] fortsetzen',
+ '[2] löschen',
+ '[0] abbrechen'
+ ]
+ userinput = self.userchoise(printtext, 3)
+ self.clear_screen()
- userinput = 0
- while not 0 < int(userinput) < 4:
- print('Soll das Event fortgesetzt oder gelöscht werden?')
- print('[1] für fortsetzen')
- print('[2] für löschen')
- print('[3] für abbrechen')
- userinput = input('Aktion: ')
- logging.debug('User input: {}'.format(userinput))
- try:
- int(userinput)
- except ValueError:
- userinput = 0
- self.clear_screen()
-
- if userinput == "1":
+ if userinput == 1:
logging.debug('Event should be continued')
self.time_stop()
- elif userinput == "2":
+ elif userinput == 2:
logging.info('Event should be deleted (eventid: {})'.format(data[0]))
self.delete_event(data[0])
self.time_start()
- else:
- logging.debug('Terminated by the user')
- exit()
else:
logging.debug('Event continues (eventid{})'.format(data[0]))
print('Event von {} Uhr geladen'.format(data[1].strftime("%H:%M")))
self.time_stop()
- # Stops existing time entries
+ ### Stops existing time entries
def time_stop(self):
data = self.get_active_event(USERID=self.USERID)
logging.debug('Event stop progess is started')
if data:
self.clear_screen()
- userinput = 0
- while not 0 < int(userinput) < 4:
- print('Event von {} Uhr beenden?'.format(data[1].strftime("%H:%M")))
- print('[1] für beenden')
- print('[2] für löschen')
- print('[3] für abbrechen')
- userinput = input('Aktion: ')
- logging.debug('User input: {}'.format(userinput))
- try:
- int(userinput)
- except ValueError:
- userinput = 0
- self.clear_screen()
+ printtext = [
+ 'Event von {} Uhr beenden?'.format(data[1].strftime("%H:%M")),
+ '[1] für beenden',
+ '[2] für löschen',
+ '[0] für abbrechen'
+ ]
+ userinput = self.userchoise(printtext, 3)
+ self.clear_screen()
- if userinput == "1":
+ if userinput == 1:
logging.debug('Event is ended')
print('Eingabe beenden mittels doppelter Leerzeile.')
print('Durchgeführte Tätigkeiten:')
@@ -492,7 +511,7 @@ class TimeTrack:
sql = "INSERT INTO time_entries ( starttime, endtime, user_id, activity ) VALUES ( ?, ?, ?, ? )"
try:
- with self.DBCON as con:
+ with sqlite3.connect(self.DATABASE, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) as con:
con.execute(sql, [data[1], endtime, self.USERID, action])
except sqlite3.Error as err:
logging.error('Time entry could not be created')
@@ -504,60 +523,134 @@ class TimeTrack:
logging.info('Time entry was created successfully')
self.delete_event(data[0])
+ self.clear_screen()
self.print_time_entry(STARTTIME=data[1], ENDTIME=endtime, ACTIVITY=action)
print('Zeiteintrag wurde gespeichert.')
- userinput = 0
- while not 0 < int(userinput) < 3:
- print('Nächsten Zeiteintrag beginnen ?')
- print('[1] Ja')
- print('[2] Nein')
- userinput = input('Aktion: ')
- logging.debug('User input: {}'.format(userinput))
- try:
- int(userinput)
- except ValueError:
- userinput = 0
- self.clear_screen()
+ printtext = [
+ 'Nächsten Zeiteintrag beginnen ?',
+ '[1] Ja',
+ '[0] Nein'
+ ]
+ userinput = self.userchoise(printtext)
+ self.clear_screen()
- if userinput == "1":
+ if userinput == 1:
self.time_start()
- else:
- self.start_interactive_mode()
- elif userinput == "2":
+ elif userinput == 2:
logging.info('Event should be deleted (eventid: {})'.format(data[0]))
self.delete_event(data[0])
- else:
- logging.debug('Terminated by the user')
- exit()
- def timedela_to_string(self, TIME):
- s = TIME.seconds
- hours, remainder = divmod(s, 3600)
- minutes, seconds = divmod(remainder, 60)
- output = '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds))
- return output
-
- # Shows a time entry
- def print_time_entry(self, STARTTIME='', ENDTIME='', ACTIVITY=''):
+ ## Interactive mode
+ #==================
+ ### Main menu of the interactive menu
+ def start_interactive_mode(self):
self.clear_screen()
- s = (ENDTIME - STARTTIME).seconds
- hours, remainder = divmod(s, 3600)
- minutes, seconds = divmod(remainder, 60)
-
- print(50*"-")
- print('Start: {} Uhr'.format(STARTTIME.strftime('%H:%M')))
- print('Ende: {} Uhr'.format(ENDTIME.strftime('%H:%M')))
- print('Dauer: {:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)))
- if not ACTIVITY == '':
- print('Aktivität:')
- print(ACTIVITY)
- print(50*"-")
+ printtext = [
+ '=> Hauptmenü - Angemeldet als {}'.format(self.USERNAME),
+ 'Was willst du tun?',
+ '[1] Zeiterfassung starten',
+ '[2] heutiger Report',
+ '[3] Reports',
+ '[4] Benutzerverwaltung',
+ '[0] Programm verlassen'
+ ]
+ userinput = self.userchoise(printtext, 4)
- def report_by_day(self, DATE=datetime.date.today(), USER=''):
+ if userinput == 1:
+ logging.debug('Start TimeTrack')
+ self.time_start()
+ elif userinput == 2:
+ logging.info('Print todays report')
+ self.report_by_day(DETAIL=True)
+ input()
+ elif userinput == 3:
+ self.interactive_reports()
+ elif userinput == 4:
+ self.interactive_usermanagment()
+ else:
+ exit()
+ self.start_interactive_mode()
+
+ ### Reports menu
+ def interactive_reports(self):
+ self.clear_screen()
+ printtext = [
+ '==> Reports für {}'.format(self.USERNAME),
+ 'Welcher Report soll angezeigt werden?',
+ '[1] Tages Report von [Datum]',
+ '[0] abbrechen'
+ ]
+ userinput = self.userchoise(printtext, 2)
+
+ if userinput == 1:
+ valid = False
+ while not valid:
+ self.clear_screen()
+ print('Report für welches Datum soll angezeigt werden?')
+ print('[0] für abbruch')
+ userdate = input("Datum: [yyyy-mm-dd]:")
+ if userdate == 0:
+ break
+ else:
+ try:
+ date = datetime.datetime.strptime(userdate, "%Y-%m-%d")
+ valid = True
+ except:
+ pass
+ self.report_by_day(date.date())
+
+ ### Usermanagment menu
+ def interactive_usermanagment(self):
+ self.clear_screen()
+ printtext = [
+ '==> Benutzerverwaltung - Angemeldet als {}'.format(self.USERNAME),
+ 'Was willst du tun?',
+ '[1] Benutzer anlegen',
+ '[2] Benutzer wechseln',
+ '[3] Benutzer löschen',
+ '[0] abbrechen'
+ ]
+ userinput = self.userchoise(printtext, 4)
+
+ if userinput == 1:
+ self.create_user()
+ elif userinput == 2:
+ users = self.get_users()
+ printtext = ['Wähle deinen User:']
+ count = 1
+ for user in users:
+ printtext.append('[{}] {}'.format(count, user[1]))
+ count += 1
+
+ printtext.append('[0] abbrechen')
+ userid = self.userchoise(printtext, count)
+ self.USERID = users[userid - 1][0]
+ self.set_user()
+ elif userinput == 3:
+ users = self.get_users()
+ printtext = ['Welcher User soll gelöscht werden?:']
+ count = 1
+ for user in users:
+ printtext.append('[{}] {}'.format(count, user[1]))
+ count += 1
+
+ printtext.append('[0] abbrechen')
+ userid = self.userchoise(printtext, count)
+ self.delete_user(users[userid - 1][0])
+
+ ## Reports
+ #=========
+ ### One day report, optionally with time entries
+ def report_by_day(self, DATE=datetime.date.today(), USER='', DETAIL=''):
if not USER:
USER = self.USERID
timedata = self.get_time_entry(DAY=DATE, USERID=USER)
+ if timedata == 0:
+ print("Es wurden keine Zeiteinträge für den angegeben Tag gefunden")
+ logging.debug('No time entries for date: '.format(DATE))
+ input()
+ return 1
STARTTIMES = []
ENDTIMES = []
@@ -577,50 +670,68 @@ class TimeTrack:
ENTRIECOUNT = len(timedata)
self.clear_screen()
- print('{:40} {}'.format("Beginn des ersten Zeiteintrags:", FIRSTSTARTTIME.strftime('%d.%m.%Y %H:%M')))
- print('{:40} {}'.format("Ende des letzen Zeiteintrags:", LASTENDTIME.strftime('%d.%m.%Y %H:%M')))
- print('{:40} {}'.format("Maximal erfassbare Zeit:", self.timedela_to_string(TRACKEDTIMESPAN)))
- print('{:40} {}'.format("Nicht erfasste Zeit:", self.timedela_to_string(NONTRACKEDTIME)))
- print('{:40} {}'.format("Erfasste Zeit:", self.timedela_to_string(TRACKEDTIME)))
- print('{:40} {}'.format("Zeiteinträge:", ENTRIECOUNT))
+ text = "{:40} {}"
+ print(text.format("Beginn des ersten Zeiteintrags:", FIRSTSTARTTIME.strftime('%d.%m.%Y %H:%M')))
+ print(text.format("Ende des letzen Zeiteintrags:", LASTENDTIME.strftime('%d.%m.%Y %H:%M')))
+ print(text.format("Maximal erfassbare Zeit:", self.timedela_to_string(TRACKEDTIMESPAN)))
+ print(text.format("Nicht erfasste Zeit:", self.timedela_to_string(NONTRACKEDTIME)))
+ print(text.format("Erfasste Zeit:", self.timedela_to_string(TRACKEDTIME)))
+ print(text.format("Zeiteinträge:", ENTRIECOUNT))
- def start_interactive_mode(self):
- self.clear_screen()
- userinput = 0
- while not 0 < int(userinput) < 4:
- print('Was willst du tun?')
- print('[1] für Zeiterfassung starten')
- print('[2] für heutiger Report')
- print('[3] für Report für Tag x')
- print('[9] für Programm verlassen')
+ printtext = [
+ 'Zeiteinträge anzeigen?',
+ '[1] Ja',
+ '[0] Nein'
+ ]
+ userinput = self.userchoise(printtext)
+ if userinput == 1:
+ for entry in timedata:
+ self.print_time_entry(entry[1], entry[2], entry[4])
+
+ ## Outputs
+ #=========
+ ### Shows a time entry
+ def print_time_entry(self, STARTTIME='', ENDTIME='', ACTIVITY=''):
+ s = (ENDTIME - STARTTIME).seconds
+ hours, remainder = divmod(s, 3600)
+ minutes, seconds = divmod(remainder, 60)
+
+ print(50*"-")
+ print('Start: {} Uhr'.format(STARTTIME.strftime('%H:%M')))
+ print('Ende: {} Uhr'.format(ENDTIME.strftime('%H:%M')))
+ print('Dauer: {:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)))
+ if not ACTIVITY == '':
+ print('Aktivität:')
+ print(ACTIVITY)
+ print(50*"-")
+
+ ## Miscellaneous
+ #===============
+ ### User selection
+ def userchoise(self, TEXT='', MAX=2):
+ userinput = -1
+ while not -1 < int(userinput) < MAX:
+ for text in TEXT:
+ print(text)
userinput = input('Aktion: ')
logging.debug('User input: {}'.format(userinput))
try:
int(userinput)
except ValueError:
- userinput = 0
+ userinput = -1
else:
- if int(userinput) == 9:
- logging.debug('Terminated by the user')
- exit()
- self.clear_screen()
+ self.clear_screen()
+ return int(userinput)
- if userinput == "1":
- logging.debug('Start TimeTrack')
- self.time_start()
- elif userinput == "2":
- logging.info('Print todays report')
- self.report_by_day()
- input()
- self.start_interactive_mode()
- elif userinput == "3":
- print('commig soon ...')
- input()
- self.start_interactive_mode()
-
- print(userinput)
+ ### Conversion to string of Timedelta typ
+ def timedela_to_string(self, TIME):
+ s = TIME.seconds
+ hours, remainder = divmod(s, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ output = '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds))
+ return output
+
if __name__ == "__main__":
test = TimeTrack()
- test.start_interactive_mode()
- test.DBCON.close()
\ No newline at end of file
+ test.start_interactive_mode()
\ No newline at end of file