#200 Implement Feed Widget and live update in React
Merged 7 years ago by atelic. Opened 7 years ago by atelic.
atelic/fedora-hubs feature/add-react  into  develop

Implements feed widget in Reactjs
Eric Barbour • 7 years ago  
file modified
+2
@@ -5,3 +5,5 @@ 

  node*

  build/

  scratch/

+ 

+ npm-debug.log

file modified
+9 -1
@@ -125,7 +125,11 @@ 

  

  Start with some required packages::

  

-     $ sudo dnf install postgresql-server python-datanommer-consumer datanommer-commands fedmsg-hub

+     $ sudo dnf install postgresql-server python-datanommer-consumer datanommer-commands fedmsg-hub npm

+ 

+ The Feed Widget runs on React so install those dependencies too::

+ 

+     $ cd hubs/static/client && npm install

  

  And there are some support libraries you'll also need::

  
@@ -184,6 +188,10 @@ 

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

+ 

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

  that *should* show up on your feed.. they should appear there.  (At very least,

@@ -0,0 +1,134 @@ 

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

file modified
+15 -1
@@ -14,6 +14,7 @@ 

  

  from flask.ext.openid import OpenID

  

+ import fmn.lib

  import hubs.models

  import hubs.widgets

  
@@ -665,17 +666,27 @@ 

          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

+     except AttributeError:      # Not logged in

          return flask.abort(403)

+ 

      preference = get_remote_preference(nickname, context)

+ 

      if preference:

          try:

              preference = rehydrate_preference(preference)
@@ -686,6 +697,7 @@ 

      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():
@@ -700,6 +712,8 @@ 

                  else:

                      markup = u"<h5>{long_form}</h5>".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)

@@ -0,0 +1,5 @@ 

+ {

+   "presets" : ["es2015", "react"],

+   "plugins": ["transform-class-properties"]

+ }

+ 

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

+ 

+             let constituentIcon;

+             if (differentIcon)

+                 constituentIcon = (

+                     <div className="media-left media-top">

+                         <a href={constituent['link']}>

+                             <img className="media-object" src={constituent['icon']} alt={constituent['subtitle']}/>

+                         </a>

+                     </div>

+ 

+                 )

+ 

+             let readMore;

+             if(longLink)

+                 readMore = (

+                     <div className="media-body">

+                         {constituent['long_form']}

+                         <a href={constituent['link']} target="_blank">

+                             Read more <span class="glyphicon glyphicon-new-window"></span>

+                         </a>

+                     </div>

+                 )

+ 

+             return (

+                 <div id="content-{{match['dom_id']}}" className="media">

+                     <ExpandCollapse match={this.props.match} />

+                     {constituentIcon}

+                     {readMore}

+                 </div>

+             )

+         } else if (match['msg_ids'].length > 1) {

+             <div id="content-{{match['dom_id']}}" className="media">

+                 <ExpandCollapse match={this.props.match} />

+                 <MessageList msgs={this.match['msg_ids']} />

+             </div>

+         }

+         else {

+             return null

+         }

+     }

+ }

@@ -0,0 +1,18 @@ 

+ import React from 'react';

+ 

+ export default class ExpandCollapse extends React.Component {

+     render() {

+         return (

+             <div className="pull-right">

+                 <a id="expand-{this.props.match['dom_id']}" href="javascript:expand_feed_entries({this.props.match['dom_id']});" className="hidden">

+                     <span className="glyphicon glyphicon-chevron-down"></span>

+                     expand

+                 </a>

+                 <a id="collapse-{this.props.match['dom_id']}" href="javascript:collapse_feed_entries('{this.props.match['dom_id']}');">

+                     <span className="glyphicon glyphicon-chevron-up"></span>

+                     collapse

+                 </a>

+             </div>

+         )

+     }

+ }

@@ -0,0 +1,67 @@ 

+ import React from 'react';

+ import {render} from 'react-dom';

+ 

+ 

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

+ 

+ 

