#517 Add the contact info in the hub header
Merged 6 years ago by abompard. Opened 6 years ago by abompard.
abompard/fedora-hubs feature/description-462  into  develop

file modified
+8
@@ -81,6 +81,14 @@ 

                  'hub': hub.id,

                  'username': msg["msg"]["user"],

              }))

+     if '.fas.user.update' in topic:

+         username = msg["msg"]["user"]

+         user = hubs.models.User.query.get(username)

+         if user is not None:

+             yield retask.task.Task(json.dumps({

+                 'type': 'sync-user',

+                 'username': msg["msg"]["user"],

+             }))

  

      # Store the list of concerned hubs to check later in the

      # should_invalidate() method of Feed widgets.

file modified
+23
@@ -175,6 +175,29 @@ 

                      )

                  # TODO: Add a message in the UI saying that the membership has

                  # been accepted?

+             elif item_type == "sync-user":

+                 username = item["username"]

+                 affected_hubs = fas.sync_user(username)

+                 for hub_id in affected_hubs:

+                     add_sse_task(

+                         sse_queue,

+                         "hubs:user-updated",

+                         {"usernames": [username]},

+                         "hub/{}".format(hub_id),

+                     )

+                 user_hub_ids = [

+                     r[0] for r in db.query(hubs.models.Hub.id).filter(

+                         hubs.models.Hub.name == username,

+                         hubs.models.Hub.hub_type.in_(["user", "stream"])

+                     )

+                 ]

+                 for hub_id in user_hub_ids:

+                     add_sse_task(

+                         sse_queue,

+                         "hubs:hub-updated",

+                         None,

+                         "hub/{}".format(hub_id),

+                     )

              log.debug("  Done.")

      except KeyboardInterrupt:

          pass

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

      'hubs.widgets.about:About',

      'hubs.widgets.badges:Badges',

      'hubs.widgets.bugzilla:Bugzilla',

-     'hubs.widgets.contact:Contact',

      'hubs.widgets.library:Library',

      'hubs.widgets.weeklyactivity:WeeklyActivity',

      'hubs.widgets.feed:Feed',
@@ -72,7 +71,6 @@ 

      'hubs.widgets.my_hubs:MyHubs',

      'hubs.widgets.pagure_pr:PagurePRs',

      'hubs.widgets.pagureissues:PagureIssues',

-     'hubs.widgets.rules:Rules',

      'hubs.widgets.sticky:Sticky',

      'hubs.widgets.workflow.updates2stable:Updates2Stable',

      ]

file modified
+3 -21
@@ -18,19 +18,13 @@ 

  

      # Right Side Widgets

      widget = hubs.models.Widget(

-         plugin='contact', index=-2,

+         plugin='my_hubs', index=0,

          _config=json.dumps({

          }))

      hub.widgets.append(widget)

  

      widget = hubs.models.Widget(

-         plugin='my_hubs', index=3,

-         _config=json.dumps({

-         }))

-     hub.widgets.append(widget)

- 

-     widget = hubs.models.Widget(

-         plugin='badges', index=4,

+         plugin='badges', index=1,

          _config=json.dumps({

              'username': hub.name,

          }))
@@ -49,21 +43,9 @@ 

          }))

      hub.widgets.append(widget)

  

-     widget = hubs.models.Widget(

-         plugin='rules', index=0,

-         _config=json.dumps({

-             # TODO -- can we guess their urls?

-             'link': None,

-             'schedule_text': None,

-             'schedule_link': None,

-             'minutes_link': None,

-         })

-     )

-     hub.widgets.append(widget)

