#447 Integrate the contact info in the rules widget for team hubs
Merged 6 years ago by abompard. Opened 6 years ago by abompard.
abompard/fedora-hubs feature/contact  into  develop

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

+     <SimpleWidgetConfig {...props} />

+   );

+ }

@@ -0,0 +1,55 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import {

+   FormattedTime

+ } from "react-intl";

+ 

+ 

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

+   }

+ 

+   render() {

+     if (!this.state.time) {

+       return null;

+     }

+     return (

+       <FormattedTime value={this.state.time} />

+     );

+   }

+ }

+ CurrentTime.propTypes = {

+   offset: PropTypes.number,  // must be in seconds.

+ }

+ CurrentTime.defaultProps = {

+   offset: 0,

+ }

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

+       <span title={`Error: ${this.state.error}`}>?</span>

+       ) : this.state.value;

+     return (

+       <span className="Karma">

+         { this.state.isLoading ?

+           <Spinner circle={true} />

+           :

+           value

+         }

+       </span>

+     );

+   }

+ }

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

+         <div className="p-3">

+           <Spinner />

+         </div>

+       );

+     } else if (this.state.userData.username) {

+       content = (

+         <div>

+           <ul className="list-unstyled">

+             <li>

+               <i className="fa fa-map-marker" aria-hidden="true"></i>

+               <span className="ml-2">

+                   {this.state.userData.country}

+               </span>

+             </li>

+             <li>

+               <i className="fa fa-clock-o" aria-hidden="true"></i>

+               <span className="ml-2 mr-1">Current Time:</span>

+               <CurrentTime offset={this.state.userData.timezone_offset} />

+             </li>

+             <li>

+               <i className="fa fa-envelope-o" aria-hidden="true"></i>

+               <span className="ml-2">

+                 {this.state.userData.email}

+               </span>

+             </li>

+             <li>

+               <i className="fa fa-comment-o" aria-hidden="true"></i>

+               <span className="ml-2">

+                 {this.state.userData.ircnick}

+               </span>

+             </li>

+             { this.props.widget.urls.karma &&

+               <li>

+                 <i className="fa fa-user-plus" aria-hidden="true"></i>

+                 <span className="ml-2">

+                   <Karma url={this.props.widget.urls.karma} />

+                 </span>

+               </li>

+             }

+           </ul>

+           <div className="text-center">

+             <i className="fa fa-certificate" aria-hidden="true"></i>

+             <span className="ml-2">

+               Member Since {this.state.userData.account_age}

+             </span>

+           </div>

+         </div>

+       );

+     }

+     return (

+       <WidgetChrome

+         widget={this.props.widget}

+         editMode={this.props.editMode}

+         >

+         <div className="ContactWidget p-2">

+           {content}

+           { this.state.error &&

+             <div className="alert alert-warning">

+               {this.state.error}

+             </div>

+           }

+         </div>

+       </WidgetChrome>

+     );

+   }

+ }

+ ContactWidget.propTypes = {

+   widget: PropTypes.object.isRequired,

+   editMode: PropTypes.bool,

+   needsUpdate: PropTypes.bool,

+ };

@@ -0,0 +1,3 @@ 

+ .Karma .SpinnerCircle {

+     display: inline-block;

+ }

@@ -11,24 +11,26 @@ 

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

  class ContactsTest(WidgetTest):

  

      plugin = "contact"

+     maxDiff = None

  

      def setUp(self):

          super(ContactsTest, self).setUp()
@@ -74,32 +68,39 @@ 

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

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

+              }

+         )

file modified
+17 -142
@@ -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,20 @@ 

      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/<user>/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/<user>/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

+     hub_types = ['user']

+     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

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

@@ -1,70 +0,0 @@ 

- <div class="contactinfo-container">

-   {% if usergroup %}

-     <ul style="list-style: none; padding-left: 5px">

-       <!--<li><i class="fa fa-map-marker" aria-hidden="true"></i>

-           <span class="col-md-offset-1">{{location}}</span></li>-->

-       <li><i class="fa fa-clock-o" aria-hidden="true"></i>

-           <span class="ml-2">Current-Time :</span>

-           <script type="text/javascript">

-             // TODO: send offset after querying fas3

-             function calcTime(offset) {

- 

-               // create Date object for current location

-               d = new Date();

- 

-               // convert to msec

-               // add local time zone offset

-               // get UTC time in msec

-               utc = d.getTime() + (d.getTimezoneOffset() * 60000);

- 

-               // create new Date object using supplied offset

-               nd = new Date(utc + (3600000 * offset));

- 

-               // return time as a string

-               return nd.toLocaleString();

-             }

- 

-             setInterval(function() {

-               $("#time").html(calcTime(-4))

-             }, 1000)

- 

-             function get_karma_stats() {

-                 var karma_url = '{{ karma_url }}';

-                   $.ajax({

-                     method: "GET",

-                     url: karma_url,

-                     dataType: 'html',

-                     success: function(html) {

-                       console.log('Success: ' + html);

-                       karma_arr = JSON.parse(html);

-                       console.log(karma_arr)

-                       $('#karma_current').html("Karma : "+karma_arr['current']+" ("+karma_arr['total']+" all time)");

-                     },

-                     error: function() {

-                       console.log('Error: getting karma');

-                     },

-                   });

-                 }

-             get_karma_stats()

-           </script>

-           <span id="time"></span></li>

-       <li><i class="fa fa-envelope" aria-hidden="true"></i>

-           <span class="ml-2">{{email}}</span></li>

-       <li><i class="fa fa-comment" aria-hidden="true"></i>

-           <span class="ml-2"></span>{{ircnick}}</li>

-       <li><i class="fa fa-user-plus" aria-hidden="true"></i>

