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