diff --git a/helpdesk/forms.py b/helpdesk/forms.py index 076df45b..502a90bb 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -16,7 +16,7 @@ from django.contrib.auth.models import User from django.utils.translation import ugettext as _ from helpdesk.lib import send_templated_mail -from helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue +from helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue, TicketDependency from helpdesk.settings import HAS_TAG_SUPPORT class EditTicketForm(forms.ModelForm): @@ -574,3 +574,7 @@ class TicketCCForm(forms.ModelForm): model = TicketCC exclude = ('ticket',) +class TicketDependencyForm(forms.ModelForm): + class Meta: + model = TicketDependency + exclude = ('ticket',) diff --git a/helpdesk/migrations/0002_auto__add_ticketdependency__add_unique_ticketdependency_ticket_depends.py b/helpdesk/migrations/0002_auto__add_ticketdependency__add_unique_ticketdependency_ticket_depends.py new file mode 100644 index 00000000..47a1f321 --- /dev/null +++ b/helpdesk/migrations/0002_auto__add_ticketdependency__add_unique_ticketdependency_ticket_depends.py @@ -0,0 +1,237 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'TicketDependency' + db.create_table('helpdesk_ticketdependency', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('ticket', self.gf('django.db.models.fields.related.ForeignKey')(related_name='ticket', to=orm['helpdesk.Ticket'])), + ('depends_on', self.gf('django.db.models.fields.related.ForeignKey')(related_name='depends_on', to=orm['helpdesk.Ticket'])), + )) + db.send_create_signal('helpdesk', ['TicketDependency']) + + # Adding unique constraint on 'TicketDependency', fields ['ticket', 'depends_on'] + db.create_unique('helpdesk_ticketdependency', ['ticket_id', 'depends_on_id']) + + + def backwards(self, orm): + + # Removing unique constraint on 'TicketDependency', fields ['ticket', 'depends_on'] + db.delete_unique('helpdesk_ticketdependency', ['ticket_id', 'depends_on_id']) + + # Deleting model 'TicketDependency' + db.delete_table('helpdesk_ticketdependency') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'helpdesk.attachment': { + 'Meta': {'ordering': "['filename']", 'object_name': 'Attachment'}, + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'followup': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.FollowUp']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'size': ('django.db.models.fields.IntegerField', [], {}) + }, + 'helpdesk.customfield': { + 'Meta': {'object_name': 'CustomField'}, + 'data_type': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'decimal_places': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'help_text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': "'30'"}), + 'list_values': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'max_length': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'unique': 'True', 'db_index': 'True'}), + 'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'helpdesk.emailtemplate': { + 'Meta': {'ordering': "['template_name', 'locale']", 'object_name': 'EmailTemplate'}, + 'heading': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'html': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locale': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'plain_text': ('django.db.models.fields.TextField', [], {}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'template_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'helpdesk.escalationexclusion': { + 'Meta': {'object_name': 'EscalationExclusion'}, + 'date': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'queues': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['helpdesk.Queue']", 'symmetrical': 'False', 'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.followup': { + 'Meta': {'ordering': "['date']", 'object_name': 'FollowUp'}, + 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2011, 5, 10, 12, 20, 8, 45416)'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_status': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.Ticket']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.ignoreemail': { + 'Meta': {'object_name': 'IgnoreEmail'}, + 'date': ('django.db.models.fields.DateField', [], {'blank': 'True'}), + 'email_address': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'keep_in_mailbox': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'queues': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['helpdesk.Queue']", 'symmetrical': 'False', 'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.kbcategory': { + 'Meta': {'ordering': "['title']", 'object_name': 'KBCategory'}, + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'helpdesk.kbitem': { + 'Meta': {'ordering': "['title']", 'object_name': 'KBItem'}, + 'answer': ('django.db.models.fields.TextField', [], {}), + 'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.KBCategory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {}), + 'recommendations': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'votes': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'helpdesk.presetreply': { + 'Meta': {'ordering': "['name']", 'object_name': 'PreSetReply'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'queues': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['helpdesk.Queue']", 'symmetrical': 'False', 'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.queue': { + 'Meta': {'ordering': "('title',)", 'object_name': 'Queue'}, + 'allow_email_submission': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'allow_public_submission': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'email_box_host': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'email_box_imap_folder': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'email_box_interval': ('django.db.models.fields.IntegerField', [], {'default': "'5'", 'null': 'True', 'blank': 'True'}), + 'email_box_last_check': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'email_box_pass': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'email_box_port': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'email_box_ssl': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_box_type': ('django.db.models.fields.CharField', [], {'max_length': '5', 'null': 'True', 'blank': 'True'}), + 'email_box_user': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'escalate_days': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'locale': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'new_ticket_cc': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'updated_ticket_cc': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.savedsearch': { + 'Meta': {'object_name': 'SavedSearch'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'query': ('django.db.models.fields.TextField', [], {}), + 'shared': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'helpdesk.ticket': { + 'Meta': {'object_name': 'Ticket'}, + 'assigned_to': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'assigned_to'", 'blank': 'True', 'null': 'True', 'to': "orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_escalation': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}), + 'on_hold': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '3', 'blank': '3'}), + 'queue': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.Queue']"}), + 'resolution': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'submitter_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'helpdesk.ticketcc': { + 'Meta': {'object_name': 'TicketCC'}, + 'can_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'can_view': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.Ticket']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.ticketchange': { + 'Meta': {'object_name': 'TicketChange'}, + 'field': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'followup': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.FollowUp']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'old_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.ticketcustomfieldvalue': { + 'Meta': {'unique_together': "(('ticket', 'field'),)", 'object_name': 'TicketCustomFieldValue'}, + 'field': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.CustomField']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['helpdesk.Ticket']"}), + 'value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'helpdesk.ticketdependency': { + 'Meta': {'unique_together': "(('ticket', 'depends_on'),)", 'object_name': 'TicketDependency'}, + 'depends_on': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'depends_on'", 'to': "orm['helpdesk.Ticket']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ticket'", 'to': "orm['helpdesk.Ticket']"}) + }, + 'helpdesk.usersettings': { + 'Meta': {'object_name': 'UserSettings'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'settings_pickled': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['helpdesk'] diff --git a/helpdesk/models.py b/helpdesk/models.py index 804c770c..4520fd32 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -413,6 +413,16 @@ class Ticket(models.Model): ) staff_url = property(_get_staff_url) + def _can_be_resolved(self): + """ + Returns a boolean. + True = any dependencies are resolved + False = There are non-resolved dependencies + """ + OPEN_STATUSES = (Ticket.OPEN_STATUS, Ticket.REOPENED_STATUS) + return TicketDependency.objects.filter(ticket=self).filter(depends_on__status__in=OPEN_STATUSES).count() == 0 + can_be_resolved = property(_can_be_resolved) + if HAS_TAG_SUPPORT: tags = TagField(blank=True) @@ -1209,3 +1219,28 @@ class TicketCustomFieldValue(models.Model): class Meta: unique_together = ('ticket', 'field'), + + +class TicketDependency(models.Model): + """ + The ticket identified by `ticket` cannot be resolved until the ticket in `depends_on` has been resolved. + To help enforce this, a helper function `can_be_resolved` on each Ticket instance checks that + these have all been resolved. + """ + ticket = models.ForeignKey( + Ticket, + verbose_name=_('Ticket'), + related_name='ticketdependency', + ) + + depends_on = models.ForeignKey( + Ticket, + verbose_name=_('Depends On Ticket'), + related_name='depends_on', + ) + + def __unicode__(self): + return '%s / %s' % (self.ticket, self.depends_on) + + class Meta: + unique_together = ('ticket', 'depends_on') diff --git a/helpdesk/templates/helpdesk/ticket.html b/helpdesk/templates/helpdesk/ticket.html index 393a70c9..48372fb8 100644 --- a/helpdesk/templates/helpdesk/ticket.html +++ b/helpdesk/templates/helpdesk/ticket.html @@ -82,16 +82,17 @@
Adding a dependency will stop you resolving this ticket until the dependent ticket has been resolved or closed.
{% endblocktrans %} + + + +{% endblock %} diff --git a/helpdesk/templates/helpdesk/ticket_dependency_del.html b/helpdesk/templates/helpdesk/ticket_dependency_del.html new file mode 100644 index 00000000..2a2c99f2 --- /dev/null +++ b/helpdesk/templates/helpdesk/ticket_dependency_del.html @@ -0,0 +1,14 @@ +{% extends "helpdesk/base.html" %}{% load i18n %} + +{% block helpdesk_title %}{% trans "Delete Ticket Dependency" %}{% endblock %} + +{% block helpdesk_body %}{% blocktrans %} +Are you sure you wish to remove the dependency on this ticket?
+{% endblocktrans %} + + + + +{% endblock %} diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index 902e3cf0..01239590 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -35,6 +35,19 @@ {% endif %} +{% trans "This ticket cannot be resolved until the following ticket(s) are resolved" %}
{% trans "This ticket has no dependencies." %}
+ {% endfor %} + +