#341 Halp widget
Merged 7 years ago by abompard. Opened 7 years ago by abompard.
abompard/fedora-hubs halp_widget  into  develop

file modified
+2
@@ -44,6 +44,7 @@ 

      }

  ]

  

+ DATAGREPPER_URI = 'https://apps.fedoraproject.org/datagrepper'

  

  WIDGETS = [

      'hubs.widgets.about:About',
@@ -58,6 +59,7 @@ 

      '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',

file modified
+6
@@ -82,6 +82,12 @@ 

              'username': username,

          }))

      hub.widgets.append(widget)

+     hub.widgets.append(

+         hubs.models.Widget(

+             plugin='halp', index=9,

+             _config=json.dumps({

+             }))

+         )

      return hub

  

  

@@ -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 (

+       <div className="completion-input">

+         <Autosuggest

+           suggestions={this.state.suggestions}

+           onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}

+           onSuggestionsClearRequested={this.onSuggestionsClearRequested}

+           onSuggestionSelected={this.onSuggestionSelected}

+           getSuggestionValue={getSuggestionValue}

+           renderSuggestion={renderSuggestion}

+           inputProps={inputProps}

+         />

+         { this.state.loading ?

+           <span className="form-control-feedback">

+             <img src={this.props.spinner} />

+           </span>

+         : null }

+       </div>

+     );

+   }

+ 

+ }

+ CompletionInput.defaultProps = {

+     placeholder: "",

+     queryDelay: 800,

+ };

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 (

+       <div className="modal fade" role="dialog" ref={(ref) => this.domNode = ref}>

+         <div className="modal-dialog" role="document">

+           <div className="modal-content">

+             <div className="modal-header">

+               <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>

+               <h4 className="modal-title">{this.props.title}</h4>

+             </div>

+             <div className="modal-body">

+               {this.getModalBody()}

+             </div>

+             {footer ?

+             <div className="modal-footer">

+               {footer}

+             </div>

+              : null}

+           </div>

+         </div>

+       </div>

+     );

+   }

+ }

+ Modal.propTypes = {

+     open: React.PropTypes.bool,

+     data: React.PropTypes.object

+ };

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 (

+         <li key={hub}>

+           <span className="hub">{hub}</span>

+           <span className="fa fa-times remove-hub"

+                 onClick={(e) => this.removeHub(hub)}

+             />

+         </li>

+         );

+     }.bind(this));

+ 

+     const IntlCompletionInput = injectIntl((props) => (

+         <CompletionInput

+           name="hub-add"

+           url={props.urls.hubs}

+           spinner={props.urls.spinnerCircle}

+           placeholder={props.intl.formatMessage(messages.placeholder)}

+           />

+     ));

+ 

+     return (

+       <IntlProvider locale={navigator.language}>

+         <div className={"modal-dialog widget-halp-config widget-" + this.props.mode}>

+           <div className="modal-content">

+             <form method="post" action={this.props.urls.post}

+                   ref={(ref) => this.formDom = ref}>

+               <div className="modal-header">

+                 <button type="button" className="close" data-dismiss="modal">&times;</button>

+                 <h4 className="modal-title">

+                   <FormattedMessage {...titleMsg} />

+                 </h4>

+               </div>

+               <div className="modal-body row">

+                 <div className="col-sm-4 hubs-list">

+                   <FormattedMessage {...messages.followed_hubs} tagName="h5" />

+                   {this.state.hubs.length === 0 ?

+                     <FormattedMessage {...messages.no_hubs} tagName="p" />

+                   :

+                     <ul className="list-unstyled">

+                       {followedHubs}

+                     </ul>

+                   }

+                 </div>

+                 <div className="col-sm-8 form-side">

+                   <FormattedMessage {...messages.main_title} tagName="h4" />

+                   <FormattedMessage {...messages.main_subtitle} tagName="p" />

+                   <input type="hidden" name="hubs" value={this.state.hubs.join(",")} />

+                   <fieldset className="row">

+                     <div className="col-sm-9">

+                       <IntlCompletionInput {...this.props} />

+                     </div>

+                     <div className="col-sm-3">

+                       <button className="btn btn-info" onClick={this.onAddClick}>

+                         <FormattedMessage {...messages.add} />

+                       </button>

+                     </div>

+                   </fieldset>

+                   <fieldset className="row">

+                     <label className="col-sm-6 control-label">

+                       <FormattedMessage {...messages.per_page} />

+                     </label>

+                     <div className="col-sm-6">

+                       <input

+                         name="per_page"

+                         type="number"

+                         value={this.state.per_page}

+                         onChange={(e) => this.setState({per_page: e.target.value})}

+                         className="form-control input-sm"

+                         />

+                     </div>

+                     <div className="col-sm-12 text-muted">

+                       <FormattedMessage {...messages.per_page_help} tagName="small" />

+                     </div>

+                   </fieldset>

+                 </div>

+               </div>

+               <div className="modal-footer">

+                 <button type="button" className="btn btn-default" data-dismiss="modal">

+                   {this.props.mode === "add" ?

+                     <FormattedMessage {...messages.cancel} />

+                   :

+                     <FormattedMessage {...messages.close} />

+                   }

+                 </button>

+                 <button type="submit" className="btn btn-primary"

+                         disabled={this.state.hubs.length === 0 ? true : null}>

+                 {this.props.mode === "add" ?

+                     <FormattedMessage {...messages.add} />

+                 :

+                     <FormattedMessage {...messages.save} />

+                 }

+                 </button>

+               </div>

+             </form>

+             {this.props.mode !== "add" &&

+               <form method="POST" action={this.props.urls.remove} className="modal-footer p-abs" >

+                   <button type="submit" className="btn btn-danger" id="delete_widget">

+                     <FormattedMessage {...messages.remove_widget} />

+                   </button>

+               </form>

+             }

+           </div>

+         </div>

+       </IntlProvider>

+     );

+   }

+ 

+ }

+ 

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -0,0 +1,4 @@ 

+ import Widget from './Widget.jsx';

+ import Config from './Config.jsx';

+ 

+ module.exports = {Widget, Config};

@@ -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 (

+       <span

+         className={"hub " + activeClass}

+         onClick={this.onClick.bind(this)}

+         >{"@" + this.props.name}</span>

+     );

+   }

+ }

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 (

+         <Hub

+           name={hub.name}

+           active={hub.active}

+           onClick={this.props.onHubClick}

+           key={idx}

+           />

+       );

+     }.bind(this));

+     return (

+       <div className="hubs row">

+         <span className="fa fa-tags" aria-hidden="true"></span>

+         { (hubNodes.length !== 0) ?

+             hubNodes

+           :

+             <FormattedMessage

+               tagName="em"

+               {...messages.watching_all_hubs}

+               />

+         }

+       </div>

+     );

+   }

+ }

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 = (<p className="no-request">

+         <FormattedMessage {...messages.no_request_found} />

+       </p>);

+     } else {

+       requestNodes = this.state.requests.map(function(request, idx) {

+         return (

+           <Request

+             request={request}

+             showdate={false}

+             onZoomRequest={this.onZoomRequest}

+             key={idx} />

+         );

+       }.bind(this));

+     }

+     return (

+       <div>

+         <form className="row form-horizontal" method="post"

+               action={this.props.urls.search} onSubmit={this.onSubmit}>

+           <div className="form-group">

+             <label htmlFor="meetingname" className="col-sm-4 control-label">

+               <FormattedMessage {...messages.search_meeting_name} />

+             </label>

+             <div className="col-sm-8">

+               <input type="text" className="form-control" name="meetingname"

+                      onChange={this.handleChange} />

+             </div>

+           </div>

+           <div className="form-group">

+             <label htmlFor="startdate" className="col-sm-4 control-label">

+               <FormattedMessage {...messages.search_between_dates} />

+             </label>

+             <div className="col-sm-4">

+               <input type="date" className="form-control" name="startdate"

+                      onChange={this.handleChange} />

+             </div>

+             <div className="col-sm-4">

+               <input type="date" className="form-control" name="enddate"

+                      onChange={this.handleChange} />

+             </div>

+           </div>

+           <div className="form-group">

+             <label htmlFor="people" className="col-sm-4 control-label">

+               <FormattedMessage {...messages.search_people} />

+             </label>

+             <div className="col-sm-8">

+               <CompletionInput

+                 name="people"

+                 url={this.props.urls.requesters}

+                 spinner={this.props.urls.spinnerCircle}

+                 onChange={this.handleChange} />

+             </div>

+           </div>

+           <div className="form-group">

+             <div className="col-sm-offset-4 col-sm-8">

+               <button type="submit" className="btn btn-primary">

+                 <FormattedMessage {...messages.search_button} />

+               </button>

+               { this.state.loading && <img style={{marginLeft: "2em"}} src={this.props.urls.spinner} /> }

+             </div>

+           </div>

+         </form>

+         <hr />

+         <div className="requests">

+           {requestNodes}

+         </div>

+       </div>

+     );

