mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-08-09 00:04:50 +02:00
Moved all project files under a "helpdesk" directory. Should make it easier to use setup.py / pypi / whatever in the future.
This commit is contained in:
348
helpdesk/lib.py
Normal file
348
helpdesk/lib.py
Normal file
@ -0,0 +1,348 @@
|
||||
"""
|
||||
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')
|
||||
|
||||
try:
|
||||
from base64 import urlsafe_b64encode as b64encode
|
||||
except ImportError:
|
||||
from base64 import encodestring as b64encode
|
||||
try:
|
||||
from base64 import urlsafe_b64decode as b64decode
|
||||
except ImportError:
|
||||
from base64 import decodestring as b64decode
|
||||
|
||||
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
|
||||
import os
|
||||
|
||||
context = Context(email_context)
|
||||
locale = getattr(context['queue'], 'locale', '')
|
||||
if not locale:
|
||||
locale = 'en'
|
||||
|
||||
if locale:
|
||||
template_localized = template_name + ':' + locale
|
||||
else:
|
||||
template_localized = None
|
||||
|
||||
t = None
|
||||
if template_localized:
|
||||
try:
|
||||
t = EmailTemplate.objects.get(template_name__iexact=template_localized)
|
||||
except EmailTemplate.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not t:
|
||||
try:
|
||||
t = EmailTemplate.objects.get(template_name__iexact=template_name)
|
||||
except EmailTemplate.DoesNotExist:
|
||||
return # just ignore if template doesn't exist
|
||||
|
||||
if not sender:
|
||||
sender = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
|
||||
|
||||
text_part = loader.get_template_from_string(
|
||||
"%s{%% include '%s' %%}" % (t.plain_text, footer_file)
|
||||
).render(context)
|
||||
|
||||
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
|
||||
|
||||
html_part = loader.get_template_from_string(
|
||||
"{%% extends '%s' %%}{%% block title %%}%s{%% endblock %%}{%% block content %%}%s{%% endblock %%}" % (email_html_base_file, t.heading, t.html)
|
||||
).render(context)
|
||||
|
||||
subject_part = loader.get_template_from_string(
|
||||
"{{ ticket.ticket }} {{ ticket.title|safe }} %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 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 + 100 for headings.'.
|
||||
chart_url = 'http://chart.apis.google.com/chart?cht=lc&chs=%sx150&chd=t:' % (min(len(column_headings)*65+100, 1000))
|
||||
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 '220px * number of months'.
|
||||
chart_url = 'http://chart.apis.google.com/chart?cht=bvg&chs=%sx150&chd=t:' % (min(len(column_headings) * 220, 1000))
|
||||
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):
|
||||
if params.get('sortreverse', None):
|
||||
params['sorting'] = "-%s" % params['sorting']
|
||||
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
|
||||
|
||||
|
||||
def text_is_spam(text, request):
|
||||
# Based on a blog post by 'sciyoshi':
|
||||
# http://sciyoshi.com/blog/2008/aug/27/using-akismet-djangos-new-comments-framework/
|
||||
# This will return 'True' is the given text is deemed to be spam, or
|
||||
# False if it is not spam. If it cannot be checked for some reason, we
|
||||
# assume it isn't spam.
|
||||
from django.contrib.sites.models import Site
|
||||
from django.conf import settings
|
||||
try:
|
||||
from helpdesk.akismet import Akismet
|
||||
except:
|
||||
return False
|
||||
|
||||
ak = Akismet(
|
||||
blog_url='http://%s/' % Site.objects.get(pk=settings.SITE_ID).domain,
|
||||
agent='Jutda Helpdesk',
|
||||
)
|
||||
|
||||
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
||||
ak.setAPIKey(key = settings.TYPEPAD_ANTISPAM_API_KEY)
|
||||
ak.baseurl = 'api.antispam.typepad.com/1.1/'
|
||||
elif hasattr(settings, 'AKISMET_API_KEY'):
|
||||
ak.setAPIKey(key = settings.AKISMET_API_KEY)
|
||||
else:
|
||||
return False
|
||||
|
||||
if ak.verify_key():
|
||||
ak_data = {
|
||||
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
|
||||
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
||||
'referrer': request.META.get('HTTP_REFERER', ''),
|
||||
'comment_type': 'comment',
|
||||
'comment_author': '',
|
||||
}
|
||||
|
||||
return ak.comment_check(text, data=ak_data)
|
||||
|
||||
return False
|
Reference in New Issue
Block a user