From f9c78c0ccc709509cdf7f83c45c898883d329db2 Mon Sep 17 00:00:00 2001
From: Paul Martin <paul@paulsputer.com>
Date: Mon, 20 Oct 2014 17:54:54 -0400
Subject: [PATCH] Tickets - Priority, Severity options

---
 src/main/java/com/gitblit/wicket/pages/TicketsPage.java      |    6 
 src/main/java/com/gitblit/tickets/QueryResult.java           |    4 
 src/main/java/com/gitblit/models/TicketModel.java            |  120 +++++++++++
 src/main/java/com/gitblit/wicket/pages/NewTicketPage.java    |   26 ++
 src/main/java/com/gitblit/wicket/pages/TicketPage.java       |    6 
 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties    |    8 
 src/main/java/com/gitblit/tickets/TicketIndexer.java         |    9 
 src/main/java/com/gitblit/wicket/panels/TicketListPanel.java |   12 +
 src/test/java/com/gitblit/tests/UITicketTest.java            |  151 +++++++++++++++
 src/main/java/com/gitblit/wicket/pages/TicketPage.html       |    2 
 src/main/java/com/gitblit/wicket/pages/NewTicketPage.html    |    2 
 src/test/java/com/gitblit/tests/GitBlitSuite.java            |    2 
 src/test/java/com/gitblit/tests/TicketServiceTest.java       |   44 ++++
 src/main/java/com/gitblit/wicket/pages/EditTicketPage.java   |   26 ++
 src/main/java/com/gitblit/wicket/panels/TicketListPanel.html |    3 
 src/main/java/com/gitblit/wicket/TicketsUI.java              |   47 ++++
 src/main/resources/gitblit.css                               |   74 +++++++
 src/main/java/com/gitblit/wicket/WicketUtils.java            |    6 
 src/main/java/com/gitblit/wicket/pages/EditTicketPage.html   |    2 
 src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java    |    4 
 20 files changed, 536 insertions(+), 18 deletions(-)

diff --git a/src/main/java/com/gitblit/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java
index 9bdb260..fd0b09e 100644
--- a/src/main/java/com/gitblit/models/TicketModel.java
+++ b/src/main/java/com/gitblit/models/TicketModel.java
@@ -91,6 +91,10 @@
 
 	public Integer deletions;
 
