""" 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 '. 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', '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: t = EmailTemplate.objects.get(template_name__iexact=template_name) 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 }} %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:' % (len(column_headings)*65+100) 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:' % (len(column_headings) * 220) 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