From 8c911650e5909464113e1ad9eb468c9f6c3cb555 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 1/32] Issues: Initial commit


---

diff --git a/hubs/default_config.py b/hubs/default_config.py
index 546342a..3d2b33d 100644
--- a/hubs/default_config.py
+++ b/hubs/default_config.py
@@ -73,5 +73,6 @@ WIDGETS = [
     'hubs.widgets.pagureissues:PagureIssues',
     'hubs.widgets.repositories:Repositories',
     'hubs.widgets.sticky:Sticky',
+    'hubs.widgets.issues:Issues',
     'hubs.widgets.workflow.updates2stable:Updates2Stable',
     ]
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
new file mode 100644
index 0000000..616df54
--- /dev/null
+++ b/hubs/widgets/issues/__init__.py
@@ -0,0 +1,102 @@
+from __future__ import unicode_literals
+
+from hubs.widgets import validators
+from hubs.widgets.base import Widget
+from hubs.widgets.caching import CachedFunction
+from hubs.widgets.view import RootWidgetView
+
+import requests
+
+
+class Issues(Widget):
+
+    name = "issues"
+    label = "Issues"
+    position = "left"
+    parameters = [
+        dict(
+            name="github_username",
+            label="Github Username",
+            default="None",
+            validator=validators.Text,
+            help="Github username to show issues of",
+        ),
+        dict(
+            name="pagure_username",
+            label="Pagure Username",
+            default="None",
+            validator=validators.Text,
+            help="Pagure username to show issues of",
+        ),
+        dict(
+            name="bugzilla_username",
+            label="Bugzilla Username",
+            default="None",
+            validator=validators.Text,
+            help="Bugzilla username to show issues of",
+        )
+    ]
+
+
+class BaseView(RootWidgetView):
+
+    def get_context(self, instance, *args, **kwargs):
+        bugzilla_bugs = GetBugzillaBugs(instance)
+        github_issues = GetGithubIssues(instance)
+        pagure_issues = GetPagureIssues(instance)
+        return dict(
+            bugzilla_bugs=bugzilla_bugs(),
+            github_issues=github_issues(),
+            pagure_issues=pagure_issues()
+        )
+
+
+class GetBugzillaBugs(CachedFunction):
+
+    def execute(self):
+        username = self.instance.config["bugzilla_username"]
+        endpoint = "https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{\"creator\":\"{}\"}}]"
+        response = requests.get(endpoint.format(username)).json()
+        issues = response['result']['bugs']
+        issues = issues[:min(5, len(issues))]
+        return issues
+
+    def should_invalidate(self, message):
+        return False
+
+    def _should_cache(self, value):
+        return False
+
+
+class GetPagureIssues(CachedFunction):
+
+    def execute(self):
+        username = self.instance.config["pagure_username"]
+        endpoint = "https://pagure.io/api/0/user/{}/issues"
+        response = requests.get(endpoint.format(username)).json()
+        issues = response['issues_created']
+        issues = issues[:min(5, len(issues))]
+        return issues
+
+    def should_invalidate(self, message):
+        return False
+
+    def _should_cache(self, value):
+        return False
+
+
+class GetGithubIssues(CachedFunction):
+
+    def execute(self):
+        username = self.instance.config["github_username"]
+        endpoint = "https://api.github.com/search/issues?q=is:issue+author:{}&sort:updated"
+        response = requests.get(endpoint.format(username)).json()
+        issues = response['items']
+        issues = issues[:min(5, len(issues))]
+        return issues
+
+    def should_invalidate(self, message):
+        return False
+
+    def _should_cache(self, value):
+        return False
diff --git a/hubs/widgets/issues/templates/root.html b/hubs/widgets/issues/templates/root.html
new file mode 100644
index 0000000..88e9b15
--- /dev/null
+++ b/hubs/widgets/issues/templates/root.html
@@ -0,0 +1,110 @@
+<h6>Bugzilla</h6>
+<ul class="list-unstyled">
+    {% for bug in bugzilla_bugs %}
+        <li class="media row py-2">
+            <div class="col-2">
+                <a  href="https://bugzilla.redhat.com/show_bug.cgi?id={{ bug.id }}"
+                    target="_blank">
+                    <span class="badge badge-primary">#{{ bug.id }}</span>
+                </a>
+            </div>
+            <div class="media-body col-10">
+                <div>
+                    <span>
+                        {{ bug.summary }}
+                    </span>
+                </div>
+                <small>
+                    Reported by: {{ bug.creator }}
+                    --
+                    {% if bug.assigned_to != "Nobody's working on this, feel free to take it" %}
+                        Assigned to: {{ bug.assigned_to }}
+                    {% else %}
+                        <span class="text-muted">Unassigned</span>
+                    {% endif %}
+                </small>
+            </div>
+        </li>
+    {% endfor %}
+</ul>
+
+<h6>Github</h6>
+<ul class="list-unstyled">
+    {% for issue in github_issues %}
+        <li class="media row py-2">
+            <div class="col-2">
+                <a  href="{{ issue.html_url }}"
+                    target="_blank">
+                    <span class="badge badge-primary">#{{ issue.number }}</span>
+                </a>
+            </div>
+            <div class="media-body col-10">
+                <div>
+                    <span>
+                        {{ issue.title }}
+                    </span>
+                </div>
+                <small>
+                    Reported by: {{ issue.user.login }}
+                    --
+                    {% if issue.assignee %}
+                        Assigned to: {{ issue.assignee.login }}
+                    {% else %}
+                        <span class="text-muted">Unassigned</span>
+                    {% endif %}
+                </small>
+            </div>
+        </li>
+    {% endfor %}
+</ul>
+
+<h6>Pagure</h6>
+<ul class="list-unstyled">
+    {% for issue in pagure_issues %}
+        <li class="media row py-2">
+            <div class="col-2">
+                <a  href="#"
+                    target="_blank">
+                    <span class="badge badge-primary">#{{ issue.id }}</span>
+                </a>
+            </div>
+            <div class="media-body col-10">
+                <div>
+                    <span>
+                        {{ issue.title }}
+                    </span>
+                </div>
+                <small>
+                    Reported by: {{ issue.user.name }}
+                    --
+                    {% if issue.assignee %}
+                        Assigned to: {{ issue.assignee.name }}
+                    {% else %}
+                        <span class="text-muted">Unassigned</span>
+                    {% endif %}
+                </small>
+            </div>
+        </li>
+    {% endfor %}
+</ul>
+
+{#<h6>Bugzilla</h6>#}
+{#<ul>#}
+{#    {% for bug in bugzilla_bugs %}#}
+{#        <li><a href="https://bugzilla.redhat.com/show_bug.cgi?id={{ bug.id }}" target="_blank">{{ bug.summary }}</a></li>#}
+{#    {% endfor %}#}
+{#</ul>#}
+{##}
+{#<h6>Github</h6>#}
+{#<ul>#}
+{#    {% for issue in github_issues %}#}
+{#        <li><a href="{{ issue.url }}" target="_blank">{{ issue.title }}</a></li>#}
+{#    {% endfor %}#}
+{#</ul>#}
+{##}
+{#<h6>Pagure</h6>#}
+{#<ul>#}
+{#    {% for issue in pagure_issues %}#}
+{#        <li><a href="#" target="_blank">{{ issue.title }}</a></li>#}
+{#    {% endfor %}#}
+{#</ul>#}

From 09bd1007ce147a2af8b076302f534cc9ad36221d Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 2/32] Issues: Fetch VCS details from hub config


---

diff --git a/hubs/models/hubconfig.py b/hubs/models/hubconfig.py
index d103681..a4a8db6 100644
--- a/hubs/models/hubconfig.py
+++ b/hubs/models/hubconfig.py
@@ -105,8 +105,9 @@ class HubConfigProxy(MutableMapping):
         "visibility", "chat_domain", "chat_channel", "mailing_list",
         "calendar", "meetings_text", "rules_url", "timezone_offset",
         "chat_nickname", "email", "creation_date", "country",
+        "pagure_username", "github_username", "bugzilla_email",
         "mailing_list_url",
-    ) + tuple(p["name"] for p in DEV_PLATFORMS)
+           ) + tuple(p["name"] for p in DEV_PLATFORMS)
     CONVERTERS = {
         "archived": BooleanConverter(),
         "left_width": Converter(int),
@@ -125,6 +126,8 @@ class HubConfigProxy(MutableMapping):
     }
     LISTS = ("pagure", "github")
     VALIDATORS = {
+        "github_username": validators.GithubOrganization,
+        "pagure_username": validators.PagureUser,
         "github": validators.GithubOrgAndRepo,
         "pagure": validators.PagureRepo,
         "chat_domain": validators.ChatDomain,
diff --git a/hubs/static/client/app/components/HubConfig/HubConfigPanelDetails.js b/hubs/static/client/app/components/HubConfig/HubConfigPanelDetails.js
new file mode 100644
index 0000000..ec639b5
--- /dev/null
+++ b/hubs/static/client/app/components/HubConfig/HubConfigPanelDetails.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import {
+  defineMessages,
+  FormattedMessage,
+  } from 'react-intl';
+
+
+const messages = defineMessages({
+  title: {
+    id: "hubs.core.config.details.title",
+    defaultMessage: "Details",
+  },
+  intro: {
+    id: "hubs.core.config.details.intro",
+    defaultMessage: "Change additional hub configuration here.",
+  },
+  pagure_username: {
+    id: "hubs.core.config.details.pagure_username",
+    defaultMessage: "Pagure Username",
+  },
+  pagure_username_help: {
+    id: "hubs.core.config.details.pagure_username_help",
+    defaultMessage: "Your pagure username",
+  },
+  github_username: {
+    id: "hubs.core.config.details.github_username",
+    defaultMessage: "Github Username",
+  },
+  github_username_help: {
+    id: "hubs.core.config.details.github_username_help",
+    defaultMessage: "Your github username",
+  },
+  bugzilla_email: {
+    id: "hubs.core.config.details.bugzilla_email",
+    defaultMessage: "Bugzilla E-mail",
+  },
+  bugzilla_email_help: {
+    id: "hubs.core.config.details.bugzilla_email_help",
+    defaultMessage: "Your bugzilla e-mail address",
+  },
+});
+
+
+export default class DetailsPanel extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.handleChange = this.handleChange.bind(this);
+  }
+
+  handleChange(e) {
+    const name = e.target.name, value = e.target.value;
+    this.props.handleChange(name, value);
+  }
+
+  render() {
+    return (
+      <form onSubmit={(e) => {e.preventDefault();}}>
+        <FormattedMessage {...messages.title} tagName="h3" />
+        <FormattedMessage {...messages.intro} tagName="p" />
+        {["pagure_username", "github_username", "bugzilla_email"].map(function(x, i) {
+          return (
+            <div key={i} className="form-group row">
+              <label htmlFor={"hub-settings-details-" + x}>
+                <FormattedMessage {...messages[x]} />
+              </label>
+              <input
+                type="text"
+                name={x}
+                className={"form-control"}
+                id={"hub-settings-details-" + x}
+                onChange={this.handleChange}
+                value={this.props.hubConfig[x] || ""}
+                />
+              <p className="form-text text-muted">
+                <FormattedMessage {...messages[x + "_help"]} />
+              </p>
+            </div>
+          )
+        }, this)}
+      </form>
+
+    );
+  }
+}
diff --git a/hubs/utils/validators.py b/hubs/utils/validators.py
index 4d5ec86..8da8e1c 100644
--- a/hubs/utils/validators.py
+++ b/hubs/utils/validators.py
@@ -107,6 +107,14 @@ def FMNContext(value):
     raise ValueError('Invalid FMN context')
 
 
+def PagureUser(value):
+    """Fails if the Pagure username does not exist."""
+    response = requests.get("https://pagure.io/user/%s" % value, timeout=5)
+    if response.ok:
+        return value
+    raise ValueError('Invalid Pagure username: {}'.format(value))
+
+
 def PagureRepo(value):
     """Fails if the Pagure repository name does not exist."""
     try:
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 616df54..c4bfd8f 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -13,29 +13,7 @@ class Issues(Widget):
     name = "issues"
     label = "Issues"
     position = "left"
-    parameters = [
-        dict(
-            name="github_username",
-            label="Github Username",
-            default="None",
-            validator=validators.Text,
-            help="Github username to show issues of",
-        ),
-        dict(
-            name="pagure_username",
-            label="Pagure Username",
-            default="None",
-            validator=validators.Text,
-            help="Pagure username to show issues of",
-        ),
-        dict(
-            name="bugzilla_username",
-            label="Bugzilla Username",
-            default="None",
-            validator=validators.Text,
-            help="Bugzilla username to show issues of",
-        )
-    ]
+    parameters = []
 
 
 class BaseView(RootWidgetView):
@@ -54,9 +32,9 @@ class BaseView(RootWidgetView):
 class GetBugzillaBugs(CachedFunction):
 
     def execute(self):
-        username = self.instance.config["bugzilla_username"]
+        email = self.instance.hub.config["bugzilla_email"]
         endpoint = "https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{\"creator\":\"{}\"}}]"
-        response = requests.get(endpoint.format(username)).json()
+        response = requests.get(endpoint.format(email)).json()
         issues = response['result']['bugs']
         issues = issues[:min(5, len(issues))]
         return issues
@@ -64,6 +42,10 @@ class GetBugzillaBugs(CachedFunction):
     def should_invalidate(self, message):
         return False
 
+    def should_invalidate_on_hub_config_change(self, old_config):
+        return True
+
+    # For testing purposes
     def _should_cache(self, value):
         return False
 
@@ -71,7 +53,7 @@ class GetBugzillaBugs(CachedFunction):
 class GetPagureIssues(CachedFunction):
 
     def execute(self):
-        username = self.instance.config["pagure_username"]
+        username = self.instance.hub.config["pagure_username"]
         endpoint = "https://pagure.io/api/0/user/{}/issues"
         response = requests.get(endpoint.format(username)).json()
         issues = response['issues_created']
@@ -81,6 +63,10 @@ class GetPagureIssues(CachedFunction):
     def should_invalidate(self, message):
         return False
 
+    def should_invalidate_on_hub_config_change(self, old_config):
+        return True
+
+    # For testing purposes
     def _should_cache(self, value):
         return False
 
@@ -88,7 +74,7 @@ class GetPagureIssues(CachedFunction):
 class GetGithubIssues(CachedFunction):
 
     def execute(self):
-        username = self.instance.config["github_username"]
+        username = self.instance.hub.config["github_username"]
         endpoint = "https://api.github.com/search/issues?q=is:issue+author:{}&sort:updated"
         response = requests.get(endpoint.format(username)).json()
         issues = response['items']
@@ -98,5 +84,9 @@ class GetGithubIssues(CachedFunction):
     def should_invalidate(self, message):
         return False
 
+    def should_invalidate_on_hub_config_change(self, old_config):
+        return True
+
+    # For testing purposes
     def _should_cache(self, value):
         return False

From aaef012875b3d784264549bbf6827fec0c6515bc Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 3/32] Issues: Use react widget


---

diff --git a/hubs/static/client/app/widgets/issues/Config.js b/hubs/static/client/app/widgets/issues/Config.js
new file mode 100644
index 0000000..5c5e4fc
--- /dev/null
+++ b/hubs/static/client/app/widgets/issues/Config.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import {
+    defineMessages,
+    injectIntl,
+    FormattedMessage
+} from 'react-intl';
+
+
+const messages = defineMessages({
+    select_author: {
+        id: "hubs.widgets.halp.config.select.author",
+        defaultMessage: "Tickets I Filed",
+    },
+    select_assignee: {
+        id: "hubs.widgets.halp.config.select.assignee",
+        defaultMessage: "Tickets Waiting on Me",
+    },
+});
+
+
+class Config extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.handleModeChanged = this.handleModeChanged.bind(this);
+    }
+
+    handleModeChanged(e) {
+        this.props.setConfig("mode", e.target.value);
+    }
+
+    render() {
+        return (
+            <div className="form-group">
+                <label>Mode</label>
+                <select className="form-control"
+                        value={this.props.values.mode}
+                        onChange={this.handleModeChanged}>
+                    <option value={1}>
+                        <FormattedMessage {...messages.select_author} />
+                    </option>
+                    <option value={2}>
+                        <FormattedMessage {...messages.select_assignee} />
+                    </option>
+                </select>
+            </div>
+        );
+    }
+}
+
+
+export default injectIntl(Config);
diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
new file mode 100644
index 0000000..1c9a8d3
--- /dev/null
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -0,0 +1,133 @@
+import React from 'react';
+import {
+  widgetDidUpdate,
+  widgetWillUpdate
+} from "../../core/actions/widget";
+import WidgetChrome from '../../components/WidgetChrome';
+import PropTypes from "prop-types";
+import Spinner from "../../components/Spinner";
+
+
+export default class IssuesWidget extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      issues: [],
+      counts: {},
+      isLoading: false
+    }
+  }
+
+  componentDidMount() {
+    if (!this.props.editMode) {
+      this.loadFromServer();
+    }
+  }
+
+  componentWillUnmount() {
+    this.serverRequest.abort();
+  }
+
+
+  componentDidUpdate(prevProps, prevState) {
+    if (this.props.needsUpdate && !this.props.editMode && !this.state.isLoading) {
+      this.loadFromServer();
+    }
+  }
+
+  loadFromServer() {
+    this.props.dispatch(widgetWillUpdate(this.props.widget.idx));
+    this.setState({isLoading: true});
+    this.serverRequest = $.ajax({
+      url: this.props.widget.urls.data,
+      method: 'GET',
+      cache: false,
+      success: (res) => {
+        this.setState({
+            issues: res.data.issues,
+            counts: res.data.counts,
+            isLoading: false
+          },
+          () => (
+            this.props.dispatch(widgetDidUpdate(this.props.widget.idx))
+          )
+        );
+      },
+      error: (xhr, status, err) => {
+        console.error(this.props.widget.urls.data, status, err.toString());
+        this.setState({isLoading: false});
+      }
+    });
+  }
+
+  render() {
+    return (
+      <WidgetChrome
+        widget={this.props.widget}
+        editMode={this.props.editMode}
+      >
+        <ul className="list-unstyled">
+          { this.state.isLoading && <Spinner className="p-3"/>}
+          {
+            this.state.issues.map((issue) => {
+              return (
+                <li key={issue.service + issue.number} className="py-2">
+                  <div className="row">
+                    <div className="col-md-6">
+                      <a  href={ issue.url }
+                          target="_blank" rel="noopener noreferrer">
+                        <span className="badge badge-primary">#{ issue.number }</span>
+                      </a>
+                    </div>
+                    <div className="col-md-6">
+                      <div className="float-right">
+                        <span>{ issue.project }</span>
+                        <img src={ issue.icon } style={{height: '16px'}} className={"ml-2"}/>
+                      </div>
+                    </div>
+                  </div>
+                  <div className="row">
+                    <div className="col-12">
+                      <span>{ issue.title }</span>
+                    </div>
+                  </div>
+                </li>
+              )
+            })
+          }
+        </ul>
+        <div className="card-footer p-1">
+          <div className="row p-1">
+            <div className="col-md-3 text-muted">
+              <span data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}>View More</span>
+            </div>
+            <div className="col-md-9 collapse" id={'counts'}>
+              <div className="row pt-0">
+                <div className="col-md-4 text-center">
+                  <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                  <span>Bugzilla ({ this.state.counts.bugzilla })</span>
+                </div>
+                <div className="col-md-4 text-center">
+                  <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                  <span>Github ({ this.state.counts.github })</span>
+                </div>
+                <div className="col-md-4 text-center">
+                  <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                  <span>Pagure ({ this.state.counts.pagure })</span>
+                </div>
+              </div>
+
+            </div>
+          </div>
+        </div>
+      </WidgetChrome>
+    )
+  }
+}
+
+IssuesWidget.propTypes = {
+  widget: PropTypes.object.isRequired,
+  editMode: PropTypes.bool,
+  needsUpdate: PropTypes.bool,
+};
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index c4bfd8f..343cef7 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -1,92 +1,55 @@
 from __future__ import unicode_literals
 
-from hubs.widgets import validators
-from hubs.widgets.base import Widget
-from hubs.widgets.caching import CachedFunction
-from hubs.widgets.view import RootWidgetView
+import arrow
+import flask
 
-import requests
+from hubs.widgets.base import Widget
+from hubs.widgets.view import WidgetView
+from .functions import *
 
 
 class Issues(Widget):
 
     name = "issues"
     label = "Issues"
-    position = "left"
-    parameters = []
-
-
-class BaseView(RootWidgetView):
-
-    def get_context(self, instance, *args, **kwargs):
-        bugzilla_bugs = GetBugzillaBugs(instance)
-        github_issues = GetGithubIssues(instance)
-        pagure_issues = GetPagureIssues(instance)
-        return dict(
-            bugzilla_bugs=bugzilla_bugs(),
-            github_issues=github_issues(),
-            pagure_issues=pagure_issues()
+    position = "both"
+    parameters = [
+        dict(
+            name="mode",
+            default=1,
         )
+    ]
+    is_react = True
+
+    def get_props(self, instance, *args, **kwargs):
+        props = super(Issues, self).get_props(instance, *args, **kwargs)
+        if instance is not None:
+            hub_name = instance.hub.name
+            props.update(dict(
+                title="Issues - " +
+                      ("Tickets I Filed" if int(instance.config["mode"]) == 1 else "Tickets Waiting on Me"),
+                urls=dict(
+                    data=flask.url_for(
+                        "issues_data", hub=hub_name, idx=instance.idx),
+                )
+            ))
+        return props
+
+
+class DataView(WidgetView):
+    """Get issues in JSON format."""
+
+    name = "data"
+    url_rules = ["data"]
+    json = True
 
+    def get_context(self, instance, *args, **kwargs):
+        [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
+        [pagure, pcnt] = GetPagureIssues(instance)()
+        [github, ghcnt] = GetGithubIssues(instance)()
 
-class GetBugzillaBugs(CachedFunction):
-
-    def execute(self):
-        email = self.instance.hub.config["bugzilla_email"]
-        endpoint = "https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{\"creator\":\"{}\"}}]"
-        response = requests.get(endpoint.format(email)).json()
-        issues = response['result']['bugs']
-        issues = issues[:min(5, len(issues))]
-        return issues
-
-    def should_invalidate(self, message):
-        return False
-
-    def should_invalidate_on_hub_config_change(self, old_config):
-        return True
-
-    # For testing purposes
-    def _should_cache(self, value):
-        return False
-
-
-class GetPagureIssues(CachedFunction):
-
-    def execute(self):
-        username = self.instance.hub.config["pagure_username"]
-        endpoint = "https://pagure.io/api/0/user/{}/issues"
-        response = requests.get(endpoint.format(username)).json()
-        issues = response['issues_created']
-        issues = issues[:min(5, len(issues))]
-        return issues
-
-    def should_invalidate(self, message):
-        return False
-
-    def should_invalidate_on_hub_config_change(self, old_config):
-        return True
-
-    # For testing purposes
-    def _should_cache(self, value):
-        return False
-
-
-class GetGithubIssues(CachedFunction):
-
-    def execute(self):
-        username = self.instance.hub.config["github_username"]
-        endpoint = "https://api.github.com/search/issues?q=is:issue+author:{}&sort:updated"
-        response = requests.get(endpoint.format(username)).json()
-        issues = response['items']
-        issues = issues[:min(5, len(issues))]
-        return issues
-
-    def should_invalidate(self, message):
-        return False
-
-    def should_invalidate_on_hub_config_change(self, old_config):
-        return True
+        all_issues = bugzilla + pagure + github
+        all_issues.sort(key=lambda x: arrow.get(x["updated_at"]), reverse=True)
 
-    # For testing purposes
-    def _should_cache(self, value):
-        return False
+        data = {"issues": all_issues, "counts": {"bugzilla": bzcnt, "pagure": pcnt, "github": ghcnt}}
+        return dict(status="OK", data=data)
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
new file mode 100644
index 0000000..68b177d
--- /dev/null
+++ b/hubs/widgets/issues/functions.py
@@ -0,0 +1,95 @@
+import requests
+
+from hubs.widgets.caching import CachedFunction
+
+
+class GetBugzillaBugs(CachedFunction):
+
+    def execute(self):
+        email = self.instance.hub.config["bugzilla_email"]
+        endpoint = "https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{\"{}\":\"{}\"}}]"
+        mode = "creator" if int(self.instance.config["mode"]) == 1 else "assigned_to"
+        response = requests.get(endpoint.format(mode, email)).json()
+        issues = response['result']['bugs']
+        issues = issues[:min(5, len(issues))]
+        urltmpl = 'https://bugzilla.redhat.com/show_bug.cgi?id={}'
+        res = []
+        for issue in issues:
+            res.append(dict(
+                number=issue['id'],
+                title=issue['summary'],
+                project=issue['product'],
+                url=urltmpl.format(issue['id']),
+                updated_at=issue['last_change_time'],
+                service='bugzilla',
+                icon='https://bugzilla.redhat.com/favicon.ico'
+            ))
+        total_count = len(response['result']['bugs'])
+        return [res, total_count]
+
+    def should_invalidate(self, message):
+        return False
+
+    def should_invalidate_on_hub_config_change(self, old_config):
+        return True
+
+
+class GetPagureIssues(CachedFunction):
+
+    def execute(self):
+        username = self.instance.hub.config["pagure_username"]
+        endpoint = "https://pagure.io/api/0/user/{}/issues?author={}"
+        author = "true" if int(self.instance.config["mode"]) == 1 else "false"
+        response = requests.get(endpoint.format(username, author)).json()
+        issues = response['issues_created'] if author == "true" else response['issues_assigned']
+        issues = issues[:min(5, len(issues))]
+        urltmpl = 'https://pagure.io/{}/issue/{}'
+        res = []
+        for issue in issues:
+            res.append(dict(
+                number=issue['id'],
+                title=issue['title'],
+                project=issue['project']['name'],
+                url=urltmpl.format(issue['project']['url_path'], issue['id']),
+                updated_at=issue['last_updated'],
+                service='pagure',
+                icon='https://pagure.io/static/favicon.ico'
+            ))
+        total_count = len(response['issues_created'] if author == "true" else response['issues_assigned'])
+        return [res, total_count]
+
+    def should_invalidate(self, message):
+        return False
+
+    def should_invalidate_on_hub_config_change(self, old_config):
+        return True
+
+
+class GetGithubIssues(CachedFunction):
+
+    def execute(self):
+        username = self.instance.hub.config["github_username"]
+        endpoint = "https://api.github.com/search/issues?q=type:issue+{}:{}+state:open&sort:updated"
+        mode = "author" if int(self.instance.config["mode"]) == 1 else "assignee"
+        response = requests.get(endpoint.format(mode, username)).json()
+        issues = response['items']
+        issues = issues[:min(5, len(issues))]
+        res = []
+        for issue in issues:
+            res.append(dict(
+                number=issue['number'],
+                title=issue['title'],
+                project=issue['repository_url'].split('/')[-1],
+                url=issue['html_url'],
+                updated_at=issue['updated_at'],
+                service='github',
+                icon='https://github.com/favicon.ico'
+            ))
+        total_count = response['total_count']
+        return [res, total_count]
+
+    def should_invalidate(self, message):
+        return False
+
+    def should_invalidate_on_hub_config_change(self, old_config):
+        return True
diff --git a/hubs/widgets/issues/templates/root.html b/hubs/widgets/issues/templates/root.html
deleted file mode 100644
index 88e9b15..0000000
--- a/hubs/widgets/issues/templates/root.html
+++ /dev/null
@@ -1,110 +0,0 @@
-<h6>Bugzilla</h6>
-<ul class="list-unstyled">
-    {% for bug in bugzilla_bugs %}
-        <li class="media row py-2">
-            <div class="col-2">
-                <a  href="https://bugzilla.redhat.com/show_bug.cgi?id={{ bug.id }}"
-                    target="_blank">
-                    <span class="badge badge-primary">#{{ bug.id }}</span>
-                </a>
-            </div>
-            <div class="media-body col-10">
-                <div>
-                    <span>
-                        {{ bug.summary }}
-                    </span>
-                </div>
-                <small>
-                    Reported by: {{ bug.creator }}
-                    --
-                    {% if bug.assigned_to != "Nobody's working on this, feel free to take it" %}
-                        Assigned to: {{ bug.assigned_to }}
-                    {% else %}
-                        <span class="text-muted">Unassigned</span>
-                    {% endif %}
-                </small>
-            </div>
-        </li>
-    {% endfor %}
-</ul>
-
-<h6>Github</h6>
-<ul class="list-unstyled">
-    {% for issue in github_issues %}
-        <li class="media row py-2">
-            <div class="col-2">
-                <a  href="{{ issue.html_url }}"
-                    target="_blank">
-                    <span class="badge badge-primary">#{{ issue.number }}</span>
-                </a>
-            </div>
-            <div class="media-body col-10">
-                <div>
-                    <span>
-                        {{ issue.title }}
-                    </span>
-                </div>
-                <small>
-                    Reported by: {{ issue.user.login }}
-                    --
-                    {% if issue.assignee %}
-                        Assigned to: {{ issue.assignee.login }}
-                    {% else %}
-                        <span class="text-muted">Unassigned</span>
-                    {% endif %}
-                </small>
-            </div>
-        </li>
-    {% endfor %}
-</ul>
-
-<h6>Pagure</h6>
-<ul class="list-unstyled">
-    {% for issue in pagure_issues %}
-        <li class="media row py-2">
-            <div class="col-2">
-                <a  href="#"
-                    target="_blank">
-                    <span class="badge badge-primary">#{{ issue.id }}</span>
-                </a>
-            </div>
-            <div class="media-body col-10">
-                <div>
-                    <span>
-                        {{ issue.title }}
-                    </span>
-                </div>
-                <small>
-                    Reported by: {{ issue.user.name }}
-                    --
-                    {% if issue.assignee %}
-                        Assigned to: {{ issue.assignee.name }}
-                    {% else %}
-                        <span class="text-muted">Unassigned</span>
-                    {% endif %}
-                </small>
-            </div>
-        </li>
-    {% endfor %}
-</ul>
-
-{#<h6>Bugzilla</h6>#}
-{#<ul>#}
-{#    {% for bug in bugzilla_bugs %}#}
-{#        <li><a href="https://bugzilla.redhat.com/show_bug.cgi?id={{ bug.id }}" target="_blank">{{ bug.summary }}</a></li>#}
-{#    {% endfor %}#}
-{#</ul>#}
-{##}
-{#<h6>Github</h6>#}
-{#<ul>#}
-{#    {% for issue in github_issues %}#}
-{#        <li><a href="{{ issue.url }}" target="_blank">{{ issue.title }}</a></li>#}
-{#    {% endfor %}#}
-{#</ul>#}
-{##}
-{#<h6>Pagure</h6>#}
-{#<ul>#}
-{#    {% for issue in pagure_issues %}#}
-{#        <li><a href="#" target="_blank">{{ issue.title }}</a></li>#}
-{#    {% endfor %}#}
-{#</ul>#}

From 6d32fe9dd8c1b1657b005ca3a337ff538e3fccf1 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 4/32] Issues: Rename details to VCS


---

diff --git a/hubs/static/client/app/components/HubConfig/HubConfigDialog.js b/hubs/static/client/app/components/HubConfig/HubConfigDialog.js
index 64d3f4c..88fe2d9 100644
--- a/hubs/static/client/app/components/HubConfig/HubConfigDialog.js
+++ b/hubs/static/client/app/components/HubConfig/HubConfigDialog.js
@@ -5,6 +5,7 @@ import {
   } from 'react-intl';
 import PropTypes from 'prop-types';
 import GeneralPanel from './HubConfigPanelGeneral';
+import VCSPanel from './HubConfigPanelVCS';
 import UserPanel from './HubConfigPanelUser';
 import CommunityPanel from './HubConfigPanelCommunity';
 import MailingListPanel from './HubConfigPanelMailingList';
@@ -26,6 +27,10 @@ const messages = defineMessages({
     id: "hubs.core.config.general",
     defaultMessage: "General",
   },
+  vcs: {
+    id: "hubs.core.config.vcs",
+    defaultMessage: "Version Control",
+  },
   owners: {
     id: "hubs.core.config.owners",
     defaultMessage: "Owners",
@@ -186,6 +191,16 @@ export default class HubConfigDialog extends React.Component {
           />
       );
     }
+    else {
+      tabs.push(
+        <VCSPanel
+          key="vcs"
+          hubConfig={this.props.hubConfig}
+          tabTitle={<FormattedMessage {...messages.vcs} />}
+          handleChange={this.props.onConfigChange}
+          />
+      )
+    }
 
     return (
       <Modal
diff --git a/hubs/static/client/app/components/HubConfig/HubConfigPanelDetails.js b/hubs/static/client/app/components/HubConfig/HubConfigPanelDetails.js
deleted file mode 100644
index ec639b5..0000000
--- a/hubs/static/client/app/components/HubConfig/HubConfigPanelDetails.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import React from 'react';
-import {
-  defineMessages,
-  FormattedMessage,
-  } from 'react-intl';
-
-
-const messages = defineMessages({
-  title: {
-    id: "hubs.core.config.details.title",
-    defaultMessage: "Details",
-  },
-  intro: {
-    id: "hubs.core.config.details.intro",
-    defaultMessage: "Change additional hub configuration here.",
-  },
-  pagure_username: {
-    id: "hubs.core.config.details.pagure_username",
-    defaultMessage: "Pagure Username",
-  },
-  pagure_username_help: {
-    id: "hubs.core.config.details.pagure_username_help",
-    defaultMessage: "Your pagure username",
-  },
-  github_username: {
-    id: "hubs.core.config.details.github_username",
-    defaultMessage: "Github Username",
-  },
-  github_username_help: {
-    id: "hubs.core.config.details.github_username_help",
-    defaultMessage: "Your github username",
-  },
-  bugzilla_email: {
-    id: "hubs.core.config.details.bugzilla_email",
-    defaultMessage: "Bugzilla E-mail",
-  },
-  bugzilla_email_help: {
-    id: "hubs.core.config.details.bugzilla_email_help",
-    defaultMessage: "Your bugzilla e-mail address",
-  },
-});
-
-
-export default class DetailsPanel extends React.Component {
-
-  constructor(props) {
-    super(props);
-    this.handleChange = this.handleChange.bind(this);
-  }
-
-  handleChange(e) {
-    const name = e.target.name, value = e.target.value;
-    this.props.handleChange(name, value);
-  }
-
-  render() {
-    return (
-      <form onSubmit={(e) => {e.preventDefault();}}>
-        <FormattedMessage {...messages.title} tagName="h3" />
-        <FormattedMessage {...messages.intro} tagName="p" />
-        {["pagure_username", "github_username", "bugzilla_email"].map(function(x, i) {
-          return (
-            <div key={i} className="form-group row">
-              <label htmlFor={"hub-settings-details-" + x}>
-                <FormattedMessage {...messages[x]} />
-              </label>
-              <input
-                type="text"
-                name={x}
-                className={"form-control"}
-                id={"hub-settings-details-" + x}
-                onChange={this.handleChange}
-                value={this.props.hubConfig[x] || ""}
-                />
-              <p className="form-text text-muted">
-                <FormattedMessage {...messages[x + "_help"]} />
-              </p>
-            </div>
-          )
-        }, this)}
-      </form>
-
-    );
-  }
-}
diff --git a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
new file mode 100644
index 0000000..6df26cf
--- /dev/null
+++ b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import {
+  defineMessages,
+  FormattedMessage,
+  } from 'react-intl';
+
+
+const messages = defineMessages({
+  title: {
+    id: "hubs.core.config.vcs.title",
+    defaultMessage: "Version Control",
+  },
+  intro: {
+    id: "hubs.core.config.vcs.intro",
+    defaultMessage: "Change VCS related details here.",
+  },
+  pagure_username: {
+    id: "hubs.core.config.vcs.pagure_username",
+    defaultMessage: "Pagure Username",
+  },
+  pagure_username_help: {
+    id: "hubs.core.config.vcs.pagure_username_help",
+    defaultMessage: "Your pagure username",
+  },
+  github_username: {
+    id: "hubs.core.config.vcs.github_username",
+    defaultMessage: "Github Username",
+  },
+  github_username_help: {
+    id: "hubs.core.config.vcs.github_username_help",
+    defaultMessage: "Your github username",
+  },
+  bugzilla_email: {
+    id: "hubs.core.config.vcs.bugzilla_email",
+    defaultMessage: "Bugzilla E-mail",
+  },
+  bugzilla_email_help: {
+    id: "hubs.core.config.vcs.bugzilla_email_help",
+    defaultMessage: "Your bugzilla e-mail address",
+  },
+});
+
+
+export default class VCSPanel extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.handleChange = this.handleChange.bind(this);
+  }
+
+  handleChange(e) {
+    const name = e.target.name, value = e.target.value;
+    this.props.handleChange(name, value);
+  }
+
+  render() {
+    return (
+      <form onSubmit={(e) => {e.preventDefault();}}>
+        <FormattedMessage {...messages.title} tagName="h3" />
+        <FormattedMessage {...messages.intro} tagName="p" />
+        {["pagure_username", "github_username", "bugzilla_email"].map(function(x, i) {
+          return (
+            <div key={i} className="form-group row">
+              <label htmlFor={"hub-settings-vcs-" + x}>
+                <FormattedMessage {...messages[x]} />
+              </label>
+              <input
+                type="text"
+                name={x}
+                className={"form-control"}
+                id={"hub-settings-vcs-" + x}
+                onChange={this.handleChange}
+                value={this.props.hubConfig[x] || ""}
+                />
+              <p className="form-text text-muted">
+                <FormattedMessage {...messages[x + "_help"]} />
+              </p>
+            </div>
+          )
+        }, this)}
+      </form>
+
+    );
+  }
+}
diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 1c9a8d3..1a98c7b 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -99,8 +99,11 @@ export default class IssuesWidget extends React.PureComponent {
         </ul>
         <div className="card-footer p-1">
           <div className="row p-1">
-            <div className="col-md-3 text-muted">
-              <span data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}>View More</span>
+            <div className="col-md-3 text-muted"
+                 data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}
+            >
+              <span>View More</span>
+              <i className="fa fa-angle-right ml-1" aria-hidden="true" />
             </div>
             <div className="col-md-9 collapse" id={'counts'}>
               <div className="row pt-0">

From 0e34ad6f00ea8604d7a584185677a13a7186af05 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 5/32] Issues: Change parameter type to string


---

diff --git a/hubs/static/client/app/widgets/issues/Config.js b/hubs/static/client/app/widgets/issues/Config.js
index 5c5e4fc..dc61da2 100644
--- a/hubs/static/client/app/widgets/issues/Config.js
+++ b/hubs/static/client/app/widgets/issues/Config.js
@@ -36,10 +36,10 @@ class Config extends React.Component {
                 <select className="form-control"
                         value={this.props.values.mode}
                         onChange={this.handleModeChanged}>
-                    <option value={1}>
+                    <option value="1">
                         <FormattedMessage {...messages.select_author} />
                     </option>
-                    <option value={2}>
+                    <option value="2">
                         <FormattedMessage {...messages.select_assignee} />
                     </option>
                 </select>
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 343cef7..986f7d7 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -16,7 +16,7 @@ class Issues(Widget):
     parameters = [
         dict(
             name="mode",
-            default=1,
+            default="1",
         )
     ]
     is_react = True
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index 68b177d..4be18f4 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -8,7 +8,7 @@ class GetBugzillaBugs(CachedFunction):
     def execute(self):
         email = self.instance.hub.config["bugzilla_email"]
         endpoint = "https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{\"{}\":\"{}\"}}]"
-        mode = "creator" if int(self.instance.config["mode"]) == 1 else "assigned_to"
+        mode = "creator" if self.instance.config["mode"] == "1" else "assigned_to"
         response = requests.get(endpoint.format(mode, email)).json()
         issues = response['result']['bugs']
         issues = issues[:min(5, len(issues))]
@@ -39,7 +39,7 @@ class GetPagureIssues(CachedFunction):
     def execute(self):
         username = self.instance.hub.config["pagure_username"]
         endpoint = "https://pagure.io/api/0/user/{}/issues?author={}"
-        author = "true" if int(self.instance.config["mode"]) == 1 else "false"
+        author = "true" if self.instance.config["mode"] == "1" else "false"
         response = requests.get(endpoint.format(username, author)).json()
         issues = response['issues_created'] if author == "true" else response['issues_assigned']
         issues = issues[:min(5, len(issues))]
@@ -70,7 +70,7 @@ class GetGithubIssues(CachedFunction):
     def execute(self):
         username = self.instance.hub.config["github_username"]
         endpoint = "https://api.github.com/search/issues?q=type:issue+{}:{}+state:open&sort:updated"
-        mode = "author" if int(self.instance.config["mode"]) == 1 else "assignee"
+        mode = "author" if self.instance.config["mode"] == "1" else "assignee"
         response = requests.get(endpoint.format(mode, username)).json()
         issues = response['items']
         issues = issues[:min(5, len(issues))]

From 121365cf0ff935b8c252c72a3b4fe9ee44d456a3 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 6/32] Issues: Invalidate cache on fedmsg


---

diff --git a/hubs/static/client/app/widgets/issues/Config.js b/hubs/static/client/app/widgets/issues/Config.js
index dc61da2..82714f2 100644
--- a/hubs/static/client/app/widgets/issues/Config.js
+++ b/hubs/static/client/app/widgets/issues/Config.js
@@ -36,10 +36,10 @@ class Config extends React.Component {
                 <select className="form-control"
                         value={this.props.values.mode}
                         onChange={this.handleModeChanged}>
-                    <option value="1">
+                    <option value="author">
                         <FormattedMessage {...messages.select_author} />
                     </option>
-                    <option value="2">
+                    <option value="assignee">
                         <FormattedMessage {...messages.select_assignee} />
                     </option>
                 </select>
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 986f7d7..cebd3e1 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -10,13 +10,13 @@ from .functions import *
 
 class Issues(Widget):
 
-    name = "issues"
-    label = "Issues"
-    position = "both"
+    name = 'issues'
+    label = 'Issues'
+    position = 'both'
     parameters = [
         dict(
-            name="mode",
-            default="1",
+            name='mode',
+            default='author',
         )
     ]
     is_react = True
@@ -27,10 +27,10 @@ class Issues(Widget):
             hub_name = instance.hub.name
             props.update(dict(
                 title="Issues - " +
-                      ("Tickets I Filed" if int(instance.config["mode"]) == 1 else "Tickets Waiting on Me"),
+                      ('Tickets I Filed' if instance.config['mode'] == 'author' else 'Tickets Waiting on Me'),
                 urls=dict(
                     data=flask.url_for(
-                        "issues_data", hub=hub_name, idx=instance.idx),
+                        'issues_data', hub=hub_name, idx=instance.idx),
                 )
             ))
         return props
@@ -49,7 +49,7 @@ class DataView(WidgetView):
         [github, ghcnt] = GetGithubIssues(instance)()
 
         all_issues = bugzilla + pagure + github
-        all_issues.sort(key=lambda x: arrow.get(x["updated_at"]), reverse=True)
+        all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
 
-        data = {"issues": all_issues, "counts": {"bugzilla": bzcnt, "pagure": pcnt, "github": ghcnt}}
-        return dict(status="OK", data=data)
+        data = {'issues': all_issues, 'counts': {'bugzilla': bzcnt, 'pagure': pcnt, 'github': ghcnt}}
+        return dict(status='OK', data=data)
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index 4be18f4..6d79923 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -6,9 +6,9 @@ from hubs.widgets.caching import CachedFunction
 class GetBugzillaBugs(CachedFunction):
 
     def execute(self):
-        email = self.instance.hub.config["bugzilla_email"]
-        endpoint = "https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{\"{}\":\"{}\"}}]"
-        mode = "creator" if self.instance.config["mode"] == "1" else "assigned_to"
+        email = self.instance.hub.config['bugzilla_email']
+        endpoint = 'https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{"{}":"{}"}}]'
+        mode = 'creator' if self.instance.config['mode'] == 'author' else 'assigned_to'
         response = requests.get(endpoint.format(mode, email)).json()
         issues = response['result']['bugs']
         issues = issues[:min(5, len(issues))]
@@ -28,20 +28,24 @@ class GetBugzillaBugs(CachedFunction):
         return [res, total_count]
 
     def should_invalidate(self, message):
-        return False
+        email = self.instance.hub.config('bugzilla_email')
+        mode = self.instance.config['mode']
+        if message['topic'].endswith('bugzilla.bug.new') or \
+           message['topic'].endswith('bugzilla.bug.update'):
+            return message['msg']['bug']['creator' if mode == 'author' else 'assigned_to'] == email
 
     def should_invalidate_on_hub_config_change(self, old_config):
-        return True
+        return old_config['bugzilla_email'] != self.instance.hub.config['bugzilla_email']
 
 
 class GetPagureIssues(CachedFunction):
 
     def execute(self):
-        username = self.instance.hub.config["pagure_username"]
+        username = self.instance.hub.config['pagure_username']
         endpoint = "https://pagure.io/api/0/user/{}/issues?author={}"
-        author = "true" if self.instance.config["mode"] == "1" else "false"
+        author = self.instance.config['mode'] == 'author'
         response = requests.get(endpoint.format(username, author)).json()
-        issues = response['issues_created'] if author == "true" else response['issues_assigned']
+        issues = response['issues_created'] if author else response['issues_assigned']
         issues = issues[:min(5, len(issues))]
         urltmpl = 'https://pagure.io/{}/issue/{}'
         res = []
@@ -55,14 +59,24 @@ class GetPagureIssues(CachedFunction):
                 service='pagure',
                 icon='https://pagure.io/static/favicon.ico'
             ))
-        total_count = len(response['issues_created'] if author == "true" else response['issues_assigned'])
+        total_count = len(response['issues_created'] if author else response['issues_assigned'])
         return [res, total_count]
 
     def should_invalidate(self, message):
-        return False
+        mode = self.instance.config['mode']
+        username = self.instance.hub.config('pagure_username')
+        if message['topic'].endswith('pagure.issue.new') or \
+           message['topic'].endswith('pagure.issue.edit') or \
+           message['topic'].endswith('pagure.issue.assigned.added') or \
+           message['topic'].endswith('pagure.issue.assigned.reset') or \
+           message['topic'].endswith('pagure.issue.drop'):
+            if mode == 'author':
+                return message['msg']['user']['name'] == username
+            elif mode == 'assignee':
+                return message['msg']['assignee']['name'] == username
 
     def should_invalidate_on_hub_config_change(self, old_config):
-        return True
+        return old_config['pagure_username'] != self.instance.hub.config['pagure_username']
 
 
 class GetGithubIssues(CachedFunction):
@@ -70,7 +84,7 @@ class GetGithubIssues(CachedFunction):
     def execute(self):
         username = self.instance.hub.config["github_username"]
         endpoint = "https://api.github.com/search/issues?q=type:issue+{}:{}+state:open&sort:updated"
-        mode = "author" if self.instance.config["mode"] == "1" else "assignee"
+        mode = "author" if self.instance.config['mode'] == 'author' else "assignee"
         response = requests.get(endpoint.format(mode, username)).json()
         issues = response['items']
         issues = issues[:min(5, len(issues))]
@@ -92,4 +106,4 @@ class GetGithubIssues(CachedFunction):
         return False
 
     def should_invalidate_on_hub_config_change(self, old_config):
-        return True
+        return old_config['github_username'] != self.instance.hub.config['github_username']

From ade03b6c79a47628bec3fc8aa3db5bde1869a513 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 7/32] Issues: Add tests and fix resulting errors


---

diff --git a/hubs/tests/widgets/test_issues.py b/hubs/tests/widgets/test_issues.py
new file mode 100644
index 0000000..cfb52a2
--- /dev/null
+++ b/hubs/tests/widgets/test_issues.py
@@ -0,0 +1,66 @@
+from __future__ import unicode_literals
+
+from . import WidgetTest
+
+
+class IssuesTest(WidgetTest):
+
+    plugin = "issues"
+
+    def populate(self):
+        super(IssuesTest, self).populate()
+        self._add_widget_under_test()
+
+    def test_author_should_invalidate_bugzilla(self):
+        msg = {
+            'topic': 'tests.bugzilla.bug.new',
+            'msg': {
+                'bug': {
+                    'creator': self.widget.hub.config['bugzilla_email'],
+                },
+            },
+        }
+        func = self.widget.module.get_cached_functions()['GetBugzillaBugs']
+        self.assertTrue(func(self.widget).should_invalidate(msg))
+
+    def test_author_should_invalidate_pagure(self):
+        msg = {
+            'topic': 'tests.pagure.issue.new',
+            'msg': {
+                'user': {
+                    'name': self.widget.hub.config['pagure_username'],
+                },
+            },
+        }
+        func = self.widget.module.get_cached_functions()['GetPagureIssues']
+        self.assertTrue(func(self.widget).should_invalidate(msg))
+
+    def test_assignee_should_invalidate_bugzilla(self):
+        self._update_widget_config({
+            "mode": "assignee",
+        })
+        msg = {
+            'topic': 'tests.bugzilla.bug.new',
+            'msg': {
+                'bug': {
+                    'assigned_to': self.widget.hub.config['bugzilla_email'],
+                },
+            },
+        }
+        func = self.widget.module.get_cached_functions()['GetBugzillaBugs']
+        self.assertTrue(func(self.widget).should_invalidate(msg))
+
+    def test_assignee_should_invalidate_pagure(self):
+        self._update_widget_config({
+            "mode": "assignee",
+        })
+        msg = {
+            'topic': 'tests.pagure.issue.new',
+            'msg': {
+                'assignee': {
+                    'name': self.widget.hub.config['pagure_username'],
+                },
+            },
+        }
+        func = self.widget.module.get_cached_functions()['GetPagureIssues']
+        self.assertTrue(func(self.widget).should_invalidate(msg))
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index cebd3e1..b3e8c62 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -20,6 +20,7 @@ class Issues(Widget):
         )
     ]
     is_react = True
+    cached_functions_module = ".functions"
 
     def get_props(self, instance, *args, **kwargs):
         props = super(Issues, self).get_props(instance, *args, **kwargs)
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index 6d79923..226ec49 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -28,7 +28,7 @@ class GetBugzillaBugs(CachedFunction):
         return [res, total_count]
 
     def should_invalidate(self, message):
-        email = self.instance.hub.config('bugzilla_email')
+        email = self.instance.hub.config['bugzilla_email']
         mode = self.instance.config['mode']
         if message['topic'].endswith('bugzilla.bug.new') or \
            message['topic'].endswith('bugzilla.bug.update'):
@@ -64,7 +64,7 @@ class GetPagureIssues(CachedFunction):
 
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
-        username = self.instance.hub.config('pagure_username')
+        username = self.instance.hub.config['pagure_username']
         if message['topic'].endswith('pagure.issue.new') or \
            message['topic'].endswith('pagure.issue.edit') or \
            message['topic'].endswith('pagure.issue.assigned.added') or \

From ccd9dc60489f376b33c95227bb08c7b9ed18d5af Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 8/32] Issues: Add links to view more


---

diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 1a98c7b..7e7ba26 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -14,8 +14,8 @@ export default class IssuesWidget extends React.PureComponent {
     super(props);
     this.state = {
       issues: [],
-      counts: {},
-      isLoading: false
+      more: {},
+      isLoading: true
     }
   }
 
@@ -46,7 +46,7 @@ export default class IssuesWidget extends React.PureComponent {
       success: (res) => {
         this.setState({
             issues: res.data.issues,
-            counts: res.data.counts,
+            more: res.data.more,
             isLoading: false
           },
           () => (
@@ -67,63 +67,70 @@ export default class IssuesWidget extends React.PureComponent {
         widget={this.props.widget}
         editMode={this.props.editMode}
       >
-        <ul className="list-unstyled">
-          { this.state.isLoading && <Spinner className="p-3"/>}
-          {
-            this.state.issues.map((issue) => {
-              return (
-                <li key={issue.service + issue.number} className="py-2">
-                  <div className="row">
-                    <div className="col-md-6">
-                      <a  href={ issue.url }
-                          target="_blank" rel="noopener noreferrer">
-                        <span className="badge badge-primary">#{ issue.number }</span>
-                      </a>
+        { this.state.isLoading && <Spinner className="p-3"/>}
+        { !this.state.isLoading &&
+        <div>
+          <ul className="list-unstyled">
+            {
+              this.state.issues.map((issue) => {
+                return (
+                  <li key={issue.service + issue.number} className="py-2">
+                    <div className="row">
+                      <div className="col-md-6">
+                        <a  href={ issue.url }
+                            target="_blank" rel="noopener noreferrer">
+                          <span className="badge badge-primary">#{ issue.number }</span>
+                        </a>
+                      </div>
+                      <div className="col-md-6">
+                        <div className="float-right">
+                          <span>{ issue.project }</span>
+                          <img src={ issue.icon } style={{height: '16px'}} className={"ml-2"}/>
+                        </div>
+                      </div>
                     </div>
-                    <div className="col-md-6">
-                      <div className="float-right">
-                        <span>{ issue.project }</span>
-                        <img src={ issue.icon } style={{height: '16px'}} className={"ml-2"}/>
+                    <div className="row">
+                      <div className="col-12">
+                        <span>{ issue.title }</span>
                       </div>
                     </div>
+                  </li>
+                )
+              })
+            }
+          </ul>
+          <div className="card-footer p-1">
+            <div className="row p-1">
+              <div className="col-md-3 text-muted"
+                   data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}
+              >
+                <span>View More</span>
+                <i className="fa fa-angle-right ml-1" aria-hidden="true" />
+              </div>
+              <div className="col-md-9 collapse" id={'counts'}>
+                <div className="row pt-0">
+                  <div className="col-md-4 text-center">
+                    <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                    <a target="blank"
+                       href={ this.state.more.bugzilla.link }>Bugzilla ({ this.state.more.bugzilla.count })</a>
                   </div>
-                  <div className="row">
-                    <div className="col-12">
-                      <span>{ issue.title }</span>
-                    </div>
+                  <div className="col-md-4 text-center">
+                    <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                    <a target="blank"
+                       href={ this.state.more.github.link }>Github ({ this.state.more.github.count })</a>
+                  </div>
+                  <div className="col-md-4 text-center">
+                    <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                    <a target="blank"
+                       href={ this.state.more.pagure.link }>Pagure ({ this.state.more.pagure.count })</a>
                   </div>
-                </li>
-              )
-            })
-          }
-        </ul>
-        <div className="card-footer p-1">
-          <div className="row p-1">
-            <div className="col-md-3 text-muted"
-                 data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}
-            >
-              <span>View More</span>
-              <i className="fa fa-angle-right ml-1" aria-hidden="true" />
-            </div>
-            <div className="col-md-9 collapse" id={'counts'}>
-              <div className="row pt-0">
-                <div className="col-md-4 text-center">
-                  <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                  <span>Bugzilla ({ this.state.counts.bugzilla })</span>
-                </div>
-                <div className="col-md-4 text-center">
-                  <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                  <span>Github ({ this.state.counts.github })</span>
-                </div>
-                <div className="col-md-4 text-center">
-                  <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                  <span>Pagure ({ this.state.counts.pagure })</span>
                 </div>
-              </div>
 
+              </div>
             </div>
           </div>
         </div>
+        }
       </WidgetChrome>
     )
   }
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index b3e8c62..a14a7c5 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -52,5 +52,16 @@ class DataView(WidgetView):
         all_issues = bugzilla + pagure + github
         all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
 
-        data = {'issues': all_issues, 'counts': {'bugzilla': bzcnt, 'pagure': pcnt, 'github': ghcnt}}
+        data = {
+            'issues': all_issues,
+            'more': {
+                'bugzilla': {'count': bzcnt, 'link': '#'},
+                'pagure': {'count': pcnt,
+                           'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])},
+                'github': {'count': ghcnt,
+                           'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
+                                    .format(instance.config['mode'], instance.hub.config['github_username'])
+                           }
+            }
+        }
         return dict(status='OK', data=data)

From 5265fe04ee7bd4b49da495d470361ef506a95fdd Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 9/32] Issues: Update bugzilla endpoint


---

diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index a14a7c5..4838f88 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -55,9 +55,13 @@ class DataView(WidgetView):
         data = {
             'issues': all_issues,
             'more': {
-                'bugzilla': {'count': bzcnt, 'link': '#'},
+                'bugzilla': {'count': bzcnt,
+                             'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch=reporter:{}'
+                                     .format(instance.hub.config['bugzilla_email'])
+                             },
                 'pagure': {'count': pcnt,
-                           'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])},
+                           'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])
+                           },
                 'github': {'count': ghcnt,
                            'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
                                     .format(instance.config['mode'], instance.hub.config['github_username'])
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index 226ec49..7e1524a 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -7,7 +7,11 @@ class GetBugzillaBugs(CachedFunction):
 
     def execute(self):
         email = self.instance.hub.config['bugzilla_email']
-        endpoint = 'https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=[{{"{}":"{}"}}]'
+        endpoint = 'https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=' \
+                   '[{{' \
+                   '"{}":"{}",' \
+                   '"status":["NEW","VERIFIED","ASSIGNED","MODIFIED","ON_DEV","ON_QA","RELEASE_PENDING","POST"]' \
+                   '}}]'
         mode = 'creator' if self.instance.config['mode'] == 'author' else 'assigned_to'
         response = requests.get(endpoint.format(mode, email)).json()
         issues = response['result']['bugs']

From 6b159030fe6adb522e83f6715114cdec2ae13ed4 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 10/32] Issues: Fix bug in sorting


---

diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 4838f88..0a2fda4 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -51,6 +51,7 @@ class DataView(WidgetView):
 
         all_issues = bugzilla + pagure + github
         all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
+        all_issues = all_issues[:min(15, len(all_issues))]
 
         data = {
             'issues': all_issues,
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index 7e1524a..d033c99 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -15,7 +15,6 @@ class GetBugzillaBugs(CachedFunction):
         mode = 'creator' if self.instance.config['mode'] == 'author' else 'assigned_to'
         response = requests.get(endpoint.format(mode, email)).json()
         issues = response['result']['bugs']
-        issues = issues[:min(5, len(issues))]
         urltmpl = 'https://bugzilla.redhat.com/show_bug.cgi?id={}'
         res = []
         for issue in issues:
@@ -50,7 +49,6 @@ class GetPagureIssues(CachedFunction):
         author = self.instance.config['mode'] == 'author'
         response = requests.get(endpoint.format(username, author)).json()
         issues = response['issues_created'] if author else response['issues_assigned']
-        issues = issues[:min(5, len(issues))]
         urltmpl = 'https://pagure.io/{}/issue/{}'
         res = []
         for issue in issues:
@@ -91,7 +89,6 @@ class GetGithubIssues(CachedFunction):
         mode = "author" if self.instance.config['mode'] == 'author' else "assignee"
         response = requests.get(endpoint.format(mode, username)).json()
         issues = response['items']
-        issues = issues[:min(5, len(issues))]
         res = []
         for issue in issues:
             res.append(dict(

From 1db3dd1cb01cb783afeb38f902e9b21e8bac83ad Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 11/32] Issues: Fix bug in bugzilla endpoint


---

diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 0a2fda4..dbc919c 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -57,8 +57,9 @@ class DataView(WidgetView):
             'issues': all_issues,
             'more': {
                 'bugzilla': {'count': bzcnt,
-                             'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch=reporter:{}'
-                                     .format(instance.hub.config['bugzilla_email'])
+                             'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch={}:{}'
+                                     .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
+                                             instance.hub.config['bugzilla_email'])
                              },
                 'pagure': {'count': pcnt,
                            'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])

From 4d6634443a15f4d5a2773642c1b727e7191bbdaf Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 12/32] Issues: Move views to separate file


---

diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index dbc919c..751e0e3 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -1,11 +1,7 @@
 from __future__ import unicode_literals
-
-import arrow
 import flask
 
 from hubs.widgets.base import Widget
-from hubs.widgets.view import WidgetView
-from .functions import *
 
 
 class Issues(Widget):
@@ -21,6 +17,7 @@ class Issues(Widget):
     ]
     is_react = True
     cached_functions_module = ".functions"
+    views_module = ".views"
 
     def get_props(self, instance, *args, **kwargs):
         props = super(Issues, self).get_props(instance, *args, **kwargs)
@@ -35,39 +32,3 @@ class Issues(Widget):
                 )
             ))
         return props
-
-
-class DataView(WidgetView):
-    """Get issues in JSON format."""
-
-    name = "data"
-    url_rules = ["data"]
-    json = True
-
-    def get_context(self, instance, *args, **kwargs):
-        [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
-        [pagure, pcnt] = GetPagureIssues(instance)()
-        [github, ghcnt] = GetGithubIssues(instance)()
-
-        all_issues = bugzilla + pagure + github
-        all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
-        all_issues = all_issues[:min(15, len(all_issues))]
-
-        data = {
-            'issues': all_issues,
-            'more': {
-                'bugzilla': {'count': bzcnt,
-                             'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch={}:{}'
-                                     .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
-                                             instance.hub.config['bugzilla_email'])
-                             },
-                'pagure': {'count': pcnt,
-                           'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])
-                           },
-                'github': {'count': ghcnt,
-                           'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
-                                    .format(instance.config['mode'], instance.hub.config['github_username'])
-                           }
-            }
-        }
-        return dict(status='OK', data=data)
diff --git a/hubs/widgets/issues/views.py b/hubs/widgets/issues/views.py
new file mode 100644
index 0000000..b0ef61d
--- /dev/null
+++ b/hubs/widgets/issues/views.py
@@ -0,0 +1,39 @@
+from hubs.widgets.view import WidgetView
+from .functions import *
+import arrow
+
+
+class DataView(WidgetView):
+    """Get issues in JSON format."""
+
+    name = "data"
+    url_rules = ["data"]
+    json = True
+
+    def get_context(self, instance, *args, **kwargs):
+        [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
+        [pagure, pcnt] = GetPagureIssues(instance)()
+        [github, ghcnt] = GetGithubIssues(instance)()
+
+        all_issues = bugzilla + pagure + github
+        all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
+        all_issues = all_issues[:min(15, len(all_issues))]
+
+        data = {
+            'issues': all_issues,
+            'more': {
+                'bugzilla': {'count': bzcnt,
+                             'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch={}:{}'
+                                     .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
+                                             instance.hub.config['bugzilla_email'])
+                             },
+                'pagure': {'count': pcnt,
+                           'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])
+                           },
+                'github': {'count': ghcnt,
+                           'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
+                                    .format(instance.config['mode'], instance.hub.config['github_username'])
+                           }
+            }
+        }
+        return dict(status='OK', data=data)

From d38da66d61714e82af9f2ea7b3f4c8003f2be58f Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 13/32] Issues: Handle exception in PagureUser validator


---

diff --git a/hubs/utils/validators.py b/hubs/utils/validators.py
index 8da8e1c..f319188 100644
--- a/hubs/utils/validators.py
+++ b/hubs/utils/validators.py
@@ -109,10 +109,13 @@ def FMNContext(value):
 
 def PagureUser(value):
     """Fails if the Pagure username does not exist."""
-    response = requests.get("https://pagure.io/user/%s" % value, timeout=5)
-    if response.ok:
-        return value
-    raise ValueError('Invalid Pagure username: {}'.format(value))
+    try:
+        response = requests.get("/".join([PAGURE_URL, "user", value]), timeout=5)
+    except requests.exceptions.ReadTimeout:
+        raise ValueError("Could not connect to Pagure, please try again.")
+    if not response.ok:
+        raise ValueError('Invalid Pagure username: {}'.format(value))
+    return value
 
 
 def PagureRepo(value):

From efbcd808d50ee575880b3a6e79435ae70f137a18 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 14/32] Issues: Handle missing hub config


---

diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 7e7ba26..8daf21a 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -1,8 +1,5 @@
 import React from 'react';
-import {
-  widgetDidUpdate,
-  widgetWillUpdate
-} from "../../core/actions/widget";
+import { apiCall } from '../../core/utils';
 import WidgetChrome from '../../components/WidgetChrome';
 import PropTypes from "prop-types";
 import Spinner from "../../components/Spinner";
@@ -15,8 +12,10 @@ export default class IssuesWidget extends React.PureComponent {
     this.state = {
       issues: [],
       more: {},
+      error: null,
       isLoading: true
-    }
+    };
+    this.loadFromServer = this.loadFromServer.bind(this);
   }
 
   componentDidMount() {
@@ -25,112 +24,103 @@ export default class IssuesWidget extends React.PureComponent {
     }
   }
 
-  componentWillUnmount() {
-    this.serverRequest.abort();
-  }
-
-
-  componentDidUpdate(prevProps, prevState) {
-    if (this.props.needsUpdate && !this.props.editMode && !this.state.isLoading) {
-      this.loadFromServer();
-    }
-  }
-
   loadFromServer() {
-    this.props.dispatch(widgetWillUpdate(this.props.widget.idx));
     this.setState({isLoading: true});
-    this.serverRequest = $.ajax({
-      url: this.props.widget.urls.data,
-      method: 'GET',
-      cache: false,
-      success: (res) => {
-        this.setState({
-            issues: res.data.issues,
-            more: res.data.more,
-            isLoading: false
-          },
-          () => (
-            this.props.dispatch(widgetDidUpdate(this.props.widget.idx))
-          )
-        );
+    apiCall(this.props.widget.urls.data).then(
+      (data) => {
+        this.setState({issues: data.issues, more: data.more, isLoading: false});
       },
-      error: (xhr, status, err) => {
-        console.error(this.props.widget.urls.data, status, err.toString());
-        this.setState({isLoading: false});
+      (error) => {
+        this.setState({error: error.message, isLoading: false});
       }
-    });
+    );
   }
 
   render() {
-    return (
-      <WidgetChrome
-        widget={this.props.widget}
-        editMode={this.props.editMode}
-      >
-        { this.state.isLoading && <Spinner className="p-3"/>}
-        { !this.state.isLoading &&
-        <div>
-          <ul className="list-unstyled">
-            {
-              this.state.issues.map((issue) => {
-                return (
-                  <li key={issue.service + issue.number} className="py-2">
-                    <div className="row">
-                      <div className="col-md-6">
-                        <a  href={ issue.url }
-                            target="_blank" rel="noopener noreferrer">
-                          <span className="badge badge-primary">#{ issue.number }</span>
-                        </a>
+    let content = null;
+    if (this.state.isLoading) {
+      content = <Spinner className="p-3"/>
+    }
+    else {
+      if (this.state.error) {
+        content = (
+          <div className="alert alert-warning m-2">
+            {this.state.error}
+          </div>
+        )
+      }
+      else {
+        content = (
+          <div>
+            <ul className="list-unstyled">
+              {
+                this.state.issues.map((issue) => {
+                  return (
+                    <li key={issue.service + issue.number} className="py-2">
+                      <div className="row">
+                        <div className="col-md-6">
+                          <a href={issue.url}
+                             target="_blank" rel="noopener noreferrer">
+                            <span className="badge badge-primary">#{issue.number}</span>
+                          </a>
+                        </div>
+                        <div className="col-md-6">
+                          <div className="float-right">
+                            <span>{issue.project}</span>
+                            <img src={issue.icon} style={{height: '16px'}} className={"ml-2"}/>
+                          </div>
+                        </div>
                       </div>
-                      <div className="col-md-6">
-                        <div className="float-right">
-                          <span>{ issue.project }</span>
-                          <img src={ issue.icon } style={{height: '16px'}} className={"ml-2"}/>
+                      <div className="row">
+                        <div className="col-12">
+                          <span>{issue.title}</span>
                         </div>
                       </div>
+                    </li>
+                  )
+                })
+              }
+            </ul>
+            <div className="card-footer p-1">
+              <div className="row p-1">
+                <div className="col-md-3 text-muted"
+                     data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}
+                >
+                  <span>View More</span>
+                  <i className="fa fa-angle-right ml-1" aria-hidden="true"/>
+                </div>
+                <div className="col-md-9 collapse" id={'counts'}>
+                  <div className="row pt-0">
+                    <div className="col-md-4 text-center">
+                      <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                      <a target="blank"
+                         href={this.state.more.bugzilla.link}>Bugzilla ({this.state.more.bugzilla.count})</a>
                     </div>
-                    <div className="row">
-                      <div className="col-12">
-                        <span>{ issue.title }</span>
-                      </div>
+                    <div className="col-md-4 text-center">
+                      <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                      <a target="blank"
+                         href={this.state.more.github.link}>Github ({this.state.more.github.count})</a>
+                    </div>
+                    <div className="col-md-4 text-center">
+                      <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                      <a target="blank"
+                         href={this.state.more.pagure.link}>Pagure ({this.state.more.pagure.count})</a>
                     </div>
-                  </li>
-                )
-              })
-            }
-          </ul>
-          <div className="card-footer p-1">
-            <div className="row p-1">
-              <div className="col-md-3 text-muted"
-                   data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}
-              >
-                <span>View More</span>
-                <i className="fa fa-angle-right ml-1" aria-hidden="true" />
-              </div>
-              <div className="col-md-9 collapse" id={'counts'}>
-                <div className="row pt-0">
-                  <div className="col-md-4 text-center">
-                    <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                    <a target="blank"
-                       href={ this.state.more.bugzilla.link }>Bugzilla ({ this.state.more.bugzilla.count })</a>
-                  </div>
-                  <div className="col-md-4 text-center">
-                    <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                    <a target="blank"
-                       href={ this.state.more.github.link }>Github ({ this.state.more.github.count })</a>
-                  </div>
-                  <div className="col-md-4 text-center">
-                    <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                    <a target="blank"
-                       href={ this.state.more.pagure.link }>Pagure ({ this.state.more.pagure.count })</a>
                   </div>
-                </div>
 
+                </div>
               </div>
             </div>
           </div>
-        </div>
-        }
+        )
+      }
+    }
+    return (
+      <WidgetChrome
+        widget={this.props.widget}
+        editMode={this.props.editMode}
+      >
+        {content}
       </WidgetChrome>
     )
   }
diff --git a/hubs/widgets/issues/views.py b/hubs/widgets/issues/views.py
index b0ef61d..aea1c95 100644
--- a/hubs/widgets/issues/views.py
+++ b/hubs/widgets/issues/views.py
@@ -11,9 +11,23 @@ class DataView(WidgetView):
     json = True
 
     def get_context(self, instance, *args, **kwargs):
-        [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
-        [pagure, pcnt] = GetPagureIssues(instance)()
-        [github, ghcnt] = GetGithubIssues(instance)()
+
+        if not (instance.hub.config['bugzilla_email'] or instance.hub.config['github_username'] or
+                instance.hub.config['pagure_username']):
+            return dict(
+                status="ERROR",
+                message="You must configure VCS details in the hub configuration in order to use this widget."
+            )
+
+        bugzilla = pagure = github = []
+        bzcnt = pcnt = ghcnt = None
+
+        if instance.hub.config['bugzilla_email']:
+            [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
+        if instance.hub.config['pagure_username']:
+            [pagure, pcnt] = GetPagureIssues(instance)()
+        if instance.hub.config['github_username']:
+            [github, ghcnt] = GetGithubIssues(instance)()
 
         all_issues = bugzilla + pagure + github
         all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)

From 52fc483fc8da9f0fe58b4de296348358e3301522 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 15/32] Issues: Show widget only on user hubs


---

diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 751e0e3..36a9b9e 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -16,6 +16,7 @@ class Issues(Widget):
         )
     ]
     is_react = True
+    hub_types = ['user']
     cached_functions_module = ".functions"
     views_module = ".views"
 

From 2d9f29fd05632d97710d68fceea6277d17e833ba Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 16/32] Issues: Refactoring after review


---

diff --git a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
index 6df26cf..fb7d147 100644
--- a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
+++ b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
@@ -58,22 +58,22 @@ export default class VCSPanel extends React.Component {
       <form onSubmit={(e) => {e.preventDefault();}}>
         <FormattedMessage {...messages.title} tagName="h3" />
         <FormattedMessage {...messages.intro} tagName="p" />
-        {["pagure_username", "github_username", "bugzilla_email"].map(function(x, i) {
+        {["pagure_username", "github_username", "bugzilla_email"].map(function(configParam) {
           return (
-            <div key={i} className="form-group row">
-              <label htmlFor={"hub-settings-vcs-" + x}>
-                <FormattedMessage {...messages[x]} />
+            <div key={configParam} className="form-group row">
+              <label htmlFor={"hub-settings-vcs-" + configParam}>
+                <FormattedMessage {...messages[configParam]} />
               </label>
               <input
                 type="text"
-                name={x}
+                name={configParam}
                 className={"form-control"}
-                id={"hub-settings-vcs-" + x}
+                id={"hub-settings-vcs-" + configParam}
                 onChange={this.handleChange}
-                value={this.props.hubConfig[x] || ""}
+                value={this.props.hubConfig[configParam] || ""}
                 />
               <p className="form-text text-muted">
-                <FormattedMessage {...messages[x + "_help"]} />
+                <FormattedMessage {...messages[configParam + "_help"]} />
               </p>
             </div>
           )
diff --git a/hubs/static/client/app/widgets/issues/Config.js b/hubs/static/client/app/widgets/issues/Config.js
index 82714f2..5ef68f8 100644
--- a/hubs/static/client/app/widgets/issues/Config.js
+++ b/hubs/static/client/app/widgets/issues/Config.js
@@ -1,52 +1,68 @@
 import React from 'react';
 import {
-    defineMessages,
-    injectIntl,
-    FormattedMessage
+  defineMessages,
+  FormattedMessage
 } from 'react-intl';
 
 
 const messages = defineMessages({
-    select_author: {
-        id: "hubs.widgets.halp.config.select.author",
-        defaultMessage: "Tickets I Filed",
-    },
-    select_assignee: {
-        id: "hubs.widgets.halp.config.select.assignee",
-        defaultMessage: "Tickets Waiting on Me",
-    },
+  select_author: {
+    id: "hubs.widgets.halp.config.select.author",
+    defaultMessage: "Tickets I Filed",
+  },
+  select_assignee: {
+    id: "hubs.widgets.halp.config.select.assignee",
+    defaultMessage: "Tickets Waiting on Me",
+  },
 });
 
 
 class Config extends React.Component {
 
-    constructor(props) {
-        super(props);
-        this.handleModeChanged = this.handleModeChanged.bind(this);
-    }
-
-    handleModeChanged(e) {
-        this.props.setConfig("mode", e.target.value);
-    }
-
-    render() {
-        return (
-            <div className="form-group">
-                <label>Mode</label>
-                <select className="form-control"
-                        value={this.props.values.mode}
-                        onChange={this.handleModeChanged}>
-                    <option value="author">
-                        <FormattedMessage {...messages.select_author} />
-                    </option>
-                    <option value="assignee">
-                        <FormattedMessage {...messages.select_assignee} />
-                    </option>
-                </select>
-            </div>
-        );
-    }
+  constructor(props) {
+    super(props);
+    this.handleModeChanged = this.handleModeChanged.bind(this);
+    this.handleCountChanged = this.handleCountChanged.bind(this);
+  }
+
+  handleModeChanged(e) {
+    let mode = e.target.value;
+    this.props.setConfig("mode", mode);
+  }
+
+  handleCountChanged(e) {
+    let count = e.target.value;
+    this.props.setConfig("count", count);
+  }
+
+  render() {
+    return (
+      <div>
+        <div className="form-group">
+          <label>Mode</label>
+          <select className="form-control"
+                  value={this.props.values.mode}
+                  onChange={this.handleModeChanged}>
+            <option value="author">
+              <FormattedMessage {...messages.select_author} />
+            </option>
+            <option value="assignee">
+              <FormattedMessage {...messages.select_assignee} />
+            </option>
+          </select>
+        </div>
+        <div className="form-group">
+          <label>Count</label>
+          <input type="number"
+                 className="form-control"
+                 value={this.props.values.count}
+                 onChange={this.handleCountChanged}
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
 
-export default injectIntl(Config);
+export default Config;
diff --git a/hubs/static/client/app/widgets/issues/Footer.js b/hubs/static/client/app/widgets/issues/Footer.js
new file mode 100644
index 0000000..258bd00
--- /dev/null
+++ b/hubs/static/client/app/widgets/issues/Footer.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from "prop-types";
+
+
+export default class Footer extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      expanded: false
+    };
+    this.toggleCounts = this.toggleCounts.bind(this);
+  }
+
+  toggleCounts() {
+    this.setState({
+      expanded: !this.state.expanded
+    });
+  }
+
+  render() {
+    return (
+      <div className="card-footer p-1">
+        <div className="row p-1">
+          <div className="col-md-3 text-muted" style={{cursor: 'pointer'}} onClick={this.toggleCounts}>
+            <span>View More</span>
+            <i className="fa fa-angle-right ml-1" aria-hidden="true"/>
+          </div>
+          {this.state.expanded &&
+          <div className="col-md-9">
+            <div className="row pt-0">
+              {this.props.counts.bugzilla &&
+              <div className="col-md-4 text-center">
+                <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                <a target="blank"
+                   href={this.props.counts.bugzilla.link}>Bugzilla ({this.props.counts.bugzilla.count})</a>
+              </div>
+              }
+              {this.props.counts.github &&
+              <div className="col-md-4 text-center">
+                <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                <a target="blank"
+                   href={this.props.counts.github.link}>Github ({this.props.counts.github.count})</a>
+              </div>
+              }
+              {this.props.counts.pagure &&
+              <div className="col-md-4 text-center">
+                <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                <a target="blank"
+                   href={this.props.counts.pagure.link}>Pagure ({this.props.counts.pagure.count})</a>
+              </div>
+              }
+            </div>
+
+          </div>
+          }
+        </div>
+      </div>
+    )
+  }
+}
+Footer.propTypes = {
+  counts: PropTypes.object.isRequired,
+};
diff --git a/hubs/static/client/app/widgets/issues/Issue.js b/hubs/static/client/app/widgets/issues/Issue.js
new file mode 100644
index 0000000..eb5094b
--- /dev/null
+++ b/hubs/static/client/app/widgets/issues/Issue.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from "prop-types";
+
+
+export default class Issue extends React.Component {
+
+  render() {
+    return (
+      <li className="py-2">
+        <div className="row">
+          <div className="col-md-6">
+            <a href={this.props.issue.url}
+               target="_blank" rel="noopener noreferrer">
+              <span className="badge badge-primary">#{this.props.issue.number}</span>
+            </a>
+          </div>
+          <div className="col-md-6">
+            <div className="float-right">
+              <span>{this.props.issue.project}</span>
+              <img src={this.props.issue.icon} style={{height: '16px'}} className={"ml-2"}/>
+            </div>
+          </div>
+        </div>
+        <div className="row">
+          <div className="col-12">
+            <span>{this.props.issue.title}</span>
+          </div>
+        </div>
+      </li>
+    )
+  }
+}
+Issue.propTypes = {
+  issue: PropTypes.object.isRequired,
+};
diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 8daf21a..b83bab6 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -3,6 +3,8 @@ import { apiCall } from '../../core/utils';
 import WidgetChrome from '../../components/WidgetChrome';
 import PropTypes from "prop-types";
 import Spinner from "../../components/Spinner";
+import Issue from "./Issue";
+import Footer from "./Footer";
 
 
 export default class IssuesWidget extends React.PureComponent {
@@ -11,7 +13,7 @@ export default class IssuesWidget extends React.PureComponent {
     super(props);
     this.state = {
       issues: [],
-      more: {},
+      counts: {},
       error: null,
       isLoading: true
     };
@@ -28,7 +30,7 @@ export default class IssuesWidget extends React.PureComponent {
     this.setState({isLoading: true});
     apiCall(this.props.widget.urls.data).then(
       (data) => {
-        this.setState({issues: data.issues, more: data.more, isLoading: false});
+        this.setState({issues: data.issues, counts: data.counts, isLoading: false});
       },
       (error) => {
         this.setState({error: error.message, isLoading: false});
@@ -41,79 +43,28 @@ export default class IssuesWidget extends React.PureComponent {
     if (this.state.isLoading) {
       content = <Spinner className="p-3"/>
     }
+    else if (this.state.error) {
+      content = (
+        <div className="alert alert-warning m-2">
+          {this.state.error}
+        </div>
+      )
+    }
     else {
-      if (this.state.error) {
-        content = (
-          <div className="alert alert-warning m-2">
-            {this.state.error}
-          </div>
-        )
-      }
-      else {
-        content = (
-          <div>
-            <ul className="list-unstyled">
-              {
-                this.state.issues.map((issue) => {
-                  return (
-                    <li key={issue.service + issue.number} className="py-2">
-                      <div className="row">
-                        <div className="col-md-6">
-                          <a href={issue.url}
-                             target="_blank" rel="noopener noreferrer">
-                            <span className="badge badge-primary">#{issue.number}</span>
-                          </a>
-                        </div>
-                        <div className="col-md-6">
-                          <div className="float-right">
-                            <span>{issue.project}</span>
-                            <img src={issue.icon} style={{height: '16px'}} className={"ml-2"}/>
-                          </div>
-                        </div>
-                      </div>
-                      <div className="row">
-                        <div className="col-12">
-                          <span>{issue.title}</span>
-                        </div>
-                      </div>
-                    </li>
-                  )
-                })
-              }
-            </ul>
-            <div className="card-footer p-1">
-              <div className="row p-1">
-                <div className="col-md-3 text-muted"
-                     data-toggle="collapse" data-target="#counts" style={{cursor: 'pointer'}}
-                >
-                  <span>View More</span>
-                  <i className="fa fa-angle-right ml-1" aria-hidden="true"/>
-                </div>
-                <div className="col-md-9 collapse" id={'counts'}>
-                  <div className="row pt-0">
-                    <div className="col-md-4 text-center">
-                      <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                      <a target="blank"
-                         href={this.state.more.bugzilla.link}>Bugzilla ({this.state.more.bugzilla.count})</a>
-                    </div>
-                    <div className="col-md-4 text-center">
-                      <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                      <a target="blank"
-                         href={this.state.more.github.link}>Github ({this.state.more.github.count})</a>
-                    </div>
-                    <div className="col-md-4 text-center">
-                      <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
-                      <a target="blank"
-                         href={this.state.more.pagure.link}>Pagure ({this.state.more.pagure.count})</a>
-                    </div>
-                  </div>
-
-                </div>
-              </div>
-            </div>
-          </div>
-        )
-      }
+      content = (
+        <div>
+          <ul className="list-unstyled">
+            {
+              this.state.issues.map((issue) => {
+                return (
+                  <Issue key={issue.service + issue.number} issue={issue} />
+                )
+              })
+            }
+          </ul>
+          <Footer counts={this.state.counts} />
+        </div>
+      )
     }
     return (
       <WidgetChrome
diff --git a/hubs/tests/widgets/test_issues.py b/hubs/tests/widgets/test_issues.py
index cfb52a2..7bc85df 100644
--- a/hubs/tests/widgets/test_issues.py
+++ b/hubs/tests/widgets/test_issues.py
@@ -12,6 +12,9 @@ class IssuesTest(WidgetTest):
         self._add_widget_under_test()
 
     def test_author_should_invalidate_bugzilla(self):
+        self._update_widget_config({
+            "mode": "author",
+        })
         msg = {
             'topic': 'tests.bugzilla.bug.new',
             'msg': {
@@ -23,7 +26,27 @@ class IssuesTest(WidgetTest):
         func = self.widget.module.get_cached_functions()['GetBugzillaBugs']
         self.assertTrue(func(self.widget).should_invalidate(msg))
 
+    def test_author_should_invalidate_github(self):
+        self._update_widget_config({
+            "mode": "author",
+        })
+        msg = {
+            'topic': 'tests.github.issue.opened',
+            'msg': {
+                'issue': {
+                    'user': {
+                        'login': self.widget.hub.config['github_username'],
+                    }
+                },
+            },
+        }
+        func = self.widget.module.get_cached_functions()['GetGithubIssues']
+        self.assertTrue(func(self.widget).should_invalidate(msg))
+
     def test_author_should_invalidate_pagure(self):
+        self._update_widget_config({
+            "mode": "author",
+        })
         msg = {
             'topic': 'tests.pagure.issue.new',
             'msg': {
@@ -50,6 +73,23 @@ class IssuesTest(WidgetTest):
         func = self.widget.module.get_cached_functions()['GetBugzillaBugs']
         self.assertTrue(func(self.widget).should_invalidate(msg))
 
+    def test_assignee_should_invalidate_github(self):
+        self._update_widget_config({
+            "mode": "assignee",
+        })
+        msg = {
+            'topic': 'tests.github.issue.opened',
+            'msg': {
+                'issue': {
+                    'assignee': {
+                        'login': self.widget.hub.config['github_username'],
+                    }
+                },
+            },
+        }
+        func = self.widget.module.get_cached_functions()['GetGithubIssues']
+        self.assertTrue(func(self.widget).should_invalidate(msg))
+
     def test_assignee_should_invalidate_pagure(self):
         self._update_widget_config({
             "mode": "assignee",
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 36a9b9e..90118c7 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -13,6 +13,10 @@ class Issues(Widget):
         dict(
             name='mode',
             default='author',
+        ),
+        dict(
+            name='count',
+            default=15,
         )
     ]
     is_react = True
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index d033c99..cf19bab 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -72,10 +72,7 @@ class GetPagureIssues(CachedFunction):
            message['topic'].endswith('pagure.issue.assigned.added') or \
            message['topic'].endswith('pagure.issue.assigned.reset') or \
            message['topic'].endswith('pagure.issue.drop'):
-            if mode == 'author':
-                return message['msg']['user']['name'] == username
-            elif mode == 'assignee':
-                return message['msg']['assignee']['name'] == username
+            return message['msg']['user' if mode == 'author' else 'assignee']['name'] == username
 
     def should_invalidate_on_hub_config_change(self, old_config):
         return old_config['pagure_username'] != self.instance.hub.config['pagure_username']
@@ -104,7 +101,13 @@ class GetGithubIssues(CachedFunction):
         return [res, total_count]
 
     def should_invalidate(self, message):
-        return False
+        mode = self.instance.config['mode']
+        username = self.instance.hub.config['github_username']
+        if message['topic'].endswith('github.issue.opened') or \
+           message['topic'].endswith('github.issue.reopened') or \
+           message['topic'].endswith('github.issue.edited') or \
+           message['topic'].endswith('github.issue.closed'):
+            return message['msg']['issue']['user' if mode == 'author' else 'assignee']['login'] == username
 
     def should_invalidate_on_hub_config_change(self, old_config):
         return old_config['github_username'] != self.instance.hub.config['github_username']
diff --git a/hubs/widgets/issues/views.py b/hubs/widgets/issues/views.py
index aea1c95..e6d1e48 100644
--- a/hubs/widgets/issues/views.py
+++ b/hubs/widgets/issues/views.py
@@ -31,23 +31,35 @@ class DataView(WidgetView):
 
         all_issues = bugzilla + pagure + github
         all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
-        all_issues = all_issues[:min(15, len(all_issues))]
+        all_issues = all_issues[:int(instance.config['count'])]
 
         data = {
             'issues': all_issues,
-            'more': {
+            'counts': {}
+        }
+
+        if bzcnt is not None:
+            data['counts'].update({
                 'bugzilla': {'count': bzcnt,
                              'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch={}:{}'
-                                     .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
-                                             instance.hub.config['bugzilla_email'])
-                             },
+                                 .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
+                                         instance.hub.config['bugzilla_email'])
+                             }
+            })
+
+        if pcnt is not None:
+            data['counts'].update({
                 'pagure': {'count': pcnt,
                            'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])
-                           },
+                           }
+            })
+
+        if ghcnt is not None:
+            data['counts'].update({
                 'github': {'count': ghcnt,
                            'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
-                                    .format(instance.config['mode'], instance.hub.config['github_username'])
+                               .format(instance.config['mode'], instance.hub.config['github_username'])
                            }
-            }
-        }
+            })
+
         return dict(status='OK', data=data)

From b303860ea0d542e5ec15c980b71b36b26ce40e98 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 17/32] Issues: Rename config parameter


---

diff --git a/hubs/static/client/app/widgets/issues/Config.js b/hubs/static/client/app/widgets/issues/Config.js
index 5ef68f8..c3ce202 100644
--- a/hubs/static/client/app/widgets/issues/Config.js
+++ b/hubs/static/client/app/widgets/issues/Config.js
@@ -52,7 +52,7 @@ class Config extends React.Component {
           </select>
         </div>
         <div className="form-group">
-          <label>Count</label>
+          <label>Number of Tickets</label>
           <input type="number"
                  className="form-control"
                  value={this.props.values.count}

From 494fb93e6a0a85978ae730bb3f1a67ebdfa27e42 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 18/32] Issues: Fix bug in cache invalidation


---

diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index cf19bab..cf34813 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -72,7 +72,8 @@ class GetPagureIssues(CachedFunction):
            message['topic'].endswith('pagure.issue.assigned.added') or \
            message['topic'].endswith('pagure.issue.assigned.reset') or \
            message['topic'].endswith('pagure.issue.drop'):
-            return message['msg']['user' if mode == 'author' else 'assignee']['name'] == username
+            entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
+            return entry and entry['name'] == username
 
     def should_invalidate_on_hub_config_change(self, old_config):
         return old_config['pagure_username'] != self.instance.hub.config['pagure_username']
@@ -107,7 +108,8 @@ class GetGithubIssues(CachedFunction):
            message['topic'].endswith('github.issue.reopened') or \
            message['topic'].endswith('github.issue.edited') or \
            message['topic'].endswith('github.issue.closed'):
-            return message['msg']['issue']['user' if mode == 'author' else 'assignee']['login'] == username
+            entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
+            return entry and entry['login'] == username
 
     def should_invalidate_on_hub_config_change(self, old_config):
         return old_config['github_username'] != self.instance.hub.config['github_username']

From 32fbfac5a1adea92d5ef7eae1feb794744acc127 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 19/32] Issues: Set title in React


---

diff --git a/hubs/models/hubconfig.py b/hubs/models/hubconfig.py
index a4a8db6..32e4106 100644
--- a/hubs/models/hubconfig.py
+++ b/hubs/models/hubconfig.py
@@ -107,7 +107,7 @@ class HubConfigProxy(MutableMapping):
         "chat_nickname", "email", "creation_date", "country",
         "pagure_username", "github_username", "bugzilla_email",
         "mailing_list_url",
-           ) + tuple(p["name"] for p in DEV_PLATFORMS)
+    ) + tuple(p["name"] for p in DEV_PLATFORMS)
     CONVERTERS = {
         "archived": BooleanConverter(),
         "left_width": Converter(int),
diff --git a/hubs/static/client/app/components/WidgetChrome.js b/hubs/static/client/app/components/WidgetChrome.js
index 3c94f64..2cca9d8 100644
--- a/hubs/static/client/app/components/WidgetChrome.js
+++ b/hubs/static/client/app/components/WidgetChrome.js
@@ -15,6 +15,7 @@ class WidgetChrome extends React.PureComponent {
 
   propTypes: {
     widget: PropTypes.object.isRequired,
+    title: PropTypes.string,
     editMode: PropTypes.bool,
   }
   defaultProps: {
@@ -39,7 +40,7 @@ class WidgetChrome extends React.PureComponent {
 
   render() {
     // Make sure there's always a title in edit mode
-    const title = (this.props.editMode && !this.props.widget.title) ? this.props.widget.label : this.props.widget.title;
+    const title = (this.props.editMode && !this.props.title) ? this.props.widget.label : this.props.title;
     let cssClass = `WidgetChrome card widget-${this.props.widget.name}`;
     if (this.props.widget.cssClass) {
       cssClass += " " + this.props.widget.cssClass;
@@ -93,9 +94,10 @@ class WidgetChrome extends React.PureComponent {
 }
 
 
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
   return {
     editMode: state.ui.widgetsEditMode,
+    title: ownProps.title || ownProps.widget.title,
   }
 };
 const mapDispatchToProps = dispatch => {
diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index b83bab6..6baecbb 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -40,6 +40,7 @@ export default class IssuesWidget extends React.PureComponent {
 
   render() {
     let content = null;
+    let subtitle = this.props.widget.mode === "author" ? "Tickets I Filed" : "Tickets Waiting on Me";
     if (this.state.isLoading) {
       content = <Spinner className="p-3"/>
     }
@@ -70,6 +71,7 @@ export default class IssuesWidget extends React.PureComponent {
       <WidgetChrome
         widget={this.props.widget}
         editMode={this.props.editMode}
+        title={"Issues - " + subtitle}
       >
         {content}
       </WidgetChrome>
diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 90118c7..0388121 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -29,8 +29,7 @@ class Issues(Widget):
         if instance is not None:
             hub_name = instance.hub.name
             props.update(dict(
-                title="Issues - " +
-                      ('Tickets I Filed' if instance.config['mode'] == 'author' else 'Tickets Waiting on Me'),
+                mode=instance.config['mode'],
                 urls=dict(
                     data=flask.url_for(
                         'issues_data', hub=hub_name, idx=instance.idx),

From c3ea33aeed6db59bf5c31b0fa997171378939e1f Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 20/32] Issues: Update cache invalidation


---

diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index cf34813..c71b50e 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -36,9 +36,11 @@ class GetBugzillaBugs(CachedFunction):
         if message['topic'].endswith('bugzilla.bug.new') or \
            message['topic'].endswith('bugzilla.bug.update'):
             return message['msg']['bug']['creator' if mode == 'author' else 'assigned_to'] == email
-
-    def should_invalidate_on_hub_config_change(self, old_config):
-        return old_config['bugzilla_email'] != self.instance.hub.config['bugzilla_email']
+        elif message["topic"].endswith('.hubs.hub.updated'):
+            if message['msg']['hub_id'] == self.instance.hub.id:
+                return 'bugzilla_email' in message['msg']['changed_keys']
+        else:
+            return False
 
 
 class GetPagureIssues(CachedFunction):
@@ -74,9 +76,11 @@ class GetPagureIssues(CachedFunction):
            message['topic'].endswith('pagure.issue.drop'):
             entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
             return entry and entry['name'] == username
-
-    def should_invalidate_on_hub_config_change(self, old_config):
-        return old_config['pagure_username'] != self.instance.hub.config['pagure_username']
+        elif message["topic"].endswith('.hubs.hub.updated'):
+            if message['msg']['hub_id'] == self.instance.hub.id:
+                return 'pagure_username' in message['msg']['changed_keys']
+        else:
+            return False
 
 
 class GetGithubIssues(CachedFunction):
@@ -110,6 +114,8 @@ class GetGithubIssues(CachedFunction):
            message['topic'].endswith('github.issue.closed'):
             entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
             return entry and entry['login'] == username
-
-    def should_invalidate_on_hub_config_change(self, old_config):
-        return old_config['github_username'] != self.instance.hub.config['github_username']
+        elif message["topic"].endswith('.hubs.hub.updated'):
+            if message['msg']['hub_id'] == self.instance.hub.id:
+                return 'github_username' in message['msg']['changed_keys']
+        else:
+            return False

From fb64942bd58b99cff631f85546209273fa788e4c Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 21/32] Issues: Never return None in cache invalidation


---

diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index c71b50e..e483442 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -37,8 +37,8 @@ class GetBugzillaBugs(CachedFunction):
            message['topic'].endswith('bugzilla.bug.update'):
             return message['msg']['bug']['creator' if mode == 'author' else 'assigned_to'] == email
         elif message["topic"].endswith('.hubs.hub.updated'):
-            if message['msg']['hub_id'] == self.instance.hub.id:
-                return 'bugzilla_email' in message['msg']['changed_keys']
+            return message['msg']['hub_id'] == self.instance.hub.id and \
+                   'bugzilla_email' in message['msg']['changed_keys']
         else:
             return False
 
@@ -77,8 +77,8 @@ class GetPagureIssues(CachedFunction):
             entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
             return entry and entry['name'] == username
         elif message["topic"].endswith('.hubs.hub.updated'):
-            if message['msg']['hub_id'] == self.instance.hub.id:
-                return 'pagure_username' in message['msg']['changed_keys']
+            return message['msg']['hub_id'] == self.instance.hub.id and \
+                   'pagure_username' in message['msg']['changed_keys']
         else:
             return False
 
@@ -115,7 +115,7 @@ class GetGithubIssues(CachedFunction):
             entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
             return entry and entry['login'] == username
         elif message["topic"].endswith('.hubs.hub.updated'):
-            if message['msg']['hub_id'] == self.instance.hub.id:
-                return 'github_username' in message['msg']['changed_keys']
+            return message['msg']['hub_id'] == self.instance.hub.id and \
+                   'github_username' in message['msg']['changed_keys']
         else:
             return False

From ad44edd9fe50353bef30fecdd07033845e0ff3d6 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 22/32] Issues: Change footer design


---

diff --git a/hubs/static/client/app/widgets/issues/Footer.js b/hubs/static/client/app/widgets/issues/Footer.js
index 258bd00..79ae7b5 100644
--- a/hubs/static/client/app/widgets/issues/Footer.js
+++ b/hubs/static/client/app/widgets/issues/Footer.js
@@ -22,38 +22,43 @@ export default class Footer extends React.Component {
     return (
       <div className="card-footer p-1">
         <div className="row p-1">
-          <div className="col-md-3 text-muted" style={{cursor: 'pointer'}} onClick={this.toggleCounts}>
-            <span>View More</span>
-            <i className="fa fa-angle-right ml-1" aria-hidden="true"/>
-          </div>
-          {this.state.expanded &&
-          <div className="col-md-9">
-            <div className="row pt-0">
+          <div className="text-center" style={{width: '100%'}}>
+            <div className={"btn-group " + (this.state.expanded ? "show": "")}>
+              <button className="btn btn-secondary dropdown-toggle" type="button" onClick={this.toggleCounts}>
+                View More
+              </button>
+            </div>
+            <div className={"dropdown-menu " + (this.state.expanded ? "show": "")}
+                 style={{left: "50%", transform: "translateX(-50%)"}}>
               {this.props.counts.bugzilla &&
-              <div className="col-md-4 text-center">
-                <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+              <div className="dropdown-item">
                 <a target="blank"
-                   href={this.props.counts.bugzilla.link}>Bugzilla ({this.props.counts.bugzilla.count})</a>
+                   href={this.props.counts.bugzilla.link}>
+                  <img src="https://bugzilla.redhat.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                  Bugzilla ({this.props.counts.bugzilla.count})
+                </a>
               </div>
               }
               {this.props.counts.github &&
-              <div className="col-md-4 text-center">
-                <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+              <div className="dropdown-item">
                 <a target="blank"
-                   href={this.props.counts.github.link}>Github ({this.props.counts.github.count})</a>
+                   href={this.props.counts.github.link}>
+                  <img src="https://github.com/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                  Github ({this.props.counts.github.count})
+                </a>
               </div>
               }
               {this.props.counts.pagure &&
-              <div className="col-md-4 text-center">
-                <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+              <div className="dropdown-item">
                 <a target="blank"
-                   href={this.props.counts.pagure.link}>Pagure ({this.props.counts.pagure.count})</a>
+                   href={this.props.counts.pagure.link}>
+                  <img src="https://pagure.io/static/favicon.ico" style={{height: '16px'}} className={"mr-2"}/>
+                  Pagure ({this.props.counts.pagure.count})
+                </a>
               </div>
               }
             </div>
-
           </div>
-          }
         </div>
       </div>
     )
diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 6baecbb..7e88cd1 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -56,9 +56,9 @@ export default class IssuesWidget extends React.PureComponent {
         <div>
           <ul className="list-unstyled">
             {
-              this.state.issues.map((issue) => {
+              this.state.issues.map((issue, index) => {
                 return (
-                  <Issue key={issue.service + issue.number} issue={issue} />
+                  <Issue key={issue.service + index} issue={issue} />
                 )
               })
             }

From f4094f2a579c045e6c4d993c64c32436a4327d08 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 23/32] Issues: Add to stream hubs


---

diff --git a/hubs/widgets/issues/__init__.py b/hubs/widgets/issues/__init__.py
index 0388121..a41e56f 100644
--- a/hubs/widgets/issues/__init__.py
+++ b/hubs/widgets/issues/__init__.py
@@ -20,7 +20,7 @@ class Issues(Widget):
         )
     ]
     is_react = True
-    hub_types = ['user']
+    hub_types = ['user', 'stream']
     cached_functions_module = ".functions"
     views_module = ".views"
 
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index e483442..8b4d935 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -1,12 +1,19 @@
 import requests
 
+from hubs.models.hub import Hub
 from hubs.widgets.caching import CachedFunction
 
 
 class GetBugzillaBugs(CachedFunction):
 
     def execute(self):
-        email = self.instance.hub.config['bugzilla_email']
+        if self.instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            email = user_hub.config.get('bugzilla_email')
+        else:
+            email = self.instance.hub.config.get('bugzilla_email')
+        if email is None:
+            return [[], None]
         endpoint = 'https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=' \
                    '[{{' \
                    '"{}":"{}",' \
@@ -31,7 +38,11 @@ class GetBugzillaBugs(CachedFunction):
         return [res, total_count]
 
     def should_invalidate(self, message):
-        email = self.instance.hub.config['bugzilla_email']
+        if self.instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            email = user_hub.config.get('bugzilla_email')
+        else:
+            email = self.instance.hub.config.get('bugzilla_email')
         mode = self.instance.config['mode']
         if message['topic'].endswith('bugzilla.bug.new') or \
            message['topic'].endswith('bugzilla.bug.update'):
@@ -46,7 +57,13 @@ class GetBugzillaBugs(CachedFunction):
 class GetPagureIssues(CachedFunction):
 
     def execute(self):
-        username = self.instance.hub.config['pagure_username']
+        if self.instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            username = user_hub.config.get('pagure_username')
+        else:
+            username = self.instance.hub.config.get('pagure_username')
+        if username is None:
+            return [[], None]
         endpoint = "https://pagure.io/api/0/user/{}/issues?author={}"
         author = self.instance.config['mode'] == 'author'
         response = requests.get(endpoint.format(username, author)).json()
@@ -68,7 +85,11 @@ class GetPagureIssues(CachedFunction):
 
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
-        username = self.instance.hub.config['pagure_username']
+        if self.instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            username = user_hub.config.get('pagure_username')
+        else:
+            username = self.instance.hub.config.get('pagure_username')
         if message['topic'].endswith('pagure.issue.new') or \
            message['topic'].endswith('pagure.issue.edit') or \
            message['topic'].endswith('pagure.issue.assigned.added') or \
@@ -86,7 +107,13 @@ class GetPagureIssues(CachedFunction):
 class GetGithubIssues(CachedFunction):
 
     def execute(self):
-        username = self.instance.hub.config["github_username"]
+        if self.instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            username = user_hub.config.get('github_username')
+        else:
+            username = self.instance.hub.config.get('github_username')
+        if username is None:
+            return [[], None]
         endpoint = "https://api.github.com/search/issues?q=type:issue+{}:{}+state:open&sort:updated"
         mode = "author" if self.instance.config['mode'] == 'author' else "assignee"
         response = requests.get(endpoint.format(mode, username)).json()
@@ -107,7 +134,11 @@ class GetGithubIssues(CachedFunction):
 
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
-        username = self.instance.hub.config['github_username']
+        if self.instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            username = user_hub.config.get('github_username')
+        else:
+            username = self.instance.hub.config.get('github_username')
         if message['topic'].endswith('github.issue.opened') or \
            message['topic'].endswith('github.issue.reopened') or \
            message['topic'].endswith('github.issue.edited') or \
diff --git a/hubs/widgets/issues/views.py b/hubs/widgets/issues/views.py
index e6d1e48..f4ddc72 100644
--- a/hubs/widgets/issues/views.py
+++ b/hubs/widgets/issues/views.py
@@ -12,22 +12,25 @@ class DataView(WidgetView):
 
     def get_context(self, instance, *args, **kwargs):
 
-        if not (instance.hub.config['bugzilla_email'] or instance.hub.config['github_username'] or
-                instance.hub.config['pagure_username']):
+        if instance.hub.hub_type == "stream":
+            user_hub = Hub.query.filter_by(name=instance.hub.name).first()
+            bugzilla_email = user_hub.config.get('bugzilla_email')
+            github_username = user_hub.config.get('github_username')
+            pagure_username = user_hub.config.get('pagure_username')
+        else:
+            bugzilla_email = instance.hub.config.get('bugzilla_email')
+            github_username = instance.hub.config.get('github_username')
+            pagure_username = instance.hub.config.get('pagure_username')
+
+        if not (bugzilla_email or github_username or pagure_username):
             return dict(
                 status="ERROR",
                 message="You must configure VCS details in the hub configuration in order to use this widget."
             )
 
-        bugzilla = pagure = github = []
-        bzcnt = pcnt = ghcnt = None
-
-        if instance.hub.config['bugzilla_email']:
-            [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
-        if instance.hub.config['pagure_username']:
-            [pagure, pcnt] = GetPagureIssues(instance)()
-        if instance.hub.config['github_username']:
-            [github, ghcnt] = GetGithubIssues(instance)()
+        [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
+        [pagure, pcnt] = GetPagureIssues(instance)()
+        [github, ghcnt] = GetGithubIssues(instance)()
 
         all_issues = bugzilla + pagure + github
         all_issues.sort(key=lambda x: arrow.get(x['updated_at']), reverse=True)
@@ -43,14 +46,14 @@ class DataView(WidgetView):
                 'bugzilla': {'count': bzcnt,
                              'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch={}:{}'
                                  .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
-                                         instance.hub.config['bugzilla_email'])
+                                         bugzilla_email)
                              }
             })
 
         if pcnt is not None:
             data['counts'].update({
                 'pagure': {'count': pcnt,
-                           'link': 'https://pagure.io/user/{}/issues'.format(instance.hub.config['pagure_username'])
+                           'link': 'https://pagure.io/user/{}/issues'.format(pagure_username)
                            }
             })
 
@@ -58,7 +61,7 @@ class DataView(WidgetView):
             data['counts'].update({
                 'github': {'count': ghcnt,
                            'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
-                               .format(instance.config['mode'], instance.hub.config['github_username'])
+                               .format(instance.config['mode'], github_username)
                            }
             })
 

From a05361d08923fa04a73a52463597e88e43415a92 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 24/32] Issues: Update cache invalidation


---

diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index 8b4d935..e29caa8 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -43,6 +43,8 @@ class GetBugzillaBugs(CachedFunction):
             email = user_hub.config.get('bugzilla_email')
         else:
             email = self.instance.hub.config.get('bugzilla_email')
+        if email is None:
+            return False
         mode = self.instance.config['mode']
         if message['topic'].endswith('bugzilla.bug.new') or \
            message['topic'].endswith('bugzilla.bug.update'):
@@ -90,6 +92,8 @@ class GetPagureIssues(CachedFunction):
             username = user_hub.config.get('pagure_username')
         else:
             username = self.instance.hub.config.get('pagure_username')
+        if username is None:
+            return False
         if message['topic'].endswith('pagure.issue.new') or \
            message['topic'].endswith('pagure.issue.edit') or \
            message['topic'].endswith('pagure.issue.assigned.added') or \
@@ -139,6 +143,8 @@ class GetGithubIssues(CachedFunction):
             username = user_hub.config.get('github_username')
         else:
             username = self.instance.hub.config.get('github_username')
+        if username is None:
+            return False
         if message['topic'].endswith('github.issue.opened') or \
            message['topic'].endswith('github.issue.reopened') or \
            message['topic'].endswith('github.issue.edited') or \

From 1169ce98b44a5c54d5e6e935b83ba72d9b046a85 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 25/32] Issues: Add message on no open issues


---

diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 7e88cd1..84bd147 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -51,6 +51,13 @@ export default class IssuesWidget extends React.PureComponent {
         </div>
       )
     }
+    else if (this.state.issues.length === 0) {
+      content = (
+        <div className="alert alert-warning m-2">
+          There are no open issues
+        </div>
+      )
+    }
     else {
       content = (
         <div>

From 0df3294a53e14a153d346937072eba4d54115500 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 26/32] Issues: Change No open issues message color


---

diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 84bd147..889d749 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -53,7 +53,7 @@ export default class IssuesWidget extends React.PureComponent {
     }
     else if (this.state.issues.length === 0) {
       content = (
-        <div className="alert alert-warning m-2">
+        <div className="alert alert-secondary m-2">
           There are no open issues
         </div>
       )

From 74093f685fbc471ff7f90274b30dfd44cdf525bb Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 27/32] Issues: Show validation errors in hub config


---

diff --git a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
index fb7d147..b4916c2 100644
--- a/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
+++ b/hubs/static/client/app/components/HubConfig/HubConfigPanelVCS.js
@@ -59,6 +59,10 @@ export default class VCSPanel extends React.Component {
         <FormattedMessage {...messages.title} tagName="h3" />
         <FormattedMessage {...messages.intro} tagName="p" />
         {["pagure_username", "github_username", "bugzilla_email"].map(function(configParam) {
+          let invalid = null;
+          if (this.props.error && this.props.error.fields) {
+            invalid = this.props.error.fields[configParam];
+          }
           return (
             <div key={configParam} className="form-group row">
               <label htmlFor={"hub-settings-vcs-" + configParam}>
@@ -67,11 +71,16 @@ export default class VCSPanel extends React.Component {
               <input
                 type="text"
                 name={configParam}
-                className={"form-control"}
+                className={"form-control" + (invalid ? " is-invalid" : "")}
                 id={"hub-settings-vcs-" + configParam}
                 onChange={this.handleChange}
                 value={this.props.hubConfig[configParam] || ""}
                 />
+              { invalid &&
+                <div className="invalid-feedback w-100">
+                  {invalid}
+                </div>
+              }
               <p className="form-text text-muted">
                 <FormattedMessage {...messages[configParam + "_help"]} />
               </p>

From d8f7684447a0ccc7a98c42a2eb1b5fcf8c9a0732 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 28/32] Issues: Update widget on backend change


---

diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 889d749..98bc88b 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -1,5 +1,9 @@
 import React from 'react';
 import { apiCall } from '../../core/utils';
+import {
+  widgetDidUpdate,
+  widgetWillUpdate
+  } from "../../core/actions/widget";
 import WidgetChrome from '../../components/WidgetChrome';
 import PropTypes from "prop-types";
 import Spinner from "../../components/Spinner";
@@ -7,7 +11,7 @@ import Issue from "./Issue";
 import Footer from "./Footer";
 
 
-export default class IssuesWidget extends React.PureComponent {
+export default class IssuesWidget extends React.Component {
 
   constructor(props) {
     super(props);
@@ -26,11 +30,26 @@ export default class IssuesWidget extends React.PureComponent {
     }
   }
 
+  componentDidUpdate(prevProps, prevState) {
+    if (this.props.needsUpdate && !this.props.editMode && !this.state.isLoading) {
+      this.loadFromServer();
+    }
+  }
+
   loadFromServer() {
+    this.props.dispatch(widgetWillUpdate(this.props.widget.idx));
     this.setState({isLoading: true});
     apiCall(this.props.widget.urls.data).then(
       (data) => {
-        this.setState({issues: data.issues, counts: data.counts, isLoading: false});
+        this.setState({
+          issues: data.issues,
+          counts: data.counts,
+          isLoading: false,
+        },
+          () => (
+            this.props.dispatch(widgetDidUpdate(this.props.widget.idx))
+          )
+        );
       },
       (error) => {
         this.setState({error: error.message, isLoading: false});

From 270e5725e8c8dc17044c7b4effdaf0b839f2bb01 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 29/32] Issues: Update CachedFunctions


---

diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index e29caa8..aaa1987 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -39,19 +39,19 @@ class GetBugzillaBugs(CachedFunction):
 
     def should_invalidate(self, message):
         if self.instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
-            email = user_hub.config.get('bugzilla_email')
+            watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
         else:
-            email = self.instance.hub.config.get('bugzilla_email')
+            watched_hub = self.instance.hub
+        if message["topic"].endswith('.hubs.hub.updated'):
+            return (message['msg']['hub_id'] == watched_hub.id and
+                    'bugzilla_email' in message['msg']['changed_keys'])
+        email = watched_hub.config.get('bugzilla_email')
         if email is None:
             return False
         mode = self.instance.config['mode']
         if message['topic'].endswith('bugzilla.bug.new') or \
            message['topic'].endswith('bugzilla.bug.update'):
             return message['msg']['bug']['creator' if mode == 'author' else 'assigned_to'] == email
-        elif message["topic"].endswith('.hubs.hub.updated'):
-            return message['msg']['hub_id'] == self.instance.hub.id and \
-                   'bugzilla_email' in message['msg']['changed_keys']
         else:
             return False
 
@@ -88,10 +88,13 @@ class GetPagureIssues(CachedFunction):
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
         if self.instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
-            username = user_hub.config.get('pagure_username')
+            watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
         else:
-            username = self.instance.hub.config.get('pagure_username')
+            watched_hub = self.instance.hub
+        if message["topic"].endswith('.hubs.hub.updated'):
+            return (message['msg']['hub_id'] == watched_hub.id and
+                    'pagure_username' in message['msg']['changed_keys'])
+        username = watched_hub.config.get('pagure_username')
         if username is None:
             return False
         if message['topic'].endswith('pagure.issue.new') or \
@@ -101,9 +104,6 @@ class GetPagureIssues(CachedFunction):
            message['topic'].endswith('pagure.issue.drop'):
             entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
             return entry and entry['name'] == username
-        elif message["topic"].endswith('.hubs.hub.updated'):
-            return message['msg']['hub_id'] == self.instance.hub.id and \
-                   'pagure_username' in message['msg']['changed_keys']
         else:
             return False
 
@@ -139,10 +139,13 @@ class GetGithubIssues(CachedFunction):
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
         if self.instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
-            username = user_hub.config.get('github_username')
+            watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
         else:
-            username = self.instance.hub.config.get('github_username')
+            watched_hub = self.instance.hub
+        if message["topic"].endswith('.hubs.hub.updated'):
+            return (message['msg']['hub_id'] == watched_hub.id and
+                    'github_username' in message['msg']['changed_keys'])
+        username = watched_hub.config.get('github_username')
         if username is None:
             return False
         if message['topic'].endswith('github.issue.opened') or \
@@ -151,8 +154,5 @@ class GetGithubIssues(CachedFunction):
            message['topic'].endswith('github.issue.closed'):
             entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
             return entry and entry['login'] == username
-        elif message["topic"].endswith('.hubs.hub.updated'):
-            return message['msg']['hub_id'] == self.instance.hub.id and \
-                   'github_username' in message['msg']['changed_keys']
         else:
             return False

From 40b8e8ab8326e5baca985f66d9d1ba1f4ec168e7 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 30/32] Issues: Fix lint warnings


---

diff --git a/hubs/models/hub.py b/hubs/models/hub.py
index 7514277..b45eb2e 100644
--- a/hubs/models/hub.py
+++ b/hubs/models/hub.py
@@ -313,9 +313,10 @@ class Hub(ObjectAuthzMixin, BASE):
         }
         if result["config"]["description"]:
             result["description_md2html"] = {
-                '__html': clean_input.clean(markdown(result["config"]["description"],
-                                                     extensions=['markdown.extensions.extra',
-                                                                 'markdown.extensions.sane_lists']))
+                '__html': clean_input.clean(markdown(
+                    result["config"]["description"],
+                    extensions=['markdown.extensions.extra',
+                                'markdown.extensions.sane_lists']))
             }
         for assoc in sorted(self.associations, key=lambda a: a.user.username):
             if assoc.role not in ROLES:
diff --git a/hubs/utils/validators.py b/hubs/utils/validators.py
index f319188..602770a 100644
--- a/hubs/utils/validators.py
+++ b/hubs/utils/validators.py
@@ -110,7 +110,8 @@ def FMNContext(value):
 def PagureUser(value):
     """Fails if the Pagure username does not exist."""
     try:
-        response = requests.get("/".join([PAGURE_URL, "user", value]), timeout=5)
+        response = requests.get("/".join([PAGURE_URL, "user", value]),
+                                timeout=5)
     except requests.exceptions.ReadTimeout:
         raise ValueError("Could not connect to Pagure, please try again.")
     if not response.ok:
diff --git a/hubs/widgets/issues/functions.py b/hubs/widgets/issues/functions.py
index aaa1987..8f1347a 100644
--- a/hubs/widgets/issues/functions.py
+++ b/hubs/widgets/issues/functions.py
@@ -8,18 +8,22 @@ class GetBugzillaBugs(CachedFunction):
 
     def execute(self):
         if self.instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
-            email = user_hub.config.get('bugzilla_email')
+            watched_hub = Hub.query.filter_by(
+                name=self.instance.hub.name).first()
         else:
-            email = self.instance.hub.config.get('bugzilla_email')
+            watched_hub = self.instance.hub
+        email = watched_hub.config.get('bugzilla_email')
         if email is None:
             return [[], None]
-        endpoint = 'https://bugzilla.redhat.com/jsonrpc.cgi?method=Bug.search&params=' \
-                   '[{{' \
-                   '"{}":"{}",' \
-                   '"status":["NEW","VERIFIED","ASSIGNED","MODIFIED","ON_DEV","ON_QA","RELEASE_PENDING","POST"]' \
+        endpoint = 'https://bugzilla.redhat.com/jsonrpc.cgi' \
+                   '?method=Bug.search&params=[{{"{}":"{}",' \
+                   '"status":["NEW","VERIFIED","ASSIGNED","MODIFIED",' \
+                   '"ON_DEV","ON_QA","RELEASE_PENDING","POST"]' \
                    '}}]'
-        mode = 'creator' if self.instance.config['mode'] == 'author' else 'assigned_to'
+        if self.instance.config['mode'] == 'author':
+            mode = 'creator'
+        else:
+            mode = 'assigned_to'
         response = requests.get(endpoint.format(mode, email)).json()
         issues = response['result']['bugs']
         urltmpl = 'https://bugzilla.redhat.com/show_bug.cgi?id={}'
@@ -39,7 +43,8 @@ class GetBugzillaBugs(CachedFunction):
 
     def should_invalidate(self, message):
         if self.instance.hub.hub_type == "stream":
-            watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            watched_hub = Hub.query.filter_by(
+                name=self.instance.hub.name).first()
         else:
             watched_hub = self.instance.hub
         if message["topic"].endswith('.hubs.hub.updated'):
@@ -51,7 +56,8 @@ class GetBugzillaBugs(CachedFunction):
         mode = self.instance.config['mode']
         if message['topic'].endswith('bugzilla.bug.new') or \
            message['topic'].endswith('bugzilla.bug.update'):
-            return message['msg']['bug']['creator' if mode == 'author' else 'assigned_to'] == email
+            key = 'creator' if mode == 'author' else 'assigned_to'
+            return message['msg']['bug'][key] == email
         else:
             return False
 
@@ -60,16 +66,18 @@ class GetPagureIssues(CachedFunction):
 
     def execute(self):
         if self.instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
-            username = user_hub.config.get('pagure_username')
+            watched_hub = Hub.query.filter_by(
+                name=self.instance.hub.name).first()
         else:
-            username = self.instance.hub.config.get('pagure_username')
+            watched_hub = self.instance.hub
+        username = watched_hub.config.get('pagure_username')
         if username is None:
             return [[], None]
         endpoint = "https://pagure.io/api/0/user/{}/issues?author={}"
         author = self.instance.config['mode'] == 'author'
         response = requests.get(endpoint.format(username, author)).json()
-        issues = response['issues_created'] if author else response['issues_assigned']
+        issues = response['issues_created'] if author \
+            else response['issues_assigned']
         urltmpl = 'https://pagure.io/{}/issue/{}'
         res = []
         for issue in issues:
@@ -82,13 +90,15 @@ class GetPagureIssues(CachedFunction):
                 service='pagure',
                 icon='https://pagure.io/static/favicon.ico'
             ))
-        total_count = len(response['issues_created'] if author else response['issues_assigned'])
+        total_count = len(response['issues_created'] if author
+                          else response['issues_assigned'])
         return [res, total_count]
 
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
         if self.instance.hub.hub_type == "stream":
-            watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            watched_hub = Hub.query.filter_by(
+                name=self.instance.hub.name).first()
         else:
             watched_hub = self.instance.hub
         if message["topic"].endswith('.hubs.hub.updated'):
@@ -102,7 +112,8 @@ class GetPagureIssues(CachedFunction):
            message['topic'].endswith('pagure.issue.assigned.added') or \
            message['topic'].endswith('pagure.issue.assigned.reset') or \
            message['topic'].endswith('pagure.issue.drop'):
-            entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
+            key = 'user' if mode == 'author' else 'assignee'
+            entry = message['msg']['issue'][key]
             return entry and entry['name'] == username
         else:
             return False
@@ -112,14 +123,19 @@ class GetGithubIssues(CachedFunction):
 
     def execute(self):
         if self.instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
-            username = user_hub.config.get('github_username')
+            watched_hub = Hub.query.filter_by(
+                name=self.instance.hub.name).first()
         else:
-            username = self.instance.hub.config.get('github_username')
+            watched_hub = self.instance.hub
+        username = watched_hub.config.get('github_username')
         if username is None:
             return [[], None]
-        endpoint = "https://api.github.com/search/issues?q=type:issue+{}:{}+state:open&sort:updated"
-        mode = "author" if self.instance.config['mode'] == 'author' else "assignee"
+        endpoint = "https://api.github.com/search/issues?" \
+                   "q=type:issue+{}:{}+state:open&sort:updated"
+        if self.instance.config['mode'] == 'author':
+            mode = 'author'
+        else:
+            mode = 'assignee'
         response = requests.get(endpoint.format(mode, username)).json()
         issues = response['items']
         res = []
@@ -139,7 +155,8 @@ class GetGithubIssues(CachedFunction):
     def should_invalidate(self, message):
         mode = self.instance.config['mode']
         if self.instance.hub.hub_type == "stream":
-            watched_hub = Hub.query.filter_by(name=self.instance.hub.name).first()
+            watched_hub = Hub.query.filter_by(
+                name=self.instance.hub.name).first()
         else:
             watched_hub = self.instance.hub
         if message["topic"].endswith('.hubs.hub.updated'):
@@ -152,7 +169,8 @@ class GetGithubIssues(CachedFunction):
            message['topic'].endswith('github.issue.reopened') or \
            message['topic'].endswith('github.issue.edited') or \
            message['topic'].endswith('github.issue.closed'):
-            entry = message['msg']['issue']['user' if mode == 'author' else 'assignee']
+            key = 'user' if mode == 'author' else 'assignee'
+            entry = message['msg']['issue'][key]
             return entry and entry['login'] == username
         else:
             return False
diff --git a/hubs/widgets/issues/views.py b/hubs/widgets/issues/views.py
index f4ddc72..80f4566 100644
--- a/hubs/widgets/issues/views.py
+++ b/hubs/widgets/issues/views.py
@@ -1,7 +1,10 @@
-from hubs.widgets.view import WidgetView
-from .functions import *
 import arrow
 
+from hubs.widgets.view import WidgetView
+from hubs.models.hub import Hub
+
+from .functions import GetBugzillaBugs, GetGithubIssues, GetPagureIssues
+
 
 class DataView(WidgetView):
     """Get issues in JSON format."""
@@ -25,7 +28,8 @@ class DataView(WidgetView):
         if not (bugzilla_email or github_username or pagure_username):
             return dict(
                 status="ERROR",
-                message="You must configure VCS details in the hub configuration in order to use this widget."
+                message="You must configure VCS details in the hub "
+                        "configuration in order to use this widget."
             )
 
         [bugzilla, bzcnt] = GetBugzillaBugs(instance)()
@@ -42,26 +46,32 @@ class DataView(WidgetView):
         }
 
         if bzcnt is not None:
+            if instance.config['mode'] == 'author':
+                key = 'reporter'
+            else:
+                key = 'assigned_to'
             data['counts'].update({
                 'bugzilla': {'count': bzcnt,
-                             'link': 'https://bugzilla.redhat.com/buglist.cgi?quicksearch={}:{}'
-                                 .format('reporter' if instance.config['mode'] == 'author' else 'assigned_to',
-                                         bugzilla_email)
+                             'link': 'https://bugzilla.redhat.com/buglist.cgi?'
+                                     'quicksearch={}:{}'.format(key,
+                                                                bugzilla_email)
                              }
             })
 
         if pcnt is not None:
             data['counts'].update({
                 'pagure': {'count': pcnt,
-                           'link': 'https://pagure.io/user/{}/issues'.format(pagure_username)
+                           'link': 'https://pagure.io/user/'
+                                   '{}/issues'.format(pagure_username)
                            }
             })
 
         if ghcnt is not None:
             data['counts'].update({
                 'github': {'count': ghcnt,
-                           'link': 'https://github.com/issues?q=type:issue+state:open+{}:{}'
-                               .format(instance.config['mode'], github_username)
+                           'link': 'https://github.com/issues?q=type:issue'
+                                   '+state:open+{}:{}'.format(
+                                    instance.config['mode'], github_username)
                            }
             })
 

From 9c7d744f3e7cb9e6bfb34212657b16b8d716160a Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 31/32] Issues: Refactoring views.py


---

diff --git a/hubs/widgets/issues/views.py b/hubs/widgets/issues/views.py
index 80f4566..a7d2d5c 100644
--- a/hubs/widgets/issues/views.py
+++ b/hubs/widgets/issues/views.py
@@ -16,14 +16,13 @@ class DataView(WidgetView):
     def get_context(self, instance, *args, **kwargs):
 
         if instance.hub.hub_type == "stream":
-            user_hub = Hub.query.filter_by(name=instance.hub.name).first()
-            bugzilla_email = user_hub.config.get('bugzilla_email')
-            github_username = user_hub.config.get('github_username')
-            pagure_username = user_hub.config.get('pagure_username')
+            watched_hub = Hub.query.filter_by(name=instance.hub.name).first()
         else:
-            bugzilla_email = instance.hub.config.get('bugzilla_email')
-            github_username = instance.hub.config.get('github_username')
-            pagure_username = instance.hub.config.get('pagure_username')
+            watched_hub = instance.hub
+
+        bugzilla_email = watched_hub.config.get('bugzilla_email')
+        github_username = watched_hub.config.get('github_username')
+        pagure_username = watched_hub.config.get('pagure_username')
 
         if not (bugzilla_email or github_username or pagure_username):
             return dict(

From 0da71434342b4e3fb969add16f52eb5f489d82f3 Mon Sep 17 00:00:00 2001
From: Shaily <shaily15297@yahoo.com>
Date: Feb 05 2018 14:33:19 +0000
Subject: [PATCH 32/32] Issues: Reset error on successful API call


---

diff --git a/hubs/static/client/app/widgets/issues/Widget.js b/hubs/static/client/app/widgets/issues/Widget.js
index 98bc88b..63e5448 100644
--- a/hubs/static/client/app/widgets/issues/Widget.js
+++ b/hubs/static/client/app/widgets/issues/Widget.js
@@ -45,6 +45,7 @@ export default class IssuesWidget extends React.Component {
           issues: data.issues,
           counts: data.counts,
           isLoading: false,
+          error: null,
         },
           () => (
             this.props.dispatch(widgetDidUpdate(this.props.widget.idx))