+ class Panel extends React.Component {

+     render() {

+         return (

+             <div className="panel panel-default">

+                 <div className="panel-body">

+                     <Icon match={this.props.match} />

+                     <Markup match={this.props.match} />

+                     <Constituent match={this.props.match} />

+                 </div>

+             </div>

+         );

+     }

+ }

+ 

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

+     }

+     render() {

+         const feedNodes = this.state.matches.map(match => {

+             return <Panel match={match}/>

+         });

+         return (

+             <div>

+                 {feedNodes}

+             </div>

+         )

+     }

+ }

+ 

+ 

+ window.Feed = Feed;

+ window.React = React;

+ window.reactRender = render;

@@ -0,0 +1,15 @@ 

+ import React from 'react';

+ 

+ export default class Icon extends React.Component {

+     render() {

+         return (

+             <div>

+                 <div className="media-left">

+                     <a href={this.props.match['link'] ? this.props.match['link'] : '#'} target="_blank">

+                         <img className="media-object square-32 img-circle" src={this.props.match['secondary_icon']} />

+                     </a>

+                 </div>

+             </div>

+         )

+     }

+ }

@@ -0,0 +1,17 @@ 

+ import React from 'react';

+ 

+ export default class Markup extends React.Component {

+     createMarkup() {

+         return {__html: this.props.match['markup']};

+     }

+     render() {

+         return (

+             <div className="media-body">

+                 <h4 className="media-heading"

+                     dangerouslySetInnerHTML={this.createMarkup()}

+                 ></h4>

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

+             </div>

+         )

+     }

+ }

@@ -0,0 +1,36 @@ 

+ import React from 'react';

+ 

+ class Message extends React.Component {

+     render() {

+         if(this.props.constituent['link']) {

+             return (

+                 <li>

+                     {this.props.constituent['markup']}

+                     <a href={this.props.constituent['link']} target="_blank">

+                         <span className="glyphicon glyphicon-new-window"></span>

+                     </a>

+                 </li>

+             )

+         } else {

+             return (<li>{ this.props.constituent['markup'] }</li>)

+         }

+     }

+ }

+ 

+ class MessageList extends React.Component {

+     render() {

+         const messageNodes =  this.props.items.map(function(item) {

+             return <Message constituent={item}/>;

+         });

+         return (

+             <ul id="content-{this.props.match['dom_id']}" className="hidden list-unstyled">

+                 {messageNodes}

+             </ul>

+         )

+     }

+ }

+ 

+ module.exports = {

+     Message: Message,

+     MessageList: MessageList

+ }

@@ -0,0 +1,10 @@ 

+ import React from 'react';

+ import { render } from 'react-dom';

+ 

+ import Feed from './components/Feed.jsx'

+ 

+ 

+ render(

+     <Feed matches={[]} />,

+     document.getElementById('feed')

+ );

@@ -0,0 +1,33 @@ 

+ {

+   "name": "fedora-hubs",

+   "version": "0.0.0",

+   "description": "Fedora Hubs will provide a communication and collaboration center for Fedora contributors of all types. The idea is that contributors will be able to visit Hubs to check on their involvements across Fedora, discover new places that they can contribute, and more.",

+   "main": "webpack.config.js",

+   "directories": {

+     "doc": "docs"

+   },

+   "dependencies": {

+     "babel-core": "~6.9.1",

+     "babel-loader": "~6.2.4",

+     "babel-preset-es2015": "~6.9.0",

+     "babel-preset-react": "~6.5.0",

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

+   },

+   "scripts": {

+     "dev": "webpack --watch",

+     "build": "webpack -p",

+     "test": "echo \"Error: no test specified\" && exit 1"

+   },

+   "author": "",

+   "license": "BSD"

+ }

@@ -0,0 +1,27 @@ 

+ const webpack = require('webpack');

+ const path = require('path');

+ 

+ const PATHS = {

+     app: path.join(__dirname, 'app'),

+     build: path.join(__dirname, '../js')

+ };

