#231 Add My Stream page
Merged 7 years ago by atelic. Opened 7 years ago by atelic.
atelic/fedora-hubs feature/mystream  into  develop

file modified
+20 -16
@@ -138,14 +138,27 @@ 

  Make sure to leave the '' around you username and password - don't delete them! Some widgets will work without the above info being present..

  but it is needed for a subset of them.

  

- Feed Widget - the Extra Mile

- ----------------------------

+ JavaScript assets

+ --------------

+ 

+ Some widgets and pages run on React so to install those dependencies::

+ 

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

+ 

+ For the Feed and My Stream, change the ``SSE_URL`` location in the ``config`` you created

+ to point to the streaming server then build JavaScript assets. Add ``--w`` for live reloading::

+ 

+     $ cd hubs/static/client && node_modules/.bin/webpack

+ 

  

- One widget (the big tamale -- the feed widget) requires more legwork to stand

- up.  If you just want to see how hubs works and you want to hack on other

+ My Stream - the Extra Mile

+ ------------------------------------------

+ 

+ One portion of Hubs - the My Stream page - requires more legwork to stand up.

+ If you just want to see how hubs works and you want to hack on other

  peripheral stuff around it, you don't need to bother with these steps.

  

- The feed widget requires a direct DB connection to the datanommer

+ The stream requires a direct DB connection to the datanommer

  database; it can't proxy through datagrepper because it needs more

  flexibility.  To get this working, you're going to set up:

  
@@ -156,10 +169,6 @@ 

  

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

  

      $ sudo dnf install python-psycopg2 python-fedmsg-meta-fedora-infrastructure
@@ -217,19 +226,14 @@ 

      [2015-07-01 14:33:21][    fedmsg    INFO] copr has 6 entries

      [2015-07-01 14:33:21][    fedmsg    INFO] askbot has 2 entries

  

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

- you shouldn't get an error message about that widget being unable to be

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

+ you shouldn't get an error message about that page being unable to be

  displayed).

  

  Stubbing out a new Widget

file added
+2446
The added file is too large to be shown here, see it at: hubs/actions.json
file modified
+66 -7
@@ -4,20 +4,16 @@ 

  import logging

  import os

  import urlparse

- import uuid

  

  import flask

  import flask.json

- import fmn.lib

  import munch

- import pygments.formatters

- import six

  

  from flask.ext.oidc import OpenIDConnect

  

- import fmn.lib

  import hubs.models

  import hubs.widgets

+ import hubs.stream

  

  import datanommer.models

  
@@ -103,7 +99,7 @@ 

      if not authenticated():

          return flask.redirect(flask.url_for('login'))

  

-     return flask.redirect(flask.url_for('hub', name=flask.g.auth.nickname))

+     return flask.redirect(flask.url_for('stream', name=flask.g.auth.nickname))

  

  

  @app.route('/groups')
@@ -454,7 +450,7 @@ 

      from hubs.widgets import registry

      base = '/hubs/'

      fname = ''

-     

+ 

      try:

          fname = base + registry[name].__file__.split(base, 1)[1]

      except KeyError:
@@ -519,6 +515,69 @@ 

  

      return decorated_function

  

+ @app.route('/<name>/stream')

+ @app.route('/<name>/stream/')

+ @login_required

+ def stream(name):

+     hub = get_hub(session, name)

+     saved = hubs.models.SavedNotification.by_username(session, name)

+     saved = [n.__json__() for n in saved]

+ 

+     stream = hubs.stream.Stream()

+     actions = stream.get_json()

+ 

+     return flask.render_template(

+         'stream.html',

+         hub=hub,

+         saved=json.dumps(saved),

+         actions=actions

+     )

+ 

+ 

+ @app.route('/<user>/notifications', methods=['GET', 'POST'])

+ @app.route('/<user>/notifications/', methods=['GET', 'POST'])

+ @login_required

+ def notifications(user):

+     if flask.request.method == 'GET':

+         notifications = hubs.models.SavedNotification.by_username(session, user)

+         notifications = [n.__json__() for n in notifications]

+         return flask.jsonify(notifications)

+ 

+     if flask.request.method == 'POST':

+         data = json.loads(flask.request.data)

+         user = hubs.models.User.by_username(session, user)

+         if not user:

+             return flask.abort(400)

+         try:

+             markup = data['markup']

+             link = data['link']

+             icon = data['secondary_icon']

+             dom_id = data['dom_id']

+         except:

+             return flask.abort(400)

+         notification = hubs.models.SavedNotification(

+             username=user.username,

+             markup=markup,

+             link=link,

+             secondary_icon=icon,

+             dom_id=dom_id

+         )

