diff --git a/Web/modules/database/items.py b/Web/modules/database/items.py index 609d177..9dbf02b 100755 --- a/Web/modules/database/items.py +++ b/Web/modules/database/items.py @@ -18,14 +18,6 @@ Collection Structure: - Optional fields: Images, Filter, Filter2, Filter3, Anschaffungsjahr, Anschaffungskosten, Code_4 - Status fields: Verfuegbar, User (if currently borrowed) """ -''' - Copyright 2025-2026 AIIrondev - - Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag). - See Legal/LICENSE for the full license text. - Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited. - For commercial licensing inquiries: https://github.com/AIIrondev -''' from bson.objectid import ObjectId import datetime import Web.modules.database.settings as cfg diff --git a/Web/modules/database/settings.py b/Web/modules/database/settings.py index 2138b0f..2ca2675 100644 --- a/Web/modules/database/settings.py +++ b/Web/modules/database/settings.py @@ -166,32 +166,18 @@ def _get_int_env(name, default): def get_version(): # Prefer an explicit release marker if present (created by release process). project_root = os.path.abspath(os.path.join(BASE_DIR, '..', '..', '..')) - release_file = os.path.join(project_root, '.release-version') + release_file = os.path.join(project_root, '.docker-build.env') try: if os.path.isfile(release_file): with open(release_file, 'r', encoding='utf-8') as f: - val = f.read().strip() - if val: - return val + var = f.readline() + for i in var: + i = i.split(":") + i.pop[0] + return str(i) except Exception: pass - # Fallback to .docker-build.env (legacy behaviour) - env_path = os.path.join(project_root, '.docker-build.env') - try: - with open(env_path, 'r', encoding='utf-8') as f: - for l in f: - if l.startswith('INVENTAR_APP_IMAGE='): - return l.split(':', 1)[1].strip() - except Exception: - pass - - # Final fallback: use config.json value or packaged default - try: - return _get(_conf, ['ver'], DEFAULTS.get('version', '0.0.0')) - except Exception: - return DEFAULTS.get('version', '0.0.0') - # Expose settings APP_VERSION = get_version() DEBUG = _get_bool_env('INVENTAR_DEBUG', _get(_conf, ['dbg'], DEFAULTS['debug'])) @@ -230,6 +216,17 @@ SSL_ENABLED = _get(_conf, ['ssl', 'enabled'], DEFAULTS['ssl']['enabled']) SSL_CERT = _get(_conf, ['ssl', 'cert'], DEFAULTS['ssl']['cert']) SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key']) +# Email settings +EMAIL_ENABLED = _get(_conf, ['email', 'enabled'], False) +EMAIL_SMTP_HOST = _get(_conf, ['email', 'smtp_host'], 'smtp.gmail.com') +EMAIL_SMTP_PORT = int(_get(_conf, ['email', 'smtp_port'], 587)) +EMAIL_USE_TLS = bool(_get(_conf, ['email', 'use_tls'], True)) +EMAIL_USERNAME = _get(_conf, ['email', 'username'], '') +EMAIL_PASSWORD = _get(_conf, ['email', 'password'], '') +EMAIL_FROM_ADDRESS = _get(_conf, ['email', 'from_address'], EMAIL_USERNAME) +EMAIL_DEFAULT_SENDER_NAME = _get(_conf, ['email', 'default_sender_name'], 'Inventarsystem') +EMAIL_TIMEOUT_SECONDS = int(_get(_conf, ['email', 'timeout_seconds'], 30)) + # School periods SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods']) SCHOOL_INFO_DEFAULT = _get(_conf, ['school'], DEFAULTS['school']) diff --git a/Web/modules/database/termine.py b/Web/modules/database/termine.py new file mode 100644 index 0000000..576c74d --- /dev/null +++ b/Web/modules/database/termine.py @@ -0,0 +1,110 @@ +""" +Apointment Managment +========================= + +This module manages appointments in the database. It provides comprehensive +functionality for creating, updating, retrieving appointments items. + +Key Features: +- Creating and updating appointments +- Retrieving items by ID +- Managing time slots +- client retrival + +Collection Structure: +- appointments: + - Required fields: user, start_date, end_date, daytime, slots, slot_time + - Optional fields: Images, Filter, Filter2, Filter3, Anschaffungsjahr, Anschaffungskosten, Code_4 + - Status fields: slots_used_by +""" +import Web.modules.database.settings as cfg +from Web.modules.database.settings import MongoClient +from bson.objectid import ObjectId +import datetime + +def _active_record_query(extra_query=None): + """Build a query that excludes logically deleted records.""" + base_query = {'Deleted': {'$ne': True}} + if extra_query: + base_query.update(extra_query) + return base_query + + +def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str=""): + try: + client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + db = client[cfg.MONGODB_DB] + items = db['appointments'] + + item = { + 'date_start': date_start, + 'date_end': date_end, + 'time_span': time_span, + 'slots': slots, + 'slot_lenght': slot_lenght, + 'user': user, + 'mail': mail, + 'note': note, + 'slots_booked': [], # -> [(start_time, name), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable + 'Created': datetime.datetime.now(), + 'LastUpdated': datetime.datetime.now() + } + result = items.insert_one(item) + return result.inserted_id + except Exception as e: + print(f"Exception accured: {e}") + + +def get_item(id): + """ + Retrieve a specific appointment by its ID. + + Args: + id (str): ID of the appointsment to retrieve + + Returns: + dict: The appointment document or None if not found + """ + try: + client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + db = client[cfg.MONGODB_DB] + items = db['appointments'] + item = items.find_one(_active_record_query({'_id': ObjectId(id)})) + client.close() + return item + except Exception as e: + print(f"Error retrieving item: {e}") + return None + +def update(id,slots_used: list): + """ + Update an existing appointment. + + Args: + id (str): ID of the item to update + + + Returns: + bool: True if successful, False otherwise + """ + try: + client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + db = client[cfg.MONGODB_DB] + items = db['appointments'] + + update_data = { + 'slots_booked': [slots_used], + 'LastUpdated': datetime.datetime.now() + } + + result = items.update_one( + {'_id': ObjectId(id)}, + {'$set': update_data} + ) + + client.close() + return result.modified_count > 0 + except Exception as e: + print(f"Error updating item: {e}") + return False + diff --git a/Web/modules/emailservice/email.py b/Web/modules/emailservice/email.py new file mode 100644 index 0000000..ffbf92f --- /dev/null +++ b/Web/modules/emailservice/email.py @@ -0,0 +1,47 @@ +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import smtplib + +import Web.modules.database.settings as cfg + + +def _build_smtp_client(): + smtp = smtplib.SMTP(cfg.EMAIL_SMTP_HOST, cfg.EMAIL_SMTP_PORT, timeout=cfg.EMAIL_TIMEOUT_SECONDS) + smtp.ehlo() + if cfg.EMAIL_USE_TLS: + smtp.starttls() + smtp.ehlo() + if cfg.EMAIL_USERNAME: + smtp.login(cfg.EMAIL_USERNAME, cfg.EMAIL_PASSWORD or '') + return smtp + +def send(email: list, subject: str, note: str, sender: str) -> bool: + """ + Sends the email with the link to the Clients + + Input: + - email: Email list of all the addresses to send the link to ["","",""] + - subject: Subject of the email + - note: Note that is send with the Emails + + Output: + - bool: true if the sending worked and false if it didnt + """ + msg = MIMEMultipart() + msg['Subject'] = subject + msg['From'] = sender or cfg.EMAIL_FROM_ADDRESS or cfg.EMAIL_USERNAME + msg['To'] = ', '.join(email) if isinstance(email, (list, tuple)) else str(email) + msg.attach(MIMEText(note)) + smtp = None + try: + smtp = _build_smtp_client() + smtp.sendmail(from_addr=msg['From'], to_addrs=email, msg=msg.as_string()) + return True + except Exception: + return False + finally: + try: + if smtp: + smtp.quit() + except Exception: + pass \ No newline at end of file diff --git a/Web/modules/terminplaner/__init__.py b/Web/modules/terminplaner/__init__.py index ce47b77..9bda7d5 100644 --- a/Web/modules/terminplaner/__init__.py +++ b/Web/modules/terminplaner/__init__.py @@ -1 +1 @@ -print("hello") \ No newline at end of file +# Web.modules package initialization diff --git a/Web/modules/terminplaner/executive.py b/Web/modules/terminplaner/executive.py new file mode 100644 index 0000000..b0c73fe --- /dev/null +++ b/Web/modules/terminplaner/executive.py @@ -0,0 +1,43 @@ +""" +Class for all funktions of the executive -> Lehrer +""" +import datetime +import emailservice.email as mail_service +import Web.modules.database.termine as termin +import Web.modules.database.settings as cfg +from tenant import get_tenant_context + +def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="") -> str: + """ + Generates a link for the executive to send to his clients to book a time Slot + + Input: + - date_start: start of the time frae area + - date_end: end of the time frame area + - time_span: Time window for the days as a list [(first day Time Frame), (second day Time frame), (third etc.)] + - slots: amount of slots that are available + - slot_lenght: the lenght of a slot in minutes + + Output: + - link: The link for the user to send to the clients + """ + id = termin.add(date_start, date_end, time_span, slots, slot_lenght, user, mail, note) + + tenant_context = get_tenant_context() + subdomain = '' + if tenant_context: + subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or '' + + host = f"https://{subdomain}.invario.eu" if subdomain else "invario.eu" + link = host + "/terminplaner/client" + "?" + "client_id=" + id + subject = f"Terminanfrage von {user}" + note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}" + mail_service.send(mail, subject, note_link) + return link + + +def book_slot(id, date, start_time): + """ + + """ + termin.update() \ No newline at end of file diff --git a/config.json b/config.json index 5ebba49..18278f0 100755 --- a/config.json +++ b/config.json @@ -19,6 +19,17 @@ "cert": "Web/certs/cert.pem", "key": "Web/certs/key.pem" }, + "email": { + "enabled": false, + "smtp_host": "smtp.gmail.com", + "smtp_port": 587, + "use_tls": true, + "username": "", + "password": "", + "from_address": "", + "default_sender_name": "Invario Inventurprogramm", + "timeout_seconds": 30 + }, "images": { "thumbnail_size": [ 150,