- 

      hub.widgets.append(

          hubs.models.Widget(

-             plugin='irc', index=1,

+             plugin='irc', index=0,

              _config=json.dumps({

                  'height': 450,

              })

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

  

  import flask

  import sqlalchemy as sa

+ from pytz import timezone

  from sqlalchemy.orm import relation

  from sqlalchemy.orm.session import object_session

  
@@ -317,6 +318,11 @@ 

              if assoc.role not in ROLES:

                  continue

              result["users"][assoc.role].append(assoc.user.__json__())

+         if self.config["timezone"] is not None:

+             now = datetime.datetime.now()

+             offset = timezone(self.config["timezone"]).utcoffset(now)

+             result["config"]["timezone_offset"] = (

+                 offset.days * 86400 + offset.seconds)

          if self.hub_type == "user":

              user = User.query.get(self.name)

              if user is None:

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

  from __future__ import unicode_literals

  

  import logging

+ import datetime

  try:

      from collections.abc import MutableMapping

  except ImportError:
@@ -75,7 +76,7 @@ 

  class EnumConverter(Converter):

  

      def __init__(self, allowed_values):

-         self.func = lambda v: v

+         super(EnumConverter, self).__init__()

          self.allowed_values = allowed_values

  

      def to_db(self, value):
@@ -85,17 +86,30 @@ 

          return super(EnumConverter, self).to_db(value)

  

  

+ class DateConverter(Converter):

+ 

+     date_format = "%Y-%m-%dT%H:%M:%S"

+ 

+     def from_db(self, value):

+         return datetime.datetime.strptime(value, self.date_format)

+ 

+     def to_db(self, value):

+         return value.strftime(self.date_format)

+ 

+ 

  class HubConfigProxy(MutableMapping):

  

      KEYS = (

          "archived", "summary", "description", "left_width", "avatar",

          "visibility", "chat_domain", "chat_channel", "mailing_list",

-         "calendar", "meetings_text", "rules_url",

+         "calendar", "meetings_text", "rules_url", "timezone", "chat_nickname",

+         "email", "creation_date", "country"

      ) + tuple(p["name"] for p in DEV_PLATFORMS)

      CONVERTERS = {

          "archived": BooleanConverter(),

          "left_width": Converter(int),

          "visibility": EnumConverter(VISIBILITIES),

+         "creation_date": DateConverter(),

      }

      # Default is None if not specified here:

      DEFAULTS = {

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

          if Hub.by_name(self.username, "stream") is None:

              Hub.create_stream_hub(self.username)

          flask.g.task_queue.enqueue(

-             "sync-user-roles",

+             "sync-user",

              username=self.username,

+             created=True,

              )

hubs/static/client/app/components/CurrentTime.js hubs/static/client/app/widgets/contact/CurrentTime.js
file renamed
file was moved with no change to the file
@@ -1,8 +1,3 @@ 

- .HubCommunity {

-     background-color: #e7e7e7;

-     text-align: left;

- }

- 

  .HubCommunity h6 {

    font-family: 'Open Sans', sans-serif;

    text-transform: capitalize;

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

+ .HubDetails {

+     background-color: #e7e7e7;

+     text-align: left;

+ }

@@ -0,0 +1,26 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import UserContact from './UserContact';

+ import HubCommunity from './HubCommunity';

+ import "./HubDetails.css";

+ 

+ 

+ export default class HubDetails extends React.Component {

+ 

+   render() {

+     return (

+       <div className="HubDetails p-2 mt-2 rounded">

+         { this.props.hub.type === "user" &&

+           <UserContact hub={this.props.hub} />

+         }

+         { this.props.hub.type === "team" &&

+           <HubCommunity hub={this.props.hub} />

+         }

+       </div>

+     );

+   }

+ }

+ HubDetails.propTypes = {

+   hub: PropTypes.object.isRequired,

+ }

+ 

@@ -3,7 +3,7 @@ 

  import HubConfig from '../HubConfig';

  import EditModeButton from './EditModeButton';

  import HubMembership from './HubMembership';

- import HubCommunity from './HubCommunity';

+ import HubDetails from './HubDetails';

  

  

  export default class HubHeaderRight extends React.Component {
@@ -18,7 +18,7 @@ 

              <EditModeButton />

            </div>

          }

-         <HubCommunity hub={this.props.hub} />

+         <HubDetails hub={this.props.hub} />

        </div>

      );

    }

@@ -0,0 +1,52 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import {

+   FormattedDate

+ } from "react-intl";

+ import CurrentTime from "../CurrentTime";

+ 

+ 

+ export default class UserContact extends React.Component {

+ 

+   render() {

+     return (

+       <div className="UserContact">

+         <ul className="fa-ul">

+           <li>

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

+             <span className="ml-2">

+                 {this.props.hub.config.country}

+             </span>

+           </li>

+           <li>

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

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

+             <CurrentTime offset={this.props.hub.config.timezone_offset} />

+           </li>

+           <li>

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

+             <span className="ml-2">

+               {this.props.hub.config.email}

+             </span>

+           </li>

+           <li>

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

+             <span className="ml-2">

+               {this.props.hub.config.chat_nickname}

+             </span>

+           </li>

+         </ul>

+         <div className="text-center">

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

+           <span className="ml-2 mr-1">

+             Member Since

+           </span>

+           <FormattedDate value={Date.parse(this.props.hub.config.creation_date)} />

+         </div>

+       </div>

+     );

+   }

+ }

+ UserContact.propTypes = {

+   hub: PropTypes.object.isRequired,

+ };

@@ -1,11 +0,0 @@ 

- import React from 'react';

- import SimpleWidgetConfig from '../../components/SimpleWidgetConfig';

- 

- 

- // Use the default configuration, it's sufficient.

- 

- export default function Config(props) {

-   return (

-     <SimpleWidgetConfig {...props} />

-   );

- }

@@ -1,49 +0,0 @@ 

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

-     );

-   }