+ 

+ const config = {

+     entry: {

+         app: path.join(PATHS.app, 'index.jsx')

:thumbsup:

+     },

+     output: {

+         path: PATHS.build,

+         filename: 'hubs.js'

+     },

+     module : {

+         loaders : [{

+             test : /\.jsx?$/,

+             loader : 'babel',

+             exclude: /(node_modules|bowercomponents)/,

+             include : PATHS.app

+         }]

+     }

+ };

+ 

+ module.exports = config;

@@ -0,0 +1,74 @@ 

+ import json

+ 

+ import hubs.tests.test_widgets

+ from hubs.widgets.feed import PythonObjectEncoder

+ 

+ 

+ encode = lambda o: json.dumps(o, cls=PythonObjectEncoder)

+ 

+ class TestPythonObjectEncoder(hubs.tests.test_widgets.WidgetTest):

+     def test_python_object_encoder_encodes_set(self):

+         data = set([1,2,3, 'bar', 'foo'])

+         expected_str = '[1, 2, 3, "bar", "foo"]'

+         expected_list = list(data)

+ 

+         encoded = encode(data)

+         self.assertEqual(encoded, expected_str)

+ 

+         decoded = json.loads(encoded)

+         self.assertEqual(expected_list, decoded)

+ 

+     def test_python_object_encoder_encodes_tuple(self):

+         data = ('foo', 'bar', 'baz', 1)

+         expected_str = '["foo", "bar", "baz", 1]'

+         expected_list = list(data)

+ 

+         encoded = encode(data)

+         self.assertEqual(encoded, expected_str)

+ 

+         decoded = json.loads(encoded)

+         self.assertEqual(expected_list, decoded)

+ 

+ 

+     def test_python_object_encoder_encodes_object(self):

+         class Test(object):

+             def __init__(self):

+                 self.x = 'x'

+ 

+         data = Test()

+         expected_str = '{"x": "x"}'

+         expected_dict = data.__dict__

+         encoded = encode(data)

+         self.assertEqual(encoded, expected_str)

+ 

+         decoded = json.loads(encoded)

+         self.assertEqual(expected_dict, decoded)

+ 

+     def test_python_object_encoder_encodes_deep_dict(self):

+         data = {

+             'some_dict': {

+                 'one': 1,

+                 'aset': set(['two', 'three']),

+                 'four': 4.0,

+                 'tupe': (5,6),

+             },

+             'regular': 'string'

+         }

+         expected_str = '{"some_dict": {"four": 4.0, "tupe": [5, 6], "aset": ["two", "three"], "one": 1}, "regular": "string"}'

+ 

+         expected_dict = {

+             'some_dict': {

+                 'one': 1,

+                 'aset': ['two', 'three'],

+                 'four': 4.0,

+                 'tupe': [5,6],

+             },

+             'regular': 'string'

+         }

+ 

+         encoded = encode(data)

+         self.assertEqual(expected_str, encoded)

+ 

+         decoded = json.loads(encoded)

+         self.assertEqual(expected_dict, decoded)

+ 

file modified
+22 -10
@@ -21,6 +21,7 @@ 

  import hubs.validators as validators

  

  import datetime

+ import json

  import logging

  import uuid

  
@@ -30,13 +31,12 @@ 

  config = fedmsg.config.load_config()

  fmn_url = config['fmn.url']

  

- 

  bazillion = 1000

  paths = fmn.lib.load_rules(root='fmn.rules')

  

  # No chrome around the feed.

- #from hubs.widgets.chrome import panel

- #chrome = panel()

+ # from hubs.widgets.chrome import panel

+ # chrome = panel()

  template = templating.environment.get_template('templates/feed.html')

  position = 'left'

  
@@ -50,7 +50,7 @@ 

          ))

          markup = markup.replace(*args)

          # TODO -- have to add these to conglomerate first

-         #match['long_form'] = match['long_form'].replace(*args)

