Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

14 allow browsing of reports #25

Merged
merged 13 commits into from
Apr 20, 2023
Merged
Binary file added assets/browse_reports_dark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/browse_reports_light.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 6 additions & 8 deletions frames/AnydeskFrame.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def print_logs_to_textbox(self, log_filename_with_path: str):
if log_entries is not None:
self.textbox.insert("insert", f'Fetching logs from {log_filename_with_path}: \n\n')
if len(log_entries) < 1:
self.textbox.insert("insert", "No IP logs found inside file!")
self.textbox.insert("insert", "No IP logs found inside file! \n\n")
else:
for entry in log_entries:
self.textbox.insert("insert", entry + " - " + log_entries[entry] + "\n\n")
Expand Down Expand Up @@ -207,16 +207,14 @@ def search_filesystem_callback(self, search_location: str):
# Display a message if no files were found in search location
# Generate a report with a message if no files were found in search location
if number_of_found_files == 0:
self.after(500,
func=self.textbox.insert("insert", f'\n---- No files were found in {search_location}! ----\n\n'))
self.open_report_button.grid()
self.textbox.insert("insert", f'\n---- No files were found in {search_location}! ----\n\n')
with open(os.path.join(report_folder_path, "report.txt"), "a") as report_file:
report_file.write(f'---- No files were found in {search_location} ----\n\n')

# Display a message if search is finished
else:
self.after(500, self.textbox.insert("insert", "\n---- Searching for files finished! ----\n\n"))

self.open_report_button.grid()
self.open_report_button.grid()
self.textbox.insert("insert", "\n---- Searching for files finished! ----\n\n")

def generate_and_present_search_results(self):
"""A function that updates the textbox with new logs found by the search function
Expand All @@ -237,7 +235,7 @@ def generate_and_present_search_results(self):
except IndexError:
pass
if not search_finished:
self.after(500, self.generate_and_present_search_results)
self.after(200, self.generate_and_present_search_results)

@staticmethod
def turn_off_switches(switches_list: list[tkinter.BooleanVar]):
Expand Down
123 changes: 123 additions & 0 deletions frames/BrowseReportsFrame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import csv
import os
import customtkinter

from utils.file_operations import split_computer_datetime_dirname, get_reports_folder_list
from utils.widget_utils import add_widgets


class BrowseReportsFrame(customtkinter.CTkScrollableFrame):
"""Browse Reports frame class holding the list of generated reports."""

def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
self.configure(fg_color="transparent")
# add widgets onto the frame...
refresh(self)


class Report_Frame(customtkinter.CTkFrame):
"""A class representing a single report in the list of reports.
It shows the date, time and computer name of the report.
It also shows the number of files and IP addresses found in the report.
Button located in frame opens the report folder.
"""

def __init__(self, master, **kwargs):
super().__init__(master)
self.report_name = kwargs.get("report_name")
self.report_path = kwargs.get("report_path")
report_name_details = split_computer_datetime_dirname(self.report_name)
report_details = get_report_file_and_ip_numbers(report_path=self.report_path)
self.grid_columnconfigure(0, weight=1)

if report_name_details:
self.label = customtkinter.CTkLabel(self, text=report_name_details["date"] + " - " +
report_name_details["time"] + " - " + report_name_details[
"computer_name"],
text_color=("#333", "#ccc")
)
else:
self.label = customtkinter.CTkLabel(self, text=self.report_name,
text_color=("#333", "#ccc")
)
self.label.grid(row=0, column=0, sticky="ew", padx=20, pady=[10, 0])
self.label2 = customtkinter.CTkLabel(self, text="Files: " + str(report_details["number_of_files"]) + " - " +
"IP Addresses: " + str(
report_details["number_of_ip_addresses"]),
text_color=("#333", "#ccc")
)
self.label2.grid(row=0, column=1, sticky="ew", padx=20, pady=[10, 0])

self.report_button = Report_Button(self, report_name=self.report_name,
report_path=os.path.join(os.getcwd(), "REPORTS", self.report_path))


class Report_Button(customtkinter.CTkButton):
"""A class representing a button that opens the report folder."""

def __init__(self, master, **kwargs):
super().__init__(master)

self.report_name = kwargs.get("report_name")
self.report_path = kwargs.get("report_path")

self.configure(text='Open Report folder', command=self.open_report, fg_color=("gray75", "gray25"), text_color=(
"#333", "#ccc"), hover_color=("#6ca9d4", "#1c3b50"))
self.grid(row=1, column=0, columnspan=2, sticky="ew", padx=20, pady=10)

def open_report(self):
os.startfile(self.report_path)


def refresh(browse_reports_frame_instance: customtkinter.CTkScrollableFrame):
"""Function that refreshes the browse reports frame.
It deletes all widgets from the frame and adds new widgets.
It is used when new report is created so that the list of reports is updated.

