ACN: Add last changes to JIRA analysis tool code.

This commit is contained in:
Jonathan Bernard 2022-06-10 23:52:05 -05:00
parent c2da051878
commit 22af5abd26
10 changed files with 1754 additions and 83 deletions

View File

@ -1,5 +1,8 @@
PGSQL_CONTAINER_ID=`cat postgres.container.id` PGSQL_CONTAINER_ID=`cat postgres.container.id`
updatedb: startdb
./jira_analysis api-sync https://tegra118.atlassian.net jonathan.bernard@fiserv.com mS5cT0YntfQ6KYT0OWgb6A10
createdb: createdb:
docker run \ docker run \
--name postgres-tegra118 \ --name postgres-tegra118 \
@ -24,3 +27,9 @@ deletedb:
connect: connect:
PGPASSWORD=password psql -p 5500 -U postgres -h localhost tegra118 PGPASSWORD=password psql -p 5500 -U postgres -h localhost tegra118
update-fields:
curl -u 'jonathan.bernard@fiserv.com:mS5cT0YntfQ6KYT0OWgb6A10' 'https://tegra118.atlassian.net/rest/api/3/field' | jq . > fields.json
update-sample:
curl -u 'jonathan.bernard@fiserv.com:mS5cT0YntfQ6KYT0OWgb6A10' 'https://tegra118.atlassian.net/rest/api/3/search?jql=project%20%3D%20"UUP"%20and%20(labels%20is%20empty%20or%20labels%20!%3D%20"Design%26Reqs")%20ORDER%20BY%20key%20ASC&fields=summary,assignee,issuetype,customfield_10014,issuelinks,resolutiondate,status,customfield_10218,resolution,fixVersions,versions,customfield_10001&expand=changelog' | jq . > all-issues-filtered-fields.json

1145
dump.sql Normal file

File diff suppressed because it is too large Load Diff

BIN
features-2.ods Normal file

Binary file not shown.

View File

@ -12,4 +12,3 @@ bin = @["jira_analysis"]
requires @["nim >= 1.4.0", "docopt", "uuids", "timeutils", "fiber_orm >= 0.3.1"] requires @["nim >= 1.4.0", "docopt", "uuids", "timeutils", "fiber_orm >= 0.3.1"]
#requires "https://git.jdb-software.com/jdb-software/fiber-orm-nim.git" #requires "https://git.jdb-software.com/jdb-software/fiber-orm-nim.git"
requires "https://github.com/andreaferretti/csvtools.git"

View File