- }

@@ -1,115 +0,0 @@ 

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

- };

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

- .Karma .SpinnerCircle {

-     display: inline-block;

- }

file modified
-48
@@ -282,21 +282,6 @@ 

  

  

  /*

-  *  Contact Widget

-  */

- .widget-contact .card-block ul {

-   padding: 0px; 

- }

- .widget-contact .card-block i {

-   width: 25px;

-   text-align: center;

- }

- .widget-contact .contactinfo-container {

-   font-size: 80%;

- }

- 

- 

- /*

   * Issue widgets

   */

  .widget .widget-pagure_pr li,
@@ -311,39 +296,6 @@ 

  border: 0}

  

  

- /*

-  * Rules widget

-  */

- .widget-rules .rules-table {

-   width: 180px

-   padding: 0px;

-   margin: 0px;

-   display: inline-block;

- }

- .widget-rules .rules-table td {

-   font-size: 32pt;

- }

- .widget-rules .rules-container h6 {

-   font-family: 'Open Sans', sans-serif;

-   text-transform: capitalize;

-   font-size: normal;

-   font-weight: 700;

-   color: #808080;

-   margin-bottom: 5px;

- }

- .widget-rules .rules-container img {

-   margin-bottom: 15px;

- }

- .widget-rules .img-circle-lg {

-   height: 4em;

-   width: 4em;

-   padding: 10px;

- }

- .widget-rules .modal-header {

-   border-bottom: 0;

- }

- 

- 

  /** fedora bootstrap overrides **/

  

  /** tightening up card headers - they are too fat imho **/

@@ -23,4 +23,4 @@ 

          hub.widgets.append(widget)

          module_names = [w.plugin for w in triage.get_widgets()]

          self.assertNotIn("non-existant", module_names)

-         self.assertEqual(len(module_names), 69)

+         self.assertEqual(len(module_names), 64)

@@ -23,7 +23,7 @@ 

  

          # check if widgets exist

          widgets = hubs.models.Widget.query.filter_by(hub=hub).all()

-         self.assertEqual(11, len(widgets))

+         self.assertEqual(10, len(widgets))

  

          # delete the hub

          self.session.delete(hub)
@@ -51,10 +51,10 @@ 

          ralph = hubs.models.User.get(username)

          hub_ralph = hubs.models.Hub.by_name(username, "user")

          widget_ralph = hubs.models.Widget.query.filter_by(

-             hub=hub_ralph, plugin="contact").one()

+             hub=hub_ralph, plugin="my_hubs").one()

          hub_decause = hubs.models.Hub.by_name("decause", "user")

          widget_decause = hubs.models.Widget.query.filter_by(

-             hub=hub_decause, plugin="contact").one()

+             hub=hub_decause, plugin="my_hubs").one()

          self.assertEqual(

              hub_decause._get_auth_access_level(ralph),

              AccessLevel.logged_in)
@@ -96,7 +96,7 @@ 

          ralph = hubs.models.User.get(username)

          hub = hubs.models.Hub.by_name(username, "user")

          widget = hubs.models.Widget.query.filter_by(

-             hub=hub, plugin="contact").one()

