From b84cbff976245faf3eec149c2c8ff889bd763a58 Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 1/11] Move 4 spaces to 2 in JSX files In order to comply with JavaScript style standards, change 4 spaces to 2 --- diff --git a/hubs/static/client/app/components/Constituent.jsx b/hubs/static/client/app/components/Constituent.jsx index 17896a1..3691755 100644 --- a/hubs/static/client/app/components/Constituent.jsx +++ b/hubs/static/client/app/components/Constituent.jsx @@ -1,52 +1,52 @@ import React from 'react'; export default class Constituent extends React.Component { - render() { - const match = this.props.match; - const hasConstituent = (match['msg_ids'].length === 1 && Object.keys(match['msg_ids'])[0]['long_form'] !== Object.keys(match['msg_ids'])[0]['subtitle']); - if ( hasConstituent ) { - const constituentKey = Object.keys(match['msg_ids'])[0]; - const constituent = match['msg_ids'][constituentKey]; - const differentIcon = constituent['icon'] != constituent['__icon__']; - const longLink = ( constituent['link'] && constituent['long_form'].indexOf(constituent['link']) === -1 ); + render() { + const match = this.props.match; + const hasConstituent = (match['msg_ids'].length === 1 && Object.keys(match['msg_ids'])[0]['long_form'] !== Object.keys(match['msg_ids'])[0]['subtitle']); + if ( hasConstituent ) { + const constituentKey = Object.keys(match['msg_ids'])[0]; + const constituent = match['msg_ids'][constituentKey]; + const differentIcon = constituent['icon'] != constituent['__icon__']; + const longLink = ( constituent['link'] && constituent['long_form'].indexOf(constituent['link']) === -1 ); - let constituentIcon; - if (differentIcon) - constituentIcon = ( -
- - {constituent['subtitle']}/ - -
+ let constituentIcon; + if (differentIcon) + constituentIcon = ( +
+ + {constituent['subtitle']}/ + +
- ) + ) - let readMore; - if(longLink) - readMore = ( -
- {constituent['long_form']} - - Read more - -
- ) + let readMore; + if(longLink) + readMore = ( +
+ {constituent['long_form']} + + Read more + +
+ ) - return ( -
- - {constituentIcon} - {readMore} -
- ) - } else if (match['msg_ids'].length > 1) { -
- - -
- } - else { - return null - } + return ( +
+ + {constituentIcon} + {readMore} +
+ ) + } else if (match['msg_ids'].length > 1) { +
+ + +
} + else { + return null + } + } } diff --git a/hubs/static/client/app/components/ExpandCollapse.jsx b/hubs/static/client/app/components/ExpandCollapse.jsx index 80ab303..29b1e6f 100644 --- a/hubs/static/client/app/components/ExpandCollapse.jsx +++ b/hubs/static/client/app/components/ExpandCollapse.jsx @@ -1,18 +1,18 @@ import React from 'react'; export default class ExpandCollapse extends React.Component { - render() { - return ( -
- - - expand - - - - collapse - -
- ) - } + render() { + return ( +
+ + + expand + + + + collapse + +
+ ) + } } diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx index 9e6cccb..187a1d1 100644 --- a/hubs/static/client/app/components/Feed.jsx +++ b/hubs/static/client/app/components/Feed.jsx @@ -10,55 +10,55 @@ import {Message, MessageList} from './Messages.jsx'; class Panel extends React.Component { - render() { - return ( -
-
- - - -
-
- ); - } + render() { + return ( +
+
+ + + +
+
+ ); + } } export default class Feed extends React.Component { - constructor (props) { - super(props); - this.state = { - matches: this.props.matches, - sse: true, - } - this.source = (!!window.EventSource) ? new EventSource('http://localhost:9090') : {}; - this.source.addEventListener('error', () => { - this.state.sse = false; - }, false); - window.onbeforeunload = () => { - this.source.close(); - } - this.source.addEventListener('message', resp => { - $.get('/api/fedmsg/markup', { - message: resp.data, - plugin: 'feed', - }).done(data => { - if(!$.isEmptyObject(data)) { - this.state.matches.push.apply(this.state.matches, data); - this.setState({matches: this.state.matches}); - } - }); - }, false); + constructor (props) { + super(props); + this.state = { + matches: this.props.matches, + sse: true, } - render() { - const feedNodes = this.state.matches.map(match => { - return - }); - return ( -
- {feedNodes} -
- ) + this.source = (!!window.EventSource) ? new EventSource('http://localhost:9090') : {}; + this.source.addEventListener('error', () => { + this.state.sse = false; + }, false); + window.onbeforeunload = () => { + this.source.close(); } + this.source.addEventListener('message', resp => { + $.get('/api/fedmsg/markup', { + message: resp.data, + plugin: 'feed', + }).done(data => { + if(!$.isEmptyObject(data)) { + this.state.matches.push.apply(this.state.matches, data); + this.setState({matches: this.state.matches}); + } + }); + }, false); + } + render() { + const feedNodes = this.state.matches.map(match => { + return + }); + return ( +
+ {feedNodes} +
+ ) + } } diff --git a/hubs/static/client/app/components/Icon.jsx b/hubs/static/client/app/components/Icon.jsx index 577050e..70c2d8c 100644 --- a/hubs/static/client/app/components/Icon.jsx +++ b/hubs/static/client/app/components/Icon.jsx @@ -1,15 +1,15 @@ import React from 'react'; export default class Icon extends React.Component { - render() { - return ( -
-
- - - -
-
- ) - } + render() { + return ( +
+
+ + + +
+
+ ) + } } diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx index 45ccbe3..952fe06 100644 --- a/hubs/static/client/app/components/Markup.jsx +++ b/hubs/static/client/app/components/Markup.jsx @@ -1,17 +1,17 @@ import React from 'react'; export default class Markup extends React.Component { - createMarkup() { - return {__html: this.props.match['markup']}; - } - render() { - return ( -
-

- {this.props.match['human_time']} -
- ) - } + createMarkup() { + return {__html: this.props.match['markup']}; + } + render() { + return ( +
+

+ {this.props.match['human_time']} +
+ ) + } } diff --git a/hubs/static/client/app/components/Messages.jsx b/hubs/static/client/app/components/Messages.jsx index f034a87..a773b52 100644 --- a/hubs/static/client/app/components/Messages.jsx +++ b/hubs/static/client/app/components/Messages.jsx @@ -1,36 +1,36 @@ import React from 'react'; class Message extends React.Component { - render() { - if(this.props.constituent['link']) { - return ( -
  • - {this.props.constituent['markup']} - - - -
  • - ) - } else { - return (
  • { this.props.constituent['markup'] }
  • ) - } + render() { + if(this.props.constituent['link']) { + return ( +
  • + {this.props.constituent['markup']} + + + +
  • + ) + } else { + return (
  • { this.props.constituent['markup'] }
  • ) } + } } class MessageList extends React.Component { - render() { - const messageNodes = this.props.items.map(function(item) { - return ; - }); - return ( -
      - {messageNodes} -
    - ) - } + render() { + const messageNodes = this.props.items.map(function(item) { + return ; + }); + return ( +
      + {messageNodes} +
    + ) + } } module.exports = { - Message: Message, - MessageList: MessageList + Message: Message, + MessageList: MessageList } diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx index 29f5129..a753c36 100644 --- a/hubs/static/client/app/index.jsx +++ b/hubs/static/client/app/index.jsx @@ -2,4 +2,3 @@ import React from 'react'; import { render } from 'react-dom'; import Feed from './components/Feed.jsx' - diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html index 00fc7e2..3b01664 100644 --- a/hubs/widgets/templates/feed.html +++ b/hubs/widgets/templates/feed.html @@ -5,11 +5,10 @@ src ="{{url_for('static', filename='js/hubs.js')}}"> From 3bd168a4232553a43ad09dbd8d47c384825496bd Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 2/11] Move Feed to external SSE --- diff --git a/ev-server/hubs-stream-server.py b/ev-server/hubs-stream-server.py deleted file mode 100644 index 56d5b50..0000000 --- a/ev-server/hubs-stream-server.py +++ /dev/null @@ -1,134 +0,0 @@ -# encoding=utf8 -''' -This file is for development purposes only. - -The end goal is for the streaming server to be implemented on the FMN side -''' -import json -import logging -import sys -import trollius -import trollius_redis -import urllib2 -from concurrent.futures import TimeoutError -from trollius import From - -reload(sys) -sys.setdefaultencoding('utf8') - -log = logging.getLogger(__name__) -SERVER = None -REDIS_HOST = '0.0.0.0' -REDIS_PORT = 6379 -REDIS_DB = 0 -EVENTSOURCE_PORT = 9090 -''' -you need to - -pip install trollius trollius_redis - -dnf install redis httpie -systemctl start redis - - -usage: http get 0.0.0.0:9090/ - -''' - - -def get_recent_posts(): - delta = "delta=86400" # one day worth of data - rows_per_page = "rows_per_page=50" - url = "https://apps.fedoraproject.org/datagrepper/raw" + "?" + delta + "&" + rows_per_page - request = urllib2.Request(url) - contents = urllib2.urlopen(request).read() - json_response = json.loads(contents) - return json_response['raw_messages'] - - -@trollius.coroutine -def handle_client(client_reader, client_writer): - origin = '*' - if origin.endswith('/'): - origin = origin[:-1] - - client_writer.write(("HTTP/1.0 200 OK\n" - "Content-Type: text/event-stream\n" - "Cache: nocache\n" - "Connection: keep-alive\n" - "Access-Control-Allow-Origin: %s\n\n" % origin - ).encode()) - - connection = yield trollius.From(trollius_redis.Connection.create( - host=REDIS_HOST, port=REDIS_PORT, - db=REDIS_DB)) - - try: - posts = get_recent_posts() - - # send 50 latest posts - num_post_sent = 0 - while num_post_sent < 50: - reply = posts[num_post_sent] - reply = json.dumps(reply) - log.info(reply) - log.info("Sending post %s %s", str(num_post_sent), reply) - client_writer.write(('data: %s\n\n' % reply).encode()) - yield trollius.From(client_writer.drain()) - num_post_sent += 1 - yield From(trollius.sleep(2)) - - except trollius.ConnectionResetError: - log.exception("ERROR: ConnectionResetError in handle_client") - except Exception: - log.exception("ERROR: Exception in handle_client") - finally: - # Wathever happens, close the connection. - connection.close() - client_writer.close() - - -def main(): - global SERVER - - try: - loop = trollius.get_event_loop() - coro = trollius.start_server(handle_client, - host=None, - port=EVENTSOURCE_PORT, - loop=loop) - SERVER = loop.run_until_complete(coro) - log.info('Serving server at {}'.format(SERVER.sockets[0].getsockname( - ))) - loop.run_forever() - except KeyboardInterrupt: - pass - except trollius.ConnectionResetError as err: - log.exception("ERROR: ConnectionResetError in main") - except Exception as err: - log.exception("ERROR: Exception in main") - finally: - # Close the server - SERVER.close() - log.info("End Connection") - loop.run_until_complete(SERVER.wait_closed()) - loop.close() - log.info("End") - - -if __name__ == '__main__': - log = logging.getLogger("") - formatter = logging.Formatter( - "%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s") - - # setup console logging - log.setLevel(logging.DEBUG) - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) - - aslog = logging.getLogger("asyncio") - aslog.setLevel(logging.DEBUG) - - ch.setFormatter(formatter) - log.addHandler(ch) - main() diff --git a/hubs/app.py b/hubs/app.py index c975952..7e24193 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -639,76 +639,3 @@ def hub_leave(hub): return flask.abort(400) session.commit() return flask.redirect(flask.url_for('hub', name=hub.name)) - - -PATHS = fmn.lib.load_rules(root='fmn.rules') - - -@app.route('/api/fedmsg/markup', methods=['GET']) -def markup_fedmsg(): - ''' - This is a temporary endpoint to create a human-readable form of a message. - For now it serves as a development tool - - This route will be removed once its functionality is integrated into FMN - ''' - from hubs.widgets.feed import ( - apply_markup, rehydrate_preference, - get_remote_preference - ) - try: - data = flask.request.args['message'] - plugin = flask.request.args['plugin'] - except KeyError: - return flask.abort(400) - - widget = hubs.models.Widget.by_plugin(session, plugin) - - if not widget: - return flask.abort(400) - - context = widget.config.get('fmn_context') - - messages = [] - message = json.loads(data) - - if 'topic' not in message: - return flask.abort(400) - - try: - nickname = flask.g.auth.nickname - except AttributeError: # Not logged in - return flask.abort(403) - - preference = get_remote_preference(nickname, context) - - if preference: - try: - preference = rehydrate_preference(preference) - except ImportError: - pass - recipients = fmn.lib.recipients( - [preference], message, PATHS, fedmsg_config) - if recipients: - messages.append(message) - matches = fedmsg.meta.conglomerate(messages, lexers=True, **fedmsg_config) - - for match in matches: - match['markup'] = apply_markup(match) - for _, constituent in match['msg_ids'].items(): - constituent['markup'] = apply_markup(constituent) - if constituent['long_form'] != constituent['subtitle']: - if constituent.get('lexer'): - constituent['long_form'] = pygments.highlight( - constituent['long_form'], - constituent['lexer'], - pygments.formatters.HtmlFormatter(style='monokai'), - ) - else: - markup = u"
    {long_form}
    ".format(**constituent) - constituent['long_form'] = markup - - # And tack on a unique identifier for each top level entry. - match['dom_id'] = six.text_type(uuid.uuid4()) - - return flask.jsonify(matches) diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx index 187a1d1..da00b25 100644 --- a/hubs/static/client/app/components/Feed.jsx +++ b/hubs/static/client/app/components/Feed.jsx @@ -12,11 +12,10 @@ import {Message, MessageList} from './Messages.jsx'; class Panel extends React.Component { render() { return ( -
    +
    -
    ); @@ -30,28 +29,22 @@ export default class Feed extends React.Component { matches: this.props.matches, sse: true, } - this.source = (!!window.EventSource) ? new EventSource('http://localhost:9090') : {}; + this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {}; this.source.addEventListener('error', () => { this.state.sse = false; }, false); window.onbeforeunload = () => { this.source.close(); } - this.source.addEventListener('message', resp => { - $.get('/api/fedmsg/markup', { - message: resp.data, - plugin: 'feed', - }).done(data => { - if(!$.isEmptyObject(data)) { - this.state.matches.push.apply(this.state.matches, data); - this.setState({matches: this.state.matches}); - } - }); - }, false); + this.source.onmessage = resp => { + const data = JSON.parse(resp.data); + this.state.matches.push(data); + this.setState({matches: this.state.matches}); + }; } render() { const feedNodes = this.state.matches.map(match => { - return + return }); return (
    @@ -61,7 +54,6 @@ export default class Feed extends React.Component { } } - window.Feed = Feed; window.React = React; window.reactRender = render; diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html index 3b01664..4874fb0 100644 --- a/hubs/widgets/templates/feed.html +++ b/hubs/widgets/templates/feed.html @@ -6,9 +6,9 @@ From e3bfeac5cc07fe9ff3199547b46053e383c38eed Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 3/11] Add class properties req and new data to top --- diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx index da00b25..7498425 100644 --- a/hubs/static/client/app/components/Feed.jsx +++ b/hubs/static/client/app/components/Feed.jsx @@ -38,7 +38,7 @@ export default class Feed extends React.Component { } this.source.onmessage = resp => { const data = JSON.parse(resp.data); - this.state.matches.push(data); + this.state.matches.unshift(data); this.setState({matches: this.state.matches}); }; } diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json index abb023a..26274f1 100644 --- a/hubs/static/client/package.json +++ b/hubs/static/client/package.json @@ -9,6 +9,7 @@ "dependencies": { "babel-core": "~6.9.1", "babel-loader": "~6.2.4", + "babel-plugin-transform-class-properties": "^6.10.2", "babel-preset-es2015": "~6.9.0", "babel-preset-react": "~6.5.0", "react": "~15.1.0", From 8e39a4b4856f06271baee4068b4eddff14d85ce5 Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 4/11] Change feed widget to card format * Per discussion on #fedora-hubs, implement message limiting * Per same discussion format the feed to look like other widgets in card format * Update README to be explicit about what to do after building JS asseets h/t skrzepto --- diff --git a/README.rst b/README.rst index 499e7e3..095bc30 100644 --- a/README.rst +++ b/README.rst @@ -210,7 +210,11 @@ Build JavaScript assets. Add ``--w`` for live reloading:: $ cd hubs/static/client && node_modules/.bin/webpack **Lastly**, (fingers crossed) start up the fedora-hubs webapp and load your -profile page. Once there are some messages that get into your local database +profile page. Change back to the project root and run:: + + $ python runserver.py + +Once there are some messages that get into your local database that *should* show up on your feed.. they should appear there. (At very least, you shouldn't get an error message about that widget being unable to be displayed). diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx index 7498425..55ddfdb 100644 --- a/hubs/static/client/app/components/Feed.jsx +++ b/hubs/static/client/app/components/Feed.jsx @@ -8,6 +8,7 @@ import Icon from './Icon.jsx'; import Markup from './Markup.jsx'; import {Message, MessageList} from './Messages.jsx'; +const FEED_MESSAGE_LIMIT = 10; class Panel extends React.Component { render() { @@ -38,6 +39,9 @@ export default class Feed extends React.Component { } this.source.onmessage = resp => { const data = JSON.parse(resp.data); + if(this.state.matches.length >= FEED_MESSAGE_LIMIT) { + this.state.matches.pop() + } this.state.matches.unshift(data); this.setState({matches: this.state.matches}); }; @@ -47,9 +51,14 @@ export default class Feed extends React.Component { return }); return ( -
    +
    +
    + Live Feed +
    +
    {feedNodes}
    +
    ) } } From 2b5d0c2dbacbec195f4058059fdb3faa09bfd873 Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 5/11] JSX style rules from Airbnb style guide --- diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx index 55ddfdb..1f208aa 100644 --- a/hubs/static/client/app/components/Feed.jsx +++ b/hubs/static/client/app/components/Feed.jsx @@ -1,65 +1,48 @@ import React from 'react'; -import {render} from 'react-dom'; +import { render } from 'react-dom'; +import Panel from './Panel.jsx'; -import Constituent from './Constituent.jsx'; -import ExpandCollapse from './ExpandCollapse.jsx'; -import Icon from './Icon.jsx'; -import Markup from './Markup.jsx'; -import {Message, MessageList} from './Messages.jsx'; - -const FEED_MESSAGE_LIMIT = 10; - -class Panel extends React.Component { - render() { - return ( -
    -
    - - -
    -
    - ); - } -} +const FEED_MESSAGE_LIMIT = 20; export default class Feed extends React.Component { - constructor (props) { + constructor(props) { super(props); this.state = { matches: this.props.matches, + messageLimit: FEED_MESSAGE_LIMIT, sse: true, - } + }; this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {}; this.source.addEventListener('error', () => { this.state.sse = false; }, false); window.onbeforeunload = () => { this.source.close(); - } + }; this.source.onmessage = resp => { const data = JSON.parse(resp.data); - if(this.state.matches.length >= FEED_MESSAGE_LIMIT) { - this.state.matches.pop() + if (this.state.matches.length >= this.state.messageLimit) { + this.state.matches.pop(); } this.state.matches.unshift(data); - this.setState({matches: this.state.matches}); + this.setState({ matches: this.state.matches }); }; } render() { const feedNodes = this.state.matches.map(match => { - return + return ; }); return (
    Live Feed
    -
    - {feedNodes} -
    +
    + {feedNodes} +
    - ) + ); } } diff --git a/hubs/static/client/app/components/Icon.jsx b/hubs/static/client/app/components/Icon.jsx index 70c2d8c..08c8d60 100644 --- a/hubs/static/client/app/components/Icon.jsx +++ b/hubs/static/client/app/components/Icon.jsx @@ -1,15 +1,19 @@ import React from 'react'; -export default class Icon extends React.Component { - render() { - return ( -
    -
    - - - -
    +const Icon = function (props) { + return ( +
    + - ) - } -} +
    + ); +}; + +export default Icon; diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx index 952fe06..768a81b 100644 --- a/hubs/static/client/app/components/Markup.jsx +++ b/hubs/static/client/app/components/Markup.jsx @@ -2,16 +2,19 @@ import React from 'react'; export default class Markup extends React.Component { createMarkup() { - return {__html: this.props.match['markup']}; + return { __html: this.props.match.markup }; } render() { + debugger; return (
    -

    - {this.props.match['human_time']} +

    +

    + {this.props.match.human_time}
    - ) + ); } } diff --git a/hubs/static/client/app/components/Panel.jsx b/hubs/static/client/app/components/Panel.jsx new file mode 100644 index 0000000..61e8242 --- /dev/null +++ b/hubs/static/client/app/components/Panel.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import Icon from './Icon.jsx'; +import Markup from './Markup.jsx'; + +const Panel = function (props) { + return ( +
    +
    + + +
    +
    + ); +}; + +export default Panel; diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json index 26274f1..045880c 100644 --- a/hubs/static/client/package.json +++ b/hubs/static/client/package.json @@ -12,17 +12,18 @@ "babel-plugin-transform-class-properties": "^6.10.2", "babel-preset-es2015": "~6.9.0", "babel-preset-react": "~6.5.0", + "moment": "^2.14.1", "react": "~15.1.0", "react-dom": "~15.1.0", "reactify": "~1.1.1", "webpack": "~1.13.1" }, "devDependencies": { - "eslint-config-airbnb": "~9.0.1", - "eslint-plugin-react": "~5.2.1", - "eslint-plugin-jsx-a11y": "~1.5.3", - "eslint-plugin-import": "~1.8.1", - "eslint": "~2.12.0" + "eslint": "^2.13.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.10.2", + "eslint-plugin-jsx-a11y": "^1.5.5", + "eslint-plugin-react": "^5.2.2" }, "scripts": { "dev": "webpack --watch", From 3beee9df2c297fffb3a03cd1da6e2c380ef6d4c7 Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 6/11] Make feed limit configurable --- diff --git a/hubs/static/client/app/components/Feed.jsx b/hubs/static/client/app/components/Feed.jsx index 1f208aa..7d9c618 100644 --- a/hubs/static/client/app/components/Feed.jsx +++ b/hubs/static/client/app/components/Feed.jsx @@ -3,14 +3,12 @@ import { render } from 'react-dom'; import Panel from './Panel.jsx'; -const FEED_MESSAGE_LIMIT = 20; - export default class Feed extends React.Component { constructor(props) { super(props); this.state = { matches: this.props.matches, - messageLimit: FEED_MESSAGE_LIMIT, + messageLimit: this.props.messageLimit, sse: true, }; this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {}; @@ -30,17 +28,12 @@ export default class Feed extends React.Component { }; } render() { - const feedNodes = this.state.matches.map(match => { - return ; + const feedNodes = this.state.matches.map((match, idx) => { + return ; }); return ( -
    -
    - Live Feed -
    -
    - {feedNodes} -
    +
    + {feedNodes}
    ); } diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx index 768a81b..5023498 100644 --- a/hubs/static/client/app/components/Markup.jsx +++ b/hubs/static/client/app/components/Markup.jsx @@ -5,7 +5,6 @@ export default class Markup extends React.Component { return { __html: this.props.match.markup }; } render() { - debugger; return (

    Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 7/11] Fix human readable time, clean up JS/JSX --- diff --git a/.gitignore b/.gitignore index 7787cd3..74ff6cc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ scratch/ npm-debug.log +client_secrets.json \ No newline at end of file diff --git a/hubs/static/client/.eslintrc.json b/hubs/static/client/.eslintrc.json new file mode 100644 index 0000000..079ce96 --- /dev/null +++ b/hubs/static/client/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "extends": "airbnb", + "rules": { + "react/prop-types": "off", + "arrow-body-style": "off", + "func-names": "off" + } +} diff --git a/hubs/static/client/app/components/Constituent.jsx b/hubs/static/client/app/components/Constituent.jsx deleted file mode 100644 index 3691755..0000000 --- a/hubs/static/client/app/components/Constituent.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; - -export default class Constituent extends React.Component { - render() { - const match = this.props.match; - const hasConstituent = (match['msg_ids'].length === 1 && Object.keys(match['msg_ids'])[0]['long_form'] !== Object.keys(match['msg_ids'])[0]['subtitle']); - if ( hasConstituent ) { - const constituentKey = Object.keys(match['msg_ids'])[0]; - const constituent = match['msg_ids'][constituentKey]; - const differentIcon = constituent['icon'] != constituent['__icon__']; - const longLink = ( constituent['link'] && constituent['long_form'].indexOf(constituent['link']) === -1 ); - - let constituentIcon; - if (differentIcon) - constituentIcon = ( -
    - - {constituent['subtitle']}/ - -
    - - ) - - let readMore; - if(longLink) - readMore = ( -
    - {constituent['long_form']} - - Read more - -
    - ) - - return ( -
    - - {constituentIcon} - {readMore} -
    - ) - } else if (match['msg_ids'].length > 1) { -
    - - -
    - } - else { - return null - } - } -} diff --git a/hubs/static/client/app/components/ExpandCollapse.jsx b/hubs/static/client/app/components/ExpandCollapse.jsx deleted file mode 100644 index 29b1e6f..0000000 --- a/hubs/static/client/app/components/ExpandCollapse.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -export default class ExpandCollapse extends React.Component { - render() { - return ( - - ) - } -} diff --git a/hubs/static/client/app/components/Markup.jsx b/hubs/static/client/app/components/Markup.jsx index 5023498..66f4da4 100644 --- a/hubs/static/client/app/components/Markup.jsx +++ b/hubs/static/client/app/components/Markup.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import TimeAgo from 'react-timeago'; export default class Markup extends React.Component { createMarkup() { @@ -12,7 +13,7 @@ export default class Markup extends React.Component { dangerouslySetInnerHTML={this.createMarkup()} >

    - {this.props.match.human_time} +
    ); } diff --git a/hubs/static/client/app/components/Messages.jsx b/hubs/static/client/app/components/Messages.jsx deleted file mode 100644 index a773b52..0000000 --- a/hubs/static/client/app/components/Messages.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -class Message extends React.Component { - render() { - if(this.props.constituent['link']) { - return ( -
  • - {this.props.constituent['markup']} - - - -
  • - ) - } else { - return (
  • { this.props.constituent['markup'] }
  • ) - } - } -} - -class MessageList extends React.Component { - render() { - const messageNodes = this.props.items.map(function(item) { - return ; - }); - return ( -
      - {messageNodes} -
    - ) - } -} - -module.exports = { - Message: Message, - MessageList: MessageList -} diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx index a753c36..b12429e 100644 --- a/hubs/static/client/app/index.jsx +++ b/hubs/static/client/app/index.jsx @@ -1,4 +1,4 @@ import React from 'react'; import { render } from 'react-dom'; -import Feed from './components/Feed.jsx' +import Feed from './components/Feed.jsx'; diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json index 045880c..f717e0d 100644 --- a/hubs/static/client/package.json +++ b/hubs/static/client/package.json @@ -7,29 +7,24 @@ "doc": "docs" }, "dependencies": { - "babel-core": "~6.9.1", "babel-loader": "~6.2.4", "babel-plugin-transform-class-properties": "^6.10.2", "babel-preset-es2015": "~6.9.0", "babel-preset-react": "~6.5.0", - "moment": "^2.14.1", "react": "~15.1.0", "react-dom": "~15.1.0", - "reactify": "~1.1.1", + "react-timeago": "^3.1.1", "webpack": "~1.13.1" }, "devDependencies": { "eslint": "^2.13.1", - "eslint-config-airbnb": "^9.0.1", - "eslint-plugin-import": "^1.10.2", - "eslint-plugin-jsx-a11y": "^1.5.5", - "eslint-plugin-react": "^5.2.2" + "depcheck": "~0.6.3" }, "scripts": { "dev": "webpack --watch", "build": "webpack -p", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "eslint app/* && depcheck ." }, "author": "", - "license": "BSD" + "license": "AGPL-3.0" } diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html index 4874fb0..9973c29 100644 --- a/hubs/widgets/templates/feed.html +++ b/hubs/widgets/templates/feed.html @@ -7,7 +7,8 @@ (function() { const FeedElement = React.createElement(Feed, { matches: {{ matches }}, - url: ':8080/user/skrzepto' + url: 'localhost:8080/user/skrzepto', + messageLimit: {{ message_limit }} }); reactRender(FeedElement, document.getElementById('feed')); })(); From 791b715958004f08f03dbabc82629b370eda9f4a Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 8/11] Use application config to set the SSE url * Use app.config to set SSE url * Pin min version for flask-oidc * Readme instructions for SSE url --- diff --git a/.gitignore b/.gitignore index 74ff6cc..096613a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ build/ scratch/ npm-debug.log -client_secrets.json \ No newline at end of file +client_secrets.json +hubs.js diff --git a/README.rst b/README.rst index 095bc30..531a78a 100644 --- a/README.rst +++ b/README.rst @@ -205,14 +205,15 @@ will just grow and grow over time:: [2015-07-01 14:33:21][ fedmsg INFO] copr has 6 entries [2015-07-01 14:33:21][ fedmsg INFO] askbot has 2 entries -Build JavaScript assets. Add ``--w`` for live reloading:: +Change the ``SSE_URL`` location in ``default_config.py`` to point to the +streaming server then build JavaScript assets. Add ``--w`` for live reloading:: $ cd hubs/static/client && node_modules/.bin/webpack **Lastly**, (fingers crossed) start up the fedora-hubs webapp and load your profile page. Change back to the project root and run:: - $ python runserver.py + $ python runserver.py -c config Once there are some messages that get into your local database that *should* show up on your feed.. they should appear there. (At very least, diff --git a/hubs/default_config.py b/hubs/default_config.py index 0ce12f8..4125923 100755 --- a/hubs/default_config.py +++ b/hubs/default_config.py @@ -9,6 +9,7 @@ PROMOTED_GROUPS = [ HUB_OF_THE_MONTH = 'commops' +SSE_URL = 'http://localhost:8080/user/' OIDC_CLIENT_SECRETS = os.path.join(os.path.dirname( os.path.abspath(__file__)), '..', 'client_secrets.json') diff --git a/hubs/defaults.py b/hubs/defaults.py index 4b2049d..ddd04db 100755 --- a/hubs/defaults.py +++ b/hubs/defaults.py @@ -11,7 +11,8 @@ def add_user_widgets(session, hub, username, fullname): plugin='feed', index=0, left=True, _config=json.dumps({ 'username': username, - 'fmn_context': 'irc', # TODO -- make this 'hubs' + 'fmn_context': 'irc', # TODO -- make this 'hubs', + 'message_limit': 20 })) hub.widgets.append(widget) diff --git a/hubs/widgets/feed.py b/hubs/widgets/feed.py index f6cf651..42b3cb5 100755 --- a/hubs/widgets/feed.py +++ b/hubs/widgets/feed.py @@ -92,6 +92,9 @@ class PythonObjectEncoder(json.JSONEncoder): def data(session, widget, username, fmn_context, message_limit): messages = [] matches = [] + # Avoid circular import + from hubs.app import app + feed_url = app.config['SSE_URL'] + username preference = get_remote_preference(username, fmn_context) if preference: try: @@ -114,7 +117,7 @@ def data(session, widget, username, fmn_context, message_limit): total, pages, rows = datanommer.models.Message.grep( start=end - delta, end=end, - rows_per_page=bazillion, + rows_per_page=message_limit, page=page, order='desc', **fmn_hinting @@ -154,7 +157,11 @@ def data(session, widget, username, fmn_context, message_limit): # And tack on a unique identifier for each top level entry. match['dom_id'] = six.text_type(uuid.uuid4()) matches = json.dumps(matches, cls=PythonObjectEncoder) - return dict(matches=matches, message_limit=message_limit) + return dict( + matches=matches, + message_limit=message_limit, + feed_url=feed_url + ) @hint(ubiquitous=True) diff --git a/hubs/widgets/templates/feed.html b/hubs/widgets/templates/feed.html index 9973c29..755bec9 100644 --- a/hubs/widgets/templates/feed.html +++ b/hubs/widgets/templates/feed.html @@ -7,7 +7,7 @@ (function() { const FeedElement = React.createElement(Feed, { matches: {{ matches }}, - url: 'localhost:8080/user/skrzepto', + url: '{{ feed_url }}', messageLimit: {{ message_limit }} }); reactRender(FeedElement, document.getElementById('feed')); diff --git a/requirements.txt b/requirements.txt index 883cb24..01e9cc9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ dogpile.cache fedmsg fedmsg_meta_fedora_infrastructure flask -flask-oidc +flask-oidc>=1.0.3 fmn.lib fmn.rules gunicorn From d89312eb0e4eb5999a58f722dd640da4efe5f061 Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 14:40:39 +0000 Subject: [PATCH 9/11] Remove 'irc' feed context, add package.json author --- diff --git a/hubs/defaults.py b/hubs/defaults.py index ddd04db..706bb03 100755 --- a/hubs/defaults.py +++ b/hubs/defaults.py @@ -11,7 +11,6 @@ def add_user_widgets(session, hub, username, fullname): plugin='feed', index=0, left=True, _config=json.dumps({ 'username': username, - 'fmn_context': 'irc', # TODO -- make this 'hubs', 'message_limit': 20 })) hub.widgets.append(widget) diff --git a/hubs/static/client/package.json b/hubs/static/client/package.json index f717e0d..235be28 100644 --- a/hubs/static/client/package.json +++ b/hubs/static/client/package.json @@ -25,6 +25,6 @@ "build": "webpack -p", "test": "eslint app/* && depcheck ." }, - "author": "", + "author": "Eric Barbour ", "license": "AGPL-3.0" } diff --git a/hubs/widgets/feed.py b/hubs/widgets/feed.py index 42b3cb5..d3870c3 100755 --- a/hubs/widgets/feed.py +++ b/hubs/widgets/feed.py @@ -81,21 +81,17 @@ class PythonObjectEncoder(json.JSONEncoder): default=None, validator=validators.username, help="A FAS username.") -@argument(name="fmn_context", - default="irc", # TODO - Make this 'hubs', or... - validator=validators.fmn_context, - help="A FMN context.") @argument(name="message_limit", default=20, validator=validators.integer, help="Max number of feed messages to display") -def data(session, widget, username, fmn_context, message_limit): +def data(session, widget, username, message_limit): messages = [] matches = [] # Avoid circular import from hubs.app import app feed_url = app.config['SSE_URL'] + username - preference = get_remote_preference(username, fmn_context) + preference = get_remote_preference(username, 'sse') if preference: try: preference = rehydrate_preference(preference) @@ -167,7 +163,7 @@ def data(session, widget, username, fmn_context, message_limit): @hint(ubiquitous=True) def should_invalidate(message, session, widget): username = widget.config['username'] - fmn_context = widget.config['fmn_context'] + fmn_context = 'sse' preference = get_remote_preference(username, fmn_context) if not preference: return False From 7866df8ec4033b9919e201a7a73303925b5ffc38 Mon Sep 17 00:00:00 2001 From: Eric Barbour Date: Jul 14 2016 17:09:01 +0000 Subject: [PATCH 10/11] Fix tests fmn_context change + fedmsg api removal --- diff --git a/hubs/tests/test_api/test_fedmsg.py b/hubs/tests/test_api/test_fedmsg.py deleted file mode 100644 index 332d4d1..0000000 --- a/hubs/tests/test_api/test_fedmsg.py +++ /dev/null @@ -1,80 +0,0 @@ -import json -import os - -import hubs.tests -import hubs.models -from hubs.app import app -from hubs.tests import json_path - - -class TestFeed(hubs.tests.APPTest): - - with open(json_path + 'message_involved.json', 'r') as fp: - message_involved = json.loads(fp.read()) - - with open(json_path + 'message_not_involved.json', 'r') as fp: - message_not_involved = json.loads(fp.read()) - - # TODO this test relies on a specific filter for the atelic user - # It would be better to fake preferences on FAS - user = hubs.tests.FakeAuthorization('atelic') - - plugin = 'feed' - - def test_returns_400_when_no_message(self): - payload = {'plugin': 'feed'} - response = self.app.get('/api/fedmsg/markup', query_string=payload) - self.assertEqual(response.status_code, 400) - - def test_returns_400_when_no_plugin(self): - payload = {'message': json.dumps(self.message_involved)} - response = self.app.get('/api/fedmsg/markup', query_string=payload) - self.assertEqual(response.status_code, 400) - - def test_returns_400_when_bad_plugin_name(self): - payload = { - 'message': json.dumps(self.message_involved), - 'plugin': 'notarealplugin' - } - response = self.app.get('/api/fedmsg/markup', query_string=payload) - self.assertEqual(response.status_code, 400) - - def test_returns_403_when_not_logged_in(self): - payload = { - 'message': json.dumps(self.message_involved), - 'plugin': 'feed' - } - - response = self.app.get('/api/fedmsg/markup', query_string=payload) - self.assertEqual(response.status_code, 403) - - def test_returns_match_if_involved(self): - payload = { - 'message': json.dumps(self.message_involved), - 'plugin': 'feed' - } - with self.app.session_transaction() as sess: - sess['nickname'] = 'atelic' - - with hubs.tests.auth_set(app, self.user): - response = self.app.get('/api/fedmsg/markup', query_string=payload) - - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - self.assertTrue(data) - self.assertTrue(isinstance(data[0], dict)) - - def test_returns_no_match_if_not_involved(self): - payload = { - 'message': json.dumps(self.message_not_involved), - 'plugin': 'feed' - } - with self.app.session_transaction() as sess: - sess['nickname'] = 'atelic' - - with hubs.tests.auth_set(app, self.user): - response = self.app.get('/api/fedmsg/markup', query_string=payload) - - self.assertEqual(response.status_code, 200) - data = json.loads(response.data) - self.assertFalse(data) diff --git a/hubs/tests/test_fedora_hubs_flask_api.py b/hubs/tests/test_fedora_hubs_flask_api.py index 254b40e..7b7dbff 100644 --- a/hubs/tests/test_fedora_hubs_flask_api.py +++ b/hubs/tests/test_fedora_hubs_flask_api.py @@ -268,7 +268,7 @@ class HubsAPITest(hubs.tests.APPTest): with tests.auth_set(app, user): result = self.app.get('/ralph/31/edit', follow_redirects=True) self.assertEqual(result.status_code, 200) - expected_str = ' Date: Jul 14 2016 20:02:43 +0000 Subject: [PATCH 11/11] Explain seemingly unused import in index.jsx --- diff --git a/hubs/static/client/app/index.jsx b/hubs/static/client/app/index.jsx index b12429e..7641684 100644 --- a/hubs/static/client/app/index.jsx +++ b/hubs/static/client/app/index.jsx @@ -1,4 +1,5 @@ -import React from 'react'; -import { render } from 'react-dom'; - -import Feed from './components/Feed.jsx'; +/* This import is needed even though it's not used + * since the feed component places itself on window. + * The feed will not work without it. + */ +import Feed from './components/Feed.jsx'; // eslint-disable-line