+   }

+ 

+   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 (

+       <nav aria-label="...">

+         <ul className="pager">

+           <li className={"pager-prev" + (this.state.page.has_prev ? "" : " disabled")}>

+             <a href="#" onClick={this.changePage.bind(this, -1)}>

+               <span aria-hidden="true">&larr;</span>

+               <FormattedMessage {...messages.pager_newer} />

+               </a>

+           </li>

+           <FormattedMessage

+             tagName="li"

+             {...messages.pager_status}

+             values={{page: this.state.page.nr, total: this.state.page.total_pages}}

+             />

+           <li className={"pager-next" + (this.state.page.has_next ? "" : " disabled")}>

+             <a href="#" onClick={this.changePage.bind(this, 1)}>

+               <FormattedMessage {...messages.pager_older} />

+               <span aria-hidden="true">&rarr;</span></a>

+           </li>

+         </ul>

+       </nav>

+     );

+   }

+ }

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 (

+       <div className="zoomed-request">

+         {getRequestView(this.props.data)}

+         <hr />

+         <FormattedMessage

+           tagName="h4"

+           {...messages.zoom_part2_title}

+           />

+         <FormattedMessage

+           tag="p"

+           {...messages.zoom_part2_subtitle}

+           />

+         <ol>

+           { this.props.data.urls ?

+           <li>

+             <FormattedMessage

+               {...messages.zoom_tip1}

+               />

+             <ul>

+               { this.props.data.urls.map(function(url, index) {

+                 return (

+                   <li key={index}><a href={url}>

+                     {url.replace(/^https?:\/\//, '')}

+                   </a></li>);

+                 }) }

+             </ul>

+           </li>

+           : null }

+           <li>

+             <FormattedMessage

+               {...messages.zoom_tip2}

+               />

+             <ul style={{listStyleType: "none"}}>

+               <li className="media">

+                 <div className="media-left media-middle">

+                   <a href={this.props.data.author.url} target="_blank">

+                     <img className="square-32 img-circle"

+                          src={this.props.data.author.avatar} />

+                   </a>

+                 </div>

+                 <div className="media-body">

+                   <a href={this.props.data.author.url} target="_blank">

+                     {this.props.data.author.name}

+                   </a>

+                 </div>

+               </li>

+             </ul>

+           </li>

+           <li>

+             <FormattedMessage

+               {...messages.zoom_tip3}

+               />

+             <ul style={{listStyleType: "none"}}>

+               <a href={this.props.data.context_url}>

+                 {this.props.data.context_url.replace(/^https?:\/\//, '')}

+               </a>

+             </ul>

+           </li>

+         </ol>

+       </div>

+     );

+   }

+ }

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 (

+       <div>

+         { this.props.showdate ?

+             <div className="date text-uppercase">

+               <FormattedRelative value={new Date(this.props.request.date * 1000)} />

+             </div>

+           : ""

+         }

+         {requestView}

+       </div>

+     );

+   }

+ }

+ Request.defaultProps = {

+     onZoomRequest: null,

+ };

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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"

Maybe lowercase "help"?

+   },

+ });

+ 

+ 

+ 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 = (

+         <p className="no-request">

+           <FormattedMessage {...messages.no_request} />

+         </p>

+         );

+     } else {

+       var previousDate = null;

+       requestNodes = this.state.requests.map(function(request, idx) {

+         var showDate = (request.date !== previousDate);

+         previousDate = request.date;

+         return (

+           <Request

+             request={request}

+             showdate={showDate}

+             onZoomRequest={this.zoomOnRequest}

+             key={idx} />

+         );

+       }.bind(this));

+     }

+     var hubs = this.props.hubs.map(function(hub) {

+       return {

+         name: hub,

+         active: (this.state.inactiveHubs.indexOf(hub) === -1)

+       }

+     }.bind(this));

+     return (

+       <IntlProvider locale={navigator.language}>

+         <div className="widget-halp">

+           <HubsList

+             hubs={hubs}

+             onHubClick={this.onHubClick}

+             />

+           { this.state.loading ?

+               <img className="center-block" src={this.props.urls.spinner} />

+             :

+               <div className="requests">

+                 {requestNodes}

+               </div>

+           }

+           <div className="view-more text-muted">

+             <button className="btn btn-secondary" onClick={this.openAllRequests}>

+               <FormattedMessage {...messages.view_more} />

+               &nbsp; <span className="fa fa-chevron-right" aria-hidden="true"></span>

+             </button>

+           </div>

+           <ModalRequest

+             open={this.state.zoomedRequest !== null}

+             data={this.state.zoomedRequest}

+             title={<FormattedMessage {...messages.zoom_title} />}

+             onHidden={this.onZoomClose} />

+           <ModalAllRequests

+             open={this.state.allRequestsOpen}

+             onHidden={this.onAllRequestsClose.bind(this)}

+             onZoomRequest={this.zoomOnRequest}

+             title={<FormattedMessage {...messages.search_title} />}

+             urls={this.props.urls}

+             hubs={hubs}

+             />

+         </div>

+       </IntlProvider>

+     );

+   }

+ }

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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 = (

+         <div className="media-right media-middle">

+           <button className="btn btn-secondary btn-sm" onClick={openCallback}>

+             <span className="fa fa-chevron-right" aria-hidden="true"></span>

+           </button>

+         </div>

+       );

+     }

+     return (

+       <div className="request row">

+         <div className="hub col-sm-6">

+           <span>@{request.hub}</span>

+         </div>

+         <div className="channel col-sm-6">

+           {request.channel}

+         </div>

+         <div className="text col-sm-12">

+           { request.text ?

+             <Linkify>"{request.text}"</Linkify>

+           :

+             <em><FormattedMessage {...messages.req_no_text } /></em>

+           }

+         </div>

+         { request.url ?

+         <div className="url col-sm-12">

+           <a href={request.url}>

+             {request.url.replace(/^https?:\/\//, '')}

+           </a>

+         </div>

+         : null }

+         <div className="author media col-sm-12">

+           <div className="media-left media-middle">

+             <a href={request.author.url} target="_blank">

+               <img className="media-object square-32 img-circle"

+                    src={request.author.avatar} />

+             </a>

+           </div>

+           <div className="media-body">

+             <a href={request.author.url}>

+               {request.author.name}

+             </a>

+           </div>

+           {openButton}

+         </div>

+       </div>

+     );

+ }

+ 

+ 

+ // vim: set ts=2 sw=2 et:

@@ -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": {

@@ -10,6 +10,7 @@ 

      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.js')

      },

      output: {

          path: PATHS.build,

file modified
+168
@@ -451,6 +451,170 @@ 

  }

  

  

+ /*

+  * 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;

+ }

+ 

+ /* 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 **/

  

  /** tightening up card headers - they are too fat imho **/
@@ -470,3 +634,7 @@ 

  .card .panel-footer {

    padding-bottom: 2rem; /* fixes request new meeting button padding on the upcoming meeting widget */

  }

+ 

+ 

+ 

+ /* vim: set ts=2 sw=2 et: */

empty or binary file added
file modified
+27
@@ -6,6 +6,7 @@ 

  import os

  

  import munch

+ from flask import template_rendered

  from os.path import dirname

  import unittest

  
@@ -155,3 +156,29 @@ 

          self.avatar = 'avatar_src_url'

          self.nickname = username

          self.username = username

+ 

+ 

+ @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):

+         recorded.append((template, context))

+     template_rendered.connect(record, app)

+     try:

+         yield recorded

+     finally:

+         template_rendered.disconnect(record, app)

@@ -83,7 +83,7 @@ 

              "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)))

  

file modified
+2 -2
@@ -32,7 +32,7 @@ 

  

          # 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 @@ 

  

          # 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)

@@ -0,0 +1,447 @@ 

+ 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

+ 

+ 