+	public Priority priority;
+	
+	public Severity severity;
+	
 	/**
 	 * Builds an effective ticket from the collection of changes.  A change may
 	 * Add or Subtract information from a ticket, but the collection of changes
@@ -141,6 +145,8 @@
 		changes = new ArrayList<Change>();
 		status = Status.New;
 		type = Type.defaultType;
+		priority = Priority.defaultPriority;
+		severity = Severity.defaultSeverity;
 	}
 
 	public boolean isOpen() {
@@ -516,6 +522,12 @@
 					break;
 				case mergeSha:
 					mergeSha = toString(value);
+					break;
+				case priority:
+					priority = TicketModel.Priority.fromObject(value, priority);
+					break;
+				case severity:
+					severity = TicketModel.Severity.fromObject(value, severity);
 					break;
 				default:
 					// unknown
@@ -1183,7 +1195,7 @@
 
 	public static enum Field {
 		title, body, responsible, type, status, milestone, mergeSha, mergeTo,
-		topic, labels, watchers, reviewers, voters, mentions;
+		topic, labels, watchers, reviewers, voters, mentions, priority, severity;
 	}
 
 	public static enum Type {
@@ -1310,4 +1322,110 @@
 			return null;
 		}
 	}
+
+	public static enum Priority {
+		Low(-1), Normal(0), High(1), Urgent(2);
+
+		public static Priority defaultPriority = Normal;
+
+		final int value;
+
+		Priority(int value) {
+			this.value = value;
+		}
+
+		public int getValue() {
+			return value;
+		}
+		
+		public static Priority [] choices() {
+			return new Priority [] { Urgent, High, Normal, Low };
+		}
+
+		@Override
+		public String toString() {
+			return name().toLowerCase().replace('_', ' ');
+		}
+
+		public static Priority fromObject(Object o, Priority defaultPriority) {
+			if (o instanceof Priority) {
+				// cast and return
+				return (Priority) o;
+			} else if (o instanceof String) {
+				// find by name
+				for (Priority priority : values()) {
+					String str = o.toString();
+					if (priority.name().equalsIgnoreCase(str)
+							|| priority.toString().equalsIgnoreCase(str)) {
+						return priority;
+					}
+				}
+			} else if (o instanceof Number) {
+
+				switch (((Number) o).intValue()) {
+					case -1: return Priority.Low;
+					case 0:  return Priority.Normal;
+					case 1:  return Priority.High;
+					case 2:  return Priority.Urgent;
+					default: return Priority.Normal;
+				}
+			}
+
+			return defaultPriority;
+		}
+	}
+	
+	public static enum Severity {
+		Unrated(-1), Negligible(1), Minor(2), Serious(3), Critical(4), Catastrophic(5);
+
+		public static Severity defaultSeverity = Unrated;
+		
+		final int value;
+		
+		Severity(int value) {
+			this.value = value;
+		}
+
+		public int getValue() {
+			return value;
+		}
+		
+		public static Severity [] choices() {
+			return new Severity [] { Unrated, Negligible, Minor, Serious, Critical, Catastrophic };
+		}
+
+		@Override
+		public String toString() {
+			return name().toLowerCase().replace('_', ' ');
+		}
+		
+		public static Severity fromObject(Object o, Severity defaultSeverity) {
+			if (o instanceof Severity) {
+				// cast and return
+				return (Severity) o;
+			} else if (o instanceof String) {
+				// find by name
+				for (Severity severity : values()) {
+					String str = o.toString();
+					if (severity.name().equalsIgnoreCase(str)
+							|| severity.toString().equalsIgnoreCase(str)) {
+						return severity;
+					}
+				}
+			} else if (o instanceof Number) {
+				
+				switch (((Number) o).intValue()) {
+					case -1: return Severity.Unrated;
+					case 1:  return Severity.Negligible;
+					case 2:  return Severity.Minor;
+					case 3:  return Severity.Serious;
+					case 4:  return Severity.Critical;
+					case 5:  return Severity.Catastrophic;
+					default: return Severity.Unrated;
+				}
+			}
+
+			return defaultSeverity;
+		}
+	}
 }
diff --git a/src/main/java/com/gitblit/tickets/QueryResult.java b/src/main/java/com/gitblit/tickets/QueryResult.java
index 7a2b1ab..f8d6d12 100644
--- a/src/main/java/com/gitblit/tickets/QueryResult.java
+++ b/src/main/java/com/gitblit/tickets/QueryResult.java
@@ -24,6 +24,8 @@
 import com.gitblit.models.TicketModel.Patchset;
 import com.gitblit.models.TicketModel.Status;
 import com.gitblit.models.TicketModel.Type;
+import com.gitblit.models.TicketModel.Priority;
+import com.gitblit.models.TicketModel.Severity;
 import com.gitblit.utils.StringUtils;
 
 /**
@@ -62,6 +64,8 @@
 	public int commentsCount;
 	public int votesCount;
 	public int approvalsCount;
+	public Priority priority;
+	public Severity severity;
 
 	public int docId;
 	public int totalResults;
diff --git a/src/main/java/com/gitblit/tickets/TicketIndexer.java b/src/main/java/com/gitblit/tickets/TicketIndexer.java
index 11ea3a7..e2d53af 100644
--- a/src/main/java/com/gitblit/tickets/TicketIndexer.java
+++ b/src/main/java/com/gitblit/tickets/TicketIndexer.java
@@ -103,7 +103,10 @@
 		mergesha(Type.STRING),
 		mergeto(Type.STRING),
 		patchsets(Type.INT),
-		votes(Type.INT);
+		votes(Type.INT),
+		//NOTE: Indexing on the underlying value to allow flexibility on naming
+		priority(Type.INT),
+		severity(Type.INT);
 
 		final Type fieldType;
 
@@ -519,6 +522,8 @@
 		toDocField(doc, Lucene.watchedby, StringUtils.flattenStrings(ticket.getWatchers(), ";").toLowerCase());
 		toDocField(doc, Lucene.mentions, StringUtils.flattenStrings(ticket.getMentions(), ";").toLowerCase());
 		toDocField(doc, Lucene.votes, ticket.getVoters().size());
+		toDocField(doc, Lucene.priority, ticket.priority.getValue());
+		toDocField(doc, Lucene.severity, ticket.severity.getValue());
 
 		List<String> attachments = new ArrayList<String>();
 		for (Attachment attachment : ticket.getAttachments()) {
@@ -600,6 +605,8 @@
 		result.participants = unpackStrings(doc, Lucene.participants);
 		result.watchedby = unpackStrings(doc, Lucene.watchedby);
 		result.mentions = unpackStrings(doc, Lucene.mentions);
+		result.priority = TicketModel.Priority.fromObject(unpackInt(doc, Lucene.priority), TicketModel.Priority.defaultPriority);
+		result.severity = TicketModel.Severity.fromObject(unpackInt(doc, Lucene.severity), TicketModel.Severity.defaultSeverity);
 
 		if (!StringUtils.isEmpty(doc.get(Lucene.patchset.name()))) {
 			// unpack most recent patchset
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index 2760f85..5326042 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -742,4 +742,10 @@
 gb.permission = Permission
 gb.sshKeyPermissionDescription = Specify the access permission for the SSH key
 gb.transportPreference = Transport Preference
-gb.transportPreferenceDescription = Set the transport that you prefer to use for cloning
\ No newline at end of file
+gb.transportPreferenceDescription = Set the transport that you prefer to use for cloning
+gb.priority = priority
+gb.severity = severity
+gb.sortHighestPriority = highest priority
+gb.sortLowestPriority = lowest priority
+gb.sortHighestSeverity = highest severity
+gb.sortLowestSeverity = lowest severity
diff --git a/src/main/java/com/gitblit/wicket/TicketsUI.java b/src/main/java/com/gitblit/wicket/TicketsUI.java
index 347ac44..2367c98 100644
--- a/src/main/java/com/gitblit/wicket/TicketsUI.java
+++ b/src/main/java/com/gitblit/wicket/TicketsUI.java
@@ -21,6 +21,8 @@
 import org.apache.wicket.markup.html.basic.Label;
 
 import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Priority;
+import com.gitblit.models.TicketModel.Severity;
 import com.gitblit.models.TicketModel.Status;
 import com.gitblit.models.TicketModel.Type;
 import com.gitblit.utils.StringUtils;
@@ -36,7 +38,7 @@
 	public static final String [] openStatii = new String [] { Status.New.name().toLowerCase(), Status.Open.name().toLowerCase() };
 
 	public static final String [] closedStatii = new String [] { "!" + Status.New.name().toLowerCase(), "!" + Status.Open.name().toLowerCase() };
-
+	
 	public static Label getStateIcon(String wicketId, TicketModel ticket) {
 		return getStateIcon(wicketId, ticket.type, ticket.status);
 	}
@@ -48,28 +50,59 @@
 		}
 		switch (type) {
 		case Proposal:
-			WicketUtils.setCssClass(label, "fa fa-code-fork");
+			WicketUtils.setCssClass(label, "fa fa-code-fork fa-fw");
 			break;
 		case Bug:
-			WicketUtils.setCssClass(label, "fa fa-bug");
+			WicketUtils.setCssClass(label, "fa fa-bug fa-fw");
 			break;
 		case Enhancement:
-			WicketUtils.setCssClass(label, "fa fa-magic");
+			WicketUtils.setCssClass(label, "fa fa-magic fa-fw");
 			break;
 		case Question:
-			WicketUtils.setCssClass(label, "fa fa-question");
+			WicketUtils.setCssClass(label, "fa fa-question fa-fw");
 			break;
 		case Maintenance:
-			WicketUtils.setCssClass(label, "fa fa-cogs");
+			WicketUtils.setCssClass(label, "fa fa-cogs fa-fw");
 			break;
 		default:
 			// standard ticket
-			WicketUtils.setCssClass(label, "fa fa-ticket");
+			WicketUtils.setCssClass(label, "fa fa-ticket fa-fw");
 		}
 		WicketUtils.setHtmlTooltip(label, getTypeState(type, state));
+		
 		return label;
 	}
+	
+	public static Label getPriorityIcon(String wicketId, Priority priority) {
+		Label label = new Label(wicketId);
+		if (priority == null) {
+			priority = Priority.defaultPriority;
+		}
+		switch (priority) {
+		case Urgent:
+			WicketUtils.setCssClass(label, "fa fa-step-forward fa-rotate-270");
+			break;
+		case High:
+			WicketUtils.setCssClass(label, "fa fa-caret-up fa-lg");
+			break;
+		case Low:
+			WicketUtils.setCssClass(label, "fa fa-caret-down fa-lg");
+			break;
+		default:
+		}
+		WicketUtils.setHtmlTooltip(label, priority.toString());
+		
+		return label;
+	}
+	
+	public static String getPriorityClass(Priority priority) {
+		return String.format("priority-%s", priority);
+	}
 
+	public static String getSeverityClass(Severity severity) {
+		return String.format("severity-%s", severity);	
+	}
+	
 	public static String getTypeState(Type type, Status state) {
 		return state.toString() + " " + type.toString();
 	}
diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java
index d47390d..9a40931 100644
--- a/src/main/java/com/gitblit/wicket/WicketUtils.java
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -29,12 +29,14 @@
 import org.apache.wicket.Component;
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.Request;
+import org.apache.wicket.behavior.AttributeAppender;
 import org.apache.wicket.behavior.HeaderContributor;
 import org.apache.wicket.behavior.SimpleAttributeModifier;
 import org.apache.wicket.markup.html.IHeaderContributor;
 import org.apache.wicket.markup.html.IHeaderResponse;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.image.ContextImage;
+import org.apache.wicket.model.Model;
 import org.apache.wicket.protocol.http.WebRequest;
 import org.apache.wicket.resource.ContextRelativeResource;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
@@ -56,6 +58,10 @@
 		container.add(new SimpleAttributeModifier("class", value));
 	}
 
+	public static void addCssClass(Component container, String value) {
+		container.add(new AttributeAppender("class", new Model<String>(value), " "));
+	}
+	
 	public static void setCssStyle(Component container, String value) {
 		container.add(new SimpleAttributeModifier("style", value));
 	}
diff --git a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html
index b5fe0ae..e11aed8 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html
@@ -41,6 +41,8 @@
 			<tr wicket:id="status"></tr>
 			<tr wicket:id="responsible"></tr>
 			<tr wicket:id="milestone"></tr>
+			<tr><th><wicket:message key="gb.priority"></wicket:message></th><td class="edit"><select class="input-large" wicket:id="priority"></select></td></tr>
+			<tr><th><wicket:message key="gb.severity"></wicket:message></th><td class="edit"><select class="input-large" wicket:id="severity"></select></td></tr>
 			<tr wicket:id="mergeto"></tr>
 		</table>
 	</div>
diff --git a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java
index c3d405b..7c02c0b 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java
@@ -83,6 +83,10 @@
 	private IModel<TicketMilestone> milestoneModel;
 
 	private Label descriptionPreview;
+	
+	private IModel<TicketModel.Priority> priorityModel;
+	
+	private IModel<TicketModel.Severity> severityModel;
 
 	public EditTicketPage(PageParameters params) {
 		super(params);
@@ -117,6 +121,8 @@
 		milestoneModel = Model.of();
 		mergeToModel = Model.of(ticket.mergeTo == null ? getRepositoryModel().mergeTo : ticket.mergeTo);
 		statusModel = Model.of(ticket.status);
+		priorityModel = Model.of(ticket.priority);
+		severityModel = Model.of(ticket.severity);
 
 		setStatelessHint(false);
 		setOutputMarkupId(true);
@@ -219,6 +225,14 @@
 			milestone.add(new DropDownChoice<TicketMilestone>("milestone", milestoneModel, milestones));
 			form.add(milestone.setVisible(!milestones.isEmpty()));
 
+			// priority
+			List<TicketModel.Priority> priorityChoices = Arrays.asList(TicketModel.Priority.choices());
+			form.add(new DropDownChoice<TicketModel.Priority>("priority", priorityModel, priorityChoices));
+			
+			// severity
+			List<TicketModel.Severity> severityChoices = Arrays.asList(TicketModel.Severity.choices());
+			form.add(new DropDownChoice<TicketModel.Severity>("severity", severityModel, severityChoices));
+						
 			// mergeTo (integration branch)
 			List<String> branches = new ArrayList<String>();
 			for (String branch : getRepositoryModel().getLocalBranches()) {
@@ -315,7 +329,19 @@
 						change.setField(Field.milestone, milestone.name);
 					}
 				}
+				
+				TicketModel.Priority priority = priorityModel.getObject();
+				if (!ticket.priority.equals(priority))
+				{
+					change.setField(Field.priority, priority);
+				}
 
+				TicketModel.Severity severity = severityModel.getObject();
+				if (!ticket.severity.equals(severity))
+				{
+					change.setField(Field.severity, severity);
+				}
+				
 				String mergeTo = mergeToModel.getObject();
 				if ((StringUtils.isEmpty(ticket.mergeTo) && !StringUtils.isEmpty(mergeTo))
 						|| (!StringUtils.isEmpty(mergeTo) && !mergeTo.equals(ticket.mergeTo))) {
diff --git a/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java b/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
index 1396a5e..187302f 100644
--- a/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
@@ -230,6 +230,10 @@
 		sortChoices.add(new TicketSort(getString("gb.sortLeastPatchsetRevisions"), Lucene.patchsets.name(), false));
 		sortChoices.add(new TicketSort(getString("gb.sortMostVotes"), Lucene.votes.name(), true));
 		sortChoices.add(new TicketSort(getString("gb.sortLeastVotes"), Lucene.votes.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortHighestPriority"), Lucene.priority.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLowestPriority"), Lucene.priority.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortHighestSeverity"), Lucene.severity.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLowestSeverity"), Lucene.severity.name(), false));
 
 		TicketSort currentSort = sortChoices.get(0);
 		for (TicketSort ts : sortChoices) {
diff --git a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html
index 447c6aa..7b71ada 100644
--- a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html
@@ -41,6 +41,8 @@
 			<tr><th><wicket:message key="gb.type"></wicket:message><span style="color:red;">*</span></th><td class="edit"><select class="input-large" wicket:id="type"></select></td></tr>
 			<tr wicket:id="responsible"></tr>
 			<tr wicket:id="milestone"></tr>
+			<tr><th><wicket:message key="gb.priority"></wicket:message></th><td class="edit"><select class="input-large" wicket:id="priority"></select></td></tr>
+			<tr><th><wicket:message key="gb.severity"></wicket:message></th><td class="edit"><select class="input-large" wicket:id="severity"></select></td></tr>
 			<tr wicket:id="mergeto"></tr>
 		</table>
 	</div>
diff --git a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java
index 8f28055..ea3e960 100644
--- a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java
@@ -75,6 +75,10 @@
 	private IModel<TicketMilestone> milestoneModel;
 
 	private Label descriptionPreview;
+	
+	private IModel<TicketModel.Priority> priorityModel;
+	
+	private IModel<TicketModel.Severity> severityModel;
 
 	public NewTicketPage(PageParameters params) {
 		super(params);
@@ -95,6 +99,8 @@
 		mergeToModel = Model.of(Repository.shortenRefName(getRepositoryModel().mergeTo));
 		responsibleModel = Model.of();
 		milestoneModel = Model.of();
+		severityModel = Model.of(TicketModel.Severity.defaultSeverity);
+		priorityModel = Model.of(TicketModel.Priority.defaultPriority);
 
 		setStatelessHint(false);
 		setOutputMarkupId(true);
@@ -152,6 +158,12 @@
 			milestone.add(new DropDownChoice<TicketMilestone>("milestone", milestoneModel, milestones));
 			form.add(milestone.setVisible(!milestones.isEmpty()));
 
+			// priority
+			form.add(new DropDownChoice<TicketModel.Priority>("priority", priorityModel, Arrays.asList(TicketModel.Priority.choices())));
+			
+			//severity
+			form.add(new DropDownChoice<TicketModel.Severity>("severity", severityModel, Arrays.asList(TicketModel.Severity.choices())));
+			
 			// integration branch
 			List<String> branches = new ArrayList<String>();
 			for (String branch : getRepositoryModel().getLocalBranches()) {
@@ -211,6 +223,20 @@
 				if (milestone != null) {
 					change.setField(Field.milestone, milestone.name);
 				}
+				
+				// severity
+				TicketModel.Severity severity = TicketModel.Severity.defaultSeverity;
+				if (severityModel.getObject() != null) {
+					severity = severityModel.getObject();
+				}
+				change.setField(Field.severity, severity);
+				
+				// priority
+				TicketModel.Priority priority = TicketModel.Priority.defaultPriority;
+				if (priorityModel.getObject() != null) {
+					priority = priorityModel.getObject();
+				}
+				change.setField(Field.priority, priority);
 
 				// integration branch
 				String mergeTo = mergeToModel.getObject();
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.html b/src/main/java/com/gitblit/wicket/pages/TicketPage.html
index f3f38ec..f3c6b2a 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.html
@@ -67,6 +67,8 @@
 				<div style="border: 1px solid #ccc;padding: 10px;margin: 5px 0px;">
 					<table class="summary" style="width: 100%">
 						<tr><th><wicket:message key="gb.type"></wicket:message></th><td><span wicket:id="ticketType">[type]</span></td></tr>
+						<tr><th><wicket:message key="gb.priority"></wicket:message></th><td><span wicket:id="priority">[priority]</span></td></tr>
+						<tr><th><wicket:message key="gb.severity"></wicket:message></th><td><span wicket:id="severity">[severity]</span></td></tr>
 						<tr><th><wicket:message key="gb.topic"></wicket:message></th><td><span wicket:id="ticketTopic">[topic]</span></td></tr>
 						<tr><th><wicket:message key="gb.responsible"></wicket:message></th><td><span wicket:id="responsible">[responsible]</span></td></tr>
 						<tr><th><wicket:message key="gb.milestone"></wicket:message></th><td><span wicket:id="milestone">[milestone]</span></td></tr>
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
index c5b0f31..b140bfc 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -519,6 +519,10 @@
 		 * TICKET METADATA
 		 */
 		add(new Label("ticketType", ticket.type.toString()));
