Booking System
Create booking requests and booking workflows in order to avoid Environment usage conflicts.

Context

You have defined your Environments and you would like people to book your Environments for different kind of activities like UAT, demos, technical maintenance, etc.

Prerequisite

Your Environments are already defined in Apwide Golive. Not yet? Follow our Quick setup

Basic Use Case

For this use case, we are relying on Jira core features: Jira issue types, Custom Fields, Workflows, etc.

1. Create a new Jira Issue Type to support your Environment Booking requests

For instance, you can name it “Booking Request”. Associate your newly created Issue Type with an existing Jira Project, or create a new Jira Project to be used for your Booking Requests (it’s up to you).

2. Create 3 Jira Custom Fields (or reuse existing ones)

    Custom Field of type “Environment” to store the Environments linked to your Booking Requests. You can name it “Environment(s) to book”
    Custom Field of type “Date Time Picker” to store the beginning of your Booking Requests. You can name it “Start time”.
    Custom Field of type “Date Time Picker” to store the end of your Booking Requests. You can name it “End time”.
Create custom fields for your Booking Request
Related Jira Server/DC documentation How to add a custom field?

3. Create a Screen for your Booking Request Issue Type

Add the fields you defined in STEP 2 in your new screen. It should look like this:
Configure a new screen for your Booking Request
Related Jira Server/DC documentation How to create and activate a screen?

4. Create a workflow for your Booking Request Issue Type

The workflow is really up to you, you can add as many steps and approvals as you need. Here is an example of very simple workflow:
Create a workflow for your Booking Request
Related Jira Server/DC documentation How to create and activate a workflow?

5. Test your Booking Request

From your Jira Project, create a new Booking Request and make sure it works. Adjust if needed.
Test your Booking Request

6. Create a Booking Requests Calendar

Create a new Golive Timeline (more info: Timelines) Add a new Issue Calendar by typing “Booking Request” (or the name you choose for your Issue Type):
By default, all Calendar information should be there. Double-check the fields mapping to make sure they are the Custom Fields you have created before.
Create your Booking Request Calendar
After clicking on “Done”, you should see the Booking Requests on your Timeline, with their statuses.
Your Booking Request system is ready!

Conclusion

Congrats! Now you have a Calendar displaying your Booking Requests and their statuses.
You can reschedule your Booking Requests on the Timeline with drag-and-drop: the “Start time” and “End time” will be updated and the requester will be notified by Jira.
You can also move the Booking Requests from one Environment Swimlane to another in order to update the Environments booked by the request. Notification will also be sent to the requester.

Advanced: Conflict Management

Go further and list potential booking conflicts in your Booking Request using ScriptRunner Jira App.
Display potential conflicts in your Booking Requests

1. Add a new Script

After installing the ScriptRunner Jira App, add our script in the Script Editor (code reference below).
New Script in the Script Editor section (check below for the code)

2. Add a new Behaviour

Then, create a new Behaviour and map it with your Booking Request project and issue type.
New entry in the Behaviour section

3. Call the Script from your Behaviour

In your Behaviour, define your Environment and Start/End fields as "Required" and add Server-side scripts for each of them (code reference below). All Server-side scripts should be identical as they will trigger the same ConflictChecker script.
Server-side Script (check below for code) for Environment and Start/End dates fields
Script Parameters:
    issueTypeName: your Booking Request issue type name
    startDateFieldName: your booking start date/time field name
    endDateFieldName: your booking end date/time custom field name
    environmentFieldName: your Environment custom field name
    helpFieldName: the name of the field under which the warning message should appear (if not defined, it will be your endDateFieldName)
    timelineUrl: URL used in the warning message button
    dateFormat: date format used for the period (if not defined, it will be your Jira date format)

Code Reference