+ 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:

+             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"], ['fedora-devel'])

+ 

+     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"], {

+             'hubs': ['fedora-devel'],

+             'per_page': 3,

+             })

+         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']

+         })

+         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):

+     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_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)

+         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)

+ 

+ 

+ class PaginateTestCase(WidgetTest):

+ 

+     def setUp(self):

+         super(PaginateTestCase, self).setUp()

+         from hubs.widgets.halp.utils import paginate  # circular

Very often circular imports indicate that two modules should be combined, or that something they both use should be a third module. I recommend considering what this circular import might be telling you about an organization issue.

+         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,

+             })

@@ -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 |

+         H4sIAAAAAAAAA+2b+4+qypbH/xXj/HJv9rbl6WMn94dGQUXF5lUo0zedgkJBngK2j5v9v88CtR/7

+         9jmTM3MmOZmYdNJUQa1aa7Gq/HwF/9XE+WYfe0lZNH/8q+ni0tukeeBB6z//+b3ppkmJg+TaIl5U

+         4uaPXoejKOqB+t70EtL8QXN9iqG6dIeuujZ5us886F7jqPC+N2OvGlKNTtLy5Vf7VV+G3RBvPvaU

+         aRa4H9r7wsuvzTQnXt78Aa4UbvN78/PYDA7Bn+/NPD0UL5mXv1x6GP57syhxXlbO9vosx16dfZsI

+         7G4e1h5Jc5zl6dZzywf4Tx5izyudtKz/B8nmISi9+MH3oqwJ07259bNK1D4B8/zFieLiBT68xF5R

+         XB2E7Hp5GayDKgkQwkynyplODzSTyEYgDDUkm7ppyyolmVp9jioHIS3rJkI4VIaqQWibkljdlAWb

+         ioYqSjlCaZplZrPnxKEzGY/trRFKmmbaA9OUWJuRBE9UOqqZDWyKt9RQspEoFShSpwaSzoRSYDT8

+         IUnWlmRq0FJHQ5LynKiGNlYNCWmmzBmmjxzTPlsRKZxYEjXkK2qErueV1DD7uoPsfLVVpvgxk7VQ

+         5ldb+eBI4dlc+uJz4oXSQo3JAoWy4NLawEFZYYXuEfII8wmKaghXaxLMRnBtLfIXTkhrizERTRSJ

+         g8jWzKUgPSeEzkrwRSej+WnF+lMcSjlYz13Lz7Hp8uQsDFRakhC1OmlxGSDmSJuVVSkbL5aRbYnv

+         /j0n5tKcVp7DXANF8kUkmgfD1GxC9U19GSnayD0YyH61RM3UDUHRpIg14kz2TFq0z4KC4j7kGdV5

+         hrx9zPRjJjiSbf5epn8vz8/Jb2V6EE/Oeuz/W6Z/L89gbWkr6vlDpi16O6dt1kZkSmJtODcmUwsR

+         bo4yU136cyeUX21TkwnKSgR5nFNZuqJ8uFYqnxMUZ6F6RsyKobeeeOyoS+0auZIjMdsbW2mrj44Z

+         5FYjj1lYRWZLBBGKNpQl8fUR4Y0RXd+V5+TX+2IuUTXf3kAK3IlMqGr8Y4VjSh7hkZYOomw8XwoB

+         tC81ESvgm5aQrWNER+hZzMVjCdaxFyoDJ8kEI5QTzEopZiKoBvegXtaLoJr9AY4kgTDExtR8atOR

+         BFkonhPX4nWHFSLI0dwe9FUvlCc6rc0WSJFgLe6wdfRdSlq4YTSaU8oAxzJD4lJaLAXORf4efMJm

+         TM9UM3p6TuaP2ZwYQqAPSTCjiYljP7RF9TSjkKSZ5dEeIRmF0VSPJROLR81B2lhPBNYaRaotzo8L

+         w37C44zVJP8Iq17KEDbt0Bwqu0FIMm0o8TZanVVWecVDzVJCX7aSiEMU3bEpLZ0jNJobtj9lxJNq

+         iLQ+UvbTMxFWbAQrC3YeFipDWJl92F3sLeQi0aq9wpRyjRKPyNIkQktDaAuEOlY7j62jTFZRFCiS

+         lGgmPwAbS31UWs8JHs3PmNWQI/ID2MNCZySXKIH7LMlQlcpAe8ywJaaHFSLYlcKTthUw7E7hpYJ9

+         TRHRyKYl5KBHGqwNStsRj7IiZfL07Itzi+b0kWRZon2E6FnwNUQmESDCcBBGrEnJHSQqA8/qC8gS

+         D2os5mYYyRjZe8hbRCJkrnjTjEKVKegZBRXA+MnKknLYNRWw5sPdH8KKmt5WlIXUqWodLSTSmsuS

+         YbUroUjSnhOIjtXDvuaFSDNMkapzZaKBJ9ITZNEjl86Wzoj3NRGtDFNG1/NDLNJPUBkWPmT16oLd

+         fwC7pTU5uaOjPaclThtfKtaI7IG6VEw1vqyY+TKDikYqzHa4WoMdiB5blTVGXhkxr5iGzz4ng1C7

+         7TwH2Hlgb82WZqJt57HC6XEmwMr5ZT0omoZgbw2rPZgUBElg9Yi1c8Q/JxbyBxf/DlPLgJVtVTsR

+         n9hMKShDSbDR5LCQ/AmmEQuz2Zpk6/PREXwnrAorSzdL1jLlAXykTaDelkSGnZGt/dsKar0WYdes

+         9tNLZIrhmsTQYRZNkiQ1lDUEtaeIZKCaIW8nIuzVsoytaAE7kujnRqy9zsVMNJb2q4GijqqXoj2E

+         CMeCAFUxnNJ+vDorUIdSbpzJkojFWQ0jDvI2M7Z+irfKwg61HHyL5C2spqF6fuRW4ZzWKXuhUURf

+         xZqB6Kg0DG2yWkoqltypzgoZ3soDN1KWsIoFdyshBUmxc45sT5RM+JShQ8qhNNtcbk7zkXgyh+ZJ

+         F49QGcLZkaLBjEUr86zstZEk4yTaQ1UbaJOtXFZ40q0SeY9pzQMzE0Gk2uA3aGHzj388J4BFQfNH

+         t9cF+Co2FdXhsgRS87wa8TRc+jhJgIoAUfR0X/penryMgAAjL665xU1JmnhRBJdwAH2bIIrTHIil

+         C+dI5PkxTiqQ+t5cH7wgrnAMsGe7TnOnmoHpQaOIg9KHi4CxtgcHOuEgxJH3Coffm1GQ7I9xSmqU

+         g3YMDhKYu8NVx0ccB1EUpDAJjIrDNKonZIEFgzwIYbYK5zY4ivCmmoOFq84pAUyDRqdiMR8H+edY

+         y3zvfQzl0n7z+dq8en1tVX5fDq+eXxqfHLx0Xf26NN49q9oXdxJIJxDff1wIs3WlyWZF1IDXUe0r

+         JKWiwufmOD00yrThJdiJvEaZB3EjTRqndJ83dH3YIHnw6j03G4d0H5GG4zVwY5OmpCHVthtzvMFn

+         MNUA3A3cyGvW6faSPeSX63QqZ13w9ZZzYGC4hS8XMO52WY5jH6ifFbbXLl5QHC6XRH2waPyNoehu

+         i2JbdPfvMDQ9JDWMv4V8JeoqVLrT6zUmiZvGWeSVXuNvCQRReoDhENXfGwNICmBxI103JKYDA/d5

+         lSG/LLPiR7t94+7PSA6Q3v6cwva7Q3CmcNOH944W3XmgqObPehm8BCBJmvXJNdfxOkyfb5G+x7S4

+         Pt9v9YjTb7Eus4YU4D6DK4eKYJPgcl9VS5PQE0ksZ5Fif6PMTB4U5qQrrIan4d7TS9tPnd0x7gdr

+         hzqu095+GnS6+6cJP6SMbrEcctPe6szuXXbKaKsQtr2eRzaL8rGnyUtKOvRjc7v2x4/a/mRact7m

+         N334zMo0EqfrDraWSfvszWfGhJ+cxyG/2AGTYv3J1HNZGYnL58Q3Z/vs8ajqfs4dXHVo2OZlEyig