+             hub=hub, plugin="my_hubs").one()

          assert len(hub.associations) == 1

          assert hub.associations[0].user.username == "ralph"

          for role in ["owner", "sponsor", "member"]:

@@ -31,7 +31,7 @@ 

  

          # check if widgets still are intact

          widgets = hubs.models.Widget.query.filter_by(hub=hub)

-         self.assertEqual(11, widgets.count())

+         self.assertEqual(10, widgets.count())

  

  

  class BookmarksTest(hubs.tests.APPTest):

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

      def test_auth_widget_permission_name(self):

          hub = hubs.models.Hub.by_name("ralph", "user")

          widget = hubs.models.Widget.query.filter_by(

-             hub=hub, plugin="contact").one()

+             hub=hub, plugin="my_hubs").one()

          self.assertEqual(

              widget._get_auth_permission_name("view"), "hub.public.view")

          hub.config["visibility"] = "private"

@@ -56,7 +56,7 @@ 

      def test_move_widget(self):

          hub = Hub(name="testing", hub_type="team")

          self.session.add(hub)

-         widget_names = ["about", "badges", "bugzilla", "contact", "sticky"]

+         widget_names = ["about", "badges", "bugzilla", "my_hubs", "sticky"]

  

          def get_names():

              return [
@@ -83,23 +83,23 @@ 

          # From middle to middle forwards

          check_move(

              "badges", 3,

-             ["about", "bugzilla", "contact", "badges", "sticky"])

+             ["about", "bugzilla", "my_hubs", "badges", "sticky"])

          # From middle to middle backwards

          check_move(

              "badges", 1,

-             ["about", "badges", "bugzilla", "contact", "sticky"])

+             ["about", "badges", "bugzilla", "my_hubs", "sticky"])

          # From middle to start

          check_move(

-             "contact", 0,

-             ["contact", "about", "badges", "bugzilla", "sticky"])

+             "my_hubs", 0,

+             ["my_hubs", "about", "badges", "bugzilla", "sticky"])

          # From middle to end

          check_move(

              "badges", 5,

-             ["contact", "about", "bugzilla", "sticky", "badges"])

+             ["my_hubs", "about", "bugzilla", "sticky", "badges"])

          # From start to end

          check_move(

-             "contact", 5,

-             ["about", "bugzilla", "sticky", "badges", "contact"])

+             "my_hubs", 5,

+             ["about", "bugzilla", "sticky", "badges", "my_hubs"])

  

      def test_move_widget_disabled(self):

          # Make sure moving works even if there are disabled widgets

@@ -38,10 +38,14 @@ 

              "config": {

                  "archived": False,

                  "avatar": avatar_url,

+                 "calendar": None,

                  'chat_channel': None,

                  'chat_domain': 'irc.freenode.net',

-                 "calendar": None,

+                 'chat_nickname': None,

+                 'country': None,

+                 'creation_date': None,

                  "description": None,

+                 'email': None,

                  "mailing_list": None,

                  "meetings_text": None,

                  "github": [],
@@ -49,6 +53,7 @@ 

                  "left_width": 8,

                  "rules_url": None,

                  "summary": "Ralph",

+                 "timezone": None,

                  "visibility": "public",

              },

              "users": {

@@ -13,13 +13,16 @@ 

  

      def test_get_widgets(self):

          hub = Hub.by_name('ralph', "user")

-         expected_ids = [41, 42, 40, 43, 44, 45, 46, 47, 48, 49, 66]

+         expected_widgets = [

+             'meetings', 'feed', 'workflow.updates2stable', 'my_hubs', 'badges',

+             'pagure_pr', 'github_pr', 'bugzilla', 'halp', 'about',

+         ]

          response = self.check_url("/api/hubs/%s/widgets/" % hub.id)

          response_data = json.loads(response.get_data(as_text=True))

          self.assertEqual(response_data["status"], "OK")

          self.assertListEqual(

-             [w["idx"] for w in response_data["data"]],

-             expected_ids)

+             [w["name"] for w in response_data["data"]],

+             expected_widgets)

  

      def test_get_widgets_private(self):

          hub = Hub.by_name('ralph', "user")
@@ -288,6 +291,7 @@ 

  

      def test_delete(self):

          user = FakeAuthorization('ralph')

