-
-
Save montehurd/40ab4dd5cc81b66d78aaaa46752b1f5f to your computer and use it in GitHub Desktop.
#!/bin/bash | |
# Edit: This code is just horrible, but it was early in my shell scripting journey. | |
# This script does the following: | |
# | |
# Hits the "maniphest.search" API using a saved query's "query key": | |
# Any query key may be used. Simply construct your query using Phabricator's "Advanced Search" interface: | |
# https://phabricator.wikimedia.org/maniphest/query/advanced/ | |
# When you have it fetching what you want, use the "Save Query" button, then grab the saved query's key from its URL. | |
# Presently it's using this query I constructed for testing purposes: | |
# https://phabricator.wikimedia.org/maniphest/query/8Nu74C_eapwI | |
# | |
# Transforms the results a bit: | |
# Chooses a few fields from each result and adds a little "remarkup" to each result's ticket description remarkup. | |
# | |
# Hits the "remarkup.process" API: | |
# Converts our transformed remarkup to HTML. | |
# | |
# Wraps results in some structural HTML: | |
# Ensures things like links and styles work. | |
# | |
# Sends everthing to a browser: | |
# So you can instantly see the results. | |
# Settings: | |
BASE_URL="https://phabricator.wikimedia.org" | |
API_TOKEN="api-2lfamy3hsjzihfo6jwmnbd6lphn2" | |
QUERY_KEY="8Nu74C_eapwI" | |
# Result transformation script: | |
read -r -d '' RESULT_TRANSFORMATION << '--END' | |
import sys, json, urllib; | |
resultDicts = json.load(sys.stdin)['result']['data']; | |
remarkup = '\n{F31669139 size=full}\n'.join(list(map(lambda item: | |
'''== T{number} == | |
== {name} == | |
{description} | |
''' | |
.format( | |
number=item['id'], | |
name=item['fields']['name'].encode('utf-8'), | |
priority=item['fields']['priority']['name'].encode('utf-8'), | |
description=item['fields']['description']['raw'].encode('utf-8') | |
), resultDicts))); | |
encodedRemarkup = urllib.quote_plus(remarkup); | |
print('contents[0]=' + encodedRemarkup); | |
--END | |
# Page presentation script: | |
read -r -d '' PAGE_PRESENTATION << --END | |
import sys, json; | |
html = json.load(sys.stdin)['result'][0]['content']; | |
print(''' | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<link rel="stylesheet" type="text/css" href="https://phab.wmfusercontent.org/res/defaultX/phabricator/31c46c5c/core.pkg.css"> | |
<base href="$BASE_URL" target="_blank"> | |
</head> | |
<body style="padding:20pt"> | |
<div class="phabricator-remarkup"> | |
{html} | |
</div> | |
</body> | |
</html> | |
'''.format(html=html)); | |
--END | |
# Use the query key to hit the "maniphest.search" API: | |
curl -s "$BASE_URL/api/maniphest.search" \ | |
-d "api.token=$API_TOKEN" \ | |
-d "order=priority" \ | |
-d "queryKey=$QUERY_KEY" | | |
# Pipe the results to Python for processing: | |
# - Extracts each result | |
# - Lightly transforms each result to pretty things up by joining each phab ticket number, title ("name") and description (which is remarkup https://secure.phabricator.com/book/phabricator/article/remarkup/) around a remarkup spacer ("F31669139") | |
# This "RESULT_TRANSFORMATION" script would be the place to extract some sought description remarkup sub-string if we wanted to further mine/reduce tickets in some way. | |
python -c "$RESULT_TRANSFORMATION" | | |
# Pipe our remixed remarkup to "remarkup.process" to convert it to HTML: | |
curl -s "$BASE_URL/api/remarkup.process" \ | |
-d "api.token=$API_TOKEN" \ | |
-d "context=phriction" \ | |
-d @- | | |
# Extract HTML, add stylesheet and base URL so it looks nice and links work, then send all of it to a browser so we can see it: | |
python -c "$PAGE_PRESENTATION" > /tmp/phabricator.tmp.html && open -a "Safari" /tmp/phabricator.tmp.html | |
# To see html as text use line below instead of line above: | |
# python -c "$PAGE_PRESENTATION" | open -a "Safari" -f | |
Allow Browsers to use Javascript "fetch" method with shell script produced html
For security reasons browsers wisely lock down cross-domain communication. For a shell script, like the one above, to be able to use the Javascript fetch
method targeting the Wikimedia instance of Phabricator, one work-around is using a browser instance which has relaxed these restrictions a bit:
Chrome
A Chrome instance can be invoked from the command line with relaxed restrictions:
open -a "Google Chrome" "./queryphab/queryphab.html" --args --disable-web-security --user-data-dir="/tmp/chrome_dev_test"
The full list of Chrome's command line arguments may be found here.
Safari
Safari doesn't seem to accept command line arguments, but you can temporarily toggle these restrictions off via:
Develop
> Disable Cross Origin Restrictions
If the Develop
menu isn't visible you can turn it on via:
Safari
> Preferences
> Advanced
> Show Develop menu in menu bar
A version of the script with functional "Quick status options" buttons
Below is a version of the script which adds a row of "Quick status options" beneath each ticket.
As with the script above you can just copy / paste it into Terminal. Its only requirement is you need Chrome to be installed.
Presently it shows live data from the Wikipedia-iOS-App-Backlog board's "Bug Backlog" column, and its "Quick status options" buttons are functional, so don't go around clicking them unless updating a ticket's status is what you really want to do.
Having said this, the script can be used for any column on any board. Just follow the instructions above to grab a column's queryKey
. Then replace the string 7gQoWC3FFleG
below with the key you grabbed.
Note: the blue "hide" button seen in the screenshot leaves its ticket untouched - it just hides it from the list output by the script.
#!/bin/bash
BASE_URL="https://phabricator.wikimedia.org"
API_TOKEN="api-2lfamy3hsjzihfo6jwmnbd6lphn2"
QUERY_KEY="7gQoWC3FFleG"
# https://stackoverflow.com/a/44828706
read -r -d '' RESULT_TRANSFORMATION << '--END'
import sys, json, urllib;
resultDicts = json.load(sys.stdin)['result']['data'];
remarkup = ''.join(list(map(lambda item:
'''TICKET_START:{number}:
= T{number} =
== {name} ==
Priority: {priority}
---
{description}
TICKET_END'''
.format(
number=item['id'],
name=item['fields']['name'].encode('utf-8'),
priority=item['fields']['priority']['name'].encode('utf-8'),
description=item['fields']['description']['raw'].encode('utf-8')
), resultDicts)));
encodedRemarkup = urllib.quote_plus(remarkup);
print('contents[0]=' + encodedRemarkup);
--END
read -r -d '' PAGE_PRESENTATION << --END
import sys, json, re;
ticketsHTML = json.load(sys.stdin)['result'][0]['content'].encode('utf-8');
def buttonHTML(ticketNumber, valueString):
return '''<button id="{ticketNumber}:{valueString}" onclick="buttonAction({ticketNumber}, '{valueString}')" onblur="showButtonActionMessage({ticketNumber}, '')">{valueString}</button>'''.format(ticketNumber=ticketNumber, valueString=valueString)
def allButtonsHTML(ticketNumber):
return '\n'.join(list(map(lambda x: buttonHTML(ticketNumber, x), ['resolved', 'declined', 'stalled', 'invalid'] )))
def menuHTML(ticketNumber, allButtonsHTML):
return '''
<div class="menu phui-tag-core phui-tag-color-object">
Quick status options :{allButtonsHTML}
<button class=hide onclick="this.closest('div#T{ticketNumber}').remove()">hide</button>
<span class=buttonActionMessage id="buttonActionMessage{ticketNumber}"></span>
</div>
'''.format(ticketNumber=ticketNumber, allButtonsHTML=allButtonsHTML)
def addWrapperDivAndMenuToTicketHTML(match):
ticketNumber = match.group(1)
ticketHTML = match.group(2)
return '''
<div class=ticket id="T{ticketNumber}">
{ticketHTML}
{menuHTML}
</div>
'''.format(ticketNumber=ticketNumber, ticketHTML=ticketHTML, menuHTML=menuHTML(ticketNumber, allButtonsHTML(ticketNumber)))
ticketsHtmlIncludingWrapperDivsAndMenus = re.sub(pattern=r'TICKET_START:(\d{1,}):(.*?)TICKET_END', repl=addWrapperDivAndMenuToTicketHTML, string=ticketsHTML, flags=re.DOTALL);
print('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="https://phab.wmfusercontent.org/res/defaultX/phabricator/31c46c5c/core.pkg.css">
<base href="$BASE_URL" target="_blank">
<style>
div.ticket {{
border: solid 3px #C7CCD9;
margin: 35px 35px 100px 35px;
padding: 5px 20px 20px 20px;
border-radius: 6px;
min-width: 740px;
}}
button.hide {{
margin-left: 30px;
}}
button {{
margin-left: 5px;
}}
div.menu {{
border-radius: 6px;
padding: 6pt;
margin-top: 20px;
}}
span.buttonActionMessage {{
color: gray;
margin-left: 25px;
font-size: 1.2em;
}}
</style>
<script>
const showButtonActionMessage = (ticketNumber, messageString) => document.querySelector(\`span#buttonActionMessage\${{ticketNumber}}\`).innerHTML = messageString
const buttonAction = (ticketNumber, valueString) => {{
showButtonActionMessage(ticketNumber, '💾 saving...')
const button = event.toElement
fetch('$BASE_URL/api/maniphest.edit', {{
method: 'POST',
body: \`api.token=$API_TOKEN&output=json&transactions[0][type]=status&transactions[0][value]=\${{valueString}}&objectIdentifier=T\${{ticketNumber}}\`
}})
.then(response => {{
if (response.ok == false) throw Error()
return response.json()
}})
.then(json => {{
if (json.error_code != null) throw Error()
showButtonActionMessage(ticketNumber, '🎉 success')
setTimeout(() => {{
showButtonActionMessage(ticketNumber, '🙈 hiding...')
setTimeout(() => {{
button.closest('div.ticket').remove()
}}, 1500)
}}, 1500)
}})
.catch(error => {{
showButtonActionMessage(ticketNumber, '💩 failure')
}})
}}
</script>
</head>
<body>
<div class="phabricator-remarkup">
{ticketsHtmlIncludingWrapperDivsAndMenus}
</div>
</body>
</html>
'''.format(ticketsHtmlIncludingWrapperDivsAndMenus=ticketsHtmlIncludingWrapperDivsAndMenus));
--END
# Query the API:
curl -s "$BASE_URL/api/maniphest.search" \
-d "api.token=$API_TOKEN" \
-d "order=priority" \
-d "queryKey=$QUERY_KEY" |
# Pipe the results to Python for processing. Extracts and transforms tickets' remarkup (see: https://secure.phabricator.com/book/phabricator/article/remarkup/).
python -c "$RESULT_TRANSFORMATION" |
# Pipe our remixed remarkup to "remarkup.process" to convert it to HTML:
curl -s "$BASE_URL/api/remarkup.process" \
-d "api.token=$API_TOKEN" \
-d "context=phriction" \
-d @- |
# Extract HTML, add menu, stylesheet and base URL so it looks nice and links work, then send all of it to a browser so we can see it:
python -c "$PAGE_PRESENTATION" > /tmp/phabricator.tmp.html && open -a "Google Chrome" /tmp/phabricator.tmp.html --args --disable-web-security --user-data-dir=/tmp/chrome_dev_test
# To see html as text use line below instead of line above:
# python -c "$PAGE_PRESENTATION" | open -a "Safari" -f
Generate and use your own API token
If you replace the QutieBot
API token api-2lfamy3hsjzihfo6jwmnbd6lphn2
in the script above with your own token string, ticket status changes made with the script will be credited to you vs QutieBot
.
To generate an API token:
- load https://phabricator.wikimedia.org
- tap your user icon in the upper right
- select
Settings
- after Settings load, select
Conduit API Tokens
at the bottom of the left menu - tap the
+ Generate Token
button
Once you have your token, the API_TOKEN
line in the script is where you'd use it instead (of api-2lfamy3hsjzihfo6jwmnbd6lphn2
).
A version of the script with functional "Quick options" buttons for Status and Priority, with Comment support.
This script also moves all fetching and processing into Python.
#!/bin/bash
BASE_URL="https://phabricator.wikimedia.org"
API_TOKEN="api-2lfamy3hsjzihfo6jwmnbd6lphn2"
QUERY_KEY="F1GAHgzz693c"
# The query:
# https://phabricator.wikimedia.org/maniphest/query/F1GAHgzz693c/
# https://stackoverflow.com/a/44828706
read -r -d '' PYTHON_SCRIPT << --END
import sys, json, urllib, urllib2, base64, re;
def fetch(url, values):
data = urllib.urlencode(values)
request = urllib2.Request(url, data)
response = urllib2.urlopen(request)
page = response.read()
return json.loads(page.decode('utf-8'))
# Query the API:
searchResultsJSON = fetch("$BASE_URL/api/maniphest.search", {
'api.token' : '$API_TOKEN',
'order' : 'priority',
'queryKey' : '$QUERY_KEY'
})
resultDicts = searchResultsJSON["result"]["data"]
# Dict with ticketID as key and ticketJSON as value.
ticketsData = dict((x["id"], x) for x in resultDicts)
# Extracts and transforms tickets' remarkup (see: https://secure.phabricator.com/book/phabricator/article/remarkup/).
remarkup = ''.join(list(map(lambda item:
'''TICKET_START:{number}:
= T{number} =
== {name} ==
{description}
TICKET_END'''
.format(
number=item['id'],
name=item['fields']['name'].encode('utf-8'),
description=item['fields']['description']['raw'].encode('utf-8')
), resultDicts)))
# Hit "remarkup.process" to convert our remixed remarkup to HTML:
remarkupResultsJSON = fetch("$BASE_URL/api/remarkup.process", {
'api.token' : '$API_TOKEN',
'context' : 'phriction',
'contents[0]' : remarkup
})
# Extract HTML, add menu, stylesheet and base URL so it looks nice and links work, then send all of it to a browser so we can see it:
menusData = [
{
'endpoint': 'maniphest.edit',
'key': 'status',
'names': ['Open', 'Resolved', 'Declined', 'Stalled', 'Invalid'],
'values': ['open', 'resolved', 'declined', 'stalled', 'invalid']
},
{
'endpoint': 'maniphest.edit',
'key': 'priority',
'names': ['Unbreak Now!', 'Needs Triage', 'High', 'Medium', 'Low', 'Lowest'],
'values': ['unbreak', 'triage', 'high', 'medium', 'low', 'lowest']
}
]
def buttonHTML(ticketID, endpoint, key, name, value, isSelected):
classString = "class='selected'" if isSelected else ""
return '''\n<button id="{ticketID}:{endpoint}:{key}:{value}" {classString} onclick="buttonAction({ticketID}, '{endpoint}', '{key}', '{value}')" onblur="showButtonActionMessage({ticketID}, '')">{name}</button>'''.format(ticketID=ticketID, endpoint=endpoint, key=key, value=value, classString=classString, name=name)
def allButtonsHTML(ticketID, menuItemJSON, ticketJSON):
endpoint = menuItemJSON['endpoint']
key = menuItemJSON['key']
selectedName = ticketJSON['fields'][key]['name']
menuItemNameValuePairs = [{'name': name, 'value': value} for name, value in zip(menuItemJSON['names'], menuItemJSON['values'])]
return ''.join(list(map(lambda nameValuePair: buttonHTML(ticketID, endpoint, key, nameValuePair['name'], nameValuePair['value'], selectedName == nameValuePair['name']), menuItemNameValuePairs)))
def allMenusHTML(ticketID, ticketJSON):
menus = ''.join(
list(
map(lambda menuItemJSON:
'''
<div class="menu">
{key}:
<br>
{buttons}
</div>
'''
.format(
key=menuItemJSON['key'].capitalize(),
buttons=allButtonsHTML(ticketID, menuItemJSON, ticketJSON)
)
, menusData
)
)
)
return '''
<div class="menus phui-tag-core phui-tag-color-object">
<span class=buttonActionMessage id="buttonActionMessage{ticketID}"></span>
<h2>Quick options</h2>
{menus}
Comment: ( recorded with any <strong>Quick options</strong> chosen above )
<br>
<textarea id="ticketID{ticketID}" style="height: 70px; width: 100%;"></textarea>
</div>
'''.format(ticketID=ticketID, menus=menus)
def addWrapperDivAndMenuToTicketHTML(match):
ticketID = match.group(1)
ticketHTML = match.group(2)
ticketJSON = ticketsData[int(ticketID)]
return '''
<div class=ticket id="T{ticketID}">
<button class=hide onclick="this.closest('div#T{ticketID}').remove()">Hide</button>
{ticketHTML}
{menusString}
</div>
'''.format(ticketID=ticketID, ticketHTML=ticketHTML, menusString=allMenusHTML(ticketID, ticketJSON))
ticketsHTML = remarkupResultsJSON['result'][0]['content'].encode('utf-8')
ticketsHtmlIncludingWrapperDivsAndMenus = re.sub(pattern=r'TICKET_START:(.*?):(.*?)TICKET_END', repl=addWrapperDivAndMenuToTicketHTML, string=ticketsHTML, flags=re.DOTALL)
print('''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="https://phab.wmfusercontent.org/res/defaultX/phabricator/31c46c5c/core.pkg.css">
<base href="$BASE_URL" target="_blank">
<style>
div.ticket {{
border: solid 3px #C7CCD9;
margin: 35px 35px 100px 35px;
padding: 5px 20px 20px 20px;
border-radius: 6px;
min-width: 740px;
}}
button.hide {{
float: right;
margin-top: 20px;
}}
button:not([class=selected]) {{
background-color: white;
border: solid 1px #C7CCD9;
color: #777C89;
}}
button, button:hover {{
background-image: unset;
margin-right: 4px;
}}
div.menus {{
border-radius: 6px;
padding: 6pt;
margin-top: 20px;
}}
span.buttonActionMessage {{
color: gray;
margin-left: 25px;
font-size: 1.2em;
float: right;
}}
div.menu {{
margin: 8px 8px 8px 14px;
}}
</style>
<script>
const showButtonActionMessage = (ticketID, messageString) => document.querySelector(\`span#buttonActionMessage\${{ticketID}}\`).innerHTML = messageString
const updateMenuButtonsSelectionStyle = (ticketID, endpoint, key, value) => {{
const buttonsSelector = \`button[id^='\${{ticketID}}\:\${{endpoint}}:\${{key}}:']\`
const buttonToBeSelectedID = \`\${{ticketID}}\:\${{endpoint}}:\${{key}}:\${{value}}\`
document.querySelectorAll(buttonsSelector).forEach(button => button.classList[button.id == buttonToBeSelectedID ? 'add' : 'remove']('selected'))
}}
const buttonAction = (ticketID, endpoint, key, value) => {{
showButtonActionMessage(ticketID, '💾 Saving...')
const button = event.toElement
const commentTextArea = document.querySelector(\`textarea#ticketID\${{ticketID}}\`)
const comment = commentTextArea.value
const commentParameters = comment ? \`&transactions[1][type]=comment&transactions[1][value]=\${{comment}}\` : ''
fetch(\`$BASE_URL/api/\${{endpoint}}\`, {{
method: 'POST',
body: \`api.token=$API_TOKEN&output=json&transactions[0][type]=\${{key}}&transactions[0][value]=\${{value}}&objectIdentifier=T\${{ticketID}}\${{commentParameters}}\`
}})
.then(response => {{
if (response.ok == false) throw Error()
return response.json()
}})
.then(json => {{
if (json.error_code != null) throw Error()
showButtonActionMessage(ticketID, '🎉 Success')
updateMenuButtonsSelectionStyle(ticketID, endpoint, key, value)
commentTextArea.value = ''
}})
.catch(error => {{
showButtonActionMessage(ticketID, '💩 Failure')
}})
}}
</script>
</head>
<body>
<div class="phabricator-remarkup">
{ticketsHtmlIncludingWrapperDivsAndMenus}
</div>
</body>
</html>
'''.format(ticketsHtmlIncludingWrapperDivsAndMenus=ticketsHtmlIncludingWrapperDivsAndMenus));
--END
python -c "$PYTHON_SCRIPT"> /tmp/phabricator.tmp.html && open -a "Google Chrome" /tmp/phabricator.tmp.html --args --disable-web-security --user-data-dir=/tmp/chrome_dev_test
# To see html as text use line below instead of line above:
# python -c "$PYTHON_SCRIPT" | open -a "Safari" -f
This version moves everything to Python3.
#!/usr/local/bin/python3
import sys, json, urllib.parse, urllib.request, base64, re, subprocess;
BASE_URL = "https://phabricator.wikimedia.org"
API_TOKEN = "api-2lfamy3hsjzihfo6jwmnbd6lphn2"
QUERY_KEY = "F1GAHgzz693c"
def fetch(url, values):
data = urllib.parse.urlencode(values)
request = urllib.request.Request(url = url, headers = {}, data = data.encode('utf-8'))
response = urllib.request.urlopen(request)
page = response.read()
return json.loads(page.decode('utf-8'))
# Query the API:
searchResultsJSON = fetch(f"{BASE_URL}/api/maniphest.search", {
'api.token' : API_TOKEN,
'order' : 'priority',
'queryKey' : QUERY_KEY
})
resultDicts = searchResultsJSON["result"]["data"]
# Dict with ticketID as key and ticketJSON as value.
ticketsData = dict((x["id"], x) for x in resultDicts)
# Extracts and transforms tickets' remarkup (see: https://secure.phabricator.com/book/phabricator/article/remarkup/).
remarkup = ''.join(list(map(lambda item:
'''TICKET_START:{number}:
= T{number} =
== {name} ==
{description}
TICKET_END'''
.format(
number=item['id'],
name=item['fields']['name'],
description=item['fields']['description']['raw']
), resultDicts)))
# Hit "remarkup.process" to convert our remixed remarkup to HTML:
remarkupResultsJSON = fetch(f"{BASE_URL}/api/remarkup.process", {
'api.token' : API_TOKEN,
'context' : 'phriction',
'contents[0]' : remarkup
})
# Extract HTML, add menu, stylesheet and base URL so it looks nice and links work, then send all of it to a browser so we can see it:
menusData = [
{
'endpoint': 'maniphest.edit',
'key': 'status',
'names': ['Open', 'Resolved', 'Declined', 'Stalled', 'Invalid'],
'values': ['open', 'resolved', 'declined', 'stalled', 'invalid']
},
{
'endpoint': 'maniphest.edit',
'key': 'priority',
'names': ['Unbreak Now!', 'Needs Triage', 'High', 'Medium', 'Low', 'Lowest'],
'values': ['unbreak', 'triage', 'high', 'medium', 'low', 'lowest']
}
]
def buttonHTML(ticketID, endpoint, key, name, value, isSelected):
classString = "class='selected'" if isSelected else ""
return '''\n<button id="{ticketID}:{endpoint}:{key}:{value}" {classString} onclick="buttonAction({ticketID}, '{endpoint}', '{key}', '{value}')" onblur="showButtonActionMessage({ticketID}, '')">{name}</button>'''.format(ticketID=ticketID, endpoint=endpoint, key=key, value=value, classString=classString, name=name)
def allButtonsHTML(ticketID, menuItemJSON, ticketJSON):
endpoint = menuItemJSON['endpoint']
key = menuItemJSON['key']
selectedName = ticketJSON['fields'][key]['name']
menuItemNameValuePairs = [{'name': name, 'value': value} for name, value in zip(menuItemJSON['names'], menuItemJSON['values'])]
return ''.join(list(map(lambda nameValuePair: buttonHTML(ticketID, endpoint, key, nameValuePair['name'], nameValuePair['value'], selectedName == nameValuePair['name']), menuItemNameValuePairs)))
def allMenusHTML(ticketID, ticketJSON):
menus = ''.join(
list(
map(lambda menuItemJSON:
'''
<div class="menu">
{key}:
<br>
{buttons}
</div>
'''
.format(
key=menuItemJSON['key'].capitalize(),
buttons=allButtonsHTML(ticketID, menuItemJSON, ticketJSON)
)
, menusData
)
)
)
return '''
<div class="menus phui-tag-core phui-tag-color-object">
<span class=buttonActionMessage id="buttonActionMessage{ticketID}"></span>
<h2>Quick options</h2>
{menus}
Comment: ( recorded with any <strong>Quick options</strong> chosen above )
<br>
<textarea id="ticketID{ticketID}" style="height: 70px; width: 100%;"></textarea>
</div>
'''.format(ticketID=ticketID, menus=menus)
def addWrapperDivAndMenuToTicketHTML(match):
ticketID = match.group(1)
ticketHTML = match.group(2)
ticketJSON = ticketsData[int(ticketID)]
return '''
<div class=ticket id="T{ticketID}">
<button class=hide onclick="this.closest('div#T{ticketID}').remove()">Hide</button>
{ticketHTML}
{menusString}
</div>
'''.format(ticketID=ticketID, ticketHTML=ticketHTML, menusString=allMenusHTML(ticketID, ticketJSON))
ticketsHTML = remarkupResultsJSON['result'][0]['content']
ticketsHtmlIncludingWrapperDivsAndMenus = re.sub(pattern=r'TICKET_START:(.*?):(.*?)TICKET_END', repl=addWrapperDivAndMenuToTicketHTML, string=ticketsHTML, flags=re.DOTALL)
pageHTML = f'''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="https://phab.wmfusercontent.org/res/defaultX/phabricator/31c46c5c/core.pkg.css">
<base href="{BASE_URL}" target="_blank">
<style>
div.ticket {{
border: solid 3px #C7CCD9;
margin: 35px 35px 100px 35px;
padding: 5px 20px 20px 20px;
border-radius: 6px;
min-width: 740px;
}}
button.hide {{
float: right;
margin-top: 20px;
}}
button:not([class=selected]) {{
background-color: white;
border: solid 1px #C7CCD9;
color: #777C89;
}}
button, button:hover {{
background-image: unset;
margin-right: 4px;
}}
div.menus {{
border-radius: 6px;
padding: 6pt;
margin-top: 20px;
}}
span.buttonActionMessage {{
color: gray;
margin-left: 25px;
font-size: 1.2em;
float: right;
}}
div.menu {{
margin: 8px 8px 8px 14px;
}}
</style>
<script>
const showButtonActionMessage = (ticketID, messageString) => document.querySelector(`span#buttonActionMessage${{ticketID}}`).innerHTML = messageString
const updateMenuButtonsSelectionStyle = (ticketID, endpoint, key, value) => {{
const buttonsSelector = `button[id^='${{ticketID}}\:${{endpoint}}:${{key}}:']`
const buttonToBeSelectedID = `${{ticketID}}\:${{endpoint}}:${{key}}:${{value}}`
document.querySelectorAll(buttonsSelector).forEach(button => button.classList[button.id == buttonToBeSelectedID ? 'add' : 'remove']('selected'))
}}
const buttonAction = (ticketID, endpoint, key, value) => {{
showButtonActionMessage(ticketID, '💾 Saving...')
const button = event.toElement
const commentTextArea = document.querySelector(`textarea#ticketID${{ticketID}}`)
const comment = commentTextArea.value
const commentParameters = comment ? `&transactions[1][type]=comment&transactions[1][value]=${{comment}}` : ''
fetch(`{BASE_URL}/api/${{endpoint}}`, {{
method: 'POST',
body: `api.token={API_TOKEN}&output=json&transactions[0][type]=${{key}}&transactions[0][value]=${{value}}&objectIdentifier=T${{ticketID}}${{commentParameters}}`
}})
.then(response => {{
if (response.ok == false) throw Error()
return response.json()
}})
.then(json => {{
if (json.error_code != null) throw Error()
showButtonActionMessage(ticketID, '🎉 Success')
updateMenuButtonsSelectionStyle(ticketID, endpoint, key, value)
commentTextArea.value = ''
}})
.catch(error => {{
showButtonActionMessage(ticketID, '💩 Failure')
}})
}}
</script>
</head>
<body>
<div class="phabricator-remarkup">
{ticketsHtmlIncludingWrapperDivsAndMenus}
</div>
</body>
</html>
'''
def sendToBrowser(string, extension):
filePath = '/tmp/chrome.tmp.' + extension
f = open(filePath, 'wt', encoding='utf-8')
f.write(string)
subprocess.run(f'open -a "Google Chrome" {filePath} --args --disable-web-security --user-data-dir=/tmp/chrome_dev_test', shell=True, check=True, text=True)
sendToBrowser(pageHTML, 'html')
# To see html as text use line below instead of line above:
# sendToBrowser(pageHTML, 'txt')
Change a ticket's status
Example manifest.edit Javascript for changing a ticket's status:
open
can be any valid statusobjectIdentifier
can be any phab ticket number