+         YlzvJcFxHQIucZLG1XJ9O/UK+qUu4yb10Hngr8UAtyjOPhfE+339wzrp5/e77rnrnrvuueueu+65

+         65677rnrnv9j3cP2vtY9mOD4UEsKp6RqOQH/SVomQbLFdf+7PICT23B/ERvcVQrQFfoH0QbXp2+S

+         pVIoDiiZ/eUwOMcp+PAmCWim+kI4Bya/qJksx6/11+lU9fVwQtK4+hL50kydE9lXbF43Cj+o53yX

+         E53qS2yA96K8jjiyMPWpPnHTP91P8uca8kWXfIr2N/TQNeab0LlEe2tdAr62rjFfW5ewvxBDb7Ff

+         m7fwL82PGbj2vCXh1q7z8JWy+pSNS8ctIf+d8GoxX0ovw/dAPuVhfQ1oFRw3Djgpi0qN4SJsgFps

+         VEzbWOdp3NAziOR7Y4adoqFPRkUDgmkMxWsDhmRpto8AeRugsBsljsLKapYGtUUflxdb+/ra+G3a

+         zEuzz6qNptg31Xa9Q59UW6fPsJ3e16qN6TQeo8zHDc3DBAwWRWP+Jj5v8u3d6HXYWxL+x7qsxbwr

+         s057zXRauHKjld/cuF34rtc6Lbr/pV5zGYencJdqsSxZtzjWpVoOx9Otfp90KabH9gjX/0Wvvebt

+         k8OzvY44HSzQ2bGD46icPfmHMjTm/O4crmxN1gt1OenMtK6UtbX9mVb1jDzNfeMx2b4uvqUrk2zi

+         Wdt4TkSf4ba56Y3Vjr45KYEUjjq55u+P+pr1WJ0r88fNmDtryyf3cZ2/fhMWrxH7KHP9drB6FMfO

+         q7x5QqH4tPE66nMy2eWUtdzbYqFMrHy22zGPf5peu5XCXa/d9dpdr9312l2v3fXaXa/d9dpfWK+x

+         Hf5rvSbgY432oG4GEa6wDrTUQp/McVFUDAQiC4D9dhUgsnvc1df4XkCC6j0guMJPDzgn6/r89fil

+         boT7oojxPqo1FqB2udt7XvKyCRJQPNxF7ry8vNTT7/a4olHQlbmfngDOq+ddeRaEl5eNijJNvNOV

+         0y+Nl/qSEoM8SdPaqbK4yLlTGNb2b4KN+yTYbq5fHzu9+XjTO5eJvpA3aVEwIKNevS+FzSc10eu+

+         qYmrwU9qgqcZnvn6GdDTQtfFxkQb3DREg35gPj4GejN3HXClzD+iIt4CedcPXDuDXu8lyN2Xm0sw

+         8bty4Fp090vlQK9dmup2uq1ut9dtcbyHQTSAkHBdzLiu58Aexf76pMczhdftbrW10iLtURtjPR5B

+         URbmypWKUXgSqCwZG742XKHXJD0oy7gtjoJxuOUV+5zoAqwlIWaUjaORWYUL2mTdNzNfXnj7g0Kv

+         VoijBqFeGh1HN/rUhDY9MXekWMLuTIePIW6jiUFvnRS8OncfPT/femO+u3S0LiyneArn8GjNqhRP

+         YrTlnD/vSc/ttt+Vw1053JXDXTnclcNdOdyVw105/IWVA/Nbb7gRDH1s9U6ak+YeTvpsDe9bHx9C

+         rzikef1qWvWMp6wIOr80kqA8x9hPK2Ku3jC7EfrnN8pq29enJe/Wrx2fJrj23aa4PRx5n+TfMR4n

+         ReBE3u++H/aJ5atX364s/8HyJ57neJqjul/yvBUkJD0UDSvN62cTo+qnGx94/rPJ6yDmgW0AN5b7

+         4g9g/S+BfWT7w8WJl8PFiZf69yMf2b76pckXbM9gj2Y4impxmADb4w7b6nmYbfUdtk/zXYbqU94v

+         bK+lbblcBPYsgo/N3VIWnOV5dZ7mibVzKLoc9vvSaZn5u1mnPy+mvsHSO+ZcRnOW9uU4dGZu6eRL

+         e29qigIAGZfDKU6YwQrZpxl/0D1Fd1LuGz14pSyyOFLHUDjKJz/ivce2/+3gKru0HJ5fy6TfM0/q

+         QXBW64CT1Rk2fEAXUjpOONfgI9p5enTs9slT/zS2v5XAne3vbH9n+zvb39n+zvZ3tr+z/V+a7enf

+         YfveJ/ju/kre/Afs5j8zd/XjlL8o2B+C0m8ECXBbFAHRNQCMX+IiaLxeDitA27vlR/zn++/vBb25

+         /Av8UyxL/f+Df6dLvJ7nMS2P5egWxzq9Vp/vMS3So/vemqF5hia/wP/G2jHRXl0gYTUdbcUndPTm

+         /Vmne0bbdluhe0/lbNfrL+Oz+SqPJ/1AOR8VM15MnGI4eCpzSc4mtNxec2a/mD8nxVJ6GqPlWvkW

+         mABEE13MhqtVZ+Yrw91Ul2ws2x2XI5vDyXpqD0+Z2VG4NpvIdKS0d0e2bLe3JHF7u1NwhPU176X7

+         vhPE8n6UhSZj9YTiT4T/Swn8b+D/n9VgqEsouZ//BWcXbfcxQAAA

+     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

@@ -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

@@ -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

@@ -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 |

+         H4sIAAAAAAAAA+2b+4+qypbH/xXj/HJv9rbl6WMn94dGQUXF5lUo0zedgkJBngK2j5v9v88CtR/7

+         9jmTM3MmOZmYdNJUQa1aa7Gq/HwF/9XE+WYfe0lZNH/8q+ni0tukeeBB6z//+b3ppkmJg+TaIl5U

+         4uaPXoejKOqB+t70EtL8QXN9iqEZju5VXZs83WcedK9xVHjfm7FXDalGJ2n58qv9qi/Dbog3H3vK

+         NAvcD+194eXXZpoTL2/+AFcKt/m9+XlsBofgz/dmnh6Kl8zLXy49DP+9WZQ4Lytne32W612dfZsI

+         7G4e1h5Jc5zl6dZzywf4Tx5izyudtKz/B8nmISi9+MH3oqwJ07259bNK1D4B8/zFieLiBT68xF5R

+         XB2E7Hp5GayDKgkQwkynyplODzSTyEYgDDUkm7ppyyolmVp9jioHIS3rJkI4VIaqQWibkljdlAWb

+         ioYqSjlCaZplZrPnxKEzGY/trRFKmmbaA9OUWJuRBE9UOqqZDWyKt9RQspEoFShSpwaSzoRSYDT8

+         IUnWlmRq0FJHQ5LynKiGNlYNCWmmzBmmjxzTPlsRKZxYEjXkK2qErueV1DD7uoPsfLVVpvgxk7VQ

+         5ldb+eBI4dlc+uJz4oXSQo3JAoWy4NLawEFZYYXuEfII8wmKaghXaxLMRnBtLfIXTkhrizERTRSJ

+         g8jWzKUgPSeEzkrwRSej+WnF+lMcSjlYz13Lz7Hp8uQsDFRakhC1OmlxGSDmSJuVVSkbL5aRbYnv

+         /j0n5tKcVp7DXANF8kUkmgfD1GxC9U19GSnayD0YyH61RM3UDUHRpIg14kz2TFq0z4KC4j7kGdV5

+         hrx9zPRjJjiSbf5epn8vz8/Jb2V6EE/Oeuz/W6Z/L89gbWkr6vlDpi16O6dt1kZkSmJtODcmUwsR

+         bo4yU136cyeUX21TkwnKSgR5nFNZuqJ8uFYqnxMUZ6F6RsyKobeeeOyoS+0auZIjMdsbW2mrj44Z

+         5FYjj1lYRWZLBBGKNpQl8fUR4Y0RXd+V5+TX+2IuUTXf3kAK3IlMqGr8Y4VjSh7hkZYOomw8XwoB

+         tC81ESvgm5aQrWNER+hZzMVjCdaxFyoDJ8kEI5QTzEopZiKoBvegXtaLoJr9AY4kgTDExtR8atOR

