diff --git a/rhodecode/apps/journal/__init__.py b/rhodecode/apps/journal/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/journal/__init__.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+from rhodecode.apps._base import ADMIN_PREFIX
+
+
+def admin_routes(config):
+
+ config.add_route(
+ name='journal', pattern='/journal')
+ config.add_route(
+ name='journal_rss', pattern='/journal/rss')
+ config.add_route(
+ name='journal_atom', pattern='/journal/atom')
+
+ config.add_route(
+ name='journal_public', pattern='/public_journal')
+ config.add_route(
+ name='journal_public_atom', pattern='/public_journal/atom')
+ config.add_route(
+ name='journal_public_atom_old', pattern='/public_journal_atom')
+
+ config.add_route(
+ name='journal_public_rss', pattern='/public_journal/rss')
+ config.add_route(
+ name='journal_public_rss_old', pattern='/public_journal_rss')
+
+ config.add_route(
+ name='toggle_following', pattern='/toggle_following')
+
+
+def includeme(config):
+ config.include(admin_routes, route_prefix=ADMIN_PREFIX)
+ # Scan module for configuration decorators.
+ config.scan()
\ No newline at end of file
diff --git a/rhodecode/apps/journal/tests/__init__.py b/rhodecode/apps/journal/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/journal/tests/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
diff --git a/rhodecode/tests/functional/test_journal.py b/rhodecode/apps/journal/tests/test_journal.py
rename from rhodecode/tests/functional/test_journal.py
rename to rhodecode/apps/journal/tests/test_journal.py
--- a/rhodecode/tests/functional/test_journal.py
+++ b/rhodecode/apps/journal/tests/test_journal.py
@@ -19,24 +19,62 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import datetime
-from rhodecode.tests import TestController, url
+
+import pytest
+
+from rhodecode.apps._base import ADMIN_PREFIX
+from rhodecode.tests import TestController
from rhodecode.model.db import UserFollowing, Repository
-class TestJournalController(TestController):
+def route_path(name, params=None, **kwargs):
+ import urllib
- def test_index(self):
+ base_url = {
+ 'journal': ADMIN_PREFIX + '/journal',
+ 'journal_rss': ADMIN_PREFIX + '/journal/rss',
+ 'journal_atom': ADMIN_PREFIX + '/journal/atom',
+ 'journal_public': ADMIN_PREFIX + '/public_journal',
+ 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
+ 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
+ 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
+ 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
+ 'toggle_following': ADMIN_PREFIX + '/toggle_following',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestJournalViews(TestController):
+
+ def test_journal(self):
self.log_user()
- response = self.app.get(url(controller='journal', action='index'))
- response.mustcontain(
- """
%s
""" % datetime.date.today())
+ response = self.app.get(route_path('journal'))
+ # response.mustcontain(
+ # """%s
""" % datetime.date.today())
+
+ @pytest.mark.parametrize("feed_type, content_type", [
+ ('rss', "application/rss+xml"),
+ ('atom', "application/atom+xml")
+ ])
+ def test_journal_feed(self, feed_type, content_type):
+ self.log_user()
+ response = self.app.get(
+ route_path(
+ 'journal_{}'.format(feed_type)),
+ status=200)
+
+ assert response.content_type == content_type
def test_toggle_following_repository(self, backend):
user = self.log_user()
repo = Repository.get_by_repo_name(backend.repo_name)
repo_id = repo.repo_id
- self.app.post(url('toggle_following'), {'follows_repo_id': repo_id,
- 'csrf_token': self.csrf_token})
+ self.app.post(
+ route_path('toggle_following'), {'follows_repo_id': repo_id,
+ 'csrf_token': self.csrf_token})
followings = UserFollowing.query()\
.filter(UserFollowing.user_id == user['user_id'])\
@@ -44,8 +82,9 @@ class TestJournalController(TestControll
assert len(followings) == 0
- self.app.post(url('toggle_following'), {'follows_repo_id': repo_id,
- 'csrf_token': self.csrf_token})
+ self.app.post(
+ route_path('toggle_following'), {'follows_repo_id': repo_id,
+ 'csrf_token': self.csrf_token})
followings = UserFollowing.query()\
.filter(UserFollowing.user_id == user['user_id'])\
@@ -53,12 +92,15 @@ class TestJournalController(TestControll
assert len(followings) == 1
- def test_public_journal_atom(self):
+ @pytest.mark.parametrize("feed_type, content_type", [
+ ('rss', "application/rss+xml"),
+ ('atom', "application/atom+xml")
+ ])
+ def test_public_journal_feed(self, feed_type, content_type):
self.log_user()
- response = self.app.get(url(controller='journal',
- action='public_journal_atom'),)
+ response = self.app.get(
+ route_path(
+ 'journal_public_{}'.format(feed_type)),
+ status=200)
- def test_public_journal_rss(self):
- self.log_user()
- response = self.app.get(url(controller='journal',
- action='public_journal_rss'),)
+ assert response.content_type == content_type
diff --git a/rhodecode/apps/journal/views.py b/rhodecode/apps/journal/views.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/journal/views.py
@@ -0,0 +1,377 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2010-2017 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+import logging
+import itertools
+
+from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
+
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPBadRequest
+from pyramid.response import Response
+from pyramid.renderers import render
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.model.db import (
+ or_, joinedload, UserLog, UserFollowing, User, UserApiKeys)
+from rhodecode.model.meta import Session
+import rhodecode.lib.helpers as h
+from rhodecode.lib.helpers import Page
+from rhodecode.lib.user_log_filter import user_log_filter
+from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
+from rhodecode.lib.utils2 import safe_int, AttributeDict
+from rhodecode.model.scm import ScmModel
+
+log = logging.getLogger(__name__)
+
+
+class JournalView(BaseAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context(include_app_defaults=True)
+ self._register_global_c(c)
+ self._load_defaults(c.rhodecode_name)
+
+ # TODO(marcink): what is this, why we need a global register ?
+ c.search_term = self.request.GET.get('filter') or ''
+ return c
+
+ def _get_config(self, rhodecode_name):
+ import rhodecode
+ config = rhodecode.CONFIG
+
+ return {
+ 'language': 'en-us',
+ 'feed_ttl': '5', # TTL of feed,
+ 'feed_items_per_page':
+ safe_int(config.get('rss_items_per_page', 20)),
+ 'rhodecode_name': rhodecode_name
+ }
+
+ def _load_defaults(self, rhodecode_name):
+ config = self._get_config(rhodecode_name)
+ # common values for feeds
+ self.language = config["language"]
+ self.ttl = config["feed_ttl"]
+ self.feed_items_per_page = config['feed_items_per_page']
+ self.rhodecode_name = config['rhodecode_name']
+
+ def _get_daily_aggregate(self, journal):
+ groups = []
+ for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
+ user_group = []
+ # groupby username if it's a present value, else
+ # fallback to journal username
+ for _, g2 in itertools.groupby(
+ list(g), lambda x: x.user.username if x.user else x.username):
+ l = list(g2)
+ user_group.append((l[0].user, l))
+
+ groups.append((k, user_group,))
+
+ return groups
+
+ def _get_journal_data(self, following_repos, search_term):
+ repo_ids = [x.follows_repository.repo_id for x in following_repos
+ if x.follows_repository is not None]
+ user_ids = [x.follows_user.user_id for x in following_repos
+ if x.follows_user is not None]
+
+ filtering_criterion = None
+
+ if repo_ids and user_ids:
+ filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
+ UserLog.user_id.in_(user_ids))
+ if repo_ids and not user_ids:
+ filtering_criterion = UserLog.repository_id.in_(repo_ids)
+ if not repo_ids and user_ids:
+ filtering_criterion = UserLog.user_id.in_(user_ids)
+ if filtering_criterion is not None:
+ journal = Session().query(UserLog)\
+ .options(joinedload(UserLog.user))\
+ .options(joinedload(UserLog.repository))
+ # filter
+ try:
+ journal = user_log_filter(journal, search_term)
+ except Exception:
+ # we want this to crash for now
+ raise
+ journal = journal.filter(filtering_criterion)\
+ .order_by(UserLog.action_date.desc())
+ else:
+ journal = []
+
+ return journal
+
+ def _atom_feed(self, repos, search_term, public=True):
+ _ = self.request.translate
+ journal = self._get_journal_data(repos, search_term)
+ if public:
+ _link = h.route_url('journal_public_atom')
+ _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
+ 'atom feed')
+ else:
+ _link = h.route_url('journal_atom')
+ _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
+
+ feed = Atom1Feed(
+ title=_desc, link=_link, description=_desc,
+ language=self.language, ttl=self.ttl)
+
+ for entry in journal[:self.feed_items_per_page]:
+ user = entry.user
+ if user is None:
+ # fix deleted users
+ user = AttributeDict({'short_contact': entry.username,
+ 'email': '',
+ 'full_contact': ''})
+ action, action_extra, ico = h.action_parser(entry, feed=True)
+ title = "%s - %s %s" % (user.short_contact, action(),
+ entry.repository.repo_name)
+ desc = action_extra()
+ _url = h.route_url('home')
+ if entry.repository is not None:
+ _url = h.route_url('repo_changelog',
+ repo_name=entry.repository.repo_name)
+
+ feed.add_item(title=title,
+ pubdate=entry.action_date,
+ link=_url,
+ author_email=user.email,
+ author_name=user.full_contact,
+ description=desc)
+
+ response = Response(feed.writeString('utf-8'))
+ response.content_type = feed.mime_type
+ return response
+
+ def _rss_feed(self, repos, search_term, public=True):
+ _ = self.request.translate
+ journal = self._get_journal_data(repos, search_term)
+ if public:
+ _link = h.route_url('journal_public_atom')
+ _desc = '%s %s %s' % (
+ self.rhodecode_name, _('public journal'), 'rss feed')
+ else:
+ _link = h.route_url('journal_atom')
+ _desc = '%s %s %s' % (
+ self.rhodecode_name, _('journal'), 'rss feed')
+
+ feed = Rss201rev2Feed(
+ title=_desc, link=_link, description=_desc,
+ language=self.language, ttl=self.ttl)
+
+ for entry in journal[:self.feed_items_per_page]:
+ user = entry.user
+ if user is None:
+ # fix deleted users
+ user = AttributeDict({'short_contact': entry.username,
+ 'email': '',
+ 'full_contact': ''})
+ action, action_extra, ico = h.action_parser(entry, feed=True)
+ title = "%s - %s %s" % (user.short_contact, action(),
+ entry.repository.repo_name)
+ desc = action_extra()
+ _url = h.route_url('home')
+ if entry.repository is not None:
+ _url = h.route_url('repo_changelog',
+ repo_name=entry.repository.repo_name)
+
+ feed.add_item(title=title,
+ pubdate=entry.action_date,
+ link=_url,
+ author_email=user.email,
+ author_name=user.full_contact,
+ description=desc)
+
+ response = Response(feed.writeString('utf-8'))
+ response.content_type = feed.mime_type
+ return response
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='journal', request_method='GET',
+ renderer=None)
+ def journal(self):
+ c = self.load_default_context()
+
+ p = safe_int(self.request.GET.get('page', 1), 1)
+ c.user = User.get(self._rhodecode_user.user_id)
+ following = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ journal = self._get_journal_data(following, c.search_term)
+
+ def url_generator(**kw):
+ query_params = {
+ 'filter': c.search_term
+ }
+ query_params.update(kw)
+ return self.request.current_route_path(_query=query_params)
+
+ c.journal_pager = Page(
+ journal, page=p, items_per_page=20, url=url_generator)
+ c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
+
+ c.journal_data = render(
+ 'rhodecode:templates/journal/journal_data.mako',
+ self._get_template_context(c), self.request)
+
+ if self.request.is_xhr:
+ return Response(c.journal_data)
+
+ html = render(
+ 'rhodecode:templates/journal/journal.mako',
+ self._get_template_context(c), self.request)
+ return Response(html)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @NotAnonymous()
+ @view_config(
+ route_name='journal_atom', request_method='GET',
+ renderer=None)
+ def journal_atom(self):
+ """
+ Produce an atom-1.0 feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+ return self._atom_feed(following_repos, c.search_term, public=False)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @NotAnonymous()
+ @view_config(
+ route_name='journal_rss', request_method='GET',
+ renderer=None)
+ def journal_rss(self):
+ """
+ Produce an rss feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+ return self._rss_feed(following_repos, c.search_term, public=False)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='toggle_following', request_method='POST',
+ renderer='json_ext')
+ def toggle_following(self):
+ user_id = self.request.POST.get('follows_user_id')
+ if user_id:
+ try:
+ ScmModel().toggle_following_user(
+ user_id, self._rhodecode_user.user_id)
+ Session().commit()
+ return 'ok'
+ except Exception:
+ raise HTTPBadRequest()
+
+ repo_id = self.request.POST.get('follows_repo_id')
+ if repo_id:
+ try:
+ ScmModel().toggle_following_repo(
+ repo_id, self._rhodecode_user.user_id)
+ Session().commit()
+ return 'ok'
+ except Exception:
+ raise HTTPBadRequest()
+
+ raise HTTPBadRequest()
+
+ @LoginRequired()
+ @view_config(
+ route_name='journal_public', request_method='GET',
+ renderer=None)
+ def journal_public(self):
+ c = self.load_default_context()
+ # Return a rendered template
+ p = safe_int(self.request.GET.get('page', 1), 1)
+
+ c.following = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ journal = self._get_journal_data(c.following, c.search_term)
+
+ def url_generator(**kw):
+ query_params = {}
+ query_params.update(kw)
+ return self.request.current_route_path(_query=query_params)
+
+ c.journal_pager = Page(
+ journal, page=p, items_per_page=20, url=url_generator)
+ c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
+
+ c.journal_data = render(
+ 'rhodecode:templates/journal/journal_data.mako',
+ self._get_template_context(c), self.request)
+
+ if self.request.is_xhr:
+ return Response(c.journal_data)
+
+ html = render(
+ 'rhodecode:templates/journal/public_journal.mako',
+ self._get_template_context(c), self.request)
+ return Response(html)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @view_config(
+ route_name='journal_public_atom', request_method='GET',
+ renderer=None)
+ def journal_public_atom(self):
+ """
+ Produce an atom-1.0 feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ return self._atom_feed(following_repos, c.search_term)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @view_config(
+ route_name='journal_public_rss', request_method='GET',
+ renderer=None)
+ def journal_public_rss(self):
+ """
+ Produce an rss2 feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ return self._rss_feed(following_repos, c.search_term)
diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py
--- a/rhodecode/config/middleware.py
+++ b/rhodecode/config/middleware.py
@@ -288,6 +288,7 @@ def includeme(config):
config.include('rhodecode.apps.channelstream')
config.include('rhodecode.apps.login')
config.include('rhodecode.apps.home')
+ config.include('rhodecode.apps.journal')
config.include('rhodecode.apps.repository')
config.include('rhodecode.apps.repo_group')
config.include('rhodecode.apps.search')
diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py
--- a/rhodecode/config/routing.py
+++ b/rhodecode/config/routing.py
@@ -444,35 +444,6 @@ def make_map(config):
m.connect('my_account_password', '/my_account/password',
action='my_account_password', conditions={'method': ['GET']})
- # USER JOURNAL
- rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
- controller='journal', action='index')
- rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
- controller='journal', action='journal_rss')
- rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
- controller='journal', action='journal_atom')
-
- rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
- controller='journal', action='public_journal')
-
- rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
- controller='journal', action='public_journal_rss')
-
- rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
- controller='journal', action='public_journal_rss')
-
- rmap.connect('public_journal_atom',
- '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
- action='public_journal_atom')
-
- rmap.connect('public_journal_atom_old',
- '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
- action='public_journal_atom')
-
- rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
- controller='journal', action='toggle_following', jsroute=True,
- conditions={'method': ['POST']})
-
#==========================================================================
# REPOSITORY ROUTES
#==========================================================================
diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py
deleted file mode 100644
--- a/rhodecode/controllers/journal.py
+++ /dev/null
@@ -1,304 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2010-2017 RhodeCode GmbH
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License, version 3
-# (only), as published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-# This program is dual-licensed. If you wish to learn more about the
-# RhodeCode Enterprise Edition, including its added features, Support services,
-# and proprietary license terms, please see https://rhodecode.com/licenses/
-
-"""
-Journal / user event log controller for rhodecode
-"""
-
-import logging
-from itertools import groupby
-
-from sqlalchemy import or_
-from sqlalchemy.orm import joinedload
-
-from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
-
-from webob.exc import HTTPBadRequest
-from pylons import request, tmpl_context as c, response, url
-from pylons.i18n.translation import _
-
-from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
-from rhodecode.model.meta import Session
-import rhodecode.lib.helpers as h
-from rhodecode.lib.helpers import Page
-from rhodecode.lib.user_log_filter import user_log_filter
-from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
-from rhodecode.lib.base import BaseController, render
-from rhodecode.lib.utils2 import safe_int, AttributeDict
-
-log = logging.getLogger(__name__)
-
-
-class JournalController(BaseController):
-
- def __before__(self):
- super(JournalController, self).__before__()
- self.language = 'en-us'
- self.ttl = "5"
- self.feed_nr = 20
- c.search_term = request.GET.get('filter')
-
- def _get_daily_aggregate(self, journal):
- groups = []
- for k, g in groupby(journal, lambda x: x.action_as_day):
- user_group = []
- #groupby username if it's a present value, else fallback to journal username
- for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
- l = list(g2)
- user_group.append((l[0].user, l))
-
- groups.append((k, user_group,))
-
- return groups
-
- def _get_journal_data(self, following_repos):
- repo_ids = [x.follows_repository.repo_id for x in following_repos
- if x.follows_repository is not None]
- user_ids = [x.follows_user.user_id for x in following_repos
- if x.follows_user is not None]
-
- filtering_criterion = None
-
- if repo_ids and user_ids:
- filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
- UserLog.user_id.in_(user_ids))
- if repo_ids and not user_ids:
- filtering_criterion = UserLog.repository_id.in_(repo_ids)
- if not repo_ids and user_ids:
- filtering_criterion = UserLog.user_id.in_(user_ids)
- if filtering_criterion is not None:
- journal = self.sa.query(UserLog)\
- .options(joinedload(UserLog.user))\
- .options(joinedload(UserLog.repository))
- #filter
- try:
- journal = user_log_filter(journal, c.search_term)
- except Exception:
- # we want this to crash for now
- raise
- journal = journal.filter(filtering_criterion)\
- .order_by(UserLog.action_date.desc())
- else:
- journal = []
-
- return journal
-
- def _atom_feed(self, repos, public=True):
- journal = self._get_journal_data(repos)
- if public:
- _link = url('public_journal_atom', qualified=True)
- _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
- 'atom feed')
- else:
- _link = url('journal_atom', qualified=True)
- _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
-
- feed = Atom1Feed(title=_desc,
- link=_link,
- description=_desc,
- language=self.language,
- ttl=self.ttl)
-
- for entry in journal[:self.feed_nr]:
- user = entry.user
- if user is None:
- #fix deleted users
- user = AttributeDict({'short_contact': entry.username,
- 'email': '',
- 'full_contact': ''})
- action, action_extra, ico = h.action_parser(entry, feed=True)
- title = "%s - %s %s" % (user.short_contact, action(),
- entry.repository.repo_name)
- desc = action_extra()
- _url = None
- if entry.repository is not None:
- _url = h.route_url('repo_changelog',
- repo_name=entry.repository.repo_name)
-
- feed.add_item(title=title,
- pubdate=entry.action_date,
- link=_url or url('', qualified=True),
- author_email=user.email,
- author_name=user.full_contact,
- description=desc)
-
- response.content_type = feed.mime_type
- return feed.writeString('utf-8')
-
- def _rss_feed(self, repos, public=True):
- journal = self._get_journal_data(repos)
- if public:
- _link = url('public_journal_atom', qualified=True)
- _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
- 'rss feed')
- else:
- _link = url('journal_atom', qualified=True)
- _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
-
- feed = Rss201rev2Feed(title=_desc,
- link=_link,
- description=_desc,
- language=self.language,
- ttl=self.ttl)
-
- for entry in journal[:self.feed_nr]:
- user = entry.user
- if user is None:
- #fix deleted users
- user = AttributeDict({'short_contact': entry.username,
- 'email': '',
- 'full_contact': ''})
- action, action_extra, ico = h.action_parser(entry, feed=True)
- title = "%s - %s %s" % (user.short_contact, action(),
- entry.repository.repo_name)
- desc = action_extra()
- _url = None
- if entry.repository is not None:
- _url = h.route_url('repo_changelog',
- repo_name=entry.repository.repo_name)
-
- feed.add_item(title=title,
- pubdate=entry.action_date,
- link=_url or url('', qualified=True),
- author_email=user.email,
- author_name=user.full_contact,
- description=desc)
-
- response.content_type = feed.mime_type
- return feed.writeString('utf-8')
-
- @LoginRequired()
- @NotAnonymous()
- def index(self):
- # Return a rendered template
- p = safe_int(request.GET.get('page', 1), 1)
- c.user = User.get(c.rhodecode_user.user_id)
- following = self.sa.query(UserFollowing)\
- .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
- .options(joinedload(UserFollowing.follows_repository))\
- .all()
-
- journal = self._get_journal_data(following)
-
- def url_generator(**kw):
- return url.current(filter=c.search_term, **kw)
-
- c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
- c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
-
- c.journal_data = render('journal/journal_data.mako')
- if request.is_xhr:
- return c.journal_data
-
- return render('journal/journal.mako')
-
- @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
- @NotAnonymous()
- def journal_atom(self):
- """
- Produce an atom-1.0 feed via feedgenerator module
- """
- following = self.sa.query(UserFollowing)\
- .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
- .options(joinedload(UserFollowing.follows_repository))\
- .all()
- return self._atom_feed(following, public=False)
-
- @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
- @NotAnonymous()
- def journal_rss(self):
- """
- Produce an rss feed via feedgenerator module
- """
- following = self.sa.query(UserFollowing)\
- .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
- .options(joinedload(UserFollowing.follows_repository))\
- .all()
- return self._rss_feed(following, public=False)
-
- @CSRFRequired()
- @LoginRequired()
- @NotAnonymous()
- def toggle_following(self):
- user_id = request.POST.get('follows_user_id')
- if user_id:
- try:
- self.scm_model.toggle_following_user(
- user_id, c.rhodecode_user.user_id)
- Session().commit()
- return 'ok'
- except Exception:
- raise HTTPBadRequest()
-
- repo_id = request.POST.get('follows_repo_id')
- if repo_id:
- try:
- self.scm_model.toggle_following_repo(
- repo_id, c.rhodecode_user.user_id)
- Session().commit()
- return 'ok'
- except Exception:
- raise HTTPBadRequest()
-
-
- @LoginRequired()
- def public_journal(self):
- # Return a rendered template
- p = safe_int(request.GET.get('page', 1), 1)
-
- c.following = self.sa.query(UserFollowing)\
- .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
- .options(joinedload(UserFollowing.follows_repository))\
- .all()
-
- journal = self._get_journal_data(c.following)
-
- c.journal_pager = Page(journal, page=p, items_per_page=20)
-
- c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
-
- c.journal_data = render('journal/journal_data.mako')
- if request.is_xhr:
- return c.journal_data
- return render('journal/public_journal.mako')
-
- @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
- def public_journal_atom(self):
- """
- Produce an atom-1.0 feed via feedgenerator module
- """
- c.following = self.sa.query(UserFollowing)\
- .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
- .options(joinedload(UserFollowing.follows_repository))\
- .all()
-
- return self._atom_feed(c.following)
-
- @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
- def public_journal_rss(self):
- """
- Produce an rss2 feed via feedgenerator module
- """
- c.following = self.sa.query(UserFollowing)\
- .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
- .options(joinedload(UserFollowing.follows_repository))\
- .all()
-
- return self._rss_feed(c.following)
diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js
--- a/rhodecode/public/js/rhodecode/routes.js
+++ b/rhodecode/public/js/rhodecode/routes.js
@@ -15,7 +15,6 @@ function registerRCRoutes() {
pyroutes.register('new_repo', '/_admin/create_repository', []);
pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
- pyroutes.register('toggle_following', '/_admin/toggle_following', []);
pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
@@ -92,6 +91,15 @@ function registerRCRoutes() {
pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
pyroutes.register('repo_list_data', '/_repos', []);
pyroutes.register('goto_switcher_data', '/_goto_data', []);
+ pyroutes.register('journal', '/_admin/journal', []);
+ pyroutes.register('journal_rss', '/_admin/journal/rss', []);
+ pyroutes.register('journal_atom', '/_admin/journal/atom', []);
+ pyroutes.register('journal_public', '/_admin/public_journal', []);
+ pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
+ pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
+ pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
+ pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
+ pyroutes.register('toggle_following', '/_admin/toggle_following', []);
pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako
--- a/rhodecode/templates/base/base.mako
+++ b/rhodecode/templates/base/base.mako
@@ -381,13 +381,13 @@
## ROOT MENU
%if c.rhodecode_user.username != h.DEFAULT_USER:
-
%else:
-
diff --git a/rhodecode/templates/journal/journal.mako b/rhodecode/templates/journal/journal.mako
--- a/rhodecode/templates/journal/journal.mako
+++ b/rhodecode/templates/journal/journal.mako
@@ -6,12 +6,13 @@
· ${h.branding(c.rhodecode_name)}
%endif
%def>
+
<%def name="breadcrumbs()">
${h.form(None, id_="filter_form", method="get")}
- ${_('Journal')} - ${ungettext('%s entry', '%s entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
+ ${_('Journal')} - ${_ungettext('%s entry', '%s entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
${h.end_form()}
${_('Example Queries')}
@@ -20,9 +21,10 @@
${self.menu_items(active='journal')}
%def>
<%def name="head_extra()">
-
-
+
+
%def>
+
<%def name="main()">
@@ -31,14 +33,14 @@
${self.breadcrumbs()}
- ${c.journal_data}
+ ${c.journal_data|n}
%def>
diff --git a/rhodecode/templates/journal/public_journal.mako b/rhodecode/templates/journal/public_journal.mako
--- a/rhodecode/templates/journal/public_journal.mako
+++ b/rhodecode/templates/journal/public_journal.mako
@@ -6,32 +6,38 @@
· ${h.branding(c.rhodecode_name)}
%endif
%def>
+
<%def name="breadcrumbs()">
- ${h.branding(c.rhodecode_name)}
+
+ ${_('Public Journal')} - ${_ungettext('%s entry', '%s entries', c.journal_pager.item_count) % (c.journal_pager.item_count)}
+
%def>
+
<%def name="menu_bar_nav()">
${self.menu_items(active='journal')}
%def>
+
<%def name="head_extra()">
-
-
+
+
%def>
+
<%def name="main()">
-
-
${_('Public Journal')}
-
%def>