+         session.add(notification)

+         session.commit()

+         return flask.jsonify(

+             dict(notification=notification.__json__(), success=True)

+         )

+ 

+ @app.route('/<user>/notifications/<int:idx>', methods=['DELETE'])

+ @app.route('/<user>/notifications/<int:idx>/', methods=['DELETE'])

+ @login_required

+ def delete_notifications(user, idx):

+     notification = session.query(hubs.models.SavedNotification).filter_by(idx=idx).first()

+     if not notification:

+         return flask.abort(400)

+     session.delete(notification)

+     session.commit()

+     return flask.jsonify(dict(status_code=200))

  

  @app.before_request

  def check_auth():

file modified
+43
@@ -28,6 +28,7 @@ 

  import os

  import random

  

+ import bleach

  import sqlalchemy as sa

  from sqlalchemy import create_engine

  from sqlalchemy.ext.declarative import declarative_base
@@ -353,6 +354,8 @@ 

      username = sa.Column(sa.Text, primary_key=True)

      fullname = sa.Column(sa.Text)

      created_on = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)

+     saved_notifications = relation('SavedNotification', backref='users',

+                                        lazy='dynamic')

  

      def __json__(self, session):

          return {
@@ -478,3 +481,43 @@ 

              session.add(self)

              session.commit()

          return self

+ 

+ 

+ class SavedNotification(BASE):

+     __tablename__ = 'savednotifications'

+     user = sa.Column(sa.Text, sa.ForeignKey('users.username'))

+ 

+     created = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)

+     dom_id = sa.Column(sa.Text)

+     idx = sa.Column(sa.Integer, primary_key=True)

+     link = sa.Column(sa.Text)

+     markup = sa.Column(sa.Text)

+     secondary_icon = sa.Column(sa.Text)

+ 

+ 

+     def __init__(self, username=None, markup='', link='', secondary_icon='', dom_id=''):

+         self.user = username

+         self.markup = markup

+         self.link = link

+         self.secondary_icon = secondary_icon

+         self.dom_id = dom_id

+ 

+     def __json__(self):

+         return {

+             'created': str(self.created),

+             'date_time': str(self.created),

+             'dom_id': self.dom_id,

+             'idx': self.idx,

+             'link': bleach.linkify(self.link),

+             'markup': bleach.linkify(self.markup),

+             'saved': True,

+             'secondary_icon': self.secondary_icon

+         }

+ 

+     @classmethod

+     def by_username(cls, session, username):

+         return session.query(cls).filter_by(user=username).all()

+ 

+     @classmethod

+     def all(cls, session):

+         return session.query(cls).all()

@@ -0,0 +1,71 @@ 

+ import React from 'react';

Should we keep this file if dropdown doesnt work?

Right now it conditionally renders the Save/Delete buttons. Hopefully we will figure out the markup for a correct dropdown after Flock and this file will wrap those buttons in that markup. It's not the most idiomatically named file right now but I'm hoping the dropdown will come

thanks for the explanation :)

+ 

+ export default class Dropdown extends React.Component {

+   save() {

+     const payload = {

+       link: this.props.match.link,

+       markup: this.props.match.markup,

+       secondary_icon: this.props.match.secondary_icon,

+       dom_id: this.props.match.dom_id,

+     };

+ 

+     $.ajax({

+       type: 'POST',

+       url: this.props.options.saveUrl,

+       data: JSON.stringify(payload),

+       contentType: 'application/json',

+     }).done(() => {

+       const id = `#save-${this.props.match.dom_id}`;

+       const $saveBtn = $(id);

+       $saveBtn.removeClass('btn-primary').addClass('btn-success');

+       $saveBtn.text('Saved');

+     });

+   }

+ 

+   delete() {

+     $.ajax({

+       type: 'DELETE',

+       url: `${this.props.options.saveUrl}${this.props.match.idx}/`,

+     }).done((resp) => {

+       if (resp.status_code === 200) {

+         const $notification = $(`#${this.props.match.dom_id}`);

+         $notification.closest('.card-block').remove();

+       }

+     });

+   }

+ 

+   render() {

+     let saveBtn;

+     if (!this.props.match.saved && this.props.options.saveUrl) {

+       saveBtn = (

+         <button

+           id={`save-${this.props.match.dom_id}`}

+           className="btn btn-sm btn-primary"

+           onClick={this.save.bind(this)}

+           href="#"

+         >

+           Save

+         </button>

+       );

+     }

+     let deleteBtn;

+     if (this.props.options.delete && this.props.options.saveUrl) {

+       deleteBtn = (

+         <button

+           id={`delete-${this.props.match.dom_id}`}

+           className="btn btn-sm btn-danger"

+           onClick={this.delete.bind(this)}

+         >

+           Remove

+         </button>

+       );

+     }

+ 

+     return (

+       <div className="pull-right">

+        {saveBtn}

+         {deleteBtn}

+       </div>

+     );

+   }

+ }

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

  import React from 'react';

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