+         BFkonhPX4nWHFSLI0dwe9FUvlCc6rc0WSJFgLe6wdfRdSlq4YTSaU8oAxzJD4lJaLAXORf4efMJm

+         TM9UM3p6TuaP2ZwYQqAPSTCjiYljP7RF9TSjkKSZ5dEeIRmF0VSPJROLR81B2lhPBNYaRaotzo8L

+         w37C44zVJP8Iq17KEDbt0Bwqu0FIMm0o8TZanVVWecVDzVJCX7aSiEMU3bEpLZ0jNJobtj9lxJNq

+         iLQ+UvbTMxFWbAQrC3YeFipDWJl92F3sLeQi0aq9wpRyjRKPyNIkQktDaAuEOlY7j62jTFZRFCiS

+         lGgmPwAbS31UWs8JHs3PmNWQI/ID2MNCZySXKIH7LMlQlcpAe8ywJaaHFSLYlcKTthUw7E7hpYJ9

+         TRHRyKYl5KBHGqwNStsRj7IiZfL07Itzi+b0kWRZon2E6FnwNUQmESDCcBBGrEnJHSQqA8/qC8gS

+         D2os5mYYyRjZe8hbRCJkrnjTjEKVKegZBRXA+MnKknLYNRWw5sPdH8KKmt5WlIXUqWodLSTSmsuS

+         YbUroUjSnhOIjtXDvuaFSDNMkapzZaKBJ9ITZNEjl86Wzoj3NRGtDFNG1/NDLNJPUBkWPmT16oLd

+         fwC7pTU5uaOjPaclThtfKtaI7IG6VEw1vqyY+TKDikYqzHa4WoMdiB5blTVGXhkxr5iGzz4ng1C7

+         7TwH2Hlgb82WZqJt57HC6XEmwMr5ZT0omoZgbw2rPZgUBElg9Yi1c8Q/JxbyBxf/DlPLgJVtVTsR

+         n9hMKShDSbDR5LCQ/AmmEQuz2Zpk6/PREXwnrAorSzdL1jLlAXykTaDelkSGnZGt/dsKar0WYdes

+         9tNLZIrhmsTQYRZNkiQ1lDUEtaeIZKCaIW8nIuzVsoytaAE7kujnRqy9zsVMNJb2q4GijqqXoj2E

+         CMeCAFUxnNJ+vDorUIdSbpzJkojFWQ0jDvI2M7Z+irfKwg61HHyL5C2spqF6fuRW4ZzWKXuhUURf

+         xZqB6Kg0DG2yWkoqltypzgoZ3soDN1KWsIoFdyshBUmxc45sT5RM+JShQ8qhNNtcbk7zkXgyh+ZJ

+         F49QGcLZkaLBjEUr86zstZEk4yTaQ1UbaJOtXFZ40q0SeY9pzQMzE0Gk2uA3aGHzj388J4BFQfNH

+         t9cF+Co2FdXhsgRS87wa8TRc+jhJgIoAUfR0X/penryMgAAjL665xU1JmnhRBJdwAH2bIIrTHIil

+         C+dI5PkxTiqQ+t5cH7wgrnAMsGe7TnOnmoHpQaOIg9KHi4CxtgcHOuEgxJH3Coffm1GQ7I9xSmqU

+         g3YMDhKYu8NVx0ccB1EUpDAJjIrDNKonZIEFgzwIYbYK5zY4ivCmmoOFq84pAUyDRqdiMR8H+edY

+         y3zvfQzl0n7z+dq8en1tVX5fDq+eXxqfHLx0Xf26NN49q9oXdxJIJxDff1wIs3WlyWZF1IDXUe0r

+         JKWiwufmOD00yrThJdiJvEaZB3EjTRqndJ83dH3YIHnw6j03G4d0H5GG4zVwY5OmpCHVthtzvMFn

+         MNUA3A3cyGvW6faSPeSX63QqZ13w9ZZzYGC4hS8XMO52WY5jH6ifFbbXLl5QHC6XRH2waPyNoehu

+         i2JbdPfvMDQ9JDWMv4V8JeoqVLrT6zUmiZvGWeSVXuNvCQRReoDhENXfGwNICmBxI103JKYDA/d5

+         lSG/LLPiR7t94+7PSA6Q3v6cwva7Q3CmcNOH944W3XmgqObPehm8BCBJmvXJNdfxOkyfb5G+x7S4

+         Pt9v9YjTb7Eus4YU4D6DK4eKYJPgcl9VS5PQE0ksZ5Fif6PMTB4U5qQrrIan4d7TS9tPnd0x7gdr

+         hzqu095+GnS6+6cJP6SMbrEcctPe6szuXXbKaKsQtr2eRzaL8rGnyUtKOvRjc7v2x4/a/mRact7m

+         N334zMo0EqfrDraWSfvszWfGhJ+cxyG/2AGTYv3J1HNZGYnL58Q3Z/vs8ajqfs4dXHVo2OZlEyig

+         YlzvJcFxHQIucZLG1XJ9O/UK+qUu4yb10Hngr8UAtyjOPhfE+339wzrp5/e77rnrnrvuueueu+65

+         65677rnrnv9j3cP2vtY9mOD4UEsKp6RqOQH/SVomQbLFdf+7PICT23B/ERvcVQrQFfoH0QbXp2+S

+         pVIoDiiZ/eUwOMcp+PAmCWim+kI4Bya/qJksx6/11+lU9fVwQtK4+hL50kydE9lXbF43Cj+o53yX

+         E53qS2yA96K8jjiyMPWpPnHTP91P8uca8kWXfIr2N/TQNeab0LlEe2tdAr62rjFfW5ewvxBDb7Ff

+         m7fwL82PGbj2vCXh1q7z8JWy+pSNS8ctIf+d8GoxX0ovw/dAPuVhfQ1oFRw3Djgpi0qN4SJsgFps

+         VEzbWOdp3NAziOR7Y4adoqFPRkUDgmkMxWsDhmRpto8AeRugsBsljsLKapYGtUUflxdb+/ra+G3a

+         zEuzz6qNptg31Xa9Q59UW6fPsJ3e16qN6TQeo8zHDc3DBAwWRWP+Jj5v8u3d6HXYWxL+x7qsxbwr

+         s057zXRauHKjld/cuF34rtc6Lbr/pV5zGYencJdqsSxZtzjWpVoOx9Otfp90KabH9gjX/0Wvvebt

+         k8OzvY44HSzQ2bGD46icPfmHMjTm/O4crmxN1gt1OenMtK6UtbX9mVb1jDzNfeMx2b4uvqUrk2zi

+         Wdt4TkSf4ba56Y3Vjr45KYEUjjq55u+P+pr1WJ0r88fNmDtryyf3cZ2/fhMWrxH7KHP9drB6FMfO

+         q7x5QqH4tPE66nMy2eWUtdzbYqFMrHy22zGPf5peu5XCXa/d9dpdr9312l2v3fXaXa/d9dpfWK+x

+         Hf5rvSbgY432oG4GEa6wDrTUQp/McVFUDAQiC4D9dhUgsnvc1df4XkCC6j0guMJPDzgn6/r89fil

+         boT7oojxPqo1FqB2udt7XvKyCRJQPNxF7ry8vNTT7/a4olHQlbmfngDOq+ddeRaEl5eNijJNvNOV

+         0y+Nl/qSEoM8SdPaqbK4yLlTGNb2b4KN+yTYbq5fHzu9+XjTO5eJvpA3aVEwIKNevS+FzSc10eu+

+         qYmrwU9qgqcZnvn6GdDTQtfFxkQb3DREg35gPj4GejN3HXClzD+iIt4CedcPXDuDXu8lyN2Xm0sw

+         8bty4Fp090vlQK9dmup2uq1ut9dtcbyHQTSAkHBdzLiu58Aexf76pMczhdftbrW10iLtURtjPR5B

+         URbmypWKUXgSqCwZG742XKHXJD0oy7gtjoJxuOUV+5zoAqwlIWaUjaORWYUL2mTdNzNfXnj7g0Kv

+         VoijBqFeGh1HN/rUhDY9MXekWMLuTIePIW6jiUFvnRS8OncfPT/femO+u3S0LiyneArn8GjNqhRP

+         YrTlnD/vSc/ttt+Vw1053JXDXTnclcNdOdyVw105/IWVA/Nbb7gRDH1s9U6ak+YeTvpsDe9bHx9C