+		
+		add(new Label("priority", ticket.priority.toString()));
+		add(new Label("severity", ticket.severity.toString()));
+		
 		if (StringUtils.isEmpty(ticket.topic)) {
 			add(new Label("ticketTopic").setVisible(false));
 		} else {
@@ -527,6 +531,8 @@
 			String safeTopic = app().xssFilter().relaxed(topic);
 			add(new Label("ticketTopic", safeTopic).setEscapeModelStrings(false));
 		}
+		
+		
 
 
 		/*
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
index 745cabf..ecfed25 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
@@ -464,7 +464,11 @@
 		sortChoices.add(new TicketSort(getString("gb.sortLeastPatchsetRevisions"), Lucene.patchsets.name(), false));
 		sortChoices.add(new TicketSort(getString("gb.sortMostVotes"), Lucene.votes.name(), true));
 		sortChoices.add(new TicketSort(getString("gb.sortLeastVotes"), Lucene.votes.name(), false));
-
+		sortChoices.add(new TicketSort(getString("gb.sortHighestPriority"), Lucene.priority.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLowestPriority"), Lucene.priority.name(), false));
+		sortChoices.add(new TicketSort(getString("gb.sortHighestSeverity"), Lucene.severity.name(), true));
+		sortChoices.add(new TicketSort(getString("gb.sortLowestSeverity"), Lucene.severity.name(), false));
+		
 		TicketSort currentSort = sortChoices.get(0);
 		for (TicketSort ts : sortChoices) {
 			if (ts.sortBy.equals(sortBy) && desc == ts.desc) {
diff --git a/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html
index 30f5036..659baea 100644
--- a/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.html
@@ -30,6 +30,9 @@
         	<td class="hidden-phone ticket-list-state">
        			<i wicket:message="title:gb.watching" style="color:#888;" class="fa fa-eye" wicket:id="watching"></i>
         	</td>
+        	<td class="ticket-list-priority">
+       			<div wicket:id="priority"></div>
+        	</td>
         	<td class="ticket-list-state">
        			<div wicket:id="status"></div>
         	</td>
diff --git a/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java
index cc0b57a..b4c43cb 100644
--- a/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/TicketListPanel.java
@@ -53,7 +53,7 @@
 public class TicketListPanel extends BasePanel {
 
 	private static final long serialVersionUID = 1L;
-
+	
 	public TicketListPanel(String wicketId, List<QueryResult> list, final boolean showSwatch, final boolean showRepository) {
 		super(wicketId);
 
@@ -83,7 +83,10 @@
 					item.add(new Label("ticketsLink").setVisible(false));
 				}
 
-				item.add(TicketsUI.getStateIcon("state", ticket.type, ticket.status));
+				Label icon = TicketsUI.getStateIcon("state", ticket.type, ticket.status);
+				WicketUtils.addCssClass(icon, TicketsUI.getSeverityClass(ticket.severity));
+				item.add(icon);
+				
 				item.add(new Label("id", "" + ticket.number));
 				UserModel creator = app().users().getUserModel(ticket.createdBy);
 				if (creator != null) {
@@ -167,6 +170,11 @@
 				// watching indicator
 				item.add(new Label("watching").setVisible(ticket.isWatching(GitBlitWebSession.get().getUsername())));
 
+				// priority indicator
+				Label priorityIcon = TicketsUI.getPriorityIcon("priority", ticket.priority);
+				WicketUtils.addCssClass(priorityIcon, TicketsUI.getPriorityClass(ticket.priority));
+				item.add(priorityIcon.setVisible(true));
+				
 				// status indicator
 				String css = TicketsUI.getLozengeClass(ticket.status, true);
 				Label l = new Label("status", ticket.status.toString());
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 748a319..ba6f534 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -782,6 +782,10 @@
 
 td.ticket-list-state {
 	vertical-align: middle;
+}
+
+td.ticket-list-priority {
+	vertical-align: middle;
 }
 
 .ticket-list-details {
@@ -2079,4 +2083,72 @@
     background-color: #fff;
     border-color: #ece7e2;
     color: #815b3a;
-}
\ No newline at end of file
+}
+.severity-catastrophic {
+	color:#CC79A7;
+}
+.severity-catastrophic:after {
+	font-family: Helvetica,arial,freesans,clean,sans-serif ;
+	content: "Ca";
+	font-weight:900;
+	font-size:.6em;	
+	font-variant:small-caps;
+	display:flex;
+}
+.severity-critical {
+	color:#D55E00;
+}
+.severity-critical:after {
+	font-family: Helvetica,arial,freesans,clean,sans-serif ;
+	content: "c";
+	font-weight:900;
+	font-size:.6em;	
+	font-variant:small-caps;
+	display:flex;
+}
+.severity-serious {
+	color:#E69F00;
+}
+.severity-serious:after {
+	font-family: Helvetica,arial,freesans,clean,sans-serif ;
+	content: "s";
+	font-weight:900;
+	font-size:.6em;	
+	font-variant:small-caps;
+	display:flex;
+}
+.severity-minor {
+	color:#0072B2;
+}
+.severity-minor:after {
+	font-family: Helvetica,arial,freesans,clean,sans-serif ;
+	content: "m";
+	font-weight:900;
+	font-size:.6em;	
+	font-variant:small-caps;
+	display:flex;
+}
+.severity-negligible {
+	color:#009E73;
+}
+.severity-negligible:after {
+	font-family: Helvetica,arial,freesans,clean,sans-serif ;
+	content: "n";
+	font-weight:900;
+	font-size:.6em;	
+	font-variant:small-caps;
+	display:flex;
+}
+.severity-unrated {
+}
+.priority-urgent {
+	color:#CC79A7;
+}
+.priority-high {
+	color:#D55E00;
+}
+.priority-normal {
+}
+.priority-low {
+	color:#0072B2;
+}
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index 5a7dcea..f2dfcc0 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -65,7 +65,7 @@
 		FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class,
 		ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class,
 		BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class,
-		SshKeysDispatcherTest.class })
+		SshKeysDispatcherTest.class, UITicketTest.class })
 public class GitBlitSuite {
 
 	public static final File BASEFOLDER = new File("data");
diff --git a/src/test/java/com/gitblit/tests/TicketServiceTest.java b/src/test/java/com/gitblit/tests/TicketServiceTest.java
index 1676e34..c654383 100644
--- a/src/test/java/com/gitblit/tests/TicketServiceTest.java
+++ b/src/test/java/com/gitblit/tests/TicketServiceTest.java
@@ -293,9 +293,47 @@
 			assertTrue("failed to delete label " + label.name, service.deleteLabel(getRepository(), label.name, "lucifer"));
 		}
 	}
-
-
-
+	
+	@Test
+	public void testPriorityAndSeverity() throws Exception {
+		// C1: create and insert a ticket
+		Change c1 = newChange("testPriorityAndSeverity() " + Long.toHexString(System.currentTimeMillis()));
+		TicketModel ticket = service.createTicket(getRepository(), c1);
+		assertTrue(ticket.number > 0);
+		assertEquals(TicketModel.Priority.Normal, ticket.priority);
+		assertEquals(TicketModel.Severity.Unrated, ticket.severity);
+		
+		TicketModel constructed = service.getTicket(getRepository(), ticket.number);
+		compare(ticket, constructed);
+		
+		// C2: Change Priority max
+		Change c2 = new Change("C2");
+		c2.setField(Field.priority, TicketModel.Priority.Urgent);
+		constructed = service.updateTicket(getRepository(), ticket.number, c2);
+		assertNotNull(constructed);
+		assertEquals(2, constructed.changes.size());
+		assertEquals(TicketModel.Priority.Urgent, constructed.priority);
+		assertEquals(TicketModel.Severity.Unrated, constructed.severity);
+		
+		// C3: Change Severity max
+		Change c3 = new Change("C3");
+		c3.setField(Field.severity, TicketModel.Severity.Catastrophic);
+		constructed = service.updateTicket(getRepository(), ticket.number, c3);
+		assertNotNull(constructed);
+		assertEquals(3, constructed.changes.size());
+		assertEquals(TicketModel.Priority.Urgent, constructed.priority);
+		assertEquals(TicketModel.Severity.Catastrophic, constructed.severity);
+		
+		// C4: Change Priority min
+		Change c4 = new Change("C3");
+		c4.setField(Field.priority, TicketModel.Priority.Low);
+		constructed = service.updateTicket(getRepository(), ticket.number, c4);
+		assertNotNull(constructed);
+		assertEquals(4, constructed.changes.size());
+		assertEquals(TicketModel.Priority.Low, constructed.priority);
+		assertEquals(TicketModel.Severity.Catastrophic, constructed.severity);
+	}
+	
 	private Change newChange(String summary) {
 		Change change = new Change("C1");
 		change.setField(Field.title, summary);
diff --git a/src/test/java/com/gitblit/tests/UITicketTest.java b/src/test/java/com/gitblit/tests/UITicketTest.java
new file mode 100644
index 0000000..54aa1e1
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/UITicketTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.tests;
+
+import java.io.File;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.bouncycastle.util.Arrays;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.manager.INotificationManager;
+import com.gitblit.manager.IPluginManager;
+import com.gitblit.manager.IRepositoryManager;
+import com.gitblit.manager.IRuntimeManager;
+import com.gitblit.manager.IUserManager;
+import com.gitblit.manager.NotificationManager;
+import com.gitblit.manager.PluginManager;
+import com.gitblit.manager.RepositoryManager;
+import com.gitblit.manager.RuntimeManager;
+import com.gitblit.manager.UserManager;
+import com.gitblit.models.Mailing;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Attachment;
+import com.gitblit.models.TicketModel.Change;
+import com.gitblit.models.TicketModel.Field;
+import com.gitblit.models.TicketModel.Patchset;
+import com.gitblit.models.TicketModel.Priority;
+import com.gitblit.models.TicketModel.Severity;
+import com.gitblit.models.TicketModel.Status;
+import com.gitblit.models.TicketModel.Type;
+import com.gitblit.tests.mock.MemorySettings;
+import com.gitblit.tickets.ITicketService;
+import com.gitblit.tickets.ITicketService.TicketFilter;
+import com.gitblit.tickets.QueryResult;
+import com.gitblit.tickets.TicketIndexer.Lucene;
+import com.gitblit.tickets.BranchTicketService;
+import com.gitblit.tickets.TicketLabel;
+import com.gitblit.tickets.TicketMilestone;
+import com.gitblit.tickets.TicketNotifier;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.XssFilter;
+import com.gitblit.utils.XssFilter.AllowXssFilter;
+
+/**
+ * Generates the range of tickets to ease testing of the look and feel of tickets
+ */
+public class UITicketTest extends GitblitUnitTest {
+
+	private ITicketService service;	
+	final String repoName = "UITicketTest.git";
+	final RepositoryModel repo = new RepositoryModel(repoName, null, null, null);
+	
+	protected ITicketService getService(boolean deleteAll) throws Exception {
+
+		IStoredSettings settings = getSettings(deleteAll);
+		XssFilter xssFilter = new AllowXssFilter();
+		IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start();
+		IPluginManager pluginManager = new PluginManager(runtimeManager).start();
+		INotificationManager notificationManager = new NotificationManager(settings).start();
+		IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
+		IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
+
+		BranchTicketService service = new BranchTicketService(
+				runtimeManager,
+				pluginManager,
+				notificationManager,
+				userManager,
+				repositoryManager).start();
+
+		if (deleteAll) {
+			service.deleteAll(repo);
+		}
+		return service;
+	}
+	
+	protected IStoredSettings getSettings(boolean deleteAll) throws Exception {
+		File dir = new File(GitBlitSuite.REPOSITORIES, repoName);
+		if (deleteAll) {
+			FileUtils.deleteDirectory(dir);
+			JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, repoName).close();
+		}
+		
+		File luceneDir = new File(dir, "tickets/lucene");
+		luceneDir.mkdirs();
+
+		Map<String, Object> map = new HashMap<String, Object>();
+		map.put(Keys.git.repositoriesFolder, GitBlitSuite.REPOSITORIES.getAbsolutePath());
+		map.put(Keys.tickets.indexFolder, luceneDir.getAbsolutePath());
+
+		IStoredSettings settings = new MemorySettings(map);
+		return settings;
+	}
+
+	@Before
+	public void setup() throws Exception {
+		service = getService(true);
+	}
+
+	@After
+	public void cleanup() {
+		service.stop();
+	}
+
+	@Test
+	public void UITicketOptions() throws Exception {
+		
+		for (TicketModel.Type t : TicketModel.Type.values())
+		{
+			for (TicketModel.Priority p : TicketModel.Priority.values())
+			{
+				for (TicketModel.Severity s : TicketModel.Severity.values())
+				{
+					assertNotNull(service.createTicket(repo, newChange(t, p, s)));
+				}
+			}	
+		}
+	}
+	
+	private Change newChange(Type type, Priority priority, Severity severity) {
+		Change change = new Change("JUnit");
+		change.setField(Field.title, String.format("Type: %s | Priority: %s | Severity: %s", type, priority, severity));
+		change.setField(Field.type, type);
+		change.setField(Field.severity, severity);
+		change.setField(Field.priority, priority);
+		return change;
+	}
+
+}
\ No newline at end of file

--
Gitblit v1.9.1