mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2025-05-31 06:55:56 +02:00
* Added force_insert and force_update parameters to model save() overrides (as per Django rev 8670)
* Added 'UserSettings' model to provide a user profile system independent of existing Django user profiles, for two reasons: 1) Avoids users having to update settings.py and 2) Allows jutda-helpdesk to integrate with websites who already use a User Profile * Settings added in this revision allow a user to control e-mail alerts, and to determine whether they see the dashboard or ticket list at login. * New 'Settings' link in page footer for signed-in users * Logout now takes you to the Helpdesk homepage * Fixed file attachment bug in management/commands/get_email.py which seemed to have been un-done (fixes issue # 4. Jutda-helpdesk is now compatible with Django 1.0!
This commit is contained in:
parent
a162d77d70
commit
3f8fc2cd68
4
README
4
README
@ -31,10 +31,6 @@ LICENSE.JQUERY and LICENSE.NICEDIT for their respective license terms.
|
|||||||
3. An existing WORKING Django project with database etc. If you
|
3. An existing WORKING Django project with database etc. If you
|
||||||
cannot log into the Admin, you won't get this product working.
|
cannot log into the Admin, you won't get this product working.
|
||||||
|
|
||||||
4. pygooglechart (needs minor mods to @staticmethod calls for python 2.3)
|
|
||||||
http://pygooglechart.slowchop.com/
|
|
||||||
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
3. Installation
|
3. Installation
|
||||||
#########################
|
#########################
|
||||||
|
27
forms.py
27
forms.py
@ -112,7 +112,7 @@ class TicketForm(forms.Form):
|
|||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if t.assigned_to and t.assigned_to != user:
|
if t.assigned_to and t.assigned_to != user and getattr(t.assigned_to.usersettings.settings, 'email_on_ticket_assign', False):
|
||||||
send_templated_mail(
|
send_templated_mail(
|
||||||
'assigned_owner',
|
'assigned_owner',
|
||||||
context,
|
context,
|
||||||
@ -240,3 +240,28 @@ class PublicTicketForm(forms.Form):
|
|||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsForm(forms.Form):
|
||||||
|
login_view_ticketlist = forms.BooleanField(
|
||||||
|
label=_('Show Ticket List on Login?'),
|
||||||
|
help_text=_('Display the ticket list upon login? Otherwise, the dashboard is shown.'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_on_ticket_change = forms.BooleanField(
|
||||||
|
label=_('E-mail me on ticket change?'),
|
||||||
|
help_text=_('If you\'re the ticket owner and the ticket is changed via the web by somebody else, do you want to receive an e-mail?'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_on_ticket_assign = forms.BooleanField(
|
||||||
|
label=_('E-mail me when assigned a ticket?'),
|
||||||
|
help_text=_('If you are assigned a ticket via the web, do you want to receive an e-mail?'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
email_on_ticket_apichange = forms.BooleanField(
|
||||||
|
label=_('E-mail me when a ticket is changed via the API?'),
|
||||||
|
help_text=_('If a ticket is altered by the API, do you want to receive an e-mail?'),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
@ -245,15 +245,16 @@ def ticket_from_message(message, queue):
|
|||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
filename = file['filename'].replace(' ', '_')
|
filename = file['filename'].replace(' ', '_')
|
||||||
a = Attachment(
|
if file['content']:
|
||||||
followup=f,
|
a = Attachment(
|
||||||
filename=filename,
|
followup=f,
|
||||||
mime_type=file['type'],
|
filename=filename,
|
||||||
size=len(file['content']),
|
mime_type=file['type'],
|
||||||
)
|
size=len(file['content']),
|
||||||
a.file.save(file['filename'], ContentFile(file['content']))
|
)
|
||||||
a.save()
|
a.file.save(file['filename'], ContentFile(file['content']))
|
||||||
print " - %s" % file['filename']
|
a.save()
|
||||||
|
print " - %s" % file['filename']
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
86
models.py
86
models.py
@ -182,7 +182,7 @@ class Queue(models.Model):
|
|||||||
return u'%s <%s>' % (self.title, self.email_address)
|
return u'%s <%s>' % (self.title, self.email_address)
|
||||||
from_address = property(_from_address)
|
from_address = property(_from_address)
|
||||||
|
|
||||||
def save(self):
|
def save(self, force_insert=False, force_update=False):
|
||||||
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
|
if self.email_box_type == 'imap' and not self.email_box_imap_folder:
|
||||||
self.email_box_imap_folder = 'INBOX'
|
self.email_box_imap_folder = 'INBOX'
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ class Queue(models.Model):
|
|||||||
self.email_box_port = 995
|
self.email_box_port = 995
|
||||||
elif self.email_box_type == 'pop3' and not self.email_box_ssl:
|
elif self.email_box_type == 'pop3' and not self.email_box_ssl:
|
||||||
self.email_box_port = 110
|
self.email_box_port = 110
|
||||||
super(Queue, self).save()
|
super(Queue, self).save(force_insert, force_update)
|
||||||
|
|
||||||
|
|
||||||
class Ticket(models.Model):
|
class Ticket(models.Model):
|
||||||
@ -402,7 +402,7 @@ class Ticket(models.Model):
|
|||||||
return ('helpdesk_view', [str(self.id)])
|
return ('helpdesk_view', [str(self.id)])
|
||||||
get_absolute_url = models.permalink(get_absolute_url)
|
get_absolute_url = models.permalink(get_absolute_url)
|
||||||
|
|
||||||
def save(self):
|
def save(self, force_insert=False, force_update=False):
|
||||||
if not self.id:
|
if not self.id:
|
||||||
# This is a new ticket as no ID yet exists.
|
# This is a new ticket as no ID yet exists.
|
||||||
self.created = datetime.now()
|
self.created = datetime.now()
|
||||||
@ -412,7 +412,7 @@ class Ticket(models.Model):
|
|||||||
|
|
||||||
self.modified = datetime.now()
|
self.modified = datetime.now()
|
||||||
|
|
||||||
super(Ticket, self).save()
|
super(Ticket, self).save(force_insert, force_update)
|
||||||
|
|
||||||
|
|
||||||
class FollowUpManager(models.Manager):
|
class FollowUpManager(models.Manager):
|
||||||
@ -488,12 +488,12 @@ class FollowUp(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
||||||
|
|
||||||
def save(self):
|
def save(self, force_insert=False, force_update=False):
|
||||||
t = self.ticket
|
t = self.ticket
|
||||||
t.modified = datetime.now()
|
t.modified = datetime.now()
|
||||||
self.date = datetime.now()
|
self.date = datetime.now()
|
||||||
t.save()
|
t.save()
|
||||||
super(FollowUp, self).save()
|
super(FollowUp, self).save(force_insert, force_update)
|
||||||
|
|
||||||
|
|
||||||
class TicketChange(models.Model):
|
class TicketChange(models.Model):
|
||||||
@ -796,9 +796,9 @@ class KBItem(models.Model):
|
|||||||
'changed.'),
|
'changed.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self):
|
def save(self, force_insert=False, force_update=False):
|
||||||
self.last_updated = datetime.now()
|
self.last_updated = datetime.now()
|
||||||
return super(KBItem, self).save()
|
return super(KBItem, self).save(force_insert, force_update)
|
||||||
|
|
||||||
def _score(self):
|
def _score(self):
|
||||||
if self.votes > 0:
|
if self.votes > 0:
|
||||||
@ -855,3 +855,73 @@ class SavedSearch(models.Model):
|
|||||||
return u'%s (*)' % self.title
|
return u'%s (*)' % self.title
|
||||||
else:
|
else:
|
||||||
return u'%s' % self.title
|
return u'%s' % self.title
|
||||||
|
|
||||||
|
class UserSettings(models.Model):
|
||||||
|
"""
|
||||||
|
A bunch of user-specific settings that we want to be able to define, such
|
||||||
|
as notification preferences and other things that should probably be
|
||||||
|
configurable.
|
||||||
|
|
||||||
|
We should always refer to user.usersettings.settings['setting_name'].
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = models.OneToOneField(User)
|
||||||
|
|
||||||
|
settings_pickled = models.TextField(
|
||||||
|
_('Settings Dictionary'),
|
||||||
|
help_text=_('This is a base64-encoded representation of a pickled Python dictionary. Do not change this field via the admin.'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_settings(self, data):
|
||||||
|
# data should always be a Python dictionary.
|
||||||
|
import cPickle, base64
|
||||||
|
self.settings_pickled = base64.urlsafe_b64encode(cPickle.dumps(data))
|
||||||
|
|
||||||
|
def _get_settings(self):
|
||||||
|
# return a python dictionary representing the pickled data.
|
||||||
|
import cPickle, base64
|
||||||
|
try:
|
||||||
|
return cPickle.loads(base64.urlsafe_b64decode(str(self.settings_pickled)))
|
||||||
|
except cPickle.UnpicklingError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
settings = property(_get_settings, _set_settings)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u'Preferences for %s' % self.user
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'User Settings'
|
||||||
|
verbose_name_plural = 'User Settings'
|
||||||
|
|
||||||
|
|
||||||
|
def create_usersettings(sender, created_models=[], instance=None, created=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Helper function to create UserSettings instances as
|
||||||
|
required, eg when we first create the UserSettings database
|
||||||
|
table via 'syncdb' or when we save a new user.
|
||||||
|
|
||||||
|
If we end up with users with no UserSettings, then we get horrible
|
||||||
|
'DoesNotExist: UserSettings matching query does not exist.' errors.
|
||||||
|
"""
|
||||||
|
if sender == User and created:
|
||||||
|
# This is a new user, so lets create their settings entry.
|
||||||
|
s = UserSettings(user=instance)
|
||||||
|
s.save()
|
||||||
|
elif UserSettings in created_models:
|
||||||
|
# We just created the UserSettings model, lets create a UserSettings
|
||||||
|
# entry for each existing user. This will only happen once (at install
|
||||||
|
# time, or at upgrade) when the UserSettings model doesn't already
|
||||||
|
# exist.
|
||||||
|
for u in User.objects.all():
|
||||||
|
try:
|
||||||
|
s = UserSettings.objects.get(user=u)
|
||||||
|
except UserSettings.DoesNotExist:
|
||||||
|
s = UserSettings(user=u)
|
||||||
|
s.save()
|
||||||
|
|
||||||
|
models.signals.post_syncdb.connect(create_usersettings)
|
||||||
|
models.signals.post_save.connect(create_usersettings, sender=User)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
{% block helpdesk_body %}{% endblock %}
|
{% block helpdesk_body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div id='footer'>
|
<div id='footer'>
|
||||||
<p>{% trans "Powered by <a href='http://www.jutda.com.au/'>Jutda Helpdesk</a>." %} <a href='{% url helpdesk_rss_index %}'><img src='{{ MEDIA_URL }}/helpdesk/rss_icon.png' width='14' height='14' alt='{% trans "RSS Icon" %}' title='{% trans "RSS Feeds" %}' border='0' />{% trans "RSS Feeds" %}</a> <a href='{% url helpdesk_api_help %}'>{% trans "API" %}</a></p>
|
<p>{% trans "Powered by <a href='http://www.jutda.com.au/'>Jutda Helpdesk</a>." %} <a href='{% url helpdesk_rss_index %}'><img src='{{ MEDIA_URL }}/helpdesk/rss_icon.png' width='14' height='14' alt='{% trans "RSS Icon" %}' title='{% trans "RSS Feeds" %}' border='0' />{% trans "RSS Feeds" %}</a> <a href='{% url helpdesk_api_help %}'>{% trans "API" %}</a> <a href='{% url helpdesk_user_settings %}'>{% trans "User Settings" %}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "helpdesk/debug.html" %}
|
{% include "helpdesk/debug.html" %}
|
||||||
|
22
templates/helpdesk/user_settings.html
Normal file
22
templates/helpdesk/user_settings.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "helpdesk/base.html" %}{% load i18n %}
|
||||||
|
|
||||||
|
{% block helpdesk_title %}{% trans "Change User Settings" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block helpdesk_body %}{% blocktrans %}
|
||||||
|
<h2>User Settings</h2>
|
||||||
|
|
||||||
|
<p>Use the following options to change the way your helpdesk system works for you. These settings do not impact any other user.</p>{% endblocktrans %}
|
||||||
|
|
||||||
|
<form method='post' action='./'>
|
||||||
|
<fieldset>
|
||||||
|
<dl>{% for field in form %}
|
||||||
|
<dt><label for='id_{{ field.name }}'>{{ field.label }}</label></dt>
|
||||||
|
<dd>{{ field }}</dd>
|
||||||
|
{% if field.errors %}<dd class='error'>{{ field.errors }}</dd>{% endif %}
|
||||||
|
{% if field.help_text %}<dd class='form_help_text'>{{ field.help_text }}</dd>{% endif %}
|
||||||
|
{% endfor %}</dl>
|
||||||
|
</fieldset>
|
||||||
|
<input type='submit' value='{% trans "Save Options" %}' />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
5
urls.py
5
urls.py
@ -69,6 +69,10 @@ urlpatterns = patterns('helpdesk.views.staff',
|
|||||||
url(r'^delete_query/(?P<id>[0-9]+)/$',
|
url(r'^delete_query/(?P<id>[0-9]+)/$',
|
||||||
'delete_saved_query',
|
'delete_saved_query',
|
||||||
name='helpdesk_delete_query'),
|
name='helpdesk_delete_query'),
|
||||||
|
|
||||||
|
url(r'^settings/$',
|
||||||
|
'user_settings',
|
||||||
|
name='helpdesk_user_settings'),
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns += patterns('helpdesk.views.public',
|
urlpatterns += patterns('helpdesk.views.public',
|
||||||
@ -97,6 +101,7 @@ urlpatterns += patterns('',
|
|||||||
|
|
||||||
url(r'^logout/$',
|
url(r'^logout/$',
|
||||||
'django.contrib.auth.views.logout',
|
'django.contrib.auth.views.logout',
|
||||||
|
{'next_page': '../'},
|
||||||
name='logout'),
|
name='logout'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -215,7 +215,7 @@ class API:
|
|||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if ticket.assigned_to and self.request.user != ticket.assigned_to:
|
if ticket.assigned_to and self.request.user != ticket.assigned_to and getattr(ticket.assigned_to.usersettings.settings, 'email_on_ticket_apichange', False):
|
||||||
send_templated_mail(
|
send_templated_mail(
|
||||||
'updated_owner',
|
'updated_owner',
|
||||||
context,
|
context,
|
||||||
@ -276,7 +276,7 @@ class API:
|
|||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if ticket.assigned_to and self.request.user != ticket.assigned_to:
|
if ticket.assigned_to and self.request.user != ticket.assigned_to and getattr(ticket.assigned_to.usersettings.settings, 'email_on_ticket_apichange', False):
|
||||||
send_templated_mail(
|
send_templated_mail(
|
||||||
'resolved_resolved',
|
'resolved_resolved',
|
||||||
context,
|
context,
|
||||||
|
@ -22,7 +22,10 @@ from helpdesk.models import Ticket, Queue
|
|||||||
|
|
||||||
def homepage(request):
|
def homepage(request):
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
return HttpResponseRedirect(reverse('helpdesk_dashboard'))
|
if getattr(request.user.usersettings.settings, 'login_view_ticketlist', False):
|
||||||
|
return HttpResponseRedirect(reverse('helpdesk_list'))
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse('helpdesk_dashboard'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = PublicTicketForm(request.POST)
|
form = PublicTicketForm(request.POST)
|
||||||
|
@ -20,7 +20,7 @@ from django.shortcuts import render_to_response, get_object_or_404
|
|||||||
from django.template import loader, Context, RequestContext
|
from django.template import loader, Context, RequestContext
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from helpdesk.forms import TicketForm
|
from helpdesk.forms import TicketForm, UserSettingsForm
|
||||||
from helpdesk.lib import send_templated_mail, line_chart, bar_chart, query_to_dict, apply_query, safe_template_context
|
from helpdesk.lib import send_templated_mail, line_chart, bar_chart, query_to_dict, apply_query, safe_template_context
|
||||||
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch
|
from helpdesk.models import Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch
|
||||||
|
|
||||||
@ -234,13 +234,14 @@ def update_ticket(request, ticket_id):
|
|||||||
else:
|
else:
|
||||||
template_staff = 'updated_owner'
|
template_staff = 'updated_owner'
|
||||||
|
|
||||||
send_templated_mail(
|
if (not reassigned or ( reassigned and getattr(ticket.assigned_to.usersettings.settings, 'email_on_ticket_assign', False))) or (not reassigned and getattr(ticket.assigned_to.usersettings.settings, 'email_on_ticket_change', False)):
|
||||||
template_staff,
|
send_templated_mail(
|
||||||
context,
|
template_staff,
|
||||||
recipients=ticket.assigned_to.email,
|
context,
|
||||||
sender=ticket.queue.from_address,
|
recipients=ticket.assigned_to.email,
|
||||||
fail_silently=True,
|
sender=ticket.queue.from_address,
|
||||||
)
|
fail_silently=True,
|
||||||
|
)
|
||||||
|
|
||||||
if ticket.queue.updated_ticket_cc:
|
if ticket.queue.updated_ticket_cc:
|
||||||
if reassigned:
|
if reassigned:
|
||||||
@ -636,3 +637,19 @@ def delete_saved_query(request, id):
|
|||||||
'query': query,
|
'query': query,
|
||||||
}))
|
}))
|
||||||
delete_saved_query = login_required(delete_saved_query)
|
delete_saved_query = login_required(delete_saved_query)
|
||||||
|
|
||||||
|
def user_settings(request):
|
||||||
|
s = request.user.usersettings
|
||||||
|
if request.POST:
|
||||||
|
form = UserSettingsForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
s.settings = form.cleaned_data
|
||||||
|
s.save()
|
||||||
|
else:
|
||||||
|
form = UserSettingsForm(s.settings)
|
||||||
|
|
||||||
|
return render_to_response('helpdesk/user_settings.html',
|
||||||
|
RequestContext(request, {
|
||||||
|
'form': form,
|
||||||
|
}))
|
||||||
|
user_settings = login_required(user_settings)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user