From a1b80713066f3595de26561022e5903ed8c15bce Mon Sep 17 00:00:00 2001
From: Aurélien Bompard
Date: Nov 21 2017 14:11:08 +0000
Subject: [PATCH 1/5] Allow manual invalidation of multiple widgets
---
diff --git a/smart_cache_invalidator.py b/smart_cache_invalidator.py
index b74fdad..b63b4d2 100755
--- a/smart_cache_invalidator.py
+++ b/smart_cache_invalidator.py
@@ -39,19 +39,25 @@ def do_list(args):
def do_clean(args):
''' Clean the widget for which there is data cached. '''
for widget in args.widgets:
- wid_obj = hubs.models.Widget.get(widget)
+ widget_instance = hubs.models.Widget.get(widget)
- if not wid_obj:
- wid_obj = session.query(hubs.models.Widget).filter_by(
- plugin=widget).first()
+ if widget_instance is None:
+ widget_instances = session.query(hubs.models.Widget).filter_by(
+ plugin=widget).all()
+ else:
+ widget_instances = [widget_instance]
- if not wid_obj:
+ if not widget_instances:
print('No widget found for {0}'.format(widget))
- print('- Removing cached {0} (#{1}) in {2}'.format(
- wid_obj.hub_id, wid_obj.idx, wid_obj.plugin))
- for fn_class in wid_obj.module.get_cached_functions().values():
- fn_class(wid_obj).invalidate()
+ for widget_instance in widget_instances:
+ print('- Removing cached {0} (#{1}) in {2}'.format(
+ widget_instance.hub_id,
+ widget_instance.idx,
+ widget_instance.plugin))
+ functions = widget_instance.module.get_cached_functions().values()
+ for fn_class in functions:
+ fn_class(widget_instance).invalidate()
def setup_parser():
From 16cf92bbf538893c43ea4da175abd6558684393b Mon Sep 17 00:00:00 2001
From: Aurélien Bompard
Date: Nov 22 2017 10:45:29 +0000
Subject: [PATCH 2/5] Rewrite the contact widget with React to use Javascript
---
diff --git a/hubs/static/client/app/widgets/contact/Config.js b/hubs/static/client/app/widgets/contact/Config.js
new file mode 100644
index 0000000..1fc4fa8
--- /dev/null
+++ b/hubs/static/client/app/widgets/contact/Config.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import SimpleWidgetConfig from '../../components/SimpleWidgetConfig';
+
+
+// Use the default configuration, it's sufficient.
+
+export default function Config(props) {
+ return (
+
+ );
+}
diff --git a/hubs/static/client/app/widgets/contact/CurrentTime.js b/hubs/static/client/app/widgets/contact/CurrentTime.js
new file mode 100644
index 0000000..6c80d33
--- /dev/null
+++ b/hubs/static/client/app/widgets/contact/CurrentTime.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+
+export default class CurrentTime extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {time: null};
+ this.setTime = this.setTime.bind(this);
+ this.timer = null;
+ }
+
+ componentDidMount() {
+ this.timer = window.setInterval(this.setTime, 1000);
+ this.setTime();
+ }
+
+ componentWillUnmount() {
+ if (this.timer) {
+ window.clearInterval(this.timer);
+ }
+ }
+
+ setTime() {
+ // create Date object for current location
+ const d = new Date();
+ // convert to msec
+ // add local time zone offset
+ // get UTC time in msec
+ const utc = d.getTime() + (d.getTimezoneOffset() * 60000);
+ // create new Date object using supplied offset
+ const nd = new Date(utc + (1000 * this.props.offset));
+ // set time as a string
+ this.setState({time: nd.toLocaleTimeString()});
+ }
+
+ render() {
+ return (
+
+ {this.state.time}
+
+ );
+ }
+}
+CurrentTime.propTypes = {
+ offset: PropTypes.number, // must be in seconds.
+}
+CurrentTime.defaultProps = {
+ offset: 0,
+}
diff --git a/hubs/static/client/app/widgets/contact/Karma.js b/hubs/static/client/app/widgets/contact/Karma.js
new file mode 100644
index 0000000..279ce60
--- /dev/null
+++ b/hubs/static/client/app/widgets/contact/Karma.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { apiCall } from '../../core/utils';
+import Spinner from "../../components/Spinner";
+
+
+export default class Karma extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ value: null,
+ error: null,
+ isLoading: false,
+ };
+ this.loadFromServer = this.loadFromServer.bind(this);
+ }
+
+ componentDidMount() {
+ this.loadFromServer();
+ }
+
+ loadFromServer() {
+ this.setState({isLoading: true});
+ apiCall(this.props.url).then(
+ (karma) => {
+ this.setState({value: karma, isLoading: false});
+ },
+ (error) => {
+ this.setState({error: error.message, isLoading: false});
+ }
+ );
+ }
+
+ render() {
+ const value = this.state.error ? (
+ ?
+ ) : this.state.value;
+ return (
+
+ { this.state.isLoading ?
+
+ :
+ value
+ }
+
+ );
+ }
+}
diff --git a/hubs/static/client/app/widgets/contact/Widget.js b/hubs/static/client/app/widgets/contact/Widget.js
new file mode 100644
index 0000000..79585ce
--- /dev/null
+++ b/hubs/static/client/app/widgets/contact/Widget.js
@@ -0,0 +1,115 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { apiCall } from '../../core/utils';
+import WidgetChrome from '../../components/WidgetChrome';
+import Spinner from "../../components/Spinner";
+import CurrentTime from "./CurrentTime";
+import Karma from "./Karma";
+import "./contact.css";
+
+
+export default class ContactWidget extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ userData: {},
+ error: null,
+ isLoading: false,
+ };
+ this.loadFromServer = this.loadFromServer.bind(this);
+ }
+
+ componentDidMount() {
+ if (!this.props.editMode) {
+ this.loadFromServer();
+ }
+ }
+
+ loadFromServer() {
+ this.setState({isLoading: true});
+ apiCall(this.props.widget.urls.data).then(
+ (userData) => {
+ this.setState({userData, isLoading: false});
+ },
+ (error) => {
+ this.setState({error: error.message, isLoading: false});
+ }
+ );
+ }
+
+ render() {
+ let content = null;
+ if (this.state.isLoading) {
+ content = (
+
+
+
+ );
+ } else if (this.state.userData.username) {
+ content = (
+
+
+ -
+
+
+ {this.state.userData.country}
+
+
+ -
+
+ Current Time:
+
+
+ -
+
+
+ {this.state.userData.email}
+
+
+ -
+
+
+ {this.state.userData.ircnick}
+
+
+ { this.props.widget.urls.karma &&
+ -
+
+
+
+
+
+ }
+
+
+
+
+ Member Since {this.state.userData.account_age}
+
+
+
+ );
+ }
+ return (
+
+
+ {content}
+ { this.state.error &&
+
+ {this.state.error}
+
+ }
+
+
+ );
+ }
+}
+ContactWidget.propTypes = {
+ widget: PropTypes.object.isRequired,
+ editMode: PropTypes.bool,
+ needsUpdate: PropTypes.bool,
+};
diff --git a/hubs/static/client/app/widgets/contact/contact.css b/hubs/static/client/app/widgets/contact/contact.css
new file mode 100644
index 0000000..51af1c7
--- /dev/null
+++ b/hubs/static/client/app/widgets/contact/contact.css
@@ -0,0 +1,3 @@
+.Karma .SpinnerCircle {
+ display: inline-block;
+}
diff --git a/hubs/tests/widgets/test_contact.py b/hubs/tests/widgets/test_contact.py
index f011392..63caf96 100644
--- a/hubs/tests/widgets/test_contact.py
+++ b/hubs/tests/widgets/test_contact.py
@@ -11,24 +11,26 @@ from hubs.tests import FakeAuthorization, auth_set
from . import WidgetTest
-def mocked_requests_get(*args, **kwargs):
- class MockResponse:
- def __init__(self, json_data, status_code):
- self.json_data = json_data
- self.status_code = status_code
- self.text = str(json_data)
+class MockResponse:
+ def __init__(self, json_data, status_code):
+ self.json_data = json_data
+ self.status_code = status_code
+ self.text = str(json_data)
+ self.ok = (status_code == 200)
+
+ def json(self):
+ return self.json_data
- def json(self):
- return self.json_data
- if '/decause' in args[0]:
+def mocked_requests_get(*args, **kwargs):
+ if '/ralph' in kwargs["url"]:
data = {
"current": 0,
"decrements": 0,
"increments": 0,
"release": "f24",
"total": 0,
- "username": "decause"
+ "username": "ralph"
}
return MockResponse(json_data=data, status_code=200)
@@ -36,23 +38,14 @@ def mocked_requests_get(*args, **kwargs):
def mocked_requests_post(*args, **kwargs):
- class MockResponse:
- def __init__(self, json_data, status_code):
- self.json_data = json_data
- self.status_code = status_code
- self.text = str(json_data)
-
- def json(self):
- return self.json_data
-
- if '/decause' in kwargs['url']:
+ if '/ralph' in kwargs['url']:
data = {
"current": 1,
"decrements": 0,
"increments": 1,
"release": "f24",
"total": 1,
- "username": "decause"
+ "username": "ralph"
}
return MockResponse(json_data=data, status_code=200)
@@ -62,6 +55,7 @@ def mocked_requests_post(*args, **kwargs):
class ContactsTest(WidgetTest):
plugin = "contact"
+ maxDiff = None
def setUp(self):
super(ContactsTest, self).setUp()
@@ -74,32 +68,39 @@ class ContactsTest(WidgetTest):
self.session.commit()
self.widget_idx = widget.idx
- def test_data_simple(self):
- user = FakeAuthorization('ralph')
- widget = Widget.query.get(self.widget_idx)
- response = self.check_url(
- '/ralph/w/contact/%i/' % self.widget_idx, user)
- self.assertDictEqual(response.context, {
- 'account_age': 'Oct 2010',
+ @mock.patch('hubs.widgets.contact.fedora.client.fas2')
+ def test_data_simple(self, mock_fas2):
+ fake_account_system = mock.Mock()
+ fake_account_system.person_by_username.return_value = {
+ 'creation': '2010-10-01',
'email': 'ralph@fedoraproject.org',
'ircnick': 'ralph',
- 'karma_url': '/ralph/w/contact/%i/plus-plus/ralph/status'
- % self.widget_idx,
- 'location': 'United States',
+ 'country_code': 'US',
'timezone': 'UTC',
- 'usergroup': True,
- 'edit_mode': False,
- 'widget': widget.module,
- 'widget_instance': widget,
- })
-
- def test_view_authz(self):
- self._test_view_authz()
-
- @mock.patch('requests.get', side_effect=mocked_requests_get)
- def test_plus_plus_get_valid(self, mock_get):
- url = "/ralph/w/contact/%d/plus-plus/%s/status" % (
- self.widget_idx, "decause")
+ 'username': 'ralph',
+ }
+ mock_fas2.AccountSystem.return_value = fake_account_system
+ user = FakeAuthorization('ralph')
+ response = self.check_url(
+ '/ralph/w/contact/%i/data' % self.widget_idx, user)
+ self.assertDictEqual(
+ json.loads(response.get_data(as_text=True)),
+ {
+ "status": "OK",
+ "data": {
+ 'account_age': 'Oct 2010',
+ 'email': 'ralph@fedoraproject.org',
+ 'ircnick': 'ralph',
+ 'country': 'United States',
+ 'timezone': 'UTC',
+ 'timezone_offset': 0,
+ 'username': 'ralph',
+ }
+ })
+
+ @mock.patch('requests.request', side_effect=mocked_requests_get)
+ def test_plus_plus_get_valid(self, mock_request):
+ url = "/ralph/w/contact/%d/plus-plus" % self.widget_idx
result = self.app.get(url)
expected = {
"current": 0,
@@ -107,80 +108,77 @@ class ContactsTest(WidgetTest):
"increments": 0,
"release": "f24",
"total": 0,
- "username": "decause"
+ "username": "ralph"
}
self.assertEqual(result.status_code, 200)
self.assertEqual(
json.loads(result.get_data(as_text=True)),
- expected)
+ dict(status="OK", data=expected))
- @mock.patch('requests.post', side_effect=mocked_requests_post)
- def test_plus_plus_post_increment_valid(self, mock_post):
- url = "/ralph/w/contact/%d/plus-plus/%s/update" % (
- self.widget_idx, "decause")
- user = FakeAuthorization('ralph')
+ @mock.patch('requests.request', side_effect=mocked_requests_post)
+ def test_plus_plus_post_increment_valid(self, mock_request):
+ url = "/ralph/w/contact/%d/plus-plus" % self.widget_idx
+ user = FakeAuthorization('decause')
with auth_set(hubs.app.app, user):
- result = self.app.post(url, data={'increment': True})
+ result = self.app.post(
+ url,
+ content_type="application/json",
+ data=json.dumps({'increment': True}))
expected = {
"current": 1,
"decrements": 0,
"increments": 1,
"release": "f24",
"total": 1,
- "username": "decause"
+ "username": "ralph"
}
self.assertEqual(result.status_code, 200)
self.assertEqual(
json.loads(result.get_data(as_text=True)),
- expected)
-
- @mock.patch('requests.post', side_effect=mocked_requests_post)
- def test_plus_plus_post_increment_myself_error(self, mock_post):
- url = "/ralph/w/contact/%d/plus-plus/%s/update" % (
- self.widget_idx, "ralph")
- user = FakeAuthorization('ralph')
- with auth_set(hubs.app.app, user):
- result = self.app.post(url, data={'increment': True})
- self.assertEqual(result.status_code, 403)
- self.assertEqual(
- result.get_data(as_text=True),
- 'You may not modify your own karma.')
+ dict(status="OK", data=expected))
- @mock.patch('requests.post', side_effect=mocked_requests_post)
- def test_plus_plus_post_increment_user_does_not_exist(self, mock_post):
- url = "/ralph/w/contact/%d/plus-plus/%s/update" % (
- self.widget_idx, "doesnotexist")
+ @mock.patch('requests.request', side_effect=mocked_requests_post)
+ def test_plus_plus_post_increment_myself_error(self, mock_request):
+ url = "/ralph/w/contact/%d/plus-plus" % self.widget_idx
user = FakeAuthorization('ralph')
with auth_set(hubs.app.app, user):
- result = self.app.post(url, data={'increment': True})
- self.assertEqual(result.status_code, 404)
+ result = self.app.post(
+ url,
+ content_type="application/json",
+ data=json.dumps({'increment': True}))
self.assertEqual(
- result.get_data(as_text=True),
- 'User does not exist')
-
- @mock.patch('requests.post', side_effect=mocked_requests_post)
- def test_plus_plus_post_increment_no_data_error(self, mock_post):
- url = "/ralph/w/contact/%d/plus-plus/%s/update" % (
- self.widget_idx, "decause")
- user = FakeAuthorization('ralph')
+ json.loads(result.get_data(as_text=True)),
+ {
+ "status": "ERROR",
+ "message": "You may not modify your own karma.",
+ })
+
+ @mock.patch('requests.request', side_effect=mocked_requests_post)
+ def test_plus_plus_post_increment_no_data_error(self, mock_request):
+ url = "/ralph/w/contact/%d/plus-plus" % self.widget_idx
+ user = FakeAuthorization('decause')
with auth_set(hubs.app.app, user):
- result = self.app.post(url, data={})
- self.assertEqual(result.status_code, 400)
+ result = self.app.post(
+ url,
+ content_type="application/json",
+ data=json.dumps({}))
exp_str = "You must set 'decrement' or 'increment' " \
"with a boolean value in the body"
- self.assertEqual(result.get_data(as_text=True), exp_str)
+ self.assertEqual(
+ json.loads(result.get_data(as_text=True)),
+ {"status": "ERROR", "message": exp_str}
+ )
- def test_plus_plus_receiver_does_not_exist(self):
- url = "/ralph/w/contact/%d/plus-plus/%s/status" % (
- self.widget_idx, "doesnotexist")
- result = self.app.get(url)
- self.assertEqual(result.status_code, 404)
- self.assertEqual(result.get_data(as_text=True), 'User does not exist')
-
- @mock.patch('requests.get')
- def test_plus_plus_connection_error(self, mock_get):
- mock_get.side_effect = requests.ConnectionError("connection error")
- url = "/ralph/w/contact/%d/plus-plus/%s/status" % (
- self.widget_idx, "decause")
+ @mock.patch('requests.request')
+ def test_plus_plus_connection_error(self, mock_request):
+ mock_request.side_effect = requests.ConnectionError("connection error")
+ url = "/ralph/w/contact/%d/plus-plus" % self.widget_idx
result = self.app.get(url)
- self.assertEqual(result.status_code, 504)
+ self.assertEqual(
+ json.loads(result.get_data(as_text=True)),
+ {
+ "status": "ERROR",
+ "message": "Could not connect to "
+ "http://localhost:5001/user/ralph",
+ }
+ )
diff --git a/hubs/widgets/contact/__init__.py b/hubs/widgets/contact/__init__.py
index 6992015..0ae55d0 100644
--- a/hubs/widgets/contact/__init__.py
+++ b/hubs/widgets/contact/__init__.py
@@ -1,13 +1,8 @@
from __future__ import unicode_literals
import flask
-import hubs.models
-import requests
-import six
from hubs.widgets.base import Widget
-from hubs.widgets.view import WidgetView, RootWidgetView
-from hubs.utils.views import login_required
class Contact(Widget):
@@ -15,140 +10,19 @@ class Contact(Widget):
name = "contact"
position = "both"
display_title = None
-
-
-class BaseView(RootWidgetView):
-
- def get_context(self, instance, *args, **kwargs):
- ''' Data for Contact widget. Checks if the hub associated
- with instance is of a user or not. If the hub is of a user, return
- data related to the user else, hub is of a fedora team
- - return data related to the team '''
- # TODO: update this section when FAS3 is deployed
-
- hub = instance.hub
- if hub.user_hub:
- usergroup = True
- user = hubs.models.User.by_username(hub.name)
- email = user.username + '@fedoraproject.org'
- karma_url = flask.url_for(
- 'contact_plus_plus_status',
- hub=hub.name, idx=instance.idx, user=user.username)
- fas_info = {
- 'usergroup': usergroup,
- 'location': 'United States',
- 'timezone': 'UTC',
- 'email': email,
- 'ircnick': user.username,
- 'karma_url': karma_url,
- 'account_age': 'Oct 2010',
- }
- else:
- usergroup = False
- # TODO: update this section integrating with FAS3
- if hub.name == 'infrastructure':
- ircchannel = 'apps'
- hubname = 'infrastructure'
- elif hub.name == 'designteam':
- ircchannel = 'design'
- hubname = 'design'
- elif hub.name == 'marketing':
- ircchannel = 'mktg'
- hubname = 'marketing'
- else:
- ircchannel = hub.name
- hubname = hub.name
- mailinglist = 'https://lists.fedoraproject.org/archives/list/{}'
- '@lists.fedoraproject.org/'.format(hubname)
- wikilink = 'https://fedoraproject.org/wiki/' + hubname
- fas_info = {
- 'usergroup': usergroup,
- 'hubname': hubname,
- 'ircchannel': 'fedora-%s' % ircchannel,
- 'mailinglist': mailinglist,
- 'wikilink': wikilink,
- }
- return fas_info
-
-
-def _get_pp_url(username):
- pp_url = flask.current_app.config['PLUS_PLUS_URL']
- if not pp_url.endswith("/"):
- pp_url += "/"
- pp_url += username
- return pp_url
-
-
-class PlusPlusStatus(WidgetView):
-
- name = "plus_plus_status"
- url_rules = ["/plus-plus//status"]
-
- def dispatch_request(self, *args, **kwargs):
- username = kwargs["user"]
- receiver = hubs.models.User.by_username(username)
- if not receiver:
- return 'User does not exist', 404
- pp_url = _get_pp_url(username)
- try:
- req = requests.get(pp_url, timeout=5)
- except requests.Timeout:
- return 'The request to {url} timed out'.format(url=pp_url), 504
- except requests.ConnectionError:
- return 'Could not connect to {url}'.format(url=pp_url), 504
- if req.status_code == 200:
- return flask.jsonify(req.json())
- else:
- return req.text, req.status_code
-
-
-def _pp_update_bool_helper(val):
- if isinstance(val, bool):
- return val
- elif isinstance(val, six.string_types):
- fmt_str = str(val).replace("'", "").replace('"', '').lower()
- return fmt_str in ("yes", "true", "t", "1")
- else:
- raise ValueError
-
-
-class PlusPlusUpdate(WidgetView):
-
- name = "plus_plus_update"
- url_rules = ["/plus-plus//update"]
- methods = ['POST']
- decorators = [login_required]
-
- def dispatch_request(self, *args, **kwargs):
- username = kwargs["user"]
- receiver = hubs.models.User.by_username(username)
- if not receiver:
- return 'User does not exist', 404
-
- if username == flask.g.auth.nickname:
- return 'You may not modify your own karma.', 403
-
- if 'decrement' not in flask.request.form \
- and 'increment' not in flask.request.form:
- return "You must set 'decrement' or 'increment' " \
- "with a boolean value in the body", 400
-
- update = ('increment' if 'increment' in flask.request.form
- else 'decrement')
-
- update_bool_val = _pp_update_bool_helper(
- flask.request.form[update])
- pp_url = _get_pp_url(username)
- sender = hubs.models.User.by_username(flask.g.auth.nickname)
- pp_token = flask.current_app.config['PLUS_PLUS_TOKEN']
- auth_header = {'Authorization': 'token {}'.format(pp_token)}
- data = {'sender': sender.username, update: update_bool_val}
- try:
- req = requests.post(
- url=pp_url, headers=auth_header, data=data, timeout=5)
- except requests.Timeout:
- return 'The request to {url} timed out'.format(url=pp_url), 504
- if req.status_code == 200:
- return flask.jsonify(req.json())
- else:
- return req.text, req.status_code
+ is_react = True
+ views_module = ".views"
+ cached_functions_module = ".functions"
+
+ def get_props(self, instance, *args, **kwargs):
+ props = super(Contact, self).get_props(instance, *args, **kwargs)
+ if instance is not None:
+ hub_name = instance.hub.name
+ props["urls"] = dict(
+ data=flask.url_for(
+ "contact_data", hub=hub_name, idx=instance.idx),
+ # Don't use the plus-plus server, it's not deployed yet.
+ # karma=flask.url_for(
+ # "contact_plus_plus", hub=hub_name, idx=instance.idx),
+ )
+ return props
diff --git a/hubs/widgets/contact/functions.py b/hubs/widgets/contact/functions.py
new file mode 100644
index 0000000..36e7aec
--- /dev/null
+++ b/hubs/widgets/contact/functions.py
@@ -0,0 +1,44 @@
+from __future__ import unicode_literals
+
+import fedora.client.fas2
+from dateutil.parser import parse as parse_date
+from iso3166 import countries
+
+from hubs.utils import get_fedmsg_config
+from hubs.widgets.caching import CachedFunction
+
+
+fedmsg_config = get_fedmsg_config()
+
+
+class GetFASInfo(CachedFunction):
+
+ def execute(self):
+ try:
+ fas_username = fedmsg_config["fas_credentials"]["username"]
+ fas_password = fedmsg_config["fas_credentials"]["password"]
+ except KeyError:
+ return None
+ fas_client = fedora.client.fas2.AccountSystem(
+ username=fas_username,
+ password=fas_password,
+ )
+ person = fas_client.person_by_username(self.instance.hub.name)
+ filter_fields = (
+ "timezone",
+ "ircnick",
+ "username",
+ "email",
+ )
+ result = dict([(field, person[field]) for field in filter_fields])
+ result["account_age"] = parse_date(
+ person["creation"]).strftime("%b %Y")
+ result["country"] = countries.get(person["country_code"]).name
+ return result
+
+ def should_invalidate(self, message):
+ if message['topic'].endswith('fas.user.update'):
+ username = self.instance.hub.name
+ if message['msg']['user'] == username:
+ return True
+ return False
diff --git a/hubs/widgets/contact/templates/root.html b/hubs/widgets/contact/templates/root.html
deleted file mode 100644
index 249542f..0000000
--- a/hubs/widgets/contact/templates/root.html
+++ /dev/null
@@ -1,70 +0,0 @@
-
diff --git a/hubs/widgets/contact/views.py b/hubs/widgets/contact/views.py
new file mode 100644
index 0000000..a9d17a6
--- /dev/null
+++ b/hubs/widgets/contact/views.py
@@ -0,0 +1,133 @@
+from __future__ import unicode_literals
+
+from datetime import datetime
+
+import flask
+import requests
+import six
+from pytz import timezone
+
+import hubs.models
+from hubs.utils import get_fedmsg_config
+from hubs.utils.views import authenticated
+from hubs.widgets.view import WidgetView
+from .functions import GetFASInfo
+
+
+fedmsg_config = get_fedmsg_config()
+
+
+class DataView(WidgetView):
+
+ name = "data"
+ url_rules = ["data"]
+ json = True
+
+ def get_context(self, instance, *args, **kwargs):
+ ''' Data for Contact widget. Only works for user hubs.'''
+ # TODO: update this section when FAS3 is deployed
+
+ hub = instance.hub
+ if not hub.user_hub:
+ return dict(
+ status="ERROR",
+ message=("The contact widget only works for personal hubs, "
+ "not team hubs."),
+ )
+ try:
+ fedmsg_config["fas_credentials"]["username"]
+ fedmsg_config["fas_credentials"]["password"]
+ except KeyError:
+ return dict(
+ status="ERROR",
+ message=("No FAS credentials configured, report this to the "
+ "system administrator.")
+ )
+ get_fas_info = GetFASInfo(instance)
+ fas_info = get_fas_info()
+ now = datetime.now()
+ offset = timezone(fas_info["timezone"]).utcoffset(now)
+ fas_info["timezone_offset"] = offset.days * 86400 + offset.seconds
+ return dict(status="OK", data=fas_info)
+
+
+def _pp_update_bool_helper(val):
+ if isinstance(val, bool):
+ return val
+ elif isinstance(val, six.string_types):
+ fmt_str = str(val).replace("'", "").replace('"', '').lower()
+ return fmt_str in ("yes", "true", "t", "1")
+ else:
+ raise ValueError
+
+
+class PlusPlus(WidgetView):
+
+ name = "plus_plus"
+ url_rules = ["plus-plus"]
+ methods = ['GET', 'POST']
+ json = True
+
+ def get_context(self, instance, *args, **kwargs):
+ username = instance.hub.name
+ if not hubs.models.User.by_username(username):
+ return dict(status="ERROR", message="User does not exist")
+ if flask.request.method == "POST":
+ if not authenticated():
+ return dict(status="ERROR", message="You must be logged-in")
+ if username == flask.g.auth.nickname:
+ return dict(
+ status="ERROR",
+ message="You may not modify your own karma.",
+ )
+ request_data = flask.request.get_json()
+ if request_data is None:
+ return dict(
+ status="ERROR",
+ message="You must post data in JSON format.",
+ )
+ if 'decrement' not in request_data \
+ and 'increment' not in request_data:
+ return dict(
+ status="ERROR",
+ message=("You must set 'decrement' or 'increment' "
+ "with a boolean value in the body"),
+ )
+ update = ('increment' if 'increment' in request_data
+ else 'decrement')
+ update_bool_val = _pp_update_bool_helper(request_data[update])
+ sender = hubs.models.User.by_username(flask.g.auth.nickname)
+ data = {'sender': sender.username, update: update_bool_val}
+ return pp_request(username, data)
+ return pp_request(username)
+
+
+def pp_request(username, data=None):
+ pp_url = flask.current_app.config['PLUS_PLUS_URL']
+ if not pp_url.endswith("/"):
+ pp_url += "/"
+ pp_url += username
+ if data is None:
+ auth_header = None
+ method = "GET"
+ else:
+ pp_token = flask.current_app.config['PLUS_PLUS_TOKEN']
+ auth_header = {'Authorization': 'token {}'.format(pp_token)}
+ method = "POST"
+ try:
+ req = requests.request(
+ method, url=pp_url, headers=auth_header, data=data, timeout=5)
+ except requests.Timeout:
+ return dict(
+ status="ERROR",
+ message="The request to {url} timed out".format(url=pp_url),
+ )
+ except requests.ConnectionError:
+ return dict(
+ status="ERROR",
+ message="Could not connect to {url}".format(url=pp_url),
+ )
+ if req.ok:
+ return dict(status="OK", data=req.json())
+ else:
+ return dict(status="ERROR", message=req.text)
diff --git a/requirements.txt b/requirements.txt
index 8232aff..52dd919 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,7 @@ alembic
arrow
bleach<2.0.0
blinker
+python-dateutil
decorator
dogpile.cache
enum34
@@ -13,6 +14,7 @@ fmn.lib
fmn.rules
gunicorn
html5lib==0.9999999
+iso3166
markdown
munch
psycopg2
From d97db99a77024ca5bc663ce9e5c69ed409d5d1d0 Mon Sep 17 00:00:00 2001
From: Aurélien Bompard
Date: Nov 22 2017 10:45:29 +0000
Subject: [PATCH 3/5] Add a Communication section to the Rules widget
Fixes: #442
---
diff --git a/hubs/widgets/rules/__init__.py b/hubs/widgets/rules/__init__.py
index e0e619c..f3dc7d6 100644
--- a/hubs/widgets/rules/__init__.py
+++ b/hubs/widgets/rules/__init__.py
@@ -59,6 +59,14 @@ class BaseView(RootWidgetView):
owners = ordereddict([
(o.username, username2avatar(o.username)) for o in owners
])
+ mailing_list = "{}@lists.fedoraproject.org".format(instance.hub.name)
+ mailing_list_url = (
+ 'https://lists.fedoraproject.org/archives/list/{}@'
+ 'lists.fedoraproject.org/').format(instance.hub.name)
+ irc_channel = irc_network = None
+ if instance.hub.config.chat_channel:
+ irc_channel = instance.hub.config.chat_channel
+ irc_network = instance.hub.config.chat_domain
return dict(
oldest_owners=oldest_owners,
owners=owners,
@@ -66,4 +74,8 @@ class BaseView(RootWidgetView):
schedule_text=instance.config["schedule_text"],
schedule_link=instance.config["schedule_link"],
minutes_link=instance.config["minutes_link"],
+ mailing_list=mailing_list,
+ mailing_list_url=mailing_list_url,
+ irc_channel=irc_channel,
+ irc_network=irc_network,
)
diff --git a/hubs/widgets/rules/templates/root.html b/hubs/widgets/rules/templates/root.html
index bd1533f..f78674b 100644
--- a/hubs/widgets/rules/templates/root.html
+++ b/hubs/widgets/rules/templates/root.html
@@ -40,6 +40,24 @@
{% endif %}
{% endif %}
+
+ Communication
+
+ -
+
+
+ {{mailing_list}}
+
+
+ {% if irc_channel %}
+ -
+
+
+ {{irc_channel}} on {{irc_network}}
+
+
+ {% endif %}
+
From f1e4ffa59bffdfa6d21e5612e21632331dea2e90 Mon Sep 17 00:00:00 2001
From: Aurélien Bompard
Date: Nov 22 2017 10:45:52 +0000
Subject: [PATCH 4/5] Set the allowed hub types for the rules and contact widgets
---
diff --git a/hubs/widgets/contact/__init__.py b/hubs/widgets/contact/__init__.py
index 0ae55d0..e5526d4 100644
--- a/hubs/widgets/contact/__init__.py
+++ b/hubs/widgets/contact/__init__.py
@@ -11,6 +11,7 @@ class Contact(Widget):
position = "both"
display_title = None
is_react = True
+ hub_types = ['user']
views_module = ".views"
cached_functions_module = ".functions"
diff --git a/hubs/widgets/contact/views.py b/hubs/widgets/contact/views.py
index a9d17a6..a082fbc 100644
--- a/hubs/widgets/contact/views.py
+++ b/hubs/widgets/contact/views.py
@@ -24,16 +24,8 @@ class DataView(WidgetView):
json = True
def get_context(self, instance, *args, **kwargs):
- ''' Data for Contact widget. Only works for user hubs.'''
+ ''' Data for Contact widget.'''
# TODO: update this section when FAS3 is deployed
-
- hub = instance.hub
- if not hub.user_hub:
- return dict(
- status="ERROR",
- message=("The contact widget only works for personal hubs, "
- "not team hubs."),
- )
try:
fedmsg_config["fas_credentials"]["username"]
fedmsg_config["fas_credentials"]["password"]
diff --git a/hubs/widgets/rules/__init__.py b/hubs/widgets/rules/__init__.py
index f3dc7d6..12e830e 100644
--- a/hubs/widgets/rules/__init__.py
+++ b/hubs/widgets/rules/__init__.py
@@ -17,6 +17,7 @@ class Rules(Widget):
position = "both"
display_css = "card-info"
display_title = None
+ hub_types = ['group']
parameters = [
dict(
name="link",
From 204bd015467cf218b8367fae49455610ff2ca1d5 Mon Sep 17 00:00:00 2001
From: Aurélien Bompard
Date: Nov 22 2017 11:12:03 +0000
Subject: [PATCH 5/5] Don't display the seconds in the contact widget
---
diff --git a/hubs/static/client/app/widgets/contact/CurrentTime.js b/hubs/static/client/app/widgets/contact/CurrentTime.js
index 6c80d33..73d9eea 100644
--- a/hubs/static/client/app/widgets/contact/CurrentTime.js
+++ b/hubs/static/client/app/widgets/contact/CurrentTime.js
@@ -1,5 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {
+ FormattedTime
+} from "react-intl";
export default class CurrentTime extends React.Component {
@@ -32,14 +35,15 @@ export default class CurrentTime extends React.Component {
// create new Date object using supplied offset
const nd = new Date(utc + (1000 * this.props.offset));
// set time as a string
- this.setState({time: nd.toLocaleTimeString()});
+ this.setState({time: nd});
}
render() {
+ if (!this.state.time) {
+ return null;
+ }
return (
-
- {this.state.time}
-
+
);
}
}