+         widget_id = self.widget.idx

          with auth_set(app, user):

              result = self.app.delete(self.url)

          self.assertEqual(result.status_code, 200)
@@ -296,7 +300,7 @@ 

              {"status": "OK"})

          response = self.check_url("/api/hubs/%s/widgets/" % self.hub.id)

          response_data = json.loads(response.get_data(as_text=True))

-         self.assertNotIn(37, [w["idx"] for w in response_data["data"]])

+         self.assertNotIn(widget_id, [w["idx"] for w in response_data["data"]])

  

      def test_delete_unauthorized(self):

          user = FakeAuthorization('decause')

@@ -1,206 +0,0 @@ 

- from __future__ import unicode_literals

- 

- import json

- 

- import mock

- import requests

- 

- import hubs

- from hubs.models import Hub, Widget

- from hubs.tests import FakeAuthorization, auth_set

- from hubs.widgets.contact.functions import GetFASInfo

- from . import WidgetTest

- 

- 

- 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 mocked_requests_get(*args, **kwargs):

-     if '/ralph' in kwargs["url"]:

-         data = {

-             "current": 0,

-             "decrements": 0,

-             "increments": 0,

-             "release": "f24",

-             "total": 0,

-             "username": "ralph"

-         }

-         return MockResponse(json_data=data, status_code=200)

- 

-     return MockResponse({}, 404)

- 

- 

- def mocked_requests_post(*args, **kwargs):

-     if '/ralph' in kwargs['url']:

-         data = {

-             "current": 1,

-             "decrements": 0,

-             "increments": 1,

-             "release": "f24",

-             "total": 1,

-             "username": "ralph"

-         }

-         return MockResponse(json_data=data, status_code=200)

- 

-     return MockResponse({}, 404)

- 

- 

- class ContactsTest(WidgetTest):

- 

-     plugin = "contact"

-     maxDiff = None

- 

-     def setUp(self):

-         super(ContactsTest, self).setUp()

-         hub = Hub.by_name('ralph', "user")

-         self.widget = Widget(

-             plugin='contact',

-             index=1,

-         )

-         hub.widgets.append(self.widget)

-         self.session.commit()

-         self.widget_idx = self.widget.idx

- 

-     @mock.patch('hubs.widgets.contact.functions.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',

-             'country_code': 'US',

-             'timezone': 'UTC',

-             'username': 'ralph',

-         }

-         mock_fas2.AccountSystem.return_value = fake_account_system

-         user = FakeAuthorization('ralph')

-         response = self.check_url(

-             '/widgets/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('hubs.widgets.contact.functions.fedmsg_config')

-     def test_no_fas_credentials_function(self, fedmsg_config):

-         fedmsg_config.__getitem__.return_value = {}

-         func = GetFASInfo(self.widget)

-         result = func.execute()

-         self.assertIsNone(result)

- 

-     @mock.patch('hubs.widgets.contact.views.fedmsg_config')

-     def test_no_fas_credentials_view(self, fedmsg_config):

-         fedmsg_config.__getitem__.return_value = {}

-         user = FakeAuthorization('ralph')

-         response = self.check_url(

-             '/widgets/contact/%i/data' % self.widget_idx, user)

-         self.assertDictEqual(

-             json.loads(response.get_data(as_text=True)),

-             {

-                 "status": "ERROR",

-                 "message": "No FAS credentials configured, report this "

-                            "to the system administrator.",

-             })

- 

-     @mock.patch('requests.request', side_effect=mocked_requests_get)

-     def test_plus_plus_get_valid(self, mock_request):

-         url = "/widgets/contact/%d/plus-plus" % self.widget_idx

-         result = self.app.get(url)

-         expected = {

-             "current": 0,

-             "decrements": 0,

-             "increments": 0,

-             "release": "f24",

-             "total": 0,

-             "username": "ralph"

-         }

-         self.assertEqual(result.status_code, 200)

-         self.assertEqual(

-             json.loads(result.get_data(as_text=True)),

-             dict(status="OK", data=expected))

- 

-     @mock.patch('requests.request', side_effect=mocked_requests_post)

-     def test_plus_plus_post_increment_valid(self, mock_request):

-         url = "/widgets/contact/%d/plus-plus" % self.widget_idx

-         user = FakeAuthorization('decause')

-         with auth_set(hubs.app.app, user):

-             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": "ralph"

-             }

