#499 Rework the My Stream page
Merged 6 years ago by abompard. Opened 6 years ago by abompard.
abompard/fedora-hubs feature/stream  into  develop

file modified
+1 -1
@@ -20,7 +20,7 @@ 

      print("Found %r.  Deleting." % user)

      db.delete(user)

  

- hub = hubs.models.Hub.get(username)

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

  if not hub:

      print("No such hub %r" % username)

  else:

file modified
+12 -5
@@ -2,13 +2,15 @@ 

  

  from __future__ import unicode_literals

  

+ import logging

+ 

  import fedmsg.consumers

  import fedmsg.meta

- 

  import retask.task

  import retask.queue

  

- import logging

+ from hubs.utils import get_fedmsg_config

+ 

  log = logging.getLogger("hubs")

  

  
@@ -17,11 +19,16 @@ 

      config_key = 'hubs.consumer.enabled'

      validate_signatures = False

  

-     def __init__(self, *args, **kwargs):

+     def __init__(self, hub):

+         # Monkey-patch the hub config. I'm not very proud of that one but I

+         # didn't find any other way to tell Fedmsg to read my default config.

+         fedmsg_config = get_fedmsg_config()

+         hub.config = fedmsg_config

+ 

          log.debug("CacheInvalidatorExtraordinaire initializing")

-         super(CacheInvalidatorExtraordinaire, self).__init__(*args, **kwargs)

+         super(CacheInvalidatorExtraordinaire, self).__init__(hub)

  

-         queue_name = self.hub.config['hubs.redis.triage-queue-name']

+         queue_name = hub.config['hubs.redis.triage-queue-name']

          self.queue = retask.queue.Queue(queue_name)

  

          log.debug("CacheInvalidatorExtraordinaire initialized")

file modified
+1 -1
@@ -76,7 +76,7 @@ 

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

                      'type': 'widget-cache',

                      'idx': widget.idx,

-                     'hub': widget.hub.name,

+                     'hub': widget.hub.id,

                      'fn_name': fn_name,

                      'msg_id': msg['msg_id'],

                  }))

file modified
+21
@@ -127,3 +127,24 @@ 

      # other preset.

  

      return hub

+ 

+ 

+ def add_stream_widgets(hub):

+     """ Some defaults for an user's stream page. """

+     # Feed

+     hub.widgets.append(hubs.models.Widget(

+         plugin='feed', index=0, left=True,

+         _config=json.dumps({

+             'message_limit': 20

+         })))

+     # Library

+     hub.widgets.append(hubs.models.Widget(

+         plugin='library', index=0,

+         _config=json.dumps({})))

+     # Help requests

+     hub.widgets.append(hubs.models.Widget(

+         plugin='halp', index=1,

+         _config=json.dumps({})))

+     # TODO: "my assigned issues" widget

+     # TODO: "Newest Opened Pull Requests" widget

+     # TODO: "Newest Opened Issues" widget

file modified
+42 -28
@@ -10,7 +10,7 @@ 

  import pymongo

  from fedmsg.encoding import loads, dumps

  

- from hubs.models import Hub, User, HubConfig

+ from hubs.models import Hub, User, HubConfig, Association

  from hubs.utils import get_fedmsg_config, pagure

  

  
@@ -26,15 +26,16 @@ 

          if user is None:

              log.debug("Message concerning an unknown user: %s", username)

              continue

-         if Hub.query.get(username) is None:

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

+         if user_hub is None:

              log.debug("User exists but has no personal hub: %s", username)

              continue

-         hubs.append(username)

+         hubs.append(user_hub.id)

  

      # Group hubs

      if ".meetbot.meeting." in msg["topic"]:

          # Chat

