feat: add email service and appointment management module with configuration settings
This commit is contained in:
@@ -18,14 +18,6 @@ Collection Structure:
|
|||||||
- Optional fields: Images, Filter, Filter2, Filter3, Anschaffungsjahr, Anschaffungskosten, Code_4
|
- Optional fields: Images, Filter, Filter2, Filter3, Anschaffungsjahr, Anschaffungskosten, Code_4
|
||||||
- Status fields: Verfuegbar, User (if currently borrowed)
|
- 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
|
from bson.objectid import ObjectId
|
||||||
import datetime
|
import datetime
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
|
|||||||
@@ -166,32 +166,18 @@ def _get_int_env(name, default):
|
|||||||
def get_version():
|
def get_version():
|
||||||
# Prefer an explicit release marker if present (created by release process).
|
# Prefer an explicit release marker if present (created by release process).
|
||||||
project_root = os.path.abspath(os.path.join(BASE_DIR, '..', '..', '..'))
|
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:
|
try:
|
||||||
if os.path.isfile(release_file):
|
if os.path.isfile(release_file):
|
||||||
with open(release_file, 'r', encoding='utf-8') as f:
|
with open(release_file, 'r', encoding='utf-8') as f:
|
||||||
val = f.read().strip()
|
var = f.readline()
|
||||||
if val:
|
for i in var:
|
||||||
return val
|
i = i.split(":")
|
||||||
|
i.pop[0]
|
||||||
|
return str(i)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
# Expose settings
|
||||||
APP_VERSION = get_version()
|
APP_VERSION = get_version()
|
||||||
DEBUG = _get_bool_env('INVENTAR_DEBUG', _get(_conf, ['dbg'], DEFAULTS['debug']))
|
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_CERT = _get(_conf, ['ssl', 'cert'], DEFAULTS['ssl']['cert'])
|
||||||
SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key'])
|
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
|
||||||
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
|
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
|
||||||
SCHOOL_INFO_DEFAULT = _get(_conf, ['school'], DEFAULTS['school'])
|
SCHOOL_INFO_DEFAULT = _get(_conf, ['school'], DEFAULTS['school'])
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -1 +1 @@
|
|||||||
print("hello")
|
# Web.modules package initialization
|
||||||
|
|||||||
@@ -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()
|
||||||
+11
@@ -19,6 +19,17 @@
|
|||||||
"cert": "Web/certs/cert.pem",
|
"cert": "Web/certs/cert.pem",
|
||||||
"key": "Web/certs/key.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": {
|
"images": {
|
||||||
"thumbnail_size": [
|
"thumbnail_size": [
|
||||||
150,
|
150,
|
||||||
|
|||||||
Reference in New Issue
Block a user