From 68f92a71f78750f2393c3bff2e2cdedb9c06da45 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mar 21 2017 09:44:19 +0000 Subject: [PATCH 1/6] Add Halp widget --- diff --git a/hubs/default_config.py b/hubs/default_config.py index 5837486..72cda31 100644 --- a/hubs/default_config.py +++ b/hubs/default_config.py @@ -44,6 +44,7 @@ CHAT_NETWORKS = [ } ] +DATAGREPPER_URI = 'https://apps.fedoraproject.org/datagrepper' WIDGETS = [ 'hubs.widgets.about:About', @@ -58,6 +59,7 @@ WIDGETS = [ 'hubs.widgets.fhosted:FedoraHosted', 'hubs.widgets.github_pr:GitHubPRs', 'hubs.widgets.githubissues:GitHubIssues', + 'hubs.widgets.halp:Halp', 'hubs.widgets.meetings:Meetings', 'hubs.widgets.memberships:Memberships', 'hubs.widgets.pagure_pr:PagurePRs', diff --git a/hubs/defaults.py b/hubs/defaults.py index 2d27239..7bda776 100644 --- a/hubs/defaults.py +++ b/hubs/defaults.py @@ -82,6 +82,12 @@ def add_user_widgets(session, hub, username, fullname): 'username': username, })) hub.widgets.append(widget) + hub.widgets.append( + hubs.models.Widget( + plugin='halp', index=9, + _config=json.dumps({ + })) + ) return hub diff --git a/hubs/static/client/app/components/CompletionInput.jsx b/hubs/static/client/app/components/CompletionInput.jsx new file mode 100644 index 0000000..9b8b581 --- /dev/null +++ b/hubs/static/client/app/components/CompletionInput.jsx @@ -0,0 +1,150 @@ +import React from 'react'; +import Autosuggest from 'react-autosuggest'; + + +export default class CompletionInput extends React.Component { + + constructor(props) { + super(props); + this.state = { + value: "", + suggestions: [], + loading: false + } + this.resultsDomNodeStyle = { + position: "absolute", + width: "auto" + } + this.queryTimeout = null; + this.onResultClick = this.onResultClick.bind(this); + this.onChange = this.onChange.bind(this); + this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this); + this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this); + this.onSuggestionSelected = this.onSuggestionSelected.bind(this); + this.loadFromServer = this.loadFromServer.bind(this); + } + + componentWillUnmount() { + if (this.serverRequest) { this.serverRequest.abort(); } + } + + onResultClick(result) { + this.setState({ + results: [], + value: result + }); + } + + onChange(e, {newValue}) { + this.setState({ + value: newValue + }); + } + + onSuggestionSelected(e, {suggestionValue}) { + // Transmit a "fake" changed event to the parent, if asked to. + if (!this.props.onChange) { return; } + const mockedEvent = { + type: 'change', + target: { + type: 'input', + name: this.props.name, + value: suggestionValue, + }, + } + this.props.onChange(mockedEvent); + } + + // Autosuggest will call this function every time you need to update + // suggestions. + onSuggestionsFetchRequested({value}) { + var inputValue = value.trim().toLowerCase(); + if (this.queryTimeout !== null) { + window.clearTimeout(this.queryTimeout); + } + if (this.serverRequest) { this.serverRequest.abort(); } + if (inputValue.length === 0) { + this.setState({ + suggestions: [], + loading: false + }); + } else { + this.queryTimeout = window.setTimeout( + this.loadFromServer, this.props.queryDelay, inputValue); + } + } + + loadFromServer(value) { + this.setState({loading: true}); + this.serverRequest = $.ajax({ + url: this.props.url, + method: 'GET', + data: {q: value}, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({ + suggestions: data.results, + loading: false + }); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + this.setState({loading: false}); + }.bind(this) + }); + } + + // Autosuggest will call this function every time you need to clear + // suggestions. + onSuggestionsClearRequested() { + this.setState({ + suggestions: [] + }); + } + + render() { + // When a suggestion is clicked, this method will return the value to + // populate the input with. + const getSuggestionValue = suggestion => suggestion; + // Render suggestions. + const renderSuggestion = suggestion => suggestion; + // Autosuggest will pass through all these props to the input element. + const inputProps = { + //placeholder: 'Type a programming language', + name: this.props.name, + className: "form-control", + value: this.state.value, + placeholder: this.props.placeholder, + onChange: this.onChange, + }; + + // Render the widget. + return ( +
+ + { this.state.loading ? + + + + : null } +
+ ); + } + +} +CompletionInput.defaultProps = { + placeholder: "", + queryDelay: 800, +}; + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/components/Modal.jsx b/hubs/static/client/app/components/Modal.jsx new file mode 100644 index 0000000..1ba31b1 --- /dev/null +++ b/hubs/static/client/app/components/Modal.jsx @@ -0,0 +1,109 @@ +import React from 'react'; + + +export default class Modal extends React.Component { + + constructor(props) { + super(props); + this.onShow = this.onShow.bind(this); + this.onShown = this.onShown.bind(this); + this.onHide = this.onHide.bind(this); + this.onHidden = this.onHidden.bind(this); + this.hide = this.hide.bind(this); + this.show = this.show.bind(this); + this.getModalBody = this.getModalBody.bind(this); + this.getModalFooter = this.getModalFooter.bind(this); + } + + componentDidMount() { + if (this.domNode) { + $(this.domNode).on("show.bs.modal", this.onShow); + $(this.domNode).on("shown.bs.modal", this.onShown); + $(this.domNode).on("hide.bs.modal", this.onHide); + $(this.domNode).on("hidden.bs.modal", this.onHidden); + } + } + + componentWillUnmount() { + if (this.domNode) { + $(this.domNode).off("show.bs.modal", this.onShow); + $(this.domNode).off("shown.bs.modal", this.onShown); + $(this.domNode).off("hide.bs.modal", this.onHide); + $(this.domNode).off("hidden.bs.modal", this.onHidden); + } + } + + onShow(e) { + if (this.props.onShow) { this.props.onShow(); } + } + onShown(e) { + if (this.props.onShown) { this.props.onShown(); } + } + onHide(e) { + if (this.props.onHide) { this.props.onHide(); } + } + onHidden(e) { + if (this.props.onHidden) { this.props.onHidden(); } + } + + hide(callback) { + if (!this.domNode) { return; } + var domNode = $(this.domNode); + if (callback) { domNode.one("hidden.bs.modal", callback); } + domNode.modal("hide"); + } + + show(callback) { + if (!this.domNode) { return; } + var domNode = $(this.domNode); + if (callback) { domNode.one("shown.bs.modal", callback); } + $(this.domNode).modal("show"); + } + + componentWillUpdate(nextProps, nextState) { + if (!nextProps.open) { this.hide(); } + } + + componentDidUpdate() { + if (this.props.open) { this.show(); } + } + + getModalBody() { + if (!this.props.data) { return ""; } + } + + getModalFooter() { + return null; + } + + render() { + var footer = this.getModalFooter(); + return ( +
this.domNode = ref}> +
+
+
+ +

{this.props.title}

+
+
+ {this.getModalBody()} +
+ {footer ? +
+ {footer} +
+ : null} +
+
+
+ ); + } +} +Modal.propTypes = { + open: React.PropTypes.bool, + data: React.PropTypes.object +}; + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/Halp.jsx b/hubs/static/client/app/widgets/halp/Halp.jsx new file mode 100644 index 0000000..f1db852 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/Halp.jsx @@ -0,0 +1,182 @@ +import React from 'react'; +import { + IntlProvider, + defineMessages, + FormattedMessage, + } from 'react-intl'; +import HubsList from './HubsList.jsx'; +import ModalRequest from './ModalRequest.jsx'; +import ModalAllRequests from './ModalAllRequests.jsx'; +import Request from './Request.jsx'; + + +const messages = defineMessages({ + no_request: { + id: "hubs.widgets.halp.no_request", + defaultMessage: "No request for now." + }, + view_more: { + id: "hubs.widgets.halp.view_more", + defaultMessage: "View more" + }, + zoom_title: { + id: "hubs.widgets.halp.zoom.title", + defaultMessage: "Help request" + }, + search_title: { + id: "hubs.widgets.halp.search.title", + defaultMessage: "All Help Requests" + }, +}); + + +export default class HalpWidget extends React.Component { + + constructor(props) { + super(props); + this.state = { + requests: [], + inactiveHubs: [], + zoomedRequest: null, + allRequestsOpen: false, + loading: false + }; + this.zoomOnRequest = this.zoomOnRequest.bind(this); + this.onZoomClose = this.onZoomClose.bind(this); + this.openAllRequests = this.openAllRequests.bind(this); + this.onAllRequestsClose = this.onAllRequestsClose.bind(this); + this.onHubClick = this.onHubClick.bind(this); + } + + loadFromServer() { + this.setState({loading: true}); + var data = { hubs: [] }; + this.props.hubs.forEach(function(hub) { + if (this.state.inactiveHubs.indexOf(hub) === -1) { + data.hubs.push(hub); + } + }.bind(this)); + this.serverRequest = $.ajax({ + url: this.props.urls.data, + method: 'GET', + data: $.param(data, true), + dataType: 'json', + cache: false, + success: function(data) { + this.setState({ + requests: data.requests, + loading: false + }); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.urls.data, status, err.toString()); + this.setState({loading: false}); + }.bind(this) + }); + } + + componentDidMount() { + this.loadFromServer(); + } + + componentWillUnmount() { + this.serverRequest.abort(); + } + + zoomOnRequest(request) { + this.setState({zoomedRequest: request}); + } + onZoomClose() { + this.setState({zoomedRequest: null}); + } + + openAllRequests(e) { + e.preventDefault(); + this.setState({allRequestsOpen: true}); + } + onAllRequestsClose() { + this.setState({allRequestsOpen: false}); + } + + onHubClick(hubName) { + var newInactiveHubs = this.state.inactiveHubs.slice(), + inactiveIndex = this.state.inactiveHubs.indexOf(hubName); + if (inactiveIndex === -1) { + newInactiveHubs.push(hubName); + } else { + newInactiveHubs.splice(inactiveIndex, 1); + } + this.setState( + { inactiveHubs: newInactiveHubs }, + this.loadFromServer); + } + + render() { + var requestNodes; + if (this.state.requests.length === 0) { + requestNodes = ( +

+ +

+ ); + } else { + var previousDate = null; + requestNodes = this.state.requests.map(function(request, idx) { + var showDate = (request.date !== previousDate); + previousDate = request.date; + return ( + + ); + }.bind(this)); + } + var hubs = this.props.hubs.map(function(hub) { + return { + name: hub, + active: (this.state.inactiveHubs.indexOf(hub) === -1) + } + }.bind(this)); + return ( + +
+ + { this.state.loading ? + + : +
+ {requestNodes} +
+ } +
+ +
+ } + onHidden={this.onZoomClose} /> + } + urls={this.props.urls} + hubs={hubs} + /> +
+
+ ); + } +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/Hub.jsx b/hubs/static/client/app/widgets/halp/Hub.jsx new file mode 100644 index 0000000..044e6d9 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/Hub.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + + +export default class Hub extends React.Component { + + onClick(e) { + e.preventDefault(); + this.props.onClick(this.props.name); + } + + render() { + var activeClass = this.props.active ? "active" : "inactive"; + return ( + {"@" + this.props.name} + ); + } +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/HubsList.jsx b/hubs/static/client/app/widgets/halp/HubsList.jsx new file mode 100644 index 0000000..3915dd4 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/HubsList.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { defineMessages, FormattedMessage } from 'react-intl'; +import Hub from './Hub.jsx'; + + +const messages = defineMessages({ + watching_all_hubs: { + id: "hubs.widgets.halp.watching_all_hubs", + defaultMessage: "Watching all hubs" + }, +}); + + +export default class HubsList extends React.Component { + render() { + var hubNodes = this.props.hubs.map(function(hub, idx) { + return ( + + ); + }.bind(this)); + return ( +
+ + { (hubNodes.length !== 0) ? + hubNodes + : + + } +
+ ); + } +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/ModalAllRequests.jsx b/hubs/static/client/app/widgets/halp/ModalAllRequests.jsx new file mode 100644 index 0000000..c3d13cd --- /dev/null +++ b/hubs/static/client/app/widgets/halp/ModalAllRequests.jsx @@ -0,0 +1,234 @@ +import { defineMessages, FormattedMessage } from 'react-intl'; +import CompletionInput from '../../components/CompletionInput.jsx'; +import Modal from '../../components/Modal.jsx'; +import Request from './Request.jsx'; + + +const messages = defineMessages({ + no_request_found: { + id: "hubs.widgets.halp.no_request_found", + defaultMessage: "No request found." + }, + search_meeting_name: { + id: "hubs.widgets.halp.search.meeting_name", + defaultMessage: "Meeting name" + }, + search_between_dates: { + id: "hubs.widgets.halp.search.between_dates", + defaultMessage: "Between dates" + }, + search_people: { + id: "hubs.widgets.halp.search.people", + defaultMessage: "People" + }, + search_button: { + id: "hubs.widgets.halp.search.button", + defaultMessage: "Search" + }, + pager_newer: { + id: "hubs.widgets.halp.pager.newer", + defaultMessage: "Newer" + }, + pager_older: { + id: "hubs.widgets.halp.pager.older", + defaultMessage: "Older" + }, + pager_status: { + id: "hubs.widgets.halp.pager.status", + defaultMessage: "Page {page} / {total}" + }, +}); + + +export default class ModalAllRequests extends Modal { + + constructor(props) { + super(props); + this.state = { + requests: [], + page: {nr: 1, has_prev: false, has_next: false}, + formData: { + hubs: this.props.hubs.filter( + function(hub) { return hub.active; } + ).map( + function(hub) { return hub.name; } + ), + }, + loading: false, + }; + this.onSubmit = this.onSubmit.bind(this); + this.loadFromServer = this.loadFromServer.bind(this); + this.onZoomRequest = this.onZoomRequest.bind(this); + this.changePage = this.changePage.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + componentWillUnmount() { + super.componentWillUnmount(); + this.serverRequest.abort(); + } + + onShown(e) { + super.onShown(e); + if (this.state.requests.length === 0) { this.loadFromServer(); } + } + + onHidden(e) { + super.onHidden(e); + if (this.serverRequest) { this.serverRequest.abort(); } + } + + onZoomRequest(request) { + // Close this modal and open the request modal + this.hide(function() { + this.props.onZoomRequest(request); + }.bind(this)); + } + + onSubmit(e) { + e.preventDefault(); + this.loadFromServer(); + } + + changePage(inc) { + var requestedPage = this.state.page.nr + inc; + this.loadFromServer(requestedPage); + } + + loadFromServer(requestedPage) { + const formData = this.state.formData; + formData.page = requestedPage || 1; + this.setState({loading: true}); + this.serverRequest = $.ajax({ + url: this.props.urls.search, + method: 'GET', + data: formData, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({ + requests: data.requests, + page: data.page, + loading: false + }); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.urls.search, status, err.toString()); + this.setState({loading: false}); + }.bind(this) + }); + } + + handleChange(e) { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; + const name = e.target.name; + var data = this.state.formData; + data[name] = value; + this.setState({formData: data}); + } + + getModalBody() { + var requestNodes; + if (this.state.requests.length === 0) { + requestNodes = (

+ +

); + } else { + requestNodes = this.state.requests.map(function(request, idx) { + return ( + + ); + }.bind(this)); + } + return ( +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+ + { this.state.loading && } +
+
+
+
+
+ {requestNodes} +
+
+ ); + } + + getModalFooter() { + if (this.state.requests.length === 0 + || (!this.state.page.has_prev && !this.state.page.has_next)) { + // No results or only one page: don't show the pager. + return null; + } + return ( + + ); + } +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/ModalRequest.jsx b/hubs/static/client/app/widgets/halp/ModalRequest.jsx new file mode 100644 index 0000000..b6d0860 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/ModalRequest.jsx @@ -0,0 +1,99 @@ +import { defineMessages, FormattedMessage } from 'react-intl'; +import Modal from '../../components/Modal.jsx'; +import getRequestView from './getRequestView.jsx'; + + +const messages = defineMessages({ + zoom_part2_title: { + id: "hubs.widgets.halp.zoom.part2_title", + defaultMessage: "Want to help with this request?" + }, + zoom_part2_subtitle: { + id: "hubs.widgets.halp.zoom.part2_subtitle", + defaultMessage: "Awesome! Here's some tips on getting started:" + }, + zoom_tip1: { + id: "hubs.widgets.halp.zoom.tip1", + defaultMessage: "Take a look at the link provided in the request. It should have more information about the problem and may have instructions on how to get starter helping out:" + }, + zoom_tip2: { + id: "hubs.widgets.halp.zoom.tip2", + defaultMessage: "Contact the person who requested help to get more information. By clicking on their name, you'll be taken to their profile where you can contact them:" + }, + zoom_tip3: { + id: "hubs.widgets.halp.zoom.tip3", + defaultMessage: "Read through the chat log where the request occured. There may be additional context in the log that can help you figure out how to get started:" + }, +}); + + +export default class ModalRequest extends Modal { + + getModalBody() { + if (!this.props.data) { return ""; } + return ( +
+ {getRequestView(this.props.data)} +
+ + +
    + { this.props.data.urls ? +
  1. + + +
  2. + : null } +
  3. + + +
  4. +
  5. + + +
  6. +
+
+ ); + } +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/Request.jsx b/hubs/static/client/app/widgets/halp/Request.jsx new file mode 100644 index 0000000..bc80c6c --- /dev/null +++ b/hubs/static/client/app/widgets/halp/Request.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {FormattedRelative} from 'react-intl'; +import getRequestView from './getRequestView.jsx'; + + +export default class Request extends React.Component { + + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + } + + onClick(e) { + e.preventDefault; + this.props.onZoomRequest(this.props.request); + } + + render() { + var requestView = getRequestView( + this.props.request, + this.props.onZoomRequest ? this.onClick : null); + return ( +
+ { this.props.showdate ? +
+ +
+ : "" + } + {requestView} +
+ ); + } +} +Request.defaultProps = { + onZoomRequest: null, +}; + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/getRequestView.jsx b/hubs/static/client/app/widgets/halp/getRequestView.jsx new file mode 100644 index 0000000..3e0296f --- /dev/null +++ b/hubs/static/client/app/widgets/halp/getRequestView.jsx @@ -0,0 +1,65 @@ +import { FormattedMessage, defineMessages } from 'react-intl'; +import Linkify from 'react-linkify'; + + +const messages = defineMessages({ + req_no_text: { + id: "hubs.widgets.halp.req_no_text", + defaultMessage: "(no message)" + }, +}); + + +export default function getRequestView(request, openCallback) { + var openButton = ""; + if (openCallback) { + openButton = ( +
+ +
+ ); + } + return ( +
+
+ @{request.hub} +
+
+ {request.channel} +
+
+ { request.text ? + "{request.text}" + : + + } +
+ { request.url ? + + : null } +
+
+ + + +
+ + {openButton} +
+
+ ); +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json index 746d6c6..f237da9 100644 --- a/hubs/static/client/package.json +++ b/hubs/static/client/package.json @@ -14,6 +14,9 @@ "react": "^15.4.0", "react-dom": "^15.4.0", "react-timeago": "^3.1.1", + "react-autosuggest": "~6.1.0", + "react-linkify": "^0.1.3", + "react-intl": "~2.1.5", "webpack": "~1.13.1" }, "devDependencies": { diff --git a/hubs/static/client/webpack.config.js b/hubs/static/client/webpack.config.js index ba91ad0..4bd3fd9 100644 --- a/hubs/static/client/webpack.config.js +++ b/hubs/static/client/webpack.config.js @@ -10,6 +10,7 @@ const config = { entry: { Hubs: path.join(PATHS.app, 'core', 'Hubs.js'), Feed: path.join(PATHS.app, 'widgets', 'feed', 'Feed.jsx'), + Halp: path.join(PATHS.app, 'widgets', 'halp', 'Halp.jsx') }, output: { path: PATHS.build, diff --git a/hubs/static/css/style.css b/hubs/static/css/style.css index ab51d60..7516bea 100644 --- a/hubs/static/css/style.css +++ b/hubs/static/css/style.css @@ -451,6 +451,137 @@ font-size: 32pt; } +/* + * Autosuggest + */ +.react-autosuggest__suggestions-container { + position: absolute; + z-index: 10; + width: 100%; + padding-right: 2em; +} +.react-autosuggest__suggestions-container ul { + list-style: none; + padding: 0.3em 0.8em; + background-color: #eee; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.form-horizontal .form-control-feedback { + right: 15px; +} +.form-inline .form-control-feedback { + top: 0; +} + + + +/* + * Widget halp + */ +.widget-halp { +} +.widget-halp .hubs { + color: gray; +} +.widget-halp .hubs .fa { + margin-right: 4px; +} +.widget-halp .hubs span.hub { + border: 1px solid gray; + border-radius: 2px; + background-color: #eee; + display: inline-block; + margin: 2px 3px 2px 0; + padding: 1px 2px; + font-size: 90%; + cursor: pointer; +} +.widget-halp .hubs span.hub.inactive { + background-color: white; +} +.widget-halp .requests { +} +.widget-halp .requests .no-request { + margin-top: 10px; +} +.widget-halp .requests .date { + text-transform: uppercase; + font-size: 110%; + margin-top: 10px; +} +.widget-halp .request { + background-color: #f5f5f5; + padding: 1em 0; + margin-top: 5px; +} +.widget-halp .request .hub span { + float: left; + font-weight: bold; + color: white; + background-color: #5bc0de; + border-radius: 10px; + padding: 2px 4px; +} +.widget-halp .request .channel { + color: gray; + text-align: right; +} +.widget-halp .request .text { + margin-top: 10px; +} +.widget-halp .request .url { + margin-top: 5px; + margin-bottom: 15px; +} +.widget-halp .request .author { +} +.widget-halp .request .open { + text-align: right; +} +.widget-halp .request .open button { + background-color: white; + border-color: gray; +} +.widget-halp .request .open button:hover { + background-color: #ccc; +} +.widget-halp .zoomed-request ol>li { + margin-top: 1em; +} +.widget-halp .view-more { + margin-top: 2em; + text-align: center; +} +.widget-halp .view-more a { + color: gray; +} + + + /** fedora bootstrap overrides **/ /** tightening up card headers - they are too fat imho **/ @@ -470,3 +601,7 @@ header h5.m-b-1 { .card .panel-footer { padding-bottom: 2rem; /* fixes request new meeting button padding on the upcoming meeting widget */ } + + + +/* vim: set ts=2 sw=2 et: */ diff --git a/hubs/static/img/spinner-circle.gif b/hubs/static/img/spinner-circle.gif new file mode 100644 index 0000000..d0bce15 Binary files /dev/null and b/hubs/static/img/spinner-circle.gif differ diff --git a/hubs/tests/test_fedora_hubs_flask_api.py b/hubs/tests/test_fedora_hubs_flask_api.py index c9e8c39..2c8f1ff 100644 --- a/hubs/tests/test_fedora_hubs_flask_api.py +++ b/hubs/tests/test_fedora_hubs_flask_api.py @@ -83,7 +83,7 @@ class HubsAPITest(hubs.tests.APPTest): "name": "ralph", "owners": ["ralph"], "subscribers": [], - "widgets": [34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 56] + "widgets": [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 61], } self.assertDictEqual(data, json.loads(result.get_data(as_text=True))) diff --git a/hubs/tests/test_models.py b/hubs/tests/test_models.py index 66c1488..8669160 100644 --- a/hubs/tests/test_models.py +++ b/hubs/tests/test_models.py @@ -32,7 +32,7 @@ class ModelTest(hubs.tests.APPTest): # check if widgets still are intact widgets = hubs.models.Widget.by_hub_id_all(hub.name) - self.assertEqual(12, len(widgets)) + self.assertEqual(13, len(widgets)) def test_delete_hubs(self): # verify the hub exists @@ -48,7 +48,7 @@ class ModelTest(hubs.tests.APPTest): # check if widgets exist widgets = hubs.models.Widget.by_hub_id_all(hub.name) - self.assertEqual(12, len(widgets)) + self.assertEqual(13, len(widgets)) # delete the hub self.session.delete(hub) diff --git a/hubs/widgets/halp/__init__.py b/hubs/widgets/halp/__init__.py new file mode 100644 index 0000000..92df5a4 --- /dev/null +++ b/hubs/widgets/halp/__init__.py @@ -0,0 +1,35 @@ +from __future__ import unicode_literals + + +import arrow + +from hubs.widgets.base import Widget +from hubs.widgets import validators + + +class Halp(Widget): + + name = "halp" + label = "Help Requests" + position = "both" + parameters = [ + dict( + name="hubs", + label="Hubs", + default=[], + validator=validators.CommaSeparatedList, + help="A comma-separated list of hubs to monitor.", + ), dict( + name="per_page", + label="Requests per page", + default=4, + validator=validators.Integer, + help="The number of requests per page to display.", + )] + views_module = ".views" + cached_functions_module = ".functions" + + def get_template_environment(self): + env = super(Halp, self).get_template_environment() + env.filters['humanize'] = lambda d: arrow.get(d).humanize() + return env diff --git a/hubs/widgets/halp/functions.py b/hubs/widgets/halp/functions.py new file mode 100644 index 0000000..b7f2b88 --- /dev/null +++ b/hubs/widgets/halp/functions.py @@ -0,0 +1,149 @@ +from __future__ import unicode_literals + + +import datetime +import flask +import fedmsg.meta +import re +import logging +import requests +import time + +from jinja2.utils import urlize + +from hubs.widgets.caching import CachedFunction + + +log = logging.getLogger('hubs.widgets') + + +class GetRequests(CachedFunction): + + TOPIC = "org.fedoraproject.prod.meetbot.meeting.item.help" + + def execute(self): + # return [_format_message(msg) for msg in EXAMPLE_DATA] + from hubs.app import app + url = "%s/raw" % app.config["DATAGREPPER_URI"] + args = { + "topic": self.TOPIC, + "delta": str(86400 * 10), # 10 days + # "rows_per_page": per_page, + } + log.info("Getting request data from Datagrepper") + response = requests.get(url, args) + if response.status_code != 200: + log.warning("Could not get requests from Datagrepper: %s", + response.text) + return [] + raw_messages = response.json()["raw_messages"] + return [_format_message(msg) for msg in raw_messages] + + def should_invalidate(self, message): + if message["topic"] != self.TOPIC: + return False + hubs = channel_to_hubs(message['msg']['channel']) + return bool(set(hubs) & set(self.instance.config["hubs"])) + + +def channel_to_hubs(channel): + # TODO: Link the channel to the hubs that use it. + return [channel.lstrip("#")] + + +def _format_message(raw_message): + msg = { + "date": raw_message["msg"]["details"]["time_"], + "channel": raw_message["msg"]["channel"], + "hubs": channel_to_hubs(raw_message["msg"]["channel"]), + "meeting_topic": raw_message["msg"]["meeting_topic"], + "text": raw_message["msg"]["details"]["line"], + "context_url": "%s.log.html#l-%s" % ( + raw_message["msg"]["url"], + raw_message["msg"]["details"]["linenum"]), + "author": { + "name": raw_message["msg"]["details"]["nick"], + "avatar": fedmsg.meta.msg2secondary_icon(raw_message["msg"]), + "url": flask.url_for( + "hub", name=raw_message["msg"]["details"]["nick"]), + } + } + # Extract an URL in the line. Use jinja2's urlize function because the + # regexp are better and well-tested. + urlized = urlize(raw_message["msg"]["details"]["line"]) + msg["urls"] = re.findall(']*>', urlized) + return msg + + +EXAMPLE_DATA = [ + { + "msg": { + "details": { + "time_": time.mktime(( + datetime.datetime.now() - datetime.timedelta(seconds=23) + ).timetuple()), + "line": ("The #fedora-apps team needs people to help review " + "code: https://pagure.io/fedora-hubs/issue/21."), + "linenum": 42, + "nick": "threebean", + }, + "channel": "#fedora-devel", + "meeting_topic": "Hubs meeting", + "topic": "Hubs meeting", + "url": "https://meetbot.fedoraproject.org/fedora-devel/", + }, + }, + { + "msg": { + "details": { + "time_": time.mktime(( + datetime.datetime.now() - datetime.timedelta(hours=2) + ).timetuple()), + "line": ("I need help creating signs for Flock: " + " and" + " ."), + "linenum": 84, + "nick": "mizmo", + }, + "channel": "#fedora-design", + "meeting_topic": "FLOCK meeting", + "topic": "FLOCK meeting", + "url": "https://meetbot.fedoraproject.org/fedora-design/", + } + }, + { + "msg": { + "details": { + "time_": time.mktime(( + datetime.datetime.now() - + datetime.timedelta(days=1, hours=2) + ).timetuple()), + "line": ("I need help with the log out bug: " + "https://pagure.io/fedora-hubs/issue/99"), + "linenum": 63, + "nick": "decause", + }, + "channel": "#fedora-hubs", + "meeting_topic": "Hubs meeting", + "topic": "Hubs meeting", + "url": "https://meetbot.fedoraproject.org/fedora-hubs", + } + }, + { + "msg": { + "details": { + "time_": time.mktime(( + datetime.datetime.now() - + datetime.timedelta(days=3, hours=20) + ).timetuple()), + "line": "Fedora-Hubs should be translated", + "linenum": 18, + "nick": "mizmo", + }, + "channel": "#fedora-i18n", + "meeting_topic": "I18n meeting", + "topic": "I18n meeting", + "url": "https://meetbot.fedoraproject.org/fedora-hubs", + } + }, +] diff --git a/hubs/widgets/halp/templates/halp.html b/hubs/widgets/halp/templates/halp.html new file mode 100644 index 0000000..908d916 --- /dev/null +++ b/hubs/widgets/halp/templates/halp.html @@ -0,0 +1,22 @@ +{% extends "panel.html" %} + +{% block content %} +
+
+ + +{% endblock %} diff --git a/hubs/widgets/halp/views.py b/hubs/widgets/halp/views.py new file mode 100644 index 0000000..1aaf1ad --- /dev/null +++ b/hubs/widgets/halp/views.py @@ -0,0 +1,160 @@ +from __future__ import unicode_literals, absolute_import + +import datetime +import flask +import time + +from hubs.widgets.base import WidgetView +from .functions import GetRequests + + +def paginate(values, per_page): + try: + total = values.count() + except TypeError: + total = len(values) + try: + page = flask.request.values.get("page", 1, int) + except ValueError: + page = 1 + last_page = int(total / per_page) + if total > last_page * per_page: + last_page += 1 + if last_page < 1: + last_page = 1 + if page < 1: + page = 1 + if page > last_page: + page = last_page + start = (page - 1) * per_page + end = start + per_page + page_data = { + "nr": page, + "has_prev": page > 1, + "has_next": page < last_page, + "total_entries": total, + "total_pages": last_page, + } + return values[start:end], page_data + + +class BaseView(WidgetView): + + name = "root" + url_rules = ["/"] + template_name = "halp.html" + + def get_context(self, instance, *args, **kwargs): + return dict( + hubs=instance.config["hubs"], + title=self.widget.label, + ) + + +class DataView(WidgetView): + + name = "data" + url_rules = ["data"] + + def dispatch_request(self, *args, **kwargs): + """ + The method that subclasses of :py:class:`flask.views.View` must + implement. It binds the other methods together. + """ + instance = self._get_instance(*args, **kwargs) + get_requests = GetRequests(instance) + data = {"requests": []} + hubs_filter = flask.request.args.getlist("hubs") + for req in get_requests(): + req["hub"] = req["hubs"][0] # Arbitrarily assign the first hub. + if "hubs" in flask.request.args: + # Find which hubs are targeted by this message. + hubs = set(req["hubs"]) & set(hubs_filter) + if not hubs: + continue + # Arbitrarily assign the first common hub. + req["hub"] = list(hubs)[0] + data["requests"].append(req) + if len(data["requests"]) == 3: + # Only 3 requests on the main widget. + break + return flask.jsonify(data) + + +class SearchView(WidgetView): + + name = "search" + url_rules = ["search"] + methods = ['GET', 'POST'] + + def dispatch_request(self, *args, **kwargs): + """ + The method that subclasses of :py:class:`flask.views.View` must + implement. It binds the other methods together. + """ + instance = self._get_instance(*args, **kwargs) + get_requests = GetRequests(instance) + # prepare filters + if "hubs" in flask.request.values: + # Find which hubs are targeted by this message. + filter_hubs = set(flask.request.values.getlist("hubs")) + else: + filter_hubs = None + filter_people = flask.request.values.get("people") + filter_meetingname = flask.request.values.get("meetingname") + if flask.request.values.get("startdate"): + filter_startdate = datetime.datetime.strptime( + flask.request.values.get("startdate"), "%Y-%m-%d") + filter_startdate = time.mktime(filter_startdate.timetuple()) + else: + filter_startdate = None + if flask.request.values.get("enddate"): + filter_enddate = datetime.datetime.strptime( + flask.request.values.get("enddate"), "%Y-%m-%d") + filter_enddate = time.mktime(filter_enddate.timetuple()) + else: + filter_enddate = None + + # filter the data + data = {"requests": []} + for req in get_requests(): + req["hub"] = req["hubs"][0] # Arbitrarily assign the first hub. + if filter_hubs is not None and not set(req["hubs"]) & filter_hubs: + continue + if filter_people and filter_people not in req["author"]["name"]: + continue + if (filter_meetingname and + filter_meetingname not in req["meeting_topic"]): + continue + if filter_startdate is not None and req["date"] < filter_startdate: + continue + if filter_enddate is not None and req["date"] > filter_enddate: + continue + data["requests"].append(req) + requests, page_data = paginate( + data["requests"], instance.config["per_page"]) + data["requests"] = requests + data["page"] = page_data + return flask.jsonify(data) + + +class RequestersView(WidgetView): + + name = "requesters" + url_rules = ["requesters"] + MAX_SUGGESTS = 5 + + def dispatch_request(self, *args, **kwargs): + """ + The method that subclasses of :py:class:`flask.views.View` must + implement. It binds the other methods together. + """ + instance = self._get_instance(*args, **kwargs) + get_requests = GetRequests(instance) + results = set([ + req["author"]["name"] for req in get_requests() + if req["author"]["name"].startswith( + flask.request.args.get("q")) + ]) + results = sorted(list(results))[:self.MAX_SUGGESTS] + return flask.jsonify({"results": results}) diff --git a/hubs/widgets/validators.py b/hubs/widgets/validators.py index a037a94..f1dcea7 100644 --- a/hubs/widgets/validators.py +++ b/hubs/widgets/validators.py @@ -148,3 +148,18 @@ class FedorahostedProject(Validator): if response.status_code == 200: return value raise ValueError('Invalid fedorahosted project') + + +class CommaSeparatedList(Validator): + + @classmethod + def from_string(cls, value): + return [ + elem.strip() for elem in value.split(",") if elem.strip() + ] + + @classmethod + def to_string(cls, value): + if not value: + return "" + return ", ".join(value) From ed8fa02507a900e7bb802aa7be4565cd2fcc7e3d Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mar 21 2017 09:44:19 +0000 Subject: [PATCH 2/6] Add the Halp configuration panel --- diff --git a/hubs/static/client/app/widgets/halp/Config.jsx b/hubs/static/client/app/widgets/halp/Config.jsx new file mode 100644 index 0000000..9e81477 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/Config.jsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { + IntlProvider, + defineMessages, + injectIntl, + FormattedMessage, + } from 'react-intl'; +import CompletionInput from '../../components/CompletionInput.jsx'; + + +const messages = defineMessages({ + title_add: { + id: "hubs.widgets.halp.config.title.add", + defaultMessage: "Adding \"halp\" widget", + }, + title_edit: { + id: "hubs.widgets.halp.config.title.edit", + defaultMessage: "Configuring \"halp\" widget", + }, + followed_hubs: { + id: "hubs.widgets.halp.config.followed_hubs", + defaultMessage: "Followed hubs:", + }, + no_hubs: { + id: "hubs.widgets.halp.config.no_hubs", + defaultMessage: "No hubs. Use the form to the right to add some.", + }, + main_title: { + id: "hubs.widgets.halp.config.main_title", + defaultMessage: "Track help requests in hubs", + }, + main_subtitle: { + id: "hubs.widgets.halp.config.main_subtitle", + defaultMessage: "Enter a team name below to add it to the list of hubs this widget will follow.", + }, + placeholder: { + id: "hubs.widgets.halp.config.placeholder", + defaultMessage: "Enter team name here...", + }, + add: { + id: "hubs.widgets.halp.config.add", + defaultMessage: "Add", + }, + per_page: { + id: "hubs.widgets.halp.config.per_page", + defaultMessage: "Requests per page:", + }, + per_page_help: { + id: "hubs.widgets.halp.config.per_page_help", + defaultMessage: "The number of requests per page to display.", + }, + close: { + id: "hubs.widgets.halp.config.close", + defaultMessage: "Close", + }, + cancel: { + id: "hubs.widgets.halp.config.cancel", + defaultMessage: "Cancel", + }, + save: { + id: "hubs.widgets.halp.config.save", + defaultMessage: "Save", + }, + remove_widget: { + id: "hubs.widgets.halp.config.remove_widget", + defaultMessage: "Remove this widget", + }, +}); + + +export default class Config extends React.Component { + + constructor(props) { + super(props); + this.state = this.props.initialState; + this.addHub = this.addHub.bind(this); + this.removeHub = this.removeHub.bind(this); + this.onAddClick = this.onAddClick.bind(this); + } + + addHub(name) { + // React may batch state updates: + // https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous + this.setState(function(prevState, props) { + if (prevState.hubs.indexOf(name) === -1) { + prevState.hubs.push(name); + } + return {hubs: prevState.hubs}; + }); + } + + removeHub(name) { + // React may batch state updates: + // https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous + this.setState(function(prevState, props) { + prevState.hubs.splice(prevState.hubs.indexOf(name), 1); + return {hubs: prevState.hubs}; + }); + } + + onAddClick(e) { + e.preventDefault(); + var hubName = $(this.formDom).find("input[name='hub-add']").val(); + this.addHub(hubName); + } + + render() { + var titleMsg, followedHubs; + if (this.props.mode == "add") { + titleMsg = messages.title_add; + } else { + titleMsg = messages.title_edit; + } + + followedHubs = this.state.hubs.map(function(hub, idx) { + return ( +
  • + {hub} + this.removeHub(hub)} + /> +
  • + ); + }.bind(this)); + + const IntlCompletionInput = injectIntl((props) => ( + + )); + + return ( + +
    +
    +
    this.formDom = ref}> +
    + +

    + +

    +
    +
    +
    + + {this.state.hubs.length === 0 ? + + : +
      + {followedHubs} +
    + } +
    +
    + + + +
    +
    + +
    +
    + +
    +
    +
    + +
    + this.setState({per_page: e.target.value})} + className="form-control input-sm" + /> +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    + {this.props.mode !== "add" && +
    + +
    + } +
    +
    +
    + ); + } + +} + + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/Halp.js b/hubs/static/client/app/widgets/halp/Halp.js new file mode 100644 index 0000000..5b821f5 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/Halp.js @@ -0,0 +1,4 @@ +import Widget from './Widget.jsx'; +import Config from './Config.jsx'; + +module.exports = {Widget, Config}; diff --git a/hubs/static/client/app/widgets/halp/Halp.jsx b/hubs/static/client/app/widgets/halp/Halp.jsx deleted file mode 100644 index f1db852..0000000 --- a/hubs/static/client/app/widgets/halp/Halp.jsx +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; -import { - IntlProvider, - defineMessages, - FormattedMessage, - } from 'react-intl'; -import HubsList from './HubsList.jsx'; -import ModalRequest from './ModalRequest.jsx'; -import ModalAllRequests from './ModalAllRequests.jsx'; -import Request from './Request.jsx'; - - -const messages = defineMessages({ - no_request: { - id: "hubs.widgets.halp.no_request", - defaultMessage: "No request for now." - }, - view_more: { - id: "hubs.widgets.halp.view_more", - defaultMessage: "View more" - }, - zoom_title: { - id: "hubs.widgets.halp.zoom.title", - defaultMessage: "Help request" - }, - search_title: { - id: "hubs.widgets.halp.search.title", - defaultMessage: "All Help Requests" - }, -}); - - -export default class HalpWidget extends React.Component { - - constructor(props) { - super(props); - this.state = { - requests: [], - inactiveHubs: [], - zoomedRequest: null, - allRequestsOpen: false, - loading: false - }; - this.zoomOnRequest = this.zoomOnRequest.bind(this); - this.onZoomClose = this.onZoomClose.bind(this); - this.openAllRequests = this.openAllRequests.bind(this); - this.onAllRequestsClose = this.onAllRequestsClose.bind(this); - this.onHubClick = this.onHubClick.bind(this); - } - - loadFromServer() { - this.setState({loading: true}); - var data = { hubs: [] }; - this.props.hubs.forEach(function(hub) { - if (this.state.inactiveHubs.indexOf(hub) === -1) { - data.hubs.push(hub); - } - }.bind(this)); - this.serverRequest = $.ajax({ - url: this.props.urls.data, - method: 'GET', - data: $.param(data, true), - dataType: 'json', - cache: false, - success: function(data) { - this.setState({ - requests: data.requests, - loading: false - }); - }.bind(this), - error: function(xhr, status, err) { - console.error(this.props.urls.data, status, err.toString()); - this.setState({loading: false}); - }.bind(this) - }); - } - - componentDidMount() { - this.loadFromServer(); - } - - componentWillUnmount() { - this.serverRequest.abort(); - } - - zoomOnRequest(request) { - this.setState({zoomedRequest: request}); - } - onZoomClose() { - this.setState({zoomedRequest: null}); - } - - openAllRequests(e) { - e.preventDefault(); - this.setState({allRequestsOpen: true}); - } - onAllRequestsClose() { - this.setState({allRequestsOpen: false}); - } - - onHubClick(hubName) { - var newInactiveHubs = this.state.inactiveHubs.slice(), - inactiveIndex = this.state.inactiveHubs.indexOf(hubName); - if (inactiveIndex === -1) { - newInactiveHubs.push(hubName); - } else { - newInactiveHubs.splice(inactiveIndex, 1); - } - this.setState( - { inactiveHubs: newInactiveHubs }, - this.loadFromServer); - } - - render() { - var requestNodes; - if (this.state.requests.length === 0) { - requestNodes = ( -

    - -

    - ); - } else { - var previousDate = null; - requestNodes = this.state.requests.map(function(request, idx) { - var showDate = (request.date !== previousDate); - previousDate = request.date; - return ( - - ); - }.bind(this)); - } - var hubs = this.props.hubs.map(function(hub) { - return { - name: hub, - active: (this.state.inactiveHubs.indexOf(hub) === -1) - } - }.bind(this)); - return ( - -
    - - { this.state.loading ? - - : -
    - {requestNodes} -
    - } -
    - -
    - } - onHidden={this.onZoomClose} /> - } - urls={this.props.urls} - hubs={hubs} - /> -
    -
    - ); - } -} - - -// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/app/widgets/halp/Widget.jsx b/hubs/static/client/app/widgets/halp/Widget.jsx new file mode 100644 index 0000000..71234a4 --- /dev/null +++ b/hubs/static/client/app/widgets/halp/Widget.jsx @@ -0,0 +1,182 @@ +import React from 'react'; +import { + IntlProvider, + defineMessages, + FormattedMessage, + } from 'react-intl'; +import HubsList from './HubsList.jsx'; +import ModalRequest from './ModalRequest.jsx'; +import ModalAllRequests from './ModalAllRequests.jsx'; +import Request from './Request.jsx'; + + +const messages = defineMessages({ + no_request: { + id: "hubs.widgets.halp.no_request", + defaultMessage: "No request for now." + }, + view_more: { + id: "hubs.widgets.halp.view_more", + defaultMessage: "View more" + }, + zoom_title: { + id: "hubs.widgets.halp.zoom.title", + defaultMessage: "Help request" + }, + search_title: { + id: "hubs.widgets.halp.search.title", + defaultMessage: "All Help Requests" + }, +}); + + +export default class Widget extends React.Component { + + constructor(props) { + super(props); + this.state = { + requests: [], + inactiveHubs: [], + zoomedRequest: null, + allRequestsOpen: false, + loading: false + }; + this.zoomOnRequest = this.zoomOnRequest.bind(this); + this.onZoomClose = this.onZoomClose.bind(this); + this.openAllRequests = this.openAllRequests.bind(this); + this.onAllRequestsClose = this.onAllRequestsClose.bind(this); + this.onHubClick = this.onHubClick.bind(this); + } + + loadFromServer() { + this.setState({loading: true}); + var data = { hubs: [] }; + this.props.hubs.forEach(function(hub) { + if (this.state.inactiveHubs.indexOf(hub) === -1) { + data.hubs.push(hub); + } + }.bind(this)); + this.serverRequest = $.ajax({ + url: this.props.urls.data, + method: 'GET', + data: $.param(data, true), + dataType: 'json', + cache: false, + success: function(data) { + this.setState({ + requests: data.requests, + loading: false + }); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.urls.data, status, err.toString()); + this.setState({loading: false}); + }.bind(this) + }); + } + + componentDidMount() { + this.loadFromServer(); + } + + componentWillUnmount() { + this.serverRequest.abort(); + } + + zoomOnRequest(request) { + this.setState({zoomedRequest: request}); + } + onZoomClose() { + this.setState({zoomedRequest: null}); + } + + openAllRequests(e) { + e.preventDefault(); + this.setState({allRequestsOpen: true}); + } + onAllRequestsClose() { + this.setState({allRequestsOpen: false}); + } + + onHubClick(hubName) { + var newInactiveHubs = this.state.inactiveHubs.slice(), + inactiveIndex = this.state.inactiveHubs.indexOf(hubName); + if (inactiveIndex === -1) { + newInactiveHubs.push(hubName); + } else { + newInactiveHubs.splice(inactiveIndex, 1); + } + this.setState( + { inactiveHubs: newInactiveHubs }, + this.loadFromServer); + } + + render() { + var requestNodes; + if (this.state.requests.length === 0) { + requestNodes = ( +

    + +

    + ); + } else { + var previousDate = null; + requestNodes = this.state.requests.map(function(request, idx) { + var showDate = (request.date !== previousDate); + previousDate = request.date; + return ( + + ); + }.bind(this)); + } + var hubs = this.props.hubs.map(function(hub) { + return { + name: hub, + active: (this.state.inactiveHubs.indexOf(hub) === -1) + } + }.bind(this)); + return ( + +
    + + { this.state.loading ? + + : +
    + {requestNodes} +
    + } +
    + +
    + } + onHidden={this.onZoomClose} /> + } + urls={this.props.urls} + hubs={hubs} + /> +
    +
    + ); + } +} + + +// vim: set ts=2 sw=2 et: diff --git a/hubs/static/client/webpack.config.js b/hubs/static/client/webpack.config.js index 4bd3fd9..f197a0e 100644 --- a/hubs/static/client/webpack.config.js +++ b/hubs/static/client/webpack.config.js @@ -10,7 +10,7 @@ const config = { entry: { Hubs: path.join(PATHS.app, 'core', 'Hubs.js'), Feed: path.join(PATHS.app, 'widgets', 'feed', 'Feed.jsx'), - Halp: path.join(PATHS.app, 'widgets', 'halp', 'Halp.jsx') + Halp: path.join(PATHS.app, 'widgets', 'halp', 'Halp.js') }, output: { path: PATHS.build, diff --git a/hubs/static/css/style.css b/hubs/static/css/style.css index 7516bea..7ff066f 100644 --- a/hubs/static/css/style.css +++ b/hubs/static/css/style.css @@ -580,6 +580,39 @@ font-size: 32pt; color: gray; } +/* Config panel */ +.widget-halp-config .modal-body { + padding-top: 1em; +} +.widget-halp-config .hubs-list h5 { + font-weight: bold; +} +.widget-halp-config .hubs-list ul { + color: gray; +} +.widget-halp-config .hubs-list .hub { + border: 1px solid #aaa; + border-radius: 4px; + display: inline-block; + margin: 0.7em 0.5em 0 0; + padding: 1px 4px; + color: #aaa; +} +.widget-halp-config .hubs-list .remove-hub { + cursor: pointer; + font-weight: bold; +} +.widget-halp-config .modal-body .form-side { + border-left: 1px solid #aaa; +} +.widget-halp-config .modal-body h4 { + font-weight: bold; + margin-bottom: 1em; +} +.widget-halp-config fieldset { + margin-top: 1.5em; +} + /** fedora bootstrap overrides **/ diff --git a/hubs/widgets/halp/__init__.py b/hubs/widgets/halp/__init__.py index 92df5a4..474fc83 100644 --- a/hubs/widgets/halp/__init__.py +++ b/hubs/widgets/halp/__init__.py @@ -2,9 +2,11 @@ from __future__ import unicode_literals import arrow +import flask from hubs.widgets.base import Widget from hubs.widgets import validators +from .views import add_view, hubs_suggest_view class Halp(Widget): @@ -33,3 +35,22 @@ class Halp(Widget): env = super(Halp, self).get_template_environment() env.filters['humanize'] = lambda d: arrow.get(d).humanize() return env + + def get_add_url(self, hub, position): + return "%s?position=%s" % ( + flask.url_for('halp_config_add', hub=hub.name), position) + + def get_edit_url(self, hub, widget_instance): + return flask.url_for( + 'halp_config', hub=hub.name, idx=widget_instance.idx) + + def register_routes(self, app): + super(Halp, self).register_routes(app) + # Add the widget add view + rule = "//w/%s/add" % self.name + endpoint = "%s_config_add" % self.name + app.add_url_rule(rule, endpoint=endpoint, view_func=add_view) + # Add the hubs suggestion view + rule = "/w/%s/hubs" % self.name + endpoint = "%s_config_hubs" % self.name + app.add_url_rule(rule, endpoint=endpoint, view_func=hubs_suggest_view) diff --git a/hubs/widgets/halp/templates/halp.html b/hubs/widgets/halp/templates/halp.html index 908d916..8709b0e 100644 --- a/hubs/widgets/halp/templates/halp.html +++ b/hubs/widgets/halp/templates/halp.html @@ -6,7 +6,7 @@ + diff --git a/hubs/widgets/halp/views.py b/hubs/widgets/halp/views.py index 1aaf1ad..46d530e 100644 --- a/hubs/widgets/halp/views.py +++ b/hubs/widgets/halp/views.py @@ -4,6 +4,9 @@ import datetime import flask import time +from hubs.models import Hub +from hubs.views.utils import get_hub +from hubs.widgets import registry from hubs.widgets.base import WidgetView from .functions import GetRequests @@ -139,6 +142,9 @@ class SearchView(WidgetView): class RequestersView(WidgetView): + """ + Returns suggestions for the "requesters" filter. + """ name = "requesters" url_rules = ["requesters"] @@ -158,3 +164,62 @@ class RequestersView(WidgetView): ]) results = sorted(list(results))[:self.MAX_SUGGESTS] return flask.jsonify({"results": results}) + + +class ConfigView(WidgetView): + """ + The custom configuration panel. + """ + + name = "config" + url_rules = ["config"] + template_name = "halp_config.html" + + def get_context(self, instance, *args, **kwargs): + post_url = flask.url_for( + 'widget_edit', hub=instance.hub.name, idx=instance.idx) + initial = instance.config.copy() + return dict( + mode="edit", + url=post_url, + initial=initial, + ) + + +def hubs_suggest_view(): + """ + Returns suggestions for the "hubs" list in the configuration. + """ + MAX_SUGGESTS = 5 + results = Hub.query + if flask.request.args.get("q"): + results = results.filter(Hub.name.ilike( + "%s%%" % flask.request.args.get("q"))) + results = results.order_by(Hub.name)[:MAX_SUGGESTS] + return flask.jsonify({"results": [h.name for h in results]}) + + +def add_view(hub): + """The custom configuration panel when adding the widget. + + This is not a subclass of WidgetView because it is not specific + to a widget instance, therefore it does not have the same URL + parameters. + """ + hub = get_hub(hub) + widget = registry["halp"] + tpl_env = widget.get_template_environment() + template = tpl_env.get_template("halp_config.html") + post_url = "%s?position=%s" % ( + flask.url_for('widget_add', hub=hub.name, widget="halp"), + flask.request.args.get("position")) + initial = dict( + (param.name, param.default) + for param in widget.get_parameters() + ) + initial["hubs"] = [hub.name] + return template.render( + mode="add", + url=post_url, + initial=initial, + ) From 6dc1f63a5693c33c8123b17a09a81ebaaf446e44 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mar 21 2017 09:44:19 +0000 Subject: [PATCH 3/6] Make widget views emit the template signals And add a way to inspect rendered templates in unit tests. --- diff --git a/hubs/tests/__init__.py b/hubs/tests/__init__.py index a3cc5f7..c430043 100644 --- a/hubs/tests/__init__.py +++ b/hubs/tests/__init__.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import os import munch +from flask import template_rendered from os.path import dirname import unittest @@ -155,3 +156,18 @@ class FakeAuthorization(object): self.avatar = 'avatar_src_url' self.nickname = username self.username = username + + +# http://flask.pocoo.org/docs/0.11/signals/#subscribing-to-signals + +@contextmanager +def captured_templates(app): + recorded = [] + + def record(sender, template, context, **extra): + recorded.append((template, context)) + template_rendered.connect(record, app) + try: + yield recorded + finally: + template_rendered.disconnect(record, app) diff --git a/hubs/widgets/halp/views.py b/hubs/widgets/halp/views.py index 46d530e..b4ce683 100644 --- a/hubs/widgets/halp/views.py +++ b/hubs/widgets/halp/views.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals, absolute_import import datetime import flask import time +from flask.signals import template_rendered, before_render_template from hubs.models import Hub from hubs.views.utils import get_hub @@ -218,8 +219,8 @@ def add_view(hub): for param in widget.get_parameters() ) initial["hubs"] = [hub.name] - return template.render( - mode="add", - url=post_url, - initial=initial, - ) + context = dict(mode="add", url=post_url, initial=initial) + before_render_template.send(widget, template=template, context=context) + output = template.render(**context) + template_rendered.send(widget, template=template, context=context) + return output diff --git a/hubs/widgets/view.py b/hubs/widgets/view.py index 19ff77c..943302d 100644 --- a/hubs/widgets/view.py +++ b/hubs/widgets/view.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals, absolute_import +from flask.signals import template_rendered, before_render_template from flask.views import View @@ -97,4 +98,10 @@ class WidgetView(View): instance = self._get_instance(*args, **kwargs) context = self.get_context(instance, *args, **kwargs) context.update(self.get_extra_context(instance, *args, **kwargs)) - return self.get_template().render(**context) + template = self.get_template() + before_render_template.send( + self.widget, template=template, context=context) + output = template.render(**context) + template_rendered.send( + self.widget, template=template, context=context) + return output From d28b4a91a0028f4d96069779d9f46d00c3c53121 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mar 21 2017 09:44:19 +0000 Subject: [PATCH 4/6] Map channels in messages to configured channels --- diff --git a/hubs/widgets/halp/functions.py b/hubs/widgets/halp/functions.py index b7f2b88..0c30728 100644 --- a/hubs/widgets/halp/functions.py +++ b/hubs/widgets/halp/functions.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import import datetime @@ -12,6 +12,7 @@ import time from jinja2.utils import urlize from hubs.widgets.caching import CachedFunction +from .utils import find_hubs_for_msg log = logging.getLogger('hubs.widgets') @@ -42,20 +43,14 @@ class GetRequests(CachedFunction): def should_invalidate(self, message): if message["topic"] != self.TOPIC: return False - hubs = channel_to_hubs(message['msg']['channel']) - return bool(set(hubs) & set(self.instance.config["hubs"])) - - -def channel_to_hubs(channel): - # TODO: Link the channel to the hubs that use it. - return [channel.lstrip("#")] + hubs = find_hubs_for_msg(message['msg']) + return bool(set(hubs) & set(self.instance.config.get("hubs", []))) def _format_message(raw_message): msg = { "date": raw_message["msg"]["details"]["time_"], "channel": raw_message["msg"]["channel"], - "hubs": channel_to_hubs(raw_message["msg"]["channel"]), "meeting_topic": raw_message["msg"]["meeting_topic"], "text": raw_message["msg"]["details"]["line"], "context_url": "%s.log.html#l-%s" % ( diff --git a/hubs/widgets/halp/utils.py b/hubs/widgets/halp/utils.py new file mode 100644 index 0000000..86a5f00 --- /dev/null +++ b/hubs/widgets/halp/utils.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +from hubs.models import Hub, HubConfig + + +def find_hubs_for_msg(msg): + """ + Returns the hubs working in the given channel. + + TODO: link meeting channels to the hub using them at that time. + """ + return [ + h[0] for h in + Hub.query.join(HubConfig).filter( + HubConfig.chat_channel == msg["channel"] + ).values(Hub.name) + ] diff --git a/hubs/widgets/halp/views.py b/hubs/widgets/halp/views.py index b4ce683..3c992f4 100644 --- a/hubs/widgets/halp/views.py +++ b/hubs/widgets/halp/views.py @@ -10,6 +10,7 @@ from hubs.views.utils import get_hub from hubs.widgets import registry from hubs.widgets.base import WidgetView from .functions import GetRequests +from .utils import find_hubs_for_msg def paginate(values, per_page): @@ -50,7 +51,7 @@ class BaseView(WidgetView): def get_context(self, instance, *args, **kwargs): return dict( - hubs=instance.config["hubs"], + hubs=instance.config.get("hubs", []), title=self.widget.label, ) @@ -70,6 +71,9 @@ class DataView(WidgetView): data = {"requests": []} hubs_filter = flask.request.args.getlist("hubs") for req in get_requests(): + req["hubs"] = find_hubs_for_msg(req) + if not req["hubs"]: + continue req["hub"] = req["hubs"][0] # Arbitrarily assign the first hub. if "hubs" in flask.request.args: # Find which hubs are targeted by this message. @@ -122,6 +126,9 @@ class SearchView(WidgetView): # filter the data data = {"requests": []} for req in get_requests(): + req["hubs"] = find_hubs_for_msg(req) + if not req["hubs"]: + continue req["hub"] = req["hubs"][0] # Arbitrarily assign the first hub. if filter_hubs is not None and not set(req["hubs"]) & filter_hubs: continue From 5b80a602143b5263fa48c9a338afe909a62dc53c Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mar 21 2017 09:44:19 +0000 Subject: [PATCH 5/6] Add tests for the halp widget --- diff --git a/hubs/tests/test_widgets/test_halp.py b/hubs/tests/test_widgets/test_halp.py new file mode 100644 index 0000000..8950429 --- /dev/null +++ b/hubs/tests/test_widgets/test_halp.py @@ -0,0 +1,107 @@ +from __future__ import unicode_literals, absolute_import + +import json + +from hubs.models import Hub +from hubs.tests import captured_templates +from hubs.tests.test_widgets import WidgetTest +from hubs.widgets import registry + + +class HalpViewsTestCase(WidgetTest): + plugin = 'halp' # The name in hubs.widgets.registry + + def setUp(self): + super(HalpViewsTestCase, self).setUp() + self.widget = self.widget_instance('ralph', self.plugin) + config = self.widget.config + config["hubs"] = ["fedora-devel"] + + def test_root(self): + with captured_templates(self.widget.module) as templates: + response = self.app.get('/ralph/w/halp/%i/' % self.widget.idx) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(templates), 1) + _template, context = templates[0] + self.assertEqual(context["hubs"], []) + + def test_json(self): + response = self.app.get('/ralph/%i/json' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertEquals(data['plugin'], 'halp') + self.assertIn('hubs', data['data'].keys()) + + def test_config_add(self): + with captured_templates(self.widget.module) as templates: + response = self.app.get('/ralph/w/halp/add?position=right') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(templates), 1) + _template, context = templates[0] + self.assertEqual(context["mode"], "add") + self.assertEqual(context["initial"], { + "hubs": ["ralph"], "per_page": 4, + }) + self.assertEqual(context["url"], "/ralph/add/halp?position=right") + + def test_config_edit(self): + with captured_templates(self.widget.module) as templates: + response = self.app.get('/ralph/w/halp/%i/config' + % self.widget.idx) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(templates), 1) + _template, context = templates[0] + self.assertEqual(context["mode"], "edit") + self.assertEqual(context["initial"], {}) + self.assertEqual(context["url"], "/ralph/%i/edit" % self.widget.idx) + + def test_config_hubs_suggest(self): + response = self.app.get('/w/halp/hubs') + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertDictEqual(data, { + 'results': ['decause', 'devyani7', 'dhrish', 'i18n', 'infra'] + }) + + +class HalpFunctionsTestCase(WidgetTest): + plugin = 'halp' # The name in hubs.widgets.registry + + def setUp(self): + super(HalpFunctionsTestCase, self).setUp() + module = registry[self.plugin] + func_class = module.get_cached_functions()['GetRequests'] + self.widget = self.widget_instance('ralph', self.plugin) + self.func = func_class(self.widget) + # The tested widget watches the infra hub. + config = self.widget.config + config["hubs"] = ["infra"] + self.widget.config = config + # The infra hub works in the fedora-infra channel. + infra_hub = Hub.query.filter_by(name="infra").one() + infra_hub.config.chat_channel = "#fedora-infra" + self.session.commit() + + def test_should_invalidate_wrong_topic(self): + msg = {'topic': 'hubs.WRONG.TOPIC'} + result = self.func.should_invalidate(msg) + self.assertFalse(result) + + def test_should_invalidate_wrong_hub(self): + # The decause hub works in the fedora-commops channel. + decause_hub = Hub.query.filter_by(name="decause").one() + decause_hub.config.chat_channel = "#fedora-commops" + self.session.commit() + msg = {'topic': 'org.fedoraproject.prod.meetbot.meeting.item.help', + 'msg': {'channel': '#fedora-commops'}, + } + # The decause hub cache would be invalidated, but not ours. + result = self.func.should_invalidate(msg) + self.assertFalse(result) + + def test_should_invalidate_good_match(self): + msg = {'topic': 'org.fedoraproject.prod.meetbot.meeting.item.help', + 'msg': {'channel': '#fedora-infra'}, + } + result = self.func.should_invalidate(msg) + self.assertTrue(result) From eef96dc161bfe0f05cf315c03bdda1d8cd2f4492 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mar 22 2017 22:58:16 +0000 Subject: [PATCH 6/6] Implement most of the review comments and add tests --- diff --git a/hubs/tests/__init__.py b/hubs/tests/__init__.py index c430043..67d82fa 100644 --- a/hubs/tests/__init__.py +++ b/hubs/tests/__init__.py @@ -158,10 +158,21 @@ class FakeAuthorization(object): self.username = username -# http://flask.pocoo.org/docs/0.11/signals/#subscribing-to-signals - @contextmanager def captured_templates(app): + """Context manager to inspect rendered templates. + + See http://flask.pocoo.org/docs/0.11/signals/#subscribing-to-signals for + usage. + + The argument is the signal emitter. When calling a core Hubs view, this is + the Hubs app (``hubs.app.app`` or ``self.app.application`` if you're in an + :py:class:`APPTest` subclass). If you're calling a widget view, the signal + emitter is the widget class, as found in the widgets registry. + + Args: + app: The signal emitter. + """ recorded = [] def record(sender, template, context, **extra): diff --git a/hubs/tests/test_widgets/test_halp.py b/hubs/tests/test_widgets/test_halp.py index 8950429..81c1831 100644 --- a/hubs/tests/test_widgets/test_halp.py +++ b/hubs/tests/test_widgets/test_halp.py @@ -8,14 +8,105 @@ from hubs.tests.test_widgets import WidgetTest from hubs.widgets import registry +SAMPLE_DATA = [ + { + 'author': { + 'url': '/mattdm/', 'name': 'mattdm', 'avatar': None, + }, + 'channel': u'#fedora-meeting', + 'context_url': ( + 'https://meetbot.fedoraproject.org/fedora-meeting/' + '2017-03-17/fesco.2017-03-17-16.00.log.html#l-466' + ), + 'date': 1489773443.0, + 'meeting_topic': 'FESCO (2017-03-17)', + 'text': ('"How to enable trim on your SSD drive" would ' + 'be a good Fedora Magazine article'), + 'urls': [], + }, { + 'author': { + 'url': '/jkurik/', 'name': 'jkurik', 'avatar': None, + }, + 'channel': '#fedora-meeting-2', + 'context_url': ( + 'https://meetbot.fedoraproject.org/fedora-meeting-2/2017-03-16/' + 'f26-alpha-readiness-meeting.2017-03-16-19.00.log.html#l-103'), + 'date': 1489692368.0, + 'meeting_topic': 'F26 Alpha Readiness Meeting', + 'text': ('The Marketing team wants to ask for help from ' + 'Spins, Labs SIGs and DEs SIGs to populate the ' + 'talking points that help us to marketing people'), + 'urls': [], + }, { + 'author': { + 'url': '/stoney/', 'name': 'stoney', 'avatar': None, + }, + 'channel': '#foss2serve', + 'context_url': ( + 'https://meetbot.fedoraproject.org/foss2serve/2017-03-14/' + 'posse_irc_meeting_1.2.2017-03-14-17.00.log.html#l-187'), + 'date': 1489512523.0, + 'meeting_topic': 'POSSE IRC Meeting 1.2', + 'text': '', + 'urls': [], + }, { + 'author': { + 'url': '/nitzmahone/', 'name': 'nitzmahone', 'avatar': None, + }, + 'channel': '#ansible-meeting', + 'context_url': ( + 'https://meetbot.fedoraproject.org/ansible-meeting/2017-03-14/' + 'windows_working_group.2017-03-14-00.00.log.html#l-130'), + 'date': 1489451407.0, + 'meeting_topic': 'Windows Working Group', + 'text': '', + 'urls': [], + }, { + 'author': { + 'url': '/jborean93/', 'name': 'jborean93', 'avatar': None + }, + 'channel': '#ansible-meeting', + 'context_url': ( + 'https://meetbot.fedoraproject.org/ansible-meeting/2017-03-14/' + 'windows_working_group.2017-03-14-00.00.log.html#l-59'), + 'date': 1489450330.0, + 'meeting_topic': 'Windows Working Group', + 'text': 'with installers win_msi vs win_product', + 'urls': [], + }, +] + + class HalpViewsTestCase(WidgetTest): plugin = 'halp' # The name in hubs.widgets.registry + maxDiff = None def setUp(self): super(HalpViewsTestCase, self).setUp() self.widget = self.widget_instance('ralph', self.plugin) config = self.widget.config config["hubs"] = ["fedora-devel"] + config["per_page"] = 3 + self.widget.config = config + infra_hub = Hub.query.get("infra") + infra_hub.config.chat_channel = "#fedora-meeting" + devyani7 = Hub.query.get("devyani7") + devyani7.config.chat_channel = "#fedora-meeting-2" + decause = Hub.query.get("decause") + decause.config.chat_channel = "#ansible-meeting" + dhrish = Hub.query.get("dhrish") + dhrish.config.chat_channel = "#foss2serve" + self.session.commit() + + def _get_sample_data_with_hub_keys(self): + hub_names = ["infra", "devyani7", "dhrish", "decause", "decause"] + data = [req.copy() for req in SAMPLE_DATA] + for index, hub_name in enumerate(hub_names): + data[index].update({ + 'hub': hub_name, + 'hubs': [hub_name], + }) + return data def test_root(self): with captured_templates(self.widget.module) as templates: @@ -23,7 +114,7 @@ class HalpViewsTestCase(WidgetTest): self.assertEqual(response.status_code, 200) self.assertEqual(len(templates), 1) _template, context = templates[0] - self.assertEqual(context["hubs"], []) + self.assertEqual(context["hubs"], ['fedora-devel']) def test_json(self): response = self.app.get('/ralph/%i/json' % self.widget.idx) @@ -52,7 +143,10 @@ class HalpViewsTestCase(WidgetTest): self.assertEqual(len(templates), 1) _template, context = templates[0] self.assertEqual(context["mode"], "edit") - self.assertEqual(context["initial"], {}) + self.assertEqual(context["initial"], { + 'hubs': ['fedora-devel'], + 'per_page': 3, + }) self.assertEqual(context["url"], "/ralph/%i/edit" % self.widget.idx) def test_config_hubs_suggest(self): @@ -62,6 +156,152 @@ class HalpViewsTestCase(WidgetTest): self.assertDictEqual(data, { 'results': ['decause', 'devyani7', 'dhrish', 'i18n', 'infra'] }) + response = self.app.get('/w/halp/hubs?q=de') + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertDictEqual(data, { + 'results': ['decause', 'devyani7'] + }) + + def test_data(self): + expected = self._get_sample_data_with_hub_keys() + # Test with no hub selected + response = self.app.get('/ralph/w/halp/%i/data' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertEqual(data, {"requests": expected[:3]}) + # Test with the right hub selected + response = self.app.get('/ralph/w/halp/%i/data?hubs=decause' + % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertEqual(data, {"requests": expected[3:5]}) + + def test_data_wrong_hub(self): + # Test with the wrong hub selected + response = self.app.get('/ralph/w/halp/%i/data?hubs=ralph' + % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertEqual(data, {"requests": []}) + + def test_search_requesters(self): + response = self.app.get( + '/ralph/w/halp/%i/requesters' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertDictEqual(data, { + 'results': ['jborean93', 'jkurik', 'mattdm', 'nitzmahone', + 'stoney'] + }) + response = self.app.get( + '/ralph/w/halp/%i/requesters?q=j' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertDictEqual(data, { + 'results': ['jborean93', 'jkurik'] + }) + + def test_search_all(self): + expected = self._get_sample_data_with_hub_keys() + response = self.app.get( + '/ralph/w/halp/%i/search' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertEqual(data, { + "requests": expected[:3], + 'page': { + 'has_next': True, + 'has_prev': False, + 'nr': 1, + 'total_entries': 5, + 'total_pages': 2, + }, + }) + # Page 2 + response = self.app.get( + '/ralph/w/halp/%i/search?page=2' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + self.assertEqual(data, { + "requests": expected[3:], + 'page': { + 'has_next': False, + 'has_prev': True, + 'nr': 2, + 'total_entries': 5, + 'total_pages': 2, + }, + }) + + def test_search_hub(self): + response = self.app.get( + '/ralph/w/halp/%i/search?hubs=infra' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + expected = self._get_sample_data_with_hub_keys() + self.assertEqual(data, { + "requests": expected[0:1], + 'page': { + 'has_next': False, + 'has_prev': False, + 'nr': 1, + 'total_entries': 1, + 'total_pages': 1 + }, + }) + + def test_search_people(self): + response = self.app.get( + '/ralph/w/halp/%i/search?people=nitzmahone' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + expected = self._get_sample_data_with_hub_keys() + self.assertEqual(data, { + "requests": expected[3:4], + 'page': { + 'has_next': False, + 'has_prev': False, + 'nr': 1, + 'total_entries': 1, + 'total_pages': 1 + }, + }) + + def test_search_meetingname(self): + response = self.app.get( + '/ralph/w/halp/%i/search?meetingname=Readiness' % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + expected = self._get_sample_data_with_hub_keys() + self.assertEqual(data, { + "requests": expected[1:2], + 'page': { + 'has_next': False, + 'has_prev': False, + 'nr': 1, + 'total_entries': 1, + 'total_pages': 1 + }, + }) + + def test_search_date(self): + response = self.app.get( + '/ralph/w/halp/%i/search?startdate=2017-03-16&enddate=2017-03-17' + % self.widget.idx) + self.assertEqual(response.status_code, 200) + data = json.loads(response.get_data(as_text=True)) + expected = self._get_sample_data_with_hub_keys() + self.assertEqual(data, { + "requests": expected[1:2], + 'page': { + 'has_next': False, + 'has_prev': False, + 'nr': 1, + 'total_entries': 1, + 'total_pages': 1 + }, + }) class HalpFunctionsTestCase(WidgetTest): @@ -82,6 +322,11 @@ class HalpFunctionsTestCase(WidgetTest): infra_hub.config.chat_channel = "#fedora-infra" self.session.commit() + def test_execute(self): + with self.app.application.test_request_context('/'): + result = self.func.execute() + self.assertEqual(result, SAMPLE_DATA) + def test_should_invalidate_wrong_topic(self): msg = {'topic': 'hubs.WRONG.TOPIC'} result = self.func.should_invalidate(msg) @@ -105,3 +350,98 @@ class HalpFunctionsTestCase(WidgetTest): } result = self.func.should_invalidate(msg) self.assertTrue(result) + + +class PaginateTestCase(WidgetTest): + + def setUp(self): + super(PaginateTestCase, self).setUp() + from hubs.widgets.halp.utils import paginate # circular + self.paginate = paginate + self.values = ["A", "B", "C", "D"] + + def test_base(self): + with self.app.application.test_request_context('/'): + result = self.paginate(self.values, 3) + self.assertListEqual(result[0], self.values[:3]) + self.assertDictEqual(result[1], { + 'has_next': True, + 'has_prev': False, + 'nr': 1, + 'total_entries': len(self.values), + 'total_pages': 2, + }) + + def test_page_2(self): + with self.app.application.test_request_context('/?page=2'): + result = self.paginate(self.values, 3) + self.assertListEqual(result[0], self.values[3:]) + self.assertDictEqual(result[1], { + 'has_next': False, + 'has_prev': True, + 'nr': 2, + 'total_entries': len(self.values), + 'total_pages': 2, + }) + + def test_page_0(self): + with self.app.application.test_request_context('/?page=0'): + result = self.paginate(self.values, 3) + self.assertListEqual(result[0], self.values[:3]) + self.assertDictEqual(result[1], { + 'has_next': True, + 'has_prev': False, + 'nr': 1, + 'total_entries': len(self.values), + 'total_pages': 2, + }) + + def test_invalid_page(self): + with self.app.application.test_request_context('/?page=blah'): + result = self.paginate(self.values, 3) + self.assertListEqual(result[0], self.values[:3]) + self.assertDictEqual(result[1], { + 'has_next': True, + 'has_prev': False, + 'nr': 1, + 'total_entries': len(self.values), + 'total_pages': 2, + }) + + def test_page_too_high(self): + with self.app.application.test_request_context('/?page=3'): + result = self.paginate(self.values, 3) + self.assertListEqual(result[0], self.values[3:]) + self.assertDictEqual(result[1], { + 'has_next': False, + 'has_prev': True, + 'nr': 2, + 'total_entries': len(self.values), + 'total_pages': 2, + }) + + def test_single_page(self): + values = self.values[:3] + with self.app.application.test_request_context('/'): + result = self.paginate(values, 3) + self.assertListEqual(result[0], values) + self.assertDictEqual(result[1], { + 'has_next': False, + 'has_prev': False, + 'nr': 1, + 'total_entries': len(values), + 'total_pages': 1, + }) + + def test_no_value(self): + values = [] + with self.app.application.test_request_context('/'): + result = self.paginate(values, 3) + self.assertListEqual(result[0], []) + self.assertDictEqual(result[1], { + 'has_next': False, + 'has_prev': False, + 'nr': 1, + 'total_entries': 0, + 'total_pages': 1, + }) diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpFunctionsTestCase.test_execute b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpFunctionsTestCase.test_execute new file mode 100644 index 0000000..1e03fdc --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpFunctionsTestCase.test_execute @@ -0,0 +1,98 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy04.fedoraproject.org] + apptime: [D=630853] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 18:26:01 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data new file mode 100644 index 0000000..4d48985 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data @@ -0,0 +1,194 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy06.fedoraproject.org] + apptime: [D=634315] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4028'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 18:58:53 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy04.fedoraproject.org] + apptime: [D=583692] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4027'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:24:03 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data_wrong_hub b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data_wrong_hub new file mode 100644 index 0000000..6b0a36a --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data_wrong_hub @@ -0,0 +1,98 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy02.fedoraproject.org] + apptime: [D=886956] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:28:42 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_all b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_all new file mode 100644 index 0000000..dfd7580 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_all @@ -0,0 +1,194 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy11.fedoraproject.org] + apptime: [D=574648] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:53:38 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy06.fedoraproject.org] + apptime: [D=684600] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4028'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 21:58:21 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_date b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_date new file mode 100644 index 0000000..49d31ac --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_date @@ -0,0 +1,98 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy06.fedoraproject.org] + apptime: [D=560197] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:51:05 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_hub b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_hub new file mode 100644 index 0000000..c2ca0a7 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_hub @@ -0,0 +1,98 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy06.fedoraproject.org] + apptime: [D=461727] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:51:07 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_meetingname b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_meetingname new file mode 100644 index 0000000..07ed82f --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_meetingname @@ -0,0 +1,98 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy04.fedoraproject.org] + apptime: [D=621781] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:36:08 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_people b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_people new file mode 100644 index 0000000..d3b8371 --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_people @@ -0,0 +1,98 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy06.fedoraproject.org] + apptime: [D=474498] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 19:28:44 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_requesters b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_requesters new file mode 100644 index 0000000..cd075ec --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_requesters @@ -0,0 +1,194 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy04.fedoraproject.org] + apptime: [D=642014] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4029'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 22:29:51 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.13.0] + method: GET + uri: https://apps.fedoraproject.org/datagrepper/raw?topic=org.fedoraproject.prod.meetbot.meeting.item.help&delta=864000 + response: + body: + string: !!binary | + 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 + headers: + appserver: [proxy06.fedoraproject.org] + apptime: [D=442751] + connection: [Keep-Alive] + content-encoding: [gzip] + content-length: ['4028'] + content-type: [application/json] + date: ['Wed, 22 Mar 2017 22:34:13 GMT'] + keep-alive: ['timeout=15, max=500'] + server: [Apache/2.4.6 (Red Hat Enterprise Linux) mod_wsgi/3.4 Python/2.7.5] + strict-transport-security: [max-age=15768000; includeSubDomains; preload] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +version: 1 diff --git a/hubs/widgets/halp/__init__.py b/hubs/widgets/halp/__init__.py index 474fc83..47b47fb 100644 --- a/hubs/widgets/halp/__init__.py +++ b/hubs/widgets/halp/__init__.py @@ -10,6 +10,13 @@ from .views import add_view, hubs_suggest_view class Halp(Widget): + """Widget to list and query help requests. + + In IRC channels, people can request help using the #halp keyword. + This widget will list those requests and allow searching through them. + + See https://pagure.io/fedora-hubs/issue/98 + """ name = "halp" label = "Help Requests" diff --git a/hubs/widgets/halp/functions.py b/hubs/widgets/halp/functions.py index 0c30728..89cf0df 100644 --- a/hubs/widgets/halp/functions.py +++ b/hubs/widgets/halp/functions.py @@ -2,13 +2,13 @@ from __future__ import unicode_literals, absolute_import import datetime -import flask -import fedmsg.meta -import re import logging -import requests +import re import time +import flask +import fedmsg.meta +import requests from jinja2.utils import urlize from hubs.widgets.caching import CachedFunction @@ -19,6 +19,11 @@ log = logging.getLogger('hubs.widgets') class GetRequests(CachedFunction): + """Get the help requests from Datagrepper. + + This function will request recent help requests from datagrepper and cache + them. + """ TOPIC = "org.fedoraproject.prod.meetbot.meeting.item.help" @@ -29,7 +34,6 @@ class GetRequests(CachedFunction): args = { "topic": self.TOPIC, "delta": str(86400 * 10), # 10 days - # "rows_per_page": per_page, } log.info("Getting request data from Datagrepper") response = requests.get(url, args) @@ -48,6 +52,10 @@ class GetRequests(CachedFunction): def _format_message(raw_message): + """ + Format a raw Fedmsg message dict into a dict that the Halp widget will be + able to use more directly. + """ msg = { "date": raw_message["msg"]["details"]["time_"], "channel": raw_message["msg"]["channel"], diff --git a/hubs/widgets/halp/utils.py b/hubs/widgets/halp/utils.py index 86a5f00..4d4c210 100644 --- a/hubs/widgets/halp/utils.py +++ b/hubs/widgets/halp/utils.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import flask + from hubs.models import Hub, HubConfig @@ -8,6 +10,10 @@ def find_hubs_for_msg(msg): Returns the hubs working in the given channel. TODO: link meeting channels to the hub using them at that time. + + Args: + msg (dict): the ``msg`` value in the raw message dict sent through + Fedmsg. """ return [ h[0] for h in @@ -15,3 +21,60 @@ def find_hubs_for_msg(msg): HubConfig.chat_channel == msg["channel"] ).values(Hub.name) ] + + +def paginate(values, per_page): + """Paginate the values. + + The requested page will be extracted from the ``page`` query string + element. + + The returned value will be a tuple with two elements: + + - the paginated values + - the page information + + The page information is a dictionary with the following keys: + + - ``nr``: the page number + - ``has_prev``: ``True`` if there is a previous page, ``False`` otherwise + - ``has_next``: ``True`` if there is a following page, ``False`` otherwise + - ``total_entries``: the total number of entries in the ``values`` argument + - ``total_pages``: the total number of pages + + Args: + values (list or query): The list or SQLAlchemy query object to + paginate. + per_page (int): the number of elements per page. + + Returns: + tuple: a tuple containing the paginated values and the page + information. + """ + try: + total = values.count() + except TypeError: + total = len(values) + try: + page = flask.request.values.get("page", 1, int) + except ValueError: + page = 1 + last_page = int(total / per_page) + if total > last_page * per_page: + last_page += 1 + if last_page < 1: + last_page = 1 + if page < 1: + page = 1 + if page > last_page: + page = last_page + start = (page - 1) * per_page + end = start + per_page + page_data = { + "nr": page, + "has_prev": page > 1, + "has_next": page < last_page, + "total_entries": total, + "total_pages": last_page, + } + return values[start:end], page_data diff --git a/hubs/widgets/halp/views.py b/hubs/widgets/halp/views.py index 3c992f4..264da3d 100644 --- a/hubs/widgets/halp/views.py +++ b/hubs/widgets/halp/views.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals, absolute_import import datetime -import flask import time + +import flask from flask.signals import template_rendered, before_render_template from hubs.models import Hub @@ -10,40 +11,11 @@ from hubs.views.utils import get_hub from hubs.widgets import registry from hubs.widgets.base import WidgetView from .functions import GetRequests -from .utils import find_hubs_for_msg - - -def paginate(values, per_page): - try: - total = values.count() - except TypeError: - total = len(values) - try: - page = flask.request.values.get("page", 1, int) - except ValueError: - page = 1 - last_page = int(total / per_page) - if total > last_page * per_page: - last_page += 1 - if last_page < 1: - last_page = 1 - if page < 1: - page = 1 - if page > last_page: - page = last_page - start = (page - 1) * per_page - end = start + per_page - page_data = { - "nr": page, - "has_prev": page > 1, - "has_next": page < last_page, - "total_entries": total, - "total_pages": last_page, - } - return values[start:end], page_data +from .utils import find_hubs_for_msg, paginate class BaseView(WidgetView): + """The base view to instantiate the widget.""" name = "root" url_rules = ["/"] @@ -57,15 +29,12 @@ class BaseView(WidgetView): class DataView(WidgetView): + """Get the requests in JSON format.""" name = "data" url_rules = ["data"] def dispatch_request(self, *args, **kwargs): - """ - The method that subclasses of :py:class:`flask.views.View` must - implement. It binds the other methods together. - """ instance = self._get_instance(*args, **kwargs) get_requests = GetRequests(instance) data = {"requests": []} @@ -90,16 +59,13 @@ class DataView(WidgetView): class SearchView(WidgetView): + """Search the requests and return the result as JSON.""" name = "search" url_rules = ["search"] methods = ['GET', 'POST'] def dispatch_request(self, *args, **kwargs): - """ - The method that subclasses of :py:class:`flask.views.View` must - implement. It binds the other methods together. - """ instance = self._get_instance(*args, **kwargs) get_requests = GetRequests(instance) # prepare filters @@ -150,26 +116,21 @@ class SearchView(WidgetView): class RequestersView(WidgetView): - """ - Returns suggestions for the "requesters" filter. - """ + """Returns suggestions for the "requesters" filter.""" name = "requesters" url_rules = ["requesters"] MAX_SUGGESTS = 5 def dispatch_request(self, *args, **kwargs): - """ - The method that subclasses of :py:class:`flask.views.View` must - implement. It binds the other methods together. - """ instance = self._get_instance(*args, **kwargs) get_requests = GetRequests(instance) results = set([ req["author"]["name"] for req in get_requests() - if req["author"]["name"].startswith( - flask.request.args.get("q")) ]) + query = flask.request.args.get("q") + if query: + results = [name for name in results if name.startswith(query)] results = sorted(list(results))[:self.MAX_SUGGESTS] return flask.jsonify({"results": results}) diff --git a/hubs/widgets/validators.py b/hubs/widgets/validators.py index f1dcea7..780a3d4 100644 --- a/hubs/widgets/validators.py +++ b/hubs/widgets/validators.py @@ -151,6 +151,13 @@ class FedorahostedProject(Validator): class CommaSeparatedList(Validator): + """Fails if the value isn't a comma-separated list. + + The value will be converted to a Python list. If there is no comma in the + original value, it will produce a list with a single element. Whitespaces + will be stripped from the elements, so spaces are allowed around the + commas. + """ @classmethod def from_string(cls, value):