From f294254be3afaad222c5c3df14e69a5c9578a4ec Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 18 2018 14:36:23 +0000 Subject: [PATCH 1/3] Sync some user properties from FAS --- diff --git a/hubs/backend/triage.py b/hubs/backend/triage.py index 2226acf..d296c03 100755 --- a/hubs/backend/triage.py +++ b/hubs/backend/triage.py @@ -81,6 +81,14 @@ def triage(msg): '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. diff --git a/hubs/backend/worker.py b/hubs/backend/worker.py index 5a2e5cb..9ccaccf 100755 --- a/hubs/backend/worker.py +++ b/hubs/backend/worker.py @@ -175,6 +175,29 @@ def main(args=None): ) # 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 diff --git a/hubs/models/hubconfig.py b/hubs/models/hubconfig.py index 0c8e332..e576823 100644 --- a/hubs/models/hubconfig.py +++ b/hubs/models/hubconfig.py @@ -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 BooleanConverter(Converter): 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 @@ class EnumConverter(Converter): 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 = { diff --git a/hubs/models/user.py b/hubs/models/user.py index c72c3b2..dcd519a 100644 --- a/hubs/models/user.py +++ b/hubs/models/user.py @@ -151,6 +151,7 @@ class User(BASE): 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, ) diff --git a/hubs/static/css/style.css b/hubs/static/css/style.css index 628c691..0ecfd55 100644 --- a/hubs/static/css/style.css +++ b/hubs/static/css/style.css @@ -311,39 +311,6 @@ header span.edit { 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 **/ diff --git a/hubs/tests/views/test_api_hub_config.py b/hubs/tests/views/test_api_hub_config.py index 63f093b..d91faae 100644 --- a/hubs/tests/views/test_api_hub_config.py +++ b/hubs/tests/views/test_api_hub_config.py @@ -38,10 +38,14 @@ class TestAPIHubConfig(APPTest): "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 @@ class TestAPIHubConfig(APPTest): "left_width": 8, "rules_url": None, "summary": "Ralph", + "timezone": None, "visibility": "public", }, "users": { diff --git a/hubs/utils/fas.py b/hubs/utils/fas.py index ce03613..90ddf8f 100644 --- a/hubs/utils/fas.py +++ b/hubs/utils/fas.py @@ -5,6 +5,8 @@ import smtplib 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 @@ class FASClient(object): 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 @@ class FASClient(object): 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 @@ def sync_team_hub_roles(hub_id): 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 @@ def sync_user_roles(username, hub_id): 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 diff --git a/requirements.txt b/requirements.txt index af5cdea..9aea1a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ iso3166 markdown munch psycopg2 +pycountry pymongo pytz requests From baad9529368fb07d707ffbc120d8ea62ae51ba21 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 18 2018 14:36:23 +0000 Subject: [PATCH 2/3] Display the user contact info --- diff --git a/hubs/models/hub.py b/hubs/models/hub.py index 3f9a12f..03bc5a5 100644 --- a/hubs/models/hub.py +++ b/hubs/models/hub.py @@ -27,6 +27,7 @@ import logging 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 @@ class Hub(ObjectAuthzMixin, BASE): 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: diff --git a/hubs/static/client/app/components/CurrentTime.js b/hubs/static/client/app/components/CurrentTime.js new file mode 100644 index 0000000..73d9eea --- /dev/null +++ b/hubs/static/client/app/components/CurrentTime.js @@ -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 ( + + ); + } +} +CurrentTime.propTypes = { + offset: PropTypes.number, // must be in seconds. +} +CurrentTime.defaultProps = { + offset: 0, +} diff --git a/hubs/static/client/app/components/HubHeader/HubCommunity.css b/hubs/static/client/app/components/HubHeader/HubCommunity.css index a5f70c6..17d04bc 100644 --- a/hubs/static/client/app/components/HubHeader/HubCommunity.css +++ b/hubs/static/client/app/components/HubHeader/HubCommunity.css @@ -1,8 +1,3 @@ -.HubCommunity { - background-color: #e7e7e7; - text-align: left; -} - .HubCommunity h6 { font-family: 'Open Sans', sans-serif; text-transform: capitalize; diff --git a/hubs/static/client/app/components/HubHeader/HubDetails.css b/hubs/static/client/app/components/HubHeader/HubDetails.css new file mode 100644 index 0000000..fc24cca --- /dev/null +++ b/hubs/static/client/app/components/HubHeader/HubDetails.css @@ -0,0 +1,4 @@ +.HubDetails { + background-color: #e7e7e7; + text-align: left; +} diff --git a/hubs/static/client/app/components/HubHeader/HubDetails.js b/hubs/static/client/app/components/HubHeader/HubDetails.js new file mode 100644 index 0000000..cf3c713 --- /dev/null +++ b/hubs/static/client/app/components/HubHeader/HubDetails.js @@ -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 ( +
+ { this.props.hub.type === "user" && + + } + { this.props.hub.type === "team" && + + } +
+ ); + } +} +HubDetails.propTypes = { + hub: PropTypes.object.isRequired, +} + diff --git a/hubs/static/client/app/components/HubHeader/HubHeaderRight.js b/hubs/static/client/app/components/HubHeader/HubHeaderRight.js index f58dc19..2cc2734 100644 --- a/hubs/static/client/app/components/HubHeader/HubHeaderRight.js +++ b/hubs/static/client/app/components/HubHeader/HubHeaderRight.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; 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 @@ export default class HubHeaderRight extends React.Component { } - + ); } diff --git a/hubs/static/client/app/components/HubHeader/UserContact.js b/hubs/static/client/app/components/HubHeader/UserContact.js new file mode 100644 index 0000000..27e55bc --- /dev/null +++ b/hubs/static/client/app/components/HubHeader/UserContact.js @@ -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 ( +
+
    +
  • + + + {this.props.hub.config.country} + +
  • +
  • + + Current Time: + +
  • +
  • + + + {this.props.hub.config.email} + +
  • +
  • + + + {this.props.hub.config.chat_nickname} + +
  • +
+
+ + + Member Since + + +
+
+ ); + } +} +UserContact.propTypes = { + hub: PropTypes.object.isRequired, +}; From 0dbc6eee44d27a11ede5c35c10a577ad9ab10aff Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 18 2018 14:39:14 +0000 Subject: [PATCH 3/3] Remove the contact and rules widgets --- diff --git a/hubs/default_config.py b/hubs/default_config.py index 2262593..1ee9a73 100644 --- a/hubs/default_config.py +++ b/hubs/default_config.py @@ -59,7 +59,6 @@ WIDGETS = [ '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 @@ WIDGETS = [ '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', ] diff --git a/hubs/defaults.py b/hubs/defaults.py index 991a222..1865abd 100644 --- a/hubs/defaults.py +++ b/hubs/defaults.py @@ -18,19 +18,13 @@ def add_user_widgets(hub): # 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 @@ def add_group_widgets(hub): })) 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, }) diff --git a/hubs/static/client/app/widgets/contact/Config.js b/hubs/static/client/app/widgets/contact/Config.js deleted file mode 100644 index 1fc4fa8..0000000 --- a/hubs/static/client/app/widgets/contact/Config.js +++ /dev/null @@ -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 ( - - ); -} diff --git a/hubs/static/client/app/widgets/contact/CurrentTime.js b/hubs/static/client/app/widgets/contact/CurrentTime.js deleted file mode 100644 index 73d9eea..0000000 --- a/hubs/static/client/app/widgets/contact/CurrentTime.js +++ /dev/null @@ -1,55 +0,0 @@ -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 ( - - ); - } -} -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 deleted file mode 100644 index 279ce60..0000000 --- a/hubs/static/client/app/widgets/contact/Karma.js +++ /dev/null @@ -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 ? ( - ? - ) : 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 deleted file mode 100644 index 79585ce..0000000 --- a/hubs/static/client/app/widgets/contact/Widget.js +++ /dev/null @@ -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 = ( -
- -
- ); - } 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 deleted file mode 100644 index 51af1c7..0000000 --- a/hubs/static/client/app/widgets/contact/contact.css +++ /dev/null @@ -1,3 +0,0 @@ -.Karma .SpinnerCircle { - display: inline-block; -} diff --git a/hubs/static/css/style.css b/hubs/static/css/style.css index 0ecfd55..0c2306b 100644 --- a/hubs/static/css/style.css +++ b/hubs/static/css/style.css @@ -282,21 +282,6 @@ header span.edit { /* - * 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, diff --git a/hubs/tests/backend/test_triage.py b/hubs/tests/backend/test_triage.py index ff4ada4..9705e8d 100644 --- a/hubs/tests/backend/test_triage.py +++ b/hubs/tests/backend/test_triage.py @@ -23,4 +23,4 @@ class TriageTest(APPTest): 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) diff --git a/hubs/tests/models/test_hub.py b/hubs/tests/models/test_hub.py index 9486d34..40faeba 100644 --- a/hubs/tests/models/test_hub.py +++ b/hubs/tests/models/test_hub.py @@ -23,7 +23,7 @@ class HubTest(hubs.tests.APPTest): # 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 @@ class HubTest(hubs.tests.APPTest): 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 @@ class HubTest(hubs.tests.APPTest): 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"]: diff --git a/hubs/tests/models/test_user.py b/hubs/tests/models/test_user.py index 9ffad59..0768746 100644 --- a/hubs/tests/models/test_user.py +++ b/hubs/tests/models/test_user.py @@ -31,7 +31,7 @@ class UserTest(hubs.tests.APPTest): # 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): diff --git a/hubs/tests/models/test_widget.py b/hubs/tests/models/test_widget.py index b31ee4b..3a2e79a 100644 --- a/hubs/tests/models/test_widget.py +++ b/hubs/tests/models/test_widget.py @@ -10,7 +10,7 @@ class WidgetTest(hubs.tests.APPTest): 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" diff --git a/hubs/tests/utils/test_views.py b/hubs/tests/utils/test_views.py index 7b7e329..aae8ba6 100644 --- a/hubs/tests/utils/test_views.py +++ b/hubs/tests/utils/test_views.py @@ -56,7 +56,7 @@ class ViewUtilsTest(APPTest): 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 @@ class ViewUtilsTest(APPTest): # 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 diff --git a/hubs/tests/views/test_api_hub_widget.py b/hubs/tests/views/test_api_hub_widget.py index 1422c6f..59be3c3 100644 --- a/hubs/tests/views/test_api_hub_widget.py +++ b/hubs/tests/views/test_api_hub_widget.py @@ -13,13 +13,16 @@ class TestAPIHubWidgets(APPTest): 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 @@ class TestAPIHubWidget(APPTest): 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 @@ class TestAPIHubWidget(APPTest): {"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') diff --git a/hubs/tests/widgets/test_contact.py b/hubs/tests/widgets/test_contact.py deleted file mode 100644 index 9544d97..0000000 --- a/hubs/tests/widgets/test_contact.py +++ /dev/null @@ -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", - } - ) diff --git a/hubs/widgets/contact/__init__.py b/hubs/widgets/contact/__init__.py deleted file mode 100644 index 5804653..0000000 --- a/hubs/widgets/contact/__init__.py +++ /dev/null @@ -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 diff --git a/hubs/widgets/contact/functions.py b/hubs/widgets/contact/functions.py deleted file mode 100644 index 36e7aec..0000000 --- a/hubs/widgets/contact/functions.py +++ /dev/null @@ -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 diff --git a/hubs/widgets/contact/views.py b/hubs/widgets/contact/views.py deleted file mode 100644 index a082fbc..0000000 --- a/hubs/widgets/contact/views.py +++ /dev/null @@ -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) diff --git a/hubs/widgets/rules/__init__.py b/hubs/widgets/rules/__init__.py deleted file mode 100644 index 40b8414..0000000 --- a/hubs/widgets/rules/__init__.py +++ /dev/null @@ -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, - ) diff --git a/hubs/widgets/rules/templates/root.html b/hubs/widgets/rules/templates/root.html deleted file mode 100644 index 6c4b661..0000000 --- a/hubs/widgets/rules/templates/root.html +++ /dev/null @@ -1,91 +0,0 @@ -
- {% if link %} -
community rules
-

Community Rules and Guidelines - {% endif %} -

group owners ({{ owners|length }})
-
- {% if owners|length > 5%} - {% for owner in oldest_owners %} - - {% endfor %} -
- View All - {% else %} - {% for owner in owners %} -
- - {{owner}} -
- {% endfor %} - {% endif %} -
- {% if schedule_text or schedule_link or minutes_link %} -
meetings
- {% if schedule_text %} -

{{schedule_text}}

- {% endif %} -

- {% if schedule_link %} - Meeting Schedule - {% endif %} - {% if schedule_link and minutes_link %} - | - {% endif %} - {% if minutes_link %} - Past Meeting Minutes - {% endif %} -

- {% endif %} - - {% if mailing_list_url or irc_channel %} -
Communication
-
    - {% if mailing_list and mailing_list_url %} -
  • - - - {{mailing_list}} - -
  • - {% endif %} - {% if irc_channel %} -
  • - - - {{irc_channel}} on {{irc_network}} - -
  • - {% endif %} -
- {% endif %} -
- - - - - -