+         rzikef1qWvWMp6wIOr80kqA8x9hPK2Ku3jC7EfrnN8pq29enJe/Wrx2fJrj23aa4PRx5n+TfMR4n

+         ReBE3u++H/aJ5atX364s/8HyJ57neJqjul/yvBUkJD0UDSvN62cTo+qnGx94/rPJ6yDmgW0AN5b7

+         4g9g/S+BfWT7w8WJl8PFiZf69yMf2b76pckXbM9gj2Y4impxmADb4w7b6nmYbfUdtk/zXYbqU94v

+         bK+lbblcBPYsgo/N3VIWnOV5dZ7mibVzKLoc9vvSaZn5u1mnPy+mvsHSO+ZcRnOW9uU4dGZu6eRL

+         e29qigIAGZfDKU6YwQrZpxl/0D1Fd1LuGz14pSyyOFLHUDjKJz/ivce2/+3gKru0HJ5fy6TfM0/q

+         QXBW64CT1Rk2fEAXUjpOONfgI9p5enTs9slT/zS2v5XAne3vbH9n+zvb39n+zvZ3tr+z/V+a7enf

+         YfveJ/ju/kre/Afs5j8zd/XjlL8o2B+C0m8ECXBbFAHRNQCMX+IiaLxeDitA27vlR/zn++/vBb25

+         /Av8UyxL/f+Df6dLvJ7nMS2P5egWxzq9Vp/vMS3So/vemqF5hia/wP/G2jHRXl0gYTUdbcUndPTm

+         /Vmne0bbdluhe0/lbNfrL+Oz+SqPJ/1AOR8VM15MnGI4eCpzSc4mtNxec2a/mD8nxVJ6GqPlWvkW

+         mABEE13MhqtVZ+Yrw91Ul2ws2x2XI5vDyXpqD0+Z2VG4NpvIdKS0d0e2bLe3JHF7u1NwhPU176X7

+         vhPE8n6UhSZj9YTiT4T/Swn8b+D/n9VgqEsouZ//BcHxihwxQAAA

+     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

@@ -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

@@ -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

@@ -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 |

+         H4sIAAAAAAAAA+2b+4+qypbH/xXj/HJv9rbl6WMn94dGQUXF5lUo0zedgkJBngK2j5v9v88CtR/7

+         9jmTM3MmOZmYdNJUQa1aa7Gq/HwF/9XE+WYfe0lZNH/8q+ni0tukeeBB6z//+b3ppkmJg+TaIl5U

+         4uaPXoejKOqB+t70EtL8QXN9iqFpttOrujZ5us886F7jqPC+N2OvGlKNTtLy5Vf7VV+G3RBvPvaU

+         aRa4H9r7wsuvzTQnXt78Aa4UbvN78/PYDA7Bn+/NPD0UL5mXv1x6GP57syhxXlbO9vos1706+zYR

+         2N08rD2S5jjL063nlg/wnzzEnlc6aVn/D5LNQ1B68YPvRVkTpntz62eVqH0C5vmLE8XFC3x4ib2i

+         uDoI2fXyMlgHVRIghJlOlTOdHmgmkY1AGGpINnXTllVKMrX6HFUOQlrWTYRwqAxVg9A2JbG6KQs2

+         FQ1VlHKE0jTLzGbPiUNnMh7bWyOUNM20B6YpsTYjCZ6odFQzG9gUb6mhZCNRKlCkTg0knQmlwGj4

+         Q5KsLcnUoKWOhiTlOVENbawaEtJMmTNMHzmmfbYiUjixJGrIV9QIXc8rqWH2dQfZ+WqrTPFjJmuh

+         zK+28sGRwrO59MXnxAulhRqTBQplwaW1gYOywgrdI+QR5hMU1RCu1iSYjeDaWuQvnJDWFmMimigS

+         B5GtmUtBek4InZXgi05G89OK9ac4lHKwnruWn2PT5clZGKi0JCFqddLiMkDMkTYrq1I2Xiwj2xLf

+         /XtOzKU5rTyHuQaK5ItINA+GqdmE6pv6MlK0kXswkP1qiZqpG4KiSRFrxJnsmbRonwUFxX3IM6rz

+         DHn7mOnHTHAk2/y9TP9enp+T38r0IJ6c9dj/t0z/Xp7B2tJW1POHTFv0dk7brI3IlMTacG5MphYi

+         3Bxlprr0504ov9qmJhOUlQjyOKeydEX5cK1UPicozkL1jJgVQ2898dhRl9o1ciVHYrY3ttJWHx0z

+         yK1GHrOwisyWCCIUbShL4usjwhsjur4rz8mv98Vcomq+vYEUuBOZUNX4xwrHlDzCIy0dRNl4vhQC

+         aF9qIlbANy0hW8eIjtCzmIvHEqxjL1QGTpIJRignmJVSzERQDe5BvawXQTX7AxxJAmGIjan51KYj

+         CbJQPCeuxesOK0SQo7k96KteKE90WpstkCLBWtxh6+i7lLRww2g0p5QBjmWGxKW0WAqci/w9+ITN

+         mJ6pZvT0nMwfszkxhEAfkmBGExPHfmiL6mlGIUkzy6M9QjIKo6keSyYWj5qDtLGeCKw1ilRbnB8X

+         hv2ExxmrSf4RVr2UIWzaoTlUdoOQZNpQ4m20Oqus8oqHmqWEvmwlEYcoumNTWjpHaDQ3bH/KiCfV

+         EGl9pOynZyKs2AhWFuw8LFSGsDL7sLvYW8hFolV7hSnlGiUekaVJhJaG0BYIdax2HltHmayiKFAk

+         KdFMfgA2lvqotJ4TPJqfMashR+QHsIeFzkguUQL3WZKhKpWB9phhS0wPK0SwK4UnbStg2J3CSwX7

+         miKikU1LyEGPNFgblLYjHmVFyuTp2RfnFs3pI8myRPsI0bPga4hMIkCE4SCMWJOSO0hUBp7VF5Al

+         HtRYzM0wkjGy95C3iETIXPGmGYUqU9AzCiqA8ZOVJeWwaypgzYe7P4QVNb2tKAupU9U6WkikNZcl

+         w2pXQpGkPScQHauHfc0LkWaYIlXnykQDT6QnyKJHLp0tnRHvayJaGaaMrueHWKSfoDIsfMjq1QW7

+         /wB2S2tyckdHe05LnDa+VKwR2QN1qZhqfFkx82UGFY1UmO1wtQY7ED22KmuMvDJiXjENn31OBqF2

+         23kOsPPA3potzUTbzmOF0+NMgJXzy3pQNA3B3hpWezApCJLA6hFr54h/TizkDy7+HaaWASvbqnYi

+         PrGZUlCGkmCjyWEh+RNMIxZmszXJ1uejI/hOWBVWlm6WrGXKA/hIm0C9LYkMOyNb+7cV1Hotwq5Z

+         7aeXyBTDNYmhwyyaJElqKGsIak8RyUA1Q95ORNirZRlb0QJ2JNHPjVh7nYuZaCztVwNFHVUvRXsI

+         EY4FAapiOKX9eHVWoA6l3DiTJRGLsxpGHORtZmz9FG+VhR1qOfgWyVtYTUP1/MitwjmtU/ZCo4i+

+         ijUD0VFpGNpktZRULLlTnRUyvJUHbqQsYRUL7lZCCpJi5xzZniiZ8ClDh5RDaba53JzmI/FkDs2T

+         Lh6hMoSzI0WDGYtW5lnZayNJxkm0h6o20CZbuazwpFsl8h7TmgdmJoJItcFv0MLmH/94TgCLguaP

+         bq8L8FVsKqrDZQmk5nk14mm49HGSABUBoujpvvS9PHkZAQFGXlxzi5uSNPGiCC7hAPo2QRSnORBL

+         F86RyPNjnFQg9b25PnhBXOEYYM92neZONQPTg0YRB6UPFwFjbQ8OdMJBiCPvFQ6/N6Mg2R/jlNQo

+         B+0YHCQwd4erjo84DqIoSGESGBWHaVRPyAILBnkQwmwVzm1wFOFNNQcLV51TApgGjU7FYj4O8s+x

+         lvne+xjKpf3m87V59fraqvy+HF49vzQ+OXjpuvp1abx7VrUv7iSQTiC+/7gQZutKk82KqAGvo9pX

