Recreate JIRA Service Desk in Python & Flask

When SaaS doesn't cut it, beat it down and take everything its got

When it comes to SaaS products in the realm of Service desks, JIRA Service Desk is at the very least, just as powerful as the next solution (Zendesk comes to mind). This naturally begs the question: Why is JIRA Service Desk's pricing model roughly 1/10th of that of it's competitor?

The answer lies within ease of use, but more importantly, presentation. While Atlassian's cloud offering is slowly playing catchup, Atlassian as a company has never quite seemed to nail down the importance of a friendly user interface, nor the importance of this when it comes to worldwide adoption. To see what I mean, here's an example that Atlassian themselves tout on their own documentation as a "ready for production" customer portal:

https://confluence.atlassian.com/confeval/jira-service-desk-evaluator-resources/jira-service-desk-customer-portal

To your average software developer (Atlasian's core demographic for years), one might see nothing wrong with this interface, but as somebody `who has rolled out over 30 of these desks for over 6 thousand people, layouts such as these commit numerous UI atrocities which simply are not acceptable for top enterprise software.

What do we do about this? We build an alternative, of course.

Method to This Madness

Our focus is not on JIRA as a product, but rather an API. Instead of attempting to work within JIRA’s boundaries via customization or add-ons, we can take matters into our own hands by owning the application that users use to interact with a JIRA instance. By using the JIRA API, we can not only extend features but actually ‘rebuild’ the application to gain full control over the UI or additional logic. JIRA is a hideous yet entirely necessary Java application, which makes it a perfect candidate for recreation.

We're going to use Flask for this. Shocking, I know. Here's the obligatory file structure of our project:

myproject
├─ app.py
├─ jira.py
├─ /static
│  └─ js
│  └─ less
│  └─ img
└─ /templates
   └─ layout.html
   └─ index.html

This tutorial will be working against the JIRA Server API for Service Desk - that said, Cloud users should still find this applicable.

Pulling Your Service Desk Form

Before we get nuts building our application, we’ll need to be sure that a Service Desk already exists in JIRA with our expected intake form. Remember: our end goal is to simply consume JIRA as an API, but that entails interacting with something that exists in the first place.

With your Service Desk created, there’s one annoyance we need to resolve before getting into code: determining your Service Desk’s ID number. Like most things in JIRA, Service Desks are identified by an ID which is simply an arbitrary grouping of integers in some way. What is the official way to find this ID, you might ask? Why, by extracting it from the portal’s URL or by inspecting your XHR requests, of course! Remember: JIRA hates you, that’s why we’re doing this in the first place.

With your Service Desk ID handy, we can finally break into retrieving our desk via the Service Desk API:

import requests
from jira_config import jira_username, jira_password

request_types_endpoint = "https://yourjirainstance.com/rest/servicedeskapi/servicedesk/[SERVICEDESKID]/requesttype/"

headers = {'Content-Type': 'application/json'}

def fieldsPerRequest(id):
    """Get form fields per request type."""
    r = requests.get(request_types_endpoint + id + '/field', auth=(jira_username, jira_password), headers=headers)
    form = r.json()
    return form


def serviceDeskRequestTypes():
    """Get request types."""
    request_array = []
    r = requests.get(request_types_endpoint, auth=(jira_username, jira_password), headers=headers)
    for requesttype in r.json()['values']:
        id = requesttype['id']
        request_dict = {
            'name': requesttype['name'],
            'description': requesttype['description'],
            'icon': requesttype['icon']['_links']['iconUrls']['32x32'],
            'fields': fieldsPerRequest(id)
        }
        request_array.append(request_dict)
    return request_array

serviceDeskRequestTypes()

By using the request_types_endpoint URL, our function serviceDeskRequestTypes() returns the request types of a given JIRA service desk; or in other words, the types of requests users can submit. This alone only gives us high-level information about the types of requests we allow but doesn't go into further detail such as the actual resulting form. That's where our next function comes in.

fieldsPerRequest()

This function gets passed the ID of each request type. With that, we extend our endpoint to look like 'https://yourjirainstance.com/rest/servicedeskapi/servicedesk/[SERVICEDESKID]/requesttype/[REQUESTID]/field'. Looping through each request type gives up exactly what we need: every request type and every form field per request type.

userSession()

There's another thing left to consider: what if the user isn't currently logged in to JIRA? At the very least, we need to check to see if a JIRA session is active:

user_session_endpoint = 'https://jira.we.co/rest/auth/1/session'

def getUserSession():
    """Get logged-in user."""
    r = requests.get(user_session_endpoint, headers=headers)
    if r.status_code == 200:
        return r.json()
    else:
        return False

If the user is logged in to JIRA, we'll receive a 200 code letting us know everything is alright. The body of the response will also contain the name of the user plus some extra metadata. What if the user isn't logged in? Let's get to that in a bit.

Easy Routing

Our view will be nice and simple:

from jira import request_forms, user_details

@app.route('/', methods=['GET', 'POST', 'OPTIONS'])
def home():
    """Landing page."""
    if user_details == False:
        return redirect('https://example.com')
    else:
        return render_template('/index.html', requests=request_forms)

Notice that all we're doing is making sure the user is signed in to JIRA. But what's with the example.com, you ask? Well, because I'm leaving this part up to you, dear friend. There's really a number of things we can do, but it depends entirely on your situation. For instance:

  • You can handle basic auth on your own
  • Register an OAuth application to handle sign-ins (perhaps the most robust solution)
  • If your JIRA instance is behind SSO, you may want to send users to your company's  SAML partner
  • Simply throw an error message

Whatever you decide to do, it's not really my problem. Obligatory smiley face emoji :).

The Template

Remember: the main reason most of you are probably doing this is to control the presentation layer as you see fit. That said, here comes a call of presentation layer text, in the form of a Jinja template:

{% block form %}
  <div>
    <h3 class="subtitle">Submit new requests here</h3>
    <ul class="collapsible">
      {% for request in requests %}
      <li class="{{request.name}}">
        <div class="collapsible-header">
          <img src="{{request.icon}}">
          <div class="info">
            <h5>{{request.name}}</h5>
            <p>{{request.description}}</p>
          </div>
        </div>
        <div class="collapsible-body">
          <div class="row">
            <form method="post">
              <div>
                {% for field in request.fields.requestTypeFields %}
                  {% if field.name in ('Category', 'Product') %}
                    <div class="input-field">
                      <select id="{{request.name}} {{field.name}}">
                        <option value="Choose your option" disabled selected>Choose your option</option>
                        {% for option in field.validValues %}
                          <option value="{{option.label}}">{{option.label}}</option>
                        {% endfor %}
                      </select>
                      <label>{{field.name}}</label>
                    </div>
                  {% elif field.name == 'Description' %}
                    <div class="input-field">
                      <textarea id="{{field.name}}" class="materialize-textarea" placeholder="{{field.description}}"></textarea>
                      <label for="{{request.name}} {{field.name}}">{{field.name}}</label>
                    </div>
                  {% else %}
                    <div class="input-field">
                      <input placeholder="{{field.description}}" id="{{request.name}} {{field.name}}" type="text" class="validate">
                      <label for="{{request.name}} {{field.name}}">{{field.name}}</label>
                    </div>
                  {% endif %}
                {% endfor %}
                <input type="button" value="Submit" class="btn cyan lighten-3 formsubmit">
              </div>
            </form>
          </div>
        </div>
      </li>
      {% endfor %}
    </ul>
  </div>
{% endblock %}

Because we passed the Service Desk JSON we extracted from the JIRA API to our form, we can go crazy setting our labels, placeholder text, or whatever, anywhere we please. In my example, I utilize Material Design's pretty package of pre-made frontend elements, because God knows nobody wants to deal with designing that shit. Sorry, I was just having a brief flashback to Frontend dev.

The code above explicitly looks for fields we know are dropdowns, so that we may fill the select options accordingly. Same goes for textarea fields. That said, the way I've handled this above is, well, stupid. Instead of hardcoding if statements to look for certain fields, leverage our JSON to determine the type of each field as you iterate over them. Do as I say, not as I do.

Going Further

There's so much more we can add here. Take a list of the user's opened tickets, for instance. The great thing about controlling your own portal UI is that you can now control the narrative of your own workload: perhaps the person in marketing who started 2 weeks ago could benefit from seeing the 200 tickets being addressed before her, thus second-guessing the urge to type URGENT across a subject line only to be violently shoved down your throat.

In all seriousness, nobody likes the experience of a vanilla helpdesk because it dehumanizes the customer. While our personal beliefs reassure us that we are special, entering a cold support queue is a stark suggestion that we may not be so special after all, which isn't exactly a fact Millennials or Executives like to ponder on. If nothing else, take this as a chance to build software friendly towards humans with full transparency, and both parties will surely benefit. Remember: happy humans bides times for the inevitable robot revolution on the horizon destined to eradicate mankind. Do your part!

Author image
New York City Website
Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.

Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.