@ -1,4 +1,4 @@
import csvtools, docopt, fiber_orm, db_postgres, sequtils, sets, strutils import db_common, docopt, fiber_orm, db_postgres, sequtils, sets, strutils
import ./jira_analysispkg/jira_api import ./jira_analysispkg/jira_api
@ -16,60 +16,43 @@ type
TmPmDb* = ref object TmPmDb* = ref object
conn: DbConn conn: DbConn
FeaturesIssue* = object
id*: int
featureId*: int
issueId*: string
issueType*: string
func connect(connString: string): TmPmDb = func connect(connString: string): TmPmDb =
result = TmPmDb(conn: open("", "", "", connString)) result = TmPmDb(conn: open("", "", "", connString))
generateProcsForModels(TmPmDb, [ChangeLog, Feature, Issue]) generateProcsForModels(TmPmDb, [ChangeLog, Feature, Issue, FeaturesIssue, LinkedIssue])
generateLookup(TmPmDb, ChangeLog, @["historyId"]) generateLookup(TmPmDb, ChangeLog, @["historyId"])
generateLookup(TmPmDb, LinkedIssue, @["fromId", "toId", "linkType"])
when isMainModule: when isMainModule:
let doc = """ let doc = """
Usage: Usage:
jira_analysis import-csv <import-file>
jira_analysis api-sync <url-base> <username> <api-key> jira_analysis api-sync <url-base> <username> <api-key>
""" """
let args = docopt(doc, version = "0.2.0") let args = docopt(doc, version = "0.2.0")
let db = connect("host=localhost port=5500 dbname=tegra118 user=postgres password=password") let db = connect("host=localhost port=5500 dbname=tegra118 user=postgres password=password")
if args["import-csv"]:
let rows = toSeq(csvRows(path = $args["<import-file>"]))
let jiraIssues = rows.map(proc (r: seq[string]): Issue =
Issue(
issueType: r[0],
id: r[1],
summary: r[2],
priority: r[3],
status: r[4],
epicId: r[5],
testPhase: r[6],
assignee: r[7],
linkedIssueIds: r[8..<r.len].filterIt(not it.isEmptyOrWhitespace)
))
for issue in jiraIssues:
discard db.createIssue(issue);
# see if the issue already exists
# try:
# let existingRecord = db.getJiraIssue(issue.id);
# except NotFoundError:
# db.createJiraIssue(issue);
if args["api-sync"]: if args["api-sync"]:
initJiraClient($args["<url-base>"], $args["<username>"], $args["<api-key>"]) initJiraClient($args["<url-base>"], $args["<username>"], $args["<api-key>"])
let issuesAndChangelogs = searchIssues( let issuesLogsAndLinks = searchIssues(
"project = \"UUP\" and (labels is empty or labels != \"Design&Reqs\") ORDER BY key ASC", "project = \"UUP\" and (labels is empty or labels != \"Design&Reqs\") ORDER BY key ASC",
includeChangelog = true includeChangelog = true
) )
var issuesUpdated = 0 var issuesUpdated = 0
var issuesCreated = 0 var issuesCreated = 0
var changelogsCreated = 0 var linksAdded = 0
stdout.write("\nRetrieved " & $issuesAndChangelogs[0].len & " issues. ") stdout.write("\nRetrieved " & $issuesLogsAndLinks[0].len & " issues. ")
for issue in issuesAndChangelogs[0]: for issue in issuesLogsAndLinks[0]:
try: try:
discard db.getIssue(issue.id) discard db.getIssue(issue.id)
discard db.updateIssue(issue) discard db.updateIssue(issue)
@ -79,15 +62,21 @@ Usage:
issuesCreated += 1; issuesCreated += 1;
stdout.writeLine("Created " & $issuesCreated & " and updated " & $issuesUpdated) stdout.writeLine("Created " & $issuesCreated & " and updated " & $issuesUpdated)
stdout.write("Retrieved " & $issuesAndChangelogs[1].len & " change logs. ") stdout.write("\nFound " & $issuesLogsAndLinks[2].len & " issue links. ")
var newHistoryIds: HashSet[string] = initHashSet[string]() for link in issuesLogsAndLinks[2]:
for changelog in issuesAndChangelogs[1]: let existingLinks = db.findLinkedIssuesByFromIdAndToIdAndLinkType(
try: link.fromId, link.toId, link.linkType)
if newHistoryIds.contains(changelog.historyId) or if existingLinks.len == 0:
db.findChangeLogsByHistoryId(changelog.historyId).len == 0: discard db.createLinkedIssue(link);
newHistoryIds.incl(changelog.historyId) linksAdded += 1;
discard db.createChangeLog(changelog) stdout.writeLine("Recorded " & $linksAdded & " we didn't already have.")
changelogsCreated += 1;
except NotFoundError: discard
stdout.writeLine("Recorded " & $changelogsCreated & " we didn't already have.\n") stdout.write("Retrieved " & $issuesLogsAndLinks[1].len & " change logs. ")
let knownHistoryIds: HashSet[string] = toHashSet[string](db.getAllChangeLogs().mapIt(it.historyId))
let newChangeLogs: seq[ChangeLog] = issuesLogsAndLinks[1].filterIt(not knownHistoryIds.contains(it.historyId))
for changelog in newChangeLogs:
try:
discard db.createChangeLog(changelog)
except NotFoundError: discard
stdout.writeLine("Recorded " & $newChangeLogs.len & " we didn't already have.\n")

View File