+         SEpFhc/NcXpolGnDS7ATeY0yD+JGmjRO6T5v6PqwQfLg1XtuNg7pPiINx2vgxiZNSUOqbTfmeIPP

+         YKoBuBu4kdes0+0le8gv1+lUzrrg6y3nwMBwC18uYNztshzHPlA/K2yvXbygOFwuifpg0fgbQ9Hd

+         FsW26O7fYWh6SGoYfwv5StRVqHSn12tMEjeNs8grvcbfEgii9ADDIaq/NwaQFMDiRrpuSEwHBu7z

+         KkN+WWbFj3b7xt2fkRwgvf05he13h+BM4aYP7x0tuvNAUc2f9TJ4CUCSNOuTa67jdZg+3yJ9j2lx

+         fb7f6hGn32JdZg0pwH0GVw4VwSbB5b6qliahJ5JYziLF/kaZmTwozElXWA1Pw72nl7afOrtj3A/W

+         DnVcp739NOh0908TfkgZ3WI55Ka91Zndu+yU0VYhbHs9j2wW5WNPk5eUdOjH5nbtjx+1/cm05LzN

+         b/rwmZVpJE7XHWwtk/bZm8+MCT85j0N+sQMmxfqTqeeyMhKXz4lvzvbZ41HV/Zw7uOrQsM3LJlBA

+         xbjeS4LjOgRc4iSNq+X6duoV9Etdxk3qofPAX4sBblGcfS6I9/v6h3XSz+933XPXPXfdc9c9d91z

+         1z133XPXPf/Huoftfa17MMHxoZYUTknVcgL+k7RMgmSL6/53eQAnt+H+Ija4qxSgK/QPog2uT98k

+         S6VQHFAy+8thcI5T8OFNEtBM9YVwDkx+UTNZjl/rr9Op6uvhhKRx9SXypZk6J7Kv2LxuFH5Qz/ku

+         JzrVl9gA70V5HXFkYepTfeKmf7qf5M815Isu+RTtb+iha8w3oXOJ9ta6BHxtXWO+ti5hfyGG3mK/

+         Nm/hX5ofM3DteUvCrV3n4Stl9Skbl45bQv474dVivpRehu+BfMrD+hrQKjhuHHBSFpUaw0XYALXY

+         qJi2sc7TuKFnEMn3xgw7RUOfjIoGBNMYitcGDMnSbB8B8jZAYTdKHIWV1SwNaos+Li+29vW18du0

+         mZdmn1UbTbFvqu16hz6ptk6fqR9nfKXamE7jMcp83NA8TMBgUTTmb+LzJt/ejV6HvSXhf6zLWsy7

+         Muu010ynhSs3WvnNjduF73qt06L7X+o1l3F4CnepFsuSdYtjXarlcDzd6vdJl2J6bI9w/V/02mve

+         Pjk82+uI08ECnR07OI7K2ZN/KENjzu/O4crWZL1Ql5POTOtKWVvbn2lVz8jT3Dcek+3r4lu6Mskm

+         nrWN50T0GW6bm95Y7eibkxJI4aiTa/7+qK9Zj9W5Mn/cjLmztnxyH9f56zdh8RqxjzLXbwerR3Hs

+         vMqbJxSKTxuvoz4nk11OWcu9LRbKxMpnux3z+KfptVsp3PXaXa/d9dpdr9312l2v3fXaXa/9hfUa

+         2+G/1msCPtZoD+pmEOEK60BLLfTJHBdFxUAgsgDYb1cBIrvHXX2N7wUkqN4Dgiv89IBzsq7PX49f

+         6ka4L4oY76NaYwFql7u95yUvmyABxcNd5M7Ly0s9/W6PKxoFXZn76QngvHrelWdBeHnZqCjTxDtd

+         Of3SeKkvKTHIkzStnSqLi5w7hWFt/ybYuE+C7eb69bHTm483vXOZ6At5kxYFAzLq1ftS2HxSE73u

+         m5q4GvykJnia4ZmvnwE9LXRdbEy0wU1DNOgH5uNjoDdz1wFXyvwjKuItkHf9wLUz6PVegtx9ubkE

+         E78rB65Fd79UDvTapalup9vqdnvdFsd7GEQDCAnXxYzreg7sUeyvT3o8U3jd7lZbKy3SHrUx1uMR

+         FGVhrlypGIUngcqSseFrwxV6TdKDsozb4igYh1tesc+JLsBaEmJG2TgamVW4oE3WfTPz5YW3Pyj0

+         aoU4ahDqpdFxdKNPTWjTE3NHiiXsznT4GOI2mhj01knBq3P30fPzrTfmu0tH68JyiqdwDo/WrErx

+         JEZbzvnznvTcbvtdOdyVw1053JXDXTnclcNdOdyVw19YOTC/9YYbwdDHVu+kOWnu4aTP1vC+9fEh

+         9IpDmtevplXPeMqKoPNLIwnKc4z9tCLm6g2zG6F/fqOstn19WvJu/drxaYJr322K28OR90n+HeNx

+         UgRO5P3u+2GfWL569e3K8h8sf+J5jqc5qvslz1tBQtJD0bDSvH42Map+uvGB5z+bvA5iHtgGcGO5

+         L/4A1v8S2Ee2P1yceDlcnHipfz/yke2rX5p8wfYM9miGo6gWhwmwPe6wrZ6H2VbfYfs032WoPuX9

+         wvZa2pbLRWDPIvjY3C1lwVmeV+dpnlg7h6LLYb8vnZaZv5t1+vNi6hssvWPOZTRnaV+OQ2fmlk6+

+         tPempigAkHE5nOKEGayQfZrxB91TdCflvtGDV8oiiyN1DIWjfPIj3nts+98OrrJLy+H5tUz6PfOk

+         HgRntQ44WZ1hwwd0IaXjhHMNPqKdp0fHbp889U9j+1sJ3Nn+zvZ3tr+z/Z3t72x/Z/s72/+l2Z7+

+         HbbvfYLv7q/kzX/Abv4zc1c/TvmLgv0hKP1GkAC3RREQXQPA+CUugsbr5bACtL1bfsR/vv/+XtCb

+         y7/AP8Wy1P8/+He6xOt5HtPyWI5ucazTa/X5HtMiPbrvrRmaZ2jyC/xvrB0T7dUFElbT0VZ8Qkdv

+         3p91ume0bbcVuvdUzna9/jI+m6/yeNIPlPNRMePFxCmGg6cyl+RsQsvtNWf2i/lzUiylpzFarpVv

+         gQlANNHFbLhadWa+MtxNdcnGst1xObI5nKyn9vCUmR2Fa7OJTEdKe3dky3Z7SxK3tzsFR1hf8166

+         7ztBLO9HWWgyVk8o/kT4v5TA/wb+/1kNhrqEkvv5X6HFLz4xQAAA

+     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

@@ -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

@@ -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

@@ -0,0 +1,63 @@ 

+ 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):