:param browse_reports_frame_instance: browse reports frame instance that will be refreshed with new data
"""

# delete all widgets from frame
for widget in browse_reports_frame_instance.winfo_children():
widget.destroy()

reports_list = {}
# Get new list of reports and add dict entries representing a pair of: name of report and a frame with report
# details
for report in get_reports_folder_list():
reports_list[report] = Report_Frame(browse_reports_frame_instance, report_name=report,
report_path=os.path.join(os.getcwd(), "REPORTS", report))

# Add a top label to the list of reports
browse_reports_frame_instance.label = customtkinter.CTkLabel(browse_reports_frame_instance, text="Reports List",
text_color=("#333", "#ccc"),
font=customtkinter.CTkFont(size=15, weight="bold"))
browse_reports_frame_instance.label.grid(row=0, column=0, padx=20)
# Add all reports to the frame
add_widgets(reports_list, rowstart=1, columnstart=0, orientation="vertical")


def get_report_file_and_ip_numbers(report_path):
"""Function that returns the number of files and IP addresses found in the report by analyzing the report.csv file.

:param report_path: path to the report folder
:return: dict with number of files and IP addresses found in the report
"""

number_of_ip_addresses = 0
found_files = set()
try:
with open(os.path.join(report_path, 'report.csv'), 'r') as file:
reader = csv.reader(file)
next(reader, None)
for row in reader:
if not row[0] == "No Anydesk logs found!":
number_of_ip_addresses += 1
found_files.add(row[2])
except FileNotFoundError:
pass

return {"number_of_ip_addresses": number_of_ip_addresses, "number_of_files": len(found_files)}
28 changes: 28 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from PIL import Image

from frames.AnydeskFrame import AnydeskFrame
from frames.BrowseReportsFrame import BrowseReportsFrame, refresh
from frames.HomeFrame import HomeFrame

customtkinter.set_appearance_mode("System")
Expand Down Expand Up @@ -43,6 +44,11 @@ def __init__(self):
dark_image=Image.open(os.path.join(image_path, "anydesk_dark.png")),
size=(20, 20))

self.browse_reports_image = customtkinter.CTkImage(
light_image=Image.open(os.path.join(image_path, "browse_reports_light.png")),
dark_image=Image.open(os.path.join(image_path, "browse_reports_dark.png")),
size=(20, 20))

# create navigation frame
self.navigation_frame = customtkinter.CTkFrame(self, corner_radius=0)
self.navigation_frame.grid(row=0, column=0, sticky="nsew")
Expand Down Expand Up @@ -70,6 +76,14 @@ def __init__(self):
command=self.frame_2_button_event)
self.frame_2_button.grid(row=2, column=0, sticky="ew")

self.browse_reports_frame_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40,
border_spacing=10, text="Browse Reports",
fg_color="transparent", text_color=("#333", "#ccc"),
hover_color=("gray70", "gray30"),
image=self.browse_reports_image, anchor="w",
command=self.browse_reports_frame_button_event)
self.browse_reports_frame_button.grid(row=5, column=0, sticky="ew")

self.appearance_mode_menu = customtkinter.CTkOptionMenu(self.navigation_frame,
text_color=("#eee", "#ccc"),
values=["Light", "Dark", "System"],
Expand All @@ -83,6 +97,9 @@ def __init__(self):
# create anydesk frame
self.anydesk_frame = AnydeskFrame(self, corner_radius=0, fg_color="transparent")

# create browse reports frame
self.browse_reports_frame = BrowseReportsFrame(self, corner_radius=0, fg_color="transparent")

# select default frame
self.select_frame_by_name("home")

Expand All @@ -91,6 +108,8 @@ def select_frame_by_name(self, name):
# set button color for selected button
self.home_button.configure(fg_color=("gray75", "gray25") if name == "home" else "transparent")
self.frame_2_button.configure(fg_color=("gray75", "gray25") if name == "anydesk_frame" else "transparent")
self.browse_reports_frame_button.configure(
fg_color=("gray75", "gray25") if name == "browse_reports_frame" else "transparent")

# show selected frame
if name == "home":
Expand All @@ -101,6 +120,10 @@ def select_frame_by_name(self, name):
self.anydesk_frame.grid(row=0, column=1, sticky="nsew")
else:
self.anydesk_frame.grid_forget()
if name == "browse_reports_frame":
self.browse_reports_frame.grid(row=0, column=1, sticky="nsew")
else:
self.browse_reports_frame.grid_forget()

def home_button_event(self):
"""Home button event handler."""
Expand All @@ -110,6 +133,11 @@ def frame_2_button_event(self):
"""Frame 2 button event handler."""
self.select_frame_by_name("anydesk_frame")

def browse_reports_frame_button_event(self):
"""Browse Reports button event handler."""
self.select_frame_by_name("browse_reports_frame")
refresh(browse_reports_frame_instance=self.browse_reports_frame)


if __name__ == "__main__":
app = App()
Expand Down
50 changes: 48 additions & 2 deletions tests/test_file_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import hashlib
import shutil
from datetime import datetime

from utils.file_operations import get_anydesk_logs, create_timestamped_directory, create_folders_from_path, \
generate_md5_file_checksum, copy_and_generate_checksum, generate_txt_report, generate_csv_report
generate_md5_file_checksum, copy_and_generate_checksum, generate_txt_report, generate_csv_report, \
split_computer_datetime_dirname, get_reports_folder_list
import os
import re
import sys
Expand Down Expand Up @@ -126,7 +128,7 @@ def test_generate_md5_file_checksum(self):
assert generate_md5_file_checksum(test_file) == test_file_hash
os.remove('test_file.txt')

def test_copy_and_generate_checksum(self):
def test_copy_and_generate_checksum(self, capsys):
test_file = TestGetAnydeskLogs.create_empty_file('test_file.txt')
test_file_hash = hashlib.md5(open('test_file.txt', 'rb').read()).hexdigest()
cwd = os.getcwd()
Expand All @@ -139,6 +141,13 @@ def test_copy_and_generate_checksum(self):
shutil.rmtree(os.path.join(os.getcwd(), 'REPORTS'))
os.remove('test_file.txt')

# Test IOError
nonexistent_file = 'nonexistent_file.txt'
path_to_nonexistent_file = os.path.join(os.getcwd(), nonexistent_file)
copy_and_generate_checksum(path_to_nonexistent_file, os.getcwd())
captured_error_print = capsys.readouterr()
assert captured_error_print.out == 'Error occurred when trying to copy\n'


class TestGeneratingTextReport:
def test_generate_txt_report_with_header(self):
Expand Down Expand Up @@ -230,3 +239,40 @@ def test_generate_empty_csv_report(self):
# assert
assert actual_output == expected_output
os.remove(os.path.join(report_directory_path, 'report.csv'))


def test_split_computer_datetime_dirname():
assert split_computer_datetime_dirname('computer_11-03-2023_19-09-48') == {'computer_name': 'computer',
'date': '11-03-2023', 'time': '19:09:48'}

assert split_computer_datetime_dirname('test_computer--203;lk;asdfj_11-03-2023_19-09-48') == {
'computer_name': 'test_computer--203;lk;asdfj',
'date': '11-03-2023', 'time': '19:09:48'}

assert split_computer_datetime_dirname('1234567890_11-03-2023_19-09-48') == {
'computer_name': '1234567890',
'date': '11-03-2023', 'time': '19:09:48'}

assert split_computer_datetime_dirname('__11-03-2023_19-09-48') == {'computer_name': '_',
'date': '11-03-2023', 'time': '19:09:48'}

assert split_computer_datetime_dirname('asfdfasd') is None
assert split_computer_datetime_dirname('asfdfasd_11-03-2023_12-dd-22') is None
assert split_computer_datetime_dirname('asfdfasd_11-03-2023_bb-dd-ee') is None


def test_get_reports_folder_list(tmp_path):
reports_folder = os.path.join(tmp_path, 'REPORTS')
# Assert that the reports folder does not exist
assert not os.path.isdir(reports_folder)
# Assert that after running the function the reports folder is created and is empty
assert get_reports_folder_list(reports_folder) == []
# Assert that the reports folder exists
assert os.path.isdir(reports_folder)
assert os.path.exists(os.path.join(tmp_path, 'REPORTS'))
os.mkdir(os.path.join(reports_folder, 'computer_11-03-2023_19-09-48'))
os.mkdir(os.path.join(reports_folder, 'computer_11-03-2023_19-09-49'))
os.mkdir(os.path.join(reports_folder, 'computer_11-03-2023_19-09-50'))
assert sorted(get_reports_folder_list(reports_folder)) == sorted(['computer_11-03-2023_19-09-48',
'computer_11-03-2023_19-09-49',
'computer_11-03-2023_19-09-50'])
27 changes: 25 additions & 2 deletions utils/file_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ def generate_md5_file_checksum(filename: str) -> str:
file_hash = hashlib.md5()
while chunk := f.read(8192):
file_hash.update(chunk)
# print(file_hash.digest())
# print(file_hash.hexdigest()) # to get a printable str instead of bytes
return file_hash.hexdigest()


Expand Down Expand Up @@ -155,3 +153,28 @@ def generate_csv_report(report_directory_path: str, write_header: bool = True, a
else:
for entry in anydesk_logs_dict:
writer.writerow([entry, anydesk_logs_dict[entry], filename])


def split_computer_datetime_dirname(dirname):
"""A function that splits a generated directory name into computer name, date and time"""
result = re.search(r"(.*)_(\d{2}-\d{2}-\d{4})_(\d{2}-\d{2}-\d{2}$)", dirname)
if result:
return {
"computer_name": result.group(1),
"date": result.group(2),
"time": result.group(3).replace("-", ":")
}
else:
return None


def get_reports_folder_list(reports_directory='REPORTS'):
"""A function that returns a list of all directories in the current working directory/reports_directory folder
If the folder does not exist, it will be created
:param reports_directory: a name of reports folder inside current working directory
"""
try:
return os.listdir(os.path.join(os.getcwd(), reports_directory))
except FileNotFoundError:
os.mkdir(os.path.join(os.getcwd(), reports_directory))
return os.listdir(os.path.join(os.getcwd(), reports_directory))
17 changes: 17 additions & 0 deletions utils/widget_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Literal


def add_widgets(widget_dict, rowstart: int = 0, columnstart: int = 0, orientation: Literal["vertical", "horizontal"] =
"vertical"):
""" A function that adds widgets to a frame.
:param widget_dict: A dictionary containing the widgets to be added.
:param rowstart: The row number where the widgets will be added.
:param columnstart: The column number where the widgets will be added.
:param orientation: The orientation of the widgets. "vertical" or "horizontal".
"""

for i, (key, value) in enumerate(widget_dict.items()):
if orientation == "vertical":
value.grid(row=rowstart + i, column=columnstart, sticky="ew", padx=20, pady=10)
elif orientation == "horizontal":
value.grid(row=rowstart + i, column=columnstart + i, sticky="ew", padx=20, pady=10)