There seems to be some backlash going on against the religion of “Agile Software Development” and it is best summarized by PragDave, reminding us that the “Agile Manifesto” first places “Individuals and interactions over processes and tools” — there are now a lot of Agile Processes and Tools which you can buy in to . . .
He then summarizes how to work agilely:
What to do:
Find out where you are
Take a small step towards your goal
Adjust your understanding based on what you learned
Repeat
How to do it:
When faced with two or more alternatives that deliver roughly the same value, take the path that makes future change easier.
Sounds like sensible advice. I think I’ll print that out and tape it on my display to help me keep focused.
Two weeks ago, I attended Atlassian Summit 2013 in San Francisco. Â This is an opportunity to train, network, and absorb propaganda about Atlassian products (JIRA, Greenhopper, Confluence, &c.) and ecosystem partners. Â I thought I would share a summary of some of the notes I took along the way, for anyone who might find interest:
At the Keynote, Atlassian launched some interesting products:
As time passes, the ticket gets crankier at you in real time about the SLA.
Jira Service Desk
Jira Service Desk is an extension to JIRA 6 oriented around IT needs.  The interesting features include:
Customer Portal with integrated KB search
Real-time visibility of ticket SLA status
The first thing helps people get their work done, and the second is manager catnip.
Confluence Knowledge Base
Confluence 5.3 features a shake-the-box Knowledge Base setup:
Improved template system — “blueprints” for different article types
Real-time search portal which integrates with JIRA Service Desk
My Questions: enforcing KB link with JIRA workflow and identifying “use count” as an article search metric
Other Stuff I looked into:
REST and Webhooks
There was a presentation on JIRA’s REST API, and mention of Webhooks.
Another feature for tight integration is Webhooks: you can configure JIRA so that certain issue actions trigger a hit to a remote URL. Â This is generally intended for building apps around JIRA. Â We might use this to implement Nagios ACKs.
Atlassian Connect
I haven’t looked too deeply as this is a JIRA 6 feature, but Atlassian Connect promises to be a new method of building JIRA extensions that is lighter-weight than their traditional plugin method.  (Plugins want you to set up Eclipse and build a Java Dev environment in your workstation… Connect sounds like just build something in your own technology stack around REST and Webhooks)
Cultivating Content: Designing Wiki Solutions that Scale
Rebecca Glassman, a tech writer at Opower, gave a really engaging talk that addresses a problem that seems commonplace: how to tame the wiki jungle! Â Her methodology went something like this:
Manage the wiki like it is a product: interview stakeholders, get some metrics, do UX testing
Metrics: Google Analytics, View Tracker Macro, Usage Macro
UX results at Opower revealed more reliance on Table of Contents vs Search (55%) and that users skip past top-level pages, so you don’t want to put content just on there
In search, users only look at the first 2-3 results before giving up
They engaged some users to track the questions they had and their success at getting answers from the wiki
The Docs people (2) built an “answer desk” situation where they took in Questions from across the company, and tracked their progress writing answers on a Kanban board
As they better learned user needs and what sort of knowledge there was, they built “The BOOK” (Body of Opower Knowledge) based on a National Parks model:
Most of the wiki is a vast wilderness, which you are free to explore
The BOOK is the nice, clean visitors center to help take care of most of your needs and help you prepare for your trek into the wilderness
The BOOK is a handbook, in its own space, with its own look-and-feel, and edits are welcome, but they are vetted by the Docs team via Ad Hoc Workflows
By having tracked Metrics from the get-go, they can quantify the utility of The BOOK …
(I have some more notes on how they built, launched, and promoted The BOOK. Â The problem they tackled sounds all to familiar and her approach is what I have always imagined as the sort of way to go.)
Ad Hoc Canvas
The Ad Hoc Canvas plugin for Confluence caught my eye.  At first glance, it is like Trello, or Kanban, where you fill out little cards and drag them around to track things.  But it has options to organize the information in different ways depending on the task at hand: wherever you are using a spreadsheet to track knowledge or work, Ad Hoc Canvas might be a much better solution.  Just look at the videos and you get an idea . . .
The Dark Art of Performance Tuning
Adaptavist gave a presentation on performance analysis of JIRA and Confluence. Â It was fairly high-level but the gist of this is that you want to monitor and trend the state of the JVM: memory, heap, garbage collection, filehandles, database connections, &c. Â He had some cool graphs of stuff like garbage collection events versus latency that had helped them to analyze issues for clients. Â One consideration is that each plugin and each code revision to a plugin brings a bunch of new code into the pool with its own potential for issues. Â Ideally, you can set up a load testing environment for your staging system. Â Short of that, the more system metrics that you can track, you can upgrade plugins one at a time and watch for any effects. Â As an example, one plugin upgrade went from reserving 30 database connections to reserving 150 database connections, and that messed up performance because the rest of the system would become starved of available database connections. Â (So, they figured that out and increased that resource..)
tl;dr: JIRA Performance Tuning is a variation of managing other JVM Applications
Collaboration For Executives
I popped in on this session near the end, but the takeaway for anyone who wants to deliver effective presentations to upper management are:
The presenter’s narrative was driven by an initial need to capture executive buy-in that their JIRA system was critical to business function and needed adequate resourcing.
As part of a project at work I’ve built some Jython code that builds iCalendar attachments to include meeting invitations for scheduled maintenance sessions. Jython is Python-in-Java which takes some getting used to but is damned handy when you’re working with JIRA. I will share a few anecdotes:
1) For doing date and time calculations, specifically to determine locale offset from UTC, you’re a lot happier calling the Java SimpleDateFormat stuff than you are dealing with Python. Python is a beautiful language but I burned a lot of time in an earlier version of this code figuring out how to convert between different time objects for manipulation and whatnot. This is not what you would expect from an intuitive, weakly-typed language, and it is interesting to find that the more obtuse, strongly-typed language handles time zones and it just fricking works.
from java.text import SimpleDateFormat
from com.atlassian.jira.timezone import TimeZoneManagerImpl
tzm = TimeZoneManagerImpl(ComponentManager.getInstance().getJiraAuthenticationContext(),
ComponentManager.getInstance().getUserPreferencesManager(),
ComponentManager.getInstance().getApplicationProperties())
# df_utc = DateFormat UTC
# df_assignee = DateFormat Assignee
df_utc = SimpleDateFormat("EEE yyyy-MM-dd HH:mm ZZZZZ (zzz)")
df_assignee = SimpleDateFormat("EEE yyyy-MM-dd HH:mm ZZZZZ (zzz)")
tz = df_utc.getTimeZone()
df_utc.setTimeZone(tz.getTimeZone("UTC"))
df_assignee.setTimeZone(tzm.getTimeZoneforUser(assignee))
issue_dict['Start_Time_text'] = df_utc.format(start_time.getTime())
issue_dict['Start_Time_html'] = df_utc.format(start_time.getTime())
if df_utc != df_assignee:
issue_dict['Start_Time_text'] += "\r\n "
issue_dict['Start_Time_text'] += df_assignee.format(start_time.getTime())
issue_dict['Start_Time_html'] += "<br />"
issue_dict['Start_Time_html'] += df_assignee.format(start_time.getTime())
# Get TimeZone of Assignee
# Start Time in Assignee TimeZone
Since our team is global I set up our announcement emails to render the time in UTC, and, if it is different, in the time zone of the person leading the change. For example:
We have a team in London. I have not yet tested it but as I understand it, once they leave BST, their timezone is UTC. I am looking forward to seeing if this understanding is correct.
As I understand it, I’m pulling the current time zone of the user, which changes when we enter and leave DST, which means that the local time will be dodgy when we send an announcement before the cutover for a time after the cut-over.
2) I was sending meeting invitations with the host set to the assignee of the maintenance event. This seemed reasonable to me, but when Mac Outlook saw that the host was set, it would not offer to add the event to the host’s calendar. After all, all meeting invitations come from Microsoft Outlook, right?! If I am the host it must already be on my calendar!!
I tried just not setting the host. This worked fine except now people would RSVP to the event and they would get an error stuck in their outboxes.
So . . . set the host to a bogus email address? My boss was like “just change the code to send two different invitations” which sounds easy enough for him but I know how creaky and fun to debug is my code. I came upon a better solution: I set the host address to user+calendar@domain.com. This way, Outlook is naive enough to believe the email address doesn’t match, but all our software which handles mail delivery knows the old ways of address extension . . . I can send one invitation, and have that much less messy code to maintain.
from icalendar import Calendar, Event, UTC, vText, vCalAddress
# [ . . . ]
event = Event()# [ . . . ]# THIS trick allows organizer to add event without breaking RSVP# decline functionality. (Outlook and its users suck.)
organizer_a = assignee.getEmailAddress().split('@')
organizer = vCalAddress('MAILTO:' + organizer_a[0]+ '+calendar@' +
organizer_a[1])
organizer.params['CN']= vText(assignee.getDisplayName() + ' (' + assignee.getName() + ')')
event['organizer']= organizer
from icalendar import Calendar, Event, UTC, vText, vCalAddress
# [ . . . ]
event = Event()
# [ . . . ]
# THIS trick allows organizer to add event without breaking RSVP
# decline functionality. (Outlook and its users suck.)
organizer_a = assignee.getEmailAddress().split('@')
organizer = vCalAddress('MAILTO:' + organizer_a[0]+ '+calendar@' +
organizer_a[1])
organizer.params['CN'] = vText(assignee.getDisplayName() + ' (' + assignee.getName() + ')')
event['organizer'] = organizer
You can get an idea of what fun it is to build iCalendar invitations, yes? The thing with the parentheses concatenation on the CN line is to follow our organization’s convention of rendering email addresses as “user@organization.com (Full Name)”.
3) Okay, third anecdote. You see in my first code fragment that I’m building up text objects for HTML and plaintext. I feed them into templates and craft a beautiful mime multipart/alternative with HTML and nicely-formatted plaintext . . . however, if there’s a Calendar invite also attached then Microsoft Exchange blows all that away, mangles the HTML to RTF and back again to HTML, and then renders its own text version of the RTF. My effort to make a pretty text email for the users gets chewed up and spat out, and my HTML gets mangled up, too. (And, yes, I work with SysAdmins so some users actually do look at the plain text . . .) I hate you, Microsoft Exchange!
I’m building out a simple template system for our email notifications, so of course I want to support multipart, text and email. But, hey, we have some text fields in JIRA that can take wiki markup, and JIRA will format that on display. So, how do I handle those fields in my text and HTML message attachments?
So, some sample code to render the custom field “Change Summary” into a pair of strings, change_summary_text and change_summary_html, suitable for inclusion into an email message:
from com.atlassian.event.apiimport EventPublisher
from com.atlassian.jiraimport ComponentManager
from com.atlassian.jira.componentimport ComponentAccessor
from com.atlassian.jira.issueimport CustomFieldManager
from com.atlassian.jira.issue.fieldsimport CustomField
from com.atlassian.jira.issue.fields.renderer.wikiimport AtlassianWikiRenderer
from com.atlassian.jira.util.velocityimport VelocityRequestContextFactory
# Get Custom Field
cfm = ComponentManager.getInstance().getCustomFieldManager()
change_summary = issue.getCustomFieldValue(cfm.getCustomFieldObjectByName("Change Summary"))# Set up Wiki renderer
eventPublisher = ComponentAccessor.getOSGiComponentInstanceOfType(EventPublisher)
velocityRequestContextFactory = ComponentAccessor.getOSGiComponentInstanceOfType(VelocityRequestContextFactory)
wikiRenderer = AtlassianWikiRenderer(eventPublisher, velocityRequestContextFactory)# Render Custom Field
change_summary_html = wikiRenderer.render(change_summary,None)
change_summary_text = wikiRenderer.renderAsText(change_summary,None)
from com.atlassian.event.api import EventPublisher
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.component import ComponentAccessor
from com.atlassian.jira.issue import CustomFieldManager
from com.atlassian.jira.issue.fields import CustomField
from com.atlassian.jira.issue.fields.renderer.wiki import AtlassianWikiRenderer
from com.atlassian.jira.util.velocity import VelocityRequestContextFactory
# Get Custom Field
cfm = ComponentManager.getInstance().getCustomFieldManager()
change_summary = issue.getCustomFieldValue(cfm.getCustomFieldObjectByName("Change Summary"))
# Set up Wiki renderer
eventPublisher = ComponentAccessor.getOSGiComponentInstanceOfType(EventPublisher)
velocityRequestContextFactory = ComponentAccessor.getOSGiComponentInstanceOfType(VelocityRequestContextFactory)
wikiRenderer = AtlassianWikiRenderer(eventPublisher, velocityRequestContextFactory)
# Render Custom Field
change_summary_html = wikiRenderer.render(change_summary, None)
change_summary_text = wikiRenderer.renderAsText(change_summary, None)
Feature request that certain JIRA dashboards should reload more frequently than every fifteen minutes. So, I cooked up some JavaScript to hide in the announcement banner:
The Cascading Select Custom Field type in JIRA is a bear. The first trick is learning to set the “null” value and then the “1” child value. The next trick is building out a ModifiedValue object to hold your change. Then you get to jump down the rabbit hole of finding the correct Option values for the custom field, and setting them with the tricks just mentioned.
So, in the interests of saving me sanity next time I need to set a Cascading Select, here’s a Jython function that works in Jira 4.2:
importloggingfrom com.atlassian.jiraimport ComponentManager
from com.atlassian.jira.issue.customfields.managerimport OptionsManager
from com.atlassian.jira.issue.customfields.viewimport CustomFieldParamsImpl
from com.atlassian.jira.issueimport ModifiedValue
from com.atlassian.jira.issue.utilimport DefaultIssueChangeHolder
from java.utilimport HashSet
# cf = custom field# issue = issue to modify# parent = top value to set (string value)# child = child value to set (string value)def set_cascading_select(cf, issue, parent, child):
# Get the managers
cfm = ComponentManager.getInstance().getCustomFieldManager()
om = ComponentManager.getComponentInstanceOfType(OptionsManager)
fli = ComponentManager.getInstance().getFieldLayoutManager().getFieldLayout(issue).getFieldLayoutItem(cf)
parent_options = om.getOptions(cf.getRelevantConfig(issue))
parent_option =None
child_option =Nonetry:
parent_option = parent_options.getOptionForValue(parent,None)except:
passtry:
child_option = parent_options.getOptionForValue(child, parent_option.getOptionId())except:
passif parent_option and child_option:
old_application = issue.getCustomFieldValue(cf)
new_application = CustomFieldParamsImpl(cf)
a_none = HashSet()
a_none.add(parent_option)
a_1 = HashSet()
a_1.add(child_option)
new_application.put(None, a_none)
new_application.put("1", a_1)
mf = ModifiedValue(old_application, new_application)
cf.updateValue(fli, issue, mf, DefaultIssueChangeHolder())logging.debug("set issue " + issue.getKey() + " cf " + cf.getName() + " setting " + parent + "/" + child)returnTrueelse:
logging.error("invalid parent/child option: " + parent + "/" + child)returnNone
import logging
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.issue.customfields.manager import OptionsManager
from com.atlassian.jira.issue.customfields.view import CustomFieldParamsImpl
from com.atlassian.jira.issue import ModifiedValue
from com.atlassian.jira.issue.util import DefaultIssueChangeHolder
from java.util import HashSet
# cf = custom field
# issue = issue to modify
# parent = top value to set (string value)
# child = child value to set (string value)
def set_cascading_select(cf, issue, parent, child):
# Get the managers
cfm = ComponentManager.getInstance().getCustomFieldManager()
om = ComponentManager.getComponentInstanceOfType(OptionsManager)
fli = ComponentManager.getInstance().getFieldLayoutManager().getFieldLayout(issue).getFieldLayoutItem(cf)
parent_options = om.getOptions(cf.getRelevantConfig(issue))
parent_option = None
child_option = None
try:
parent_option = parent_options.getOptionForValue(parent, None)
except:
pass
try:
child_option = parent_options.getOptionForValue(child, parent_option.getOptionId())
except:
pass
if parent_option and child_option:
old_application = issue.getCustomFieldValue(cf)
new_application = CustomFieldParamsImpl(cf)
a_none = HashSet()
a_none.add(parent_option)
a_1 = HashSet()
a_1.add(child_option)
new_application.put(None, a_none)
new_application.put("1", a_1)
mf = ModifiedValue(old_application, new_application)
cf.updateValue(fli, issue, mf, DefaultIssueChangeHolder())
logging.debug("set issue " + issue.getKey() + " cf " + cf.getName() + " setting " + parent + "/" + child)
return True
else:
logging.error("invalid parent/child option: " + parent + "/" + child)
return None
Example function calls from within a validation hook:
cfm = ComponentManager.getInstance().getCustomFieldManager()
application_cf = cfm.getCustomFieldObjectByName("Beverages")# good
set_cascading_select(application_cf, issue,"Hard Drinks","Whiskey")# bad child
set_cascading_select(application_cf, issue,"Hard Drinks","Coke")# bad parent
set_cascading_select(application_cf, issue,"Soft Drinks","Whiskey")# total crap
set_cascading_select(application_cf, issue,"Illicit Drugs","Bath Salts")
cfm = ComponentManager.getInstance().getCustomFieldManager()
application_cf = cfm.getCustomFieldObjectByName("Beverages")
# good
set_cascading_select(application_cf, issue, "Hard Drinks", "Whiskey")
# bad child
set_cascading_select(application_cf, issue, "Hard Drinks", "Coke")
# bad parent
set_cascading_select(application_cf, issue, "Soft Drinks", "Whiskey")
# total crap
set_cascading_select(application_cf, issue, "Illicit Drugs", "Bath Salts")
The logging stuff is useful for debugging, if you have that set up, else just remove those bits.
It took a few hours to figure this hook out, so I’m including my hard-won lines of code here.
# -*- coding: UTF-8 -*-# Check if PARENT is resolved.# Monitoring creates Events in the Event queue, these Events# automatically create Incident children.# We don't want to resolve any Incident children until the parent Event# resolves.# # (Normally you want to block on your children instead of your parent.)from com.atlassian.jiraimport ComponentManager
from com.atlassian.jira.issue.linkimport IssueLinkManager
ilm = ComponentManager.getInstance().getIssueLinkManager()# Assume we are okay ...
result =Truefor link in ilm.getInwardLinks(issue.getId()):
if link.getIssueLinkType().getName()=="Parent"and link.getSourceObject().getResolution()==None:
result =False
# -*- coding: UTF-8 -*-
# Check if PARENT is resolved.
# Monitoring creates Events in the Event queue, these Events
# automatically create Incident children.
# We don't want to resolve any Incident children until the parent Event
# resolves.
#
# (Normally you want to block on your children instead of your parent.)
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.issue.link import IssueLinkManager
ilm = ComponentManager.getInstance().getIssueLinkManager()
# Assume we are okay ...
result = True
for link in ilm.getInwardLinks(issue.getId()):
if link.getIssueLinkType().getName() == "Parent" and link.getSourceObject().getResolution() == None:
result = False
Somewhat elaborate: enforce that time worked has been logged, except under certain circumstances. See original post.
import com.atlassian.jira.issue.worklog.Worklogfrom com.atlassian.jiraimport ComponentManager
# Time Already Logged
timespent = issue.getTimeSpent()# Time Logged via current screentry:
timelogged =dict(issue.getModifiedFields())['worklog']except:
timelogged =False# Duplicate Issue? It is as good as logged!
resolution = issue.getResolution()if resolution['name']=="Duplicate":
timelogged =Trueif resolution['name']=="Self Corrected":
timelogged =True# Nagios likes to close tickets, but doesn't get paiduser= ComponentManager.getInstance().getJiraAuthenticationContext().getUser()ifuser.getName()=="nagios":
timelogged =Trueif timespent <=0and timelogged ==False:
result =False
description ="Please log the time you spent on this ticket."
import com.atlassian.jira.issue.worklog.Worklog
from com.atlassian.jira import ComponentManager
# Time Already Logged
timespent = issue.getTimeSpent()
# Time Logged via current screen
try:
timelogged = dict(issue.getModifiedFields())['worklog']
except:
timelogged = False
# Duplicate Issue? It is as good as logged!
resolution = issue.getResolution()
if resolution['name'] == "Duplicate":
timelogged = True
if resolution['name'] == "Self Corrected":
timelogged = True
# Nagios likes to close tickets, but doesn't get paid
user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser()
if user.getName() == "nagios":
timelogged = True
if timespent < = 0 and timelogged == False:
result = False
description = "Please log the time you spent on this ticket."
# -*- coding: UTF-8 -*-
from com.atlassian.jira import ComponentManager
assignee = issue.getAssignee()
user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser()
if not issue.getAssignee():
issue.setAssignee(ComponentManager.getInstance().getJiraAuthenticationContext().getUser())
Validate Custom Field Value
We have a particular custom field which can be set UNKNOWN by the Reporter, but which should be cleaned up by the Assignee.
from com.atlassian.jiraimport ComponentManager
cfm = ComponentManager.getInstance().getCustomFieldManager()
product = issue.getCustomFieldValue(cfm.getCustomFieldObject('customfield_12345'))if product =='UNKNOWN':
result =False
description ="Please set CUSTOM_FIELD value appropriately."
from com.atlassian.jira import ComponentManager
cfm = ComponentManager.getInstance().getCustomFieldManager()
product = issue.getCustomFieldValue(cfm.getCustomFieldObject('customfield_12345'))
if product == 'UNKNOWN':
result = False
description = "Please set CUSTOM_FIELD value appropriately."
I have had bad luck trying to coax this out of Google, so here’s a Perl one-liner:
perl -pi -e 's/[\x80-\xEF]//g' file.txt
Where file.txt is a file you want to clean up.
Why this comes up is because we have a web application that was set up to hit a MySQL database, which is incorrectly configured to store text as ASCII instead of UTF-8. The application assumes that all text is Unicode and that the database is correctly configured, and every week or two someone asks me why they are getting this weird gnarly error. Typically they are pasting in some weird UTF-8 whitespace character sent to us from Ukraine.
Eventually the database will be reloaded as UTF-8 and the problem will be solved. Until then, I can tell folks to use the Perl command above. It just looks for anything with the high bit set and strips it out.
I am not a DBA. I am but a humble SysAdmin who gets asked to figure out things like “how have we been at meeting our SLAs over time?” After I try to excuse myself I’ll inevitably end up say at the JIRA database running a query like this:
echo
echo "Incidents (P3)"
mysql -u jira jiradb< <__id3q
select year(created) as "Year", quarter(created) as "Quarter",
count(pkey) as "Total",
sum(resolutiondate < date_add(created, interval x day)) as "Met SLA",
sum(resolutiondate < date_add(created, interval x day)) / count(pkey) as "SLA %%"
from jiraissue where pkey like 'OPS-%' and priority = 3
and assignee != 'nagios' and issuetype = 26
group by year(created), quarter(created) order by created;
__id3q
That above is a fragment from a shell script. Shell scripts are great for complex SQL queries, I find. Set a value x at interval x day and the output looks something like:
The query does some things that are newer to my limited understanding of SQL. For me the magic bits are sum()ed columns and the availability of quarter() … you can do monthly reports just as easily with month(). I’d love to concatenate Year-Month into a string like “2012-05” but for the purposes of making my boss a little happier queries like this are good to have in the locker.
Some day I’ll be hip enough to convert things like this into JIRA widgets.
Oh yeah, and if your SLAs are measured in “business hours” or “business days” this will give you only a crude understanding of how well you have met your SLAs … an accurate measure would probably get embedded in a handler that gets called on issue close which can evaluate SLA fulfillment per issue priority and the local work schedule.
Time tracking in JIRA is a nice feature, but we have to get people to do it. My initial attempts to enforce time tracking ran into trouble, but I was able to develop a Jython Validator to hook on to transitions to the Resolved state. Now it is mandatory for our users to log time worked before they can resolve an issue:
# -*- coding: UTF-8 -*-import com.atlassian.jira.issue.worklog.Worklogfrom com.atlassian.jiraimport ComponentManager
# Time Already Logged
timespent = issue.getTimeSpent()# Time Logged via current screentry:
timelogged =dict(issue.getModifiedFields())['worklog']except:
timelogged =False# Duplicate Issue? It is as good as logged!
resolution = issue.getResolution()if resolution['name']=="Duplicate":
timelogged =Trueif resolution['name']=="Self Corrected":
timelogged =True# Nagios likes to close tickets, but doesn't get paiduser= ComponentManager.getInstance().getJiraAuthenticationContext().getUser()ifuser.getName()=="nagios":
timelogged =Trueif timespent <=0and timelogged ==False:
result =False
description ="Please log the time you spent on this ticket."
# -*- coding: UTF-8 -*-
import com.atlassian.jira.issue.worklog.Worklog
from com.atlassian.jira import ComponentManager
# Time Already Logged
timespent = issue.getTimeSpent()
# Time Logged via current screen
try:
timelogged = dict(issue.getModifiedFields())['worklog']
except:
timelogged = False
# Duplicate Issue? It is as good as logged!
resolution = issue.getResolution()
if resolution['name'] == "Duplicate":
timelogged = True
if resolution['name'] == "Self Corrected":
timelogged = True
# Nagios likes to close tickets, but doesn't get paid
user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser()
if user.getName() == "nagios":
timelogged = True
if timespent < = 0 and timelogged == False:
result = False
description = "Please log the time you spent on this ticket."
2012-01-24 Update: the script now contains additional logic, which exempts the nagios user from enforcement and allows resolution of duplicated or self-correcting issues which may not require time tracking. Hopefully this example is useful to somebody.
JIRA is an issue tracking system that is really flexible, but sometimes presents irritatingly arbitrary limitations.
I have been working on a screen which uses multiple tabs. The tabs are there to make it easier for the user to find the fields they want to edit, without scrolling through a single long, complex issue. But every tab has a Comment field rendered on it, which makes things confusing, and makes each tab look like it needs scrolling.
So, just remove the Comment field from the Screen, right? No, it isn’t in there. So, can I remove Comment via the Field Configuration Scheme? No, it is mandatory. Damn your arbitrary limitation, JIRA!
Anyway, I don’t normally speak JavaScript, but I managed to gin up the following snippet to paste into a Field description which appears in the screen I wanted to tweak. It finds the element containing the Comment, and sets its style display attribute to none. As the page loads, the Comment box is rendered, but once the page load completes, the Comment box disappears.