+ import ReactDOM from 'react-dom';

  

  import Panel from './Panel.jsx';

  
@@ -8,9 +8,13 @@ 

      super(props);

      this.state = {

        matches: this.props.matches,

-       messageLimit: this.props.messageLimit,

+       messageLimit: this.props.options.messageLimit,

        sse: true,

      };

+     // If it wasn't instantiated with a url or an error happened, bail

+     if (!this.props.url || !this.state.sse) {

+       return;

+     }

      this.source = (!!window.EventSource) ? new EventSource(this.props.url) : {};

      this.source.addEventListener('error', () => {

        this.state.sse = false;
@@ -29,11 +33,11 @@ 

    }

    render() {

      const feedNodes = this.state.matches.map((match, idx) => {

-       return <Panel match={match} key={idx} />;

+       return <Panel match={match} options={this.props.options || {}} key={idx} />;

      });

      return (

        <div>

-       {feedNodes}

+         {feedNodes}

        </div>

      );

    }
@@ -41,4 +45,4 @@ 

  

  window.Feed = Feed;

  window.React = React;

- window.reactRender = render;

+ window.ReactDOM = ReactDOM;

@@ -2,16 +2,14 @@ 

  

  const Icon = function (props) {

    return (

-     <div>

-       <div className="media-left">

-         <a href={props.match.link ? props.match.link : '#'} target="_blank">

-           <img

-             alt="User avatar"

-             className="media-object square-32 img-circle"

-             src={props.match.secondary_icon}

-           />

-         </a>

-       </div>

+     <div className="media-left">

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

+         <img

+           alt="User avatar"

+           className="media-object square-32"

+           src={props.match.secondary_icon}

+         />

+       </a>

      </div>

    );

  };

@@ -1,11 +1,15 @@ 

  import React from 'react';

+ import Dropdown from './Dropdown.jsx';

  import TimeAgo from 'react-timeago';

  

+ 

  export default class Markup extends React.Component {

    createMarkup() {

      return { __html: this.props.match.markup };

    }

    render() {

+     const timestamp = this.props.match.date_time ? (<TimeAgo date={this.props.match.date_time} />) : null;

+ 

      return (

        <div className="media-body">

          <h4
@@ -13,7 +17,11 @@ 

            dangerouslySetInnerHTML={this.createMarkup()}

          >

          </h4>

-         <TimeAgo date={this.props.match.date_time} />

+         {timestamp}

+         <Dropdown

+           match={this.props.match}

+           options={this.props.options}

+         />

        </div>

      );

    }

@@ -5,10 +5,10 @@ 

  

  const Panel = function (props) {

    return (

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

-       <div className="panel-body">

+     <div className="card">

+       <div className="card-block" id={props.match.dom_id}>

          <Icon match={props.match} />

-         <Markup match={props.match} />

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

        </div>

      </div>

    );

file added
+9
@@ -0,0 +1,9 @@ 

+ import json

+ import os

+ 

+ class Stream(object):

+     def get_json(self):

+         here = os.path.dirname(os.path.abspath(__file__))

+         with open(os.path.join(here, 'actions.json')) as fp:

+             actions = fp.read()

+         return actions

file modified
+26
@@ -3,6 +3,32 @@ 

  {% block title %}{{ hub.name }}{% endblock %}

  {% block content %}

  

+   <div class="col-md-2">

+     <nav class="navbar navbar-light p-t-2 p-b-0">

+       <button type="button" class="navbar-toggler hidden-sm-up"

+         data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">

+         <span class="sr-only">Toggle navigation</span>

+         <span class="oi" data-glyph="menu"></span>

+       </button>

+ 

+       <!-- Collect the nav links, forms, and other content for toggling -->

+       {% if g.auth.logged_in %}

+       <div class="collapse navbar-toggleable-xs" id="bs-example-navbar-collapse-1">

+         <ul class="nav navbar-nav">

+           <li class="p-x-1 nav-item {% if request.path.endswith('/' + g.auth.user.username + '/') %}active{% endif %}">

+             <a class="nav-link" href="/{{g.auth.user.username}}">me</a></li>

+           <li class="p-x-1 nav-item"><a class="nav-link" href="/{{g.auth.user.username}}/stream">My Stream</a></li>

+           {% for hub in g.auth.user.bookmarks %}

+           <li class='p-x-1 nav-item idle-{{hub.activity_class}}{% if request.path.endswith('/' + hub.name + '/') %} active{% endif %}'>

+             <a class="nav-link" href="/{{hub.name}}">{{hub.name}}</a></li>

+           {% endfor %}

+           <!-- At the end of the list, tack on a link to all groups -->

+           <li class="p-x-1 nav-item"><a class="nav-link" href="/groups">all</a></li>

+         </ul>

+       </div><!-- /.navbar-collapse -->

+       {% endif %}

+     </nav>

+   </div>

    <header class="p-t-2">

      <div class="container">

  

@@ -0,0 +1,153 @@ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User ABC mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

+ 

+ <div class="card">

+   <div class="card-block">

+     <div>

+       <div class="media-left">

+         <a href="#" target="_blank"><img alt="User avatar" class="media-object square-32 img-circle" src="https://secure.gravatar.com/avatar/ba1882fd5522d16213b0535934f77b796f0b89f76edd65460078099fe97c20ea.jpg?s=64&d=retro"></a>

+       </div>

+     </div>

+     <div class="media-body">

+       <h4 class="media-heading">User XYZ mentioned you in a <a href="#" > comment</a></h4>

+       <time datetime="2016-07-20T23:44:37.121Z" title="2016-07-20 19:44:37.121830">16 hours ago</time>

+     </div>

+   </div>

+ </div>

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

+ <div id="right_widgets">

+   {% for widget in hub.right_widgets %}

+   <div id="widget-{{ widget.idx }}" class="widget row"></div>

+   {% endfor %}

+ </div>

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

+ <!-- NOT HOOKED UP TO ANYTHING XOXO -->

+ <div class="input-group">

+   <input type="search" class="form-control" placeholder="Search this activity stream..." aria-describedby="searchform-addon"></input>

+   <span class="input-group-addon" id="searchform-addon">

+     <span><i class="fa fa-search" aria-hidden="true"></i></span>

+   </span>

+ </div>

+ <br/>

+ <div class="alert alert-warning" role="alert">

+   This stream is filtered. <a href="https://apps.fedoraproject.org/notifications/{{hub.name}}.id.fedoraproject.org/" target="_blank">View Filters</a>

+ </div>

@@ -52,32 +52,6 @@ 

    </div>

  </div>

  <div class="bodycontent">

-   <div class="col-md-2">

-     <nav class="navbar navbar-light p-t-2 p-b-0">

-       <button type="button" class="navbar-toggler hidden-sm-up"

-         data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">

-         <span class="sr-only">Toggle navigation</span>

-         <span><i class="fa fa-bars" aria-hidden="true"></i></span>

-       </button>

- 

-       <!-- Collect the nav links, forms, and other content for toggling -->

-       {% if g.auth.logged_in %}

-       <div class="collapse navbar-toggleable-xs" id="bs-example-navbar-collapse-1">

-         <ul class="nav navbar-nav">

-           <li class="p-x-1 nav-item {% if request.path.endswith('/' + g.auth.user.username + '/') %}active{% endif %}">

-             <a class="nav-link" href="/{{g.auth.user.username}}">me</a></li>

-           {% for hub in g.auth.user.bookmarks %}

-           <li class='p-x-1 nav-item idle-{{hub.activity_class}}{% if request.path.endswith('/' + hub.name + '/') %} active{% endif %}'>

-             <a class="nav-link" href="/{{hub.name}}">{{hub.name}}</a></li>

-           {% endfor %}

-           <!-- At the end of the list, tack on a link to all groups -->

- 

-             <li class="p-x-1 nav-item"><a class="nav-link" href="/groups">all</a></li>

-         </ul>

-       </div><!-- /.navbar-collapse -->

-       {% endif %}

-     </nav>

-   </div>

  

  {% block content %}{% endblock %}

  

@@ -0,0 +1,173 @@ 

+ {% extends "master.html" %}

+ 

+ {% block title %}My Stream{% endblock %}

+ {% block content %}

+ <div class="jumbotron">

+   <h1>My Stream</h1>

+   <p>Notifications, actions, and other things of interest to me</p>

+ </div>

+ <ul class="nav-tabs " role="tablist">

+   <li class="nav-item">

+     <a class="nav-link active" href="#stream" role="tab" data-toggle="tab">My Stream</a>

+   </li>

+   <li class="nav-item">

+     <a class="nav-link" href="#actions" role="tab" data-toggle="tab">My Actions</a>

+   </li>

+   <li class="nav-item">

+     <a class="nav-link" href="#mentions" role="tab" data-toggle="tab">My Mentions</a>

+   </li>

+   <li class="nav-item">

+     <a class="nav-link" href="#saved" role="tab" data-toggle="tab">Saved Notifications</a>

+   </li>

+ </ul>

+ 

+ <!-- Tab panes -->

+ <div class="tab-content">

+   <div role="tabpanel" class="tab-pane fade in active" id="stream">

+     <div class="container">

+       <div class="row">

+         <div class="col-sm-8 pull-left">

+           {% include "includes/_searchstream.html" %}

+           <div id="streamFeed"></div>

+         </div>

+         <div class="col-sm-4">

+           {% include "includes/_rightpanel.html" %}

+         </div>

+       </div>

+     </div>

+   </div>

+ 

+   <div role="tabpanel" class="tab-pane fade" id="actions">

+     <div class="container">

+       <div class="row">

+         {% include "includes/_searchstream.html" %}

+         <div class="col-sm-8 pull-left">

+           <div id="actionsFeed"></div>

+         </div>

+         <div class="col-sm-4">

+           {% include "includes/_rightpanel.html" %}

+         </div>

+       </div>

+     </div>

+   </div>

+   <div role="tabpanel" class="tab-pane fade" id="mentions">

+     <div class="container">

+       <div class="row">

+         {% include "includes/_searchstream.html" %}

+         <br/>

+         <div class="col-sm-8 pull-left">

+           <div id="mentionsFeed">

+             {% include "includes/_messages.html" %}

+           </div>

+           <div class="col-sm-4">

+             {% include "includes/_rightpanel.html" %}

+           </div>

+         </div>

+       </div>

+     </div>

+   </div>

+   <div role="tabpanel" class="tab-pane fade" id="saved">

+     <div class="container">

+       <div class="row">

+         <br/>

+         {% include "includes/_searchstream.html" %}

+         <div class="col-sm-8 pull-left">

+           <div id="savedFeed"></div>

+         </div>

+         <div class="col-sm-4">

+           {% include "includes/_rightpanel.html" %}

+         </div>

+       </div>

+     </div>

+   </div>

+ </div>

+ <style>

+  .jumbotron {

+    margin-bottom: 0;

+  }

+  .nav-tabs {

+    list-style: none;

+  }

+ </style>

+ {% endblock %}

+ 

+ {% block jscripts %}

+ {{ super() }}

+ <script type="text/javascript"

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

+ <script>

+  function setup_widgets(widgets) {

+    var all_widgets = [{% for widget in hub.widgets %}'{{ widget.idx }}',{% endfor %}];

+    if (widgets == undefined) {

+      var widgets = all_widgets;

+    }

+    $.each(widgets, function(i, widget) {

+      $.ajax({

+        url: '/{{hub.name}}/' + widget,

+        dataType: 'html',

+        success: function(html) {

+          $('#widget-' + widget).html(html);

+          setTimeout(function() {

+            $('#widget-' + widget + ' .panel').toggleClass('panel-visible');

+          }, 100);

+        },

+        error: function() {

+          $('#widget-' + widget).html('Got an error retrieving this widget.  Sorry :(');

+          console.log('error');

+          console.trace();

+        },

+      });

+    });

+  }

+  window.onload = function(){

+    setup_widgets();

+    setup_feeds();

+  }

+ 

+  function setup_feeds() {

+    var actions = {{actions|safe}};

+ 

+    var streamFeed = React.createElement(Feed, {

+      matches: actions,

+      options: {

+        saveUrl: '/{{hub.name}}/notifications/',

+        messageLimit: 100

+      }

+    });

+    /* Right now, stream and actions are the same.

+     * Once, Mentions is implemented, then each will be its own

+     */

+    ReactDOM.render(streamFeed, document.getElementById('streamFeed'));

+    ReactDOM.render(streamFeed, document.getElementById('actionsFeed'));

+ 

+    var savedFeed = React.createElement(Feed, {

+      matches: {{saved|safe}},

+      url: false,

+      options: {

+        messageLimit: 100,

+        delete: true,

+        saveUrl: '/{{hub.name}}/notifications/'

+      }

+    });

+    ReactDOM.render(savedFeed, document.getElementById('savedFeed'));

+    $('a[href="#saved"]').on('shown.bs.tab', function(){

+      $.get('/{{hub.name}}/notifications/').done(function(resp) {

+        var saved = {{saved|safe}};

+        if (resp.length != saved.length) {

+          ReactDOM.unmountComponentAtNode(document.getElementById('savedFeed'));

+          var savedFeed = React.createElement(Feed, {

+            matches: resp,

+            url: false,

+            options: {

+              messageLimit: 100,

+              delete: true,

+              saveUrl: '/{{hub.name}}/notifications/'

+            }

+          });

+          ReactDOM.render(savedFeed, document.getElementById('savedFeed'));

+        }

+      });

+    });

+  }

+ </script>

+ {% endblock %}

file modified
+4
@@ -45,6 +45,10 @@ 

              fullname = user.title()

              hubs.models.User.get_or_create(

                  hubs.app.session, username=user, fullname=fullname)

+             saved_notif = hubs.models.SavedNotification(

+                 username=user, markup='foo', link='bar'

+             )

+             hubs.app.session.add(saved_notif)

  

          hubs.app.session.flush()

  

@@ -0,0 +1,121 @@ 

+ import json

+ from urlparse import urlparse

+ 

+ import hubs.tests

+ import hubs.models

+ from hubs.app import app

+ 

+ 

+ class TestGetNotifications(hubs.tests.APPTest):

+     user = hubs.tests.FakeAuthorization('ralph')

+ 

+     def test_get_notifications_invalid_name(self):

+         name = 'notarealfasuser'

+ 

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.get('/{}/notifications/'.format(name))

+         self.assertEqual(resp.status_code, 200)

+         data = json.loads(resp.data)

+         self.assertEqual(data, [])

+ 

+     def test_get_notifications_valid_name(self):

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.get('/{}/notifications/'.format(

+                 self.user.username))

+ 

+         self.assertEqual(resp.status_code, 200)

+         data = json.loads(resp.data)

+         self.assertEqual(len(data), 1)

+ 

+         for saved in data:

+             self.assertEqual(saved['markup'], 'foo')

+             self.assertEqual(saved['link'], 'bar')

+ 

+ 

+ class TestPostNotifications(hubs.tests.APPTest):

+     user = hubs.tests.FakeAuthorization('ralph')

+     valid_payload = {

+         'username': user.username,

+         'markup': 'foobar',

+         'link': 'baz',

+         'secondary_icon': 'http://placekitten.com/g/200/300',

+         'dom_id': 'reallyuniqueuid'

+     }

+ 

+     invalid_payload = {

+         'username': user.username,

+     }

+ 

+     def test_post_notification_invalid_user(self):

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.post(

+                 '/{}/notifications/'.format('notarealfasuser'),

+                 data=json.dumps(self.valid_payload))

+         self.assertEqual(resp.status_code, 400)

+ 

+     def test_post_notification_invalid_payload(self):

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.post(

+                 '/{}/notifications/'.format(self.user.username),

+                 data=json.dumps(self.invalid_payload))

+         self.assertEqual(resp.status_code, 400)

+ 

+     def test_post_notification_valid_payload(self):

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.post(

+                 '/{}/notifications/'.format(self.user.username),

+                 data=json.dumps(self.valid_payload))

+ 

+         self.assertEqual(resp.status_code, 200)

+         data = json.loads(resp.data)

+         self.assertTrue(isinstance(data, dict))

+ 

+         notification = data['notification']

+         self.assertEqual(notification['markup'], 'foobar')

+         self.assertEqual(notification['link'], 'baz')

+ 

+         all_saved = hubs.models.SavedNotification.by_username(

+             hubs.app.session, self.user.username)

+         self.assertEqual(len(all_saved), 2)

+         all_saved = [s.__json__() for s in all_saved]

+         self.assertTrue(any(str(s['markup']) == self.valid_payload['markup']

+                             for s in all_saved))

+         self.assertTrue(any(str(s['link']) == self.valid_payload['link']

+                             for s in all_saved))

+ 

+ 

+ class TestDeleteNotifications(hubs.tests.APPTest):

+     user = hubs.tests.FakeAuthorization('ralph')

+     notification = hubs.models.SavedNotification(

+         username='ralph',

+         markup='foo',

+         link='bar',

+         secondary_icon='baz',

+         dom_id='qux'

+     )

+ 

+     def test_delete_notification(self):

+         self.session.add(self.notification)

+         self.session.commit()

+         idx = self.notification.idx

+ 

+         self.assertIsNotNone(self.notification)

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.delete(

+                 '/{}/notifications/{}/'.format(self.user.username, idx)

+             )

+ 

+         self.assertEqual(resp.status_code, 200)

+         notification = self.session.query(

+             hubs.models.SavedNotification).filter_by(idx=idx).first()

+ 

+         self.assertIsNone(notification)

+ 

+     def test_404_on_bad_idx(self):

+         idx = 'thisisastringnotanint'

+ 

+         with hubs.tests.auth_set(app, self.user):

+             resp = self.app.delete(

+                 '/{}/notifications/{}/'.format(self.user.username, idx)

+             )

+         self.assertEqual(resp.status_code, 404)

file modified
+4 -26
@@ -1,29 +1,12 @@ 

  from __future__ import print_function

  

  from hubs.hinting import hint

- from hubs.widgets.base import argument, cache

+ from hubs.widgets.base import argument

  from hubs.widgets import templating

  

- import fmn.lib

- import fmn.lib.hinting

- 

- import datanommer.models

- 

- import arrow

- import munch

- import requests

- import six

- 

- import pygments.formatters

- 

- import flask

- 

  import hubs.validators as validators

  

- import datetime

- import json

  import logging

- import uuid

  

  log = logging.getLogger('hubs')

  
@@ -32,6 +15,7 @@ 

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

  position = 'left'

  

+ 

  @argument(name="username",

            default=None,

            validator=validators.username,
@@ -44,15 +28,9 @@ 

      # Avoid circular import

      from hubs.app import app

      feed_url = app.config['SSE_URL'] + username

-     return dict(

-         matches=[],

-         message_limit=message_limit,

-         feed_url=feed_url

-     )

+     return dict(matches=[], message_limit=message_limit, feed_url=feed_url)

+ 

  

  @hint(ubiquitous=True)

  def should_invalidate(message, session, widget):

      pass

- 

- 

- 

@@ -8,8 +8,10 @@ 

     const FeedElement = React.createElement(Feed, {

       matches: {{ matches }},

       url: '{{ feed_url }}',

-      messageLimit: {{ message_limit }}

+      options: {

+        messageLimit: {{ message_limit }}

+      }

     });

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

+    ReactDOM.render(FeedElement, document.getElementById('feed'));

   })();

  </script>

This is what I still have the biggest concern about. Right now it's faking a json representation of a response from fmn to use as the rule. It would be ideal to:
- Make a request to only fetch a specific 'A particular user' filter.
- If it doesn't exist, create one and use it.

This is a temporary file that represents fake data for the My Mentions tab. This is for the sake of the Flock demo and will be removed once the mentions feature is figured out.

1 new commit added

  • Add SavedNotifications tests
7 years ago

rebased

7 years ago

1 new commit added

  • Major improvements for Feed UI
7 years ago

rebased

7 years ago

was this purposefully removed?

Yes, it was moved here: https://pagure.io/fork/atelic/fedora-hubs/blob/feature/mystream/f/hubs/templates/hubs.html#_6

for the following reasons:
- Having it in the master template meant that we had the nav bar on every page in the app rather than on hubs pages.
- It overlapped the jumbotron on stream.html
- The stream mockup does not have a nav bar so it makes sense to only have it where it is needed

I get a 404 on url

http://localhost:5000/skrzepto/stream/

this is with a fresh db and repopulated

after clearing cookies

its throwing this

OperationalError: (psycopg2.OperationalError) could not connect to server: Connection refused

for the url http://localhost:5000/skrzepto/stream/

but http://localhost:5000/skrzepto loads fine

The above was fixed by

sudo systemctl start postgresql

From Line 31 to Line 130.

Could we add this functionality to a seperate class? and just do something like this

@app.route('/<name>/stream')
@app.route('/<name>/stream/')
@login_required
def stream(name):
    actions = stream()
    return flask.render_template(
     'stream.html',
     hub=hub,
     saved=json.dumps(saved),
     actions=actions
)

I say this because this will likely get replaced with something in fmn.lib and this will make replacing it after flock much easier

look at my comment on line 31 in this file

2 new commits added

  • Respond to code review
  • Fix failing test, add on dropdown for feed nodes
7 years ago

do we need feed widget here? thought we replaced it with sse?

Whats going on here?

1 new commit added

  • Explain the save tagging loop
7 years ago

rebased

7 years ago

how likely is it that a new notification will pop up and was saved?

how many notifications are we showing in the stream?

what does

rel="nofollow"

rebased

7 years ago

Very likely. Since most of these streams aren't pulled from SSE but rather are brought in from the db, they persist over refresh so it's possible for a user to hit save, see the save feedback, refresh and see the save button again. It also prevents the save button from showing up in the saved stream

1 new commit added

  • Escape html in fedmsgs to avoid breaking components
7 years ago

'More' button doesnt drop down in stream page

15 new commits added

  • Escape html in fedmsgs and remove More dropdown
  • Update README regarding Stream and Feed setup
  • Adjust to removal of feed.py code.
  • Explain the save tagging loop
  • Respond to code review
  • Fix failing test, add on dropdown for feed nodes
  • Major improvements for Feed UI
  • Add SavedNotifications tests
  • Add widget ability to my stream page
  • Hide save button if notif has already been saved..
  • Make stream code more readable, improve save
  • Real data for actions and stream tab
  • Fake data for My Mentions for Flock demo
  • Front and back ends for SavedNotifications
  • Naive implementaion of stream prototype
7 years ago

I've removed it for now since bootstrap dropdowns and cards don't seem to play nice. It looks like this now.

15 new commits added

  • Escape html in fedmsgs and remove More dropdown
  • Update README regarding Stream and Feed setup
  • Adjust to removal of feed.py code.
  • Explain the save tagging loop
  • Respond to code review
  • Fix failing test, add on dropdown for feed nodes
  • Major improvements for Feed UI
  • Add SavedNotifications tests
  • Add widget ability to my stream page
  • Hide save button if notif has already been saved..
  • Make stream code more readable, improve save
  • Real data for actions and stream tab
  • Fake data for My Mentions for Flock demo
  • Front and back ends for SavedNotifications
  • Naive implementaion of stream prototype
7 years ago

1 new commit added

  • Add DELETE for saved notifications
7 years ago

should we add @login_required

16 new commits added

  • Add DELETE for saved notifications
  • Escape html in fedmsgs and remove More dropdown
  • Update README regarding Stream and Feed setup
  • Adjust to removal of feed.py code.
  • Explain the save tagging loop
  • Respond to code review
  • Fix failing test, add on dropdown for feed nodes
  • Major improvements for Feed UI
  • Add SavedNotifications tests
  • Add widget ability to my stream page
  • Hide save button if notif has already been saved..
  • Make stream code more readable, improve save
  • Real data for actions and stream tab
  • Fake data for My Mentions for Flock demo
  • Front and back ends for SavedNotifications
  • Naive implementaion of stream prototype
7 years ago

16 new commits added

  • Add DELETE for saved notifications
  • Escape html in fedmsgs and remove More dropdown
  • Update README regarding Stream and Feed setup
  • Adjust to removal of feed.py code.
  • Explain the save tagging loop
  • Respond to code review
  • Fix failing test, add on dropdown for feed nodes
  • Major improvements for Feed UI
  • Add SavedNotifications tests
  • Add widget ability to my stream page
  • Hide save button if notif has already been saved..
  • Make stream code more readable, improve save
  • Real data for actions and stream tab
  • Fake data for My Mentions for Flock demo
  • Front and back ends for SavedNotifications
  • Naive implementaion of stream prototype
7 years ago

Should we keep this file if dropdown doesnt work?

Right now it conditionally renders the Save/Delete buttons. Hopefully we will figure out the markup for a correct dropdown after Flock and this file will wrap those buttons in that markup. It's not the most idiomatically named file right now but I'm hoping the dropdown will come

1 new commit added

  • Fix small bugs from refactoring Feed js
7 years ago

rebased

7 years ago

since the demo is over, I dont think we should add this file. Could we just returned a preset JSON messages for now? and then we could look into fmn and see what possibilities we can do for this

rebased

7 years ago

Removed in favor of passing a file of JSON messages for now. This is going to introduce some thrashing so I'll need to rebase a lot of this.

1 new commit added

  • Remove stream.py in favor of JSON messages
7 years ago

make this a method or a class, pretend that the fmn is working and we received a json array. This implementation is very specific and will need a rewrite of this route once the fmn service is put online. If we made a new method/class and had a method called get_json all we need to do is adjust that method instead of this route? it seperates the logic and makes for future adjustments easier https://en.wikipedia.org/wiki/Single_responsibility_principle

15 new commits added

  • Modify stream.py to fake JSON messages
  • Fix bugs created by refactoring Feed js
  • Add DELETE for saved notifications
  • Escape html in fedmsgs and remove More dropdown
  • Update README regarding Stream and Feed setup
  • Readability, README, and comment improvements
  • Fix failing test, add on dropdown for feed nodes
  • Major improvements for Feed UI
  • Add SavedNotifications tests
  • Add widget ability to my stream page
  • Hide save button if notif has already been saved..
  • Make stream code more readable, improve save
  • Data for Mentions, stream, and actions
  • Front and back ends for SavedNotifications
  • Naive implementaion of stream prototype
7 years ago

rebased

7 years ago

Pull-Request has been merged by atelic

7 years ago