+         # match['long_form'] = match['long_form'].replace(*args)

  

      for package in match['packages']:

          args = (package, '<a href="{url}">{package}</a>'.format(
@@ -59,11 +59,25 @@ 

          ))

          markup = markup.replace(*args)

          # TODO -- have to add these to conglomerate first

-         #match['long_form'] = match['long_form'].replace(*args)

+         # match['long_form'] = match['long_form'].replace(*args)

  

      return markup

  

  

+ class PythonObjectEncoder(json.JSONEncoder):

+     ''' A JSON encoder that handles python data structures in encoding'''

+     def default(self, obj):

+         if isinstance(obj, (list, dict, str, unicode, int, float, bool,

+                             type(None))):

+             return json.JSONEncoder.default(self, obj)

+         if isinstance(obj, set) or isinstance(obj, tuple):

+             return list(obj)

+         if isinstance(obj, object):

+             return obj.__dict__

+         else:

+             raise TypeError

+ 

+ 

  @argument(name="username",

            default=None,

            validator=validators.username,
@@ -95,7 +109,7 @@ 

              rules = filter['rules']

              fmn_hinting = fmn.lib.hinting.gather_hinting(config, rules, paths)

              total, pages, rows = datanommer.models.Message.grep(

-                 start=end-delta,

+                 start=end - delta,

                  end=end,

                  rows_per_page=bazillion,

                  page=page,
@@ -136,10 +150,8 @@ 

  

          # And tack on a unique identifier for each top level entry.

          match['dom_id'] = six.text_type(uuid.uuid4())

- 

-     return dict(

-         matches=matches,

-     )

+     matches = json.dumps(matches, cls=PythonObjectEncoder)

+     return dict(matches=matches)

  

  

  @hint(ubiquitous=True)

@@ -1,74 +1,15 @@ 

- {% for match in matches %}

- <div class="panel panel-default">

-   <div class="panel-body">

-     <div class="media">

-       <div class="media-left">

-         <a href="{{match['link']}}" target="_blank">

-           <img class="media-object square-32 img-circle" src="{{match['secondary_icon']}}">

-         </a>

-       </div>

-       <div class="media-body">

-         <h4 class="media-heading">{{match['markup']}}</h4>

-         {{match['human_time']}}

+ <div id="feed">

+ </div>

  

+ <script type="text/javascript"

+         src ="{{url_for('static', filename='js/hubs.js')}}"></script>

+ <script>

+  (function() {

+      const FeedElement = React.createElement(Feed, {

+          matches: {{ matches }}

+      });

  

-         {% if match['msg_ids'] | length == 1 and match['msg_ids'].values()[0].get('long_form') != match['msg_ids'].values()[0]['subtitle'] %}

-             {% set constituent = match['msg_ids'].values()[0] %}

-             <div class="pull-right">

-                 <a id="expand-{{match['dom_id']}}" href="javascript:expand_feed_entries('{{match['dom_id']}}');" class="hidden">

-                     <span class="glyphicon glyphicon-chevron-down"></span>

-                     expand

-                 </a>

-                 <a id="collapse-{{match['dom_id']}}" href="javascript:collapse_feed_entries('{{match['dom_id']}}');">

-                     <span class="glyphicon glyphicon-chevron-up"></span>

-                     collapse

-                 </a>

-             </div>

+      reactRender(FeedElement, document.getElementById('feed'));

+  })();

  

-             <div id="content-{{match['dom_id']}}" class="media">

-               {% if constituent['icon'] != constituent.get('__icon__') %}

-               <div class="media-left media-top">

-                 <a href="{{constituent['link']}}">

-                   <img class="media-object" src="{{constituent['icon']}}" alt="{{constituent['subtitle']}}">

-                 </a>

-               </div>

-               {% endif %}

-               <div class="media-body">

-                   {{constituent['long_form']}}

-                     {% if constituent['link'] and constituent['link'] not in constituent['long_form'] %}

-                     <a href="{{constituent['link']}}" target="_blank">

-                         Read more <span class="glyphicon glyphicon-new-window"></span>

-                     </a>

-                     {% endif %}

-               </div>

-             </div>

-         {% endif %}

-         {% if match['msg_ids'] | length > 1 %}

-             <div class="pull-right">

-                 <a id="expand-{{match['dom_id']}}" href="javascript:expand_feed_entries('{{match['dom_id']}}');">

-                     <span class="glyphicon glyphicon-chevron-down"></span>

-                     expand

-                 </a>

-                 <a id="collapse-{{match['dom_id']}}" href="javascript:collapse_feed_entries('{{match['dom_id']}}');" class="hidden">

-                     <span class="glyphicon glyphicon-chevron-up"></span>

-                     collapse

-                 </a>

-             </div>

-             <ul id="content-{{match['dom_id']}}" class="hidden list-unstyled">

-                 {% for idx, constituent in match['msg_ids'].items() %}

-                 <li>

-                     {{ constituent['markup'] }}

-                     {% if constituent['link'] %}

-                     <a href="{{constituent['link']}}" target="_blank">

-                         <span class="glyphicon glyphicon-new-window"></span>

-                     </a>

-                     {% endif %}

-                 </li>

-                 {% endfor %}

-             </ul>

-         {% endif %}

-       </div>

-     </div>

-   </div>

- </div>

- {% endfor %}

+ </script>

Purpose

After some discussion about JS Frameworks, I have done the work to set up hubs with React. This should make it trivial to add or convert to React components in the future. This also implements the Feed Widget in react and includes live updating via Server Sent Events. This should be able to replace #193. Most of the important stuff is in Feed.jsx and feed.html.

http://paste.opensuse.org/view/raw/81727766

Just quick comment. I think I read up somewhere its not wise to insert the sources for libraries like this and to use a package manager like npm. Does react have an easy way to be installed like that and could we insert that to the setup.py script?

Hmm, I forgot that they are now installed with npm so the source folders shouldn't be needed. I was including them before I figured out how to get webpack to compile.

I've removed them locally with no problem. Hopefully that will make this easier to review too.

3 new commits added

  • Implement feed widget in Reactjs
  • Add tests for feed update and improve speed
  • Front end for live update for feed widget
7 years ago

awesome :) that would be much less daunting to review :P

rebased

7 years ago

rebased

7 years ago

Does this do a system wide install or is there a way to do it only locally (in the home folder or the cwd)?

As I understand it, when run without the -g (--global) flag, npm will only install these modules in the root of the project.

http://stackoverflow.com/a/14032346

Yes, -g does a global installation.

Using pickle because some stronger serialization on the matches list.

For example:

>>> d = { 'aset': set(['thing1', 'thing2']) }
>>> json.dumps(d)

TypeError: set(['thing1', 'thing2']) is not JSON serializable.

The more I think about it, the more that I think this isn't the right way to do it. Sure it got rid of the error messages but we are probably losing some data in there.

I don't think we loose data per say, but I do agree that going via pickle isn't really nice.

If possible I'd rather have us stick with json

What about something like::

class PythonObjectEncoder(json.JSONEncoder):
''' A JSON encoder that handles python data structures in encoding'''
def default(self, obj):
    if isinstance(obj, (list, dict, str, unicode, int, float, bool,
                        type(None))):
        return json.JSONEncoder.default(self, obj)
    if isinstance(obj, set) or isinstance(obj, tuple):
        return list(obj)
    if isinstance(obj, object):
        return obj.__dict__
    else:
        raise TypeError

rebased

7 years ago

rebased

7 years ago

rebased

7 years ago

1 new commit added

  • Fix JS that was causing errors
7 years ago

rebased

7 years ago

rebased

7 years ago

Looks fine to me but I'm clearly not the most knowledgeable person on react :)

rebased

7 years ago

Looks good to me. :thumbsup:

@atelic @sayanchowdhury gave his :thumbsup:, so let's do it! :)

Pull-Request has been merged by atelic

7 years ago