-             self.assertEqual(result.status_code, 200)

-             self.assertEqual(

-                 json.loads(result.get_data(as_text=True)),

-                 dict(status="OK", data=expected))

- 

-     @mock.patch('requests.request', side_effect=mocked_requests_post)

-     def test_plus_plus_post_increment_myself_error(self, mock_request):

-         url = "/widgets/contact/%d/plus-plus" % self.widget_idx

-         user = FakeAuthorization('ralph')

-         with auth_set(hubs.app.app, user):

-             result = self.app.post(

-                 url,

-                 content_type="application/json",

-                 data=json.dumps({'increment': True}))

-             self.assertEqual(

-                 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 = "/widgets/contact/%d/plus-plus" % self.widget_idx

-         user = FakeAuthorization('decause')

-         with auth_set(hubs.app.app, user):

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

-                 json.loads(result.get_data(as_text=True)),

-                 {"status": "ERROR", "message": exp_str}

-                 )

- 

-     @mock.patch('requests.request')

-     def test_plus_plus_connection_error(self, mock_request):

-         mock_request.side_effect = requests.ConnectionError("connection error")

-         url = "/widgets/contact/%d/plus-plus" % self.widget_idx

-         result = self.app.get(url)

-         self.assertEqual(

-             json.loads(result.get_data(as_text=True)),

-             {

-                 "status": "ERROR",

-                 "message": "Could not connect to "

-                            "http://localhost:5001/user/ralph",

-              }

-         )

file modified
+47 -1
@@ -5,6 +5,8 @@ 

  

  import fedora

  import flask

+ import pycountry

+ from dateutil.parser import parse as parse_date

  from six.moves.email_mime_text import MIMEText

  

  from hubs.database import Session
@@ -35,6 +37,8 @@ 

      def group_by_name(self, fas_name):

          return self.client.group_by_name(fas_name)

  

+     # Team

+ 

      def sync_team_hub(self, hub):

          fas_group = self.client.group_by_name(hub.name)

          # Config
@@ -100,6 +104,26 @@ 

              affected_users.add(username)

          return list(affected_users)

  

+     # User

+ 

+     def sync_user(self, user):

+         fas_user = self.client.person_by_username(user.username)

+         hub = Hub.by_name(user.username, "user")

+         # Config

+         attr_map = {

+             "timezone": "timezone",

+             "ircnick": "chat_nickname",

+             "email": "email",

+         }

+         for fas_attr, hub_attr in attr_map.items():

+             if fas_user[fas_attr] is None:

+                 continue

+             hub.config[hub_attr] = fas_user[fas_attr]

+         hub.config["creation_date"] = parse_date(fas_user["creation"])

+         country = pycountry.countries.get(alpha_2=fas_user["country_code"])

+         hub.config["country"] = country.name

+         return fas_user

+ 

      def sync_user_roles(self, user, hub=None):

          fas_user = self.client.person_by_username(user.username)

          affected_hubs = set()
@@ -206,6 +230,27 @@ 

      return affected_users

  

  

+ def sync_user(username, created=False):

+     user = User.query.get(username)

+     if user is None:

+         return []

+     log.debug("Syncing user %s with FAS", username)

+     fas_client = FASClient()

+     affected_hubs = []

+     try:

+         fas_client.sync_user(user)

+         if created:

+             # Sync roles on creation

+             affected_hubs = fas_client.sync_user_roles(user)

+         fas_client.db.commit()

+     except Exception:

+         fas_client.db.rollback()

+         raise

+     log.info("Synced user %s with FAS (%d affected hubs)",

+              username, len(affected_hubs))

+     return affected_hubs

+ 

+ 

  def sync_user_roles(username, hub_id):

      user = User.query.get(username)

      if user is None:
@@ -217,7 +262,8 @@ 

          hub = Hub.query.get(hub_id)

          if hub is None:

              return []

-         log.debug("Syncing user %s's roles on hub %s with FAS", username, hub.name)

+         log.debug("Syncing user %s's roles on hub %s with FAS",

+                   username, hub.name)

      fas_client = FASClient()

      try:

          # Sync user roles

