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 @@ -
- {% if usergroup %} -
    - -
  • - Current-Time : - -
  • -
  • - {{email}}
  • -
  • - {{ircnick}}
  • -
  • -
  • -
  • - Member Since {{account_age}}
  • -
- {% else %} - - {% endif %} -
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} - + ); } }