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 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 name="head_extra()"> - - + + + <%def name="main()">
    @@ -31,14 +33,14 @@ ${self.breadcrumbs()}
    -
    ${c.journal_data}
    +
    ${c.journal_data|n}
    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 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 name="menu_bar_nav()"> ${self.menu_items(active='journal')} + <%def name="head_extra()"> - - + + + <%def name="main()">
    -
    -
    ${_('Public Journal')}
    -
    - -
    ${c.journal_data}
    +
    ${c.journal_data|n}