-         hubs.extend(_get_group_hub_names_by_config(

+         hubs.extend(_get_group_hub_ids_by_config(

              HubConfig.key == "chat_channel",

              HubConfig.value == msg["msg"]["channel"],

          ))
@@ -45,13 +46,13 @@ 

          # this metadata:

          # list_address = msg["msg"]["mlist"]["fqdn_listname"]

          # in the meantime, we have to use a LIKE (which is very slow)

-         hubs.extend(_get_group_hub_names_by_config(

+         hubs.extend(_get_group_hub_ids_by_config(

              HubConfig.key == "mailing_list",

              HubConfig.value.like("{}@%".format(list_name)),

          ))

      elif ".fedocal." in msg["topic"]:

          # Calendar

-         hubs.extend(_get_group_hub_names_by_config(

+         hubs.extend(_get_group_hub_ids_by_config(

              HubConfig.key == "calendar",

              HubConfig.value == msg["msg"]["calendar"]["calendar_name"],

          ))
@@ -62,7 +63,7 @@ 

          except KeyError:

              pass

          else:

-             hubs.extend(_get_group_hub_names_by_config(

+             hubs.extend(_get_group_hub_ids_by_config(

                  HubConfig.key == "pagure",

                  HubConfig.value == project_name,

              ))
@@ -73,18 +74,29 @@ 

          except KeyError:

              pass

          else:

-             hubs.extend(_get_group_hub_names_by_config(

+             hubs.extend(_get_group_hub_ids_by_config(

                  HubConfig.key == "github",

                  HubConfig.value == project_name,

              ))

+     # Copy the message to each subscribed user's stream page

+     for hub_id in hubs[:]:

+         subscribers = Association.query.join(Hub).filter(

+             Hub.id == hub_id,

+             Association.role == "subscriber",

+             )

+         for assoc in subscribers:

+             stream = Hub.by_name(assoc.user.username, "stream")

+             if stream is None:

+                 continue

+             hubs.append(stream.id)

      return hubs

  

  

- def _get_group_hub_names_by_config(*args):

+ def _get_group_hub_ids_by_config(*args):

      query = Hub.query.filter(

-             Hub.user_hub == False,  # noqa:E712

+             Hub.hub_type == "team",

          ).join(HubConfig).filter(*args)

-     return [result[0] for result in query.values(Hub.name)]

+     return [result[0] for result in query.values(Hub.id)]

  

  

  def on_new_notification(msg):
@@ -95,31 +107,30 @@ 

      if user is None:

          log.debug("Notification for an unknown user: %s", username)

          return

-     # Users may exists without their hub if they have never logged

-     # in but are just added to the members list.  Don't check that

-     # the user Hub actually exists, it will be created when the user

-     # logs in, and this way the feed will be already populated.

+     stream = Hub.by_name(username, "stream")

+     if stream is None:

+         return

      log.debug("Received a notification concerning %s", username)

-     feed = Notifications(username)

+     feed = Notifications(stream.id)

      feed.add(msg)

  

  

  def on_new_message(msg):

-     for hub_name in msg["_hubs"]:

-         log.debug("Received a feed item for hub %s", hub_name)

-         feed = Activity(hub_name)

+     for hub_id in msg["_hubs"]:

+         log.debug("Received a feed item for hub %s", hub_id)

+         feed = Activity(hub_id)

          feed.add(msg)

  

  

- def add_dom_id(msg):

-     """Compute a deterministic dom_id.

+ def add_notif_id(msg):

+     """Compute a deterministic notification id.

  

      Since this is the identifier stored when the message is saved by the user

      in the SQL DB, it has to be invariant through conglomerate() calls.

      """

      if "msg_ids" not in msg:

          msg = fedmsg.meta.conglomerate([msg])[0]

-     msg["dom_id"] = hashlib.sha1(

+     msg["notif_id"] = hashlib.sha1(

          b":".join(

              [mid.encode("utf-8") for mid in sorted(msg["msg_ids"])]

          )).hexdigest()
@@ -172,7 +183,8 @@ 

              lead, middle, trail = match.groups()

              if middle in usernames:

                  middle = '<a href="{}">{}</a>'.format(

-                     flask.url_for("hub", name=middle), middle)

+                     flask.url_for("hub", hub_name=middle, hub_type="u"),

+                     middle)

              if lead + middle + trail != word:

                  words[i] = lead + middle + trail

      return ''.join(words)
@@ -183,15 +195,15 @@ 

      max_items = 100

      msgtype = None

  

-     def __init__(self, owner):

+     def __init__(self, hub_id):

          """

          Args:

-             owner (str): User name or Hub name.

+             hub_id (int): Hub primary key in the SQL database.

          """

          if self.msgtype is None:

              raise NotImplementedError(

                  "You must subclass Feed and set self.msgtype.")

-         self.owner = owner

+         self.hub_id = hub_id

          self.db = None

          fedmsg_config = get_fedmsg_config()

          self.db_config = {
@@ -228,7 +240,9 @@ 

          try:

              client = pymongo.MongoClient(self.db_config["url"])

              database = client[self.db_config["db"]]

-             collection_name = "|".join(["feed", self.msgtype, self.owner])

+             collection_name = "|".join([

+                 "feed", self.msgtype, str(self.hub_id)

+                 ])

              existing = database.collection_names(

                  include_system_collections=False)

              if collection_name in existing:
@@ -256,7 +270,7 @@ 

      msgtype = "notif"

  

      def _preprocess_msg(self, msg):

-         return add_dom_id(msg)

+         return add_notif_id(msg)

  

  

  class Activity(Feed):

file modified
+2 -3
@@ -39,9 +39,8 @@ 

  

      __tablename__ = 'association'

  

-     hub_id = sa.Column(sa.String(50),

-                        sa.ForeignKey('hubs.name'),

-                        primary_key=True)

+     hub_id = sa.Column(

+         sa.Integer, sa.ForeignKey('hubs.id'), primary_key=True)

      user_id = sa.Column(sa.Text,

                          sa.ForeignKey('users.username'),

                          primary_key=True)

@@ -23,6 +23,8 @@ 

  from __future__ import unicode_literals

  

  

+ HUB_TYPES = ("user", "team", "stream")

+ 

  ROLES = ('subscriber', 'member', 'owner', 'stargazer')

  

  VISIBILITIES = ("public", "preview", "private")

file modified
+42 -18
@@ -36,7 +36,7 @@ 

  from hubs.utils import username2avatar

  from hubs.signals import hub_created

  from .association import Association

- from .constants import ROLES

+ from .constants import ROLES, HUB_TYPES

  from .hubconfig import HubConfigProxy

  from .user import User

  
@@ -47,12 +47,17 @@ 

  class Hub(ObjectAuthzMixin, BASE):

  

      __tablename__ = 'hubs'

- 

-     name = sa.Column(sa.String(50), primary_key=True)

+     __table_args__ = (

+         sa.schema.UniqueConstraint('name', 'hub_type'),

+         )

+ 

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

+     name = sa.Column(sa.String(50), nullable=False)

+     hub_type = sa.Column(

+         sa.Enum(*HUB_TYPES, name="hub_types"), nullable=False)

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

      widgets = relation('Widget', cascade='all,delete', backref='hub',

                         order_by="Widget.index")

-     user_hub = sa.Column(sa.Boolean, default=False)

      # Timestamps about various kinds of "freshness"

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

      last_edited = sa.Column(sa.DateTime, default=datetime.datetime.utcnow)
@@ -72,6 +77,16 @@ 

          proxy.update(config)

  

      @property

+     def url(self):

+         if self.hub_type == "user":

+             type_short = "u"

+         elif self.hub_type == "team":

+             type_short = "t"

+         else:

+             raise ValueError("Unhandled hub type: {}".format(self.hub_type))

+         return flask.url_for("hub", hub_name=self.name, hub_type=type_short)

+ 

+     @property

      def days_idle(self):

          return (datetime.datetime.utcnow() - self.last_refreshed).days

  
@@ -143,23 +158,21 @@ 

          session.commit()

  

      @classmethod

-     def by_name(cls, name):

-         return cls.query.filter_by(name=name).first()

- 

-     get = by_name

+     def by_name(cls, name, hub_type):

+         return cls.query.filter_by(name=name, hub_type=hub_type).first()

  

      @classmethod

      def all_group_hubs(cls):

-         return cls.query.filter_by(user_hub=False).all()

+         return cls.query.filter_by(hub_type="team").all()

  

      @classmethod

      def all_user_hubs(cls):

-         return cls.query.filter_by(user_hub=True).all()

+         return cls.query.filter_by(hub_type="user").all()

  

      @classmethod

      def create_user_hub(cls, username, fullname):

          session = Session()

-         hub = cls(name=username, user_hub=True)

+         hub = cls(name=username, hub_type="user")

          session.add(hub)

          hub.config["summary"] = fullname

          hub.config["avatar"] = username2avatar(username)
@@ -170,7 +183,7 @@ 

      @classmethod

      def create_group_hub(cls, name, summary, **extra):

          session = Session()

-         hub = cls(name=name, user_hub=False)

+         hub = cls(name=name, hub_type="team")

          session.add(hub)

          hub.config["summary"] = summary

          if extra.get("irc_channel") and extra.get("irc_network"):
@@ -182,13 +195,23 @@ 

          hub_created.send(hub, **extra)

          return hub

  

+     @classmethod

+     def create_stream_hub(cls, username):

+         session = Session()

+         hub = cls(name=username, hub_type="stream")

+         session.add(hub)

+         hub_created.send(hub)

+         return hub

+ 

      def on_created(self, **extra):

-         if self.user_hub:

+         if self.hub_type == "user":

              hubs.defaults.add_user_widgets(self)

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

              self.subscribe(user, role='owner')

-         else:

+         elif self.hub_type == "team":

              hubs.defaults.add_group_widgets(self, **extra)

+         elif self.hub_type == "stream":

+             hubs.defaults.add_stream_widgets(self)

  

      def on_updated(self, old_config):

          for widget_instance in self.widgets:
@@ -219,8 +242,8 @@ 

                          )

  

      def _get_auth_user_access_level(self, user):

-         # overridden to handle user hubs.

-         if self.user_hub and user.username == self.name:

+         # overridden to handle user and stream hubs.

+         if self.hub_type in ("user", "stream") and user.username == self.name:

              return AccessLevel.owner

          return super(Hub, self)._get_auth_user_access_level(user)

  
@@ -255,17 +278,18 @@ 

      def get_props(self):

          """Get the hub properties for the Javascript UI"""

          result = {

+             "id": self.id,

              "name": self.name,

              "config": self.config.to_dict(),

              "users": {role: [] for role in ROLES},

              "mtime": self.last_refreshed,

-             "user_hub": self.user_hub,

+             "type": self.hub_type,

          }

          for assoc in sorted(self.associations, key=lambda a: a.user.username):

              if assoc.role not in ROLES:

                  continue

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

-         if self.user_hub:

+         if self.hub_type == "user":

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

              if user is None:

                  result["subscribed_to"] = []

file modified
+1 -1
@@ -242,6 +242,6 @@ 

  

      id = sa.Column(sa.Integer, primary_key=True)

      hub_id = sa.Column(

-         sa.String(50), sa.ForeignKey('hubs.name'), index=True, nullable=False)

+         sa.Integer, sa.ForeignKey('hubs.id'), index=True, nullable=False)

      key = sa.Column(sa.String(256), index=True, nullable=False)

      value = sa.Column(sa.Text, index=True, nullable=False)

@@ -25,7 +25,6 @@ 

  import datetime

  import logging

  

- import bleach

  import sqlalchemy as sa

  

  from hubs.database import BASE
@@ -37,39 +36,28 @@ 

  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)

+     __table_args__ = (

+         sa.schema.UniqueConstraint('username', 'notif_id'),

+         )

+ 

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

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

+     created = sa.Column(

+         sa.DateTime, default=datetime.datetime.utcnow, nullable=False)

+     notif_id = sa.Column(sa.Text, nullable=False)

      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

+     markup = sa.Column(sa.Text, nullable=False)

+     icon = sa.Column(sa.Text)

+     timestamp = sa.Column(sa.Integer, nullable=False)

  

-     def __json__(self):

+     def to_dict(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),

+             'id': self.id,

+             'created': self.created,

+             'timestamp': self.timestamp,

+             'notif_id': self.notif_id,

+             'link': self.link,

+             'markup': self.markup,

+             'secondary_icon': self.icon,

              'saved': True,

-             'secondary_icon': self.secondary_icon

          }

- 

-     @classmethod

-     def by_username(cls, username):

-         return cls.query.filter_by(user=username).all()

- 

-     @classmethod

-     def all(cls):

-         return cls.query.all()

file modified
+11 -3
@@ -33,16 +33,22 @@ 

  from hubs.utils import username2avatar

  from hubs.signals import user_created

  

+ from .savednotification import SavedNotification

+ 

+ 

  log = logging.getLogger(__name__)

  

  

  class User(BASE):

+ 

      __tablename__ = 'users'

+ 

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

+     saved_notifications = relation(

+         'SavedNotification', backref='user', lazy='dynamic',

+         order_by=SavedNotification.created.desc())

  

      def __json__(self):

          return {
@@ -135,5 +141,7 @@ 

  

      def on_created(self):

          from .hub import Hub

-         if Hub.query.get(self.username) is None:

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

              Hub.create_user_hub(self.username, self.fullname)

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

+             Hub.create_stream_hub(self.username)

file modified
+2 -5
@@ -61,7 +61,8 @@ 

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

      plugin = sa.Column(sa.String(50), nullable=False)

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

-     hub_id = sa.Column(sa.String(50), sa.ForeignKey('hubs.name'))

+     hub_id = sa.Column(

+         sa.Integer, sa.ForeignKey('hubs.id'), index=True, nullable=False)

      _config = sa.Column(sa.Text, default="{}")

  

      index = sa.Column(sa.Integer, nullable=False)
@@ -78,10 +79,6 @@ 

      def by_plugin(cls, plugin):

          return cls.query.filter_by(plugin=plugin).first()

  

-     @classmethod

-     def by_hub_id_all(cls, hub_id):

-         return cls.query.filter_by(hub_id=hub_id).all()

- 

      get = by_idx

  

      @property

@@ -99,6 +99,78 @@ 

        </div>

      );

  

+     let tabs = [

+       <GeneralPanel

+         key="general"

+         hubConfig={this.props.hubConfig}

+         globalConfig={this.props.globalConfig}

+         tabTitle={<FormattedMessage {...messages.general} />}

+         handleChange={this.props.onConfigChange}

+         hub={this.props.hub}

+         />

+     ]

+     if (this.props.hub.type === "team") {

+       tabs.push(

+         <UserPanel

+           key="owners"

+           users={this.props.users.owner}

+           globalConfig={this.props.globalConfig}

+           role="owner"

+           handleChange={this.props.onUserChange}

+           tabTitle={<FormattedMessage {...messages.owners} />}

+           urls={this.props.urls}

+           currentUser={this.props.currentUser}

+           />

+         ,

+         <UserPanel

+           key="members"

+           users={this.props.users.member}

+           globalConfig={this.props.globalConfig}

+           role="member"

+           handleChange={this.props.onUserChange}

+           tabTitle={<FormattedMessage {...messages.members} />}

+           urls={this.props.urls}

+           currentUser={this.props.currentUser}

+           />

+         ,

+         <MailingListPanel

+           key="mailing_list"

+           hubConfig={this.props.hubConfig}

+           globalConfig={this.props.globalConfig}

+           tabTitle={<FormattedMessage {...messages.mailinglist} />}

+           handleChange={this.props.onConfigChange}

+           error={this.props.hub.error}

+           />

+         ,

+         <ChatPanel

+           key="chat"

+           hubConfig={this.props.hubConfig}

+           globalConfig={this.props.globalConfig}

+           tabTitle={<FormattedMessage {...messages.chat} />}

+           handleChange={this.props.onConfigChange}

+           error={this.props.hub.error}

+           />

+         ,

+         <CalendarPanel

+           key="calendar"

+           hubConfig={this.props.hubConfig}

+           globalConfig={this.props.globalConfig}

+           tabTitle={<FormattedMessage {...messages.calendar} />}

+           handleChange={this.props.onConfigChange}

+           error={this.props.hub.error}

+           />

+         ,

+         <DevPlatformPanel

+           key="devplatform"

+           hubConfig={this.props.hubConfig}

+           globalConfig={this.props.globalConfig}

+           tabTitle={<FormattedMessage {...messages.devplatform} />}

+           handleChange={this.props.onConfigListChange}

+           error={this.props.hub.error}

+           />

+       );

+     }

+ 

      return (

        <Modal

          onCloseClicked={this.props.onCloseClicked}
@@ -113,71 +185,7 @@ 

            tabLinkClass="my-md-2"

            tabContentClass="col-md-9 p-3 pb-4"

            >

-           <GeneralPanel

-             hubConfig={this.props.hubConfig}

-             globalConfig={this.props.globalConfig}

-             tabTitle={<FormattedMessage {...messages.general} />}

-             handleChange={this.props.onConfigChange}

-             hub={this.props.hub}

-             />

-           {!this.props.hub.user_hub &&

-             <UserPanel

-               users={this.props.users.owner}

-               globalConfig={this.props.globalConfig}

-               role="owner"

-               handleChange={this.props.onUserChange}

-               tabTitle={<FormattedMessage {...messages.owners} />}

-               urls={this.props.urls}

-               currentUser={this.props.currentUser}

-               />

-           }

-           {!this.props.hub.user_hub &&

-             <UserPanel

-               users={this.props.users.member}

-               globalConfig={this.props.globalConfig}

-               role="member"

-               handleChange={this.props.onUserChange}

-               tabTitle={<FormattedMessage {...messages.members} />}

-               urls={this.props.urls}

-               currentUser={this.props.currentUser}

-               />

-           }

-           {!this.props.hub.user_hub &&

-             <MailingListPanel

-               hubConfig={this.props.hubConfig}

-               globalConfig={this.props.globalConfig}

-               tabTitle={<FormattedMessage {...messages.mailinglist} />}

-               handleChange={this.props.onConfigChange}

-               error={this.props.hub.error}

-               />

-           }

-           {!this.props.hub.user_hub &&

-             <ChatPanel

-               hubConfig={this.props.hubConfig}

-               globalConfig={this.props.globalConfig}

-               tabTitle={<FormattedMessage {...messages.chat} />}

-               handleChange={this.props.onConfigChange}

-               error={this.props.hub.error}

-               />

-           }

-           {!this.props.hub.user_hub &&

-             <CalendarPanel

-               hubConfig={this.props.hubConfig}

-               globalConfig={this.props.globalConfig}

-               tabTitle={<FormattedMessage {...messages.calendar} />}

-               handleChange={this.props.onConfigChange}

-               error={this.props.hub.error}

-               />

-           }

-           {!this.props.hub.user_hub &&

-             <DevPlatformPanel

-               hubConfig={this.props.hubConfig}

-               globalConfig={this.props.globalConfig}

-               tabTitle={<FormattedMessage {...messages.devplatform} />}

-               handleChange={this.props.onConfigListChange}

-               error={this.props.hub.error}

-               />

-           }

+           {tabs}

            {/*<NotImplementedPanel

              tabTitle={<FormattedMessage {...messages.other} />}

              />*/}

@@ -49,11 +49,11 @@ 

    }

  

    render() {

-     var networks = [],

-         channel = "#",

-         stillLoading = (

-           typeof this.props.hubConfig.chat_channel === "undefined"

-         );

+     let networks = [],

+         channel = "#";

+     const stillLoading = (

+       typeof this.props.hubConfig.chat_channel === "undefined"

+     );

  

      if (this.props.hubConfig.chat_channel) {

        channel = "#" + this.props.hubConfig.chat_channel.replace(/^#*/, "");
@@ -61,13 +61,11 @@ 

  

      if (this.props.globalConfig.chat_networks) {

        networks = this.props.globalConfig.chat_networks.map(

-         function(network, index) {

-           return (

-             <option value={network.domain} key={network.domain}>

-               {network.name} - {network.domain}

-             </option>

-           );

-         }.bind(this)

+         (network, index) => (

+           <option value={network.domain} key={network.domain}>

+             {network.name} - {network.domain}

+           </option>

+         )

        );

      }

  

@@ -133,7 +133,7 @@ 

                />

            </p>

          </div>

-         {!this.props.hub.user_hub &&

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

          <div className="form-group row">

            <label htmlFor="hub-settings-general-visibility">

              <FormattedMessage {...messages.visibility} />
@@ -161,7 +161,7 @@ 

            </p>

          </div>

          }

-         {!this.props.hub.user_hub &&

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

          <div className="form-group row">

            <label htmlFor="hub-settings-general-avatar">

              <FormattedMessage {...messages.avatar} />

hubs/static/client/app/components/HubHeader/EditModeButton.css hubs/static/client/app/components/EditModeButton.css
file renamed
file was moved with no change to the file
hubs/static/client/app/components/HubHeader/EditModeButton.js hubs/static/client/app/components/EditModeButton.js
file renamed
+1 -1
@@ -5,7 +5,7 @@ 

    defineMessages,

    FormattedMessage,

    } from 'react-intl';

- import { setEditMode } from "../core/actions/widgets";

+ import { setEditMode } from "../../core/actions/widgets";

  import "./EditModeButton.css";

  

  

@@ -0,0 +1,25 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import { monogramColour } from '../../core/utils';

+ 

+ 

+ export default class HubAvatar extends React.Component {

+   render() {

+     if (this.props.avatar) {

+       return (

+         <img

+           className="avatar"

+           src={this.props.avatar}

+           />

+       );

+     } else {

+       const mColour = monogramColour(this.props.name);

+       const className = `monogram-avatar avatar bg-fedora-${mColour} color-fedora-${mColour}-dark`;

+       return (

+         <div className={className}>

+           {this.props.name.charAt(0).toUpperCase()}

+         </div>

+       );

+     }

+   }

+ }

hubs/static/client/app/components/HubHeader/HubHeader.css hubs/static/client/app/components/HubHeader.css
file renamed
file was moved with no change to the file
@@ -0,0 +1,36 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import HubStats from './HubStats';

+ import HubStar from './HubStar';

+ import HubAvatar from './HubAvatar';

+ 

+ 

+ export default class HubHeaderLeft extends React.Component {

+ 

+   render() {

+     return (

+       <div className="HubHeaderLeft h-100">

+         <HubStats

+           hub={this.props.hub}

+           />

+         <div>

+           <HubAvatar

+             name={this.props.hub.name}

+             avatar={this.props.hub.config.avatar}

+             />

+           <h2 className="user pt-0">

+             {this.props.hub.name}

+             <HubStar />

+           </h2>

+           <h5 className="mt-1 mb-1">

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

+           </h5>

+           <div className="clearfix"></div>

+         </div>

+       </div>

+     );

+   }

+ }

+ HubHeaderLeft.propTypes = {

+   hub: PropTypes.object.isRequired,

+ }

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

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import HubConfig from '../HubConfig';

+ import EditModeButton from './EditModeButton';

+ import HubMembership from './HubMembership';

+ 

+ 

+ export default class HubHeaderRight extends React.Component {

+ 

+   render() {

+     return (

+       <div className="HubHeaderRight">

+         <HubMembership />

+         { this.props.hub.perms.config &&

+           <div>

+             <HubConfig />

+             <EditModeButton />

+           </div>

+         }

+       </div>

+     );

+   }

+ }

+ HubHeaderRight.propTypes = {

+   hub: PropTypes.object.isRequired,

+ }

hubs/static/client/app/components/HubHeader/HubMembership.js hubs/static/client/app/components/HubMembership.js
file renamed
+4 -4
@@ -4,8 +4,8 @@ 

  import {

    associateUser,

    dissociateUser

- } from "../core/actions/hub";

- import StateButton from "./StateButton";

+ } from "../../core/actions/hub";

+ import StateButton from "../StateButton";

  

  

  
@@ -53,7 +53,7 @@ 

      if (!this.props.hub.name) {

        return null;

      }

-     if (this.props.hub.user_hub && this.props.hub.perms.config) {

+     if (this.props.hub.type === "user" && this.props.hub.perms.config) {

        return null;  // The user's own hub.

      }

      let commonProps = {disabled: false, title: ""}
@@ -65,7 +65,7 @@ 

        commonProps.title = "loading...";

      }

      let secondButton = null;

-     if (!this.props.hub.user_hub) {

+     if (this.props.hub.type === "team") {

        if (this.userHasRole("owner")) {

          secondButton = (

            <HubGiveUpAdminButton

hubs/static/client/app/components/HubHeader/HubStar.css hubs/static/client/app/components/HubStar.css
file renamed
file was moved with no change to the file
hubs/static/client/app/components/HubHeader/HubStar.js hubs/static/client/app/components/HubStar.js
file renamed
+1 -1
@@ -4,7 +4,7 @@ 

  import {

    associateUser,

    dissociateUser

- } from "../core/actions/hub";

+ } from "../../core/actions/hub";

  import "./HubStar.css";

  

  

hubs/static/client/app/components/HubHeader/HubStats.css hubs/static/client/app/components/HubStats.css
file renamed
file was moved with no change to the file
hubs/static/client/app/components/HubHeader/HubStats.js hubs/static/client/app/components/HubStats.js
file renamed
+3 -3
@@ -7,7 +7,7 @@ 

    join,

    leave,

    giveUpAdmin,

- } from "../core/actions/hub";

+ } from "../../core/actions/hub";

  import "./HubStats.css";

  

  
@@ -19,7 +19,7 @@ 

      }

      return (

        <div className="HubStats d-flex float-right h-100">

-         { !this.props.hub.user_hub &&

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

            <HubStatsCounter

              title="Members"

              value={
@@ -32,7 +32,7 @@ 

            title="Subscribers"

            value={this.props.hub.users.subscriber.length}

            />

-         { this.props.hub.user_hub &&

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

            <HubStatsCounter

              title="Subscribed to"

              value={this.props.hub.subscribed_to.length}

@@ -0,0 +1,16 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ 

+ 

+ export default class StreamHeaderLeft extends React.Component {

+ 

+   render() {

+     return (

+       <div className="StreamHeaderLeft h-100 mb-2">

+         <h2>My Stream</h2>

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

+         <div className="clearfix"></div>

+       </div>

+     );

+   }

+ }

hubs/static/client/app/components/HubHeader/StreamHeaderRight.js hubs/static/client/app/components/StreamsHeader.js
file renamed
+4 -4
@@ -1,14 +1,14 @@ 

  import React from 'react';

  import PropTypes from 'prop-types';

+ import EditModeButton from './EditModeButton';

  

  

- export default class StreamsHeader extends React.Component {

+ export default class StreamHeaderRight extends React.Component {

  

    render() {

      return (

-       <div className="StreamsHeader">

-         <h1>My Stream</h1>

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

+       <div className="StreamHeaderRight">

+         <EditModeButton />

        </div>

      );

    }

hubs/static/client/app/components/HubHeader/index.js hubs/static/client/app/components/HubHeader.js
file renamed
+29 -44
@@ -1,59 +1,50 @@ 

  import React from 'react';

  import PropTypes from 'prop-types';

  import { connect } from 'react-redux';

- import Spinner from "./Spinner";

- import HubConfig from './HubConfig';

- import EditModeButton from './EditModeButton';

- import HubStats from './HubStats';

- import HubMembership from './HubMembership';

- import HubStar from './HubStar';

- import { monogramColour } from '../core/utils';

+ import Spinner from "../Spinner";

+ import HubHeaderLeft from "./HubHeaderLeft";

+ import HubHeaderRight from "./HubHeaderRight";

+ import StreamHeaderLeft from "./StreamHeaderLeft";

+ import StreamHeaderRight from "./StreamHeaderRight";

  import "./HubHeader.css";

  

  

  class HubHeader extends React.Component {

+ 

    render() {

-     const right_width = this.props.hub.config ? 12 - this.props.hub.config.left_width : 4;

+     let left_width = 8,

+         HeaderLeft = null,

+         HeaderRight = null;

+     const isLoaded = this.props.hub.name !== null;

+     if (isLoaded) {

+       // It's loaded now

+       if (this.props.hub.type === "stream") {

+         left_width = 8;

+         HeaderLeft = StreamHeaderLeft,

+         HeaderRight = StreamHeaderRight;

+       } else {

+         left_width = this.props.hub.config.left_width;

+         HeaderLeft = HubHeaderLeft,

+         HeaderRight = HubHeaderRight;

+       }

+     }

+     const right_width = 12 - left_width;

      return (

        <div className="HubHeader">

          { this.props.isLoading &&

            <Spinner />

          }

-         { this.props.hub.name &&

+         { isLoaded &&

            <div className="row">

-             <div className={`col-md-${this.props.hub.config.left_width} pl-0`}>

-               <HubStats

+             <div className={`col-md-${left_width} pl-0`}>

+               <HeaderLeft

                  hub={this.props.hub}

                  />

-                 <div>

-                 { !this.props.hub.config.avatar ?

-                   <div className={`monogram-avatar avatar bg-fedora-${monogramColour(this.props.hub.name)} color-fedora-${monogramColour(this.props.hub.name)}-dark`}>

-                     {this.props.hub.name.charAt(0).toUpperCase()}

-                   </div>

-                 :

-                   <img

-                     className="avatar"

-                     src={this.props.hub.config.avatar}

-                     />

-                 }

-                   <h2 className="user pt-0">

-                     {this.props.hub.name}

-                     <HubStar />

-                   </h2>

-                   <h5 className="mt-1 mb-1">

-                     {this.props.hub.config.summary}

-                   </h5>

-                   <div className="clearfix"></div>

-                 </div>

              </div>

              <div className={`col-md-${right_width} text-center align-self-center`}>

-               <HubMembership />

-               { this.props.hub.perms.config &&

-                 <div>

-                   <HubConfig />

-                   <EditModeButton />

-                 </div>

-               }

+               <HeaderRight

+                 hub={this.props.hub}

+                 />

              </div>

            </div>

          }
@@ -61,12 +52,6 @@ 

      );

    }

  }

- HubHeader.propTypes = {

-   hub: PropTypes.object.isRequired,

-   isLoading: PropTypes.bool,

-   currentUser: PropTypes.object,

- }

- 

  

  

  const mapStateToProps = (state) => {

@@ -9,7 +9,6 @@ 

  import WidgetsArea from './WidgetsArea';

  import HubHeader from './HubHeader';

  import { fetchHub } from '../core/actions/hub';

- import "./HubPage.css";

  

  

  class HubPage extends React.Component {

@@ -41,14 +41,10 @@ 

          );

        }

        leftMenuEntries.push(

-         ...this.props.user.starred_hubs.map((entry) => (

-           <LeftMenuEntry

-             key={entry.url}

-             icon={entry.user_hub ? "user" : "users"}

-             text={entry.name}

-             user_hub={entry.user_hub}

-             url={entry.url}

-             cssClass={entry.cssClass}

+         ...this.props.user.starred_hubs.map((hub) => (

+           <LeftMenuHubEntry

+             key={hub.url}

+             hub={hub}

              />

          ))

        )
@@ -61,14 +57,10 @@ 

          );

        }

        leftMenuEntries.push(

-         ...this.props.user.memberships.map((entry) => (

-           <LeftMenuEntry

-             key={entry.url}

-             icon={entry.user_hub ? "user" : "users"}

-             text={entry.name}

-             user_hub={entry.user_hub}

-             url={entry.url}

-             cssClass={entry.cssClass}

+         ...this.props.user.memberships.map((hub) => (

+           <LeftMenuHubEntry

+             key={hub.url}

+             hub={hub}

              />

          ))

        )
@@ -81,14 +73,10 @@ 

          );

        }

        leftMenuEntries.push(

-         ...this.props.user.subscriptions.map((entry) => (

-           <LeftMenuEntry

-             key={entry.url}

-             icon={entry.user_hub ? "user" : "users"}

-             text={entry.name}

-             user_hub={entry.user_hub}

-             url={entry.url}

-             cssClass={entry.cssClass}

+         ...this.props.user.subscriptions.map((hub) => (

+           <LeftMenuHubEntry

+             key={hub.url}

+             hub={hub}

              />

          ))

        )
@@ -144,7 +132,6 @@ 

  

  LeftMenuEntry.propTypes = {

    url: PropTypes.string.isRequired,

-   user_hub: PropTypes.bool,

    icon: PropTypes.string.isRequired,

    text: PropTypes.string.isRequired,

    cssClass: PropTypes.string,
@@ -153,11 +140,26 @@ 

    cssClass: null,

  }

  

+ 

+ class LeftMenuHubEntry extends React.Component {

+   render() {

+     return (

+       <LeftMenuEntry

+         icon={this.props.hub.type === "user" ? "user" : "users"}

+         text={this.props.hub.name}

+         url={this.props.hub.url}

+         cssClass={this.props.hub.cssClass}

+         />

+     );

+   }

+ }

+ 

+ 

  class LeftMenuHeader extends React.Component {

      render() {

        return (

          <li className="nav-item ml-2 mt-2 text-muted">

-             <small><strong>{this.props.text}</strong></small>

+           <small><strong>{this.props.text}</strong></small>

          </li>

        );

      }

hubs/static/client/app/components/StreamPage.js hubs/static/client/app/components/StreamsPage.js
file renamed
+13 -13
@@ -6,31 +6,31 @@ 

    FormattedMessage,

    } from 'react-intl';

  import PageStructure from './PageStructure';

- import StreamsHeader from './StreamsHeader';

- import Streams from './Streams';

+ import HubHeader from './HubHeader';

+ import WidgetsArea from './WidgetsArea';

+ import { fetchHub } from '../core/actions/hub';

  

  

- export default class StreamsPage extends React.Component {

+ class StreamPage extends React.Component {

+ 

+   componentDidMount() {

+     this.props.dispatch(fetchHub());

+   }

  

    render() {

      return (

        <PageStructure

          cssClass="StreamsPage"

          header={

-           <div className="row">

-             <div className="col-md-12">

-               <StreamsHeader />

-             </div>

-           </div>

+           <HubHeader />

          }

          content={

-           <div className="row">

-             <div className="col-md-12">

-               <Streams />

-             </div>

-           </div>

+           <WidgetsArea />

          }

          />

      );

    }

  }

+ 

+ 

+ export default connect()(StreamPage);

@@ -1,193 +0,0 @@ 

- import React from 'react';

- import { connect } from 'react-redux';

- import {

-   defineMessages,

-   FormattedMessage,

-   } from 'react-intl';

- import {

-   streamWillUpdate,

-   streamDidUpdate,

-   } from "../core/actions/stream";

- import SSESource from "./SSESource";

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

- import ItemsGetter from '../components/feed/ItemsGetter';

- import Feed from '../components/feed/Feed';

- 

- 

- class Streams extends React.Component {

- 

-   constructor(props) {

-     super(props);

-     this.state = {

-       notifItems: [],

-       savedItems: [],

-     };

-     this.handleStreamData = this.handleStreamData.bind(this);

-     this.handleStreamRequestStart = this.handleStreamRequestStart.bind(this);

-     this.handleSavedData = this.handleSavedData.bind(this);

-     this.handleSave = this.handleSave.bind(this);

-     this.handleUnsave = this.handleUnsave.bind(this);

-   }

- 

-   handleStreamData(data) {

-     this.setState({

-       notifItems: data

-     },

-       () => (

-         this.props.dispatch(streamDidUpdate())

-       )

-     );

-   }

- 

-   handleStreamRequestStart() {

-     this.props.dispatch(streamWillUpdate());

-   }

- 

-   handleSavedData(data) {

-     this.setState({savedItems: data});

-   }

- 

-   handleSave(item) {

-     if (!this.props.savedUrl) { return; }

-     const payload = {

-       link: item.link,

-       markup: item.markup,

-       secondary_icon: item.secondary_icon,

-       dom_id: item.dom_id,

-     };

-     $.ajax({

-       type: 'POST',

-       url: this.props.savedUrl,

-       data: JSON.stringify(payload),

-       contentType: 'application/json',

-       success: (data) => {

-         this.setState((prevState, props) => {

-           prevState.savedItems.push(data.data);

-           return {savedItems: prevState.savedItems};

-         });

-       },

-     });

-   }

- 

-   handleUnsave(item) {

-     if (!this.props.savedUrl) { return; }

-     var updateSavedItems = (item) => {

-       this.setState((prevState, props) => {

-         var items = prevState.savedItems.filter(

-           (currentItem) => (currentItem.dom_id !== item.dom_id)

-         );

-         return {savedItems: items};

-       });

-     };

-     if (item.idx) {

-       // Already saved

-       $.ajax({

-         type: 'DELETE',

-         url: `${this.props.savedUrl}${item.idx}/`,

-         success: () => { updateSavedItems(item) },

-       });

-     } else {

-       updateSavedItems(item);

-     }

-   }

- 

-   render() {

-     // Add saved state.

-     var savedItemsIds = this.state.savedItems.map((item) => {

-       return item.dom_id;

-     });

-     var notifs = this.state.notifItems.map((item) => {

-       item.saved = (savedItemsIds.indexOf(item.dom_id) !== -1);

-       return item;

-     });

- 

-     return (

-       <div className="Streams">

-         <TabSet tabListClass="mb-3">

-           <FeedPanel

-             username={this.props.username}

-             tabTitle="My Stream"

-             >

-             <ItemsGetter

-               url={this.props.notificationsUrl}

-               useSSE={true}

-               handleData={this.handleStreamData}

-               needsUpdate={false}

-               >

-               <Feed

-                 items={notifs}

-                 handleSave={this.handleSave}

-                 handleUnsave={this.handleUnsave}

-                 />

-             </ItemsGetter>

-           </FeedPanel>

-           <FeedPanel

-             username={this.props.username}

-             tabTitle="My Actions"

-             >

-             <Feed

-               items={this.state.notifItems}

-               />

-           </FeedPanel>

-           <FeedPanel

-             username={this.props.username}

-             tabTitle="Saved Notifications"

-             >

-             <ItemsGetter

-               url={this.props.savedUrl}

-               handleData={this.handleSavedData}

-               useSSE={false}

-               >

-               <Feed

-                 items={this.state.savedItems}

-                 handleUnsave={this.handleUnsave}

-                 />

-             </ItemsGetter>

-           </FeedPanel>

-         </TabSet>

- 

-         <SSESource />

-       </div>

-     );

-   }

- }

- 

- 

- const mapStateToProps = (state) => {

-   return {

-     savedUrl: state.urls.saved,

-     notificationsUrl: state.urls.notifications,

-     username: state.currentUser.nickname,

-   }

- };

- 

- export default connect(mapStateToProps)(Streams);

- 

- 

- 

- class FeedPanel extends React.Component {

- 

-   render() {

-     const filters_url = `https://apps.fedoraproject.org/notifications/${this.props.username}.id.fedoraproject.org/`;

- 

-     return (

-       <div>

-         <div className="input-group">

-           <input

-             type="search" className="form-control"

-             placeholder="Search this activity stream..."

-             aria-describedby="searchform-addon" />

-           <span className="input-group-btn">

-             <button className="btn btn-secondary"><i className="fa fa-search" aria-hidden="true"></i></button>

-           </span>

-         </div>

-         <br/>

-         <div className="alert alert-warning" role="alert">

-           This stream is filtered. <a href={filters_url} target="_blank">View Filters</a>

-         </div>

-         {this.props.children}

-       </div>

-     );

-   }

- 

- }

@@ -5,7 +5,7 @@ 

  

    render() {

      var buttonProps = {

-       id: `save-${this.props.item.dom_id}`,

+       id: `save-${this.props.item.notif_id}`,

        className: "btn btn-sm ",

      };

      var buttonText;

@@ -19,10 +19,10 @@ 

  export default class Feed extends React.Component {

  

    render() {

-     var items = this.props.items || [];

+     let items = this.props.items || [];

      items = items.map((item, idx) => {

        return (

-         <Panel item={item} {...this.props} key={item.dom_id} />

+         <Panel item={item} {...this.props} key={item.notif_id} />

          );

      });

      return (

@@ -44,29 +44,35 @@ 

    }

  

    loadFromServer() {

-     if (this.props.onRequestStart) {

-       this.props.onRequestStart();

-     }

-     this.setState({isLoading: true});

      if (this.serverRequest &&

          this.serverRequest.readyState !== XMLHttpRequest.DONE) {

        this.serverRequest.abort();

      }

-     this.serverRequest = $.ajax({

-       url: this.props.url,

-       method: 'GET',

-       dataType: 'json',

-       cache: false,

-       success: (data, textStatus, jqXHR) => {

-         this.props.handleData(data.data);

-       },

-       error: (xhr, status, err) => {

-         console.error(status, err.toString());

-       },

-       complete: (xhr, status) => {

-         this.setState({isLoading: false});

-       },

+     const promise = new Promise((resolve, reject) => {

+       this.setState({isLoading: true});

+       this.serverRequest = $.ajax({

+         url: this.props.url,

+         method: 'GET',

+         dataType: 'json',

+         cache: false,

+         success: (data, textStatus, jqXHR) => {

+           this.props.handleData(data.data);

+           resolve(data.data);

+         },

+         error: (xhr, status, err) => {

+           const errText = err.toString();

+           console.error(status, errText);

+           reject(errText);

+         },

+         complete: (xhr, status) => {

+           this.setState({isLoading: false});

+         },

+       });

      });

+     if (this.props.onRequestStart) {

+       this.props.onRequestStart(promise);

+     }

+     return promise;

    }

  

    showErrorMessage() {
@@ -104,6 +110,8 @@ 

  ItemsGetter.propTypes = {

    useSSE: PropTypes.bool,

    needsUpdate: PropTypes.bool,

+   onRequestStart: PropTypes.func,

+   handleData: PropTypes.func,

  }

  ItemsGetter.defaultProps = {

    useSSE: false,

@@ -18,7 +18,7 @@ 

    }

    var items = [item, item, item];

    items = items.map((obj, idx) => {

-     return Object.assign({dom_id: "item-" + idx}, obj);

+     return Object.assign({notif_id: "item-" + idx}, obj);

    });

  

    it('should create the children', () => {

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

    "Loading...",

    "Sorry, there was a problem loading the page."

  );

- const Streams = makeLoadable(

-   () => import(/* webpackChunkName: "page-streams" */ '../components/StreamsPage'),

+ const Stream = makeLoadable(

+   () => import(/* webpackChunkName: "page-stream" */ '../components/StreamPage'),

    "Loading...",

    "Sorry, there was a problem loading the page."

  );
@@ -20,4 +20,4 @@ 

    "Sorry, there was a problem loading the page."

  );

  

- export {Hub, Streams, Groups};

+ export {Hub, Stream, Groups};

@@ -0,0 +1,54 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import WidgetChrome from '../../components/WidgetChrome';

+ import ItemsGetter from '../../components/feed/ItemsGetter';

+ import Feed from '../../components/feed/Feed';

+ 

+ 

+ export default class ActionsFeed extends React.Component {

+ 

+   constructor(props) {

+     super(props);

+     this.state = {

+       items: [],

+       loaded: false,

+     };

+     this.handleServerData = this.handleServerData.bind(this);

+   }

+ 

+   handleServerData(data) {

+     this.setState({

+       items: data,

+       loaded: true,

+     },

+       this.props.onServerRequestStop

+     );

+   }

+ 

+   render() {

+     return (

+       <WidgetChrome

+         widget={this.props.widget}

+         editMode={this.props.editMode}

+         >

+         <ItemsGetter

+           url={this.props.widget.url_existing}

+           useSSE={true}

+           needsUpdate={this.props.needsUpdate}

+           handleData={this.handleServerData}

+           onRequestStart={this.props.onServerRequestStart}

+           >

+           <Feed

+             items={this.state.items}

+             loaded={this.state.loaded}

+             />

+         </ItemsGetter>

+       </WidgetChrome>

+     );

+   }

+ 

+ }

+ ActionsFeed.propTypes = {

+   widget: PropTypes.object.isRequired,

+   needsUpdate: PropTypes.bool,

+ }

@@ -0,0 +1,255 @@ 

+ import React from 'react';

+ import PropTypes from 'prop-types';

+ import {

+   defineMessages,

+   FormattedMessage,

+   } from 'react-intl';

+ import { apiCall } from '../../core/utils';

+ import TabSet from '../../components/TabSet';

+ import ItemsGetter from '../../components/feed/ItemsGetter';

+ import Feed from '../../components/feed/Feed';

+ 

+ 

+ export default class StreamFeed extends React.Component {

+ 

+   constructor(props) {

+     super(props);

+     this.state = {

+       notifItems: [],

+       //notifItemsById: {},

+       actionItems: [],

+       savedItems: [],

+       //savedItemsById: {},

+       savedItemsNeedUpdate: false,

+     };

+     this.handleStreamData = this.handleStreamData.bind(this);

+     this.handleActionData = this.handleActionData.bind(this);

+     this.handleSavedData = this.handleSavedData.bind(this);

+     this.handleSave = this.handleSave.bind(this);

+     this.handleUnsave = this.handleUnsave.bind(this);

+     this.handleStreamServerRequestStart = this.handleStreamServerRequestStart.bind(this);

+     this.handleActionServerRequestStart = this.handleActionServerRequestStart.bind(this);

+     this.handleServerRequestStart = this.handleServerRequestStart.bind(this);

+     this.makeServerRequestCompletePromise = this.makeServerRequestCompletePromise.bind(this);

+     this.serverRequestCompletePromises = [];

+   }

+ 

+   makeServerRequestCompletePromise() {

+     // Only call this.props.onServerRequestStop when both server requests

+     // are done, otherwise the first to reload will cancel the other's reload.

+     const promise = Promise.all([

+       ...this.serverRequestCompletePromises

+     ]).finally(

+       () => {

+         this.props.onServerRequestStop();

+       }

+     );

+     this.serverRequestCompletePromises = [];

+   }

+ 

+   handleStreamServerRequestStart(promise) {

+     this.serverRequestCompletePromises.push(promise);

+     this.handleServerRequestStart();

+   }

+   handleActionServerRequestStart(promise) {

+     this.serverRequestCompletePromises.push(promise);

+     this.handleServerRequestStart();

+   }

+   handleServerRequestStart() {

+     if (this.serverRequestCompletePromises.length >= 2) {

+       // Both requests have started, make the envelope promise that will fire

+       // when they both resolve.

+       this.props.onServerRequestStart()

+       this.makeServerRequestCompletePromise();

+     }

+   }

+ 

+   handleStreamData(data) {

+     this.setState({

+       notifItems: data

+     });

+   }

+ 

+   handleActionData(data) {

+     this.setState({

+       actionItems: data,

+     });

+   }

+ 

+   handleSavedData(data) {

+     this.setState({

+       savedItems: data,

+       savedItemsNeedUpdate: false,

+     });

+   }

+ 

+   handleSave(item) {

+     if (!this.props.widget.url_saved) { return; }

+     const newItem = {

+       link: item.link,

+       markup: item.markup,

+       icon: item.secondary_icon,

+       notif_id: item.notif_id,

+       timestamp: item.end_time || item.timestamp,

+     };

+     // Optimistic update

+     this.setState((prevState, props) => ({

+         savedItems: [

+           ...prevState.savedItems,

+           newItem,

+         ],

+       })

+     );

+     // Backend call

+     const body = JSON.stringify(newItem);

+     apiCall(this.props.widget.url_saved, {method: "POST", body}).then(

+       (result) => {

+         this.setState((prevState, props) => ({

+             //savedItems: [

+             //  ...prevState.savedItems,

+             //  result,

+             //],

+             savedItemsNeedUpdate: true,

+           })

+         );

+       },

+       (error) => {

+         console.log(error);

+         this.setState({

+           savedItemsNeedUpdate: true,

+         });

+       }

+     );

+   }

+ 

+   handleUnsave(item) {

+     if (!this.props.widget.url_saved) { return; }

+     // Optimistic update

+     this.setState((prevState, props) => {

+       const items = prevState.savedItems.filter(

+         (currentItem) => (currentItem.notif_id !== item.notif_id)

+       );

+       return {savedItems: items};

+     });

+     // Backend call

+     apiCall(`${this.props.widget.url_saved}${item.notif_id}`, {method: "DELETE"}).then(

+       (result) => {

+         this.setState((prevState, props) => ({

+           savedItemsNeedUpdate: true,

+           })

+         );

+       },

+       (error) => {

+         console.log(error);

+         this.setState({

+           savedItemsNeedUpdate: true,

+         });

+       }

+     );

+   }

+ 

+   render() {

+     // Add saved state.

+     const savedItemsIds = this.state.savedItems.map((item) => (item.notif_id));

+     const notifs = this.state.notifItems.map((item) => {

+       item.saved = (savedItemsIds.indexOf(item.notif_id) !== -1);

+       return item;

+     });

+     const actions = this.state.actionItems.map((item) => {

+       item.saved = (savedItemsIds.indexOf(item.notif_id) !== -1);

+       return item;

+     });

+     const username = this.props.currentUser.nickname;

+ 

+     return (

+       <div className="Streams">

+         <TabSet tabListClass="mb-3">

+           <FeedPanel

+             username={username}

+             tabTitle="My Stream"

+             >

+             <ItemsGetter

+               url={this.props.widget.url_existing}

+               useSSE={true}

+               handleData={this.handleStreamData}

+               needsUpdate={this.props.needsUpdate}

+               onRequestStart={this.handleStreamServerRequestStart}

+               >

+               <Feed

+                 items={notifs}

+                 handleSave={this.handleSave}

+                 handleUnsave={this.handleUnsave}

+                 />

+             </ItemsGetter>

+           </FeedPanel>

+           { this.props.widget.url_actions &&

+             <FeedPanel

+               username={username}

+               tabTitle="My Actions"

+               >

+               <ItemsGetter

+                 url={this.props.widget.url_actions}

+                 handleData={this.handleActionData}

+                 useSSE={true}

+                 needsUpdate={this.props.needsUpdate}

+                 onRequestStart={this.handleActionServerRequestStart}

+                 >

+                 <Feed

+                   items={actions}

+                   handleSave={this.handleSave}

+                   handleUnsave={this.handleUnsave}

+                   />

+               </ItemsGetter>

+             </FeedPanel>

+           }

+           <FeedPanel

+             username={username}

+             tabTitle="Saved Notifications"

+             >

+             <ItemsGetter

+               url={this.props.widget.url_saved}

+               handleData={this.handleSavedData}

+               useSSE={false}

+               needsUpdate={this.state.savedItemsNeedUpdate}

+               >

+               <Feed

+                 items={this.state.savedItems}

+                 handleUnsave={this.handleUnsave}

+                 />

+             </ItemsGetter>

+           </FeedPanel>

+         </TabSet>

+       </div>

+     );

+   }

+ }

+ 

+ 

+ class FeedPanel extends React.Component {

+ 

+   render() {

+     const filters_url = `https://apps.fedoraproject.org/notifications/${this.props.username}.id.fedoraproject.org/`;

+ 

+     return (

+       <div>

+         { /*

+         <div className="input-group">

+           <input

+             type="search" className="form-control"

+             placeholder="Search this activity stream..."

+             aria-describedby="searchform-addon" />

+           <span className="input-group-btn">

+             <button className="btn btn-secondary"><i className="fa fa-search" aria-hidden="true"></i></button>

+           </span>

+         </div>

+         <br/>

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

+           This stream is filtered. <a href={filters_url} target="_blank">View Filters</a>

+         </div>

+         */ }

+         {this.props.children}

+       </div>

+     );

+   }

+ 

+ }

@@ -1,66 +1,54 @@ 

  import React from 'react';

  import PropTypes from 'prop-types';

+ import { connect } from 'react-redux';

  import {

    widgetDidUpdate,

    widgetWillUpdate

    } from "../../core/actions/widget";

- import ItemsGetter from '../../components/feed/ItemsGetter';

- import Feed from '../../components/feed/Feed';

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

+ import ActionsFeed from "./ActionsFeed";

+ import StreamFeed from "./StreamFeed";

  

  

- export default class FeedWidget extends React.PureComponent {

+ class FeedWidget extends React.PureComponent {

  

    constructor(props) {

      super(props);

-     this.state = {

-       items: [],

-       loaded: false,

-     };

-     this.handleServerData = this.handleServerData.bind(this);

      this.handleServerRequestStart = this.handleServerRequestStart.bind(this);

-   }

- 

-   handleServerData(data) {

-     this.setState({

-       items: data,

-       loaded: true,

-     },

-       () => (

-         this.props.dispatch(widgetDidUpdate(this.props.widget.idx))

-       )

-     );

+     this.handleServerRequestStop = this.handleServerRequestStop.bind(this);

    }

  

    handleServerRequestStart() {

      this.props.dispatch(widgetWillUpdate(this.props.widget.idx));

    }

  

+   handleServerRequestStop() {

+     this.props.dispatch(widgetDidUpdate(this.props.widget.idx));

+   }

+ 

    render() {

+     const FeedComponent = this.props.hub.type === "stream" ? StreamFeed : ActionsFeed;

      return (

-       <WidgetChrome

-         widget={this.props.widget}

-         editMode={this.props.editMode}

-         >

-         <ItemsGetter

-           url={this.props.widget.url}

-           useSSE={true}

-           needsUpdate={this.props.needsUpdate}

-           handleData={this.handleServerData}

-           onRequestStart={this.handleServerRequestStart}

-           >

-           <Feed

-             items={this.state.items}

-             loaded={this.state.loaded}

-             />

-         </ItemsGetter>

-       </WidgetChrome>

+       <FeedComponent

+         {...this.props}

+         onServerRequestStart={this.handleServerRequestStart}

+         onServerRequestStop={this.handleServerRequestStop}

+         />

      );

    }

  

  }

  FeedWidget.propTypes = {

+   hub: PropTypes.object.isRequired,

    widget: PropTypes.object.isRequired,

    editMode: PropTypes.bool,

    needsUpdate: PropTypes.bool,

  }

+ 

+ const mapStateToProps = (state) => {

+   return {

+     hub: state.entities.hub,

+     currentUser: state.currentUser,

+   }

+ };

+ 

+ export default connect(mapStateToProps)(FeedWidget);

file modified
+4 -4
@@ -38,16 +38,16 @@ 

                  if (datum.config.avatar){

                      output = output+'<img width="60px" height="60px" src="'+datum.config.avatar+'"/>'

                  } else {

-                     output = output+'<div class="monogram-avatar bg-fedora-'+datum.monogram_colour+' text-fedora-'+datum.monogram_colour+'-dark">'+datum.name.charAt(0).toUpperCase()+'</div>'                   

+                     output = output+'<div class="monogram-avatar bg-fedora-'+datum.monogram_colour+' text-fedora-'+datum.monogram_colour+'-dark">'+datum.name.charAt(0).toUpperCase()+'</div>'

                  }

                  output = output +'<div class="media-body pl-3">'

                  output = output +'<div><strong>'+datum.name+'</strong></div>'

                  output = output +'<div>'+datum.config.summary+'</div>'

                  output = output +'<div>'

-                 if (datum.user_hub){

+                 if (datum.type === "user"){

                      output = output +'<span class="badge badge-primary">User</span>'

                  } else {

-                     output = output +'<span class="badge badge-info">Group</span>'                    

+                     output = output +'<span class="badge badge-info">Group</span>'

                  }

                  output = output +'</div>'

                  output = output +'</div>'
@@ -61,4 +61,4 @@ 

      $('#bloodhound input.typeahead').on('typeahead:selected', function (e, datum) {

          window.location.href = datum.hub_url;

      });

- }); 

\ No newline at end of file

+ });

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

        <ul class="nav nav-pills flex-lg-column mb-3 rounded" id="vertical-navbar" role="navigation">

          <li class="nav-item">

            <a class="nav-link {% if request.path.endswith('/' + g.user.username + '/') %}active{% endif %}"

-              href="{{ url_for("hub", name=g.user.username) }}">

+              href="{{ url_for("hub", hub_name=g.user.username, hub_type="u") }}">

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

              My Hub

            </a>
@@ -138,8 +138,8 @@ 

            </li>

          {% endif %}

          {% for hub in g.user.bookmarks["starred"] %}

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

-             <a class="nav-link" href="{{ url_for("hub", name=hub.name) }}">

+           <li class='nav-item idle-{{hub.activity_class}}{% if request.path == hub.url %} active{% endif %}'>

+             <a class="nav-link" href="{{ hub.url }}">

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

                {{hub.name}}

              </a>
@@ -151,8 +151,8 @@ 

            </li>

          {% endif %}

          {% for hub in g.user.bookmarks["memberships"] %}

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

-             <a class="nav-link" href="{{ url_for("hub", name=hub.name) }}">

+           <li class='nav-item idle-{{hub.activity_class}}{% if request.path == hub.url %} active{% endif %}'>

+             <a class="nav-link" href="{{ hub.url }}">

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

                {{hub.name}}

              </a>
@@ -165,7 +165,7 @@ 

          {% endif %}

          {% for hub in g.user.bookmarks["subscriptions"] %}

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

-             <a class="nav-link" href="{{ url_for("hub", name=hub.name) }}">

+             <a class="nav-link" href="{{ hub.url }}">

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

                {{hub.name}}

              </a>

file modified
+9 -5
@@ -61,13 +61,14 @@ 

              hubs.models.User.get_or_create(

                  username=user, fullname=fullname)

              saved_notif = hubs.models.SavedNotification(

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

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

+                 timestamp=123456789,

              )

              self.session.add(saved_notif)

  

          self.session.flush()

  

-         hub = hubs.models.Hub.by_name('ralph')

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

          widget = hubs.models.Widget(

              plugin='about',

              index=500,
@@ -76,7 +77,7 @@ 

          hub.widgets.append(widget)

  

          for team in ['i18n', 'infra', 'old']:

-             hub = hubs.models.Hub(name=team)

+             hub = hubs.models.Hub(name=team, hub_type="team")

              if hub.name == 'old':

                  hub.last_refreshed = datetime.utcnow() - timedelta(days=100)

                  hub.last_updated = datetime.utcnow() - timedelta(days=200)
@@ -115,8 +116,11 @@ 

  

  

  def widget_instance(hubname, plugin):

-     hub = hubs.models.Hub.by_name(hubname)

-     if not hub:

+     for hub_type in hubs.models.constants.HUB_TYPES:

+         hub = hubs.models.Hub.by_name(hubname, hub_type)

+         if hub is not None:

+             break

+     else:

          raise ValueError("No such hub %r" % hubname)

      for widget in hub.widgets:

          if widget.plugin == plugin:

@@ -14,7 +14,7 @@ 

  

      def test_get_widgets(self):

          # Get all widgets except those whose module isn't installed.

-         hub = Hub.by_name('ralph')

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

          widget = Widget(

              plugin='non-existant',

              index=500,
@@ -23,4 +23,4 @@ 

          hub.widgets.append(widget)

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

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

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

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

file modified
+11 -11
@@ -11,7 +11,7 @@ 

      def test_delete_hubs(self):

          # verify the hub exists

          hub_name = 'ralph'

-         hub = hubs.models.Hub.get(hub_name)

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

          self.assertIsNotNone(hub)

  

          # check if association exists
@@ -21,7 +21,7 @@ 

          self.assertIsNotNone(assoc)

  

          # check if widgets exist

-         widgets = hubs.models.Widget.by_hub_id_all(hub.name)

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

          self.assertEqual(11, len(widgets))

  

          # delete the hub
@@ -32,11 +32,11 @@ 

          self.assertIsNone(assoc)

  

          # check if widgets are removed

-         widgets = hubs.models.Widget.by_hub_id_all(hub.name)

-         self.assertEqual([], widgets)

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

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

  

          # verify hub is deleted

-         hub = hubs.models.Hub.get(hub_name)

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

          self.assertIsNone(hub)

  

          # check if user is still intact
@@ -47,10 +47,10 @@ 

      def test_auth_hub_widget_access_level(self):

          username = 'ralph'

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

-         hub_ralph = hubs.models.Hub.get(username)

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

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

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

-         hub_decause = hubs.models.Hub.get("decause")

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

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

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

          self.assertEqual(
@@ -68,14 +68,14 @@ 

  

      def test_auth_hub_auth_group(self):

          username = "ralph"

-         hub = hubs.models.Hub.get(username)

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

          self.assertEqual(hub._get_auth_group(), username)

          # Uncomment this when CAIAPI is active.

          # hub.config["auth_group"] = "testing"

          # self.assertEqual(hub._get_auth_group(), "testing")

  

      def test_auth_hub_permission_name(self):

-         hub = hubs.models.Hub.get("ralph")

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

          self.assertEqual(

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

          self.assertEqual(
@@ -92,7 +92,7 @@ 

      def test_auth_hub_widget_user_roles(self):

          username = "ralph"

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

-         hub = hubs.models.Hub.get(username)

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

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

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

          assert len(hub.associations) == 1
@@ -106,7 +106,7 @@ 

  

      def test_unsubscribe_owner(self):

          # Owners must be turned into regular members when unsubscribed.

-         hub = hubs.models.Hub.query.get("infra")

+         hub = hubs.models.Hub.by_name("infra", "team")

          ralph = hubs.models.User.query.get("ralph")

          self.session.add(hubs.models.Association(

              hub=hub, user=ralph, role='owner'))

@@ -12,7 +12,7 @@ 

      @patch.object(hubs.models.hubconfig.HubConfigProxy, "VALIDATORS",

                    {"pagure": lambda v: v})

      def test_validate_list(self):

-         hub = hubs.models.Hub.get("ralph")

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

          self.assertEqual(

              hub.config.validate({"pagure": ["testrepo"]}),

              {"pagure": ["testrepo"]}

@@ -14,7 +14,7 @@ 

          self.assertIsNotNone(user)

  

          # check if association exists

-         hub = hubs.models.Hub.get(username)

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

          assoc = hubs.models.Association.get(hub, user, 'owner')

          self.assertIsNotNone(assoc)

  
@@ -24,19 +24,19 @@ 

          self.assertIsNone(user)

  

          # checking to see if the hub is still intact

-         hub = hubs.models.Hub.get(username)

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

          self.assertIsNotNone(hub)

          self.assertEqual('ralph', hub.name)

  

          # check if widgets still are intact

-         widgets = hubs.models.Widget.by_hub_id_all(hub.name)

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

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

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

  

  

  class BookmarksTest(hubs.tests.APPTest):

  

      def _add_assoc(self, hubname, username, role):

-         hub = hubs.models.Hub.query.get(hubname)

+         hub = hubs.models.Hub.by_name(hubname, "team")

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

          self.session.add(hubs.models.Association(

              hub=hub, user=user, role=role))
@@ -47,7 +47,7 @@ 

          when adding bookmarks for different hubs, we should

          see them all in the result.

          """

-         commops = hubs.models.Hub(name="commops")

+         commops = hubs.models.Hub(name="commops", hub_type="team")

          self.session.add(commops)

          self._add_assoc("infra", "ralph", "stargazer")

          self._add_assoc("i18n", "ralph", "member")

@@ -8,7 +8,7 @@ 

  class WidgetTest(hubs.tests.APPTest):

  

      def test_auth_widget_permission_name(self):

-         hub = hubs.models.Hub.get("ralph")

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

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

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

          self.assertEqual(
@@ -26,7 +26,7 @@ 

              widget._get_auth_permission_name("view"), "widget.restricted.view")

  

      def test_widget_enabled(self):

-         hub = hubs.models.Hub.get("ralph")

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

          widget = hubs.models.Widget(hub=hub, plugin="does-not-exist")

          self.session.add(widget)

          self.assertFalse(widget.enabled)

file modified
+43 -36
@@ -8,7 +8,7 @@ 

  from hubs.app import app

  from hubs.models import User, Hub, Association

  from hubs.feed import (

-     Notifications, Activity, add_dom_id, format_msgs, get_hubs_for_msg,

+     Notifications, Activity, add_notif_id, format_msgs, get_hubs_for_msg,

      on_new_message, on_new_notification)

  from hubs.tests import APPTest

  
@@ -34,8 +34,8 @@ 

              database.collection_names.return_value = []

              database.create_collection.return_value = db = object()

              client.__getitem__.return_value = database

-             db_name = "feed|%s|testuser" % msgtype

-             feed = feed_class("testuser")

+             db_name = "feed|%s|42" % msgtype

+             feed = feed_class(42)

              feed.connect()

              pymongo_mock.MongoClient.assert_called_with(None)

              client.__getitem__.assert_called_with("hubs")
@@ -46,7 +46,7 @@ 

  

      def test_get(self):

          for feed_class, msgtype in self.feed_classes:

-             feed = feed_class("testuser")

+             feed = feed_class(42)

              feed.db = Mock()

              feed.db.find.return_value = []

              feed.get()
@@ -61,7 +61,7 @@ 

              "testkey": "testvalue",

              }

          for feed_class, msgtype in self.feed_classes:

-             feed = feed_class("testuser")

+             feed = feed_class(42)

              feed.db = Mock()

              feed.add(msg)

              feed.db.insert_one.assert_called_once()
@@ -74,15 +74,15 @@ 

  

      def test_length(self):

          for feed_class, msgtype in self.feed_classes:

-             feed = feed_class("testuser")

+             feed = feed_class(42)

              feed.db = Mock()

-             feed.db.count.return_value = 42

-             self.assertEqual(feed.length(), 42)

+             feed.db.count.return_value = 4200

+             self.assertEqual(feed.length(), 4200)

              feed.db.count.assert_called_once_with()

  

      def test_close(self):

          for feed_class, msgtype in self.feed_classes:

-             feed = feed_class("testuser")

+             feed = feed_class(42)

              db_mock = feed.db = Mock()

              feed.close()

              db_mock.database.client.close.assert_called_once()
@@ -90,12 +90,12 @@ 

  

      @patch("hubs.feed.Activity")

      def test_on_new_message(self, mock_activity):

-         msg = {"_hubs": ["testhub1", "testhub2"]}

+         msg = {"_hubs": [42, 43]}

          feed = Mock()

          mock_activity.return_value = feed

          on_new_message(msg)

          call_args = [c[0] for c in mock_activity.call_args_list]

-         self.assertEqual(call_args, [("testhub1", ), ("testhub2", )])

+         self.assertEqual(call_args, [(42, ), (43, )])

          self.assertEqual(feed.add.call_count, 2)

          feed.add.assert_called_with(msg)

  
@@ -108,7 +108,8 @@ 

              msg2agent.return_value = "ralph"

              on_new_notification(msg)

          msg2agent.assert_called()

-         mock_notifications.assert_called_with("ralph")

+         stream = Hub.by_name("ralph", "stream")

+         mock_notifications.assert_called_with(stream.id)

          feed.add.assert_called_with(msg)

  

      @patch("hubs.feed.Notifications")
@@ -120,7 +121,7 @@ 

          # User does not exist, no feed instance should have been created.

          mock_notifications.assert_not_called()

  

-     def test_add_dom_id(self):

+     def test_add_notif_id(self):

          msg = {

              "msg_ids": {

                  "testid1": {"msg_id": "testid1"},
@@ -132,9 +133,9 @@ 

              "subjective": "your ticket was commented by decause",

          }

          with app.test_request_context():

-             result = add_dom_id(msg)

+             result = add_notif_id(msg)

          self.assertEqual(

-             result["dom_id"],

+             result["notif_id"],

              "067306d0b091ec48d9e2dded1ca9b1f8b1b2ac93")

  

      def test_format_msgs(self):
@@ -147,12 +148,12 @@ 

              result = format_msgs([msg])[0]

          self.assertEqual(

              result["markup"],

-             """<a href="/ralph/">ralph</a>'s ticket was commented by """

-             """<a href="/decause/">decause</a>"""

+             """<a href="/u/ralph/">ralph</a>'s ticket was commented by """

+             """<a href="/u/decause/">decause</a>"""

              )

          self.assertEqual(

              result["markup_subjective"],

-             """your ticket was commented by <a href="/decause/">decause</a>"""

+             "your ticket was commented by <a href=\"/u/decause/\">decause</a>"

              )

  

  
@@ -177,33 +178,39 @@ 

          self.assertListEqual(get_hubs_for_msg(self.dummy_msg), [])

  

      def test_user_hub_owner(self):

+         # Ralph's actions don't necessarily end up in its stream.

          self.msg2usernames.return_value = ["ralph"]

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

+         # stream = Hub.by_name("ralph", "stream")

          self.assertListEqual(

-             get_hubs_for_msg(self.dummy_msg), ["ralph"])

+             get_hubs_for_msg(self.dummy_msg),

+             # [hub.id, stream.id]

+             [hub.id]

+             )

  

      def test_group_hub_owner(self):

          # Don't send a message to a group hub just because the user is the

          # owner.

          ralph = User.query.get("ralph")

-         infra = Hub.query.get("infra")

+         infra = Hub.by_name("infra", "team")

          self.session.add(Association(hub=infra, user=ralph, role="owner"))

          self.msg2usernames.return_value = ["ralph"]

-         self.assertListEqual(

-             get_hubs_for_msg(self.dummy_msg), ["ralph"])

+         self.assertNotIn(infra.id, get_hubs_for_msg(self.dummy_msg))

  

      def test_group_hub_member(self):

+         # Don't send a message to a group hub just because the user is a

+         # member.

          ralph = User.query.get("ralph")

-         test_hub = Hub(name="testhub", user_hub=False)

+         test_hub = Hub(name="testhub", hub_type="team")

          self.session.add(test_hub)

          self.session.add(Association(hub=test_hub, user=ralph, role="member"))

          self.msg2usernames.return_value = ["ralph"]

-         self.assertListEqual(

-             get_hubs_for_msg(self.dummy_msg), ["ralph"])

+         self.assertNotIn(test_hub.id, get_hubs_for_msg(self.dummy_msg))

  

      def test_group_hub_irc(self):

-         test_hub = Hub(name="testhub", user_hub=False)

+         test_hub = Hub(name="testhub", hub_type="team")

          self.session.add(test_hub)

-         test_hub.config["chat_network"] = "irc.freenode.net"

+         test_hub.config["chat_domain"] = "irc.freenode.net"

          test_hub.config["chat_channel"] = "testchannel"

          messages = [{

              "msg_id": "testmsg",
@@ -237,10 +244,10 @@ 

                  },

              }]

          for msg in messages:

-             self.assertListEqual(get_hubs_for_msg(msg), ["testhub"])

+             self.assertListEqual(get_hubs_for_msg(msg), [test_hub.id])

  

      def test_group_hub_mailinglist(self):

-         test_hub = Hub(name="testhub", user_hub=False)

+         test_hub = Hub(name="testhub", hub_type="team")

          self.session.add(test_hub)

          test_hub.config["mailing_list"] = "testlist@lists.fpo"

          msg = {
@@ -250,10 +257,10 @@ 

                  "mlist": {"list_name": "testlist"},

              },

          }

-         self.assertListEqual(get_hubs_for_msg(msg), ["testhub"])

+         self.assertListEqual(get_hubs_for_msg(msg), [test_hub.id])

  

      def test_group_hub_calendar(self):

-         test_hub = Hub(name="testhub", user_hub=False)

+         test_hub = Hub(name="testhub", hub_type="team")

          self.session.add(test_hub)

          test_hub.config["calendar"] = "testcal"

          topics = [
@@ -269,10 +276,10 @@ 

                      "calendar": {"calendar_name": "testcal"},

                      },

                  }

-             self.assertListEqual(get_hubs_for_msg(msg), ["testhub"])

+             self.assertListEqual(get_hubs_for_msg(msg), [test_hub.id])

  

      def test_group_hub_pagure(self):

-         test_hub = Hub(name="testhub", user_hub=False)

+         test_hub = Hub(name="testhub", hub_type="team")

          self.session.add(test_hub)

          projects_ok = [

              "testproject-1",
@@ -312,7 +319,7 @@ 

              }

              self.assertListEqual(get_hubs_for_msg(msg), expected)

          for project in projects_ok:

-             _do_test_project(project, ["testhub"])

+             _do_test_project(project, [test_hub.id])

          for project in projects_fail:

              _do_test_project(project, [])

          # Handle new and/or differently formatted messages
@@ -324,7 +331,7 @@ 

          self.assertListEqual(get_hubs_for_msg(msg), [])

  

      def test_group_hub_github(self):

-         test_hub = Hub(name="testhub", user_hub=False)

+         test_hub = Hub(name="testhub", hub_type="team")

          self.session.add(test_hub)

          projects_ok = [

              "testgroup/testproject-1",
@@ -346,7 +353,7 @@ 

              }

              self.assertListEqual(get_hubs_for_msg(msg), expected)

          for project in projects_ok:

-             _do_test_project(project, ["testhub"])

+             _do_test_project(project, [test_hub.id])

          for project in projects_fail:

              _do_test_project(project, [])

          msg = {

@@ -4,6 +4,7 @@ 

  from mock import Mock

  

  from hubs.app import app

+ from hubs.models.constants import HUB_TYPES

  from hubs.tests import APPTest, widget_instance

  from hubs.widgets.base import Widget

  from hubs.widgets.caching import CachedFunction
@@ -24,6 +25,8 @@ 

  

  class WidgetTest(APPTest):

  

+     maxDiff = None

+ 

      def setUp(self):

          super(WidgetTest, self).setUp()

          # Backup the URL map
@@ -139,14 +142,14 @@ 

          self.assertIn("testing_root", app.view_functions)

          rules = list(app.url_map.iter_rules(endpoint="testing_root"))

          self.assertEqual(len(rules), 2)

-         self.assertEqual(rules[0].rule, "/<hub>/w/testing/<int:idx>/")

-         self.assertEqual(rules[1].rule, "/<hub>/w/testing/<int:idx>/test-1/")

+         self.assertEqual(rules[0].rule, "/widgets/testing/<int:idx>/")

+         self.assertEqual(rules[1].rule, "/widgets/testing/<int:idx>/test-1/")

          # TestView2

          self.assertIn("testing_test2", app.url_map._rules_by_endpoint)

          self.assertIn("testing_test2", app.view_functions)

          rules = list(app.url_map.iter_rules(endpoint="testing_test2"))

          self.assertEqual(len(rules), 1)

-         self.assertEqual(rules[0].rule, "/<hub>/w/testing/<int:idx>/test-2")

+         self.assertEqual(rules[0].rule, "/widgets/testing/<int:idx>/test-2")

  

      def test_get_props(self):

          widget = widget_instance('ralph', "about")
@@ -154,10 +157,10 @@ 

              props = widget.get_props()

          self.assertDictEqual(props, {

              'config': {'text': 'Testing.'},

-             'contentUrl': '/ralph/w/about/{}/'.format(widget.idx),

+             'contentUrl': '/widgets/about/{}/'.format(widget.idx),

              'cssClass': None,

              'hiddenIfEmpty': False,

-             'hub_types': ['user', 'group'],

+             'hub_types': HUB_TYPES,

              'idx': widget.idx,

              'index': 500,

              'isReact': False,
@@ -174,6 +177,7 @@ 

                   },

              ],

              'position': 'right',

-             'selfUrl': '/api/hubs/ralph/widgets/{}/'.format(widget.idx),

+             'selfUrl': '/api/hubs/{}/widgets/{}/'.format(

+                 widget.hub.id, widget.idx),

              'title': 'About',

          })

@@ -63,7 +63,7 @@ 

                      "http://example.com/hub/ralph")

  

      def test_move_widget(self):

-         hub = Hub(name="testing")

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

          self.session.add(hub)

          widget_names = ["about", "badges", "bugzilla", "contact", "dummy"]

  
@@ -111,7 +111,7 @@ 

  

      def test_move_widget_disabled(self):

          # Make sure moving works even if there are disabled widgets

-         hub = Hub(name="testing")

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

          self.session.add(hub)

  

          def _make_widget(name, index):

@@ -12,66 +12,66 @@ 

      user = hubs.tests.FakeAuthorization('decause')

  

      def test_forbidden_when_logged_out(self):

-         hub = hubs.models.Hub.by_name('infra')

-         resp = self.app.post('/api/hubs/{}/subscribers'.format(hub.name))

+         hub = hubs.models.Hub.by_name('infra', "team")

+         resp = self.app.post('/api/hubs/{}/subscribers'.format(hub.id))

          self.assertEqual(resp.status_code, 403)

  

      def test_subscribe(self):

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

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

-             resp = self.app.post('/api/hubs/{}/subscribers'.format(hub.name))

+             resp = self.app.post('/api/hubs/{}/subscribers'.format(hub.id))

              self.assertEqual(resp.status_code, 200)

              # Need to find the Hub again to avoid DetachedInstanceError

-             h = hubs.models.Hub.by_name('infra')

+             h = hubs.models.Hub.by_name('infra', "team")

              self.assertIn(self.user.nickname, usernames(h.subscribers))

  

      def test_unsubscribe(self):

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

          # Need a real user model to subscribe to the hub

          User = hubs.models.User.by_username(self.user.nickname)

          hub.subscribe(User)

          self.assertIn(self.user.nickname, usernames(hub.subscribers))

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

-             resp = self.app.delete('/api/hubs/{}/subscribers'.format(hub.name))

+             resp = self.app.delete('/api/hubs/{}/subscribers'.format(hub.id))

              self.assertEqual(resp.status_code, 200)

-             h = hubs.models.Hub.by_name('infra')

+             h = hubs.models.Hub.by_name('infra', "team")

              self.assertNotIn(self.user.nickname, usernames(h.subscribers))

  

      def test_star(self):

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

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

-             resp = self.app.post('/api/hubs/{}/stargazers'.format(hub.name))

+             resp = self.app.post('/api/hubs/{}/stargazers'.format(hub.id))

              self.assertEqual(resp.status_code, 200)

-             h = hubs.models.Hub.by_name('infra')

+             h = hubs.models.Hub.by_name('infra', "team")

              self.assertIn(self.user.nickname, usernames(h.stargazers))

  

      def test_unstar(self):

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

          # Need a real user model to subscribe to the hub

          User = hubs.models.User.by_username(self.user.nickname)

          hub.subscribe(User, role='stargazer')

          self.assertIn(self.user.nickname, [u.username for u in hub.stargazers])

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

-             resp = self.app.delete('/api/hubs/{}/stargazers'.format(hub.name))

+             resp = self.app.delete('/api/hubs/{}/stargazers'.format(hub.id))

              self.assertEqual(resp.status_code, 200)

-             h = hubs.models.Hub.by_name('infra')

+             h = hubs.models.Hub.by_name('infra', "team")

              self.assertNotIn(self.user.nickname, usernames(h.stargazers))

  

      def test_join(self):

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

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

-             resp = self.app.post('/api/hubs/{}/members'.format(hub.name))

+             resp = self.app.post('/api/hubs/{}/members'.format(hub.id))

              self.assertEqual(resp.status_code, 200)

-             h = hubs.models.Hub.by_name('infra')

+             h = hubs.models.Hub.by_name('infra', "team")

              self.assertIn(self.user.nickname, usernames(h.members))

  

      def test_leave(self):

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

          # Need a real user model to subscribe to the hub

          User = hubs.models.User.by_username(self.user.nickname)

          hub.subscribe(User, role='member')

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

-             resp = self.app.delete('/api/hubs/{}/members'.format(hub.name))

+             resp = self.app.delete('/api/hubs/{}/members'.format(hub.id))

              self.assertEqual(resp.status_code, 200)

-             h = hubs.models.Hub.by_name('infra')

+             h = hubs.models.Hub.by_name('infra', "team")

              self.assertNotIn(self.user.nickname, usernames(h.members))

@@ -14,20 +14,22 @@ 

      maxDiff = None

  

      def test_get(self):

-         hub = Hub.by_name('ralph')

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

          hub.last_refreshed = datetime.datetime(2017, 1, 1, 0, 0, 0)

          user = User.query.get("ralph")

          user.created_on = datetime.datetime(2017, 1, 1, 0, 0, 0)

          self.session.commit()

          auth_user = FakeAuthorization('ralph')

-         response = self.check_url("/api/hubs/ralph/config", user=auth_user)

+         response = self.check_url(

+             "/api/hubs/%s/config" % hub.id, user=auth_user)

          avatar_url = (

              "https://seccdn.libravatar.org/avatar/9c9f7784935381befc302fe3"

              "c814f9136e7a33953d0318761669b8643f4df55c?s=312&d=retro"

              )

          expected_data = {

+             "id": 7,

              "name": "ralph",

-             "user_hub": True,

+             "type": "user",

              "mtime": "Sun, 01 Jan 2017 00:00:00 GMT",

              "subscribed_to": [],

              'perms': {'config': True},
@@ -63,11 +65,11 @@ 

              response_data, {"status": "OK", "data": expected_data})

  

      def test_private_get(self):

-         hub = Hub.by_name('ralph')

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

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

          self.session.commit()

          # Private hubs are not accessible to anonymous users.

-         response = self.check_url("/api/hubs/ralph/config", code=403)

+         response = self.check_url("/api/hubs/%s/config" % hub.id, code=403)

          self.assertDictEqual(

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

              {
@@ -80,13 +82,14 @@ 

          decause = User.by_username("decause")

          hub.subscribe(decause, role='member')

          self.check_url(

-             "/api/hubs/ralph/config", user=FakeAuthorization("decause"))

+             "/api/hubs/%s/config" % hub.id, user=FakeAuthorization("decause"))

  

      def test_put(self):

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

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/config',

+                 '/api/hubs/%s/config' % hub.id,

                  content_type="application/json",

                  data=json.dumps({

                      "config": {
@@ -97,15 +100,16 @@ 

          self.assertEqual(result.status_code, 200)

          result_data = json.loads(result.get_data(as_text=True))

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

-         hub_config = Hub.query.get("ralph").config

+         hub_config = Hub.by_name("ralph", "user").config

          self.assertEqual(hub_config["summary"], "changed value")

  

      def test_put_unknown_data(self):

          # Unknown PUT data is silently ignored

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

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/config',

+                 '/api/hubs/%s/config' % hub.id,

                  content_type="application/json",

                  data=json.dumps({"config": {"non_existant": "dummy"}}))

          self.assertEqual(result.status_code, 200)
@@ -116,9 +120,10 @@ 

  

      def test_put_invalid_chat_domain(self):

          user = FakeAuthorization('ralph')

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

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/config',

+                 '/api/hubs/%s/config' % hub.id,

                  content_type="application/json",

                  data=json.dumps({"config": {"chat_domain": "dummy"}}))

          self.assertEqual(result.status_code, 200)
@@ -133,17 +138,18 @@ 

  

      def test_put_unauthorized(self):

          user = FakeAuthorization('ralph')

+         hub = Hub.by_name('decause', "user")

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/decause/config',

+                 '/api/hubs/%s/config' % hub.id,

                  content_type="application/json",

                  data=json.dumps({"config": {"summary": "Defaced!"}}))

          self.assertEqual(result.status_code, 403)

-         hub_config = Hub.query.get("decause").config

+         hub_config = Hub.by_name("decause", "user").config

          self.assertEqual(hub_config["summary"], "Decause")

  

      def test_put_users(self):

-         hub = Hub.by_name('ralph')

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

          for username in ["devyani7", "dhrish"]:

              hub.associations.append(

                  Association(user=User.query.get(username), role="member")
@@ -154,7 +160,7 @@ 

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/config',

+                 '/api/hubs/%s/config' % hub.id,

                  content_type="application/json",

                  data=json.dumps({

                      "users": {
@@ -181,11 +187,11 @@ 

  

      def test_put_users_other_roles(self):

          # Only deal with the "member" and "owner" roles.

-         hub = Hub.by_name('ralph')

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

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/config',

+                 '/api/hubs/%s/config' % hub.id,

                  content_type="application/json",

                  data=json.dumps({

                      "users": {
@@ -206,6 +212,7 @@ 

          )

  

      def test_suggest_users_no_filter(self):

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

          user = FakeAuthorization('ralph')

          expected = [

              dict(username=u.username, fullname=u.fullname)
@@ -213,13 +220,14 @@ 

              ]

          # Check without filter

          response = self.check_url(

-             "/api/hubs/ralph/config/suggest-users", user=user)

+             "/api/hubs/%s/config/suggest-users" % hub.id, user=user)

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

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

          self.assertListEqual(result_data["data"], expected)

  

      def test_suggest_users_filter_owners(self):

          # Filters on owners

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

          user = FakeAuthorization('ralph')

          expected = [

              dict(username=u.username, fullname=u.fullname)
@@ -228,7 +236,7 @@ 

                  User.username != "ralph").all()

              ]

          response = self.check_url(

-             "/api/hubs/ralph/config/suggest-users?exclude-role=owner",

+             "/api/hubs/%s/config/suggest-users?exclude-role=owner" % hub.id,

              user=user)

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

          self.assertEqual(result_data["status"], "OK")
@@ -237,7 +245,7 @@ 

      def test_suggest_users_filter_members(self):

          # Filters on members

          user = FakeAuthorization('ralph')

-         hub = Hub.get('ralph')

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

          decause = User.query.get("decause")

          devyani7 = User.query.get("devyani7")

          hub.subscribe(decause, "member")
@@ -252,7 +260,7 @@ 

                  User.username != "devyani7"

              ).all()]

          response = self.check_url(

-             "/api/hubs/ralph/config/suggest-users?exclude-role=member",

+             "/api/hubs/%s/config/suggest-users?exclude-role=member" % hub.id,

              user=user)

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

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

@@ -6,14 +6,15 @@ 

  from hubs.app import app

  from hubs.models import Hub, User, Widget

  from hubs.widgets import registry

- from hubs.tests import APPTest, FakeAuthorization, auth_set

+ from hubs.tests import APPTest, FakeAuthorization, auth_set, widget_instance

  

  

  class TestAPIHubWidgets(APPTest):

  

      def test_get_widgets(self):

-         expected_ids = [32, 33, 31, 34, 35, 36, 37, 38, 39, 40, 51]

-         response = self.check_url("/api/hubs/ralph/widgets/")

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

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

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

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

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

          self.assertListEqual(
@@ -21,11 +22,11 @@ 

              expected_ids)

  

      def test_get_widgets_private(self):

-         hub = Hub.by_name('ralph')

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

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

          self.session.commit()

          # Private hubs are not accessible to anonymous users.

-         response = self.check_url("/api/hubs/ralph/widgets/", code=403)

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

          self.assertDictEqual(

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

              {
@@ -38,10 +39,11 @@ 

          decause = User.by_username("decause")

          hub.subscribe(decause, role='member')

          self.check_url(

-             "/api/hubs/ralph/widgets/", user=FakeAuthorization("decause"))

+             "/api/hubs/%s/widgets/" % hub.id,

+             user=FakeAuthorization("decause"))

  

      def test_get_widgets_preview(self):

-         hub = Hub.by_name('ralph')

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

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

          meetings = Widget.query.filter_by(

              hub=hub, plugin="meetings").first()
@@ -49,27 +51,27 @@ 

          meetings.visibility = "restricted"

          # Restricted widgets in preview hubs aren't displayed to

          # anonymous users.

-         response = self.check_url("/api/hubs/ralph/widgets/")

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

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

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

          self.assertNotIn(

              "meetings", [w["name"] for w in response_data["data"]])

          # But they are displayed to members.

          response = self.check_url(

-             "/api/hubs/ralph/widgets/", user=FakeAuthorization('ralph'))

+             "/api/hubs/%s/widgets/" % hub.id, user=FakeAuthorization('ralph'))

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

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

          self.assertIn(

              "meetings", [w["name"] for w in response_data["data"]])

  

      def test_get_removed_widget(self):

-         hub = Hub.get("ralph")

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

          widget = Widget(

              hub=hub, plugin="does-not-exist",

              left=True, index=-1, _config="{}")

          self.session.add(widget)

          response = self.check_url(

-             "/api/hubs/ralph/widgets/", user=FakeAuthorization('ralph'))

+             "/api/hubs/%s/widgets/" % hub.id, user=FakeAuthorization('ralph'))

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

          self.assertNotIn(

              "does-not-exist", [w["name"] for w in response_data["data"]])
@@ -79,7 +81,9 @@ 

          # the widgets in the registry. At the moment, the subscription

          # widget is the only widget that is resctrited (it is not available)

          # on the group hub.

-         response = self.check_url("/api/hubs/ralph/available-widgets/")

+         ralph_hub = Hub.by_name("ralph", "user")

+         response = self.check_url(

+             "/api/hubs/%s/available-widgets/" % ralph_hub.id)

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

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

          self.assertListEqual(
@@ -88,12 +92,14 @@ 

  

          # next, test that a group hub returns all the widgets other than the

          # subscription widget

-         response = self.check_url("/api/hubs/infra/available-widgets/")

+         infra_hub = Hub.by_name("infra", "team")

+         response = self.check_url(

+             "/api/hubs/%s/available-widgets/" % infra_hub.id)

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

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

          self.assertListEqual(

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

-             [w.name for w in registry.values() if "group" in w.hub_types])

+             [w.name for w in registry.values() if "team" in w.hub_types])

  

      def test_post_invalid_request(self):

          invalid_data = [
@@ -112,11 +118,12 @@ 

                  'position': 'invalid_position',

              },

          ]

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

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              for data in invalid_data:

                  result = self.app.post(

-                     '/api/hubs/ralph/widgets/',

+                     '/api/hubs/%s/widgets/' % hub.id,

                      content_type="application/json",

                      data=json.dumps(data))

                  self.assertEqual(result.status_code, 400)
@@ -131,11 +138,12 @@ 

                  'position': 'right',

              },

          ]

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

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              for data in invalid_data:

                  result = self.app.post(

-                     '/api/hubs/ralph/widgets/',

+                     '/api/hubs/%s/widgets/' % hub.id,

                      content_type="application/json",

                      data=json.dumps(data))

                  self.assertEqual(result.status_code, 400)
@@ -143,6 +151,7 @@ 

                  self.assertIn(expected_str, result.get_data(as_text=True))

  

      def test_post_valid_widget_name_no_config(self):

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

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              data = {
@@ -151,25 +160,23 @@ 

                  "config": {},

                  }

              result = self.app.post(

-                 '/api/hubs/ralph/widgets/',

+                 '/api/hubs/%s/widgets/' % hub.id,

                  content_type="application/json",

                  data=json.dumps(data))

          self.assertEqual(result.status_code, 200)

          self.assertEqual(

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

              {"status": "OK"})

-         response = self.check_url("/api/hubs/ralph/widgets/")

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

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

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

          self.assertIn("memberships",

                        [w["name"] for w in response_data["data"]])

  

      def test_post_valid_widget_name_with_config(self):

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

          self.assertEqual(

-             Widget.query.filter(

-                 Hub.name == "ralph",

-                 Widget.plugin == "about",

-             ).count(), 1)

+             Widget.query.filter_by(hub=hub, plugin="about").count(), 1)

          data = {

              "name": "about",

              "config": {'text': 'text of widget'},
@@ -178,7 +185,7 @@ 

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.post(

-                 '/api/hubs/ralph/widgets/',

+                 '/api/hubs/%s/widgets/' % hub.id,

                  content_type="application/json",

                  data=json.dumps(data))

          self.assertEqual(result.status_code, 200)
@@ -186,17 +193,13 @@ 

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

              {"status": "OK"})

          self.assertEqual(

-             Widget.query.filter(

-                 Hub.name == "ralph",

-                 Widget.plugin == "about",

-             ).count(), 2)

+             Widget.query.filter_by(hub=hub, plugin="about").count(), 2)

  

      def test_post_invalid_config(self):

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

          self.assertEqual(

-             Widget.query.join(Hub).filter(

-                 Hub.name == "ralph",

-                 Widget.plugin == "meetings",

-             ).count(), 1)

+             Widget.query.filter_by(

+                 hub=hub, plugin="meetings").count(), 1)

          data = {

              "name": "meetings",

              "config": {
@@ -208,7 +211,7 @@ 

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.post(

-                 '/api/hubs/ralph/widgets/',

+                 '/api/hubs/%s/widgets/' % hub.id,

                  content_type="application/json",

                  data=json.dumps(data))

          self.assertEqual(result.status_code, 200)
@@ -229,18 +232,25 @@ 

  

  class TestAPIHubWidget(APPTest):

  

+     def setUp(self):

+         super(TestAPIHubWidget, self).setUp()

+         self.hub = Hub.by_name("ralph", "user")

+         self.widget = widget_instance("ralph", "pagure_pr")

+         self.url = "/api/hubs/%s/widgets/%s/" % (self.hub.id, self.widget.idx)

+ 

      def test_get_logged_in(self):

          user = FakeAuthorization('ralph')

-         response = self.check_url("/api/hubs/ralph/widgets/37/", user=user)

+         response = self.check_url(self.url, user=user)

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

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

          self.assertEqual(response_data["data"]["name"], "pagure_pr")

  

      def test_get_logged_out(self):

-         hub = Hub.by_name('ralph')

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

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

+         widget = widget_instance("ralph", "pagure_pr")

          self.session.commit()

-         response = self.check_url("/api/hubs/ralph/widgets/31/", code=403)

+         response = self.check_url(

+             "/api/hubs/%s/widgets/%s/" % (self.hub.id, widget.idx), code=403)

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

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

  
@@ -251,7 +261,7 @@ 

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/widgets/37/',

+                 self.url,

                  content_type="application/json",

                  data=json.dumps({}))

          self.assertEqual(result.status_code, 200)
@@ -261,7 +271,7 @@ 

          self.assertTrue(queue.enqueue.called)

          task = queue.enqueue.call_args_list[0][0][0]

          expected = {

-             "hub": "ralph", "idx": 37,

+             "hub": "ralph", "idx": self.widget.idx,

              "type": "widget-cache",

              "fn_name": "GetPRs",

          }
@@ -271,7 +281,7 @@ 

          user = FakeAuthorization('decause')

          with auth_set(app, user):

              result = self.app.put(

-                 '/api/hubs/ralph/widgets/37/',

+                 self.url,

                  content_type="application/json",

                  data=json.dumps({"config": {"text": "Defaced!"}}))

          self.assertEqual(result.status_code, 403)
@@ -279,22 +289,23 @@ 

      def test_delete(self):

          user = FakeAuthorization('ralph')

          with auth_set(app, user):

-             result = self.app.delete('/api/hubs/ralph/widgets/37/')

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

          self.assertEqual(result.status_code, 200)

          self.assertEqual(

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

              {"status": "OK"})

-         response = self.check_url("/api/hubs/ralph/widgets/")

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

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

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

  

      def test_delete_unauthorized(self):

          user = FakeAuthorization('decause')

          with auth_set(app, user):

-             response = self.app.delete('/api/hubs/ralph/widgets/37/')

+             response = self.app.delete(self.url)

          self.assertEqual(response.status_code, 403)

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

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

-         response = self.check_url("/api/hubs/ralph/widgets/")

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

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

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

+         self.assertIn(

+             self.widget.idx, [w["idx"] for w in response_data["data"]])

@@ -13,12 +13,12 @@ 

          test that if we try to look at a hub when not

          logged in, we get directed to the /

          """

-         response = self.check_url("/ralph/", code=302)

+         response = self.check_url("/u/ralph/", code=302)

          self.assertEqual(urlparse(response.location).path, "/")

  

      def test_hub_logged_in(self):

          user = FakeAuthorization('ralph')

-         response = self.check_url("/ralph/", user=user)

+         response = self.check_url("/u/ralph/", user=user)

          self.assertNotIn(

              'Not logged in.  Click to <a href="/login">login</a>',

              response.get_data(as_text=True))
@@ -26,12 +26,12 @@ 

          self.assertTrue(state["currentUser"]["logged_in"])

  

      def test_hub_private(self):

-         hub = Hub.by_name('ralph')

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

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

          self.session.commit()

          # Private hubs are not accessible to anonymous users.

-         self.check_url("/ralph/", code=403)

+         self.check_url("/u/ralph/", code=403)

          # But they are accessible to members.

          decause = User.by_username("decause")

          hub.subscribe(decause, role='member')

-         self.check_url("/ralph/", user=FakeAuthorization("decause"))

+         self.check_url("/u/ralph/", user=FakeAuthorization("decause"))

@@ -53,7 +53,7 @@ 

          return json.loads(result)

  

      def _add_assoc(self, hubname, username, role):

-         hub = hubs.models.Hub.query.get(hubname)

+         hub = hubs.models.Hub.by_name(hubname, "team")

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

          self.session.add(hubs.models.Association(

              hub=hub, user=user, role=role))

@@ -1,144 +0,0 @@ 

- from __future__ import unicode_literals

- 

- import json

- 

- from mock import Mock, patch

- 

- import hubs.tests

- import hubs.models

- from hubs.app import app

- 

- 

- class TestStreamExisting(hubs.tests.APPTest):

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

- 

-     @patch("hubs.feed.Notifications")

-     def test_get(self, Notifications):

-         feed = Mock()

-         Notifications.return_value = feed

-         feed.get.return_value = [{

-             "subtitle": "foo", "link": "bar",

-         }]

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

-             resp = self.app.get('/stream/existing')

- 

-         self.assertEqual(resp.status_code, 200)

-         data = json.loads(resp.get_data(as_text=True))

-         self.assertEqual(data["status"], "OK")

-         self.assertEqual(len(data["data"]), 1)

-         self.assertEqual(data["data"][0]['subtitle'], 'foo')

-         self.assertEqual(data["data"][0]['markup'], 'foo')

-         self.assertEqual(data["data"][0]['link'], 'bar')

- 

- 

- 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('/stream/saved/'.format(name))

-         self.assertEqual(resp.status_code, 200)

-         data = json.loads(resp.get_data(as_text=True))

-         self.assertEqual(data["status"], "OK")

-         self.assertEqual(len(data["data"]), 1)

-         self.assertEqual(data["data"][0]['markup'], 'foo')

-         self.assertEqual(data["data"][0]['link'], 'bar')

- 

-     def test_get_notifications_valid_name(self):

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

-             resp = self.app.get('/stream/saved/'.format(

-                 self.user.nickname))

- 

-         self.assertEqual(resp.status_code, 200)

-         data = json.loads(resp.get_data(as_text=True))

-         self.assertEqual(data["status"], "OK")

-         self.assertEqual(len(data["data"]), 1)

-         self.assertEqual(data["data"][0]['markup'], 'foo')

-         self.assertEqual(data["data"][0]['link'], 'bar')

- 

- 

- class TestPostNotifications(hubs.tests.APPTest):

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

-     valid_payload = {

-         'username': user.nickname,

-         'markup': 'foobar',

-         'link': 'baz',

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

-         'dom_id': 'reallyuniqueuid'

-     }

- 

-     invalid_payload = {

-         'username': user.nickname,

-     }

- 

-     def test_post_notification_invalid_payload(self):

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

-             resp = self.app.post(

-                 '/stream/saved/',

-                 data=json.dumps(self.invalid_payload),

-                 content_type='application/json')

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

-                 '/stream/saved/',

-                 data=json.dumps(self.valid_payload),

-                 content_type='application/json')

- 

-         self.assertEqual(resp.status_code, 200)

-         data = json.loads(resp.get_data(as_text=True))

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

-         self.assertEqual(data["status"], "OK")

- 

-         notification = data['data']

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

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

- 

-         all_saved = hubs.models.SavedNotification.by_username(

-             self.user.nickname)

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

-                 '/stream/saved/{}/'.format(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(

-                 '/stream/saved/{}/'.format(self.user.nickname, idx)

-             )

-         self.assertEqual(resp.status_code, 404)

@@ -14,7 +14,7 @@ 

  

      def populate(self):

          super(WidgetTest, self).populate()

-         hub = hubs.models.Hub.query.get("ralph")

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

          widget = hubs.models.Widget(

              plugin='library',

              index=51,
@@ -25,7 +25,7 @@ 

          self.session.commit()

  

      def _add_widget_under_test(self):

-         hub = hubs.models.Hub.query.get("ralph")

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

          self.widget = hubs.models.Widget(

              plugin=self.plugin,

              index=1,
@@ -44,9 +44,9 @@ 

          # Test authorizations on the root view.

          if not self.plugin:

              raise unittest.SkipTest

-         hub = hubs.models.Hub.query.get("ralph")

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

          widget = widget_instance(hub.name, self.plugin)

-         url = '/%s/w/%s/%i/' % (hub.name, self.plugin, widget.idx)

+         url = '/widgets/%s/%i/' % (self.plugin, widget.idx)

          # Public

          self.check_url(url, None, 200)

          # Preview

@@ -60,7 +60,7 @@ 

  

      def setUp(self):

          super(ContactsTest, self).setUp()

-         hub = Hub.by_name('ralph')

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

          self.widget = Widget(

              plugin='contact',

              index=1,
@@ -83,7 +83,7 @@ 

          mock_fas2.AccountSystem.return_value = fake_account_system

          user = FakeAuthorization('ralph')

          response = self.check_url(

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

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

          self.assertDictEqual(

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

              {
@@ -111,7 +111,7 @@ 

          fedmsg_config.__getitem__.return_value = {}

          user = FakeAuthorization('ralph')

          response = self.check_url(

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

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

          self.assertDictEqual(

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

              {
@@ -122,7 +122,7 @@ 

  

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

      def test_plus_plus_get_valid(self, mock_request):

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

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

          result = self.app.get(url)

          expected = {

              "current": 0,
@@ -139,7 +139,7 @@ 

  

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

      def test_plus_plus_post_increment_valid(self, mock_request):

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

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

          user = FakeAuthorization('decause')

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

              result = self.app.post(
@@ -161,7 +161,7 @@ 

  

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

      def test_plus_plus_post_increment_myself_error(self, mock_request):

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

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

          user = FakeAuthorization('ralph')

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

              result = self.app.post(
@@ -177,7 +177,7 @@ 

  

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

      def test_plus_plus_post_increment_no_data_error(self, mock_request):

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

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

          user = FakeAuthorization('decause')

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

              result = self.app.post(
@@ -194,7 +194,7 @@ 

      @mock.patch('requests.request')

      def test_plus_plus_connection_error(self, mock_request):

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

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

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

          result = self.app.get(url)

          self.assertEqual(

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

@@ -0,0 +1,103 @@ 

+ from __future__ import unicode_literals

+ 

+ import json

+ 

+ from mock import Mock, patch

+ 

+ from hubs.app import app

+ from hubs.models import SavedNotification, User

+ from hubs.tests import FakeAuthorization, widget_instance, auth_set

+ from . import WidgetTest

+ 

+ 

+ class FeedWidgetTestCase(WidgetTest):

+ 

+     plugin = 'feed'  # The name in hubs.widgets.registry

+ 

+     def setUp(self):

+         super(FeedWidgetTestCase, self).setUp()

+         self.widget = widget_instance('ralph', self.plugin)

+         self.user = FakeAuthorization('ralph')

+ 

+     @patch("hubs.widgets.feed.views.GetData")

+     def test_existing(self, GetData):

+         func = Mock()

+         GetData.return_value = func

+         func.return_value = [{

+             "foo": "bar",

+         }]

+         response = self.check_url(

+             '/widgets/%s/%i/existing' % (self.plugin, self.widget.idx),

+             self.user)

+         expected_dict = {

+             'data': [{'foo': 'bar', 'markup': '', 'markup_subjective': ''}],

+             'status': 'OK',

+         }

+         data = json.loads(response.get_data(as_text=True))

+         self.assertDictEqual(data, expected_dict)

+ 

+     def test_save_notif(self):

+         payload = {

+             'username': self.user.nickname,

+             'markup': 'foobar',

+             'link': 'baz',

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

+             'notif_id': 'reallyuniqueuid',

+             'timestamp': 123456789,

+         }

+         with auth_set(app, self.user):

+             resp = self.app.post(

+                 '/widgets/%s/%i/saved/' % (self.plugin, self.widget.idx),

+                 data=json.dumps(payload),

+                 content_type='application/json')

+         self.assertEqual(resp.status_code, 200)

+         data = json.loads(resp.get_data(as_text=True))

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

+         self.assertEqual(data["status"], "OK")

+ 

+         notification = data['data']

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

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

+ 

+         user = User.query.get(self.user.nickname)

+         all_saved = user.saved_notifications

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

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

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

+                             for s in all_saved))

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

+                             for s in all_saved))

+ 

+     def test_save_notif_invalid(self):

+         with auth_set(app, self.user):

+             resp = self.app.post(

+                 '/widgets/%s/%i/saved/' % (self.plugin, self.widget.idx),

+                 data=json.dumps({"foo": "bar"}),

+                 content_type='application/json')

+         self.assertEqual(resp.status_code, 400)

+ 

+     def test_delete_notif(self):

+         notification = SavedNotification(

+             username='ralph',

+             markup='foo',

+             link='bar',

+             icon='baz',

+             notif_id='qux',

+             timestamp=123456789,

+         )

+         self.session.add(notification)

+         self.session.commit()

+         with auth_set(app, self.user):

+             resp = self.app.delete(

+                 '/widgets/%s/%i/saved/qux' % (self.plugin, self.widget.idx)

+             )

+         self.assertEqual(resp.status_code, 200)

+         self.assertEqual(

+             SavedNotification.query.filter_by(notif_id="qux").count(), 0)

+ 

+     def test_delete_bad_id(self):

+         with auth_set(app, self.user):

+             resp = self.app.delete(

+                 '/widgets/%s/%i/saved/invalid' % (self.plugin, self.widget.idx)

+             )

+         self.assertEqual(resp.status_code, 404)

file modified
+31 -29
@@ -4,6 +4,7 @@ 

  

  from hubs.app import app

  from hubs.models import Hub

+ from hubs.models.constants import HUB_TYPES

  from hubs.utils.views import configure_widget_instance, WidgetConfigError

  from hubs.tests import widget_instance

  from hubs.widgets import registry
@@ -13,7 +14,7 @@ 

  SAMPLE_DATA = [

      {

          'author': {

-             'url': '/mattdm/',

+             'url': '/u/mattdm/',

              'name': 'mattdm',

              'avatar':

                  'https://seccdn.libravatar.org/avatar/6c172561d124a0a29f75c9'
@@ -31,7 +32,7 @@ 

          'urls': [],

      }, {

          'author': {

-             'url': '/jkurik/',

+             'url': '/u/jkurik/',

              'name': 'jkurik',

              'avatar':

                  'https://seccdn.libravatar.org/avatar/365c34d57aa5760cb742d7'
@@ -49,7 +50,7 @@ 

          'urls': [],

      }, {

          'author': {

-             'url': '/stoney/',

+             'url': '/u/stoney/',

              'name': 'stoney',

              'avatar':

                  'https://seccdn.libravatar.org/avatar/7f450d8365ea319ce8ad92'
@@ -65,7 +66,7 @@ 

          'urls': [],

      }, {

          'author': {

-             'url': '/nitzmahone/',

+             'url': '/u/nitzmahone/',

              'name': 'nitzmahone',

              'avatar':

                  'https://seccdn.libravatar.org/avatar/94bca7e512e7932858af8'
@@ -81,7 +82,7 @@ 

          'urls': [],

      }, {

          'author': {

-             'url': '/jborean93/',

+             'url': '/u/jborean93/',

              'name': 'jborean93',

              'avatar':

                  'https://seccdn.libravatar.org/avatar/0c83dd01acb93691829675'
@@ -110,13 +111,13 @@ 

          config["hubs"] = ["fedora-devel"]

          config["per_page"] = 3

          self.widget.config = config

-         infra_hub = Hub.query.get("infra")

+         infra_hub = Hub.by_name("infra", "team")

          infra_hub.config["chat_channel"] = "#fedora-meeting"

-         devyani7 = Hub.query.get("devyani7")

+         devyani7 = Hub.by_name("devyani7", "user")

          devyani7.config["chat_channel"] = "#fedora-meeting-2"

-         decause = Hub.query.get("decause")

+         decause = Hub.by_name("decause", "user")

          decause.config["chat_channel"] = "#ansible-meeting"

-         dhrish = Hub.query.get("dhrish")

+         dhrish = Hub.by_name("dhrish", "user")

          dhrish.config["chat_channel"] = "#foss2serve"

          self.session.commit()

          self.session.refresh(self.widget)
@@ -139,7 +140,7 @@ 

              'config': {'hubs': ['fedora-devel'], 'per_page': 3},

              'cssClass': None,

              'hiddenIfEmpty': False,

-             'hub_types': ['user', 'group'],

+             'hub_types': HUB_TYPES,

              'idx': self.widget.idx,

              'index': 9,

              'isReact': True,
@@ -163,26 +164,27 @@ 

                   },

              ],

              'position': 'right',

-             'selfUrl': '/api/hubs/ralph/widgets/{}/'.format(self.widget.idx),

+             'selfUrl': '/api/hubs/{}/widgets/{}/'.format(

+                 self.widget.hub.id, self.widget.idx),

              'title': 'Help Requests',

              'urls': {

-                 'allHubs': '/w/halp/hubs',

-                 'data': '/ralph/w/halp/{}/data'.format(self.widget.idx),

-                 'requesters': '/ralph/w/halp/{}/requesters'.format(

+                 'allHubs': '/widgets/halp/hubs',

+                 'data': '/widgets/halp/{}/data'.format(self.widget.idx),

+                 'requesters': '/widgets/halp/{}/requesters'.format(

                      self.widget.idx),

-                 'search': '/ralph/w/halp/{}/search'.format(self.widget.idx),

+                 'search': '/widgets/halp/{}/search'.format(self.widget.idx),

              }

          })

  

      def test_config_hubs_suggest(self):

-         response = self.app.get('/w/halp/hubs')

+         response = self.app.get('/widgets/halp/hubs')

          self.assertEqual(response.status_code, 200)

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

          self.assertDictEqual(data, {

              'status': 'OK',

              'data': ['decause', 'devyani7', 'dhrish', 'i18n', 'infra']

          })

-         response = self.app.get('/w/halp/hubs?q=de')

+         response = self.app.get('/widgets/halp/hubs?q=de')

          self.assertEqual(response.status_code, 200)

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

          self.assertDictEqual(data, {
@@ -193,7 +195,7 @@ 

      def test_data(self):

          expected = self._get_sample_data_with_hub_keys()

          # Test with no hub selected

-         response = self.app.get('/ralph/w/halp/%i/data' % self.widget.idx)

+         response = self.app.get('/widgets/halp/%i/data' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          self.assertEqual(data, {
@@ -201,7 +203,7 @@ 

              "data": {"requests": expected[:3]}

          })

          # Test with the right hub selected

-         response = self.app.get('/ralph/w/halp/%i/data?hubs=decause'

+         response = self.app.get('/widgets/halp/%i/data?hubs=decause'

                                  % self.widget.idx)

          self.assertEqual(response.status_code, 200)

          data = json.loads(response.get_data(as_text=True))
@@ -212,7 +214,7 @@ 

  

      def test_data_wrong_hub(self):

          # Test with the wrong hub selected

-         response = self.app.get('/ralph/w/halp/%i/data?hubs=ralph'

+         response = self.app.get('/widgets/halp/%i/data?hubs=ralph'

                                  % self.widget.idx)

          self.assertEqual(response.status_code, 200)

          data = json.loads(response.get_data(as_text=True))
@@ -222,7 +224,7 @@ 

  

      def test_search_requesters(self):

          response = self.app.get(

-             '/ralph/w/halp/%i/requesters' % self.widget.idx)

+             '/widgets/halp/%i/requesters' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          self.assertDictEqual(data, {
@@ -230,7 +232,7 @@ 

              'data': ['jborean93', 'jkurik', 'mattdm', 'nitzmahone', 'stoney']

              })

          response = self.app.get(

-             '/ralph/w/halp/%i/requesters?q=j' % self.widget.idx)

+             '/widgets/halp/%i/requesters?q=j' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          self.assertDictEqual(data, {
@@ -241,7 +243,7 @@ 

      def test_search_all(self):

          expected = self._get_sample_data_with_hub_keys()

          response = self.app.get(

-             '/ralph/w/halp/%i/search' % self.widget.idx)

+             '/widgets/halp/%i/search' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          self.assertEqual(data, {
@@ -257,7 +259,7 @@ 

              })

          # Page 2

          response = self.app.get(

-             '/ralph/w/halp/%i/search?page=2' % self.widget.idx)

+             '/widgets/halp/%i/search?page=2' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          self.assertEqual(data, {
@@ -274,7 +276,7 @@ 

  

      def test_search_hub(self):

          response = self.app.get(

-             '/ralph/w/halp/%i/search?hubs=infra' % self.widget.idx)

+             '/widgets/halp/%i/search?hubs=infra' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          expected = self._get_sample_data_with_hub_keys()
@@ -292,7 +294,7 @@ 

  

      def test_search_people(self):

          response = self.app.get(

-             '/ralph/w/halp/%i/search?people=nitzmahone' % self.widget.idx)

+             '/widgets/halp/%i/search?people=nitzmahone' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          expected = self._get_sample_data_with_hub_keys()
@@ -310,7 +312,7 @@ 

  

      def test_search_meetingname(self):

          response = self.app.get(

-             '/ralph/w/halp/%i/search?meetingname=Readiness' % self.widget.idx)

+             '/widgets/halp/%i/search?meetingname=Readiness' % self.widget.idx)

          self.assertEqual(response.status_code, 200)

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

          expected = self._get_sample_data_with_hub_keys()
@@ -328,7 +330,7 @@ 

  

      def test_search_date(self):

          response = self.app.get(

-             '/ralph/w/halp/%i/search?startdate=2017-03-16&enddate=2017-03-17'

+             '/widgets/halp/%i/search?startdate=2017-03-16&enddate=2017-03-17'

              % self.widget.idx)

          self.assertEqual(response.status_code, 200)

          data = json.loads(response.get_data(as_text=True))
@@ -386,7 +388,7 @@ 

  

      def test_should_invalidate_wrong_hub(self):

          # The decause hub works in the fedora-commops channel.

-         decause_hub = Hub.query.filter_by(name="decause").one()

+         decause_hub = Hub.by_name("decause", "user")

          decause_hub.config["chat_channel"] = "#fedora-commops"

          self.session.commit()

          msg = {'topic': 'org.fedoraproject.prod.meetbot.meeting.item.help',

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

  import json

  

  from hubs.app import app

+ from hubs.models.constants import HUB_TYPES

  from hubs.tests import FakeAuthorization, widget_instance

  from . import WidgetTest

  
@@ -15,7 +16,7 @@ 

          widget = widget_instance('ralph', self.plugin)

          user = FakeAuthorization('ralph')

          response = self.check_url(

-             '/ralph/w/%s/%i/data' % (self.plugin, widget.idx), user)

+             '/widgets/%s/%i/data' % (self.plugin, widget.idx), user)

          expected_dict = {

              'data': [],

              'page': {
@@ -45,7 +46,7 @@ 

              'isLarge': False,

              'label': 'Library',

              'name': 'library',

-             'hub_types': ['user', 'group'],

+             'hub_types': HUB_TYPES,

              'params': [

                  {'default': [],

                   'help': 'A JSON list of dicts with `url`, `title`, '
@@ -64,11 +65,12 @@ 

                   },

              ],

              'position': 'right',

-             'selfUrl': '/api/hubs/ralph/widgets/{}/'.format(widget.idx),

+             'selfUrl': '/api/hubs/{}/widgets/{}/'.format(

+                 widget.hub.id, widget.idx),

              'title': 'Library',

              'urls': {

-                 'data': '/ralph/w/library/{}/data'.format(widget.idx),

-                 'getLink': '/ralph/w/library/{}/get-link'.format(

+                 'data': '/widgets/library/{}/data'.format(widget.idx),

+                 'getLink': '/widgets/library/{}/get-link'.format(

                      widget.idx),

              }

          })
@@ -76,7 +78,7 @@ 

      def test_get_link(self):

          widget = widget_instance('ralph', self.plugin)

          user = FakeAuthorization('ralph')

-         backend_url = "/ralph/w/{}/{}/get-link".format(self.plugin, widget.idx)

+         backend_url = "/widgets/{}/{}/get-link".format(self.plugin, widget.idx)

          test_urls = [

              {

                  'url': 'http://fedoraproject.org/wiki/Design',

@@ -13,7 +13,7 @@ 

          widget.hub.config["calendar"] = team

          user = FakeAuthorization('ralph')

          response = self.check_url(

-             '/%s/w/%s/%i/' % (team, self.plugin, widget.idx), user)

+             '/widgets/%s/%i/' % (self.plugin, widget.idx), user)

          self.assertIn(team, response.context["calendar"])

  

      def test_render_simple(self):
@@ -21,7 +21,7 @@ 

          widget = widget_instance(team, self.plugin)

          widget.hub.config["calendar"] = team

          user = FakeAuthorization('ralph')

-         url = '/%s/w/%s/%i/' % (team, self.plugin, widget.idx)

+         url = '/widgets/%s/%i/' % (self.plugin, widget.idx)

          response = self.check_url(url, user)

          self.assertIn('i18n', response.get_data(as_text=True))

          self.assertIn('<strong>Request A New Meeting</strong>',

@@ -12,14 +12,14 @@ 

      def populate(self):

          super(MyHubsTest, self).populate()

          self._add_widget_under_test()

-         hub = hubs.models.Hub.by_name('infra')

+         hub = hubs.models.Hub.by_name('infra', "team")

          hub.subscribe(hubs.models.User.by_username('ralph'), 'owner')

  

-         hub = hubs.models.Hub.by_name('i18n')

+         hub = hubs.models.Hub.by_name('i18n', "team")

          hub.subscribe(hubs.models.User.by_username('ralph'), 'subscriber')

  

-         self.session.add(hubs.models.Hub(name="designteam"))

-         hub = hubs.models.Hub.by_name('designteam')

+         self.session.add(hubs.models.Hub(name="designteam", hub_type="team"))

+         hub = hubs.models.Hub.by_name('designteam', "team")

          hub.config["summary"] = "the designteam team"

          hub.subscribe(hubs.models.User.by_username('ralph'), 'member')

  
@@ -32,7 +32,7 @@ 

          })

          user = FakeAuthorization('ralph')

          response = self.check_url(

-             '/ralph/w/%s/%i/' % (self.plugin, self.widget.idx), user)

+             '/widgets/%s/%i/' % (self.plugin, self.widget.idx), user)

          self.assertEquals(len(response.context['memberships']), 1)

          self.assertEquals(len(response.context['ownerships']), 1)

          self.assertEquals(len(response.context['subscriptions']), 1)

file modified
+45 -18
@@ -14,22 +14,45 @@ 

  from sqlalchemy.orm.exc import NoResultFound

  

  from hubs.models import Hub, HubConfig, Widget

+ from hubs.models.constants import HUB_TYPES

  

  

  log = logging.getLogger(__name__)

  

  

- def get_hub(name):

-     """ Utility shorthand to get a hub and 404 if not found. """

-     query = Hub.query.filter(Hub.name == name)

+ def get_hub(name, hub_type):

+     """ Get a hub by its name and type, or return 404 if not found. """

+     if hub_type == "u":

+         hub_type = "user"

+     elif hub_type == "t":

+         hub_type = "team"

+     if hub_type not in HUB_TYPES:

+         flask.abort(400)

+     query = Hub.query.filter(

+         Hub.name == name,

+         Hub.hub_type == hub_type,

+         )

      try:

          return query.one()

      except NoResultFound:

          flask.abort(404)

  

  

+ def get_hub_by_id(hub_id):

+     """ Get a hub by its id, or return 404 if not found. """

+     try:

+         hub_id = int(hub_id)

+     except (TypeError, ValueError):

+         flask.abort(400)

+     hub = Hub.query.get(hub_id)

+     if hub is None:

+         flask.abort(404)

+     return hub

+ 

+ 

  def query_hubs(querystring):

      query = Hub.query.join(HubConfig).filter(

+         Hub.hub_type.in_(["user", "team"]),

          or_(

              Hub.name.ilike('%{}%'.format(querystring)),

              and_(
@@ -41,15 +64,14 @@ 

      return query.all()

  

  

- def get_widget_instance(hub, idx):

+ def get_widget_instance(idx):

      """ Utility shorthand to get a widget and 404 if not found. """

      try:

          idx = int(idx)

      except (TypeError, ValueError):

          flask.abort(400)

      try:

-         return Widget.query.join(Hub).filter(

-                 Hub.name == hub,

+         return Widget.query.filter(

                  Widget.idx == idx,

              ).one()

      except NoResultFound:
@@ -60,15 +82,16 @@ 

      current_user = flask.g.auth.copy()

      if flask.g.auth.logged_in:

          user = flask.g.user

-         current_user["hub"] = flask.url_for("hub", name=user.username)

+         current_user["hub"] = flask.url_for(

+             "hub", hub_name=user.username, hub_type="u")

          current_user["stream"] = flask.url_for("stream")

  

          current_user["memberships"] = []

          for hub in user.bookmarks["memberships"]:

              current_user["memberships"].append({

                  "name": hub.name,

-                 "user_hub": hub.user_hub,

-                 "url": flask.url_for("hub", name=hub.name),

+                 "type": hub.hub_type,

+                 "url": hub.url,

                  "cssClass": "idle-{}".format(hub.activity_class),

              })

  
@@ -76,8 +99,8 @@ 

          for hub in user.bookmarks["starred"]:

              current_user["starred_hubs"].append({

                  "name": hub.name,

-                 "user_hub": hub.user_hub,

-                 "url": flask.url_for("hub", name=hub.name),

+                 "type": hub.hub_type,

+                 "url": hub.url,

                  "cssClass": "idle-{}".format(hub.activity_class),

              })

  
@@ -85,8 +108,8 @@ 

          for hub in user.bookmarks["subscriptions"]:

              current_user["subscriptions"].append({

                  "name": hub.name,

-                 "user_hub": hub.user_hub,

-                 "url": flask.url_for("hub", name=hub.name),

+                 "type": hub.hub_type,

+                 "url": hub.url,

                  "cssClass": "idle-{}".format(hub.activity_class),

              })

  
@@ -99,7 +122,7 @@ 

      if flask.g.auth.logged_in:

          user = flask.g.user

          menu.append({

-             "url": flask.url_for("hub", name=user.username),

+             "url": flask.url_for("hub", hub_name=user.username, hub_type="u"),

              "icon": "home",

              "text": "My Hub",

          })
@@ -110,7 +133,7 @@ 

          })

          for hub in user.bookmarks:

              menu.append({

-                 "url": flask.url_for("hub", name=hub.name),

+                 "url": hub.url,

                  "icon": "bookmark",

                  "text": hub.name,

                  "cssClass": "idle-{}".format(hub.activity_class),
@@ -269,13 +292,17 @@ 

      return True

  

  

- def require_hub_access(action, url_param="name", json=False):

+ def require_hub_access(action, json=False):

      """View decorator to check access to the hub for the specified action."""

      def decorator(function):

          @functools.wraps(function)

          def wrapper(*args, **kwargs):

-             hub_name = kwargs[url_param]

-             hub = get_hub(hub_name)

+             if "hub_id" in kwargs:

+                 hub = get_hub_by_id(kwargs["hub_id"])

+             elif "hub_name" in kwargs and "hub_type" in kwargs:

+                 hub = get_hub(kwargs["hub_name"], kwargs["hub_type"])

+             else:

+                 flask.abort(400)

              check_hub_access(hub, action, json)

              return function(*args, **kwargs)

          return wrapper

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

  

  from hubs.app import app

  from hubs.utils import hubname2monogramcolour

- from hubs.utils.views import (get_hub, get_user_permissions,

+ from hubs.utils.views import (get_hub_by_id, get_user_permissions,

                                require_hub_access, query_hubs)

  

  
@@ -13,18 +13,18 @@ 

      data = []

      for hub in query_hubs(searchterm):

          props = hub.get_props()

-         if not hub.user_hub:

+         if hub.hub_type == "team":

              props["monogram_colour"] = hubname2monogramcolour(hub.name)

-         props["hub_url"] = flask.url_for('hub', name=hub.name)

+         props["hub_url"] = hub.url

          data.append(props)

      result = {"status": "OK", "data": data}

      return flask.jsonify(result)

  

  

- @app.route('/api/hubs/<name>/', methods=['GET'])

+ @app.route('/api/hubs/<hub_id>/', methods=['GET'])

  @require_hub_access("view", json=True)

- def api_hub(name):

-     hub = get_hub(name)

+ def api_hub(hub_id):

+     hub = get_hub_by_id(hub_id)

      data = hub.get_props()

      data["perms"] = get_user_permissions(hub)

      result = {"status": "OK", "data": data}

@@ -6,21 +6,21 @@ 

  

  from hubs.app import app

  from hubs.utils.views import (

-     get_hub, require_hub_access, authenticated, get_user_permissions

+     get_hub_by_id, require_hub_access, authenticated, get_user_permissions

      )

  

  log = logging.getLogger(__name__)

  

  

- @app.route('/api/hubs/<hub>/<role>s', methods=['POST', 'DELETE'])

- @require_hub_access("view", url_param="hub", json=True)

- def api_hub_associations(hub, role):

+ @app.route('/api/hubs/<hub_id>/<role>s', methods=['POST', 'DELETE'])

+ @require_hub_access("view", json=True)

+ def api_hub_associations(hub_id, role):

      if not authenticated():

          return flask.jsonify({

              "status": "ERROR",

              "message": "You must be logged-in",

          }), 403

-     hub = get_hub(hub)

+     hub = get_hub_by_id(hub_id)

      if flask.request.method == "POST":

          if role == "owner":

              return flask.jsonify({

file modified
+7 -7
@@ -8,7 +8,7 @@ 

  from hubs.app import app

  from hubs.signals import hub_updated

  from hubs.utils.views import (

-     get_hub, get_user_permissions, check_hub_access, require_hub_access,

+     get_hub_by_id, get_user_permissions, check_hub_access, require_hub_access,

      )

  

  log = logging.getLogger(__name__)
@@ -18,10 +18,10 @@ 

      pass

  

  

- @app.route('/api/hubs/<name>/config', methods=['GET', 'PUT'])

+ @app.route('/api/hubs/<hub_id>/config', methods=['GET', 'PUT'])

  @require_hub_access("view", json=True)

- def api_hub_config(name):

-     hub = get_hub(name)

+ def api_hub_config(hub_id):

+     hub = get_hub_by_id(hub_id)

      if flask.request.method == 'PUT':

          check_hub_access(hub, "config", json=True)

          old_config = hub.config.to_dict()
@@ -114,11 +114,11 @@ 

      flask.g.db.commit()

  

  

- @app.route('/api/hubs/<name>/config/suggest-users')

+ @app.route('/api/hubs/<hub_id>/config/suggest-users')

  @require_hub_access("config", json=True)

- def api_hub_config_suggest_users(name):

+ def api_hub_config_suggest_users(hub_id):

      MAX_SUGGESTS = 10

-     hub = get_hub(name)

+     hub = get_hub_by_id(hub_id)

      results = flask.g.db.query(

          hubs.models.User.username,

          hubs.models.User.fullname,

file modified
+17 -17
@@ -7,7 +7,7 @@ 

  from hubs.signals import widget_updated

  from hubs.widgets import registry

  from hubs.utils.views import (

-     create_widget_instance, configure_widget_instance, get_hub,

+     create_widget_instance, configure_widget_instance, get_hub_by_id,

      get_widget_instance, WidgetConfigError,

      check_hub_access, require_hub_access,

      move_widget, reorder_widgets,
@@ -16,22 +16,22 @@ 

  log = logging.getLogger(__name__)

  

  

- @app.route('/api/hubs/<hub>/available-widgets/', methods=['GET'])

- def api_widgets(hub):

+ @app.route('/api/hubs/<hub_id>/available-widgets/', methods=['GET'])

+ def api_widgets(hub_id):

      widgets = []

-     hub = get_hub(hub)

-     for widget in registry.values():

-         if hub.user_hub and 'user' in widget.hub_types:

-             widgets.append(widget.get_props(None))

-         if not hub.user_hub and 'group' in widget.hub_types:

-             widgets.append(widget.get_props(None))

+     hub = get_hub_by_id(hub_id)

+     widgets = [

+         widget.get_props(None)

+         for widget in registry.values()

+         if hub.hub_type in widget.hub_types

+         ]

      return flask.jsonify({"status": "OK", "data": widgets})

  

  

- @app.route('/api/hubs/<hub>/widgets/', methods=['GET', 'POST'])

- @require_hub_access("view", url_param="hub", json=True)

- def api_hub_widgets(hub):

-     hub = get_hub(hub)

+ @app.route('/api/hubs/<hub_id>/widgets/', methods=['GET', 'POST'])

+ @require_hub_access("view", json=True)

+ def api_hub_widgets(hub_id):

+     hub = get_hub_by_id(hub_id)

      try:

          user = flask.g.user

      except AttributeError:
@@ -74,11 +74,11 @@ 

      return flask.jsonify({"status": "OK", "data": widgets})

  

  

- @app.route('/api/hubs/<hub>/widgets/<int:idx>/',

+ @app.route('/api/hubs/<int:hub_id>/widgets/<int:idx>/',

             methods=['GET', 'PUT', 'DELETE'])

- @require_hub_access("view", url_param="hub", json=True)

- def api_hub_widget(hub, idx):

-     widget_instance = get_widget_instance(hub, idx)

+ @require_hub_access("view", json=True)

+ def api_hub_widget(hub_id, idx):

+     widget_instance = get_widget_instance(idx)

      hub = widget_instance.hub

      try:

          user = flask.g.user

file modified
+9 -9
@@ -9,11 +9,11 @@ 

      )

  

  

- @app.route('/<name>/')

+ @app.route('/<hub_type>/<hub_name>/')

  @require_hub_access("view")

  @login_required

- def hub(name):

-     hub = get_hub(name)

+ def hub(hub_type, hub_name):

+     hub = get_hub(hub_name, hub_type)

      global_config = {

          "chat_networks": app.config["CHAT_NETWORKS"],

          "hub_visibility": hubs.models.constants.VISIBILITIES,
@@ -21,13 +21,13 @@ 

          "dev_platforms": hubs.models.constants.DEV_PLATFORMS,

      }

      urls = {

-         "widgets": flask.url_for("api_hub_widgets", hub=hub.name),

-         "availableWidgets": flask.url_for("api_widgets", hub=hub.name),

-         "sse": get_sse_url("hub/{}".format(hub.name)),

-         "hub": flask.url_for("api_hub", name=hub.name),

-         "hubConfig": flask.url_for("api_hub_config", name=hub.name),

+         "widgets": flask.url_for("api_hub_widgets", hub_id=hub.id),

+         "availableWidgets": flask.url_for("api_widgets", hub_id=hub.id),

+         "sse": get_sse_url("hub/{}".format(hub.id)),

+         "hub": flask.url_for("api_hub", hub_id=hub.id),

+         "hubConfig": flask.url_for("api_hub_config", hub_id=hub.id),

          "hubConfigSuggestUsers": flask.url_for(

-             "api_hub_config_suggest_users", name=hub.name),

+             "api_hub_config_suggest_users", hub_id=hub.id),

          "user": flask.url_for("api_user"),

          "allGroups": flask.url_for("groups"),

      }

file modified
+4 -4
@@ -16,8 +16,7 @@ 

  @app.route('/')

  def index():

      if authenticated():

-         return flask.redirect(flask.url_for('stream',

-                                             name=flask.g.auth.nickname))

+         return flask.redirect(flask.url_for('stream'))

      return flask.render_template(

          'index.html'

      )
@@ -41,7 +40,8 @@ 

      for group in groups:

          hub = group.get_props()

          hub["monogram_colour"] = hubname2monogramcolour(hub["name"])

-         hub["hub_url"] = flask.url_for('hub', name=hub["name"])

+         hub["hub_url"] = flask.url_for(

+             'hub', hub_name=hub["name"], hub_type="t")

  

          member_count = len(hub["users"]["member"])

          owner_count = len(hub["users"]["owner"])
@@ -53,7 +53,7 @@ 

          hubslist.append(hub)

  

      name_of_the_month = app.config.get('HUB_OF_THE_MONTH')

-     hub_of_the_month = hubs.models.Hub.by_name(name_of_the_month)

+     hub_of_the_month = hubs.models.Hub.by_name(name_of_the_month, "team")

      if hub_of_the_month:

          hub_of_the_month = hub_of_the_month.get_props()

      else:

file modified
+8 -66
@@ -1,12 +1,10 @@ 

  from __future__ import unicode_literals, absolute_import

  

  import flask

- import hubs.models

- import hubs.feed

  

  from hubs.app import app

  from hubs.utils.views import (

-     login_required, get_sse_url, get_user_details)

+     login_required, get_hub, get_sse_url, get_user_details)

  

  

  @app.route('/stream')
@@ -14,10 +12,13 @@ 

  @login_required

  def stream():

      current_user = get_user_details()

+     stream = get_hub(flask.g.user.username, "stream")

      urls = {

-         "sse": get_sse_url("user/{}".format(current_user["nickname"])),

-         "notifications": flask.url_for("stream_existing"),

-         "saved": flask.url_for("saved_notifs"),

+         "sse": get_sse_url("hub/{}".format(stream.id)),

+         "hub": flask.url_for("api_hub", hub_id=stream.id),

+         "hubConfig": flask.url_for("api_hub_config", hub_id=stream.id),

+         "widgets": flask.url_for("api_hub_widgets", hub_id=stream.id),

+         "availableWidgets": flask.url_for("api_widgets", hub_id=stream.id),

          "allGroups": flask.url_for("groups"),

      }

      flash_messages = [
@@ -29,69 +30,10 @@ 

          page_title="My Stream",

          initial_state=dict(

              ui=dict(

-                 page="Streams",

+                 page="Stream",

                  flashMessages=flash_messages,

              ),

              urls=urls,

              currentUser=current_user,

              ),

          )

- 

- 

- @app.route('/stream/existing')

- @login_required

- def stream_existing():

-     username = flask.g.user.username

-     feed = hubs.feed.Notifications(username)

-     existing = hubs.feed.format_msgs(feed.get())  # TODO: paging?

-     # Right now, stream and actions are the same.

-     # Once mentions is implemented, then each will be its own.

-     return flask.jsonify(dict(

-         status="OK", data=existing,

-         ))

- 

- 

- @app.route('/stream/saved', methods=['GET', 'POST'])

- @app.route('/stream/saved/', methods=['GET', 'POST'])

- @login_required

- def saved_notifs():

-     user = flask.g.user

-     if flask.request.method == "GET":

-         saved = hubs.models.SavedNotification.by_username(user.username)

-         return flask.jsonify(dict(

-             status="OK", data=[n.__json__() for n in saved],

-             ))

-     elif flask.request.method == "POST":

-         data = flask.request.get_json()

-         try:

-             markup = data['markup']

-             link = data['link']

-             icon = data['secondary_icon']

-             dom_id = data['dom_id']

-         except Exception:

-             return flask.abort(400)

-         notification = hubs.models.SavedNotification(

-             username=user.username,

-             markup=markup,

-             link=link,

-             secondary_icon=icon,

-             dom_id=dom_id

-         )

-         flask.g.db.add(notification)

-         flask.g.db.commit()

-         return flask.jsonify(dict(

-             status="OK", data=notification.__json__(),

-             ))

- 

- 

- @app.route('/stream/saved/<int:idx>', methods=['DELETE'])

- @app.route('/stream/saved/<int:idx>/', methods=['DELETE'])

- @login_required

- def delete_notifs(idx):

-     notification = flask.g.db.query(

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

-     if not notification:

-         return flask.abort(400)

-     flask.g.db.delete(notification)

-     flask.g.db.commit()

-     return flask.jsonify(dict(status="OK"))

file modified
+12 -11
@@ -5,12 +5,14 @@ 

  import flask

  import jinja2

  import six

- 

  from importlib import import_module

+ 

+ from hubs.models.constants import HUB_TYPES

+ from hubs.utils import hub2groupavatar

+ 

  from .caching import CachedFunction

  from .view import WidgetView

  from .parameters import WidgetParameter

- from hubs.utils import hub2groupavatar

  

  log = logging.getLogger(__name__)

  
@@ -30,7 +32,7 @@ 

              should be one of the following values: ``left``, ``right``, or

              ``both``.

          hub_types (list): The hub types the widget is available on. By default

-             it contains all the hubs types, i.e. ``['user','group']``

+             it contains all the hubs types, i.e. ``['user','team']``

          parameters (list): A list of dictionaries that describe a widget's

              configuration. See :py:class:`hubs.widgets.base.WidgetParameter`.

          views_module (list): The Python path to the module where the widget
@@ -60,7 +62,7 @@ 

      is_react = False

      is_large = False

      hidden_if_empty = False

-     hub_types = ['user', 'group']

+     hub_types = HUB_TYPES

      reload_on_hub_config_change = False

  

      def __init__(self):
@@ -101,9 +103,9 @@ 

              raise AttributeError(

                  '"position" attribute is not: `left`, `right` or `both`'

                  )

-         if not set(self.hub_types).issubset(['user', 'group']):

+         if not set(self.hub_types).issubset(HUB_TYPES):

              raise AttributeError(

-                 '"hub_types" attributes must only contain: `user` and `group`'

+                 '"hub_types" attributes must only contain: `user` and `team`'

                  )

          root_view = self.get_views().get("root")

          if root_view is None:
@@ -204,7 +206,7 @@ 

                  endpoint = endpoint.encode("ascii", "replace")

              view_func = view_class.as_view(endpoint, self)

              for url_rule in view_class.url_rules:

-                 rule = "/<hub>/w/%s/<int:idx>/%s" % (

+                 rule = "/widgets/%s/<int:idx>/%s" % (

                      self.name, url_rule.lstrip("/"))

                  app.add_url_rule(rule, view_func=view_func)

  
@@ -274,7 +276,8 @@ 

                  "position": "left" if instance.left else "right",

                  "index": instance.index,

                  "selfUrl": flask.url_for(

-                     "api_hub_widget", hub=instance.hub.name, idx=instance.idx),

+                     "api_hub_widget",

+                     hub_id=instance.hub.id, idx=instance.idx),

                  "config": {},

              })

              for param in self.get_parameters():
@@ -286,7 +289,5 @@ 

                  props["component"] = self.name

              else:

                  props["contentUrl"] = flask.url_for(

-                     "%s_root" % self.name,

-                     hub=instance.hub.name,

-                     idx=instance.idx)

+                     "%s_root" % self.name, idx=instance.idx)

          return props

@@ -40,7 +40,7 @@ 

          get_issues = GetIssues(instance)

          return dict(

              username=instance.config["username"],

-             issues=get_issues()

+             issues=get_issues() or []

              )

  

  
@@ -58,7 +58,11 @@ 

          for pkg_name in owned:

              if len(issues) == max_num:

                  break

-             pkg_details = pkgwat.api.bugs(pkg_name)

+             try:

+                 pkg_details = pkgwat.api.bugs(pkg_name)

+             except ValueError:

+                 # Usually a JSON decoding error. Fail now and don't cache.

+                 return None

              for row in pkg_details['rows']:

                  if len(issues) == max_num:

                      break

@@ -18,12 +18,9 @@ 

      def get_props(self, instance, *args, **kwargs):

          props = super(Contact, self).get_props(instance, *args, **kwargs)

          if instance is not None:

-             hub_name = instance.hub.name

              props["urls"] = dict(

-                 data=flask.url_for(

-                     "contact_data", hub=hub_name, idx=instance.idx),

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

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

-                 # karma=flask.url_for(

-                 #     "contact_plus_plus", hub=hub_name, idx=instance.idx),

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

                  )

          return props

file modified
+13 -28
@@ -1,19 +1,10 @@ 

  from __future__ import unicode_literals, absolute_import

  

- 

- import logging

- 

  import flask

  

- from hubs.feed import format_msgs

+ from hubs import models

  from hubs.utils import validators

  from hubs.widgets.base import Widget

- from hubs.widgets.view import WidgetView

- 

- from .functions import GetData

- 

- 

- log = logging.getLogger('hubs.widgets')

  

  

  class Feed(Widget):
@@ -30,29 +21,23 @@ 

              "help": "Max number of feed messages to display.",

          }]

      cached_functions_module = ".functions"

+     views_module = ".views"

      is_react = True

  

      def get_props(self, instance, *args, **kwargs):

          props = super(Feed, self).get_props(instance, *args, **kwargs)

          if instance is not None:

-             hub_name = instance.hub.name

              props.update(dict(

-                 url=flask.url_for(

-                     "feed_existing", hub=hub_name, idx=instance.idx),

+                 url_existing=flask.url_for("feed_existing", idx=instance.idx),

+                 url_saved=flask.url_for("feed_saved", idx=instance.idx),

                  ))

+             if instance.hub.hub_type == "stream":

+                 user_feed_widget = models.Widget.query.join(models.Hub).filter(

+                     models.Hub.name == instance.hub.name,

+                     models.Hub.hub_type == "user",

+                     models.Widget.plugin == "feed",

+                     ).first()

+                 if user_feed_widget is not None:

+                     props["url_actions"] = flask.url_for(

+                         "feed_existing", idx=user_feed_widget.idx),

          return props

- 

- 

- class ExistingView(WidgetView):

- 

-     name = "existing"

-     url_rules = ["/existing"]

-     json = True

- 

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

-         get_data = GetData(instance)

-         existing = format_msgs(get_data())

-         return {

-             "status": "OK",

-             "data": existing,

-         }

@@ -3,23 +3,48 @@ 

  

  import fedmsg.meta

  

- from hubs.feed import Activity, add_dom_id

+ from hubs.feed import Activity, add_notif_id

+ from hubs.models import Hub

  from hubs.widgets.caching import CachedFunction

  

  

  class GetData(CachedFunction):

-     """Get the feed data from Redis and aggregate it."""

+     """Get the feed data from MongoDB and aggregate it."""

  

      def execute(self):

-         hub_name = self.instance.hub.name

-         feed = Activity(hub_name)

+         hub_id = self.instance.hub.id

+         feed = Activity(hub_id)

          raw_msgs = feed.get()  # TODO: paging?

          msgs = fedmsg.meta.conglomerate(raw_msgs)

          limit = self.instance.config["message_limit"]

-         return [add_dom_id(msg) for msg in msgs[:limit]]

+         return [add_notif_id(msg) for msg in msgs[:limit]]

  

      def should_invalidate(self, message):

          if "_hubs" not in message:

              return False

-         hub_name = self.instance.hub.name

-         return (hub_name in message["_hubs"])

+         hub_id = self.instance.hub.id

+         return (hub_id in message["_hubs"])

+ 

+ 

+ class GetStreamExisting(CachedFunction):

+     """Dummy function to trigger a reload of the Stream's feed widget.

+ 

+     The user's stream page has a feed widget that gets the actions from the

+     users's public hub's feed widget, and not its own. As a result, no SSE

+     event will be emitted if an action is added to the public hub, and as a

+     result the widget won't be live-reloaded.

+ 

+     This dummy cached function caches nothing and triggers a reload when the

+     user's public hub gets a new action.

+     """

+ 

+     def execute(self):

+         return None

+ 

+     def should_invalidate(self, message):

+         if "_hubs" not in message:

+             return False

+         if self.instance.hub.hub_type != "stream":

+             return False  # Only trigger for stream hubs.

+         user_hub = Hub.by_name(self.instance.hub.name, "user")

+         return (user_hub.id in message["_hubs"])

@@ -0,0 +1,88 @@ 

+ from __future__ import unicode_literals, absolute_import

+ 

+ import flask

+ 

+ from hubs.feed import format_msgs

+ from hubs.models import SavedNotification

+ from hubs.widgets.view import WidgetView

+ 

+ from .functions import GetData

+ 

+ 

+ class ExistingView(WidgetView):

+ 

+     name = "existing"

+     url_rules = ["/existing"]

+     json = True

+ 

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

+         get_data = GetData(instance)

+         existing = format_msgs(get_data())

+         return {

+             "status": "OK",

+             "data": existing,

+         }

+ 

+ 

+ class SavedView(WidgetView):

+ 

+     name = "saved"

+     url_rules = ["/saved/"]

+     methods = ['GET', 'POST']

+     json = True

+ 

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

+         user = flask.g.user

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

+             data = flask.request.get_json()

+             try:

+                 markup = data['markup']

+                 link = data['link']

+                 icon = data['icon']

+                 notif_id = data['notif_id']

+                 timestamp = data['timestamp']

+             except Exception:

+                 return flask.abort(400)

+             existing = SavedNotification.query.filter_by(

+                 user=user, notif_id=notif_id)

+             if existing.count() > 0:

+                 return dict(

+                     status="OK", data=existing.first().to_dict(),

+                     )

+             notification = SavedNotification(

+                 user=user,

+                 markup=markup,

+                 link=link,

+                 icon=icon,

+                 notif_id=notif_id,

+                 timestamp=timestamp,

+             )

+             flask.g.db.add(notification)

+             flask.g.db.commit()

+             return dict(

+                 status="OK", data=notification.to_dict(),

+                 )

+         elif flask.request.method == "GET":

+             return dict(

+                 status="OK", data=[

+                     n.to_dict() for n in user.saved_notifications

+                     ],

+                 )

+ 

+ 

+ class DeleteNotifView(WidgetView):

+ 

+     name = "delete_notif"

+     url_rules = ["/saved/<notif_id>"]

+     methods = ['DELETE']

+     json = True

+ 

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

+         user = flask.g.user

+         notif = SavedNotification.query.filter_by(

+             user=user, notif_id=kwargs["notif_id"]).first()

+         if not notif:

+             return flask.abort(404)

+         flask.g.db.delete(notif)

+         flask.g.db.commit()

+         return dict(status="OK")

@@ -49,7 +49,7 @@ 

      def register_routes(self, app):

          super(Halp, self).register_routes(app)

          # Add the hubs suggestion view

-         rule = "/w/%s/hubs" % self.name

+         rule = "/widgets/%s/hubs" % self.name

          endpoint = "%s_config_hubs" % self.name

          app.add_url_rule(rule, endpoint=endpoint, view_func=hubs_suggest_view)

  
@@ -59,13 +59,9 @@ 

              allHubs=flask.url_for("halp_config_hubs"),

          )

          if instance is not None:

-             hub_name = instance.hub.name

              props["urls"].update(dict(

-                 data=flask.url_for(

-                     "halp_data", hub=hub_name, idx=instance.idx),

-                 search=flask.url_for(

-                     "halp_search", hub=hub_name, idx=instance.idx),

-                 requesters=flask.url_for(

-                     "halp_requesters", hub=hub_name, idx=instance.idx),

+                 data=flask.url_for("halp_data", idx=instance.idx),

+                 search=flask.url_for("halp_search", idx=instance.idx),

+                 requesters=flask.url_for("halp_requesters", idx=instance.idx),

                  ))

          return props

@@ -62,7 +62,8 @@ 

              "name": raw_message["msg"]["details"]["nick"],

              "avatar": fedmsg.meta.msg2secondary_icon(raw_message["msg"]),

              "url": flask.url_for(

-                 "hub", name=raw_message["msg"]["details"]["nick"]),

+                 "hub", hub_name=raw_message["msg"]["details"]["nick"],

+                 hub_type="u"),

          }

      }

      if not msg["author"]["avatar"]:

file modified
+3 -3
@@ -14,13 +14,13 @@ 

          msg (dict): the ``msg`` value in the raw message dict sent through

              Fedmsg.

      """

-     return [

+     return list(set([

          h[0] for h in

          Hub.query.join(HubConfig).filter(

              HubConfig.key == "chat_channel",

              HubConfig.value == msg["channel"]

          ).values(Hub.name)

-     ]

+     ]))

  

  

  def listofhubs_validator(value):
@@ -35,7 +35,7 @@ 

          return []

      if not isinstance(value, list):

          raise ValueError("Expected a list")

-     hub_names = [h.name for h in Hub.query.all()]

+     hub_names = [h[0] for h in Hub.query.values(Hub.name)]

      for hub in value:

          if hub not in hub_names:

              raise ValueError("hub \"{}\" does not exist".format(hub))

file modified
+3 -2
@@ -138,5 +138,6 @@ 

      if flask.request.args.get("q"):

          results = results.filter(Hub.name.ilike(

              "%s%%" % flask.request.args.get("q")))

-     results = results.order_by(Hub.name)[:MAX_SUGGESTS]

-     return flask.jsonify({"status": "OK", "data": [h.name for h in results]})

+     results = results.order_by(Hub.name).distinct().limit(

+         MAX_SUGGESTS).values(Hub.name)

+     return flask.jsonify({"status": "OK", "data": [h[0] for h in results]})

@@ -30,11 +30,8 @@ 

      def get_props(self, instance, *args, **kwargs):

          props = super(Library, self).get_props(instance, *args, **kwargs)

          if instance is not None:

-             hub_name = instance.hub.name

              props["urls"] = dict(

-                 data=flask.url_for(

-                     "library_data", hub=hub_name, idx=instance.idx),

-                 getLink=flask.url_for(

-                     "library_get_link", hub=hub_name, idx=instance.idx),

+                 data=flask.url_for("library_data", idx=instance.idx),

+                 getLink=flask.url_for("library_get_link", idx=instance.idx),

                  )

          return props

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

    {% for member in oldest_members %}

    <div class="card-block">

      <img class="img-responsive membership-avatar" src="{{ member.avatar }}" alt="Hub avatar for {{ member.name }}">

-     <a href="{{ url_for('hub', name=member.username)}}">{{ member.username }}</a>

+     <a href="{{ url_for('hub', hub_name=member.username, hub_type='u')}}">{{ member.username }}</a>

      {% if member.username in owners %}

        <p class="text-muted">Owner</p>

      {% else %}
@@ -16,7 +16,7 @@ 

    {% for member in memberships %}

    <div class="card-block">

      <img class="img-responsive membership-avatar" src="{{ member.avatar }}" alt="Hub avatar for {{ member.name }}">

-     <a href="{{ url_for('hub', name=member.username)}}">{{ member }}</a>

+     <a href="{{ url_for('hub', hub_name=member.username, hub_type='u')}}">{{ member }}</a>

      {% if member.username in owners %}

        <p class="text-muted">Owner</p>

      {% else %}
@@ -41,7 +41,7 @@ 

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

                <img class="img-responsive membership-avatar"

                  src="{{ member.avatar }}" alt="Hub avatar for {{ member.username }}">

-               <a href="{{ url_for('hub', name=member.username)}}">{{ member.username }}</a>

+               <a href="{{ url_for('hub', hub_name=member.username, hub_type='u')}}">{{ member.username }}</a>

                {% if member.username in owners %}

                <p class="text-muted">Owner</p>

                {% else %}

@@ -28,18 +28,18 @@ 

          ownerships = []

          assoc = []

          for hub in user.ownerships:

-             if not hub.user_hub:

+             if hub.hub_type == "team":

                  ownerships.append(hub)

                  assoc.append(hub.name)

          memberships = []

          for hub in user.memberships:

-             if not hub.user_hub:

+             if hub.hub_type == "team":

                  if hub.name not in assoc:

                      memberships.append(hub)

                      assoc.append(hub.name)

          subscriptions = []

          for hub in user.subscriptions:

-             if not hub.user_hub:

+             if hub.hub_type == "team":

                  if hub.name not in assoc:

                      subscriptions.append(hub)

          return dict(

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

              </div>

              <div class="media-body">

                  <div>

-                     <strong><a href="{{url_for('hub', name=ownership.name)}}">{{ownership.name}}</a></strong>

+                     <strong><a href="{{ownership.url}}">{{ownership.name}}</a></strong>

                  </div>

                  <small>Group Administrator</small>

              </div>
@@ -20,7 +20,7 @@ 

              </div>

              <div class="media-body">

                  <div>

-                     <strong><a href="{{url_for('hub', name=membership.name)}}">{{membership.name}}</a></strong>

+                     <strong><a href="{{membership.url}}">{{membership.name}}</a></strong>

                  </div>

                  <small>Member</small>

              </div>
@@ -33,7 +33,7 @@ 

              </div>

              <div class="media-body">

                  <div>

-                     <strong><a href="{{url_for('hub', name=subscription.name)}}">{{subscription.name}}</a></strong>

+                     <strong><a href="{{subscription.url}}">{{subscription.name}}</a></strong>

                  </div>

                  <small>Subscribed</small>

              </div>

@@ -16,7 +16,7 @@ 

      position = "both"

      display_css = "card-info"

      display_title = None

-     hub_types = ['group']

+     hub_types = ['team']

      parameters = [

          dict(

              name="link",

@@ -9,7 +9,7 @@ 

        {% for owner in oldest_owners %}

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

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

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

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

          </div>

          {% endfor %}

          <br/>
@@ -18,7 +18,7 @@ 

        {% for owner in owners %}

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

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

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

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

          </div>

        {% endfor %}

      {% endif %}
@@ -72,7 +72,7 @@ 

          {% for owner in owners %}

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

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

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

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

          </div>

          {% endfor %}

        </div>

file modified
+3 -5
@@ -29,9 +29,8 @@ 

      name as ``<widget_name>_<view_name>``, for example ``meetings_root``.

      Remember that when you want to reverse the URL with :py:meth:`url_for`.

  

-     When reversing the URL, you need to pass the ``hub`` and ``idx`` kwargs,

-     which are respectively the hub name (:py:attr:`hubs.models.Hub.name`) and

-     the widget instance (the database record) primary key

+     When reversing the URL, you need to pass the ``idx`` kwarg,

+     which is the widget instance (the database record) primary key

      (:py:attr:`hubs.models.Widget.idx`).

  

      Attributes:
@@ -95,9 +94,8 @@ 

  

      def _get_instance(self, *args, **kwargs):

          from hubs.utils.views import get_widget_instance

-         hubname = kwargs.pop("hub")

          widgetidx = kwargs.pop("idx")

-         return get_widget_instance(hubname, widgetidx)

+         return get_widget_instance(widgetidx)

  

      def dispatch_request(self, *args, **kwargs):

          """

file modified
+1 -1
@@ -80,7 +80,7 @@ 

              if any([name.endswith(suffix) for suffix in suffix_blacklist]):

                  continue

  

-             hub = hubs.models.Hub.get(name)

+             hub = hubs.models.Hub.by_name(name, "team")

              if hub is None:

                  hub = hubs.models.Hub.create_group_hub(

                      name=name,

file modified
+9 -5
@@ -39,7 +39,7 @@ 

  db.commit()

  

  # ############# Internationalizationteam

- hub = hubs.models.Hub(name='i18n')

+ hub = hubs.models.Hub(name='i18n', hub_type="team")

  db.add(hub)

  hub.config.update(dict(

      summary='The Internationalization Team',
@@ -95,9 +95,13 @@ 

  db.commit()

  

  # ############# CommOps

- hub = hubs.models.Hub(name='commops')

+ hub = hubs.models.Hub(name='commops', hub_type="team")

  db.add(hub)

  hub.config["summary"] = 'The Fedora Community Operations Team'

+ hub.config["chat_domain"] = 'irc.freenode.net'

+ hub.config["chat_channel"] = '#fedora-hubs'

+ hub.config["pagure"] = 'fedora-hubs'

+ hub.config["calendar"] = 'commops'

  

  widget = hubs.models.Widget(

      plugin='rules', index=1, _config=json.dumps({
@@ -144,7 +148,7 @@ 

  db.commit()

  

  # ############# Marketing team

- hub = hubs.models.Hub(name='marketing')

+ hub = hubs.models.Hub(name='marketing', hub_type="team")

  db.add(hub)

  hub.config["summary"] = 'The Fedora Marketing Team'

  
@@ -200,7 +204,7 @@ 

  db.commit()

  

  # ############# Design team

- hub = hubs.models.Hub(name='designteam')

+ hub = hubs.models.Hub(name='designteam', hub_type="team")

  db.add(hub)

  hub.config["summary"] = 'The Fedora Design Team'

  
@@ -253,7 +257,7 @@ 

  

  

  # ############# Infra team

- hub = hubs.models.Hub(name='infrastructure')

+ hub = hubs.models.Hub(name='infrastructure', hub_type="team")

  db.add(hub)

  hub.config["summary"] = 'The Fedora Infra Team'

  

Fixes #158

Once again, a big batch of commits. Please read one by one in order.

3 new commits added

  • Sort a user's SavedNotifications by reverse creation order
  • Merge branch 'fix/fedmsg-consumer-config' into feature/stream
  • Solve the issue of the stream's action feed not reloading
6 years ago

rebased onto 3b5afed

6 years ago

:thumbsup: looks good to me -- tests all pass too which is awesome!

thanks abompard!

Pull-Request has been merged by abompard

6 years ago
Metadata
Changes Summary 93
+1 -1
file changed
delete-user.py
+12 -5
file changed
hubs/backend/consumer.py
+1 -1
file changed
hubs/backend/triage.py
+21 -0
file changed
hubs/defaults.py
+42 -28
file changed
hubs/feed.py
+2 -3
file changed
hubs/models/association.py
+2 -0
file changed
hubs/models/constants.py
+42 -18
file changed
hubs/models/hub.py
+1 -1
file changed
hubs/models/hubconfig.py
+20 -32
file changed
hubs/models/savednotification.py
+11 -3
file changed
hubs/models/user.py
+2 -5
file changed
hubs/models/widget.py
+73 -65
file changed
hubs/static/client/app/components/HubConfig/HubConfigDialog.js
+10 -12
file changed
hubs/static/client/app/components/HubConfig/HubConfigPanelChat.js
+2 -2
file changed
hubs/static/client/app/components/HubConfig/HubConfigPanelGeneral.js
+0 -0
file renamed
hubs/static/client/app/components/EditModeButton.css
hubs/static/client/app/components/HubHeader/EditModeButton.css
+1 -1
file renamed
hubs/static/client/app/components/EditModeButton.js
hubs/static/client/app/components/HubHeader/EditModeButton.js
+25
file added
hubs/static/client/app/components/HubHeader/HubAvatar.js
+0 -0
file renamed
hubs/static/client/app/components/HubHeader.css
hubs/static/client/app/components/HubHeader/HubHeader.css
+36
file added
hubs/static/client/app/components/HubHeader/HubHeaderLeft.js
+26
file added
hubs/static/client/app/components/HubHeader/HubHeaderRight.js
+4 -4
file renamed
hubs/static/client/app/components/HubMembership.js
hubs/static/client/app/components/HubHeader/HubMembership.js
+0 -0
file renamed
hubs/static/client/app/components/HubStar.css
hubs/static/client/app/components/HubHeader/HubStar.css
+1 -1
file renamed
hubs/static/client/app/components/HubStar.js
hubs/static/client/app/components/HubHeader/HubStar.js
+0 -0
file renamed
hubs/static/client/app/components/HubStats.css
hubs/static/client/app/components/HubHeader/HubStats.css
+3 -3
file renamed
hubs/static/client/app/components/HubStats.js
hubs/static/client/app/components/HubHeader/HubStats.js
+16
file added
hubs/static/client/app/components/HubHeader/StreamHeaderLeft.js
+4 -4
file renamed
hubs/static/client/app/components/StreamsHeader.js
hubs/static/client/app/components/HubHeader/StreamHeaderRight.js
+29 -44
file renamed
hubs/static/client/app/components/HubHeader.js
hubs/static/client/app/components/HubHeader/index.js
-0
file removed
hubs/static/client/app/components/HubPage.css
+0 -1
file changed
hubs/static/client/app/components/HubPage.js
+28 -26
file changed
hubs/static/client/app/components/LeftMenu.js
+13 -13
file renamed
hubs/static/client/app/components/StreamsPage.js
hubs/static/client/app/components/StreamPage.js
-193
file removed
hubs/static/client/app/components/Streams.js
+1 -1
file changed
hubs/static/client/app/components/feed/Actions.js
+2 -2
file changed
hubs/static/client/app/components/feed/Feed.js
+26 -18
file changed
hubs/static/client/app/components/feed/ItemsGetter.js
+1 -1
file changed
hubs/static/client/app/components/feed/__tests__/Feed.test.js
+3 -3
file changed
hubs/static/client/app/core/Pages.js
+54
file added
hubs/static/client/app/widgets/feed/ActionsFeed.js
+255
file added
hubs/static/client/app/widgets/feed/StreamFeed.js
+25 -37
file changed
hubs/static/client/app/widgets/feed/Widget.js
+4 -4
file changed
hubs/static/js/search.js
+6 -6
file changed
hubs/templates/master.html
+9 -5
file changed
hubs/tests/__init__.py
+2 -2
file changed
hubs/tests/backend/test_triage.py
+11 -11
file changed
hubs/tests/models/test_hub.py
+1 -1
file changed
hubs/tests/models/test_hub_config.py
+6 -6
file changed
hubs/tests/models/test_user.py
+2 -2
file changed
hubs/tests/models/test_widget.py
+43 -36
file changed
hubs/tests/test_feed.py
+10 -6
file changed
hubs/tests/test_widget_base.py
+2 -2
file changed
hubs/tests/utils/test_views.py
+20 -20
file changed
hubs/tests/views/test_api_association.py
+28 -20
file changed
hubs/tests/views/test_api_hub_config.py
+55 -44
file changed
hubs/tests/views/test_api_hub_widget.py
+5 -5
file changed
hubs/tests/views/test_hub_view.py
+1 -1
file changed
hubs/tests/views/test_root.py
-144
file removed
hubs/tests/views/test_user.py
+4 -4
file changed
hubs/tests/widgets/__init__.py
+8 -8
file changed
hubs/tests/widgets/test_contact.py
+103
file added
hubs/tests/widgets/test_feed.py
+31 -29
file changed
hubs/tests/widgets/test_halp.py
+8 -6
file changed
hubs/tests/widgets/test_library.py
+2 -2
file changed
hubs/tests/widgets/test_meetings.py
+5 -5
file changed
hubs/tests/widgets/test_my_hubs.py
+45 -18
file changed
hubs/utils/views.py
+6 -6
file changed
hubs/views/api/hub.py
+5 -5
file changed
hubs/views/api/hub_association.py
+7 -7
file changed
hubs/views/api/hub_config.py
+17 -17
file changed
hubs/views/api/hub_widget.py
+9 -9
file changed
hubs/views/hub.py
+4 -4
file changed
hubs/views/root.py
+8 -66
file changed
hubs/views/user.py
+12 -11
file changed
hubs/widgets/base.py
+6 -2
file changed
hubs/widgets/bugzilla/__init__.py
+2 -5
file changed
hubs/widgets/contact/__init__.py
+13 -28
file changed
hubs/widgets/feed/__init__.py
+32 -7
file changed
hubs/widgets/feed/functions.py
+88
file added
hubs/widgets/feed/views.py
+4 -8
file changed
hubs/widgets/halp/__init__.py
+2 -1
file changed
hubs/widgets/halp/functions.py
+3 -3
file changed
hubs/widgets/halp/utils.py
+3 -2
file changed
hubs/widgets/halp/views.py
+2 -5
file changed
hubs/widgets/library/__init__.py
+3 -3
file changed
hubs/widgets/memberships/templates/root.html
+3 -3
file changed
hubs/widgets/my_hubs/__init__.py
+3 -3
file changed
hubs/widgets/my_hubs/templates/root.html
+1 -1
file changed
hubs/widgets/rules/__init__.py
+3 -3
file changed
hubs/widgets/rules/templates/root.html
+3 -5
file changed
hubs/widgets/view.py
+1 -1
file changed
populate-from-fas.py
+9 -5
file changed
populate.py