Compare commits

...

9 Commits

10 changed files with 520 additions and 106 deletions
+1 -1
View File
@@ -1 +1 @@
v0.8.1
v0.8.4
+240 -75
View File
@@ -74,6 +74,7 @@ import mimetypes
import subprocess
from Web.modules.inventarsystem.data_protection import (
decrypt_document_fields,
decrypt_text,
encrypt_document_fields,
encrypt_soft_deleted_media_pack,
)
@@ -154,6 +155,119 @@ def _decrypt_student_card_doc(card_doc):
return card_doc
return decrypt_document_fields(card_doc, STUDENT_CARD_ENCRYPTED_FIELDS)
def _parse_and_increment_class(klass, max_class=13, graduate_label=''):
"""Parse a class string like '7A' or '10' and increment the numeric part.
If the numeric part would exceed max_class, return graduate_label (or empty).
Leaves non-numeric / unparsable values unchanged.
"""
if not klass:
return klass
s = str(klass).strip()
m = re.match(r"^\s*(\d{1,2})(\D.*)?$", s)
if not m:
return s
try:
num = int(m.group(1))
except Exception:
return s
suffix = m.group(2) or ''
new_num = num + 1
if max_class is not None and new_num > int(max_class):
return graduate_label or ''
return f"{new_num}{suffix}"
def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_label=''):
"""Increment class years on all student cards.
Returns a summary dict with counts.
"""
client = None
updated = 0
examined = 0
failures = 0
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
col = db['student_cards']
now = datetime.datetime.now()
cursor = list(col.find({}, {'Klasse': 1}))
for doc in cursor:
examined += 1
try:
# Decrypt to work on plain class string
plain = dict(doc)
plain = _decrypt_student_card_doc(plain)
orig = (plain.get('Klasse') or '').strip()
new_class = _parse_and_increment_class(orig, max_class=max_class, graduate_label=graduate_label)
if new_class != orig:
if dry_run:
updated += 1
continue
payload = {'Klasse': new_class}
encrypted = encrypt_document_fields(payload, STUDENT_CARD_ENCRYPTED_FIELDS)
update_doc = {'Klasse': encrypted.get('Klasse'), 'Aktualisiert': now}
col.update_one({'_id': doc.get('_id')}, {'$set': update_doc})
updated += 1
except Exception:
failures += 1
app.logger.exception('Failed to process student card rollover for %s', doc.get('_id'))
finally:
if client:
client.close()
summary = {'examined': examined, 'updated': updated, 'failures': failures, 'dry_run': bool(dry_run)}
try:
_append_audit_event_standalone('student_cards_rollover', summary)
except Exception:
app.logger.warning('Audit write failed for student_cards_rollover')
return summary
# Admin route to trigger rollover manually
@app.route('/admin/trigger_school_year_rollover', methods=['POST'])
def admin_trigger_school_year_rollover():
if 'username' not in session:
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
if not us.check_admin(session['username']):
return jsonify({'ok': False, 'message': 'Administratorrechte erforderlich.'}), 403
max_class = int(os.getenv('INVENTAR_SCHOOL_MAX_CLASS', '13'))
graduate_label = os.getenv('INVENTAR_SCHOOL_GRADUATE_LABEL', '')
dry = request.args.get('dry', '0') in ('1', 'true', 'yes')
summary = rollover_student_card_classes(dry_run=dry, max_class=max_class, graduate_label=graduate_label)
return jsonify({'ok': True, 'summary': summary}), 200
# Schedule annual rollover job using APScheduler (configurable via env)
try:
if cfg.SCHEDULER_ENABLED:
_rollover_month = getattr(cfg, 'SCHOOL_ROLLOVER_MONTH', 9)
_rollover_day = getattr(cfg, 'SCHOOL_ROLLOVER_DAY', 1)
_rollover_hour = getattr(cfg, 'SCHOOL_ROLLOVER_HOUR', 3)
_rollover_minute = getattr(cfg, 'SCHOOL_ROLLOVER_MIN', 0)
_rollover_max_class = getattr(cfg, 'SCHOOL_ROLLOVER_MAX_CLASS', 13)
_rollover_grad_label = getattr(cfg, 'SCHOOL_ROLLOVER_GRADUATE_LABEL', '')
_scheduler = BackgroundScheduler()
# Use a cron-style yearly job on the configured month/day
_scheduler.add_job(
func=lambda: rollover_student_card_classes(dry_run=False, max_class=_rollover_max_class, graduate_label=_rollover_grad_label),
trigger='cron',
month=_rollover_month,
day=_rollover_day,
hour=_rollover_hour,
minute=_rollover_minute,
id='school_year_rollover',
replace_existing=True
)
_scheduler.start()
app.logger.info('Scheduled annual school year rollover: %s-%s %s:%s', _rollover_month, _rollover_day, _rollover_hour, _rollover_minute)
except Exception as e:
app.logger.warning('Failed to schedule school year rollover: %s', e)
# Thumbnail sizes
THUMBNAIL_SIZE = cfg.THUMBNAIL_SIZE
PREVIEW_SIZE = cfg.PREVIEW_SIZE
@@ -3053,6 +3167,7 @@ def api_library_items():
query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']},
'IsGroupedSubItem': {'$ne': True},
'Deleted': {'$ne': True}
}
@@ -3078,7 +3193,39 @@ def api_library_items():
raw_items = list(items_db.find(query, projection).sort([('Name', 1), ('_id', 1)]).skip(offset).limit(limit))
# Build maps for grouping by parent-child relationship (grouped sub-items)
all_ids = [str(itm.get('_id')) for itm in raw_items if itm.get('_id')]
parent_ids_list = [str(itm.get('_id')) for itm in raw_items if itm.get('_id')]
children_by_parent = {}
child_items = []
if parent_ids_list:
child_projection = {
'Name': 1,
'Autor': 1,
'Author': 1,
'ISBN': 1,
'Code_4': 1,
'Code4': 1,
'ItemType': 1,
'Verfuegbar': 1,
'Condition': 1,
'HasDamage': 1,
'User': 1,
'Ort': 1,
'Beschreibung': 1,
'Image': 1,
'ParentItemId': 1,
}
child_items = list(items_db.find({
'ParentItemId': {'$in': parent_ids_list},
'IsGroupedSubItem': True,
'Deleted': {'$ne': True}
}, child_projection))
for child in child_items:
parent_id = str(child.get('ParentItemId') or '')
if not parent_id:
continue
children_by_parent.setdefault(parent_id, []).append(child)
all_ids = parent_ids_list + [str(child.get('_id')) for child in child_items if child.get('_id')]
active_records = []
if all_ids:
active_records = list(ausleihungen_db.find({'Item': {'$in': all_ids}, 'Status': 'active'}, {'Item': 1, 'User': 1}))
@@ -3093,26 +3240,10 @@ def api_library_items():
if item_id not in active_user_by_item:
active_user_by_item[item_id] = rec.get('User', '')
# Organize children under their parent (ParentItemId) and prepare parent list
items_by_id = {}
children_by_parent = {}
parent_ids = set()
for itm in raw_items:
iid = str(itm.get('_id'))
items_by_id[iid] = itm
parent = str(itm.get('ParentItemId') or '')
if parent:
children_by_parent.setdefault(parent, []).append(itm)
else:
parent_ids.add(iid)
# Build aggregated list: for each parent id, include parent + children as one entry
# Build aggregated list: iterate ordered parent list and include children like inventory module
aggregated = []
processed = set()
for pid in list(parent_ids):
parent = items_by_id.get(pid)
if not parent:
continue
for parent in raw_items:
pid = str(parent.get('_id'))
children = children_by_parent.get(pid, [])
# Compute grouped counts and availability
@@ -3137,22 +3268,33 @@ def api_library_items():
has_damage = bool(doc.get('HasDamage')) or condition_value == 'destroyed'
has_active_borrow = any(str(unit.get('_id')) in active_item_ids for unit in grouped_units)
if has_damage and not has_active_borrow:
# Prefer 'available' when any grouped unit is available
if has_damage and len(available_units) == 0:
doc['LibraryDisplayStatus'] = 'damaged'
elif len(available_units) > 0:
doc['LibraryDisplayStatus'] = 'available'
elif has_active_borrow or doc.get('Verfuegbar') is False:
doc['LibraryDisplayStatus'] = 'borrowed'
else:
doc['LibraryDisplayStatus'] = 'available'
# Determine borrower: prefer any active borrow on group units, fallback to parent.User
borrower = active_user_by_item.get(str(parent.get('_id'))) or ''
# Determine borrower: prefer any active borrow on grouped units, fallback to parent.User
borrower = ''
for unit in grouped_units:
u_id = str(unit.get('_id'))
if active_user_by_item.get(u_id):
borrower = active_user_by_item.get(u_id)
break
if not borrower:
for unit in grouped_units:
u_id = str(unit.get('_id'))
if active_user_by_item.get(u_id):
borrower = active_user_by_item.get(u_id)
break
doc['BorrowedBy'] = borrower or doc.get('User', '')
borrower = doc.get('User', '')
# Decrypt if value is encrypted (decrypt_text returns original if not encrypted)
try:
borrower = decrypt_text(borrower) if borrower else ''
except Exception:
# Fallback: keep original string if decryption fails
borrower = borrower or ''
doc['BorrowedBy'] = borrower
doc['GroupedDisplayCount'] = 1 + len(children)
doc['Quantity'] = doc['GroupedDisplayCount']
@@ -3161,40 +3303,6 @@ def api_library_items():
doc['GroupedAllCodes'] = grouped_all_codes
aggregated.append(doc)
processed.add(pid)
for c in children:
processed.add(str(c.get('_id')))
# Include any remaining items (orphans or standalones not parented)
for itm in raw_items:
iid = str(itm.get('_id'))
if iid in processed:
continue
doc = dict(itm)
doc['_id'] = iid
if doc.get('Code4') in (None, '') and doc.get('Code_4') not in (None, ''):
doc['Code4'] = doc.get('Code_4')
condition_value = str(doc.get('Condition', '')).strip().lower()
has_damage = bool(doc.get('HasDamage')) or condition_value == 'destroyed'
has_active_borrow = iid in active_item_ids
if has_damage and not has_active_borrow:
doc['LibraryDisplayStatus'] = 'damaged'
elif has_active_borrow or doc.get('Verfuegbar') is False:
doc['LibraryDisplayStatus'] = 'borrowed'
else:
doc['LibraryDisplayStatus'] = 'available'
doc['BorrowedBy'] = active_user_by_item.get(iid) or doc.get('User', '')
# Single item: grouped count = 1
doc['GroupedDisplayCount'] = 1
doc['Quantity'] = 1
doc['AvailableGroupedCount'] = 1 if doc.get('Verfuegbar', True) else 0
doc['GroupedAvailableUnits'] = [{'id': iid, 'code': doc.get('Code_4') or doc.get('Code4') or '', 'label': f"{doc.get('Code_4') or doc.get('Code4') or '-'} ({doc.get('Name','')})"}] if doc.get('Verfuegbar', True) else []
doc['GroupedAllCodes'] = [doc.get('Code_4') or doc.get('Code4') or '']
aggregated.append(doc)
client.close()
@@ -3384,28 +3492,82 @@ def api_item_detail(item_id):
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen_col = db['ausleihungen']
items_col = db['items']
active_borrow = ausleihungen_col.find_one(
{'Item': str(item.get('_id')), 'Status': 'active'},
{'User': 1}
)
client.close()
# Determine parent/group id (if this is a child, use its ParentItemId)
parent_id = str(item.get('ParentItemId') or str(item.get('_id')))
# Load all group units (parent + children)
group_units_cursor = items_col.find({
'$or': [
{'_id': ObjectId(parent_id)},
{'ParentItemId': parent_id}
],
'Deleted': {'$ne': True}
})
group_units = list(group_units_cursor)
if not group_units:
# Fallback: just use the single item
group_units = [item]
group_unit_ids = [str(u.get('_id')) for u in group_units if u.get('_id')]
# Fetch all borrow records for the whole group (active + history)
borrow_records = list(ausleihungen_col.find({'Item': {'$in': group_unit_ids}}).sort('Start', -1))
# Compute availability and damage state across group
available_units = [u for u in group_units if u.get('Verfuegbar', True)]
condition_value = str(item.get('Condition', '')).strip().lower()
has_damage = bool(item.get('HasDamage')) or condition_value == 'destroyed'
if has_damage and not active_borrow:
if has_damage and len(available_units) == 0:
status_label = 'Defekt/Zerstört'
elif item.get('Verfuegbar') is False or active_borrow:
elif len(available_units) > 0:
status_label = 'Verfügbar'
elif any(r.get('Status') == 'active' for r in borrow_records):
status_label = 'Ausgeliehen'
else:
status_label = 'Verfügbar'
# Prefer the most recent active borrower across the group
borrower_value = ''
if active_borrow:
borrower_value = active_borrow.get('User', '')
elif item.get('User'):
borrower_value = item.get('User')
for rec in borrow_records:
if rec.get('Status') == 'active' and rec.get('User'):
try:
borrower_value = decrypt_text(rec.get('User'))
except Exception:
borrower_value = rec.get('User')
break
if not borrower_value and item.get('User'):
try:
borrower_value = decrypt_text(item.get('User'))
except Exception:
borrower_value = item.get('User')
# Helper to format datetimes
def fmt_dt(dt):
try:
return dt.strftime('%d.%m.%Y %H:%M') if dt else ''
except Exception:
return str(dt) if dt else ''
# Build HTML for borrow records (if any)
borrows_html = ''
if borrow_records:
rows = []
for rec in borrow_records:
user_raw = rec.get('User') or ''
try:
user = decrypt_text(user_raw) if user_raw else ''
except Exception:
user = user_raw
status = rec.get('Status', '')
start = fmt_dt(rec.get('Start'))
end = fmt_dt(rec.get('End'))
notes = html.escape(str(rec.get('Notes') or ''))
rows.append(f"<li><strong>{html.escape(user or '-')}</strong> — {html.escape(status)}{html.escape(start)}{html.escape(end)}{('' + notes) if notes else ''}</li>")
borrows_html = f"<h3>Ausleihhistorie</h3><ul>{''.join(rows)}</ul>"
# Basic detail HTML
detail_html = f"""
<h2>{html.escape(item.get('Name', 'Untitled'))}</h2>
@@ -3414,7 +3576,10 @@ def api_item_detail(item_id):
<p><strong>Beschreibung:</strong> {html.escape(item.get('Beschreibung', '-'))}</p>
<p><strong>Status:</strong> {html.escape(status_label)}</p>
{f'<p><strong>Ausgeliehen von:</strong> {html.escape(str(borrower_value))}</p>' if borrower_value and status_label == 'Ausgeliehen' else ''}
{borrows_html}
"""
client.close()
return detail_html, 200
return detail_html, 200
except Exception as e:
app.logger.error(f"Error fetching item detail: {e}")
-8
View File
@@ -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
+35 -20
View File
@@ -71,6 +71,15 @@ DEFAULTS = {
'logo_path': '',
'logo_thumb': '',
'logo_thumb': '',
# School-level rollover configuration for advancing class years
'rollover': {
'month': 9,
'day': 1,
'hour': 3,
'minute': 0,
'max_class': 13,
'graduate_label': ''
},
},
'schoolPeriods': {
"1": {"start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)"},
@@ -157,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']))
@@ -221,10 +216,30 @@ 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'])
# School rollover configuration (can be set in config.json under `school.rollover`)
SCHOOL_ROLLOVER = _get(_conf, ['school', 'rollover'], DEFAULTS['school'].get('rollover', {}))
SCHOOL_ROLLOVER_MONTH = int(os.getenv('INVENTAR_SCHOOL_ROLLOVER_MONTH', str(SCHOOL_ROLLOVER.get('month', 9))))
SCHOOL_ROLLOVER_DAY = int(os.getenv('INVENTAR_SCHOOL_ROLLOVER_DAY', str(SCHOOL_ROLLOVER.get('day', 1))))
SCHOOL_ROLLOVER_HOUR = int(os.getenv('INVENTAR_SCHOOL_ROLLOVER_HOUR', str(SCHOOL_ROLLOVER.get('hour', 3))))
SCHOOL_ROLLOVER_MIN = int(os.getenv('INVENTAR_SCHOOL_ROLLOVER_MIN', str(SCHOOL_ROLLOVER.get('minute', 0))))
SCHOOL_ROLLOVER_MAX_CLASS = int(os.getenv('INVENTAR_SCHOOL_MAX_CLASS', str(SCHOOL_ROLLOVER.get('max_class', 13))))
SCHOOL_ROLLOVER_GRADUATE_LABEL = os.getenv('INVENTAR_SCHOOL_GRADUATE_LABEL', str(SCHOOL_ROLLOVER.get('graduate_label', '')))
# Optional feature modules
TENANT_CONFIGS = _get(_conf, ['tenants'], {})
+110
View File
@@ -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
+47
View File
@@ -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
View File
@@ -1 +1 @@
print("hello")
# Web.modules package initialization
+51
View File
@@ -0,0 +1,51 @@
"""
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, name):
"""
Updates slot for the booking per a id
Input:
- id: the id is the id you get from the
- date_start_time: the date of the booking that was selected with date and time
- name: name that the client gave himself
Output:
- bool: if worked or not
"""
termin.update()
+24 -1
View File
@@ -754,7 +754,8 @@
const resp = await fetch(`/delete_library_item/${itemId}`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}'
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
credentials: 'same-origin'
});
@@ -863,11 +864,27 @@
setActiveStudentCard(cardId);
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
let borrowCount = parseInt(countPrompt || '1', 10);
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
borrowCount = 1;
}
if (borrowCount > maxAvailable) {
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = `/ausleihen/${itemId}`;
const csrfField = document.createElement('input');
csrfField.type = 'hidden';
csrfField.name = 'csrf_token';
csrfField.value = '{{ csrf_token }}';
form.appendChild(csrfField);
const cardField = document.createElement('input');
cardField.type = 'hidden';
cardField.name = 'borrower_card_id';
@@ -888,6 +905,12 @@
form.appendChild(durationField);
}
const countField = document.createElement('input');
countField.type = 'hidden';
countField.name = 'exemplare_count';
countField.value = String(borrowCount || 1);
form.appendChild(countField);
document.body.appendChild(form);
form.submit();
}
+11
View File
@@ -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,