django-helpdesk/lib.py
Ross Poulton a162d77d70 * Create new help page for comment template context variables
( see /help/context/; also linked from comment form)
* Refactor API help page to share template with context help
* Allow a limited number of Ticket & Queue model fields to be 
  accessible in comments, as per 'Help' page.
* New function in lib.py to build a dict of 'safe' fields from 
  ticket & queue, to prevent the power of the Django model API 
  from exposing things like passwords (imagine if a user typed
  a comment containing {{ ticket.queue.email_box_password }} !!!!
* When accessing the ticket list with no filter params (eg by 
  clicking on the "Tickets" button in the menu), the default 
  search is for tickets that aren't closed, rather than showing
  all tickets.
* Updated English locale with changed message strings.
2008-08-29 09:11:02 +00:00

328 lines
11 KiB
Python

"""
Jutda Helpdesk - A Django powered ticket tracker for small enterprise.
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
lib.py - Common functions (eg multipart e-mail)
"""
chart_colours = ('80C65A', '990066', 'FF9900', '3399CC', 'BBCCED', '3399CC', 'FFCC33')
def send_templated_mail(template_name, email_context, recipients, sender=None, bcc=None, fail_silently=False, files=None):
"""
send_templated_mail() is a warpper around Django's e-mail routines that
allows us to easily send multipart (text/plain & text/html) e-mails using
templates that are stored in the database. This lets the admin provide
both a text and a HTML template for each message.
template_name is the slug of the template to use for this message (see
models.EmailTemplate)
email_context is a dictionary to be used when rendering the template
recipients can be either a string, eg 'a@b.com', or a list of strings.
sender should contain a string, eg 'My Site <me@z.com>'. If you leave it
blank, it'll use settings.DEFAULT_FROM_EMAIL as a fallback.
bcc is an optional list of addresses that will receive this message as a
blind carbon copy.
fail_silently is passed to Django's mail routine. Set to 'True' to ignore
any errors at send time.
files can be a list of file paths to be attached, or it can be left blank.
eg ('/tmp/file1.txt', '/tmp/image.png')
"""
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template import loader, Context
from helpdesk.models import EmailTemplate
t = EmailTemplate.objects.get(template_name__iexact=template_name)
if not sender:
sender = settings.DEFAULT_FROM_EMAIL
context = Context(email_context)
text_part = loader.get_template_from_string(
"%s{%% include 'helpdesk/email_text_footer.txt' %%}" % t.plain_text
).render(context)
html_part = loader.get_template_from_string(
"{%% extends 'helpdesk/email_html_base.html' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (t.heading, t.html)
).render(context)
subject_part = loader.get_template_from_string(
"{{ ticket.ticket }} {{ ticket.title }} %s" % t.subject
).render(context)
if type(recipients) != list:
recipients = [recipients,]
msg = EmailMultiAlternatives( subject_part,
text_part,
sender,
recipients,
bcc=bcc)
msg.attach_alternative(html_part, "text/html")
if files:
if type(files) != list:
files = [files,]
for file in files:
msg.attach_file(file)
return msg.send(fail_silently)
def send_multipart_mail(template_name, email_context, subject, recipients, sender=None, bcc=None, fail_silently=False, files=None):
"""
This function will send a multi-part e-mail with both HTML and
Text parts. Note we don't use this any more; wsee send_templated_mail
instead.
template_name must NOT contain an extension. Both HTML (.html) and TEXT
(.txt) versions must exist, eg 'emails/public_submit' will use both
public_submit.html and public_submit.txt.
email_context should be a plain python dictionary. It is applied against
both the email messages (templates) & the subject.
subject can be plain text or a Django template string, eg:
New Job: {{ job.id }} {{ job.title }}
recipients can be either a string, eg 'a@b.com' or a list, eg:
['a@b.com', 'c@d.com']. Type conversion is done if needed.
sender can be an e-mail, 'Name <email>' or None. If unspecified, the
DEFAULT_FROM_EMAIL will be used.
Originally posted on my blog at http://www.rossp.org/
"""
from django.core.mail import EmailMultiAlternatives
from django.template import loader, Context
from django.conf import settings
if not sender:
sender = settings.DEFAULT_FROM_EMAIL
context = Context(email_context)
text_part = loader.get_template('%s.txt' % template_name).render(context)
html_part = loader.get_template('%s.html' % template_name).render(context)
subject_part = loader.get_template_from_string(subject).render(context)
if type(recipients) != list:
recipients = [recipients,]
msg = EmailMultiAlternatives(subject_part, text_part, sender, recipients, bcc=bcc)
msg.attach_alternative(html_part, "text/html")
if files:
if type(files) != list:
files = [files,]
for file in files:
msg.attach_file(file)
return msg.send(fail_silently)
def normalise_data(data, to=100):
"""
Used for normalising data prior to graphing with Google charting API. EG:
[1, 4, 10] becomes [10, 40, 100]
[36, 54, 240] becomes [15, 23, 100]
"""
max_value = max(data)
if max_value > to:
new_data = []
for d in data:
new_data.append(int(d/float(max_value)*to))
data = new_data
return data
def line_chart(data):
"""
'data' is a list of lists making a table.
Row 1, columns 2-n are data headings (the time periods)
Rows 2-n are data, with column 1 being the line labels
"""
column_headings = data[0][1:]
max = 0
for row in data[1:]:
for field in row[1:]:
if field > max:
max = field
# Set width to '65px * number of months'.
chart_url = 'http://chart.apis.google.com/chart?cht=lc&chs=%sx90&chd=t:' % (len(column_headings)*65)
first_row = True
row_headings = []
for row in data[1:]:
# Add data to URL, normalised to the maximum for all lines on this chart
norm = normalise_data(row[1:], max)
if not first_row:
chart_url += '|'
chart_url += ','.join([str(num) for num in norm])
row_headings.append(row[0])
first_row = False
chart_url += '&chds='
rows = len(data)-1
first = True
for row in range(rows):
# Set maximum data ranges to '0:x' where 'x' is the maximum number in use.
if not first:
chart_url += ','
chart_url += '0,%s' % max
first = False
chart_url += '&chdl=%s' % '|'.join(row_headings) # Display legend/labels
chart_url += '&chco=%s' % ','.join(chart_colours) # Default colour set
chart_url += '&chxt=x,y' # Turn on axis labels
chart_url += '&chxl=0:|%s|1:|0|%s' % ('|'.join(column_headings), max) # Axis Label Text
return chart_url
def bar_chart(data):
"""
'data' is a list of lists making a table.
Row 1, columns 2-n are data headings
Rows 2-n are data, with column 1 being the line labels
"""
column_headings = data[0][1:]
max = 0
for row in data[1:]:
for field in row[1:]:
if field > max:
max = field
# Set width to '150px * number of months'.
chart_url = 'http://chart.apis.google.com/chart?cht=bvg&chs=%sx90&chd=t:' % (len(column_headings) * 150)
first_row = True
row_headings = []
for row in data[1:]:
# Add data to URL, normalised to the maximum for all lines on this chart
norm = normalise_data(row[1:], max)
if not first_row:
chart_url += '|'
chart_url += ','.join([str(num) for num in norm])
row_headings.append(row[0])
first_row = False
chart_url += '&chds=0,%s' % max
chart_url += '&chdl=%s' % '|'.join(row_headings) # Display legend/labels
chart_url += '&chco=%s' % ','.join(chart_colours) # Default colour set
chart_url += '&chxt=x,y' # Turn on axis labels
chart_url += '&chxl=0:|%s|1:|0|%s' % ('|'.join(column_headings), max) # Axis Label Text
return chart_url
def query_to_dict(results, descriptions):
"""
Replacement method for cursor.dictfetchall() as that method no longer
exists in psycopg2, and I'm guessing in other backends too.
Converts the results of a raw SQL query into a list of dictionaries, suitable
for use in templates etc.
"""
output = []
for data in results:
row = {}
i = 0
for column in descriptions:
row[column[0]] = data[i]
i += 1
output.append(row)
return output
def apply_query(queryset, params):
"""
Apply a dict-based set of filters & paramaters to a queryset.
queryset is a Django queryset, eg MyModel.objects.all() or
MyModel.objects.filter(user=request.user)
params is a dictionary that contains the following:
filtering: A dict of Django ORM filters, eg:
{'user__id__in': [1, 3, 103], 'title__contains': 'foo'}
other_filter: Another filter of some type, most likely a
set of Q() objects.
sorting: The name of the column to sort by
"""
for key in params['filtering'].keys():
filter = {key: params['filtering'][key]}
queryset = queryset.filter(**filter)
if params.get('other_filter', None):
# eg a Q() set
queryset = queryset.filter(params['other_filter'])
if params.get('sorting', None):
queryset = queryset.order_by(params['sorting'])
return queryset
def safe_template_context(ticket):
"""
Return a dictionary that can be used as a template context to render
comments and other details with ticket or queue paramaters. Note that
we don't just provide the Ticket & Queue objects to the template as
they could reveal confidential information. Just imagine these two options:
* {{ ticket.queue.email_box_password }}
* {{ ticket.assigned_to.password }}
Ouch!
The downside to this is that if we make changes to the model, we will also
have to update this code. Perhaps we can find a better way in the future.
"""
context = {
'queue': {},
'ticket': {},
}
queue = ticket.queue
for field in ( 'title', 'slug', 'email_address', 'from_address'):
attr = getattr(queue, field, None)
if callable(attr):
context['queue'][field] = attr()
else:
context['queue'][field] = attr
for field in ( 'title', 'created', 'modified', 'submitter_email',
'status', 'get_status_display', 'on_hold', 'description',
'resolution', 'priority', 'get_priority_display',
'last_escalation', 'ticket', 'ticket_for_url',
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
):
attr = getattr(ticket, field, None)
if callable(attr):
context['ticket'][field] = '%s' % attr()
else:
context['ticket'][field] = attr
context['ticket']['queue'] = context['queue']
context['ticket']['assigned_to'] = context['ticket']['_get_assigned_to']
return context