+     """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"

+     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

+ 

+     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 = "/<hub>/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)

@@ -0,0 +1,152 @@ 

+ from __future__ import unicode_literals, absolute_import

+ 

+ 

+ import datetime

+ import logging

+ import re

+ import time

+ 

+ import flask

+ import fedmsg.meta

+ import requests

+ from jinja2.utils import urlize

+ 

+ from hubs.widgets.caching import CachedFunction

+ from .utils import find_hubs_for_msg

+ 

+ 

+ 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"

+ 

+     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

+         }

+         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 = find_hubs_for_msg(message['msg'])

+         return bool(set(hubs) & set(self.instance.config.get("hubs", [])))

+ 

+ 

+ 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"],

+         "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('<a href="([^"]+)"[^>]*>', 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: "

+                          "<https://fedorahosted.org/design-team/issue/423> and"

+                          " <https://fedorahosted.org/design-team/issue/424>."),

+                 "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",

+         }

+     },

+ ]

@@ -0,0 +1,22 @@ 

+ {% extends "panel.html" %}

+ 

+ {% block content %}

+ <div id="widget-halp">

+ </div>

+ <script src="{{url_for('static', filename='js/build/Halp.js')}}"></script>

+ <script>

+ (function() {

+     const Widget = React.createElement(Halp.Widget, {

+         hubs: {{ hubs|tojson }},

+         urls: {

+             data: '{{ url_for("halp_data", hub=widget_instance.hub.name, idx=widget_instance.idx) }}',

+             search: '{{ url_for("halp_search", hub=widget_instance.hub.name, idx=widget_instance.idx) }}',

+             requesters: '{{ url_for("halp_requesters", hub=widget_instance.hub.name, idx=widget_instance.idx) }}',

+             spinner: '{{ url_for("static", filename="img/spinner.gif") }}',

+             spinnerCircle: '{{ url_for("static", filename="img/spinner-circle.gif") }}'

+         }

+     });

+     ReactDOM.render(Widget, document.getElementById('widget-halp'));

+ })();

+ </script>

+ {% endblock %}

@@ -0,0 +1,29 @@ 

+ <div id="widget-halp-config">

+   <div class="modal-dialog">

+     <div class="modal-content">

+       <div class="modal-body">

+         <img src="{{ url_for("static", filename="img/spinner.gif") }}"

+              class="center-block" alt="loading..." />

+       </div>

+     </div>

+   </div>

+ </div>

+ 

+ <script src="{{url_for('static', filename='js/build/Halp.js')}}"></script>

+ <script>

+ (function() {

+     const ConfigPanel = React.createElement(Halp.Config, {

+         mode: {{ mode|tojson }},

+         initialState: {{ initial|tojson }},

+         urls: {

+             post: {{ url|tojson }},

+             hubs: '{{ url_for("halp_config_hubs") }}',

+             {% if mode != "add" %}

+             remove: '{{ url_for("widget_edit_delete", hub=widget_instance.hub.name, idx=widget_instance.idx) }}',

+             {% endif %}

+             spinnerCircle: '{{ url_for("static", filename="img/spinner-circle.gif") }}'

+         }

+     });

+     ReactDOM.render(ConfigPanel, document.getElementById('widget-halp-config'));

+ })();

+ </script>

@@ -0,0 +1,80 @@ 

+ from __future__ import unicode_literals

+ 

+ import flask

+ 

+ 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.

+ 

+     Args:

+         msg (dict): the ``msg`` value in the raw message dict sent through

+             Fedmsg.

+     """

+     return [

+         h[0] for h in

+         Hub.query.join(HubConfig).filter(

+             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

@@ -0,0 +1,194 @@ 

+ from __future__ import unicode_literals, absolute_import

+ 

+ import datetime

+ import time

+ 

+ import flask

+ from flask.signals import template_rendered, before_render_template

+ 

+ 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

+ from .utils import find_hubs_for_msg, paginate

+ 

+ 

+ class BaseView(WidgetView):

+     """The base view to instantiate the widget."""

+ 

+     name = "root"

+     url_rules = ["/"]

+     template_name = "halp.html"

+ 

+     def get_context(self, instance, *args, **kwargs):

+         return dict(

+             hubs=instance.config.get("hubs", []),

+             title=self.widget.label,

+             )

+ 

+ 

+ class DataView(WidgetView):

+     """Get the requests in JSON format."""

+ 

+     name = "data"

+     url_rules = ["data"]

+ 

+     def dispatch_request(self, *args, **kwargs):

+         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["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.

+                 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):

+     """Search the requests and return the result as JSON."""

+ 

+     name = "search"

+     url_rules = ["search"]

+     methods = ['GET', 'POST']

+ 

+     def dispatch_request(self, *args, **kwargs):

+         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["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

+             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):

+     """Returns suggestions for the "requesters" filter."""

+ 

+     name = "requesters"

+     url_rules = ["requesters"]

+     MAX_SUGGESTS = 5

+ 

+     def dispatch_request(self, *args, **kwargs):

+         instance = self._get_instance(*args, **kwargs)

+         get_requests = GetRequests(instance)

+         results = set([

+             req["author"]["name"] for req in get_requests()

+             ])

+         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})

+ 

+ 

+ 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]

+     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

@@ -148,3 +148,25 @@ 

          if response.status_code == 200:

              return value

          raise ValueError('Invalid fedorahosted project')

+ 

+ 

+ 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):

+         return [

+             elem.strip() for elem in value.split(",") if elem.strip()

+             ]

+ 

+     @classmethod

+     def to_string(cls, value):

+         if not value:

+             return ""

+         return ", ".join(value)

file modified
+8 -1
@@ -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 @@ 

          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

This is the branch implementing #98 and #325.

The Halp widget is more complex than most widgets : the UI is React-based because it is interactive, it has custom views and a cached function to get the help requests from Datagrepper, and it has a custom config panel (#325). I've made reusable React components where possible. I've also introduced i18n in the JS UI, it's better to do it earlier rather than later.

I recommend adding a docblock here.

I recommend adding docblocks on this class and its methods below.

For PEP-8, flask, fedmsg, and requests should be imported in the second group with jinja below.

I recommend docblocks on this class and all of its methods below.

I recommend removing this commented code.

I recommend documenting the msg argument and its type here.

For PEP-8, the flask above should eb grouped with this flask and should have a space separating it from the builtins.

I recommend docblocks here and on its functions.

I recommend a docblock for this class.

It would be good to document the args and kwargs and what they are used for.

It would be good to document the args and kwargs here too.

This method is a bit long and thus hard to follow. I recommend breaking it up into helper methods.

It would be good to document the args and kwargs.

This method could use a docblock.

This class and its methods could use docblocks.

It seems like there was a lot more Python code added than tests. Is there 100% coverage on the new code? If not, I recommend adding more tests.

All my comments are just suggestions and are at your option. LGTM.

1 new commit added

  • Implement most of the review comments
7 years ago

I've added most of the docblocks, except on view methods where there isn't more to describe than in the parent class. In that case I've just removed the docblock to make it clear.

I'm going to add more unit tests.

6 new commits added

  • Implement most of the review comments and add tests
  • Add tests for the halp widget
  • Map channels in messages to configured channels
  • Make widget views emit the template signals
  • Add the Halp configuration panel
  • Add Halp widget
7 years ago

@bowlofeggs : are you happy with the changes?

Very often circular imports indicate that two modules should be combined, or that something they both use should be a third module. I recommend considering what this circular import might be telling you about an organization issue.

Pull-Request has been merged by abompard

7 years ago
Metadata
Changes Summary 38
+2 -0
file changed
hubs/default_config.py
+6 -0
file changed
hubs/defaults.py
+150
file added
hubs/static/client/app/components/CompletionInput.jsx
+109
file added
hubs/static/client/app/components/Modal.jsx
+226
file added
hubs/static/client/app/widgets/halp/Config.jsx
+4
file added
hubs/static/client/app/widgets/halp/Halp.js
+23
file added
hubs/static/client/app/widgets/halp/Hub.jsx
+43
file added
hubs/static/client/app/widgets/halp/HubsList.jsx
+234
file added
hubs/static/client/app/widgets/halp/ModalAllRequests.jsx
+99
file added
hubs/static/client/app/widgets/halp/ModalRequest.jsx
+40
file added
hubs/static/client/app/widgets/halp/Request.jsx
+182
file added
hubs/static/client/app/widgets/halp/Widget.jsx
+65
file added
hubs/static/client/app/widgets/halp/getRequestView.jsx
+3 -0
file changed
hubs/static/client/package.json
+1 -0
file changed
hubs/static/client/webpack.config.js
+168 -0
file changed
hubs/static/css/style.css
+0
file added
hubs/static/img/spinner-circle.gif
+27 -0
file changed
hubs/tests/__init__.py
+1 -1
file changed
hubs/tests/test_fedora_hubs_flask_api.py
+2 -2
file changed
hubs/tests/test_models.py
+447
file added
hubs/tests/test_widgets/test_halp.py
+98
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpFunctionsTestCase.test_execute
+194
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data
+98
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_data_wrong_hub
+194
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_all
+98
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_date
+98
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_hub
+98
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_meetingname
+98
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_people
+194
file added
hubs/tests/vcr-request-data/hubs.tests.test_widgets.test_halp.HalpViewsTestCase.test_search_requesters
+63
file added
hubs/widgets/halp/__init__.py
+152
file added
hubs/widgets/halp/functions.py
+22
file added
hubs/widgets/halp/templates/halp.html
+29
file added
hubs/widgets/halp/templates/halp_config.html
+80
file added
hubs/widgets/halp/utils.py
+194
file added
hubs/widgets/halp/views.py
+22 -0
file changed
hubs/widgets/validators.py
+8 -1
file changed
hubs/widgets/view.py