{"push_id":2199006363,"size":47,"distinct_size":47,"ref":"refs/heads/ATMO-2090","head":"a86250561bc2346691a047e2a5833093f013b00c","before":"2b4cb7b0bc88104758aec941666527eabb3f6043","commits":[{"sha":"d5a5b8a2741c0cd465bd1987f4b2b201d4d7a6c4","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Tweak formatting","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/d5a5b8a2741c0cd465bd1987f4b2b201d4d7a6c4"},{"sha":"a3cac39a550eb30d508e1f922acbceb1490924a9","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Perform extract-method refactors\n\nThis takes the 6 views that make up \"Advanced Options\" and extracts them\nout into a method per view, plus one for Advanced Options.\n\nThis also moves around handlers, unifying to the `on${errorName}`\nscheme.\n\nMethods that are used to validate are grouped as well.\n\nIn general, this is a restructuring but the \"extract-method\" refactor\nwas leveraged to try to harmonize the \"level of abstraction\" each\nmethod operates at.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/a3cac39a550eb30d508e1f922acbceb1490924a9"},{"sha":"a6ab604f71e108a90fee099204fd6c411e514c1a","author":{"name":"Andrew Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Merge pull request #732 from lenards/atmo-1531\n\nProblem: Edit ImageVersion Modal has become overgrown","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/a6ab604f71e108a90fee099204fd6c411e514c1a"},{"sha":"04418dadbfe14600c5f96ad502c2028848a2ce79","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Use getter to fetch UUID","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/04418dadbfe14600c5f96ad502c2028848a2ce79"},{"sha":"a8f89c9d937351041f22178ff0d507b6f14c71dc","author":{"name":"Steve Gregory","email":"1927999686014308dead34c1cfcb9d8bd5271589@steve-gregory.com"},"message":"Merge pull request #733 from lenards/atmo-1531-pre\n\nProblem: Cannot add a software license to a ImageVersion","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/a8f89c9d937351041f22178ff0d507b6f14c71dc"},{"sha":"e797efef45987eb3e63b8cd56b9bc79221a1c315","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Use getter to fetch UUID","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/e797efef45987eb3e63b8cd56b9bc79221a1c315"},{"sha":"f62a0e73321c73e2dfacecced6ed5c69d744ef44","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Define DOI input component\n\nThis defines a Document Object Identifier (DOI) input component for\nassociating with an Image Version. Over time, an Image is versioned,\nand the DOI represents that *exact* Image Version.\n\nThe format looks something like this: 10.7946/P23011","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/f62a0e73321c73e2dfacecced6ed5c69d744ef44"},{"sha":"24a52b2abaa5a6330800001b5f1fe9dca0de5618","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Add helper to indicate DOI present","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/24a52b2abaa5a6330800001b5f1fe9dca0de5618"},{"sha":"856f4bfe7b69abf7e7e9214932387d9020f05125","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Render DOI, if present, on <Version/>","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/856f4bfe7b69abf7e7e9214932387d9020f05125"},{"sha":"c81e7031a3208ffe45ef4a07e892eeac4f66587b","author":{"name":"Andrew J Lenards","email":"d76d4b3b4f40f5b65a7f287820f06811739ea9dd@gmail.com"},"message":"Allow DOI to be added to Version\n\nFix lint errors","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/c81e7031a3208ffe45ef4a07e892eeac4f66587b"},{"sha":"5ba2b0d30a23ab4f9ab8a18be47883b21361d493","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Separate resource submission from resource save\n\nResource actions was renamed and simplified to be ResourceRequestActions.js\n\nI've included some comments over the diff, because many of the changes were\nrelated, and its mostly a rewrite.\n\n    diff --git a/troposphere/static/js/actions/ResourceActions.js b/troposphere/static/js/actions/ResourceActions.js\n    deleted file mode 100644\n    index fb963b595..000000000\n\nAll the actions inside ResourceRequestActions were replaced with just updateRequest. Resource actions used to do things like save quota and allocation but now they just update a status field on a request from pending to either active|deny|closed.\n\n    diff --git a/troposphere/static/js/actions/ResourceRequestActions.js b/troposphere/static/js/actions/ResourceRequestActions.js\n    new file mode 100644\n    index 000000000..328e4c721\n    --- /dev/null\n    +++ b/troposphere/static/js/actions/ResourceRequestActions.js\n    @@ -0,0 +1,23 @@\n    +import Utils from \"./Utils\";\n    +import ResourceConstants from \"constants/ResourceRequestConstants\";\n    +import errorHandler from \"actions/errorHandler\"\n    +\n    +export default {\n    +    updateRequest(request, status, reason) {\n    +        let params = Object.assign(\n    +            { status: status.id },\n    +            reason === undefined ? {} : { admin_message: reason }\n    +        )\n    +\n    +        return Promise.resolve(request.save(params, { patch: true }))\n    +            .then(() => {\n    +                Utils.dispatch(ResourceConstants.UPDATE, {\n    +                    model: request\n    +                });\n    +                Utils.dispatch(ResourceConstants.REMOVE, {\n    +                    model: request\n    +                });\n    +            })\n    +            .catch(errorHandler)\n    +    }\n    +};\n\nTheses changes are actually just a file rename, this file used to be caled AllocationSourcesView.\n\n    diff --git a/troposphere/static/js/components/admin/ResourceRequest/AllocationSourceView.jsx b/troposphere/static/js/components/admin/ResourceRequest/AllocationSourceView.jsx\n    new file mode 100644\n    index 000000000..426150b25\n    --- /dev/null\n    +++ b/troposphere/static/js/components/admin/ResourceRequest/AllocationSourceView.jsx\n    @@ -0,0 +1,250 @@\n    +import React from \"react\";\n    +\n    +import globals from \"globals\";\n    +import ProgressBar from \"components/common/ui/ProgressBar\";\n    +\n    +export default React.createClass({\n    +\n    +    propTypes: {\n    +        allocationSource: React.PropTypes.object.isRequired,\n    +        onSave: React.PropTypes.func.isRequired\n    +    },\n    +\n    +    renderAllocation(allocationSource) {\n    +        let { name } = allocationSource.pick(\"name\");\n    +        let { onSave } = this.props;\n    +        if (globals.EXTERNAL_ALLOCATION) {\n    +            return (\n    +            <AllocationView key={name} { ...{ allocationSource } } />\n    +            );\n    +        }\n    +        let props = { allocationSource, onSave };\n    +        return (\n    +        <AllocationEditableSaveableView key={name} { ...props } />\n    +        );\n    +    },\n    +\n    +    render() {\n    +        let { allocationSource } = this.props;\n    +\n    +        return (\n    +        <div>\n    +            { this.renderAllocation(allocationSource) }\n    +        </div>\n    +        );\n    +    }\n    +});\n    +\n    +class AllocationView extends React.Component {\n    +    getUsage(allocation) {\n    +        let { compute_used, compute_allowed } = allocation.pick([\"compute_used\", \"compute_allowed\"]);\n    +        let percent = 0;\n    +        let total = 0;\n    +        // Prevent division by 0\n    +        if (compute_allowed !== 0) {\n    +\n    +            // Note: Multiplying/rounding to get 2 decimal places\n    +            percent = Math.round((compute_used / compute_allowed * 100) * 100) / 100;\n    +\n    +            total = compute_allowed;\n    +        }\n    +\n    +        return { percent, total };\n    +    }\n    +\n    +    render() {\n    +        let { percent, total } = this.getUsage(this.props.allocationSource);\n    +        let { name } = this.props.allocationSource.pick(\"name\");\n    +\n    +        let allocationLabel = (\n    +        <span>\n    +             { `${percent}% used of ${total} Allocation from ${name}` }\n    +        </span>\n    +        );\n    +\n    +        return (\n    +        <ProgressBar startColor=\"#56AA21\" startValue={percent} label={allocationLabel} />\n    +        );\n    +    }\n    +}\n    +\n    +AllocationView.propTypes = {\n    +    allocationSource: React.PropTypes.object.isRequired\n    +}\n    +\n    +class AllocationEditableView extends AllocationView {\n    +    constructor(props) {\n    +        super(props);\n    +        this.state = {\n    +            isEditing: false\n    +        };\n    +    }\n    +\n    +    renderEditIcon() {\n    +        let { isEditing } = this.state;\n    +        return (\n    +        <span style={{margin:\"0 5px\", cursor: \"pointer\" }}\n    +            onClick={() => this.setState({ isEditing: !isEditing })}\n    +            className=\"glyphicon glyphicon-pencil\" />\n    +        );\n    +    }\n    +\n    +    onInputChange({ target: { value }}) {\n    +        let id = this.props.allocationSource.get(\"id\");\n    +        let compute_allowed = +value;\n    +        this.props.onChange({ id, compute_allowed });\n    +    }\n    +\n    +    renderEditableField(num) {\n    +        let style = {\n    +            margin:\"0 5px\"\n    +        }\n    +        return (\n    +        <input style={style}\n    +               onChange={this.onInputChange.bind(this)}\n    +               type=\"number\"\n    +               value={num} />\n    +        );\n    +    }\n    +\n    +    render() {\n    +        let { name } = this.props.allocationSource.pick(\"name\");\n    +        let { isEditing } = this.state;\n    +        let { percent, total } = this.getUsage(this.props.allocationSource);\n    +        let editableTotal =\n    +            isEditing\n    +            ? this.renderEditableField(total)\n    +            : total;\n    +\n    +        let allocationLabel = (\n    +        <span>\n    +             { `${percent}% used of ` }\n    +             { editableTotal }\n    +             { ` Allocation from ${name}` }\n    +             { this.renderEditIcon() }\n    +        </span>\n    +        );\n    +\n    +        return (\n    +        <ProgressBar startColor=\"#56AA21\" startValue={percent} label={allocationLabel} />\n    +        );\n    +    }\n    +}\n    +\n    +AllocationEditableView.propTypes = {\n    +    onChange: React.PropTypes.func.isRequired\n    +}\n    +\n    +class AllocationEditableSaveableView extends AllocationView {\n    +    constructor(props) {\n    +        super(props);\n    +        this.state = {\n    +            // TODO change allocationSource to alllocation\n    +            allocationSource: props.allocationSource.clone(),\n    +            isEditing: false,\n    +            isPending: false,\n    +            mounted: true\n    +        };\n    +    }\n    +\n    +    componentWillUnmount() { this.state.mounted = false; }\n    +\n    +    componentWillReceiveProps({ allocationSource }) {\n    +        this.setState({ allocationSource: allocationSource.clone() });\n    +    }\n    +\n    +    renderEditIcon() {\n    +        let { isEditing } = this.state;\n    +        return (\n    +        <span style={{margin:\"0 5px\", cursor: \"pointer\" }}\n    +            onClick={() => this.setState({ isEditing: !isEditing })}\n    +            className=\"glyphicon glyphicon-pencil\" />\n    +        );\n    +    }\n    +\n    +    onInputChange({ target: { value }}) {\n    +        let { allocationSource } = this.state;\n    +        allocationSource.set(\"compute_allowed\", +value);\n    +        this.setState({ allocationSource });\n    +    }\n    +\n    +    renderEditableField(num) {\n    +        let style = {\n    +            margin:\"0 5px\"\n    +        }\n    +        return (\n    +        <input style={style}\n    +               onChange={this.onInputChange.bind(this)}\n    +               type=\"number\"\n    +               value={num} />\n    +        );\n    +    }\n    +\n\nWhen the allocation source is saved it calls an onSave method, which returns a\npromise for the action. The view uses this promise to show a loading symbol\nwhile its pending.\n\n    +    onSave() {\n    +       let { allocationSource } = this.state;\n    +        // Clone our allocationSource, so that call to props.onSave doesn't affect our\n    +        // state\n    +        let clone = allocationSource.clone()\n    +        this.setState({ isPending: true });\n    +        this.props.onSave(clone)\n    +            .then(this.resetPending.bind(this))\n    +            .catch(this.resetPending.bind(this));\n    +    }\n    +\n    +    resetPending() {\n    +        if (this.state.mounted) {\n    +            this.setState({ isPending: false })\n    +        }\n    +    }\n    +\n    +    renderSaveText() {\n    +        let { isPending } = this.state;\n    +        let buttonText = isPending ? \"loading\" : \"save\";\n    +        return <span> <a style={{ float: \"right\" }} onClick={ this.onSave.bind(this) }>{ buttonText }</a></span>;\n    +    }\n    +\n    +    render() {\n    +        let { allocationSource: cloudAllocation } = this.props;\n    +        let { isEditing, isPending, allocationSource } = this.state;\n    +        let { percent, total } = this.getUsage(allocationSource);\n    +        let { name } = this.props.allocationSource.pick(\"name\");\n    +        let editableTotal =\n    +            isEditing\n    +            ? this.renderEditableField(total)\n    +            : total;\n    +\n    +        let hasChanged =\n    +            // Check if the cloud allocation and local allocation differ\n    +            allocationSource.keys().some(attr => allocationSource.get(attr) !== cloudAllocation.get(attr));\n    +\n    +        let statusElement = null;\n    +        if (isPending) {\n    +            statusElement = <span style={{ float: \"right\", margin: 0 }} className=\"loading-small\"></span>;\n    +        } else if (hasChanged) {\n    +            statusElement = <span> <a style={{ float: \"right\" }} onClick={ this.onSave.bind(this) }>save</a></span>;\n    +        }\n    +\n    +        let allocationLabel = (\n    +        <span>\n    +             { `${percent}% used of ` }\n    +             { editableTotal }\n    +             { ` Allocation from ${name}` }\n    +             { this.renderEditIcon() }\n    +             { statusElement }\n    +        </span>\n    +        );\n    +\n    +        return (\n    +        <ProgressBar startColor=\"#56AA21\" startValue={percent} label={allocationLabel} />\n    +        );\n    +    }\n    +}\n    +\n    +AllocationEditableSaveableView.propTypes = {\n    +    onSave: React.PropTypes.func.isRequired\n    +}\n    +\n    +\n\nThis is a view that displays an identity's provider and allows the quota to be\nsaved.\n\n    diff --git a/troposphere/static/js/components/admin/ResourceRequest/IdentityView.jsx b/troposphere/static/js/components/admin/ResourceRequest/IdentityView.jsx\n    new file mode 100644\n    index 000000000..147011109\n    --- /dev/null\n    +++ b/troposphere/static/js/components/admin/ResourceRequest/IdentityView.jsx\n    @@ -0,0 +1,79 @@\n    +import React from \"react\";\n    +\n    +import QuotaView from \"components/admin/ResourceRequest/QuotaView\";\n    +\n    +export default React.createClass({\n    +\n\nThis component has a prop: identity, and also state: identity. The prop\nidentity is the most recent thing from the stores and the state value is what\ngets manipulated via the gui. The component uses its internal identity\ncompared to the cloud identity to determine when the identity has changed and\nwhen it can be saved.\n\n    +    propTypes: {\n    +        identity: React.PropTypes.object.isRequired,\n    +    },\n    +\n    +    componentWillUnmount() {\n    +        this.setState({ mounted: false })\n    +    },\n    +\n    +    getInitialState() {\n    +        return {\n    +            identity: this.props.identity.clone(),\n    +            isPending: false,\n    +            mounted: true,\n    +        }\n    +    },\n    +\n    +    componentWillReceiveProps({ identity }) {\n    +        this.setState({ identity: identity.clone() });\n    +    },\n    +\n    +    onSave() {\n    +        let { identity } = this.state;\n    +\n    +        // Clone our identity, so that call to props.onSave doesn't affect our\n    +        // state\n    +        let clone = identity.clone()\n    +        this.setState({ isPending: true });\n    +        this.props.onSave(clone)\n    +            .then(this.resetPending)\n    +            .catch(this.resetPending)\n    +    },\n    +\n    +    resetPending() {\n    +        if (this.state.mounted) {\n    +            this.setState({ isPending: false })\n    +        }\n    +    },\n    +\n    +    onQuotaChange(quota) {\n    +        let { identity } = this.state;\n    +\n    +        identity.set(\"quota\", quota);\n    +        this.setState({ identity });\n    +    },\n    +\n    +    render() {\n    +        let { identity: cloudIdentity } = this.props;\n    +        let { identity, isPending } = this.state;\n    +        let { onQuotaChange } = this;\n    +        let localQuota = identity.get(\"quota\");\n    +        let cloudQuota = cloudIdentity.get(\"quota\");\n    +\n    +        let quotaChanged =\n    +            // Check if the the cloud identity and local identity for different quota\n    +            Object.keys(cloudQuota).some(attr => cloudQuota[attr] !== localQuota[attr]);\n    +\n    +        let statusElement = null;\n    +        if (isPending) {\n    +            statusElement = <span style={{ float: \"right\", margin: 0 }} className=\"loading-small\"></span>;\n    +        } else if (quotaChanged) {\n    +            statusElement = <span> <a style={{ float: \"right\" }} onClick={ this.onSave }>save</a></span>;\n    +        }\n    +\n    +        return (\n    +        <div style={{ marginBottom: \"10px\" }}>\n    +            <p>{ identity.get(\"provider\").name }{ statusElement }</p>\n    +            <QuotaView {...{ quota: localQuota, onQuotaChange }} />\n    +        </div>\n    +        );\n    +    }\n    +});\n\nThis is the primary view that fetches data and performs saves.\n\n    diff --git a/troposphere/static/js/components/admin/ResourceRequest/ResourceRequest.jsx b/troposphere/static/js/components/admin/ResourceRequest/ResourceRequest.jsx\n    index 8e8fe9990..48f322177 100644\n    --- a/troposphere/static/js/components/admin/ResourceRequest/ResourceRequest.jsx\n    +++ b/troposphere/static/js/components/admin/ResourceRequest/ResourceRequest.jsx\n    @@ -2,15 +2,16 @@ import React from \"react\";\n     import _ from \"underscore\";\n     import Backbone from \"backbone\";\n\n    +import Utils from \"actions/Utils\";\n     import stores from \"stores\";\n     import { browserHistory } from \"react-router\";\n     import ResourceRequestView from \"./ResourceRequestView\";\n    -import ResourceActions from \"actions/ResourceActions\";\n    +import ResourceRequestActions from \"actions/ResourceRequestActions\";\n     import Quota from \"models/Quota\";\n    +import AllocationSourceConstants from \"constants/AllocationSourceConstants\";\n    +import IdentityConstants from \"constants/IdentityConstants\"\n    +import errorHandler from \"actions/errorHandler\"\n\n    -// This component is responsible for interfacing with our stores, and\n    -// fetching/aggregating data. It does the dirty work for the\n    -// ResourceRequestView.\n     export default React.createClass({\n\n         propTypes: {\n\nThe state field: mounted was added so that the component could fire off\npromises and conditionally perform setState calls if it is still in the dom.\n\n    @@ -20,7 +21,8 @@ export default React.createClass({\n\n         getInitialState() {\n             return {\n    -            actionPending: false\n    +            actionPending: false,\n    +            mounted: true\n             };\n         },\n\nThe onApprove method (when resource requests are approved) was broken out into\nseveral methods because approve is much simpler now, allocations and quotas\nare saved separately.\n\n    -    onApprove(view) {\n    +    onAllocationSave(allocationSource) {\n             let { selectedRequest: request } = this.props;\n    -        let {\n    -            quota: storeQuota,\n    -            allocationSources: storeAllocations,\n    -            identity,\n    -            statuses\n    -        } = this.fetch();\n    -\n    -        let { quota, allocationSources } = view;\n    -\n    -        let quotas = stores.QuotaStore.getAll();\n    -        let response = view.response;\n    -\n    -        let quotaChanged =\n    -            // Test if some of the attributes are not equal\n    -            storeQuota.keys().some(attr => storeQuota.get(attr) !== quota[attr]);\n    -\n    -        let updatedQuota;\n    -        if (quotaChanged) {\n    -            updatedQuota =\n    -                // Search for quota in store based on attributes not id\n    -                quotas.findWhere(_.omit(quota, \"id\")) ||\n    -\n    -                // Updated quota doesn't exist, so create one\n    -                new Quota(_.omit(quota, \"id\"));\n    -        }\n    -\n    -        let changedAllocations = allocationSources\n    -            .filter(as => {\n    -                let storeAs = storeAllocations.get(as.uuid);\n    -                let allocationChanged =\n    -                    storeAs.keys().some(attr => storeAs.get(attr) !== as[attr])\n    -                return allocationChanged;\n    +        return Promise.resolve(allocationSource.save(allocationSource.pick(\"compute_allowed\"), { patch: true }))\n    +            .then(() => {\n    +                Utils.dispatch(AllocationSourceConstants.UPDATE, {\n    +                    allocation: allocationSource,\n    +                    username: request.get(\"created_by\").username\n    +                })\n                 })\n    -            .map(as => {\n    -                let storeAs = storeAllocations.get(as.uuid);\n    -                return storeAs.set(as);\n    +            .catch(errorHandler)\n    +    },\n    +\n    +    onIdentitySave(identity) {\n    +        let { selectedRequest: request } = this.props;\n    +        let quota = new Quota(_.omit(identity.get('quota'), [\"id\", \"uuid\"]));\n    +        return Promise.resolve(quota.save())\n    +            .then(\n    +                () => Promise.resolve(identity.save({ 'quota': quota.pick(\"id\") }, { patch: true }))\n    +            )\n    +            .then(() => {\n    +                Utils.dispatch(IdentityConstants.UPDATE, {\n    +                    identity,\n    +                    username: request.get(\"created_by\").username\n    +                });\n                 })\n    +            .catch(errorHandler)\n    +    },\n\n    +    onApprove() {\n    +        let { selectedRequest: request } = this.props;\n    +        let { statuses } = this.fetch();\n             let status = statuses.findWhere({\n                 name: \"approved\"\n             });\n\n             this.setState({ actionPending: true });\n    -        ResourceActions.approve({\n    -            allocationSources: changedAllocations,\n    -            identity,\n    -            quota: updatedQuota,\n    -            status,\n    -            request,\n    -            response\n    -        }).then(() => {\n    -            browserHistory.push(\"/application/admin/resource-requests\");\n    -        }).catch(this.setActionResolved)\n    +        ResourceRequestActions.updateRequest(request, status)\n    +            .then(() => {\n    +                browserHistory.push(\"/application/admin/resource-requests\");\n    +            })\n    +            .always(this.setActionResolved);\n         },\n\n         setActionResolved() {\n    -        this.setState({ actionPending: false });\n    +        if (this.state.mounted) {\n    +            this.setState({ actionPending: false });\n    +        }\n         },\n\n    -    onDeny(view) {\n    +    onDeny(reason) {\n             let { selectedRequest: request } = this.props;\n             let { statuses } = this.fetch();\n\n    -        let response = view.response;\n    -\n             let status = statuses.findWhere({\n                 name: \"denied\"\n             });\n\n             this.setState({ actionPending: true });\n    -        ResourceActions.deny({\n    -            request,\n    -            response,\n    -            status\n    -        }).then(() => {\n    -            browserHistory.push(\"/application/admin/resource-requests\");\n    -        }).catch(this.setActionResolved)\n    +        ResourceRequestActions.updateRequest(request, status)\n    +            .then(() => {\n    +                browserHistory.push(\"/application/admin/resource-requests\");\n    +            })\n    +            .always(this.setActionResolved);\n         },\n\n         fetch() {\n             let { selectedRequest: request } = this.props;\n    -        let quotas = stores.QuotaStore.getAll();\n             let statuses = stores.StatusStore.getAll();\n\n             let identities;\n    -        if (request)\n    +        if (request) {\n                 identities = stores.IdentityStore.fetchWhere({\n                     username: request.get(\"created_by\").username\n                 });\n    +        }\n\n    -        let identity;\n    -        if (identities && request) {\n    -            identity = identities.get(request.get(\"identity\").id);\n    +        let requestIdent;\n    +        if (request && request.has(\"identity\")) {\n    +            requestIdent = request.get(\"identity\");\n             }\n\n    -        let quota;\n    -        if (quotas && identity) {\n    -            quota = quotas.get(identity.get(\"quota\").id);\n    +        // Limit identities to identity found in resource request if it exists\n    +        if (identities && requestIdent) {\n    +            identities = identities.cfilter(i => i.get(\"id\") == requestIdent.id);\n             }\n\n             let allocationSources;\n             if (request) {\n                 allocationSources = stores.AllocationSourceStore.fetchWhere({\n                     \"username\": request.get(\"created_by\").username\n    -            });\n    +            })\n             }\n\n             return {\n                 allocationSources,\n    -            identity,\n    -            quota,\n    +            identities,\n                 statuses\n             };\n         },\n\n         render() {\n    -        let { allocationSources, quota } = this.fetch();\n    +        let { allocationSources, identities } = this.fetch();\n             let { selectedRequest } = this.props;\n             let { actionPending } = this.state;\n\n    -        // Wait for the data necessary for the view\n    -        if (actionPending || !(allocationSources && quota)) {\n    +        if (actionPending) {\n                 return (\n                     <div className=\"loading\"></div>\n                 );\n             }\n\nidentities and allocationSources are passed to the view as collections now, no\nlonger json\n             let viewProps = {\n    -            request: selectedRequest.toJSON(),\n    -            allocationSources: allocationSources.map(as => as.toJSON()),\n    -            quota: quota.toJSON(),\n    +            request: selectedRequest,\n    +            allocationSources,\n    +            identities,\n    +            onAllocationSave: this.onAllocationSave,\n    +            onIdentitySave: this.onIdentitySave,\n                 onApprove: this.onApprove,\n    -            onDeny: this.onDeny\n    +            onDeny: this.onDeny,\n             };\n\n             return (\n    diff --git a/troposphere/static/js/components/admin/ResourceRequest/ResourceRequestView.jsx b/troposphere/static/js/components/admin/ResourceRequest/ResourceRequestView.jsx\n    index 47465e146..e256492b2 100644\n    --- a/troposphere/static/js/components/admin/ResourceRequest/ResourceRequestView.jsx\n    +++ b/troposphere/static/js/components/admin/ResourceRequest/ResourceRequestView.jsx\n    @@ -1,7 +1,7 @@\n     import React from \"react\";\n\n    -import AllocationSourcesView from \"./AllocationSourcesView\";\n    -import QuotaView from \"./QuotaView\";\n    +import AllocationSourceView from \"./AllocationSourceView\";\n    +import IdentityView from \"./IdentityView\";\n\n     // This is the view for the admin Resource Requests panel. This view shouldn't\n     // fetch or retrieve any data, just renders props.\n    @@ -9,84 +9,32 @@ export default React.createClass({\n\n         propTypes: {\n             request: React.PropTypes.object.isRequired,\n    -        allocationSources: React.PropTypes.array.isRequired,\n    -        quota: React.PropTypes.object.isRequired,\n    +        allocationSources: React.PropTypes.object,\n    +        identities: React.PropTypes.object,\n             onApprove: React.PropTypes.func.isRequired,\n    -        onDeny: React.PropTypes.func.isRequired\n    +        onDeny: React.PropTypes.func.isRequired,\n    +        onAllocationSave: React.PropTypes.func.isRequired,\n    +        onIdentitySave: React.PropTypes.func.isRequired\n         },\n\n         getInitialState() {\n    -        return this.getStateFromProps(this.props);\n    -    },\n    -\n    -    componentWillReceiveProps(newProps) {\n    -        // If a new request is present\n    -        if (newProps.request.id != this.props.request.id) {\n    -            this.setState(this.getStateFromProps(newProps));\n    -        }\n    -    },\n    -\n    -    getStateFromProps(props) {\n    -        let { quota, allocationSources } = props;\n             return {\n    -            response: \"\",\n    -            quota,\n    -            allocationSources\n    +            denyReason: \"\",\n             };\n         },\n\n    -    canSubmit() {\n    -        let response = this.state.response;\n    -        return response && response.trim().length > 0;\n    -    },\n    -\n    -    handleResponseChange(e) {\n    -        this.setState({\n    -            response: e.target.value\n    -        });\n    -    },\n    -\n    -    onAllocationChange({ id, compute_allowed }) {\n    -        let allocationSources = this.state.allocationSources.map(as => {\n    -            if (as.id === id) {\n    -                return Object.assign({}, as, { id, compute_allowed });\n    -            }\n    -            return as;\n    -        });\n    -        this.setState({ allocationSources });\n    -    },\n    -\n    -    onQuotaChange(quota) {\n    -        this.setState({ quota });\n    -    },\n    -\n    -    onDeny() {\n    -        let { response, quota, allocationSources } = this.state;\n    -        this.props.onDeny({\n    -            response, quota, allocationSources\n    -        });\n    -    },\n    -\n    -    onApprove() {\n    -        let { response, quota, allocationSources } = this.state;\n    -\n    -        this.props.onApprove({\n    -            response, quota, allocationSources\n    -        });\n    +    handleResponseChange({ target: { value: denyReason } }) {\n    +        this.setState({ denyReason });\n         },\n\n         style() {\n             return {\n                 horizontalRule: {\n    -                margin: \"10px 0px\"\n    -            },\n    -            responseArea: {\n    -                resize: \"vertical\",\n    -                minHeight: \"100px\"\n    +                margin: \"30px 0px\"\n                 },\n                 section: {\n    -                paddingBottom: \"10px\",\n    -                paddingLeft: \"10px\",\n    +                marginBottom: \"20px\",\n    +                marginLeft: \"10px\",\n                     flexGrow: 1\n                 },\n                 container: {\n    @@ -96,68 +44,113 @@ export default React.createClass({\n             }\n         },\n\n    +    renderAllocationsSection() {\n    +        let { allocationSources } = this.props;\n    +        let { section } = this.style();\n    +\n    +        let body = <p>There are no allocations</p>;\n    +        if (allocationSources) {\n    +            body = allocationSources.map(allocationSource => {\n    +                let id = allocationSource.get(\"id\");\n    +                let props = {\n    +                    onSave: this.props.onAllocationSave,\n    +                    allocationSource,\n    +                }\n    +                return <AllocationSourceView key={id} { ...props }/>;\n    +            })\n    +        }\n    +\n    +        return (\n    +        <div style={section}>\n    +            { body }\n    +        </div>\n    +        );\n    +    },\n    +\n    +    renderIdentitiesSection() {\n    +        let { identities, onIdentitySave: onSave } = this.props;\n    +        let { section } = this.style();\n    +\n    +        let body = <p>There are no identities</p>;\n    +        if (identities) {\n    +            body = identities.map(identity => {\n    +                let id = identity.get(\"id\");\n    +                let props = {\n    +                    identity,\n    +                    onSave,\n    +                }\n    +                return (\n    +                <div key={id} style={{ marginBottom: \"10px\" }}>\n    +                    <IdentityView { ...props}/>\n    +                </div>\n    +                )\n    +            })\n    +        }\n    +\n    +        return (\n    +        <div style={section}>\n    +            { body }\n    +            <div style={{ clear: \"both\" }} />\n    +        </div>\n    +        );\n    +    },\n    +\n         render() {\n             let {\n                 created_by: { username },\n                 request, description\n             } = this.props.request.toJSON();\n             let {\n    -            container, section, horizontalRule, responseArea\n    +            container, section, horizontalRule\n             } = this.style();\n    -        let {\n    -            response, quota, allocationSources\n    -        } = this.state;\n    -        let { onQuotaChange, onAllocationChange } = this;\n    +        let { denyReason } = this.state;\n\n             return (\n             <div style={container}>\n    -            <div>\n    -                <h4 className=\"t-title\">User:</h4>\n    -                <div style={section}>\n    -                    <p>{username}</p>\n    -                </div>\n    -                <h4 className=\"t-title\">Request:</h4>\n    -                <div style={section}>\n    -                    <p>{request}</p>\n    -                </div>\n    -                <h4 className=\"t-title\">Description:</h4>\n    -                <div style={section}>\n    -                    <p>{description}</p>\n    -                </div>\n    -                <h4 className=\"t-title\">Allocation:</h4>\n    -                <div style={section}>\n    -                <AllocationSourcesView { ...{ allocationSources, onAllocationChange }}/>\n    -                </div>\n    -                <h4 className=\"t-title\">Quota:</h4>\n    -                <div style={section}>\n    -                    <QuotaView { ...{ quota, onQuotaChange } }/>\n    -                </div>\n    +            <h4 className=\"t-title\">User</h4>\n    +            <div style={section}>\n    +                <p>{username}</p>\n    +            </div>\n    +            <h4 className=\"t-title\">Request</h4>\n    +            <div style={section}>\n    +                <p>{request}</p>\n    +            </div>\n    +            <h4 className=\"t-title\">Description</h4>\n    +            <div style={section}>\n    +                <p>{description}</p>\n                 </div>\n    -            <div style={{ clear: \"both\" }} />\n                 <hr style={horizontalRule} />\n    -            <div className=\"form-group\">\n    -                <h4 className=\"t-title\">Response:</h4>\n    -                <textarea style={responseArea}\n    -                    className=\"form-control\"\n    -                    type=\"text\"\n    -                    form=\"admin\"\n    -                    value={response}\n    -                    onChange={this.handleResponseChange}>\n    -                </textarea>\n    +            <h4 className=\"t-title\">1. Update the user's current resources</h4>\n    +            <div style={section}>\n    +                <h5 className=\"t-title\">Allocation</h5>\n    +                { this.renderAllocationsSection() }\n    +                <h5 className=\"t-title\">Quota</h5>\n    +                { this.renderIdentitiesSection() }\n                 </div>\n    -            <div className=\"buttons\">\n    -                <button disabled={!this.canSubmit()}\n    -                    onClick={this.onApprove}\n    -                    type=\"button\"\n    -                    className=\"btn btn-default btn-sm\">\n    -                    Approve\n    -                </button>\n    -                <button disabled={!this.canSubmit()}\n    -                    onClick={this.onDeny}\n    -                    type=\"button\"\n    -                    className=\"btn btn-default btn-sm\">\n    -                    Deny\n    -                </button>\n    +            <h4 className=\"t-title\">2. Approve or Deny the request</h4>\n    +            <div style={section}>\n    +                <p>On approve or deny, the user will be notified by email and\n    +                encouraged to reach out to support if they have questions.\n    +                Under <a href=\"//application/my-requests/resources\">my requests</a>, users can track the status of each\n    +                request.</p>\n    +                <div style={{ display: \"flex\" }}>\n    +                    <div style={{ padding: \"20px\", borderRight: \"solid 1px #eee\" }} >\n    +                        <button onClick={this.props.onApprove}\n    +                            type=\"button\"\n    +                            className=\"btn btn-default btn-sm\">\n    +                            Approve\n    +                        </button>\n    +                    </div>\n    +                    <div style={{ padding: \"20px\" }} >\n    +                        <span style={{ paddingRight: \"10px\" }}>Provide a reason: <input onChange={this.handleResponseChange} value={ denyReason } /></span>\n    +                        <button onClick={() => this.props.onDeny(this.state.denyReason)}\n    +                            disabled={ this.state.denyReason == \"\" }\n    +                            type=\"button\"\n    +                            className=\"btn btn-default btn-sm\">\n    +                            Deny\n    +                        </button>\n    +                    </div>\n    +                </div>\n                 </div>\n             </div>\n             );\n    diff --git a/troposphere/static/js/components/requests/ResourceHistoryMaster.jsx b/troposphere/static/js/components/requests/ResourceHistoryMaster.jsx\n    index 7d307c06f..9bf28fa28 100644\n    --- a/troposphere/static/js/components/requests/ResourceHistoryMaster.jsx\n    +++ b/troposphere/static/js/components/requests/ResourceHistoryMaster.jsx\n    @@ -1,7 +1,7 @@\n     import React from \"react\";\n     import RaisedButton from \"material-ui/RaisedButton\";\n\n    -import ResourceActions from \"actions/ResourceActions\";\n    +import ResourceRequestActions from \"actions/ResourceRequestActions\";\n     import subscribe from \"utilities/subscribe\";\n\n    @@ -36,10 +36,10 @@ export default subscribe(React.createClass({\n\n         onClose(request, statuses) {\n             let closedStatus = statuses.findWhere({ \"name\": \"closed\" });\n    -        ResourceActions.close({\n    -            request: request,\n    -            status: closedStatus\n    -        });\n    +        ResourceRequestActions.updateRequest(\n    +            request,\n    +            closedStatus\n    +        );\n         },\n\n         render() {\n    diff --git a/troposphere/static/js/constants/AllocationSourceConstants.js b/troposphere/static/js/constants/AllocationSourceConstants.js\n    new file mode 100644\n    index 000000000..2bc4b010f\n    --- /dev/null\n    +++ b/troposphere/static/js/constants/AllocationSourceConstants.js\n    @@ -0,0 +1,3 @@\n    +export default {\n    +    UPDATE: \"UPDATE_ALLOCATION_SOURCE\"\n    +};\n    diff --git a/troposphere/static/js/constants/IdentityConstants.js b/troposphere/static/js/constants/IdentityConstants.js\n    new file mode 100644\n    index 000000000..c3799e463\n    --- /dev/null\n    +++ b/troposphere/static/js/constants/IdentityConstants.js\n    @@ -0,0 +1,3 @@\n    +export default {\n    +    UPDATE: \"UPDATE_IDENTITY\",\n    +};\n    diff --git a/troposphere/static/js/models/Identity.js b/troposphere/static/js/models/Identity.js\n    index 4c5188e93..428c143c9 100644\n    --- a/troposphere/static/js/models/Identity.js\n    +++ b/troposphere/static/js/models/Identity.js\n    @@ -1,5 +1,6 @@\n     import Backbone from \"backbone\";\n     import featureFlags from \"utilities/featureFlags\";\n    +import globals from \"globals\";\n\n     function isRelevant(model, identityId) {\n         // using double ~ to convert string to number\n    @@ -9,6 +10,7 @@ function isRelevant(model, identityId) {\n     }\n\n     export default Backbone.Model.extend({\n    +    urlRoot: globals.API_V2_ROOT + \"/identities\",\n         parse: function(attributes) {\n\n             // put default allocation data here since it isn't\n\nIn both IdentityStore and here in AllocationSourceStore an update*ByUsername\nmethod was added to allow updates to models stored in queryModels[queryString]\n\n    diff --git a/troposphere/static/js/stores/AllocationSourceStore.js b/troposphere/static/js/stores/AllocationSourceStore.js\n    index 57cde0500..8edfb949c 100644\n    --- a/troposphere/static/js/stores/AllocationSourceStore.js\n    +++ b/troposphere/static/js/stores/AllocationSourceStore.js\n    @@ -1,11 +1,41 @@\n     import BaseStore from \"stores/BaseStore\";\n     import AllocationSourceCollection from \"collections/AllocationSourceCollection\";\n    +import AllocationSourceConstants from \"constants/AllocationSourceConstants\";\n    +import Dispatcher from \"dispatchers/Dispatcher\";\n\n     let AllocationSourceStore = BaseStore.extend({\n    -    collection: AllocationSourceCollection\n    +    collection: AllocationSourceCollection,\n    +    updateAllocationByUsername({ allocation, username }) {\n    +        let queryString = this.buildQueryStringFromQueryParams({ username });\n    +        let models = this.queryModels[queryString];\n    +        if (!models) {\n    +            return;\n    +        }\n    +        models.set(allocation);\n    +    }\n     });\n\n     let store = new AllocationSourceStore();\n\n    +Dispatcher.register(function(dispatch) {\n    +    var actionType = dispatch.action.actionType;\n    +    var payload = dispatch.action.payload;\n    +    var options = dispatch.action.options || options;\n    +\n    +    switch (actionType) {\n    +        case AllocationSourceConstants.UPDATE:\n    +            store.updateAllocationByUsername(payload);\n    +            break;\n    +        default:\n    +            return true;\n    +    }\n    +\n    +    if (!options.silent) {\n    +        store.emitChange();\n    +    }\n    +\n    +    return true;\n    +});\n    +\n     export default store;\n\n    diff --git a/troposphere/static/js/stores/IdentityStore.js b/troposphere/static/js/stores/IdentityStore.js\n    index 1aaa686e4..668d31007 100644\n    --- a/troposphere/static/js/stores/IdentityStore.js\n    +++ b/troposphere/static/js/stores/IdentityStore.js\n    @@ -2,6 +2,7 @@ import BaseStore from \"stores/BaseStore\";\n     import Dispatcher from \"dispatchers/Dispatcher\";\n     import IdentityCollection from \"collections/IdentityCollection\";\n     import AccountConstants from \"constants/AccountConstants\";\n    +import IdentityConstants from \"constants/IdentityConstants\"\n\n     let IdentityStore = BaseStore.extend({\n         collection: IdentityCollection,\n    @@ -63,7 +64,17 @@ let IdentityStore = BaseStore.extend({\n                 });\n                 return versionIdentities;\n             }\n    -    }\n    +    },\n    +\n    +    updateIdentityByUsername({ identity, username }) {\n    +        let queryString = this.buildQueryStringFromQueryParams({ username });\n    +        let models = this.queryModels[queryString];\n    +        if (!models) {\n    +            return;\n    +        }\n    +\n    +        models.set(identity);\n    +    },\n\n     });\n\n    @@ -71,12 +82,14 @@ let store = new IdentityStore();\n\n     Dispatcher.register(function(dispatch) {\n         var actionType = dispatch.action.actionType;\n    -    // Payload not used in current implementation\n    -    // var payload = dispatch.action.payload;\n    +    var payload = dispatch.action.payload;\n         var options = dispatch.action.options || options;\n\n         switch (actionType) {\n\n    +        case IdentityConstants.UPDATE:\n    +            store.updateIdentityByUsername(payload);\n    +            break;\n             case AccountConstants.UPDATE_ACCOUNT:\n                 store.clearCache();\n                 break;","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/5ba2b0d30a23ab4f9ab8a18be47883b21361d493"},{"sha":"ae12f5eec318c76d47c04276b330d90eb48e10eb","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Don't include allocation section when EXTERNAL_ALLOCATION","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/ae12f5eec318c76d47c04276b330d90eb48e10eb"},{"sha":"af254d65286b269c1996e88d31db1d1515113328","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Replace broken isMounted workaround with cancellables approach\n\nI thought that you could track is mounted state in the component and call\nsetState({ mounted: false }) on componentWillUnmount. That doesn't work,\nbecause setState calls are no-ops in that method. So callback handlers\nreferring to this.state.mounted (i.e. to determine if the component is still\naround) will be wrongfully told that it is still mounted.\n\nThe cancellables approach means that you keep track of a list of functions\nwhich you want to cancel on unMount (hence cancellable).","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/af254d65286b269c1996e88d31db1d1515113328"},{"sha":"2c3c371d8e4f4bfdcc4b067f008325d441ce5db7","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Do not update models when props change\n\nProblem: The views for allocationSource and identities were being reset\nwhenever any text was typed in the deny-this-request field.\n\nEach of those views allows the user to manipulate a resource. They have to\nkeep track when the cloud resource changes and when the local resource they're\nresponsible for changes (i.e. from a user event like typing).\n\nThe existing logic said 'reset the local model to the cloud model when a new\ncloud model prop comes along'. But props come along even when they don't\nchange, any re-render of the parent re-renders all children with all props,\nnot just props that have changed.\n\nThis patch works because the only way (now) that local models are changed is\nin the success callbacks of the network calls that update the cloud resources.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/2c3c371d8e4f4bfdcc4b067f008325d441ce5db7"},{"sha":"acbf0806b01439e7ac804e1251badb63fa0e00dc","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Remove the admin_url, which is no longer included\n\nThis is part of a refactor in the api. The admin_url was submitted to the api,\nwhen a user submitted a resource request so that the RT email would include a\nurl for staff to click on to manage the resource request. However RT is no\nlonger going to be used for resource requests so the admin_url is no longer\nrelevant.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/acbf0806b01439e7ac804e1251badb63fa0e00dc"},{"sha":"1b19e88df3959292c48b237db39da73024e7ad83","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Remove identity from resource request\n\nResource requests are no longer associated with an identity. In j7m, resource\nrequests were assigned an arbitrary identity, because j7m didn't want the\nmodal to ask users to select an identity. So, rather than having an arbitrary\nor even optional identity associated its being removed for simplicity.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/1b19e88df3959292c48b237db39da73024e7ad83"},{"sha":"9b566f411e6f9b8dce379fc12ab794b2013d7afb","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Move error handler out of actions\n\nA view may want to perform an action without showing an error message. The\nview should be in control of visual effects, actions should be responsible for\nnetworking logic.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/9b566f411e6f9b8dce379fc12ab794b2013d7afb"},{"sha":"8447fe0aa74963ad9689d56bd082410e6c3c0a34","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Cleanup how promises are passed around\n\nHere are two very different things:\n\n    1)\n        return model.save().then(() => someSideEffect())\n\n    2)\n        let promise = model.save();\n        proimse.then(() => someSideEffect())\n        return promise;\n\nIn the first case, the promise returned loses the response from the server,\nfuther then chains won't be able to see the response. In the second case, the\npromise can be chained and the response will be the first argument.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/8447fe0aa74963ad9689d56bd082410e6c3c0a34"},{"sha":"613a69e941446c7e2da6d4ff5be37aefaa7369a1","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Change structure of data for patching status of a resource request\n\nThis change requires a change in the api. Since the structure of the data is\nnested, it feels natural to submit a patch with the same structure. May be\nsort of contrived. Makes it easy to patch through the drf api browser.","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/613a69e941446c7e2da6d4ff5be37aefaa7369a1"},{"sha":"8881c3a29e60f8c0c4073d18c1c0bcdbfdcb5014","author":{"name":"Connor Osborn","email":"5c73bab35c1324ce6ab195bae6aef3799bfe6370@email.arizona.edu"},"message":"Rename variable","distinct":true,"url":"https://api.github.com/repos/calvinmclean/troposphere/commits/8881c3a29e60f8c0c4073d18c1c0bcdbfdcb5014"}]}
{"actor":{"display_login":"calvinmclean"}}