@ -19,17 +19,26 @@ type
assignee*: string assignee*: string
status*: string status*: string
priority*: string priority*: string
linkedIssueIds*: seq[string] affectsVersions*: seq[string]
fixVersions*: seq[string]
resolution*: string
testPhase*: string testPhase*: string
teams*: seq[string]
LinkedIssue* = object
id*: int
toId*: string
fromId*: string
linkType*: string
let client = newHttpClient() let client = newHttpClient()
var API_BASE = ""; var API_BASE = "";
const FIELDS = "issuetype,summary,customfield_10014,assignee,status,priority,issuelinks,customfield_10218,changelog" const FIELDS = "issuetype,summary,customfield_10014,assignee,status,priority,issuelinks,customfield_10218,changelog,resolution,versions,fixVersions,customfield_10001"
proc parseIssue(json: JsonNode): (Issue, seq[ChangeLog]) = proc parseIssue(json: JsonNode): (Issue, seq[ChangeLog], seq[LinkedIssue]) =
let f = json["fields"] let f = json["fields"]
return (
Issue( let issue = Issue(
id: json["key"].getStr(), id: json["key"].getStr(),
issueType: f["issuetype"]["name"].getStr(), issueType: f["issuetype"]["name"].getStr(),
summary: f["summary"].getStr(), summary: f["summary"].getStr(),
@ -38,18 +47,39 @@ proc parseIssue(json: JsonNode): (Issue, seq[ChangeLog]) =
if f["assignee"].kind == JNull: "Unassigned" if f["assignee"].kind == JNull: "Unassigned"
else: f["assignee"]["displayName"].getStr(), else: f["assignee"]["displayName"].getStr(),
status: f["status"]["name"].getStr(), status: f["status"]["name"].getStr(),
priority: f["priority"].getStr(), priority:
linkedIssueIds: f["issuelinks"].mapIt( if f["priority"].kind == JObject: f["priority"]["name"].getStr()
if it.hasKey("inwardIssue"): it["inwardIssue"]["key"].getStr() else: "",
else: it["outwardIssue"]["key"].getStr()), resolution:
testPhase: f["customfield_10218"].getStr()), if f["resolution"].kind == JNull: ""
else: f["resolution"]["name"].getStr(),
affectsVersions:
if f["versions"].getElems().len > 0:
f["versions"].getElems().mapIt(it["name"].getStr())
else: @[],
fixVersions:
if f["fixVersions"].getElems().len > 0:
f["fixVersions"].getElems().mapIt(it["name"].getStr())
else: @[],
testPhase:
if f["customfield_10218"].kind == JNull: ""
else: f["customfield_10218"]["value"].getStr(),
teams:
if f["customfield_10001"].getElems().len > 0:
f["customfield_10001"].getElems.mapIt(it[""].getStr())
else : @[]
)
let changelogs =
if json.hasKey("changelog") and json["changelog"]["histories"].getElems().len > 0: if json.hasKey("changelog") and json["changelog"]["histories"].getElems().len > 0:
json["changelog"]["histories"].getElems().map( json["changelog"]["histories"].getElems().map(
proc (h: JsonNode): seq[ChangeLog] = h["items"].mapIt( proc (h: JsonNode): seq[ChangeLog] = h["items"].mapIt(
ChangeLog( ChangeLog(
historyId: h["id"].getStr(), historyId: h["id"].getStr(),
issueId: json["key"].getStr(), issueId: json["key"].getStr(),
author: h["author"]["displayName"].getStr(), author:
if h.hasKey("author"): h["author"]["displayName"].getStr()
else: "",
createdAt: parse( createdAt: parse(
h["created"].getStr()[0..17] & h["created"].getStr()[^6..^3], h["created"].getStr()[0..17] & h["created"].getStr()[^6..^3],
"yyyy-MM-dd'T'HH:mm:sszz"), "yyyy-MM-dd'T'HH:mm:sszz"),
@ -60,7 +90,36 @@ proc parseIssue(json: JsonNode): (Issue, seq[ChangeLog]) =
) )
).foldl(a & b) ).foldl(a & b)
else: @[] else: @[]
)
let linkedIssues =
if f.hasKey("issuelinks") and f["issuelinks"].getElems().len > 0:
f["issuelinks"].mapIt(
if it.hasKey("inwardIssue"):
@[
LinkedIssue(
fromId: json["key"].getStr(),
toId: it["inwardIssue"]["key"].getStr(),
linkType: it["type"]["inward"].getStr()),
LinkedIssue(
toId: json["key"].getStr(),
fromId: it["inwardIssue"]["key"].getStr(),
linkType: it["type"]["outward"].getStr())
]
else:
@[
LinkedIssue(
fromId: json["key"].getStr(),
toId: it["outwardIssue"]["key"].getStr(),
linkType: it["type"]["outward"].getStr()),
LinkedIssue(
toId: json["key"].getStr(),
fromId: it["outwardIssue"]["key"].getStr(),
linkType: it["type"]["inward"].getStr())
]
).foldl(a & b)
else: @[]
result = (issue, changelogs, linkedIssues)
proc initJiraClient*(apiBasePath: string, username: string, apiToken: string) = proc initJiraClient*(apiBasePath: string, username: string, apiToken: string) =
API_BASE = apiBasePath API_BASE = apiBasePath
@ -70,9 +129,9 @@ proc initJiraClient*(apiBasePath: string, username: string, apiToken: string) =
}) })
proc searchIssues*(jql: string, includeChangelog: bool = false): proc searchIssues*(jql: string, includeChangelog: bool = false):
(seq[Issue], seq[ChangeLog]) = (seq[Issue], seq[ChangeLog], seq[LinkedIssue]) =
result = (@[], @[]) result = (@[], @[], @[])
var query = @[ var query = @[
("jql", jql), ("jql", jql),
@ -99,10 +158,12 @@ proc searchIssues*(jql: string, includeChangelog: bool = false):
$body["total"].getInt() & $body["total"].getInt() &
" (" & $body["issues"].getElems().len & " records received)" " (" & $body["issues"].getElems().len & " records received)"
let issuesAndLogs = body["issues"].getElems().mapIt(parseIssue(it)) let issuesLogsAndLinks = body["issues"].getElems().mapIt(parseIssue(it))
result[0] &= issuesAndLogs.mapIt(it[0]) if issuesLogsAndLinks.len > 0:
result[1] &= issuesAndLogs.mapIt(it[1]).foldl(a & b) result[0] &= issuesLogsAndLinks.mapIt(it[0])
result[1] &= issuesLogsAndLinks.mapIt(it[1]).foldl(a & b)
result[2] &= issuesLogsAndLinks.mapIt(it[2]).foldl(a & b)
if nextStartAt > body["total"].getInt(): break if nextStartAt > body["total"].getInt(): break

View File

@ -7,15 +7,17 @@ CREATE TABLE issues (
test_phase varchar, test_phase varchar,
status varchar not null, status varchar not null,
priority varchar not null, priority varchar not null,
linked_issue_ids varchar[] resolution varchar not null default '',
affects_versions varchar[] default '{}',
fix_versions varchar[] default '{}',
teams varchar[] default '{}'
); );
CREATE TABLE features ( CREATE TABLE features (
id serial primary key, id serial primary key,
category varchar not null,
name varchar not null, name varchar not null,
epicId varchar not null default '', epicId varchar not null default '',
stories varchar[] not null default '{}',
defects varchar[] not null default '{}',
status varchar default 'todo', status varchar default 'todo',
confidence int not null default 0, confidence int not null default 0,
target_release varchar not null default '', target_release varchar not null default '',
@ -32,3 +34,16 @@ CREATE TABLE change_logs (
old_value varchar, old_value varchar,
new_value varchar new_value varchar
); );
CREATE TABLE linked_issues (
id serial primary key,
to_id varchar,
from_id varchar,
link_type varchar
);
CREATE TABLE features_issues (
id serial primary key,
feature_id int references features(id),
issue_id varchar
);

View File

View File

@ -1,7 +0,0 @@
UPDATE jira_issues SET linked_issues = collected.linked_issues from (
SELECT a.id, array_remove(array_cat(a.linked_issues, array_agg(b.id)) as linked_issues, NULL) FROM
jira_issues a LEFT OUTER JOIN
jira_issues b ON b.linked_issues @> ARRAY[a.id]
GROUP BY a.id
) AS collected
WHERE jira_issues.id = collected.id;

View File

@ -1,11 +1,18 @@
-- Show bugs moved to 'Resolved' with a full accounting of everyone who has -- Queries helpful in mining project information from Jira.
--
-- Useful VIM commands:
-- :DB g:db = postgres://postgresLpassword@localhost:5500/tegra118
-- :nmap <Leader>G mx?query:<CR>ww*vN:DB g:db<CR>'x
-- Show bugs moved to 'Done' with a full accounting of everyone who has
-- touched the issue, most recent issues first. -- touched the issue, most recent issues first.
-- query: DEFECTS_COMPLETED_WITH_PROVENANCE
SELECT SELECT
i.id, i.id,
i.epic_id, i.epic_id,
i.status, i.status,
i.test_phase, i.test_phase,
-- i.summary, i.summary,
i.assignee, i.assignee,
array_agg(DISTINCT c2.author) AS involved, array_agg(DISTINCT c2.author) AS involved,
c.created_at AS resolved_at c.created_at AS resolved_at
@ -15,19 +22,49 @@ FROM
i.issue_type = 'Bug' AND i.issue_type = 'Bug' AND
i.id = c.issue_id AND i.id = c.issue_id AND
c.field = 'status' AND c.field = 'status' AND
c.new_value = 'Resolved' JOIN c.new_value = 'Done' JOIN
change_logs c2 on i.id = c2.issue_id change_logs c2 on i.id = c2.issue_id
GROUP BY GROUP BY
i.id, i.id,
i.epic_id, i.epic_id,
i.status, i.status,
i.test_phase, i.test_phase,
-- i.summary, i.summary,
i.assignee, i.assignee,
resolved_at resolved_at
ORDER BY resolved_at DESC; ORDER BY resolved_at DESC;
-- end query: DEFECTS_COMPLETED_WITH_PROVENANCE
-- Issues moved to "In Testing" within the last 24 hours
-- query: ISSUES_IN_TESTING_RECENTLY
SELECT
i.id,
i.epic_id,
i.test_phase,
i.status,
i.summary,
array_remove(array_remove(array_agg(distinct c2.author), 'Lisa Schaffer'), 'Alec Mishra') AS involved,
c.created_at AS testing_at
FROM
issues i JOIN
change_logs c ON
i.issue_type = 'Bug' AND
i.id = c.issue_id AND
c.field = 'status' AND
(c.new_value = 'In Testing' OR c.old_value = 'In Testing') AND
c.created_at > (current_timestamp - interval '1 day') JOIN
change_logs c2 ON i.id = c2.issue_id
GROUP BY
i.id,
i.epic_id,
i.test_phase,
i.summary,
testing_at
ORDER BY testing_at DESC;
-- end query: ISSUES_IN_TESTING_RECENTLY
-- Show everyone involved with a specific ticket -- Show everyone involved with a specific ticket
-- query: WHO_TOUCHED IT
SELECT SELECT
i.id, i.id,
i.epic_id, i.epic_id,
@ -37,8 +74,431 @@ SELECT
FROM FROM
issues i JOIN issues i JOIN
change_logs c ON i.id = c.issue_id change_logs c ON i.id = c.issue_id
WHERE i.id in ('UUP-848') WHERE i.id in ('UUP-146')
GROUP BY i.id, i.epic_id, i.status; GROUP BY i.id, i.epic_id, i.status;
-- endquery: WHO_TOUCHED IT
select status, count(*) from issues where issue_type = 'Bug' group by status; -- List all linked issues for an issue
-- query: LINKED_ISSUES
SELECT
l.from_id,
l.link_type,
i.id,
i.summary
FROM linked_issues l JOIN
issues i ON l.to_id = i.id
WHERE l.from_id = 'UUP-441';
-- end query: LINKED_ISSUES
-- Summarize progress by feature
-- query: PROGRESS_BY_FEATURE
SELECT
f.id AS fid,
f.epic_id AS "Epic",
f.category AS "Feature Category",
f.name AS "Feature Name",
SUM(CASE WHEN i.issue_type = 'Story' AND i.status = 'Done' THEN 1 ELSE 0 END) AS "Stories Done",
SUM(CASE WHEN i.issue_type = 'Bug' AND i.status = 'Done' THEN 1 ELSE 0 END) AS "Defects Closed",
SUM(CASE WHEN i.issue_type = 'Story' AND i.status <> 'Done' THEN 1 ELSE 0 END) AS "Str Open",
SUM(CASE WHEN i.issue_type = 'Bug' AND i.status <> 'Done' THEN 1 ELSE 0 END) AS "Defects Open",
COUNT(i.id) AS "Tot. Issues",
f.status AS "Status",
f.target_release AS "Rev",
f.confidence AS "Conf."
FROM features f
LEFT JOIN features_issues l ON f.id = l.feature_id
LEFT JOIN issues i ON i.id = l.issue_id
WHERE
i.resolution NOT IN ('Declined', 'Duplicate', 'Won''t Do')
GROUP BY f.id
ORDER BY f.target_release, f.category, f.id;
-- query: PROGRESS_BY_FEATURE
-- Find all stories for a feature
-- query: FEATURE_STORIES
\set feature_id (127)
-- select id, epic_id, category, name, status, target_release as rev, confidence
-- from features
-- where id in :feature_id
-- order by id;
SELECT
f.id as fid,
f.category,
f.name as feature_name,
f.status,
f.target_release as rev,
f.confidence as conf,
i.id,
-- l.id as link_id,
i.issue_type,
i.status,
i.test_phase,
i.summary,
i.resolution
FROM features f
LEFT JOIN features_issues l ON f.id = l.feature_id
LEFT JOIN issues i ON
--i.status <> 'Done' AND
i.id = l.issue_id
WHERE
f.id in :feature_id AND
(i.resolution is null or i.resolution NOT IN ('Declined', 'Duplicate', 'Won''t Do'))
ORDER BY f.id, i.issue_type desc, i.status;
-- end query: FEATURE_STORIES
-- Find all issues not linked to a feature
-- query: ISSUES_WITHOUT_FEATURE
SELECT
i.id,
i.issue_type,
i.epic_id,
i.summary,
i.status
FROM issues i LEFT OUTER JOIN
features_issues l ON i.id = l.issue_id
WHERE
l.id IS NULL AND
i.issue_type <> 'Epic' AND
i.resolution NOT IN ('Declined', 'Duplicate', 'Won''t Do');
-- end query: ISSUES_WITHOUT_FEATURE
-- Feature Report for Sara
SELECT
f.id, f.name, f.epic_id, array_remove(array_agg(s.id), NULL) as stories, array_remove(array_agg(d.id), NULL) as defects
FROM features f LEFT JOIN features_issues l ON l.feature_id = f.id
LEFT JOIN issues s ON l.issue_id = s.id AND s.issue_type in ('Story', 'Task')
LEFT JOIN issues d ON l.issue_id = d.id AND d.issue_type = 'Bug'
GROUP BY f.id, f.name, f.epic_id ORDER BY f.id;
-- Find issues by keywords in the summary
select id, status, summary, resolution
from issues
where status = 'Done' and LOWER(summary) like '%delink%' order by status desc;
-- Find issues by keyword and link to feature
-- query: KEYWORD_FIND_WITH_FEATURES
select f.id, f.name, i.id, i.status, i.summary, i.resolution
from issues i
left join features_issues l on i.id = l.issue_id
left join features f on f.id = l.feature_id
where i.status = 'Done' and LOWER(i.summary) like '%quick%' order by i.status desc;
-- end query: KEYWORD_FIND_WITH_FEATURES
--- WORKING ---
select id, epic_id, category, name from features order by id asc;
select * from features order by id;
select * from issues where summary like '%onfigurable%';
select * from features_issues;
select * from change_logs limit 5;
select * from features_issues where issue_id = 'UUP-128';
update features set target_release = '2.1-alpha' where target_release = '1.1-alpha1';
select distinct target_release from features;
-- query: SET_FEATURE_STATUS
\set feature_id (109)
UPDATE features SET
target_release = 'TBD',
confidence = 100,
status = 'todo'
WHERE id in :feature_id;
-- end query: SET_FEATURE_STATUS
update features set
target_release = '2.1',
notes = 'Depends on Acount Adjuster functionality.'
where id = 82;
select * from features_issues where feature_id = 11;
-- Find issues missing the version based on the feature target_release
select
f.id, f.name as feature_name, i.id, i.summary, f.target_release, i.fix_versions
from features f
join features_issues l on f.id = l.feature_id
join issues i on i.id = l.issue_id
where not i.fix_versions @> ARRAY[f.target_release];
select * from features where category = 'Miscellaneous';
select distinct category from features;
insert into features (category, name, target_release, confidence, epic_id) values
('Miscellaneous', 'Grid Template Functionality', '2.1', 80, 'UUP-18')
returning *;
update features set epic_id = 'UUP-28' where id = 134;
update features set category = 'Account Maintenance' where id = 11;
('Drop 3.5 Items', 'Auto-adjust Column Widths', '1.1'),
('Drop 3.5 Items', 'Proposed Orders - Add Aollcation', '1.1'),
('Drop 3.5 Items', 'Auto Select Accounts when clicking Maintain', '1.1'),
('Drop 3.5 Items', 'Trade Shortcut on TCA Screens.', '1.1'),
('Drop 3.5 Items', 'Paging Defaults', '1.1-alpha'),
('Drop 3.5 Items', 'More UI Clean Up', '1.1'),
('Drop 3.5 Items', 'Proposed Orders - Enhance field validations', '1.1'),
('Blotter / Drop 3.5 Items', 'Wash Sale Options', '1.1'),
('Drop 3.5 Items', 'Terms of Use file and hyperlink', '1.1'),
('Drop 3.5 Items', 'Improve PDF Export', '1.1');
insert into features_issues (feature_id, issue_id) values
(60, 'UUP-74'),
(60, 'UUP-78'),
(60, 'UUP-30'),
(60, 'UUP-101');
insert into features_issues (feature_id, issue_id) values
('83', 'UUP-244'),('83', 'UUP-284'),('83', 'UUP-513'),('83', 'UUP-285'),
('84', 'UUP-296'),('84', 'UUP-519'),('84', 'UUP-297'),
('85', 'UUP-271'),('85', 'UUP-270'),('85', 'UUP-506'),
('86', 'UUP-287'),('86', 'UUP-286'),('86', 'UUP-514'),
('87', 'UUP-290'),('87', 'UUP-516'),('87', 'UUP-291'),
('88', 'UUP-272'),('88', 'UUP-507'),('88', 'UUP-273'),
('89', 'UUP-282'),('89', 'UUP-512'),('89', 'UUP-283'),
('90', 'UUP-281'),('90', 'UUP-280'),('90', 'UUP-511'),
('91', 'UUP-288'),('91', 'UUP-515'),('91', 'UUP-289'),
('92', 'UUP-277'),('92', 'UUP-276'),('92', 'UUP-509'),
('93', 'UUP-269'),('93', 'UUP-268'),('93', 'UUP-505'),
('94', 'UUP-292'),('94', 'UUP-517'),('94', 'UUP-293'),
('95', 'UUP-295'),('95', 'UUP-294'),('95', 'UUP-518'),
('96', 'UUP-279'),('96', 'UUP-278'),('96', 'UUP-510'),
('97', 'UUP-247'),('97', 'UUP-494'),('97', 'UUP-246'),
('98', 'UUP-249'),('98', 'UUP-495'),('98', 'UUP-248'),
('99', 'UUP-255'),('99', 'UUP-254'),('99', 'UUP-498'),
('100', 'UUP-262'),('100', 'UUP-502'),('100', 'UUP-263'),
('101', 'UUP-253'),('101', 'UUP-252'),('101', 'UUP-497'),
('102', 'UUP-258'),('102', 'UUP-500'),('102', 'UUP-259'),
('103', 'UUP-266'),('103', 'UUP-504'),('103', 'UUP-267'),
('104', 'UUP-264'),('104', 'UUP-503'),('104', 'UUP-265'),
('105', 'UUP-257'),('105', 'UUP-256'),('105', 'UUP-499'),
('106', 'UUP-250'),('106', 'UUP-496'),('106', 'UUP-251');
insert into features_issues (feature_id, issue_id) values
(131, 'UUP-631'),
(74, 'UUP-1103'),
(4, 'UUP-1114'),
(97, 'UUP-80'),
(72, 'UUP-465'),
(71, 'UUP-1159'),
(35, 'UUP-1180'),
(70, 'UUP-637'),
(45, 'UUP-76'),
(37, 'UUP-1175'),
(70, 'UUP-1216'),
(110, 'UUP-176'),
(70, 'UUP-65'),
(45, 'UUP-931'),
(69, 'UUP-1004'),
(45, 'UUP-928'),
(115, 'UUP-316'),
(49, 'UUP-1142'),
(16, 'UUP-383'),
(24, 'UUP-400'),
(69, 'UUP-1005'),
(53, 'UUP-937'),
(25, 'UUP-1118'),
(109, 'UUP-274'),
(32, 'UUP-1178'),
(70, 'UUP-431'),
(17, 'UUP-912'),
(16, 'UUP-687'),
(16, 'UUP-417'),
(12, 'UUP-1112'),
(131, 'UUP-106'),
(12, 'UUP-1108'),
(47, 'UUP-930'),
(24, 'UUP-611'),
(70, 'UUP-682'),
(57, 'UUP-936'),
(35, 'UUP-1137'),
(70, 'UUP-1158'),
(18, 'UUP-914'),
(19, 'UUP-1196'),
(12, 'UUP-376'),
(134, 'UUP-298'),
(69, 'UUP-1217'),
(24, 'UUP-1051'),
(11, 'UUP-1127'),
(16, 'UUP-548'),
(70, 'UUP-681'),
(73, 'UUP-1176'),
(70, 'UUP-955'),
(49, 'UUP-802'),
(25, 'UUP-1069'),
(135, 'UUP-194'),
(25, 'UUP-1099'),
(37, 'UUP-1227'),
(72, 'UUP-1035'),
(72, 'UUP-1073'),
(25, 'UUP-1068'),
(43, 'UUP-845'),
(47, 'UUP-1133'),
(11, 'UUP-1132'),
(45, 'UUP-138'),
(37, 'UUP-1177'),
(16, 'UUP-1163'),
(131, 'UUP-82'),
(50, 'UUP-932'),
(62, 'UUP-1151'),
(126, 'UUP-324'),
(4, 'UUP-1181'),
(16, 'UUP-1162'),
(16, 'UUP-1078'),
(41, 'UUP-557'),
(97, 'UUP-721'),
(11, 'UUP-1131'),
(72, 'UUP-1226'),
(110, 'UUP-459'),
(8, 'UUP-966'),
(47, 'UUP-36'),
(136, 'UUP-180');
UUP-1084
UUP-1152
UUP-1192
UUP-120
UUP-1055
UUP-1136
UUP-1223
UUP-1134
UUP-84
UUP-1021
UUP-1006
UUP-127
UUP-924
UUP-1167
UUP-748
UUP-1019
UUP-1002
UUP-1199
UUP-1003
UUP-439
UUP-108
UUP-1170
UUP-438
UUP-1198
UUP-1079
UUP-873
UUP-212
UUP-919
UUP-1206
UUP-929
UUP-775
UUP-1173
UUP-723
UUP-322
UUP-1190
UUP-871
UUP-61
UUP-673
UUP-338
UUP-1074
UUP-320
UUP-330
UUP-1076
UUP-1071
UUP-875
UUP-1085
UUP-690
UUP-695
UUP-1143
UUP-566
UUP-700
UUP-1072
UUP-1187
UUP-1062
UUP-1014
UUP-1224
UUP-216
UUP-1208
UUP-951
UUP-1119
UUP-1202
UUP-100
UUP-705
UUP-1113
UUP-314
UUP-1225
UUP-1222
UUP-1066
UUP-915
UUP-573
UUP-1053
UUP-876
UUP-467
UUP-1090
UUP-933
UUP-909
UUP-945
UUP-47
UUP-874
UUP-1148
UUP-956
UUP-1149
UUP-1123
UUP-952
UUP-441
UUP-140
UUP-1034
UUP-868
UUP-81
UUP-922
UUP-1182
UUP-143
UUP-1179
UUP-1204
UUP-1120
UUP-139
UUP-125
UUP-93
UUP-107
UUP-312
UUP-242
UUP-170
UUP-1080
UUP-575
UUP-549
UUP-1083
UUP-946
UUP-869
UUP-1195
UUP-571
UUP-1191
UUP-410
UUP-659
UUP-870
UUP-1077
UUP-1188
UUP-940
UUP-70
UUP-1213
UUP-971
UUP-925
UUP-35
UUP-411
UUP-432
UUP-336
UUP-1000
UUP-684
UUP-592
UUP-1157
UUP-910
UUP-1194
UUP-326
UUP-926
UUP-689
UUP-943
UUP-950
UUP-627
UUP-1220
UUP-69
UUP-606
UUP-1036
UUP-1212
UUP-917
UUP-1100