diff --git a/src/homeutils.py b/src/homeutils.py index f0ef0fc..b3adcc6 100644 --- a/src/homeutils.py +++ b/src/homeutils.py @@ -3,15 +3,15 @@ import sqlite3 import datetime from datetime import timedelta from collections import defaultdict -from sqlite_handler import get_current_week_grades +from sqlitehandlernr import fetch_grades_this_week from i18n import _ def format_grade(grade): """ Format a single grade into a readable string. """ - # For simple grade display - return str(grade['value']) + # Access the grade value as an attribute, not as a dictionary key + return str(grade.value) def RecentGradesColumn(db_path="grades.db"): """ @@ -27,12 +27,12 @@ def RecentGradesColumn(db_path="grades.db"): def load_grades(e=None): try: # Get grades from the current week - grades_list = get_current_week_grades(db_path) + grades_list = fetch_grades_this_week() # Group by subject grades_by_subject = defaultdict(list) for grade in grades_list: - subject = grade['subject'] + subject = grade.subject grades_by_subject[subject].append(grade) # Prepare the list of controls @@ -49,7 +49,6 @@ def RecentGradesColumn(db_path="grades.db"): for subject, grades in grades_by_subject.items(): # Format grades as a space-separated string formatted_grades = [format_grade(grade) for grade in grades] - grades_str = " ".join(formatted_grades) # Create a row for each subject subject_row = ft.Row([ @@ -62,7 +61,7 @@ def RecentGradesColumn(db_path="grades.db"): ), *[ ft.Container( - content=ft.Text(grade_val,color="#FFFFFF",size=16,text_align=ft.TextAlign.CENTER,width=30), + content=ft.Text(grade_val, color="#FFFFFF", size=16, text_align=ft.TextAlign.CENTER, width=30), expand=False, bgcolor=ft.Colors.with_opacity(0.3, ft.Colors.BLACK), width=30, @@ -103,7 +102,7 @@ def RecentGradesColumn(db_path="grades.db"): # Create the header section like in the image header = ft.Row([ ft.Icon(ft.Icons.FILTER_6, size=32, color="#FFFFFF"), - ft.Text((_("Recent Grades")), size=24, font_family="Roboto", weight="bold") + ft.Text((_("Recent Grades")), size=24, font_family="Roboto", weight="bold", color="#FFFFFF") ], spacing=12, alignment=ft.MainAxisAlignment.START) # Create the column with header and content diff --git a/src/main.py b/src/main.py index 8cfeef8..ffd632b 100644 --- a/src/main.py +++ b/src/main.py @@ -13,7 +13,7 @@ from pages.exams import * from pages.grades import * from pages.homework import * from pages.settings import * -from sqlite_handler import * +from sqlitehandlernr import * from pages.timetable import * from pages.behaviour import * from pages.attendance import * diff --git a/src/pages/grades.py b/src/pages/grades.py index 6959a75..c9166c3 100644 --- a/src/pages/grades.py +++ b/src/pages/grades.py @@ -1,86 +1,150 @@ import flet as ft +from sqlitehandlernr import fetch_all_grades from i18n import _ +def parse_grade_value(value): + """ + Parse grade value with optional + or - suffix. + Args: + value (str): Grade value as a string + + Returns: + float: Parsed grade value, or None if unrecognized + """ + try: + base_value = float(value.rstrip('+-')) # Remove +/- suffix and convert to float + if value.endswith('+'): + return base_value + 0.25 # Add 0.25 for "+" suffix + elif value.endswith('-'): + return base_value - 0.25 # Subtract 0.25 for "-" suffix + return base_value + except ValueError: + return None # Ignore unrecognized values def GradesPage(page): + """ + Generate grades page with subject-based panels and expandable content + + Args: + page (flet.Page): Flet page instance + """ + + # Retrieve all grades from SQLite database + grades = fetch_all_grades() + + # Create modal dialog for displaying grade details modal = ft.AlertDialog( modal=True, - title=ft.Text((_("Test modal"))), - content=ft.Placeholder(), + title=ft.Text(_("Test modal")), # Set modal title with translated text + content=ft.Text(""), # Placeholder text that will be updated dynamically actions=[ - ft.TextButton("OK", on_click=lambda e: page.close(modal)), + ft.TextButton("OK", on_click=lambda e: page.close(modal)), # Close modal when OK button clicked ], actions_alignment=ft.MainAxisAlignment.END, ) - header = ft.Container( - content=ft.Column( - [ - ft.Text("Example Subject", size=15), - ft.Row([ - ft.Text("2 "+(_("grades")), size=14), - ft.Text((_("Average"))+": 5.00", size=14), - ]) - ], - alignment=ft.MainAxisAlignment.CENTER, - horizontal_alignment=ft.CrossAxisAlignment.START, - expand=True, - ), - padding=ft.padding.all(10), - expand=True, - ) + # Initialize subject-based panels and data storage + panels = [] + subjects = {} - panel = ft.ExpansionPanel( - header=header, - content=ft.Column([ - ft.Row([ + # Group grades by subject + for grade in grades: + subject = grade.subject + if subject not in subjects: # Create new subject entry if not already present + subjects[subject] = [] + subjects[subject].append(grade) # Add grade to corresponding subject list + + def format_date(dt): + """Format a datetime object to YYYY-MM-DD string""" + return dt.strftime("%Y-%m-%d") + + def open_grade_modal(grade): + """ + Open modal dialog with detailed grade information + + Args: + grade (dict): Grade dictionary containing details + """ + modal.title = ft.Text(_("Grade Details")) # Update modal title + parsed_value = parse_grade_value(grade.value) # Parse grade value for display + + # Fixed line - don't use replace with None + grade_text = f"{_('Grade')}: {grade.value}" + if parsed_value is None: + grade_text = f"{_('Grade')}: {_('Grade not recognized')}" + + modal.content = ft.Column([ + ft.Text(f"{_('Subject')}: {grade.subject}", size=14), # Display subject and translated text + ft.Text(grade_text, size=14), + ft.Text(f"{_('Date')}: {format_date(grade.created_at)}", size=14), # Format datetime object + ft.Text(f"{_('Weight')}: {getattr(grade, 'weight', 1.0)}", size=14), + ft.Text(f"{_('Description')}: {grade.name}", size=14), + ft.Text(f"{_('Creator')}: {grade.creator}", size=14) + ], expand=False) + page.open(modal) # Open modal dialog + + # Generate subject-based panels with expandable content + for subject, grades_list in subjects.items(): + header = ft.Container( + content=ft.Column( + [ + ft.Text(subject, size=15), # Display subject name + ft.Row([ + ft.Text(f"{len(grades_list)} "+_("grades"), size=14), # Display number of grades and translated text + ft.Text((_("Average"))+": {:.2f}".format( + sum(parse_grade_value(g.value) for g in grades_list if parse_grade_value(g.value) is not None) / max(len([g for g in grades_list if parse_grade_value(g.value) is not None]), 1) + ), size=14), # Calculate and display average grade + ]) + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.START, + expand=True, + ), + padding=ft.padding.all(10), + expand=True, + ) + + grade_rows = [] + for grade in grades_list: + grade_rows.append( ft.Container( - content=ft.Text("5", text_align=ft.TextAlign.CENTER), - bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, - padding=10, - border_radius=5, - width=40, - margin=ft.margin.all(5), - on_click=lambda e: page.open(modal) - ), - ft.Column( - [ - ft.Text("Example grade"), - ft.Row([ - ft.Text("21.02.2025"), - ft.Text((_("Weight"))+": 1.0"), - ]) - ], - spacing=1 - ), - ]), - ft.Row([ - ft.Container( - content=ft.Text("5", text_align=ft.TextAlign.CENTER), - bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, - padding=10, - border_radius=5, - width=40, - margin=ft.margin.all(5), - on_click=lambda e: page.open(modal) - ), - ft.Column( - [ - ft.Text("Example grade"), - ft.Row([ - ft.Text("21.02.2025"), - ft.Text((_("Weight"))+": 1.0"), - ]) - ], - spacing=1 - ), - ]), - ]), - expand=False, - ) + content=ft.Row([ + ft.Container( + content=ft.Text(grade.value, text_align=ft.TextAlign.CENTER), # Display grade value + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, + padding=ft.padding.symmetric(horizontal=12, vertical=8), + border_radius=5, + margin=ft.margin.all(5), + width=None, # Allow the container to size based on content + on_click=lambda e, grade=grade: open_grade_modal(grade), # Open modal dialog when row clicked + ), + ft.Column( + [ + ft.Text(grade.name), # Display grade name + ft.Row([ + ft.Text(format_date(grade.created_at)), # Format datetime object + ft.Text((_("Weight"))+": {:.1f}".format(getattr(grade, 'weight', 1.0))) + ]) + ], + spacing=1 + ), + ], expand=True), # Allow Row to expand and avoid tight squeezing + on_click=lambda e, grade=grade: open_grade_modal(grade), + ) + ) + + panel = ft.ExpansionPanel( + header=header, + content=ft.Column(grade_rows), + expand=False, + #bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, + #can_tap_header=True, + ) + panels.append(panel) return ft.Column([ ft.Text((_("Grades")), size=30, weight="bold"), - ft.ExpansionPanelList([panel]), - ft.ExpansionPanelList([panel]) - ]) \ No newline at end of file + ft.ExpansionPanelList(panels) # Display subject-based panels with expandable content + ], + scroll=True + ) \ No newline at end of file diff --git a/src/sqlite_handler.py b/src/sqlite_handler.py deleted file mode 100644 index 7e0c0c8..0000000 --- a/src/sqlite_handler.py +++ /dev/null @@ -1,115 +0,0 @@ -import sqlite3 -import datetime -import re -from datetime import timedelta - -def create_grades_database(grades_list, db_path="grades.db"): - """ - Create a SQLite database from a list of Grade objects. - - Args: - grades_list (list or str): List of Grade objects or string representation of the list - db_path (str): Path to the SQLite database file - - Returns: - bool: True if database was created successfully - """ - # Connect to SQLite database (will create it if it doesn't exist) - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Create grades table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS grades ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - value TEXT NOT NULL, - is_point BOOLEAN NOT NULL, - point_numerator INTEGER, - point_denominator INTEGER, - weight REAL NOT NULL, - name TEXT NOT NULL, - created_at TIMESTAMP NOT NULL, - subject TEXT NOT NULL, - creator TEXT NOT NULL - ) - ''') - - # Clear existing data if table exists - cursor.execute('DELETE FROM grades') - - # Define regex pattern to extract grade information - pattern = r"Grade\(value='(.*?)', is_point=(.*?), point_numerator=(.*?), point_denominator=(.*?), weight=(.*?), name='(.*?)', created_at=datetime\.datetime\((.*?)\), subject='(.*?)', creator='(.*?)'\)" - - # Convert to string if it's not already - grades_str = str(grades_list) - - # Find all grades in the input string - matches = re.finditer(pattern, grades_str) - - for match in matches: - value = match.group(1) - is_point = match.group(2).lower() == 'true' - point_numerator = None if match.group(3) == 'None' else int(match.group(3)) - point_denominator = None if match.group(4) == 'None' else int(match.group(4)) - weight = float(match.group(5)) - name = match.group(6) - - # Parse datetime components - datetime_parts = match.group(7).split(', ') - year = int(datetime_parts[0]) - month = int(datetime_parts[1]) - day = int(datetime_parts[2]) - hour = int(datetime_parts[3]) - minute = int(datetime_parts[4]) - second = int(datetime_parts[5]) - microsecond = int(datetime_parts[6]) if len(datetime_parts) > 6 else 0 - - created_at = datetime.datetime(year, month, day, hour, minute, second, microsecond) - subject = match.group(8) - creator = match.group(9) - - # Insert data into the database - cursor.execute(''' - INSERT INTO grades (value, is_point, point_numerator, point_denominator, weight, name, created_at, subject, creator) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (value, is_point, point_numerator, point_denominator, weight, name, created_at, subject, creator)) - - # Commit changes and close connection - conn.commit() - conn.close() - - print(f"Database created successfully with data from {len(list(re.finditer(pattern, grades_str)))} grades") - return True - -def get_current_week_grades(db_path="grades.db"): - """ - Get all grades from the current week (Monday to Sunday). - """ - # Connect to SQLite database - conn = sqlite3.connect(db_path) - - # Configure connection to return dictionary-like objects - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - - # Calculate the start and end of the current week - today = datetime.datetime.now() - start_of_week = today - timedelta(days=today.weekday()) - start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0) - end_of_week = start_of_week + timedelta(days=6, hours=23, minutes=59, seconds=59) - - # Query to get grades from the current week - cursor.execute(''' - SELECT id, subject, name, value, is_point, point_numerator, point_denominator, created_at - FROM grades - WHERE created_at BETWEEN ? AND ? - ORDER BY subject, created_at - ''', (start_of_week, end_of_week)) - - # Convert cursor results to dictionaries - grades = [dict(row) for row in cursor.fetchall()] - - # Close the connection - conn.close() - - return grades \ No newline at end of file diff --git a/src/sqlitehandlernr.py b/src/sqlitehandlernr.py new file mode 100644 index 0000000..e52802e --- /dev/null +++ b/src/sqlitehandlernr.py @@ -0,0 +1,57 @@ +from sqlmodel import * +from datetime import datetime, timedelta +from sqlalchemy import func +from typing import Optional + +class Grades(SQLModel, table=True): + id: int = Field(default=None, primary_key=True) # Add an auto-incrementing id + value: str # value is no longer a primary key + is_point: bool + point_numerator: Optional[float] = None + point_denominator: Optional[float] = None + weight: float + name: str + created_at: datetime + subject: str + creator: str + +engine = create_engine("sqlite:///database.db") +SQLModel.metadata.create_all(engine) + +def create_grades_database(grades_list): + with Session(engine) as session: + session.execute(delete(Grades)) + for grade in grades_list: + # Format the created_at to the desired format + formatted_time = grade.created_at.strftime('%d/%m/%Y %H:%M:%S') + + grade_obj = Grades( + value=grade.value, + is_point=grade.is_point, + point_numerator=grade.point_numerator, + point_denominator=grade.point_denominator, + weight=grade.weight, + name=grade.name, + created_at=grade.created_at, + subject=grade.subject, + creator=grade.creator, + ) + + session.add(grade_obj) + session.commit() + +def fetch_grades_this_week(): + today = datetime.today() + start_of_week = today - timedelta(days=today.weekday()) + end_of_week = start_of_week + timedelta(days=7) + + with Session(engine) as session: + grades = session.query(Grades).filter(Grades.created_at >= start_of_week, Grades.created_at < end_of_week).all() + + return grades + +def fetch_all_grades(): + with Session(engine) as session: + grades = session.query(Grades).all() + + return grades