-           <span class="ml-2"></span><span id="karma_current"></span></li>

-       <li><i class="fa fa-certificate" aria-hidden="true"></i>

-           <span class="ml-2"></span> Member Since {{account_age}}</li>

-     </ul>

-   {% else %}

-     <ul style="list-style: none">

-       <li><i class="fa fa-comment" aria-hidden="true"></i>

-           <span class="ml-2"></span>fedora-{{ircchannel}}</li>

-       <li><i class="fa fa-envelope" aria-hidden="true"></i>

-           <span class="ml-2"><a href="{{ mailinglist }}">{{ hubname }}@lists.fedoraproject.org</a></span></li>

-       <li><i class="fa fa-info" aria-hidden="true"></i>

-           <span class="ml-2"><a href="{{ wikilink }}">fedoraproject.org/wiki/{{ hubname }}</a></span></li>

-     </ul>

-   {% endif %}

- </div>

@@ -0,0 +1,125 @@ 

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

+         # TODO: update this section when FAS3 is deployed

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

@@ -17,6 +17,7 @@ 

      position = "both"

      display_css = "card-info"

      display_title = None

+     hub_types = ['group']

      parameters = [

          dict(

              name="link",
@@ -59,6 +60,14 @@ 

          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 +75,8 @@ 

              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,

              )

@@ -40,6 +40,24 @@ 

    {% endif %}

    </p>

    {% endif %}

+ 

+   <h6>Communication</h6>

+   <ul class="list-unstyled mb-0">

+     <li>

+       <i class="fa fa-envelope-o" aria-hidden="true"></i>

+       <span class="ml-2">

+         <a href="{{mailing_list_url}}">{{mailing_list}}</a>

+       </span>

+     </li>

+     {% if irc_channel %}

+     <li>

+       <i class="fa fa-comment-o" aria-hidden="true"></i>

+       <span class="ml-2">

+         {{irc_channel}} on {{irc_network}}

+       </span>

+     </li>

+     {% endif %}

+   </ul>

  </div>

  

  

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

  arrow

  bleach<2.0.0

  blinker

+ python-dateutil

  decorator

  dogpile.cache

  enum34
@@ -13,6 +14,7 @@ 

  fmn.rules

  gunicorn

  html5lib==0.9999999

+ iso3166

  markdown

  munch

  psycopg2

file modified
+15 -9
@@ -39,19 +39,25 @@ 

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

Fixes #442

There's also a rework of the Contact widget which did not work since the switch to React. The difference is that jQuery auto-executes the <script> tags when filling a element with AJAX data, while React does not. As a consequence simple widgets can't use Javascript, they need to be written in React for that.

The Contact widget uses javascript to display the user's local time and refresh it every second.

Just tried this on a fresh vagrant install, and the contact widget on a user page shows the following error:

JSON.parse: unexpected character at line 1 column 1 of the JSON data

Do you have an error in the webserver's output?

rebased onto ecaec879cfc21e6d88e278b36583a0c0c61c6964

6 years ago

@abompard weirdly, no. Let me check again :)

Yup, there is a traceback... here it is:

Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 2000, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1991, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1567, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1988, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1641, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1544, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1639, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/lib/python2.7/site-packages/flask/app.py", line 1625, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/lib/python2.7/site-packages/flask/views.py", line 84, in view
    return self.dispatch_request(*args, **kwargs)
  File "/srv/hubs/fedora-hubs/hubs/widgets/view.py", line 123, in dispatch_request
    context = self.get_context(instance, *args, **kwargs)
  File "/srv/hubs/fedora-hubs/hubs/widgets/contact/__init__.py", line 59, in get_context
    fas_info = get_fas_info()
  File "/srv/hubs/fedora-hubs/hubs/widgets/caching.py", line 76, in get_data
    key, self.execute, should_cache_fn=self._should_cache)
  File "/usr/lib/python2.7/site-packages/dogpile/cache/region.py", line 825, in get_or_create
    async_creator) as value:
  File "/usr/lib/python2.7/site-packages/dogpile/lock.py", line 154, in __enter__
    return self._enter()
  File "/usr/lib/python2.7/site-packages/dogpile/lock.py", line 94, in _enter
    generated = self._enter_create(createdtime)
  File "/usr/lib/python2.7/site-packages/dogpile/lock.py", line 145, in _enter_create
    created = self.creator()
  File "/usr/lib/python2.7/site-packages/dogpile/cache/region.py", line 792, in gen_value
    created_value = creator()
  File "/srv/hubs/fedora-hubs/hubs/widgets/contact/__init__.py", line 73, in execute
    person = fas_client.person_by_username(self.instance.hub.name)
  File "/usr/lib/python2.7/site-packages/fedora/client/fas2.py", line 429, in person_by_username
    req_params=params)
  File "/usr/lib/python2.7/site-packages/fedora/client/baseclient.py", line 354, in send_request
    'Auth was requested but no way to'
AuthError: Auth was requested but no way to perform auth was given.  Please set username and password or session_id before calling this function with auth=True

Right! You need to set a login and password to be able to access FAS. I'll make an explicit error message.

1 new commit added

  • Add an error message if no FAS credentials are available
6 years ago

rebased onto a1b8071

6 years ago

:thumbsup: on this one from me.

One thing i did find distracting a bit was the seconds counter on the time. IMHO, having it down to the minute would be sufficient.

5 new commits added

  • Don't display the seconds in the contact widget
  • Set the allowed hub types for the rules and contact widgets
  • Add a Communication section to the Rules widget
  • Rewrite the contact widget with React to use Javascript
  • Allow manual invalidation of multiple widgets
6 years ago

Pull-Request has been merged by abompard

6 years ago