@@ -1,26 +0,0 @@ 

- from __future__ import unicode_literals

- 

- import flask

- 

- from hubs.widgets.base import Widget

- 

- 

- class Contact(Widget):

- 

-     name = "contact"

-     position = "both"

-     display_title = None

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

-             props["urls"] = dict(

-                 data=flask.url_for("contact_data", idx=instance.idx),

-                 # Don't use the plus-plus server, it's not deployed yet.

-                 # karma=flask.url_for("contact_plus_plus", idx=instance.idx),

-                 )

-         return props

@@ -1,44 +0,0 @@ 

- 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,125 +0,0 @@ 

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

@@ -1,86 +0,0 @@ 

- from __future__ import unicode_literals

- 

- from collections import OrderedDict as ordereddict

- 

- from hubs.utils import username2avatar, validators

- from hubs.widgets.base import Widget

- from hubs.widgets.view import RootWidgetView

- 

- 

- ELLIPSIS_LIMIT = 5

- 

- 

- class Rules(Widget):

- 

-     name = "rules"

-     position = "both"

-     display_css = "card-info"

-     display_title = None

-     hub_types = ['team']

-     parameters = [

-         dict(

-             name="link",

-             label="Link",

-             default=None,

-             validator=validators.Link,

-             help="Link to the community rules and guidelines.",

-         ), dict(

-             name="schedule_text",

-             label="Schedule text",

-             default=None,

-             validator=validators.Text,

-             help="Some text about when meetings are.",

-         ), dict(

-             name="schedule_link",

-             label="Schedule link",

-             default=None,

-             validator=validators.Link,

-             help="Link to a schedule for IRC meetings, etc.",

-         ), dict(

-             name="minutes_link",

-             label="Minutes link",

-             default=None,

-             validator=validators.Link,

-             help="Link to meeting menutes from past meetings.",

-         )]

- 

- 

- class BaseView(RootWidgetView):

- 

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

-         hub = instance.hub

-         hub_config = hub.config

-         owners = hub.owners

-         oldest_owners = sorted(

-             owners, key=lambda o: o.created_on)[:ELLIPSIS_LIMIT]

-         oldest_owners = [{

-             'username': o.username,

-             'avatar': username2avatar(o.username)

-         } for o in oldest_owners]

- 

-         owners = ordereddict([

-             (o.username, username2avatar(o.username)) for o in owners

-         ])

-         mailing_list = hub_config["mailing_list"]

-         if mailing_list is not None:

-             mailing_list_url = (

-                 'https://lists.fedoraproject.org/archives/list/{}/'.format(

-                     mailing_list))

-         else:

-             mailing_list_url = None

-         irc_channel = irc_network = None

-         if hub_config["chat_channel"]:

-             irc_channel = hub_config["chat_channel"]

-             irc_network = hub_config["chat_domain"]

-         return dict(

-             oldest_owners=oldest_owners,

-             owners=owners,

-             link=instance.config["link"],

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

-             )

@@ -1,91 +0,0 @@ 

- <div class="rules-container py-2">

-   {% if link %}

-   <h6>community rules</h6>

-   <p><a href="{{link}}">Community Rules and Guidelines</a></h6>

-   {% endif %}

-   <h6>group owners ({{ owners|length }})</h6>

-   <div class="row no-gutters">

-     {% if owners|length > 5%}

-       {% for owner in oldest_owners %}

-         <div class="col-sm-6">

-           <img class="img-circle" src="{{owner['avatar']}}"/>

-           <a href="{{ url_for('hub', hub_name=owner['username'], hub_type='u') }}">{{owner['username']}}</a>

-         </div>

-         {% endfor %}

-         <br/>

-         <a href="#" class="btn btn-secondary" data-toggle="modal" data-target="#ownersModal">View All</a>

-     {% else %}

-       {% for owner in owners %}

-         <div class="col-sm-6">

-           <img class="img-circle" src="{{owners[owner]}}"/>

-           <a href="{{ url_for('hub', hub_name=owner, hub_type='u')}}">{{owner}}</a>

-         </div>

-       {% endfor %}

-     {% endif %}

-   </div>

-   {% if schedule_text or schedule_link or minutes_link %}

-   <h6>meetings</h6>

