From 9b55063ea6cf52c7c561ff0bc0c913106009f24b Mon Sep 17 00:00:00 2001 From: Eugen Maksymenko <54111255+EMaksy@users.noreply.github.com> Date: Wed, 28 Jul 2021 15:14:12 +0200 Subject: [PATCH] Init cmd with database implementation (fix #37) (#41) * Implement init cmd subcommand (fix #37) Add sqlite dabase to the init cmd Add a symlink for tests --- MANIFEST.in | 4 +- bin/reportdaily.py | 1265 ++++++++++++++++++++++++++++++++++---- docs/conf.py | 2 +- docs/requirements.txt | 1 + setup.cfg | 10 +- tests/conftest.py | 20 + tests/test_init.py | 657 ++++++++++++++++---- workspace.code-workspace | 11 + 8 files changed, 1727 insertions(+), 243 deletions(-) create mode 100644 tests/conftest.py create mode 100644 workspace.code-workspace diff --git a/MANIFEST.in b/MANIFEST.in index 85b68d6b..219d134c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include LICENSE include C[hH][aA][nN][gG][eE][lL][oO][gG]* -grafts docs/* +graft docs prune docs/_build -global-exclude *.py[cod] __pycache__ *.so *.dylib \ No newline at end of file +global-exclude *.py[cod] __pycache__ *.so *.dylib diff --git a/bin/reportdaily.py b/bin/reportdaily.py index 4851a324..f29d9a0c 100755 --- a/bin/reportdaily.py +++ b/bin/reportdaily.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import argparse import logging from logging.config import dictConfig @@ -8,13 +7,15 @@ from datetime import date from configparser import ConfigParser import os +# database +import sqlite3 +from datetime import datetime +import textwrap -class MissingSubCommand(ValueError): - pass - - +# GLOBALS CONFIGPATH = os.path.expanduser("~/.config/reportdaily/reportdailyrc") +DATABASEPATH = os.path.expanduser("~/.config/reportdaily/database.sqlite") __version__ = "0.3.0" __author__ = "Eugen Maksymenko " @@ -60,230 +61,1247 @@ class MissingSubCommand(ValueError): #: see https://docs.python-guide.org/writing/logging/#logging-in-a-library log.addHandler(logging.NullHandler()) +# CLASS + + +class MissingSubCommand(ValueError): + pass + + +class Database(): + + VERSION = 10 + + def __init__(self, path: str, sql_data=None): -def cmd_init(args, CONFIGPATH): + self.path = path + # Initialized in other functions for database connection + self.connection = None + # data which is send by user + + self.sql_data = {} if sql_data is None else sql_data + """ + the previous code is equivalent to the following code + if sql_data is None: + self.sql_data = dict() + else: + self.sql_data = sql_data + """ + + # Create database sqlite.... + # create structure of database with required tables + self.create() + # table entry, trainee and team has been created + + def create(self): + """ + Create a database by a given path + """ + self.connection = sqlite3.connect(f"{self.path}") + log.debug("Connection to database true") + self._create_empty_database() + + def _create_empty_database(self): + """ + Create tables to given database + Tables: team, trainee and entry + :param database: path + """ + create_users_table_team = """ + CREATE TABLE IF NOT EXISTS team ( + TEAM_ID INTEGER PRIMARY KEY AUTOINCREMENT, + TEAM_NUMBER INTEGER NOT NULL, + TEAM_NAME TEXT NOT NULL, + TEAM_START TEXT NOT NULL, + TEAM_END TEXT NOT NULL + ); + """ + create_users_table_trainee = """ + CREATE TABLE IF NOT EXISTS trainee ( + TRAINEE_ID INTEGER PRIMARY KEY AUTOINCREMENT, + NAME_TRAINEE TEXT, + START_YEAR TEXT, + GRADUATION_YEAR INTEGER, + DATABASE_VERSION TEXT, + NUMBER_OF_TEAMS TEXT INTEGER, + DURATION INTEGER, + CREATION_DATE TEXT, + TEAM_ID INTEGER, + FOREIGN KEY(TEAM_ID) REFERENCES team(TEAM_ID) + ); + """ + create_users_table_entry = """ + CREATE TABLE IF NOT EXISTS entry ( + ENTRY_ID INTEGER PRIMARY KEY AUTOINCREMENT, + ENTRY_TXT TEXT NOT NULL, + ENTRY_DATE TEXT NOT NULL, + DAY_ID INTEGER, + FOREIGN KEY(DAY_ID) REFERENCES team(DAY_ID) + ); + """ + create_users_table_day = """ + CREATE TABLE IF NOT EXISTS day ( + DAY_ID INTEGER PRIMARY KEY AUTOINCREMENT, + DAY_DATE TEXT NOT NULL, + DAY_FREE INTEGER, + TRAINEE_ID INTEGER NOT NULL, + FOREIGN KEY(TRAINEE_ID) REFERENCES trainee(TRAINEE_ID) + ); + """ + # execute querys + self._execute_sql(create_users_table_team) + self._execute_sql(create_users_table_trainee) + self._execute_sql(create_users_table_entry) + self._execute_sql(create_users_table_day) + + """ + this will be added in the upcoming feature #30 + def new_day(self): + """ + # Executes sql query to add a new day for upcoming entries + """ + sql_cmd = """ + # INSERT OR IGNORE INTO DAY (DAY_DATE,TRAINEE_ID) + # VALUES (date,1); + """ + return self._execute_sql(sql_cmd) + """ + + def _execute_sql(self, sql_command): + """ + Execute a query by a given "connection"/database and a sql query + param: + str connection: Path to the database + str query: A sql command you wish to execute + """ + cursor = self.connection.cursor() + + result = cursor.execute(sql_command) + self.connection.commit() + return result + + def _fill_table_sql_cmd(self): + """ + Execute a query with all the given information from script + :param + """ + log.debug( + "Here we will write down all our data to database %s", self.sql_data) + # trainee_data + trainee_name = str(self.sql_data.get("name")) + # print(trainee_name) + trainee_current_day = self.sql_data.get("current_day") + trainee_duration = float(self.sql_data.get("duration")) + trainee_start_year = self.sql_data.get("start_year") + trainee_end_duration = self.sql_data.get("end_duration_education") + trainee_number_teams = int(self.sql_data.get("count_teams")) + # team data + team_name = self.sql_data.get("team") + team_number = self.sql_data.get("team_number") + # team duration + team_start = self.sql_data.get("team_time_start") + team_end = self.sql_data.get("team_time_end") + team_id_fk = "1" + + # sql command + sql_cmd_trainee = f""" + INSERT INTO + trainee (NAME_TRAINEE, START_YEAR, GRADUATION_YEAR, DATABASE_VERSION, + NUMBER_OF_TEAMS, DURATION, CREATION_DATE,TEAM_ID) + VALUES + ("{trainee_name}", "{trainee_start_year}" , "{trainee_end_duration}", "{self.VERSION}", + "{trainee_number_teams}","{trainee_duration}","{trainee_current_day}","{team_id_fk}"); + """ + + sql_cmd_team = f""" + INSERT INTO + team (TEAM_NAME,TEAM_NUMBER,TEAM_START,TEAM_END) + VALUES + ("{team_name}","{team_number}","{team_start}","{team_end}"); + """ + + # now its time to execute sql command with data and fill the database + self._execute_sql(sql_cmd_team) + self._execute_sql(sql_cmd_trainee) + + def _adapt_changes_to_database(self): + """ + """ + + # We need to collect all the changed data + log.debug( + """Replace old values in database with the new %s""", self.sql_data) + trainee_name = str(self.sql_data.get("name")) + # print(trainee_name) + trainee_current_day = self.sql_data.get("current_day") + trainee_duration = float(self.sql_data.get("duration")) + trainee_start_year = self.sql_data.get("start_year") + trainee_end_duration = self.sql_data.get("end_duration_education") + trainee_number_teams = int(self.sql_data.get("count_teams")) + # team data + team_name = self.sql_data.get("team") + team_number = self.sql_data.get("team_number") + # team duration + team_start = self.sql_data.get("team_time_start") + team_end = self.sql_data.get("team_time_end") + team_id_fk = "1" + team_pk = "1" + trainee_pk = "1" + + # sql command + sql_cmd_trainee = f""" + REPLACE INTO + trainee (TRAINEE_ID,NAME_TRAINEE, START_YEAR, GRADUATION_YEAR, DATABASE_VERSION, + NUMBER_OF_TEAMS, DURATION, CREATION_DATE,TEAM_ID) + VALUES + ("{trainee_pk}","{trainee_name}", "{trainee_start_year}" , "{trainee_end_duration}", "{self.VERSION}", + "{trainee_number_teams}","{trainee_duration}","{trainee_current_day}","{team_id_fk}"); + """ + + sql_cmd_team = f""" + REPLACE INTO + team (TEAM_ID,TEAM_NAME,TEAM_NUMBER,TEAM_START,TEAM_END) + VALUES + ("{team_pk}","{team_name}","{team_number}","{team_start}","{team_end}"); + """ + + # now its time to execute sql command with data and fill the database + self._execute_sql(sql_cmd_team) + self._execute_sql(sql_cmd_trainee) + + # overwrite sql database with the changes + + def close(self): + """ + Close database manually + """ + log.debug("Database closed") + self.connection.close() + + +def cmd_init(args, configpath, databasepath): """ Initializes global user data (required for the first time). Either user data can be entered directly via options or user will be asked. :param argparse.Namespace args: Arguments given by the command line - :param str CONFIGPATH: Path where the configuration file is stored + :param str configpath: Path where the configuration file is stored :return: exit code of this function :rtype: int """ log.debug("INIT selected %s", args) - # check if a config file already exist - if os.path.exists(CONFIGPATH): - show_config(CONFIGPATH) + if os.path.exists(configpath): + show_config(configpath) # check if the user wants to change in the existing file if args.change is True: - how_to_change_config(args, CONFIGPATH) + how_to_change_config(args, configpath) + # collect sql data + sql_data = data_from_configfile(configpath) + # execute database changes + sql_database = Database(databasepath, sql_data) + sql_database._adapt_changes_to_database() + # close Database + sql_database.close() return 1 # create a config if there is none else: - create_config(args, CONFIGPATH) - show_config(CONFIGPATH) + config_value = collect_config_data(args) + log.debug(f"Return Value of config:{config_value}") + create_config(config_value, configpath) + show_config(configpath) + # collect data + sql_data = data_from_configfile(configpath) + # init the database + sql_database = Database(databasepath, sql_data) + # execute sql cmd and fill the database with our values + sql_database._fill_table_sql_cmd() + # dont forget to close the database + sql_database.close() return 0 -def show_config(CONFIGPATH): +def data_from_configfile(configpath): + """ + Collect data from our configfile so it can be send to database + + :param str configpath: Path where the configuration file is stored + :return: data collection from config parser + :rtype: dict + """ + # save data here + data_collection = dict + # read data from configfile + parser = ConfigParser() + parser.read(configpath) + # write data from configfile to dict + data_collection = dict(parser.items('settings')) + return data_collection + + +def show_config(configpath): """ Show the configs to the user - :param str CONFIGPATH: String with an absolute path for looking up the values of the configfile + :param str configpath: String with an absolute path for looking up the values of the configfile """ # read the config parser = ConfigParser() - parser.read(CONFIGPATH) + parser.read(configpath) + + # calculate the start and end date + end_of_education = calculate_team_duration_end( + + int(parser.get("settings", "count_teams")), + int(parser.get("settings", "start_year")), + ) + start_education = calculate_team_duration_start( + + 1, # input 1 which is like the first team with the same team + int(parser.get("settings", "start_year")), + ) + # output of configs print( f""" - "Your current configuration at the moment" + CONFIGURATION: + Name: {parser.get("settings","name")} Team: {parser.get("settings", "team")} - Date: {parser.get("settings", "current_day")} - Year: {parser.get("settings", "start_year")} - - If you desire to make changes to the configuration try the -c or --change option for the init command + Current team number: {parser.get("settings", "team_number")} + Start date of current team: {parser.get("settings", "team_time_start")} + End date of current team: {parser.get("settings", "team_time_end")} + Start of Education: {start_education} + End of Education: {end_of_education} + Duration: {parser.get("settings", "duration")} years + Count of teams during the education: {parser.get("settings", "count_teams")} + Date of Initialization of the database: {parser.get("settings", "current_day")} """ ) + print("If you desire to make changes to the configuration try the -c or --change option for the init command") + + +def input_duration_count_teams(args, year): + """ + Input the duration of the education + Calculates the end duration of the education + + :param argparse.Namespace args: Arguments given by the command line + :param year int: Get year from existing Configfile + :return: duration + :return: count_teams + :return: end_duration_education + :rtype: float, int,str + """ + + txt_year = textwrap.dedent(""" + How long is your apprenticeship? + Default is 3 years + Optional is 2.5 years if your want to shorten it + Failed once is 3.5 years + """) + # required paramater + duration = None + count_teams = None + + # check relation for namespace + while(True): + duration = ask_for_input(args.duration, txt_year) + if duration in ("3.0", "3"): + + duration = "3.0" + log.debug("duration = 3.0 --> count teams = 6") + count_teams = 6 + + break -def create_config(args, CONFIGPATH): + elif duration == "2.5": + log.debug("duration = 2.5 --> count teams = 5") + count_teams = 5 + break + + elif duration == "3.5": + log.debug("duration = 3.5 --> count teams = 7") + count_teams = 7 + break + elif duration not in ("3.5", "2.5", "3.0"): + log.debug( + f"Count teams: {count_teams} Duration: {duration} args_duration value : {args.duration}") + print(f"Value {duration} is to high or to low") + + end_duration_education = int(year) + float(duration) + log.debug(" This is the year before the calculation: %s", + end_duration_education) + # format end_duration_education + end_duration_education = calculate_team_duration_end( + count_teams, int(year)) + log.debug( + f"""duration : {duration}, count_teams : {count_teams}, end_duration : {end_duration_education}""") + return duration, count_teams, end_duration_education + + +def collect_config_data(args): """ - Create a config file, from users input, where the user data is stored as a dict - :param argparser.Namespace args: Arguments given by the command line - :param str CONFIGPATH: Path where the configuration file is stored + Collect configuration data by user input + Return a dict of collected values for creating a configfile + + : param argparse.Namespace args: Attributes given by the command line + : return: value dict with all configuration data + : rtype: dict """ + # ALl input txt + txt_name_input = textwrap.dedent( + """Please enter your full name - -> example: 'Max Musterman' """) + txt_year_input = textwrap.dedent( + "In which year did you start your apprenticeship ?: ") + txt_year_wrong_input = "Sorry only integers are allowed , no characters, try again" + str_team_input = "Enter your current team name: " # name - print("Please enter your full name --> example: 'Max Musterman'") - name = ask_for_input(args.name, "Enter your Name: ") + name = ask_for_input( + args.name, txt_name_input) + # start of the education + while True: + try: + year = int(ask_for_input( + args.year, txt_year_input)) + break + except: + print(txt_year_wrong_input) + continue + + # user input of duration,count_teams and end_duration + duration, count_teams, end_duration_education = input_duration_count_teams( + args, year) + # team - team = ask_for_input(args.team, "Enter your Team: ") - year = int(ask_for_input( - args.year, "In which year did your start your apprenticeship ?: ")) + team = ask_for_input(args.team, str_team_input) + # ask for the number of the team + team_number = config_team_number_input(args) + # find out the teams duration + team_date_start = calculate_team_duration_start(team_number, year) + team_date_end = calculate_team_duration_end(team_number, year) # time today_date = date.today() - # create a config file + value_dict = {'name': name, + 'team': team, + 'count_teams': count_teams, + 'team_number': team_number, + 'current_day': today_date, + 'duration': duration, + 'start_year': year, + 'end_duration_education': end_duration_education, + 'team_time_start': team_date_start, + 'team_time_end': team_date_end + } + + return value_dict + + +def create_config(config_values, configpath): + """ + Create a config file by given config_values under a given configpath + + : param dict config_values: All key and values for creating the config file + : param str configpath: Path where the configuration file is stored + + """ + + for key in config_values: + config_values[key] = str(config_values[key]) + config = ConfigParser() - config["settings"] = {'name': name, - 'team': team, 'current_day': today_date, 'start_year': year} + config["settings"] = config_values # create a config file - os.makedirs(os.path.dirname(CONFIGPATH), exist_ok=True) - with open(CONFIGPATH, 'w') as user_config: + os.makedirs(os.path.dirname(configpath), exist_ok=True) + with open(configpath, 'w') as user_config: config.write(user_config) - print(f"The file was created at this path {CONFIGPATH}") + log.debug("The file was created at this path %s" % (configpath)) + + +def calculate_team_duration_start(team_number_now: int, start_year_education: int): + """ + Calculate start time in team + Format time string to TIME iso + Creates a date object + + : param int team_number: Team Number + : param int year: Year + : return: date: String object + : rtype: date_object + """ + start_month = None + start_year = None + start_day = "01" + + if team_number_now == 1: + start_month = "09" + start_year = start_year_education + elif team_number_now == 2: + start_month = "03" + start_year = start_year_education+1 + elif team_number_now == 3: + start_month = "09" + start_year = start_year_education+1 + elif team_number_now == 4: + start_month = "03" + start_year = start_year_education+2 + elif team_number_now == 5: + start_month = "09" + start_year = start_year_education+2 + elif team_number_now == 6: + start_month = "03" + start_year = start_year_education+3 + elif team_number_now == 7: + start_month = "09" + start_year = start_year_education+3 + + # format time string to iso + start_str = f"{start_year}-{start_month}-{start_day}" + date_obj = datetime.strptime(start_str, '%Y-%m-%d') # python 3.6 required + # create date objects + # start_obj = date.fromisoformat(f"{start_str}") # python 3.7 + date_string = date_obj.date() + + # create date objects + # start_obj = date.fromisoformat(f"{start_str}") # python 3.7 + return date_string + + +def calculate_team_duration_end(team_number_now: int, start_year_education: int): + """ + Calculate and format end value of a team + + : param int team_number: Team Number + : param int year: Year + """ + + end_month = None + end_year = None + end_day = None + + if team_number_now == 1: + end_month = "02" + end_year = start_year_education+1 + elif team_number_now == 2: + end_month = "08" + end_year = start_year_education+1 + elif team_number_now == 3: + end_month = "02" + end_year = start_year_education+2 + elif team_number_now == 4: + end_month = "08" + end_year = start_year_education+2 + elif team_number_now == 5: + end_month = "02" + end_year = start_year_education+3 + elif team_number_now == 6: + end_month = "08" + end_year = start_year_education+3 + elif team_number_now == 7: + end_month = "02" + end_year = start_year_education+4 + + # which day the team ends + if end_month == "08": + end_day = "31" + elif end_month == "02": + end_day = "28" + + # format time string to iso + end_str = f"{end_year}-{end_month}-{end_day}" + # create date objects + # end_obj = date.fromisoformat(f"{end_str}") + + date_obj = datetime.strptime(end_str, "%Y-%m-%d") + date_string = date_obj.date() + log.debug(f"{date_string}") + + # create date objects + # start_obj = date.fromisoformat(f"{start_str}") + return date_string + + +def config_team_number_input(args): + """ + Input the team number + + : param argparse.Namespace args. Arguments given by the command line + : return: team number + : rtype: int + """ + + # txt input messages + txt_team_number_input = textwrap.dedent(""" + What team number is that? + Deafult 1-7 + """) + txt_team_number_error_msg = textwrap.dedent(""" + Sorry your number is not between 1-7 + Try Again! + """) + + txt_team_number_no_char_error_msg = textwrap.dedent("""Sorry no character are allowed, only numbers: D + """) + + while True: + try: + team_number = int(ask_for_input( + args.team_number, txt_team_number_input)) + if team_number in range(1, 8): + break + else: + print(txt_team_number_error_msg) + continue + except: + print(txt_team_number_no_char_error_msg) + + return int(team_number) def ask_for_input(var, message): """ Asks for input if passed variable is None, otherwise return variable value. - :param str|None var: The variable to check - :param str message: The message to use as a prompt - :return: Either the input from the user or the value of a variable - :rtype: str + : param str None var: The variable to check + : param str message: The message to use as a prompt + : return: Either the input from the user or the value of a variable + : rtype: str """ if var is None: var = input(message) return var -def how_to_change_config(args, CONFIGPATH): +def how_to_change_config(args, configpath): """ Change or overwrite the configs via direct input or cli Attributes - :param argparse.Namespace args: Attributes given by the command line - :param str CONFIGPATH: Path where the configuration file is stored - :return int: int return value for testing + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + : return int: int return value for testing """ - if args.name is None and args.year is None and args.team is None and args.change: - user_input_change(args, CONFIGPATH) + + if args.name is None and args.year is None and args.team is None and args.duration is None and args.team_number is None and args.change is True: + user_input_change(args, configpath) result_value = 0 else: - namespace_config_change(args, CONFIGPATH) - + namespace_config_change(args, configpath) result_value = 1 - show_config(CONFIGPATH) + show_config(configpath) return result_value -def namespace_config_change(args, CONFIGPATH): +def namespace_config_change(args, configpath): """ Input arguments direct via the console - :param argparse.Namespace args: Attributes given by the command line - :param str CONFIGPATH: Path where the configuration file is stored + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored """ + # FIXME i should rename this values because they are not required and redundant # store all the args from namespace name = args.name team = args.team year = args.year change = args.change + duration = args.duration + # count_teams = args.count_teams + team_number = args.team_number # add config parser to file config = ConfigParser() - config.read(CONFIGPATH) - if change is True: + config.read(configpath) + log.debug("namespace_config_change") + log.debug(f"{args}") + if change == True: # overwrite if namespace is filled - if name is not None: - config.set("settings", "name", name) - if team is not None: - config.set("settings", "team", team) - if year is not None: - config.set("settings", "start_year", year) - - with open(CONFIGPATH, "w") as configfile: - config.write(configfile) + + if name != None: + log.debug(f"name != None value:{name}") + check_name_relation_namespace(args, configpath) + if team != None: + log.debug(f"team != None value:{team}") + + check_team_name_relation_namespace(args, configpath) + if year != None: + log.debug(f"year != None value:{year}") + check_start_year_relation_namespace(args, configpath) + if duration != None: + log.debug( + f"duration != None and count_teams == None value={duration}") + check_duration_relation_namespace(args, configpath) + + if team_number != None: + log.debug(f"team_number != None value={team_number}") + check_team_number_relation_namespace(args, configpath) + + # config.set("settings", "team_number", team_number) + + # with open(configpath, "w") as configfile: + # config.write(configfile) # show config to the user , so changes are visible to the user log.debug("namespace_config was selected") -def check_is_int(input_str, input_is_int): +def check_is_int(input_str): """ - Prove if the given argument is an int and return True or decline and return False + Prove if the given argument is an int and return True or return False - :param str input_str: String given that needs to be checked - :param bool input_is_int: bool value default False - :return: True if str is an int, False if str is not an int - :rtype: bool + : param str input_str: STR parameter that is checked if it can be an int + : return: True if str is an int, False if str is not an int + : rtype: bool """ - if input_str.strip().isdigit(): - print("Year is a int value") - input_is_int = True - return input_is_int - else: - print("Input is not a int sorry, try again") - input_is_int = False - return input_is_int + try: + int(input_str) + log.debug("%s is an int value", input_str) + return True + except ValueError: + log.debug("%s is not an int value", input_str) + return False -def user_input_change(args, CONFIGPATH): +def check_duration_relation(args, configpath): + """ + Change existing duration of education value to new duration value by user input + Calculate count_teams and end date of education + Save changes to config + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + : return: duration + : rtype: float """ - Input the data that is asked by function tp change configs + # txt relation variables for later change + txt_select_duration = textwrap.dedent(""" + You selected to change the duration of your traineeship. + """) + + duration = None + end_duration_education = None + count_teams = None + # get year + data_from_existing_config = data_from_configfile(configpath) + year = int(data_from_existing_config.get("start_year")) + + print(txt_select_duration) + # re-ask the user for duration and count_teams to save the relation + duration, count_teams, end_duration_education = input_duration_count_teams( + args, year) - :param argparse.Namespace args: Attributes given by the command line - :param str CONFIGPATH: Path where the configuration file is stored - :return: the ConfigParser object - :rtype: configparse.ConfigParser + # write the changes to config + write_into_config("duration", duration, configpath) + write_into_config("count_teams", count_teams, configpath) + write_into_config("end_duration_education", + end_duration_education, configpath) + + return duration + # end_duration_education, count_teams + + +def check_duration_relation_namespace(args, configpath): """ - # all user options - choice_table = {"Name": "t1", "Team": "t2", "Year": "t3"} - tmp_input = '' - overwrite_input = ' ' + Change existing name value to new name value by given paramater from cmd + Calculate count of teams , depending of duration input + Save the change to configfile - # show and ask user what he wants to overwrite - print(""" - "What do you want to change?" - Your options are - Name - Team - Year + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # txt_variables for later changes + txt_selection_relation = textwrap.dedent(""" + You selected to change the duration of your traineeship """) - # check for right user input + # values + duration = args.duration + + end_duration_education = None + count_teams = None + # get year + data_from_existing_config = data_from_configfile(configpath) + year = int(data_from_existing_config.get("start_year")) + # get old values for a visible change + old_duration = data_from_existing_config.get("duration") + old_count_teams = data_from_existing_config.get("count_teams") + + print( + f"You want to change the duration from {old_duration} years to {duration} years ") + if duration != "3.0" or duration != "3.5" or duration != "2.5" or duration != "3": + print(f"Sorry duration value: {duration} is not right") + log.debug( + f"old value: {old_count_teams} new value: {count_teams} ") + log.debug(f"{txt_selection_relation}") + log.debug(f"The duration:{duration} and the count of teams: {count_teams}") + + # re-ask the user for duration and count_teams to save the relation + if duration == "3.0" and count_teams == None or duration == "3.5" and count_teams == None or duration == "2.5" and count_teams == None: + log.debug("duration == 3.0 and count_teams == None or duration == 3.5 and count_teams == None or duration == 2.5 and count_teams == None") + if duration == "2.5": + count_teams = 5 + elif duration == "3.0": + + count_teams = 6 + elif duration == "3.5": + count_teams = 7 + end_duration_education = calculate_team_duration_end(count_teams, year) + log.debug( + f"We pass the duration value:{duration} , count teams {count_teams} and end_duration {end_duration_education}") + + elif duration != "3.0" or duration != "3.5" or duration != "2.5": + log.debug("duration != 3.0 or duration != 3.5 or duration != 2.5") + args.duration = None + # need to set value of duration to none, so next function ask for input + duration, count_teams, end_duration_education = input_duration_count_teams( + args, year) + + # write the changes to config + write_into_config("duration", duration, configpath) + write_into_config("count_teams", count_teams, configpath) + write_into_config("end_duration_education", + end_duration_education, configpath) + + return duration + + +def check_name_relation(args, configpath): + """ + Change existing name value to new name value by user input + Save the change to configfile + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # txt variables for later change + txt_input_name = "Enter the new name " + + # get old config data + config = data_from_configfile(configpath) + old_name = config.get("name") + # input new name + new_name = input(txt_input_name) + print(f"Your name has changed from old name {old_name} to {new_name}") + # write into configfile + write_into_config("name", new_name, configpath) + + +def check_name_relation_namespace(args, configpath): + """ + Change existing name value to new name value by given paramater from cmd + Save the change to configfile + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # get old config data + config = data_from_configfile(configpath) + old_name = config.get("name") + + # input new name + new_name = args.name + print(f"Your name has changed from old name {old_name} to {new_name}") + + # write into configfile + write_into_config("name", new_name, configpath) + + +def check_start_year_relation(args, configpath): + """ + Change existing year value to new year value by user input + Ask user if duration of education also changed, and process the answer + Calculate team start and end date as relation to the changed start year + Calculate end duration of education date as relation to the changed start year + Save the change to configfile + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # txt variables for upcoming changes + txt_new_start_year_input = "Enter the new start year " + txt_input_min_year = "The year cant be smaller then 1000..try again" + txt_question_year_change = textwrap.dedent("""Did the duration of your education change? + Please enter yes or no + """) + + # get old config data + config = data_from_configfile(configpath) + old_year = config.get("start_year") + new_year = None + while(True): - tmp_input = input("Name, Team, Year? ").capitalize() - if tmp_input in choice_table: - # need to map the keys right to the settings --> from Name to name - if tmp_input == "Name": - tmp_input = "name" - elif tmp_input == "Team": - tmp_input = "team" - elif tmp_input == "Year": - tmp_input = "start_year" + input_is_int = False + new_year = input(txt_new_start_year_input) + try: + input_is_int = check_is_int( + new_year) + if int(new_year) < 1000: + print(txt_input_min_year) + continue + + if input_is_int == True: + break + except ValueError: + print("Sorry only numbers are allowed. Try again") + log.debug("Value error") + + continue + + print(f"The old year {old_year} was changed to the new entry {new_year}") + + # RELATION + # print(txt_relation) + while True: + answer = input(txt_question_year_change) + if "yes" in answer: + print("We need to adopt the duration value") + duration = check_duration_relation(args, configpath) + end_duration_education = str(new_year) + str(duration) break + + elif "no" in answer: + print("We took the old duration value") + end_duration_education = int( + new_year) + float(config.get("duration")) + break + else: - print("No key in config found --> Try again") + print("Wrong answer,try again") continue - input_is_int = False + # calculate team start and end as relation to the changed start year + team_number = config.get("team_number") + new_team_date_start = calculate_team_duration_start( + int(team_number), int(new_year)) + new_team_date_end = calculate_team_duration_end( + int(team_number), int(new_year)) + # calculate end duration of education + count_of_teams = int(config.get("count_teams")) + end_duration_education = calculate_team_duration_end( + count_of_teams, int(new_year)) + # write into configfile + write_into_config("start_year", new_year, configpath) + write_into_config("end_duration_education", + end_duration_education, configpath) + write_into_config("team_time_start", new_team_date_start, configpath) + write_into_config("team_time_end", new_team_date_end, configpath) + + +def check_start_year_relation_namespace(args, configpath): + """ + Change existing year value to new year value by given paramater from cmd + Calculate team start and end date as relation to the changed start year + Calculate end duration of education date as relation to the changed start year + Save the change to configfile + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + + # get old config data + config = data_from_configfile(configpath) + old_year = config.get("start_year") + new_year = args.year + + # txt for later changes + txt_new_start_year = "Enter the new start year " + txt_smallest_input_err_msg = "The year cant be smaller then 1000..try again" + + # input year while(True): - overwrite_input = input("Enter the change ") - if tmp_input == "start_year" and input_is_int == False: - input_is_int = check_is_int( - overwrite_input, input_is_int) - if input_is_int == True: + # check value for iso format of year + log.debug(f"{new_year}") + input_is_int = False + input_is_int = check_is_int( + new_year) + if input_is_int == False: + print(textwrap.dedent( + f"Sorry your Paramater: {new_year} parsed by cmd is not a correct int value, please try again:")) + log.fatal("INPUT is NOT a number") + new_year = input(txt_new_start_year) + log.debug(f"NEW_YEAR INPUT is an INT: {input_is_int}") + continue + if int(new_year) < 1000: + log.debug("Input NUMBER is UNDER 1000") + print(textwrap.dedent( + f"Sorry your Paramater: {new_year} parsed by cmd is not a correct int value, please try again:")) + print(txt_smallest_input_err_msg) + new_year = input(txt_new_start_year) + log.debug(f"NEW_YEAR INPUT is an INT: {input_is_int}") + continue + if input_is_int == True and int(new_year) >= 1000: + break + + print(textwrap.dedent( + f"The old year {old_year} was changed to the new entry {new_year}")) + # calculate team start and end as relation to the changed start year + team_number = config.get("team_number") + new_team_date_start = calculate_team_duration_start( + int(team_number), int(new_year)) + new_team_date_end = calculate_team_duration_end( + int(team_number), int(new_year)) + # calculate end duration of education + count_of_teams = int(config.get("count_teams")) + end_duration_education = calculate_team_duration_end( + count_of_teams, int(new_year)) + # write into configfile + write_into_config("start_year", new_year, configpath) + write_into_config("end_duration_education", + end_duration_education, configpath) + write_into_config("team_time_start", new_team_date_start, configpath) + write_into_config("team_time_end", new_team_date_end, configpath) + + +def check_team_name_relation(args, configpath): + """ + Change old team name to new team name by user input + Save the change to configfile + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # get old config data + config = data_from_configfile(configpath) + old_team_name = config.get("team") + + # NO RELATION, just input the new name + new_team_name = input("Enter the new team name ") + print( + f"Change the old team name {old_team_name} to new team name {new_team_name}") + + # write the changes to config + write_into_config("team", new_team_name, configpath) + + +def check_team_name_relation_namespace(args, configpath): + """ + Change old team name to new team name by given paramater from cmd + Save the change to configfile + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # get old config data + config = data_from_configfile(configpath) + old_team_name = config.get("team") + + # NO RELATION, just input the new name + print(args.team) + new_team_name = args.team + print( + f"Change the old team name {old_team_name} to new team name {new_team_name}") + + # write the changes to config + write_into_config("team", new_team_name, configpath) + + +def check_team_number_relation_namespace(args, configpath): + """ + Input new team number for change by cmd + On wrong cmd input , ask for new input + Calculate team time start + Calculate team time end + Save changes to config + + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + : return: TRUE/FALSE + : rtype: bool + + """ + # get old config data + config = data_from_configfile(configpath) + old_team_number = config.get("team_number") + duration = config.get("duration") + # RELATION team_name,team_start,team_end and duration + + try: + new_team_number = int(args.team_number) + log.debug( + f"Old team number: {old_team_number} new_team_number: {new_team_number} duration: {duration}") + while True: + if new_team_number >= 1 and new_team_number <= 7: + print( + f"You changed the team number from {old_team_number} to {new_team_number}") break - elif tmp_input != "start_year": + elif new_team_number < 1 or new_team_number > 7: + new_team_number = int(input( + "Min/Max number is to low/high --> please enter a number between 1-7 ")) + if duration == "3.0" and new_team_number == 7: + new_team_number = int(input( + "Sorry, please input a number between 1-6. The duration of the education is under 3.5 years ")) + continue + # calculate start and end date of the team + year = int(config.get("start_year")) + new_team_date_start = calculate_team_duration_start( + new_team_number, year) + new_team_date_end = calculate_team_duration_end( + new_team_number, year) + # write down the config + write_into_config("team_number", new_team_number, configpath) + write_into_config("team_time_start", new_team_date_start, configpath) + write_into_config("team_time_end", new_team_date_end, configpath) + # write_into_config("team", name, configpath) + return True + + except ValueError: + print(textwrap.dedent(""" + Only numbers are allowed. + No changes to the config were made + Just try again the -c --team-number " " option """)) + return False + + +def check_team_number_relation(args, configpath): + """ + Change team number by user input. + Check duration of education(relation) + Write the changes to configfile + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + """ + # get old config data + config = data_from_configfile(configpath) + old_team_number = config.get("team_number") + # RELATION team_name,team_start,team_end and duration + print("What is the new team number?") + new_team_number = config_team_number_input(args) + print(f"You changed {old_team_number} to the {new_team_number}") + + # txt as variable for later changes + txt_answer_team_change = textwrap.dedent( + """ + Did the name of the team change? + Please answer with a yes or no + """) + + answer_team_change = None + while True: + answer_team_change = input(txt_answer_team_change) + if "yes" in answer_team_change: + check_team_name_relation(args, configpath) + break + elif "no" in answer_team_change: break + else: + print("Wrong input , try again") + continue - # add config parser to file + # check if count of teams and duration fits ? + txt_answer_duration_change = textwrap.dedent( + """ + Did the duration of the education change ? + Please answer with a yes or no + """) + + answer_duration_change = None + while True: + answer_duration_change = input(txt_answer_duration_change) + if "yes" in answer_duration_change: + check_duration_relation(args, configpath) + break + elif "no" in answer_duration_change: + break + else: + print("Wrong input , try again") + continue + + # calculate start and end date of the team + year = int(config.get("start_year")) + new_team_date_start = calculate_team_duration_start( + new_team_number, year) + new_team_date_end = calculate_team_duration_end( + new_team_number, year) + # write down the config + write_into_config("team_number", new_team_number, configpath) + write_into_config("team_time_start", new_team_date_start, configpath) + write_into_config("team_time_end", new_team_date_end, configpath) + + +def write_into_config(key_input, value_input, configpath): + """ + Write value to a given key underthe given configpath + : param str key_input: Key value given by the cmd line + : param str value_input: Value given by the cmd line + : param str configpath: Configpath to the configuration file + : return: the ConfigParser object + : rtype: configparse.ConfigParser + """ config = ConfigParser() - config.read(CONFIGPATH) - config.set("settings", f"{tmp_input}", f"{overwrite_input}") - with open(CONFIGPATH, "w") as configfile: + config.read(configpath) + config.set("settings", f"{key_input}", f"{value_input}") + with open(configpath, "w") as configfile: config.write(configfile) - # show config to the user , so changes are visible to the user return config -def cmd_new(args): +def user_input_change(args, configpath): + """ + Input the data that is asked by function to change configs + : param argparse.Namespace args: Attributes given by the command line + : param str configpath: Path where the configuration file is stored + : return: the ConfigParser object + : rtype: configparse.ConfigParser + """ + # txt variables for later txt changes + txt_change_option = textwrap.dedent(""" + "What do you want to change?" + Your options are + Name + Team + Start year + Duration + Current Team Number + """) + + # show and ask user what he wants to overwrite + print(txt_change_option) + # FIXME need a function which will check if the change is possiblee + # check for right user input + while(True): + key_input = input( + "Please enter one of the upper options ").capitalize() + print(key_input) + + # need to map the keys right to the settings --> from Name to name + if key_input == "Name": + key_input = "name" + check_name_relation(args, configpath) + break + elif key_input == "Team" or key_input == "team": + key_input = "team" + check_team_name_relation(args, configpath) + break + elif "Start" in key_input: + key_input = "start_year" + check_start_year_relation(args, configpath) + break + elif key_input == "Duration": + key_input = "duration" + check_duration_relation(args, configpath) + break + # no need for user change count of teams, it is calculated now + # elif "Count" in key_input: + # key_input = "count_teams" + # check_count_teams_relation(args, configpath) + # break + elif "Current" in key_input: + key_input = "team_number" + check_team_number_relation(args, configpath) + break + else: + print("No key in config found --> Try again") + continue + + +def cmd_new(args, configpath): """Creates a new day for the incoming entries""" log.debug("New selected %s", args) + # create entry for today + # sql_database.new_day() + # close database + # sql_database.close() # FIXME Add context manager to simplify the open write close process print("New selected", args) return 0 @@ -324,7 +1342,7 @@ def cmd_export(args): def parsecli(cliargs=None) -> argparse.Namespace: - """Parse CLI with: class:`argparse.ArgumentParser` and return parsed result + """Parse CLI with: class: `argparse.ArgumentParser` and return parsed result : param cliargs: Arguments to parse or None (=use sys.argv) : return: parsed CLI result @@ -353,8 +1371,15 @@ def parsecli(cliargs=None) -> argparse.Namespace: '--name', "-n", help='User Name') parser_init.add_argument( '--year', "-y", help='Start year of the trainee') + parser_init.add_argument( + '--duration', "-d", help='How long is the education?') + # This value is calculated and is not required any more + parser_init.add_argument( + '--count-teams', "-ct", help='How many teams will be visited?') parser_init.add_argument( '--team', "-t", help='Current team name') + parser_init.add_argument( + '--team-number', "-tn", help='What is the number of your current team?') parser_init.add_argument( '--change', '-c', action='store_true', help='Change an existing configuration') @@ -420,7 +1445,7 @@ def main(cliargs=None) -> int: # log.warning("I'm a warning message.") # log.error("I'm an error message.") # log.fatal("I'm a really fatal massage!") - exit_code = args.func(args, CONFIGPATH) + exit_code = args.func(args, CONFIGPATH, DATABASEPATH) return exit_code except MissingSubCommand as error: diff --git a/docs/conf.py b/docs/conf.py index a8ce0a9e..a34d3391 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ # -- Project information ----------------------------------------------------- - +master_doc = "index" project = 'reportdaily' copyright = '2021, Eugen Maksymenko' author = 'Eugen Maksymenko' diff --git a/docs/requirements.txt b/docs/requirements.txt index e69de29b..6966869c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx diff --git a/setup.cfg b/setup.cfg index ce006a2e..5f448f28 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,13 @@ [metadata] name = reportdaily version = 0.3.0 -description = "..." +description = "Create edit and export daily, weekly, or monthly reports" long_description = file: README.md long_description_content_type = text/markdown author = Eugen Maksymenko author_email = eugen.maksymenko@suse.com -url = https://github.com/SchleichsSalaticus/reportdaily -download_url = https://github.com/SchleichsSalaticus/reportdaily/download +url = https://github.com/EMaksy/reportdaily +download_url = https://github.com/EMaksy/reportdaily/download classifiers = # See https://pypi.org/pypi?%3Aaction=list_classifiers Development Status :: 1 - Planning @@ -26,7 +26,7 @@ formats = bztar, zip [tool:pytest] minversion = 3.0 testpaths= tests/ -addopts = --cov=reportdaily tests/ --cov-report=term-missing +addopts = --cov=reportdaily --cov-report=term-missing [options] scripts = bin/reportdaily.py @@ -34,4 +34,4 @@ python_requires = >=3.6.* include_package_data = True # install_requires = -# \ No newline at end of file +# diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..892bd310 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,20 @@ +import pytest +# import namespace +from argparse import Namespace + + +@pytest.fixture +def args_ns(): + return Namespace(cliargs=["init"], name="NameTest", count_teams="6", + team="TeamName", year="2020", duration="3.0", team_number="4") + + +@pytest.fixture +def args_ns_empty(): + return Namespace(cliargs=["init"], name=None, team=None, year=None, change=None, duration=None, team_number=None) + + +@pytest.fixture +def args_change(args_ns_empty): + args_ns_empty.change = True + return args_ns_empty diff --git a/tests/test_init.py b/tests/test_init.py index c3f3d65f..5b1a236a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,6 +1,5 @@ # import configparser import configparser -from unittest import mock # import script that needs to be tested import reportdaily as rd @@ -8,58 +7,86 @@ import pathlib import os # required for date +from datetime import datetime from datetime import date # import namespace from argparse import Namespace # monkey patching /mocker patch -import unittest from unittest.mock import MagicMock, patch +from unittest import mock import builtins # required fo tests import pytest - # Global data STANDARD_SECTIONS = ["settings"] -STANDARD_OPTIONS = ["name", "team", "start_year", "current_day"] +STANDARD_OPTIONS = ['name', + 'team', + 'count_teams', + 'team_number', + 'current_day', + 'duration', + 'start_year', + 'end_duration_education', + 'team_time_start', + 'team_time_end' + ] + +STANDARD_CONFIG_DATA = {'name': "Eugen", + 'team': "Doks", + 'count_teams': "7", + 'team_number': "4", + 'current_day': date.today(), + 'duration': "3.0", + 'start_year': "2019", + 'end_duration_education': "2023-08-30", + 'team_time_start': "2021-03-01", + 'team_time_end': "2021-08-31", + } + STANDARD_CONFIG = { # Section_name: list of key names "settings": tuple(STANDARD_OPTIONS), # "test": ("test1", "test2"), # ... } -ARGS = Namespace(cliargs=["init"], name="NameTest", - team="TeamName", year="2020") - -ARGS_CHANGE_CMD = Namespace(cliargs=["init"], change=True, name="ChangeName", - team="ChangeTeamName", year="2020") - -ARGS_CHANGE = Namespace( - cliargs=["init"], name=None, team=None, year=None, change=True) def test_config_exists(tmp_path: pathlib.Path): """This test will tests if the config exists""" # GIVEN - # ARGS + # STANDARD_CONFIG_DATA configpath = tmp_path / "reportdailyrc" # WHEN = expect a config file is created in tmp_path - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) # THEN config file existence is True assert os.path.exists(configpath) is True +def test_database_exists(args_ns, tmp_path: pathlib.Path): + """Check if database is created under a certain path""" + # GIVEN + + configpath = tmp_path / "reportdailyrc" + databasepath = tmp_path / "databasepath" + + # WHEN + rd.cmd_init(args_ns, configpath, databasepath) + + # THEn dababase existence is true + assert os.path.exists(databasepath) is True + + def test_config_section_option_namespace(tmp_path: pathlib.Path): """This test will check the config file if the sections and options are correct""" # GIVEN - # ARGS # STANDARD_SECTIONS # STANDARD_OPTIONS configpath = tmp_path / "reportdailyrc" # WHEN - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() config.read(configpath) @@ -73,17 +100,17 @@ def test_config_section_option_namespace(tmp_path: pathlib.Path): def test_config_values(tmp_path: pathlib): """This test will prove the values of the config file""" # GIVEN - # ARGS # STANDARD_CONFIG # convert date obj to string date_today = date.today() date_string = date_today.strftime("%Y-%m-%d") - expected = ["NameTest", "TeamName", "2020", date_string] + expected = [STANDARD_CONFIG_DATA.get("name"), STANDARD_CONFIG_DATA.get("team"), STANDARD_CONFIG_DATA.get("count_teams"), "4", date_string, + STANDARD_CONFIG_DATA.get("duration"), STANDARD_CONFIG_DATA.get("start_year"), STANDARD_CONFIG_DATA.get("end_duration_education"), STANDARD_CONFIG_DATA.get("team_time_start"), STANDARD_CONFIG_DATA.get("team_time_end")] configpath = tmp_path / "reportdailyrc" # WHEN - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() config.read(configpath) @@ -99,13 +126,12 @@ def test_config_real_section(tmp_path: pathlib): """This test will check if all the sections are correct""" # GIVEN - # ARGS = Namespace(cliargs=["init"], name="NameTest", team="TeamName", year="2020") std_keys = set(STANDARD_CONFIG) configpath = tmp_path / "reportdailyrc" # WHEN # create config - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() config.read(configpath) real_keys = set(config.keys()) @@ -115,36 +141,48 @@ def test_config_real_section(tmp_path: pathlib): assert real_keys == std_keys -def test_change_config_namespace(tmp_path: pathlib): +@pytest.mark.parametrize(("wrong_duration,right_duration,count_teams"), [ + ("5", "3.5", "7"), + ("5", "3.0", "6"), + ("f", "2.5", "5"), + ("2.5", "2.5", "5"), + ("3.0", "3.0", "6"), + ("3.5", "3.5", "7"), +]) +def test_change_config_namespace(args_ns, tmp_path: pathlib, wrong_duration, right_duration, count_teams): """This test will check if the change option works""" # GIVEN - # ARGS: name="NameTest",team="TeamName", year=int(2020) - # ARGS_CHANGE_CMD: cliargs=["init -c"], name="ChangeName", team="ChangeTeamName", year=int(2020) + # STANDARD_CONFIG_DATA configpath = tmp_path / "reportdailyrc" - data_after_change_dict = {'name': 'ChangeName', 'team': 'ChangeTeamName', - 'start_year': '2020'} + args_ns.duration = wrong_duration + args_ns.change = True - # WHEN + data_after_change_dict = {"name": args_ns.name, "team": args_ns.team, + "count_teams": count_teams, "team_number": args_ns.team_number, "start_year": args_ns.year, + "duration": right_duration, } + # WHEN # create config file and read it - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() config.read(configpath) with open(configpath, 'r') as configfile: configs_before_change = configfile.read() - print(configs_before_change) - # save all values - configs_before_change_dict = dict(config.items("settings")) - # show the change namespace - print(ARGS_CHANGE_CMD) - # use change config with namespace - rd.namespace_config_change(ARGS_CHANGE_CMD, configpath) + + def mock_input(txt): + """ + PATCH USER INPUT + """ + + return right_duration + + with patch.object(builtins, 'input', mock_input): + rd.namespace_config_change(args_ns, configpath) + # save the changed config config.read(configpath) with open(configpath, 'r') as configfile: config_after_change = configfile.read() - # output of the changed content - print(config_after_change) # save all values configs_after_change_dict = dict(config.items("settings")) @@ -158,133 +196,176 @@ def test_change_config_namespace(tmp_path: pathlib): @pytest.mark.parametrize("user_category,user_change,user_key", [("Name", "TestInputName", "name"), ("Team", "TestInputTeam", "team"), - ("Year", "2019", "start_year"), + # ("Year", "2019", "start_year"), ]) -def test_change_by_input(tmp_path: pathlib.Path, user_category, user_change, user_key): +def test_change_by_input(args_change, tmp_path: pathlib.Path, user_category, user_change, user_key): """This test will simulate user input and check the changes""" # GIVEN - - # ARGS = Namespace(cliargs=["init"], name="NameTest",team="TeamName", year="2020") # user_category, user_change, user_key configpath = tmp_path / "reportdailyrc" # WHEN - # create a config file - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() config.read(configpath) # open config and save it for visability with open(configpath, 'r') as configfile: # save dict before changes configs_before_change = dict(config.items("settings")) - print(configs_before_change) # patch your input + test_value = 0 + def mock_input(txt): "This is our function that patches the builtin input function. " - if txt.lower().startswith("name"): - print(user_category) + nonlocal test_value + if txt.lower().startswith("please"): + test_value += 1 return user_category - elif txt.lower().startswith("enter"): - print(user_change) + if txt.lower().startswith("enter"): + test_value += 2 return user_change with patch.object(builtins, 'input', mock_input): - rd.user_input_change(ARGS_CHANGE, configpath) + rd.user_input_change(args_change, configpath) # read changed value config.read(configpath) configs_after_change = dict(config.items("settings")) - print(configs_after_change.get(user_key)) # THEN assert configs_after_change.get(user_key) == user_change -def test_wrong_input_change(tmp_path: pathlib): +def test_wrong_input_change(args_change, tmp_path: pathlib): # GIVEN user_wrong_input = "TEST" user_right_input = "Name" - user_canged_value = "TESTNAME" + user_changed_value = "TESTNAME" awaited_output = "No key in config found --> Try again" # path configpath = tmp_path / "reportdailyrc" - # GIVEN Namespace for the change command - # ARGS_CHANGE = Namespace( - # cliargs=["init"], name=None, team=None, year=None, change=True) - # Use this Namespace so create a config for the test - # ARGS = Namespace(cliargs=["init"], name="NameTest", - # team="TeamName", year="2020") - # Grab stdout - run_once = 0 # WHEN # create a config file - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() config.read(configpath) # open config and save it for visibility with open(configpath, 'r') as configfile: configs_before_change = dict(config.items("settings")) - # save dict before changes - print(configs_before_change) # simultate input of user + run_once = 0 def mock_input(txt): "This is our function that patches the builtin input function. " nonlocal run_once - while 1: - if txt.lower().startswith("name") and run_once == 0: - print("wrong input") + while True: + if txt.lower().startswith("please enter") and run_once == 0: run_once += 1 return user_wrong_input - if txt.lower().startswith("name") and run_once == 1: - print("Right input") + if txt.lower().startswith("please enter") and run_once == 1: run_once += 1 return user_right_input if txt.lower().startswith("enter") and run_once == 2: run_once += 1 - return user_canged_value + return user_changed_value break # patch the input with patch.object(builtins, 'input', mock_input): # run config input function - rd.user_input_change(ARGS_CHANGE, configpath) + rd.user_input_change(args_change, configpath) # THEN assert awaited_output in awaited_output -def test_create_config_user_input(tmp_path: pathlib): +# @patch("reportdaily.input_duration_count_teams") +@pytest.mark.parametrize("duration,count_teams,end_duration_education", + [("2.5", "5", "2023-02-28"), + ("3.0", "6", "2023-08-31"), + ("3.5", "7", "2024-02-28"), + ("1", "6", "2023-08-31"), # wrong int number + ("y", "6", "2023-08-31"), # wrong character input + ]) +def test_create_config_user_input(args_ns_empty, tmp_path: pathlib, duration, count_teams, end_duration_education): # GIVEN name = "TESTNAME" team = "TESTTEAM" year = "2020" - ARGS_USER_INPUT = Namespace( - cliargs=["init"], name=None, team=None, year=None) + wrong_year = "wrong_year" + # duration + duration_right = "3.0" + # count teams + team_number = 4 + wrong_team_number = 10 + wrong_team_number_txt = "ewgwer" configpath = tmp_path / "reportdailyrc" # WHEN # patch the input of user + count_of_questions_int = 0 + count_of_questions_char = 0 + count_of_questions_wrong_input = 0 + count_of_questions_team_number = 0 + def mock_input(txt): "This is our function that patches the builtin input function. " - if txt.lower().startswith("enter your name"): + nonlocal count_of_questions_int + nonlocal count_of_questions_char + nonlocal count_of_questions_wrong_input + nonlocal count_of_questions_team_number + if txt.lower().strip().startswith("please enter your full name"): return name - if txt.lower().startswith("enter your team"): + if txt.lower().strip().startswith("in which"): # Handle the wrong input, insert right input as next + while True: + if count_of_questions_wrong_input == 0: + count_of_questions_wrong_input += 1 + return wrong_year + elif count_of_questions_wrong_input == 1: + count_of_questions_wrong_input += 1 + return year + elif count_of_questions_wrong_input == 2: + break + if txt.lower().strip().startswith("how long is your apprenticeship?") and count_of_questions_int == 0: + count_of_questions_int = 1 + return duration + # capture wrong input! + elif txt.lower().strip().startswith("how long is your apprenticeship?") and count_of_questions_int == 1: + count_of_questions_int = 2 + return duration_right + elif txt.lower().strip().startswith("how long is your apprenticeship?") and duration == "y": + count_of_questions_char = 1 + return duration_right + if txt.lower().strip().startswith("enter your current team name"): return team - if txt.lower().startswith("in which"): - return year + if txt.lower().strip().startswith("what team"): + # capture wrong input, to high number and character + while True: + if count_of_questions_team_number == 0: + count_of_questions_team_number += 1 + return wrong_team_number + elif count_of_questions_team_number == 1: + count_of_questions_team_number += 1 + return wrong_team_number_txt + elif count_of_questions_team_number == 2: + count_of_questions_team_number += 1 + return team_number + elif count_of_questions_team_number == 3: + break + return team_number with patch.object(builtins, 'input', mock_input): - rd.create_config(ARGS_USER_INPUT, configpath) + dict_values = rd.collect_config_data(args_ns_empty) # open configs + rd.create_config(dict_values, configpath) config = configparser.ConfigParser() config.read(configpath) with open(configpath, 'r') as configfile: @@ -293,47 +374,53 @@ def mock_input(txt): # THEN assert configs_after_creation.get("name") == name assert configs_after_creation.get("team") == team + assert configs_after_creation.get("team_number") == str(team_number) + if count_of_questions_int == 1: + assert configs_after_creation.get("duration") == duration + elif count_of_questions_int == 2: + assert configs_after_creation.get("duration") == duration_right + elif count_of_questions_char == 1: + assert configs_after_creation.get("duration") == duration_right + assert configs_after_creation.get("count_teams") == count_teams assert configs_after_creation.get("start_year") == year + assert configs_after_creation.get( + "end_duration_education") == end_duration_education def test_show_config(tmp_path: pathlib, capsys): """Test if awaited part of the output is in sdtout""" # GIVEN - # ARGS = Namespace(cliargs=["init"], name="NameTest",team="TeamName", year="2020") + configpath = tmp_path / "reportdailyrc" - awaited_part_of_output = "Your current configuration at the moment" + awaited_part_of_output = "CONFIGURATION:" # WHEN # create a config file - rd.create_config(ARGS, configpath) + rd.create_config(STANDARD_CONFIG_DATA, configpath) config = configparser.ConfigParser() # show the config rd.show_config(configpath) # capture input with pystest (capsys) captured = capsys.readouterr() - print(captured) - # THEN check the captured output if awaited part is in output assert awaited_part_of_output in captured.out @patch("reportdaily.user_input_change") @patch("reportdaily.show_config") -def test_how_to_change_configs_input(mocker_show_config, mocker_user_input_change, tmp_path: pathlib): +def test_how_to_change_configs_input(mocker_show_config, mocker_user_input_change, tmp_path: pathlib, args_change): """This test will check if the right function (user_input_change) was selected by a given ARGS Namespace object""" # GIVEN - # ARGS_CHANGE = Namespace(cliargs=["init"], name=None, team=None, year=None, change=True) configpath = tmp_path / "reportdailyrc" expected_value = 0 + # WHEN # patch the user_input_change function from reportdaily - mocker_show_config.return_value = MagicMock(return_value=True) + mocker_show_config.return_value = True mocker_user_input_change.return_value = MagicMock(return_value=True) # save return value of function - return_value = rd.how_to_change_config(ARGS_CHANGE, configpath) - print(return_value) - + return_value = rd.how_to_change_config(args_change, configpath) # THEN # check return value ,to prove that the right function was used assert return_value == expected_value @@ -341,21 +428,15 @@ def test_how_to_change_configs_input(mocker_show_config, mocker_user_input_chang @patch("reportdaily.namespace_config_change") @patch("reportdaily.show_config") -def test_how_to_change_configs_namespace(mocker_show_config, mocker_create_config, tmp_path: pathlib): +def test_how_to_change_configs_namespace(mocker_show_config, mocker_ns_config_change, tmp_path: pathlib, args_ns): """This test will check if the right function (namespace_config_change) was selected by a given ARGS Namespace object""" # GIVEN - # ARGS = Namespace(cliargs=["init"], name="NameTest", - # team="TeamName", year="2020") configpath = tmp_path / "reportdailyrc" expected_value = 1 # WHEN - # patch the user_input_change function from reportdaily - mocker_create_config.return_value = MagicMock(return_value=True) - mocker_show_config.return_value = MagicMock(return_value=True) # save return value of function - return_value = rd.how_to_change_config(ARGS, configpath) - print(return_value) + return_value = rd.how_to_change_config(args_ns, configpath) # THEN # check return value ,to prove that the right function was used @@ -364,44 +445,47 @@ def test_how_to_change_configs_namespace(mocker_show_config, mocker_create_confi @patch("reportdaily.create_config") @patch("reportdaily.show_config") -def test_cmd_init_without_configpath(mocker_show_config, mocker_create_config, tmp_path: pathlib): +@patch("reportdaily.Database") +@patch("reportdaily.data_from_configfile") +def test_cmd_init_without_configpath(mocker_data_from_configfile, mocker_database, mocker_show_config, mocker_create_config, tmp_path: pathlib, args_ns): """This test will check the propper use of cmd_init""" # GIVEN - # ARGS = Namespace(cliargs=["init"], name="NameTest", - # team="TeamName", year="2020") configpath = tmp_path / "reportdailyrc" + # add databasepath + databasepath = tmp_path / "database" expected_return_value = 0 # WHEN - mocker_create_config.return_value = MagicMock(return_value=True) - mocker_show_config.return_value = MagicMock(return_value=True) - return_value = rd.cmd_init(ARGS, configpath) + mocker_show_config.return_value = True + mocker_create_config.return_value = True + mock_db_inst = mocker_database.return_value + mock_db_inst._fill_table_sql_cmd.return_value = None + mocker_data_from_configfile.return_value = dict(vars(args_ns)) + return_value = rd.cmd_init(args_ns, configpath, databasepath) # THEN assert expected_return_value == return_value +@patch("reportdaily.data_from_configfile") @patch("reportdaily.os.path.exists") @patch("reportdaily.show_config") @patch("reportdaily.how_to_change_config") -def test_cmd_init_with_configpath(mocker_how_to_change_config, mocker_show_config, mocker_os_path_exists, tmp_path: pathlib): +def test_cmd_init_with_configpath(mocker_how_to_change_config, mocker_show_config, mocker_os_path_exists, mocker_data_from_configfile, tmp_path: pathlib, args_change): """This test will check the propper use of cmd_init if config already exists and the user wants to change it""" # GIVEN - # ARGS_CHANGE = Namespace( - # cliargs=["init"], name=None, - # team=None, year=None, - # change=True) configpath = tmp_path / "reportdailyrc" + databasepath = tmp_path/"database" expected_return_value = 1 # WHEN mocker_os_path_exists.return_value = MagicMock(return_value=True) mocker_show_config.return_value = MagicMock(return_value=True) mocker_how_to_change_config.return_value = MagicMock(return_value=True) - # execute command and gather the return value - return_value = rd.cmd_init(ARGS_CHANGE, configpath) + mocker_data_from_configfile.return_value = MagicMock(return_value=True) + return_value = rd.cmd_init(args_change, configpath, databasepath) # THEN assert expected_return_value == return_value @@ -411,16 +495,14 @@ def test_check_is_int_is_True(): """ Test if the function proves if the given values are int or not """ - # GIVEN given_str_is_also_int = "2009" - given_default_input_return_value = False expexted_return_value = True given_return_value = None # WHEN given_return_value = rd.check_is_int( - given_str_is_also_int, given_default_input_return_value) + given_str_is_also_int) # THEN assert expexted_return_value == given_return_value @@ -433,13 +515,358 @@ def test_check_is_int_is_False(): # GIVEN given_str_is_also_int = "TEST2019" - given_default_input_return_value = False expexted_return_value = False given_return_value = None # WHEN given_return_value = rd.check_is_int( - given_str_is_also_int, given_default_input_return_value) + given_str_is_also_int) # THEN assert expexted_return_value == given_return_value + + +@pytest.mark.parametrize("team_number,expected_date", + [(1, "2019-09-01"), + (2, "2020-03-01"), + (3, "2020-09-01"), + (4, "2021-03-01"), + (5, "2021-09-01"), + (6, "2022-03-01"), + (7, "2022-09-01"), + ]) +def test_calculate_team_duration_start(team_number, expected_date): + """ + Test calculation of the start team + """ + + # GIVEN + start_year = 2019 + expected_date_obj = datetime.strptime(expected_date, '%Y-%m-%d') + expected_date_obs = expected_date_obj.date() + + # WHEN + return_date = rd.calculate_team_duration_start(team_number, start_year) + + # THEN + assert expected_date_obs == return_date + + +@pytest.mark.parametrize("team_number,expected_date", + [(1, "2020-02-28"), + (2, "2020-08-31"), + (3, "2021-02-28"), + (4, "2021-08-31"), + (5, "2022-02-28"), + (6, "2022-08-31"), + (7, "2023-02-28"), + ]) +def test_calculate_team_duration_end(team_number, expected_date): + """ + Test calculation of the start team + """ + + # GIVEN + start_year = 2019 + expected_date_obj = datetime.strptime(expected_date, '%Y-%m-%d') + expected_date_obs = expected_date_obj.date() + + # WHEN + return_date = rd.calculate_team_duration_end(team_number, start_year) + + # THEN + assert expected_date_obs == return_date + + +@pytest.mark.parametrize("awaited_duration,input_duration,count_teams", + [("3.5", "3.5", "7"), + ("3.0", "3.0", "6"), + ("2.5", "2.5", "5"), + ("3.0", "9", "6") + ]) +def test_duration_relation(awaited_duration, input_duration, count_teams, tmp_path: pathlib.Path, args_change): + """ + Test the return value of duration by change + """ + + # GIVEN + # STANDARD_CONFIG_DATA + configpath = tmp_path / "reportdaily_config" + # function will be patched + return_duration = "None" + + # WHEN + # create config + rd.create_config(STANDARD_CONFIG_DATA, configpath) + # counter for input patch + counter = 0 + + def mock_input(txt): + "This is our function that patches the builtin input function. " + nonlocal counter + while(True): + if txt.lower().strip().startswith("how long") and counter == 0: + counter += 1 + return input_duration + elif txt.lower().strip().startswith("how") and counter == 1: + counter += 1 + return "3.0" + elif counter == 3: + break + with patch.object(builtins, 'input', mock_input): + return_duration = rd.check_duration_relation(args_change, configpath) + # read config + config = configparser.ConfigParser() + config.read(configpath) + # THEN + assert awaited_duration == return_duration + assert count_teams == config.get("settings", "count_teams") + + +@pytest.mark.parametrize("year_input,right_year_input,duration_answer", [ + ("y", 2000, "no"), + (999, 2000, "no"), + (999, 2000, "yes"), + (999, 2000, "TEST"), +]) +def test_check_start_year_relation(tmp_path: pathlib.Path, year_input, right_year_input, duration_answer, args_ns): + """ + Check if the year can be changed by user via direct input. + Test for check_start_year_relation(). + """ + + # GIVEN + configpath = tmp_path / "year_config" + + awaited_year = "2000" + + # WHEN + # create config + rd.create_config(STANDARD_CONFIG_DATA, configpath) + + # required counters for patch input + counter = 1 + second_counter = 1 + + def mock_input(txt): + """ + PATCH USER INPUT + """ + nonlocal counter + nonlocal second_counter + while(True): + if txt.lower().strip().startswith("enter") and counter == 1: + counter += 1 + return year_input + elif txt.lower().strip().startswith("enter") and counter == 2: + counter += 1 + return right_year_input + elif txt.lower().strip().startswith("did") and counter == 3: + counter += 1 + return duration_answer + elif txt.lower().strip().startswith("how long") and counter == 4: + counter += 1 + # default number for duration + duration_number = "3" + return duration_number + elif txt.lower().strip().startswith("did") and counter == 4 and second_counter == 1: + counter += 1 + second_counter += 1 + # default number for duration + answer = "no" + return answer + elif counter == 5: + break + + with patch.object(builtins, 'input', mock_input): + return_duration = rd.check_start_year_relation(args_ns, configpath) + + # read config + config = configparser.ConfigParser() + config.read(configpath) + + # THEN + assert awaited_year == config.get("settings", "start_year") + + +@pytest.mark.parametrize(("wrong_year,right_year"), [ + ("999", "1000"), + ("f", "1000"), +]) +def test_start_year_relation_namespace(tmp_path: pathlib, wrong_year, right_year, args_change): + """ + Check if the year can be changed by the cmd namespace. + Test for check_start_year_relation_namespace() function. + """ + # GIVEN + + configpath = tmp_path / "reportdaily_year_namespace" + args_change.year = wrong_year + expected_year_after_change = "1000" + data_after_change_dict = {'start_year': right_year} + + # WHEN + # create config file and read it + rd.create_config(STANDARD_CONFIG_DATA, configpath) + config = configparser.ConfigParser() + config.read(configpath) + with open(configpath, 'r') as configfile: + configs_before_change = configfile.read() + + def mock_input(txt): + """ + PATCH USER INPUT + """ + return right_year + + with patch.object(builtins, 'input', mock_input): + rd.check_start_year_relation_namespace(args_change, configpath) + + # save the changed config + config.read(configpath) + with open(configpath, 'r') as configfile: + config_after_change = configfile.read() + # save all values + configs_after_change_dict = dict(config.items("settings")) + + # read config values + config = configparser.ConfigParser() + config.read(configpath) + + # THEN + # check if the file has changed + assert configs_before_change != config_after_change + # prove if the expected values are in our changed config + assert data_after_change_dict.items() <= configs_after_change_dict.items() + # check year is the same as change + assert expected_year_after_change == config.get( + "settings", "start_year") + + +@pytest.mark.parametrize(("wrong_team_number,right_team_number"), [ + (0, 3), + (100, 3), + (100, 7), # right team number value needs to be 7, so duration == "3.0" and new_team_number == 7 case is covered + ("ERROR", 7) +]) +def test_start_team_number_relation_namespace(tmp_path: pathlib, wrong_team_number, right_team_number, args_change): + """ + Check if value of team number changed correctly. + Simulate wrong user input and correct input by the given paramater via cmd. + Tested function: check_team_number_relation_namespace(). + """ + # GIVEN + configpath = tmp_path / "reportdaily_team_number_namespace" + args_change.duration = "3.0" + args_change.team_number = wrong_team_number + expected_team_number = "3" + + # WHEN + # create config file and read it + rd.create_config(STANDARD_CONFIG_DATA, configpath) + config = configparser.ConfigParser() + config.read(configpath) + with open(configpath, 'r') as configfile: + configs_before_change = configfile.read() + + def mock_input(txt): + """ + PATCH USER INPUT for this function: check_team_number_relation_namespace + """ + nonlocal expected_team_number + if txt.lower().strip().startswith("sorry"): + # catch the case of duration =="3.0" and team number = 7 + expected_team_number = "6" + return 6 + else: + return right_team_number + + with patch.object(builtins, 'input', mock_input): + rd.check_team_number_relation_namespace(args_change, configpath) + + # save the changed config + config.read(configpath) + with open(configpath, 'r') as configfile: + config_after_change = configfile.read() + + # read config values + config = configparser.ConfigParser() + config.read(configpath) + + # THEN + # check the value if it has changed + # catch character input + if type(wrong_team_number) == str: + assert expected_team_number != config.get( + "settings", "team_number") + # catch to high or to small value + else: + assert expected_team_number == config.get( + "settings", "team_number") + + +@pytest.mark.parametrize(("wrong_input1,answer_change_name,answer_duration_change,wrong_input2,team_number"), [ + ("WRONG", "yes", "yes", "WRONG", 6), + ("WRONG", "yes", "no", "WRONG", 6), + ("WRONG", "no", "yes", "WRONG", 6), + ("WRONG", "no", "no", "WRONG", 6)]) +@patch("reportdaily.check_team_name_relation") +@patch("reportdaily.check_duration_relation") +def test_team_number_relation(mocker_check_duration, mocker_check_team, wrong_input1, answer_change_name, answer_duration_change, wrong_input2, team_number, tmp_path: pathlib, args_change): + """ + Check values of check_team_number_relation function. + Simulate wrong user input , mixed with right answer. + Assert value change is correct + """ + + # GIVEN + # STANDARD_CONFIG_DATA + configpath = tmp_path / "reportdaily_team_number_input" + expected_team_number = "6" + + # WHEN + # create config file and read it + rd.create_config(STANDARD_CONFIG_DATA, configpath) + config = configparser.ConfigParser() + config.read(configpath) + with open(configpath, 'r') as configfile: + configs_before_change = configfile.read() + + # patch the user_input_change function from reportdaily + mocker_check_duration.return_value = True + mocker_check_team.return_value = True + + # simulate user input + counter = 0 + + def mock_input(txt): + """ + Patch user input for check_team_number_relatio + """ + nonlocal counter + if txt.lower().strip().startswith("what team number"): + return int(team_number) + elif txt.lower().strip().startswith("did the name") and counter == 0: + counter += 1 + return wrong_input1 + elif txt.lower().strip().startswith("did the name") and counter == 1: + counter += 1 + return answer_change_name + elif txt.lower().strip().startswith("did the duration") and counter == 2: + counter += 1 + return wrong_input2 + elif txt.lower().strip().startswith("did the duration") and counter == 3: + counter += 1 + return answer_duration_change + else: + print("No right string found") + + with patch.object(builtins, 'input', mock_input): + rd.check_team_number_relation(args_change, configpath) + + # read config values + config = configparser.ConfigParser() + config.read(configpath) + + # THEN + assert expected_team_number == config.get("settings", "team_number") diff --git a/workspace.code-workspace b/workspace.code-workspace new file mode 100644 index 00000000..ffe1703e --- /dev/null +++ b/workspace.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": ".." + }, + { + "path": "../../../python learning/commandlinescript" + } +], + "settings": {} +}