04/07 updated timezone, report via email, etc

This commit is contained in:
2026-04-07 11:24:08 -04:00
parent 56c65bfe4c
commit fed51f8c28
14 changed files with 1022 additions and 29 deletions
+192 -2
View File
@@ -12,7 +12,7 @@ import bleach
from app import db, limiter
from app.models import (User, Ticket, Comment, ActivityLog, KnowledgeBase,
KBAttachment, UserRole, TicketStatus, CannedResponse)
from app.services.log_service import log_action
from app.services.log_service import log_action, log_ticket_history
from app.services.validation_service import validate_password, validate_file
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -930,6 +930,109 @@ def canned_response_delete(item_id):
return redirect(url_for('admin.canned_responses'))
# ─── Bulk Ticket Actions ─────────────────────────────────────────────────────
@admin_bp.route('/tickets/bulk-action', methods=['POST'])
@login_required
@it_required
def bulk_ticket_action():
"""Apply a single action to multiple selected tickets at once.
Accepted actions:
- resolve → set status to Resolved, stamp resolved_at
- close → set status to Closed, stamp closed_at
- assign_me → assign all selected tickets to current user
- unassign → clear assigned_to_id
"""
from app.services.notification_service import notify_status_change
from app.services.sla_service import clear_sla_notification
action = request.form.get('action', '')
ticket_ids = request.form.getlist('ticket_ids', type=int)
if not ticket_ids:
flash('No tickets selected.', 'warning')
return redirect(url_for('admin.all_tickets'))
valid_actions = ('resolve', 'close', 'assign_me', 'unassign')
if action not in valid_actions:
flash('Invalid action.', 'danger')
return redirect(url_for('admin.all_tickets'))
tickets = Ticket.query.filter(Ticket.id.in_(ticket_ids)).all()
now = datetime.utcnow()
count = 0
notif_tickets = [] # collect for post-commit notifications
for ticket in tickets:
old_status = ticket.status
old_assigned = ticket.assigned_to_id
changed = False
if action == 'resolve' and ticket.status not in (
TicketStatus.RESOLVED, TicketStatus.CLOSED):
ticket.status = TicketStatus.RESOLVED
ticket.resolved_at = now
clear_sla_notification(ticket.id)
log_ticket_history(ticket, 'status', old_status,
TicketStatus.RESOLVED, current_user.id)
notif_tickets.append((ticket, old_status))
changed = True
elif action == 'close' and ticket.status != TicketStatus.CLOSED:
ticket.status = TicketStatus.CLOSED
ticket.closed_at = now
clear_sla_notification(ticket.id)
log_ticket_history(ticket, 'status', old_status,
TicketStatus.CLOSED, current_user.id)
notif_tickets.append((ticket, old_status))
changed = True
elif action == 'assign_me' and ticket.assigned_to_id != current_user.id:
ticket.assigned_to_id = current_user.id
log_ticket_history(ticket, 'assigned_to',
old_assigned or 'Unassigned',
current_user.full_name, current_user.id)
changed = True
elif action == 'unassign' and ticket.assigned_to_id is not None:
ticket.assigned_to_id = None
log_ticket_history(ticket, 'assigned_to',
old_assigned or 'Unassigned',
'Unassigned', current_user.id)
changed = True
if changed:
log_action(current_user.id, f'ticket_bulk_{action}', 'ticket',
ticket.id, f'action={action}')
count += 1
db.session.commit()
logger.info(f'[BULK ACTION] action={action} affected={count} '
f'ticket_ids={ticket_ids} by user_id={current_user.id}')
# Send status-change notifications after commit
for ticket, old_status in notif_tickets:
notify_status_change(ticket, old_status, current_user)
action_labels = {
'resolve': 'resolved',
'close': 'closed',
'assign_me': 'assigned to you',
'unassign': 'unassigned',
}
flash(f'{count} ticket{"s" if count != 1 else ""} {action_labels[action]}.', 'success')
# Preserve current filter params on redirect
return redirect(url_for('admin.all_tickets',
status = request.form.get('status', ''),
priority = request.form.get('priority', ''),
assigned = request.form.get('assigned', ''),
q = request.form.get('q', ''),
page = request.form.get('page', 1),
))
def _roles():
return [UserRole.EMPLOYEE, UserRole.IT_STAFF, UserRole.ADMIN]
@@ -957,6 +1060,58 @@ def settings():
flash(f'User registration has been {state_label}.', 'success')
return redirect(url_for('admin.settings'))
# ── Timezone setting ──────────────────────────────────────────────────
if form_type == 'timezone':
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
new_tz = request.form.get('app_timezone', 'America/New_York').strip()
try:
ZoneInfo(new_tz) # validate before saving
except (ZoneInfoNotFoundError, KeyError):
flash(f'Unknown timezone "{new_tz}". Please select a valid timezone.', 'danger')
return redirect(url_for('admin.settings'))
old_tz = SystemSetting.get('app_timezone', 'America/New_York')
SystemSetting.set('app_timezone', new_tz, 'Display timezone for all dates and times in the UI')
db.session.commit()
log_action(current_user.id, 'setting_update', 'system_setting', None,
f'app_timezone changed from {old_tz!r} to {new_tz!r}')
logger.info(f'[ADMIN SETTINGS] app_timezone={new_tz} by admin_id={current_user.id}')
flash(f'Timezone updated to {new_tz}.', 'success')
return redirect(url_for('admin.settings'))
# ── Email ingestion settings ──────────────────────────────────────────
if form_type == 'email_ingestion':
fields = {
'email_ingestion_enabled' : request.form.get('email_ingestion_enabled', '0'),
'email_ingestion_host' : request.form.get('email_ingestion_host', '').strip(),
'email_ingestion_port' : request.form.get('email_ingestion_port', '993').strip(),
'email_ingestion_user' : request.form.get('email_ingestion_user', '').strip(),
'email_ingestion_folder' : request.form.get('email_ingestion_folder', 'INBOX').strip(),
'email_ingestion_move_to' : request.form.get('email_ingestion_move_to', 'Processed').strip(),
'email_ingestion_interval': request.form.get('email_ingestion_interval', '5').strip(),
}
# Password: only update if a new value was provided
new_pw = request.form.get('email_ingestion_password', '').strip()
if new_pw:
fields['email_ingestion_password'] = new_pw
changes = []
for key, value in fields.items():
old_val = SystemSetting.get(key, '')
if str(value) != str(old_val):
SystemSetting.set(key, value)
safe_key = key.replace('email_ingestion_', '')
# Never log the password value
changes.append(f'{safe_key}={"[updated]" if "password" in key else repr(value)}')
db.session.commit()
if changes:
log_action(current_user.id, 'email_ingestion_settings_update',
'system_setting', None, ', '.join(changes))
logger.info(f'[ADMIN EMAIL INGEST] Settings updated: {", ".join(changes)} '
f'by admin_id={current_user.id}')
flash('Email ingestion settings saved.', 'success')
return redirect(url_for('admin.settings'))
# ── Branding update ───────────────────────────────────────────────────
if form_type == 'branding':
fields = {
@@ -1022,6 +1177,16 @@ def settings():
return redirect(url_for('admin.settings'))
registration_enabled = SystemSetting.get_bool('registration_enabled', default=True)
email_settings = {
'enabled' : SystemSetting.get_bool('email_ingestion_enabled', default=False),
'host' : SystemSetting.get('email_ingestion_host', ''),
'port' : SystemSetting.get('email_ingestion_port', '993'),
'user' : SystemSetting.get('email_ingestion_user', ''),
'password': SystemSetting.get('email_ingestion_password', ''),
'folder' : SystemSetting.get('email_ingestion_folder', 'INBOX'),
'move_to' : SystemSetting.get('email_ingestion_move_to', 'Processed'),
'interval': SystemSetting.get('email_ingestion_interval', '5'),
}
branding_settings = {
'app_name' : SystemSetting.get('app_name', 'TechDesk'),
'app_subtitle' : SystemSetting.get('app_subtitle', 'IT Helpdesk System'),
@@ -1030,6 +1195,31 @@ def settings():
'logo_initials' : SystemSetting.get('logo_initials', 'TD'),
'primary_color' : SystemSetting.get('primary_color', '#2563eb'),
}
current_tz = SystemSetting.get('app_timezone', 'America/New_York')
# Common timezone list for the selector
common_timezones = [
('America/New_York', 'Eastern Time (ET) — New York'),
('America/Chicago', 'Central Time (CT) — Chicago'),
('America/Denver', 'Mountain Time (MT) — Denver'),
('America/Phoenix', 'Mountain Time, no DST — Phoenix'),
('America/Los_Angeles', 'Pacific Time (PT) — Los Angeles'),
('America/Anchorage', 'Alaska Time — Anchorage'),
('Pacific/Honolulu', 'Hawaii Time — Honolulu'),
('America/Puerto_Rico', 'Atlantic Time — Puerto Rico'),
('UTC', 'UTC — Coordinated Universal Time'),
('Europe/London', 'GMT/BST — London'),
('Europe/Paris', 'CET/CEST — Paris, Berlin'),
('Europe/Helsinki', 'EET/EEST — Helsinki, Athens'),
('Asia/Dubai', 'GST — Dubai'),
('Asia/Kolkata', 'IST — India'),
('Asia/Singapore', 'SGT — Singapore'),
('Asia/Tokyo', 'JST — Tokyo'),
('Australia/Sydney', 'AEST/AEDT — Sydney'),
('Pacific/Auckland', 'NZST/NZDT — Auckland'),
]
return render_template('admin/settings.html',
registration_enabled=registration_enabled,
branding_settings=branding_settings)
branding_settings=branding_settings,
email_settings=email_settings,
current_tz=current_tz,
common_timezones=common_timezones)