-   {% if schedule_text %}

-   <p>{{schedule_text}}</p>

-   {% endif %}

-   <p>

-   {% if schedule_link %}

-   <a class="small" target="_blank" href="{{schedule_link}}">Meeting Schedule</a>

-   {% endif %}

-   {% if schedule_link and minutes_link %}

-   &#124;

-   {% endif %}

-   {% if minutes_link %}

-   <a class="small" target="_blank" href="{{minutes_link}}">Past Meeting Minutes</a>

-   {% endif %}

-   </p>

-   {% endif %}

- 

-   {% if mailing_list_url or irc_channel %}

-   <h6>Communication</h6>

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

-     {% if mailing_list and mailing_list_url %}

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

-     {% endif %}

-     {% 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>

-   {% endif %}

- </div>

- 

- 

- <!-- View all modal -->

- <div class="modal fade" id="ownersModal" tabindex="-1" role="dialog" aria-labelledby="ownersModal" aria-hidden="true">

-   <div class="modal-dialog">

-     <div class="modal-content">

-       <div class="modal-header">

-         <h4 class="modal-title" id="myModalLabel">All group owners</h4>

-       </div>

-       <div id="owners">

-         {% for owner in owners %}

-         <div class="col-sm-6">

-           <img class="img-circle img-circle-lg" src="{{owners[owner]}}"/>

-           <a href="{{ url_for('hub', hub_name=owner, hub_type='u')}}">{{owner}}</a>

-         </div>

-         {% endfor %}

-       </div>

-       <div class="modal-footer">

-         <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>

-       </div>

-     </div>

-   </div>

- </div>

- 

- <style>

- </style>

file modified
+1
@@ -21,6 +21,7 @@ 

  markdown

  munch

  psycopg2

+ pycountry

  pymongo

  pytz

  requests

rebased onto f294254

6 years ago

Since this should have been part of the previous PR which is merged already, I'll merge that one.

Pull-Request has been merged by abompard

6 years ago
Metadata
Changes Summary 33
+8 -0
file changed
hubs/backend/triage.py
+23 -0
file changed
hubs/backend/worker.py
+0 -2
file changed
hubs/default_config.py
+3 -21
file changed
hubs/defaults.py
+6 -0
file changed
hubs/models/hub.py
+16 -2
file changed
hubs/models/hubconfig.py
+2 -1
file changed
hubs/models/user.py
+0 -0
file renamed
hubs/static/client/app/widgets/contact/CurrentTime.js
hubs/static/client/app/components/CurrentTime.js
+0 -5
file changed
hubs/static/client/app/components/HubHeader/HubCommunity.css
+4
file added
hubs/static/client/app/components/HubHeader/HubDetails.css
+26
file added
hubs/static/client/app/components/HubHeader/HubDetails.js
+2 -2
file changed
hubs/static/client/app/components/HubHeader/HubHeaderRight.js
+52
file added
hubs/static/client/app/components/HubHeader/UserContact.js
-11
file removed
hubs/static/client/app/widgets/contact/Config.js
-49
file removed
hubs/static/client/app/widgets/contact/Karma.js
-115
file removed
hubs/static/client/app/widgets/contact/Widget.js
-3
file removed
hubs/static/client/app/widgets/contact/contact.css
+0 -48
file changed
hubs/static/css/style.css
+1 -1
file changed
hubs/tests/backend/test_triage.py
+4 -4
file changed
hubs/tests/models/test_hub.py
+1 -1
file changed
hubs/tests/models/test_user.py
+1 -1
file changed
hubs/tests/models/test_widget.py
+8 -8
file changed
hubs/tests/utils/test_views.py
+6 -1
file changed
hubs/tests/views/test_api_hub_config.py
+8 -4
file changed
hubs/tests/views/test_api_hub_widget.py
-206
file removed
hubs/tests/widgets/test_contact.py
+47 -1
file changed
hubs/utils/fas.py
-26
file removed
hubs/widgets/contact/__init__.py
-44
file removed
hubs/widgets/contact/functions.py
-125
file removed
hubs/widgets/contact/views.py
-86
file removed
hubs/widgets/rules/__init__.py
-91
file removed
hubs/widgets/rules/templates/root.html
+1 -0
file changed
requirements.txt