116 lines
8.7 KiB
Python
116 lines
8.7 KiB
Python
|
|
"""Case-scoped selection of the existing mail templates. No sending here."""
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
from uuid import UUID
|
||
|
|
from urllib.parse import quote
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Response
|
||
|
|
from psycopg2.extras import RealDictCursor
|
||
|
|
from app.core.auth_dependencies import require_any_permission
|
||
|
|
from app.core.database import get_db_connection, release_db_connection
|
||
|
|
|
||
|
|
router=APIRouter(dependencies=[Depends(require_any_permission('cases.edit','tickets.edit'))])
|
||
|
|
MAX_FILE=20*1024*1024
|
||
|
|
MAX_TOTAL=25*1024*1024
|
||
|
|
|
||
|
|
def case_context(cursor,sag_id):
|
||
|
|
cursor.execute('SELECT s.id,s.titel,s.customer_id,c.name AS customer_name FROM sag_sager s LEFT JOIN customers c ON c.id=s.customer_id WHERE s.id=%s AND s.deleted_at IS NULL',(sag_id,))
|
||
|
|
case=cursor.fetchone()
|
||
|
|
if not case:raise HTTPException(404,detail='Sagen findes ikke')
|
||
|
|
return case
|
||
|
|
|
||
|
|
def available(template,case):
|
||
|
|
return template.get('customer_id') is None or template['customer_id']==case.get('customer_id')
|
||
|
|
|
||
|
|
def rank_templates(rows,tags):
|
||
|
|
case_tags={str(tag).strip().casefold() for tag in tags}
|
||
|
|
for row in rows:
|
||
|
|
row['matched_tags']=[tag for tag in row.get('match_tags') or [] if str(tag).strip().casefold() in case_tags]
|
||
|
|
row['recommended']=bool(row['matched_tags'])
|
||
|
|
return sorted(rows,key=lambda r:(-len(r['matched_tags']),r.get('customer_id') is None,str(r['name']).casefold()))
|
||
|
|
|
||
|
|
def render_text(text,case,contact):
|
||
|
|
values={'sag_id':str(case['id']),'sag_titel':case['titel'],'kunde_navn':case.get('customer_name') or '', 'kontakt_navn':contact.get('name') or ''}
|
||
|
|
values.update(customer_name=values['kunde_navn'],name=values['kontakt_navn'])
|
||
|
|
return re.sub(r'\{\{?\s*(sag_id|sag_titel|kunde_navn|kontakt_navn|customer_name|name)\s*\}?\}',lambda m:values[m[1]],text)
|
||
|
|
|
||
|
|
@router.get('/sag/{sag_id}/mail-templates')
|
||
|
|
async def list_templates(sag_id:int):
|
||
|
|
conn=get_db_connection()
|
||
|
|
try:
|
||
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||
|
|
case=case_context(cur,sag_id)
|
||
|
|
cur.execute("SELECT t.name FROM tags t JOIN entity_tags et ON et.tag_id=t.id WHERE et.entity_type='case' AND et.entity_id=%s",(sag_id,));tags=[r['name'] for r in cur.fetchall()]
|
||
|
|
cur.execute('SELECT t.*, (SELECT COUNT(*) FROM email_template_attachments a WHERE a.template_id=t.id) AS attachment_count FROM email_templates t WHERE t.customer_id IS NULL OR t.customer_id=%s',(case['customer_id'],))
|
||
|
|
return {'customer_id':case['customer_id'],'customer_name':case['customer_name'],'tags':tags,'templates':rank_templates([dict(r) for r in cur.fetchall()],tags)}
|
||
|
|
finally:conn.rollback();release_db_connection(conn)
|
||
|
|
|
||
|
|
@router.get('/sag/{sag_id}/mail-templates/{template_id}')
|
||
|
|
async def preview_template(sag_id:int,template_id:int):
|
||
|
|
conn=get_db_connection()
|
||
|
|
try:
|
||
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||
|
|
case=case_context(cur,sag_id)
|
||
|
|
cur.execute('SELECT * FROM email_templates WHERE id=%s',(template_id,));template=cur.fetchone()
|
||
|
|
if not template or not available(template,case):raise HTTPException(404,detail='Skabelonen er ikke tilgængelig for denne kunde')
|
||
|
|
cur.execute("SELECT CONCAT_WS(' ',c.first_name,c.last_name) AS name,c.email FROM sag_kontakter sk JOIN contacts c ON c.id=sk.contact_id WHERE sk.sag_id=%s AND sk.deleted_at IS NULL ORDER BY sk.is_primary DESC,sk.id LIMIT 1",(sag_id,));contact=cur.fetchone() or {}
|
||
|
|
cur.execute('SELECT id,filename,content_type,size_bytes FROM email_template_attachments WHERE template_id=%s ORDER BY id',(template_id,))
|
||
|
|
return {'id':template_id,'name':template['name'],'subject':render_text(template['subject'],case,contact),'body':render_text(template['body'],case,contact),'to':contact.get('email') or '', 'attachments':list(cur.fetchall())}
|
||
|
|
finally:conn.rollback();release_db_connection(conn)
|
||
|
|
|
||
|
|
@router.post('/sag/{sag_id}/mail-templates')
|
||
|
|
async def create_template(sag_id:int,data:str=Form(...),files:list[UploadFile]=File(default=[])):
|
||
|
|
try:payload=json.loads(data);request_id=str(UUID(payload['request_id']))
|
||
|
|
except (ValueError,KeyError,TypeError):raise HTTPException(400,detail='Ugyldige skabelonoplysninger')
|
||
|
|
if not isinstance(payload,dict):raise HTTPException(400,detail='Ugyldige skabelonoplysninger')
|
||
|
|
for key,limit in [('name',255),('subject',255),('body',50000)]:
|
||
|
|
value=payload.get(key)
|
||
|
|
if not isinstance(value,str) or not value.strip() or len(value)>limit:raise HTTPException(400,detail='Udfyld navn, emne og besked inden for feltets længde')
|
||
|
|
scope=payload.get('scope','customer')
|
||
|
|
if scope not in {'global','customer'}:raise HTTPException(400,detail='Vælg global eller kunde')
|
||
|
|
tags=payload.get('match_tags',[])
|
||
|
|
if not isinstance(tags,list) or len(tags)>30 or any(not isinstance(t,str) or not t.strip() or len(t)>100 for t in tags):raise HTTPException(400,detail='Ugyldige tags')
|
||
|
|
assets=[];total=0
|
||
|
|
if len(files)>10:raise HTTPException(400,detail='Vælg højst 10 filer')
|
||
|
|
for file in files:
|
||
|
|
content=await file.read(MAX_FILE+1);total+=len(content)
|
||
|
|
if len(content)>MAX_FILE or total>MAX_TOTAL:raise HTTPException(413,detail='Hver fil må fylde 20 MB og filerne samlet højst 25 MB')
|
||
|
|
name=(file.filename or 'vedhaeftning').replace('\\','/').split('/')[-1].replace('\r','').replace('\n','')[:255]
|
||
|
|
assets.append((name,file.content_type or 'application/octet-stream',content,len(content)))
|
||
|
|
conn=get_db_connection()
|
||
|
|
try:
|
||
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||
|
|
case=case_context(cur,sag_id)
|
||
|
|
customer=case['customer_id'] if scope=='customer' else None
|
||
|
|
if scope=='customer' and not customer:raise HTTPException(400,detail='Sagen skal have en kunde for at gemme en kundeskabelon')
|
||
|
|
cur.execute('SELECT pg_advisory_xact_lock(hashtextextended(%s,0))',(request_id,))
|
||
|
|
cur.execute('SELECT id,customer_id,name,subject,body,match_tags FROM email_templates WHERE case_request_id=%s',(request_id,));previous=cur.fetchone()
|
||
|
|
if previous:
|
||
|
|
if previous['customer_id']!=customer or any(previous[k]!=payload[k].strip() for k in ['name','subject','body']) or previous['match_tags']!=tags:raise HTTPException(409,detail='Gemningen tilhører en anden skabelon')
|
||
|
|
cur.execute('SELECT filename,content_type,content,size_bytes FROM email_template_attachments WHERE template_id=%s ORDER BY id',(previous['id'],))
|
||
|
|
saved=[(r['filename'],r['content_type'],bytes(r['content']),r['size_bytes']) for r in cur.fetchall()]
|
||
|
|
if saved!=assets:raise HTTPException(409,detail='Gemningen tilhører andre vedhæftninger')
|
||
|
|
return {'id':previous['id']}
|
||
|
|
cur.execute("INSERT INTO email_templates (name,slug,subject,body,category,customer_id,match_tags,case_request_id,is_system) VALUES (%s,%s,%s,%s,'case',%s,%s::jsonb,%s,false) RETURNING id",(payload['name'].strip(),'case-'+request_id,payload['subject'].strip(),payload['body'].strip(),customer,json.dumps(tags),request_id));tid=cur.fetchone()['id']
|
||
|
|
for asset in assets:cur.execute('INSERT INTO email_template_attachments (template_id,filename,content_type,content,size_bytes) VALUES (%s,%s,%s,%s,%s)',(tid,*asset))
|
||
|
|
conn.commit();return {'id':tid}
|
||
|
|
except Exception:conn.rollback();raise
|
||
|
|
finally:conn.rollback();release_db_connection(conn)
|
||
|
|
|
||
|
|
def send_attachments(sag_id,ids):
|
||
|
|
if not ids:return []
|
||
|
|
if len(ids)>10:raise HTTPException(400,detail='Vælg højst 10 skabelonfiler')
|
||
|
|
conn=get_db_connection()
|
||
|
|
try:
|
||
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||
|
|
case=case_context(cur,sag_id)
|
||
|
|
cur.execute('SELECT a.*,t.customer_id FROM email_template_attachments a JOIN email_templates t ON t.id=a.template_id WHERE a.id=ANY(%s)',(list(set(ids)),));rows=cur.fetchall()
|
||
|
|
if len(rows)!=len(set(ids)) or any(not available(row,case) for row in rows):raise HTTPException(400,detail='En vedhæftning er ikke tilgængelig for denne kunde')
|
||
|
|
if sum(r['size_bytes'] for r in rows)>MAX_TOTAL:raise HTTPException(413,detail='Skabelonfilerne må samlet fylde højst 25 MB')
|
||
|
|
return [{'filename':r['filename'],'content_type':r['content_type'],'content':bytes(r['content']),'size':r['size_bytes']} for r in rows]
|
||
|
|
finally:conn.rollback();release_db_connection(conn)
|
||
|
|
|
||
|
|
@router.get('/sag/{sag_id}/mail-template-files/{attachment_id}')
|
||
|
|
async def download_attachment(sag_id:int,attachment_id:int):
|
||
|
|
asset=send_attachments(sag_id,[attachment_id])[0]
|
||
|
|
return Response(asset['content'],media_type='application/octet-stream',headers={'Content-Disposition':f"attachment; filename*=UTF-8''{quote(asset['filename'],safe='')}",'X-Content-Type-Options':'nosniff'})
|