Server-side Script (Behaviour)
ConflictChecker (Script Editor)
1
import com.apwide.behaviour.ConflictChecker
2
import org.apache.log4j.Level
3
4
new ConflictChecker(
5
ctx: this,
6
issueTypeName: "Booking Request",
7
startDateFieldName: "Start time",
8
endDateFieldName: "End time",
9
environmentFieldName: "Environment(s) to book",
10
helpFieldName: "Start time",
11
timelineUrl: "/secure/ApwideGolive.jspa#/home/timeline?view=timeline197",
12
dateFormat: "MMM d",
13
logLevel: Level.DEBUG
14
).check()
Copied!
1
package com.apwide.behaviour
2
3
import com.atlassian.jira.bc.issue.search.SearchService
4
import com.atlassian.jira.component.ComponentAccessor
5
import com.atlassian.jira.entity.WithKey
6
import com.atlassian.jira.issue.Issue
7
import com.atlassian.jira.issue.fields.CustomField
8
import com.atlassian.jira.issue.search.SearchResults
9
import com.atlassian.jira.jql.parser.DefaultJqlQueryParser
10
import com.atlassian.jira.user.ApplicationUser
11
import com.atlassian.jira.web.bean.PagerFilter
12
import com.atlassian.query.Query
13
import com.onresolve.jira.groovy.user.FormField
14
import com.atlassian.jira.issue.context.IssueContext
15
import groovy.util.logging.Log4j
16
import org.apache.log4j.Level
17
import org.codehaus.groovy.runtime.typehandling.GroovyCastException
18
import static com.atlassian.jira.config.properties.APKeys.JIRA_DATE_TIME_PICKER_JAVA_FORMAT
19
20
@Log4j
21
class ConflictChecker {
22
23
static final String JIRA_JQL_DATE_FORMAT = 'yyyy-MM-dd HH:mm'
24
25
def ctx
26
String dateFormat
27
String issueTypeName
28
String startDateFieldName
29
String endDateFieldName
30
String environmentFieldName
31
String timelineUrl
32
String helpFieldName
33
Level logLevel
34
35
void check() {
36
log.setLevel(logLevel ?: Level.INFO)
37
38
Date startFieldValue = getDateValue(startDateFieldName)
39
Date endFieldValue = getDateValue(endDateFieldName)
40
List environmentFieldValue = getEnvironmentsValue(environmentFieldName)
41
42
if (!startFieldValue || !endFieldValue || !environmentFieldValue) {
43
log.debug("No values found")
44
writeHelpText("")
45
return
46
}
47
48
// JQL query
49
List<Issue> conflicts = searchForConflicts(startFieldValue, endFieldValue, environmentFieldValue)
50
if (conflicts.isEmpty()) {
51
log.debug("No conflicts found")
52
writeHelpText("")
53
return
54
}
55
56
log.debug("Found ${conflicts.size()} conflicts")
57
58
def warningMessage = """
59
<div class="aui-message aui-message-warning">
60
<p>There are conflicting Booking Requests:</p>
61
<table style="margin: 15px 0px; width:100%">
62
<tr>
63
<th>Key</th>
64
<th>Summary</th>
65
<th>Period</th>
66
<th>Environment(s)</th>
67
</tr>
68
${renderConflictList(conflicts)}
69
</table>
70
<a target="_blank" href="${timelineUrl}" class="aui-button">
71
<span class="aui-icon aui-icon-small aui-iconfont-search"></span>&nbsp;Open the Timeline
72
</a>
73
</div>
74
"""
75
76
writeHelpText(warningMessage)
77
}
78
79
private String renderConflictList(List<Issue> conflicts) {
80
CustomField startCustomField = getCustomFieldByName(startDateFieldName)
81
CustomField endCustomField = getCustomFieldByName(endDateFieldName)
82
CustomField environmentCustomField = getCustomFieldByName(environmentFieldName)
83
84
return conflicts.collect {issue ->
85
Date startDate = (Date) issue.getCustomFieldValue(startCustomField)
86
Date endDate = (Date) issue.getCustomFieldValue(endCustomField)
87
List environments = (List) issue.getCustomFieldValue(environmentCustomField)
88
89
return """
90
<tr>
91
<td><a target="_blank" href="${baseUrl()}/browse/${issue.key}">${issue.key}</a></td>
92
<td>${issue.summary}</td>
93
<td>${displayedDate(startDate)} - ${displayedDate(endDate)}</td>
94
<td>${displayedEnvironments(environments)}</td>
95
</tr>
96
"""
97
}.join("\n")
98
}
99
100
private void writeHelpText(String helpText) {
101
FormField endField = ctx.getFieldByName(helpFieldName ?: endDateFieldName)
102
endField.setHelpText(helpText)
103
}
104
105
private List<Issue> searchForConflicts(Date startFieldValue, Date endFieldValue, List environmentFieldValue) {
106
Query query = toQuery(startFieldValue, endFieldValue, environmentFieldValue)
107
PagerFilter pager = PagerFilter.newPageAlignedFilter(0,50)
108
SearchResults<Issue> results = searchService().searchOverrideSecurity(loggedInUser(), query, pager)
109
log.debug("Found ${results.getTotal()} issues corresponding to search")
110
IssueContext issueContext = ctx.getIssueContext()
111
if (issueContext instanceof WithKey) {
112
String issueKey = ((WithKey)issueContext).getKey()
113
log.debug("Issue context with key ${issueKey}")
114
return results.getResults().findAll { !it.key.equals(issueKey) }
115
} else {
116
log.debug("Issue context without key")
117
return results.getResults()
118
}
119
}
120
121
private Query toQuery(Date startFieldValue, Date endFieldValue, List environmentFieldValue) {
122
String environmentFieldValueString = environmentFieldValue.join(",")
123
String stringQuery = """
124
type = "${issueTypeName}"
125
AND "${startDateFieldName}" < "${jqlDate(endFieldValue)}"
126
AND "${endDateFieldName}" > "${jqlDate(startFieldValue)}"
127
AND "${environmentFieldName}" in (${environmentFieldValueString})
128
"""
129
log.debug("query is: ${stringQuery}")
130
return new DefaultJqlQueryParser().parseQuery(stringQuery)
131
}
132
133
private Date getDateValue(String fieldName) {
134
try {
135
FormField dateField = ctx.getFieldByName(fieldName)
136
return dateField ? (Date) dateField.getValue() : null
137
} catch (GroovyCastException ex) {
138
return null
139
}
140
}
141
142
private List getEnvironmentsValue(String fieldName) {
143
try {
144
FormField environmentField = ctx.getFieldByName(fieldName)
145
return environmentField ? (List) environmentField.getValue() : null
146
} catch (GroovyCastException ex) {
147
return null
148
}
149
}
150
151
private String displayedDate(Date date) {
152
return date.format(displayDateFormat())
153
}
154
155
private String displayedEnvironments(List environments) {
156
return environments ? environments.collect{ it.name }.join("<br />") : "-"
157
}
158
159
private String displayDateFormat() {
160
def format = dateFormat != null ? dateFormat : ComponentAccessor.getApplicationProperties().getString(JIRA_DATE_TIME_PICKER_JAVA_FORMAT)
161
log.debug("date format to be used will '${format}'")
162
return format
163
}
164
165
private String jqlDate(Date date) {
166
return date.format(JIRA_JQL_DATE_FORMAT)
167
}
168
169
private String baseUrl() {
170
return ComponentAccessor.getApplicationProperties().getString("jira.baseurl")
171
}
172
173
private CustomField getCustomFieldByName(String name) {
174
return ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName(name)
175
}
176
177
private SearchService searchService() {
178
return ComponentAccessor.getComponent(SearchService.class)
179
}
180
181
private ApplicationUser loggedInUser() {
182
return ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
183
}
184
}
Copied!

Questions?

Jira is very powerful for its workflows, that’s why we have decided to rely on it for our Booking System, instead of implementing our own system. The setup may be a little complex for Jira beginners, that’s why we offer free assistance for this configuration.
If you need our help, contact us.
Last modified 5mo ago