From 62b3afeeedfe054345f86093e2d243e956c1e3c9 Mon Sep 17 00:00:00 2001 From: Cédric Bonhomme Date: Wed, 26 Feb 2020 11:27:31 +0100 Subject: The project is now using Poetry. --- src/web/views/__init__.py | 23 --- src/web/views/admin.py | 119 --------------- src/web/views/api/__init__.py | 0 src/web/views/api/v2/__init__.py | 3 - src/web/views/api/v2/article.py | 53 ------- src/web/views/api/v2/category.py | 27 ---- src/web/views/api/v2/common.py | 222 ---------------------------- src/web/views/api/v2/feed.py | 47 ------ src/web/views/api/v3/__init__.py | 3 - src/web/views/api/v3/article.py | 84 ----------- src/web/views/api/v3/common.py | 109 -------------- src/web/views/api/v3/feed.py | 58 -------- src/web/views/article.py | 154 -------------------- src/web/views/bookmark.py | 256 -------------------------------- src/web/views/category.py | 86 ----------- src/web/views/common.py | 53 ------- src/web/views/feed.py | 306 --------------------------------------- src/web/views/home.py | 172 ---------------------- src/web/views/icon.py | 15 -- src/web/views/session_mgmt.py | 113 --------------- src/web/views/user.py | 203 -------------------------- src/web/views/views.py | 95 ------------ 22 files changed, 2201 deletions(-) delete mode 100644 src/web/views/__init__.py delete mode 100644 src/web/views/admin.py delete mode 100644 src/web/views/api/__init__.py delete mode 100644 src/web/views/api/v2/__init__.py delete mode 100644 src/web/views/api/v2/article.py delete mode 100644 src/web/views/api/v2/category.py delete mode 100644 src/web/views/api/v2/common.py delete mode 100644 src/web/views/api/v2/feed.py delete mode 100644 src/web/views/api/v3/__init__.py delete mode 100644 src/web/views/api/v3/article.py delete mode 100644 src/web/views/api/v3/common.py delete mode 100644 src/web/views/api/v3/feed.py delete mode 100644 src/web/views/article.py delete mode 100644 src/web/views/bookmark.py delete mode 100644 src/web/views/category.py delete mode 100644 src/web/views/common.py delete mode 100644 src/web/views/feed.py delete mode 100644 src/web/views/home.py delete mode 100644 src/web/views/icon.py delete mode 100644 src/web/views/session_mgmt.py delete mode 100644 src/web/views/user.py delete mode 100644 src/web/views/views.py (limited to 'src/web/views') diff --git a/src/web/views/__init__.py b/src/web/views/__init__.py deleted file mode 100644 index 41bb52f3..00000000 --- a/src/web/views/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from web.views.api import v2, v3 -from web.views import views, home, session_mgmt -from web.views.article import article_bp, articles_bp -from web.views.feed import feed_bp, feeds_bp -from web.views.category import category_bp, categories_bp -from web.views.icon import icon_bp -from web.views.admin import admin_bp -from web.views.user import user_bp, users_bp -from web.views.bookmark import bookmark_bp, bookmarks_bp - -__all__ = ['views', 'home', 'session_mgmt', 'v2', 'v3', - 'article_bp', 'articles_bp', 'feed_bp', 'feeds_bp', - 'category_bp', 'categories_bp', 'icon_bp', - 'admin_bp', 'user_bp', 'users_bp', 'bookmark_bp', 'bookmarks_bp'] - -import conf -from flask import request -from flask import g - - -@g.babel.localeselector -def get_locale(): - return request.accept_languages.best_match(conf.LANGUAGES.keys()) diff --git a/src/web/views/admin.py b/src/web/views/admin.py deleted file mode 100644 index 73b2b668..00000000 --- a/src/web/views/admin.py +++ /dev/null @@ -1,119 +0,0 @@ -from datetime import datetime -from flask import (Blueprint, render_template, redirect, flash, url_for) -from flask_babel import gettext, format_timedelta -from flask_login import login_required, current_user - -from lib.utils import redirect_url -from web.views.common import admin_permission -from web.controllers import UserController -from web.forms import InformationMessageForm, UserForm - -admin_bp = Blueprint('admin', __name__, url_prefix='/admin') - - -@admin_bp.route('/dashboard', methods=['GET', 'POST']) -@login_required -@admin_permission.require(http_exception=403) -def dashboard(): - last_cons, now = {}, datetime.utcnow() - users = list(UserController().read().order_by('id')) - form = InformationMessageForm() - for user in users: - last_cons[user.id] = format_timedelta(now - user.last_seen) - return render_template('admin/dashboard.html', now=datetime.utcnow(), - last_cons=last_cons, users=users, current_user=current_user, - form=form) - - -@admin_bp.route('/user/create', methods=['GET']) -@admin_bp.route('/user/edit/', methods=['GET']) -@login_required -@admin_permission.require(http_exception=403) -def user_form(user_id=None): - if user_id is not None: - user = UserController().get(id=user_id) - form = UserForm(obj=user) - message = gettext('Edit the user %(nick)s', nick=user.nickname) - else: - form = UserForm() - message = gettext('Add a new user') - return render_template('/admin/create_user.html', - form=form, message=message) - - -@admin_bp.route('/user/create', methods=['POST']) -@admin_bp.route('/user/edit/', methods=['POST']) -@login_required -@admin_permission.require(http_exception=403) -def process_user_form(user_id=None): - """ - Create or edit a user. - """ - form = UserForm() - user_contr = UserController() - - if not form.validate(): - return render_template('/admin/create_user.html', form=form, - message=gettext('Some errors were found')) - - if user_id is not None: - # Edit a user - user_contr.update({'id': user_id}, - {'nickname': form.nickname.data, - 'password': form.password.data, - 'automatic_crawling': form.automatic_crawling.data}) - user = user_contr.get(id=user_id) - flash(gettext('User %(nick)s successfully updated', - nick=user.nickname), 'success') - else: - # Create a new user (by the admin) - user = user_contr.create(nickname=form.nickname.data, - password=form.password.data, - automatic_crawling=form.automatic_crawling.data, - is_admin=False, - is_active=True) - flash(gettext('User %(nick)s successfully created', - nick=user.nickname), 'success') - return redirect(url_for('admin.user_form', user_id=user.id)) - - -@admin_bp.route('/delete_user/', methods=['GET']) -@login_required -@admin_permission.require(http_exception=403) -def delete_user(user_id=None): - """ - Delete a user (with all its data). - """ - try: - user = UserController().delete(user_id) - flash(gettext('User %(nick)s successfully deleted', - nick=user.nickname), 'success') - except Exception as error: - flash( - gettext('An error occurred while trying to delete a user: %(error)s', - error=error), 'danger') - return redirect(url_for('admin.dashboard')) - - -@admin_bp.route('/toggle_user/', methods=['GET']) -@login_required -@admin_permission.require() -def toggle_user(user_id=None): - """ - Enable or disable the account of a user. - """ - ucontr = UserController() - user = ucontr.get(id=user_id) - user_changed = ucontr.update({'id': user_id}, - {'is_active': not user.is_active}) - - if not user_changed: - flash(gettext('This user does not exist.'), 'danger') - return redirect(url_for('admin.dashboard')) - - else: - act_txt = 'activated' if user.is_active else 'desactivated' - message = gettext('User %(nickname)s successfully %(is_active)s', - nickname=user.nickname, is_active=act_txt) - flash(message, 'success') - return redirect(url_for('admin.dashboard')) diff --git a/src/web/views/api/__init__.py b/src/web/views/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/web/views/api/v2/__init__.py b/src/web/views/api/v2/__init__.py deleted file mode 100644 index 46760261..00000000 --- a/src/web/views/api/v2/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from web.views.api.v2 import article, feed, category - -__all__ = ['article', 'feed', 'category'] diff --git a/src/web/views/api/v2/article.py b/src/web/views/api/v2/article.py deleted file mode 100644 index 2be286c6..00000000 --- a/src/web/views/api/v2/article.py +++ /dev/null @@ -1,53 +0,0 @@ -from conf import API_ROOT -import dateutil.parser -from datetime import datetime -from flask import current_app -from flask_restful import Api - -from web.views.common import api_permission -from web.controllers import ArticleController -from web.views.api.v2.common import (PyAggAbstractResource, - PyAggResourceNew, PyAggResourceExisting, PyAggResourceMulti) - - -class ArticleNewAPI(PyAggResourceNew): - controller_cls = ArticleController - - -class ArticleAPI(PyAggResourceExisting): - controller_cls = ArticleController - - -class ArticlesAPI(PyAggResourceMulti): - controller_cls = ArticleController - - -class ArticlesChallenge(PyAggAbstractResource): - controller_cls = ArticleController - attrs = {'ids': {'type': list, 'default': []}} - - @api_permission.require(http_exception=403) - def get(self): - parsed_args = self.reqparse_args(right='read') - # collecting all attrs for casting purpose - attrs = self.controller_cls._get_attrs_desc('admin') - for id_dict in parsed_args['ids']: - keys_to_ignore = [] - for key in id_dict: - if key not in attrs: - keys_to_ignore.append(key) - if issubclass(attrs[key]['type'], datetime): - id_dict[key] = dateutil.parser.parse(id_dict[key]) - for key in keys_to_ignore: - del id_dict[key] - - result = list(self.controller.challenge(parsed_args['ids'])) - return result or None, 200 if result else 204 - -api = Api(current_app, prefix=API_ROOT) - -api.add_resource(ArticleNewAPI, '/article', endpoint='article_new.json') -api.add_resource(ArticleAPI, '/article/', endpoint='article.json') -api.add_resource(ArticlesAPI, '/articles', endpoint='articles.json') -api.add_resource(ArticlesChallenge, '/articles/challenge', - endpoint='articles_challenge.json') diff --git a/src/web/views/api/v2/category.py b/src/web/views/api/v2/category.py deleted file mode 100644 index 70fda1ea..00000000 --- a/src/web/views/api/v2/category.py +++ /dev/null @@ -1,27 +0,0 @@ -from conf import API_ROOT -from flask import current_app -from flask_restful import Api - -from web.controllers.category import CategoryController -from web.views.api.v2.common import (PyAggResourceNew, - PyAggResourceExisting, - PyAggResourceMulti) - - -class CategoryNewAPI(PyAggResourceNew): - controller_cls = CategoryController - - -class CategoryAPI(PyAggResourceExisting): - controller_cls = CategoryController - - -class CategoriesAPI(PyAggResourceMulti): - controller_cls = CategoryController - - -api = Api(current_app, prefix=API_ROOT) -api.add_resource(CategoryNewAPI, '/category', endpoint='category_new.json') -api.add_resource(CategoryAPI, '/category/', - endpoint='category.json') -api.add_resource(CategoriesAPI, '/categories', endpoint='categories.json') diff --git a/src/web/views/api/v2/common.py b/src/web/views/api/v2/common.py deleted file mode 100644 index 8a53d7e6..00000000 --- a/src/web/views/api/v2/common.py +++ /dev/null @@ -1,222 +0,0 @@ -"""For a given resources, classes in the module intend to create the following -routes : - GET resource/ - -> to retrieve one - POST resource - -> to create one - PUT resource/ - -> to update one - DELETE resource/ - -> to delete one - - GET resources - -> to retrieve several - POST resources - -> to create several - PUT resources - -> to update several - DELETE resources - -> to delete several -""" -import ast -import logging -from functools import wraps -from werkzeug.exceptions import Unauthorized, BadRequest, Forbidden, NotFound -from flask import request -from flask_restful import Resource, reqparse -from flask_login import current_user - -from web.views.common import admin_permission, api_permission, \ - login_user_bundle, jsonify -from web.controllers import UserController - -logger = logging.getLogger(__name__) - - -def authenticate(func): - @wraps(func) - def wrapper(*args, **kwargs): - if request.authorization: - ucontr = UserController() - try: - user = ucontr.get(nickname=request.authorization.username) - except NotFound: - raise Forbidden("Couldn't authenticate your user") - if not ucontr.check_password(user, request.authorization.password): - raise Forbidden("Couldn't authenticate your user") - if not user.is_active: - raise Forbidden("User is deactivated") - login_user_bundle(user) - if current_user.is_authenticated: - return func(*args, **kwargs) - raise Unauthorized() - return wrapper - - -class PyAggAbstractResource(Resource): - method_decorators = [authenticate, jsonify] - controller_cls = None - attrs = None - - @property - def controller(self): - if admin_permission.can(): - return self.controller_cls() - return self.controller_cls(current_user.id) - - def reqparse_args(self, right, req=None, strict=False, default=True, - allow_empty=False): - """ - strict: bool - if True will throw 400 error if args are defined and not in request - default: bool - if True, won't return defaults - args: dict - the args to parse, if None, self.attrs will be used - """ - try: - if req: - in_values = req.json - else: - in_values = request.args or request.json or {} - if not in_values and allow_empty: - return {} - except BadRequest: - if allow_empty: - return {} - raise - parser = reqparse.RequestParser() - if self.attrs is not None: - attrs = self.attrs - elif admin_permission.can(): - attrs = self.controller_cls._get_attrs_desc('admin') - elif api_permission.can(): - attrs = self.controller_cls._get_attrs_desc('api', right) - else: - attrs = self.controller_cls._get_attrs_desc('base', right) - assert attrs, "No defined attrs for %s" % self.__class__.__name__ - - for attr_name, attr in attrs.items(): - if not default and attr_name not in in_values: - continue - else: - parser.add_argument(attr_name, location='json', - default=in_values[attr_name]) - return parser.parse_args(req=request.args, strict=strict) - - -class PyAggResourceNew(PyAggAbstractResource): - - @api_permission.require(http_exception=403) - def post(self): - """Create a single new object""" - return self.controller.create(**self.reqparse_args(right='write')), 201 - - -class PyAggResourceExisting(PyAggAbstractResource): - - def get(self, obj_id=None): - """Retrieve a single object""" - return self.controller.get(id=obj_id) - - def put(self, obj_id=None): - """update an object, new attrs should be passed in the payload""" - args = self.reqparse_args(right='write', default=False) - if not args: - raise BadRequest() - return self.controller.update({'id': obj_id}, args), 200 - - def delete(self, obj_id=None): - """delete a object""" - self.controller.delete(obj_id) - return None, 204 - - -class PyAggResourceMulti(PyAggAbstractResource): - - def get(self): - """retrieve several objects. filters can be set in the payload on the - different fields of the object, and a limit can be set in there as well - """ - args = {} - try: - limit = request.json.pop('limit', 10) - order_by = request.json.pop('order_by', None) - except Exception: - args = self.reqparse_args(right='read', default=False) - limit = request.args.get('limit', 10) - order_by = request.args.get('order_by', None) - query = self.controller.read(**args) - if order_by: - query = query.order_by(order_by) - if limit: - query = query.limit(limit) - return [res for res in query] - - @api_permission.require(http_exception=403) - def post(self): - """creating several objects. payload should be: - >>> payload - [{attr1: val1, attr2: val2}, {attr1: val1, attr2: val2}] - """ - status, fail_count, results = 200, 0, [] - - class Proxy: - pass - for attrs in request.json: - try: - Proxy.json = attrs - args = self.reqparse_args('write', req=Proxy, default=False) - obj = self.controller.create(**args) - results.append(obj) - except Exception as error: - fail_count += 1 - results.append(str(error)) - if fail_count == len(results): # all failed => 500 - status = 500 - elif fail_count: # some failed => 206 - status = 206 - return results, status - - def put(self): - """updating several objects. payload should be: - >>> payload - [[obj_id1, {attr1: val1, attr2: val2}] - [obj_id2, {attr1: val1, attr2: val2}]] - """ - status, results = 200, [] - - class Proxy: - pass - for obj_id, attrs in request.json: - try: - Proxy.json = attrs - args = self.reqparse_args('write', req=Proxy, default=False) - result = self.controller.update({'id': obj_id}, args) - if result: - results.append('ok') - else: - results.append('nok') - except Exception as error: - results.append(str(error)) - if results.count('ok') == 0: # all failed => 500 - status = 500 - elif results.count('ok') != len(results): # some failed => 206 - status = 206 - return results, status - - def delete(self): - """will delete several objects, - a list of their ids should be in the payload""" - status, results = 204, [] - for obj_id in request.json: - try: - self.controller.delete(obj_id) - results.append('ok') - except Exception as error: - status = 206 - results.append(error) - # if no operation succeeded, it's not partial anymore, returning err 500 - if status == 206 and results.count('ok') == 0: - status = 500 - return results, status diff --git a/src/web/views/api/v2/feed.py b/src/web/views/api/v2/feed.py deleted file mode 100644 index a0691277..00000000 --- a/src/web/views/api/v2/feed.py +++ /dev/null @@ -1,47 +0,0 @@ -from conf import API_ROOT -from flask import current_app -from flask_restful import Api - -from web.views.common import api_permission -from web.controllers.feed import (FeedController, - DEFAULT_MAX_ERROR, - DEFAULT_LIMIT) - -from web.views.api.v2.common import PyAggAbstractResource, \ - PyAggResourceNew, \ - PyAggResourceExisting, \ - PyAggResourceMulti - - -class FeedNewAPI(PyAggResourceNew): - controller_cls = FeedController - - -class FeedAPI(PyAggResourceExisting): - controller_cls = FeedController - - -class FeedsAPI(PyAggResourceMulti): - controller_cls = FeedController - - -class FetchableFeedAPI(PyAggAbstractResource): - controller_cls = FeedController - attrs = {'max_error': {'type': int, 'default': DEFAULT_MAX_ERROR}, - 'limit': {'type': int, 'default': DEFAULT_LIMIT}} - - @api_permission.require(http_exception=403) - def get(self): - args = self.reqparse_args(right='read', allow_empty=True) - result = [feed for feed - in self.controller.list_fetchable(**args)] - return result or None, 200 if result else 204 - - -api = Api(current_app, prefix=API_ROOT) - -api.add_resource(FeedNewAPI, '/feed', endpoint='feed_new.json') -api.add_resource(FeedAPI, '/feed/', endpoint='feed.json') -api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json') -api.add_resource(FetchableFeedAPI, '/feeds/fetchable', - endpoint='fetchable_feed.json') diff --git a/src/web/views/api/v3/__init__.py b/src/web/views/api/v3/__init__.py deleted file mode 100644 index 76aa1f19..00000000 --- a/src/web/views/api/v3/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from web.views.api.v3 import article - -__all__ = ['article'] diff --git a/src/web/views/api/v3/article.py b/src/web/views/api/v3/article.py deleted file mode 100644 index 4cf35648..00000000 --- a/src/web/views/api/v3/article.py +++ /dev/null @@ -1,84 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Newspipe - A Web based news aggregator. -# Copyright (C) 2010-2018 Cédric Bonhomme - https://www.cedricbonhomme.org -# -# For more information : http://gitlab.com/newspipe/newspipe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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 General Public License -# along with this program. If not, see - -__author__ = "Cedric Bonhomme" -__version__ = "$Revision: 0.1 $" -__date__ = "$Date: 2016/04/29 $" -__revision__ = "$Date: 2016/04/29 $" -__copyright__ = "Copyright (c) Cedric Bonhomme" -__license__ = "GPLv3" - -from flask_login import current_user -from werkzeug.exceptions import NotFound -from flask_restless import ProcessingException -from web import models -from bootstrap import application, manager -from web.controllers import ArticleController, FeedController -from web.views.api.v3.common import AbstractProcessor -from web.views.api.v3.common import url_prefix, auth_func - -class ArticleProcessor(AbstractProcessor): - """Concrete processors for the Article Web service. - """ - - def get_single_preprocessor(self, instance_id=None, **kw): - try: - article = ArticleController(current_user.id).get(id=instance_id) - except NotFound: - raise ProcessingException(description='No such article.', code=404) - self.is_authorized(current_user, article) - - def post_preprocessor(self, data=None, **kw): - data["user_id"] = current_user.id - - try: - feed = FeedController(current_user.id).get(id=data["feed_id"]) - except NotFound: - raise ProcessingException(description='No such feed.', code=404) - self.is_authorized(current_user, feed) - - data["category_id"] = feed.category_id - - def delete_preprocessor(self, instance_id=None, **kw): - try: - article = ArticleController(current_user.id).get(id=instance_id) - except NotFound: - raise ProcessingException(description='No such article.', code=404) - self.is_authorized(current_user, article) - -article_processor = ArticleProcessor() - -blueprint_article = manager.create_api_blueprint(models.Article, - url_prefix=url_prefix, - methods=['GET', 'POST', 'PUT', 'DELETE'], - preprocessors=dict(GET_SINGLE=[auth_func, - article_processor.get_single_preprocessor], - GET_MANY=[auth_func, - article_processor.get_many_preprocessor], - POST=[auth_func, - article_processor.post_preprocessor], - PUT_SINGLE=[auth_func, - article_processor.put_single_preprocessor], - PUT_MANY=[auth_func, - article_processor.put_many_preprocessor], - DELETE=[auth_func, - article_processor.delete_preprocessor])) -application.register_blueprint(blueprint_article) diff --git a/src/web/views/api/v3/common.py b/src/web/views/api/v3/common.py deleted file mode 100644 index d5e94a3f..00000000 --- a/src/web/views/api/v3/common.py +++ /dev/null @@ -1,109 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Newspipe - A Web based news aggregator. -# Copyright (C) 2010-2018 Cédric Bonhomme - https://www.cedricbonhomme.org -# -# For more information : http://gitlab.com/newspipe/newspipe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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 General Public License -# along with this program. If not, see - -__author__ = "Cedric Bonhomme" -__version__ = "$Revision: 0.1 $" -__date__ = "$Date: 2016/04/29 $" -__revision__ = "$Date: 2016/04/29 $" -__copyright__ = "Copyright (c) Cedric Bonhomme" -__license__ = "GPLv3" - -from flask import request -from flask_login import current_user -from flask_restless import ProcessingException -from werkzeug.exceptions import NotFound -from web.controllers import ArticleController, UserController -from web.views.common import login_user_bundle - -url_prefix = '/api/v3' - -def auth_func(*args, **kw): - if request.authorization: - ucontr = UserController() - try: - user = ucontr.get(nickname=request.authorization.username) - except NotFound: - raise ProcessingException("Couldn't authenticate your user", - code=401) - if not ucontr.check_password(user, request.authorization.password): - raise ProcessingException("Couldn't authenticate your user", - code=401) - if not user.is_active: - raise ProcessingException("User is deactivated", code=401) - login_user_bundle(user) - if not current_user.is_authenticated: - raise ProcessingException(description='Not authenticated!', code=401) - -class AbstractProcessor(): - """Abstract processors for the Web services. - """ - - def is_authorized(self, user, obj): - if user.id != obj.user_id: - raise ProcessingException(description='Not Authorized', code=401) - - def get_single_preprocessor(self, instance_id=None, **kw): - # Check if the user is authorized to modify the specified - # instance of the model. - pass - - def get_many_preprocessor(self, search_params=None, **kw): - """Accepts a single argument, `search_params`, which is a dictionary - containing the search parameters for the request. - """ - filt = dict(name="user_id", - op="eq", - val=current_user.id) - - # Check if there are any filters there already. - if "filters" not in search_params: - search_params["filters"] = [] - - search_params["filters"].append(filt) - - def post_preprocessor(self, data=None, **kw): - pass - - def put_single_preprocessor(instance_id=None, data=None, **kw): - """Accepts two arguments, `instance_id`, the primary key of the - instance of the model to patch, and `data`, the dictionary of fields - to change on the instance. - """ - pass - - def put_many_preprocessor(search_params=None, data=None, **kw): - """Accepts two arguments: `search_params`, which is a dictionary - containing the search parameters for the request, and `data`, which - is a dictionary representing the fields to change on the matching - instances and the values to which they will be set. - """ - filt = dict(name="user_id", - op="eq", - val=current_user.id) - - # Check if there are any filters there already. - if "filters" not in search_params: - search_params["filters"] = [] - - search_params["filters"].append(filt) - - def delete_preprocessor(self, instance_id=None, **kw): - pass diff --git a/src/web/views/api/v3/feed.py b/src/web/views/api/v3/feed.py deleted file mode 100644 index 2cbbafd9..00000000 --- a/src/web/views/api/v3/feed.py +++ /dev/null @@ -1,58 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Newspipe - A Web based news aggregator. -# Copyright (C) 2010-2018 Cédric Bonhomme - https://www.cedricbonhomme.org -# -# For more information : http://gitlab.com/newspipe/newspipe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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 General Public License -# along with this program. If not, see - -__author__ = "Cedric Bonhomme" -__version__ = "$Revision: 0.1 $" -__date__ = "$Date: 2016/04/29 $" -__revision__ = "$Date: 2016/04/29 $" -__copyright__ = "Copyright (c) Cedric Bonhomme" -__license__ = "GPLv3" - -from flask_login import current_user -from web import models -from bootstrap import application, manager -from web.controllers import FeedController -from web.views.api.v3.common import AbstractProcessor -from web.views.api.v3.common import url_prefix, auth_func - -class FeedProcessor(AbstractProcessor): - """Concrete processors for the Feed Web service. - """ - - def get_single_preprocessor(self, instance_id=None, **kw): - # Check if the user is authorized to modify the specified - # instance of the model. - feed = FeedController(current_user.id).get(id=instance_id) - self.is_authorized(current_user, feed) - -feed_processor = FeedProcessor() - -blueprint_feed = manager.create_api_blueprint(models.Feed, - url_prefix=url_prefix, - methods=['GET', 'POST', 'PUT', 'DELETE'], - preprocessors=dict(GET_SINGLE=[auth_func, - feed_processor.get_single_preprocessor], - GET_MANY=[auth_func, - feed_processor.get_many_preprocessor], - PUT_SINGLE=[auth_func], - POST=[auth_func], - DELETE=[auth_func])) -application.register_blueprint(blueprint_feed) diff --git a/src/web/views/article.py b/src/web/views/article.py deleted file mode 100644 index bf39795d..00000000 --- a/src/web/views/article.py +++ /dev/null @@ -1,154 +0,0 @@ -from datetime import datetime, timedelta -from flask import (Blueprint, g, render_template, redirect, - flash, url_for, make_response, request) - -from flask_babel import gettext -from flask_login import login_required, current_user - - -from bootstrap import db -from lib.utils import clear_string, redirect_url -from lib.data import export_json -from web.controllers import (ArticleController, UserController, - CategoryController) -from web.lib.view_utils import etag_match - -articles_bp = Blueprint('articles', __name__, url_prefix='/articles') -article_bp = Blueprint('article', __name__, url_prefix='/article') - - -@article_bp.route('/redirect/', methods=['GET']) -@login_required -def redirect_to_article(article_id): - contr = ArticleController(current_user.id) - article = contr.get(id=article_id) - if not article.readed: - contr.update({'id': article.id}, {'readed': True}) - return redirect(article.link) - - -@article_bp.route('/', methods=['GET']) -@login_required -@etag_match -def article(article_id=None): - """ - Presents an article. - """ - article = ArticleController(current_user.id).get(id=article_id) - return render_template('article.html', - head_titles=[clear_string(article.title)], - article=article) - -@article_bp.route('/public/', methods=['GET']) -@etag_match -def article_pub(article_id=None): - """ - Presents an article of a public feed if the profile of the owner is also - public. - """ - article = ArticleController().get(id=article_id) - if article.source.private or not article.source.user.is_public_profile: - return render_template('errors/404.html'), 404 - return render_template('article_pub.html', - head_titles=[clear_string(article.title)], - article=article) - - -@article_bp.route('/like/', methods=['GET']) -@login_required -def like(article_id=None): - """ - Mark or unmark an article as favorites. - """ - art_contr = ArticleController(current_user.id) - article = art_contr.get(id=article_id) - art_contr = art_contr.update({'id': article_id}, - {'like': not article.like}) - return redirect(redirect_url()) - - -@article_bp.route('/delete/', methods=['GET']) -@login_required -def delete(article_id=None): - """ - Delete an article from the database. - """ - article = ArticleController(current_user.id).delete(article_id) - flash(gettext('Article %(article_title)s deleted', - article_title=article.title), 'success') - return redirect(url_for('home')) - - -@articles_bp.route('/history', methods=['GET']) -@articles_bp.route('/history/', methods=['GET']) -@articles_bp.route('/history//', methods=['GET']) -@login_required -def history(year=None, month=None): - cntr, artcles = ArticleController(current_user.id).get_history(year, month) - return render_template('history.html', articles_counter=cntr, - articles=artcles, year=year, month=month) - - -@article_bp.route('/mark_as/', methods=['GET']) -@article_bp.route('/mark_as//article/', - methods=['GET']) -@login_required -def mark_as(new_value='read', feed_id=None, article_id=None): - """ - Mark all unreaded articles as read. - """ - readed = new_value == 'read' - art_contr = ArticleController(current_user.id) - filters = {'readed': not readed} - if feed_id is not None: - filters['feed_id'] = feed_id - message = 'Feed marked as %s.' - elif article_id is not None: - filters['id'] = article_id - message = 'Article marked as %s.' - else: - message = 'All article marked as %s.' - art_contr.update(filters, {"readed": readed}) - flash(gettext(message % new_value), 'info') - - if readed: - return redirect(redirect_url()) - return redirect('home') - - -@articles_bp.route('/expire_articles', methods=['GET']) -@login_required -def expire(): - """ - Delete articles older than the given number of weeks. - """ - current_time = datetime.utcnow() - weeks_ago = current_time - timedelta(int(request.args.get('weeks', 10))) - art_contr = ArticleController(current_user.id) - - query = art_contr.read(__or__={'date__lt': weeks_ago, - 'retrieved_date__lt': weeks_ago}) - count = query.count() - query.delete() - db.session.commit() - flash(gettext('%(count)d articles deleted', count=count), 'info') - return redirect(redirect_url()) - - -@articles_bp.route('/export', methods=['GET']) -@login_required -def export(): - """ - Export articles to JSON. - """ - user = UserController(current_user.id).get(id=current_user.id) - try: - json_result = export_json(user) - except Exception as e: - flash(gettext("Error when exporting articles."), 'danger') - return redirect(redirect_url()) - response = make_response(json_result) - response.mimetype = 'application/json' - response.headers["Content-Disposition"] \ - = 'attachment; filename=account.json' - return response diff --git a/src/web/views/bookmark.py b/src/web/views/bookmark.py deleted file mode 100644 index 21d832d2..00000000 --- a/src/web/views/bookmark.py +++ /dev/null @@ -1,256 +0,0 @@ -#! /usr/bin/env python -#-*- coding: utf-8 -*- - -# Newspipe - A Web based news aggregator. -# Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org -# -# For more information : https://gitlab.com/newspipe/newspipe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# 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 Affero 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 . - -__author__ = "Cedric Bonhomme" -__version__ = "$Revision: 0.1 $" -__date__ = "$Date: 2017/05/24 $" -__revision__ = "$Date: 2017/05/24 $" -__copyright__ = "Copyright (c) Cedric Bonhomme" -__license__ = "AGPLv3" - -import logging -import datetime -from werkzeug.exceptions import BadRequest - -from flask import Blueprint, render_template, flash, \ - redirect, request, url_for, make_response -from flask_babel import gettext -from flask_login import login_required, current_user -from flask_paginate import Pagination, get_page_args -from sqlalchemy import desc - -import conf -from lib.utils import redirect_url -from lib.data import import_pinboard_json, export_bookmarks -from bootstrap import db -from web.forms import BookmarkForm -from web.controllers import BookmarkController, BookmarkTagController -from web.models import BookmarkTag - -logger = logging.getLogger(__name__) -bookmarks_bp = Blueprint('bookmarks', __name__, url_prefix='/bookmarks') -bookmark_bp = Blueprint('bookmark', __name__, url_prefix='/bookmark') - - -@bookmarks_bp.route('/', defaults={'per_page': '50'}, methods=['GET']) -@bookmarks_bp.route('/', defaults={'per_page': '50'}, - methods=['GET']) -def list_(per_page, status='all'): - "Lists the bookmarks." - head_titles = [gettext("Bookmarks")] - user_id = None - filters = {} - tag = request.args.get('tag', None) - if tag: - filters['tags_proxy__contains'] = tag - query = request.args.get('query', None) - if query: - query_regex = '%' + query + '%' - filters['__or__'] = {'title__ilike': query_regex, - 'description__ilike': query_regex} - if current_user.is_authenticated: - # query for the bookmarks of the authenticated user - user_id = current_user.id - if status == 'public': - # load public bookmarks only - filters['shared'] = True - elif status == 'private': - # load private bookmarks only - filters['shared'] = False - else: - # no filter: load shared and public bookmarks - pass - if status == 'unread': - filters['to_read'] = True - else: - pass - else: - # query for the shared bookmarks (of all users) - head_titles = [gettext("Recent bookmarks")] - not_created_before = datetime.datetime.today() - \ - datetime.timedelta(days=900) - filters['time__gt'] = not_created_before # only "recent" bookmarks - filters['shared'] = True - - bookmarks = BookmarkController(user_id) \ - .read(**filters) \ - .order_by(desc('time')) - - #tag_contr = BookmarkTagController(user_id) - #tag_contr.read().join(bookmarks).all() - - page, per_page, offset = get_page_args() - pagination = Pagination(page=page, total=bookmarks.count(), - css_framework='bootstrap3', - search=False, record_name='bookmarks', - per_page=per_page) - - return render_template('bookmarks.html', - head_titles=head_titles, - bookmarks=bookmarks.offset(offset).limit(per_page), - pagination=pagination, - tag=tag, - query=query) - - -@bookmark_bp.route('/create', methods=['GET']) -@bookmark_bp.route('/edit/', methods=['GET']) -@login_required -def form(bookmark_id=None): - "Form to create/edit bookmarks." - action = gettext("Add a new bookmark") - head_titles = [action] - if bookmark_id is None: - return render_template('edit_bookmark.html', action=action, - head_titles=head_titles, form=BookmarkForm()) - bookmark = BookmarkController(current_user.id).get(id=bookmark_id) - action = gettext('Edit bookmark') - head_titles = [action] - form = BookmarkForm(obj=bookmark) - form.tags.data = ", ".join(bookmark.tags_proxy) - return render_template('edit_bookmark.html', action=action, - head_titles=head_titles, bookmark=bookmark, - form=form) - - -@bookmark_bp.route('/create', methods=['POST']) -@bookmark_bp.route('/edit/', methods=['POST']) -@login_required -def process_form(bookmark_id=None): - "Process the creation/edition of bookmarks." - form = BookmarkForm() - bookmark_contr = BookmarkController(current_user.id) - tag_contr = BookmarkTagController(current_user.id) - - if not form.validate(): - return render_template('edit_bookmark.html', form=form) - - if form.title.data == '': - title = form.href.data - else: - title = form.title.data - - bookmark_attr = {'href': form.href.data, - 'description': form.description.data, - 'title': title, - 'shared': form.shared.data, - 'to_read': form.to_read.data} - - if bookmark_id is not None: - tags = [] - for tag in form.tags.data.split(','): - new_tag = tag_contr.create(text=tag.strip(), user_id=current_user.id, - bookmark_id=bookmark_id) - tags.append(new_tag) - bookmark_attr['tags'] = tags - bookmark_contr.update({'id': bookmark_id}, bookmark_attr) - flash(gettext('Bookmark successfully updated.'), 'success') - return redirect(url_for('bookmark.form', bookmark_id=bookmark_id)) - - # Create a new bookmark - new_bookmark = bookmark_contr.create(**bookmark_attr) - tags = [] - for tag in form.tags.data.split(','): - new_tag = tag_contr.create(text=tag.strip(), user_id=current_user.id, - bookmark_id=new_bookmark.id) - tags.append(new_tag) - bookmark_attr['tags'] = tags - bookmark_contr.update({'id': new_bookmark.id}, bookmark_attr) - flash(gettext('Bookmark successfully created.'), 'success') - return redirect(url_for('bookmark.form', bookmark_id=new_bookmark.id)) - - -@bookmark_bp.route('/delete/', methods=['GET']) -@login_required -def delete(bookmark_id=None): - "Delete a bookmark." - bookmark = BookmarkController(current_user.id).delete(bookmark_id) - flash(gettext("Bookmark %(bookmark_name)s successfully deleted.", - bookmark_name=bookmark.title), 'success') - return redirect(url_for('bookmarks.list_')) - - -@bookmarks_bp.route('/delete', methods=['GET']) -@login_required -def delete_all(): - "Delete all bookmarks." - bookmark = BookmarkController(current_user.id).read().delete() - db.session.commit() - flash(gettext("Bookmarks successfully deleted."), 'success') - return redirect(redirect_url()) - - -@bookmark_bp.route('/bookmarklet', methods=['GET', 'POST']) -@login_required -def bookmarklet(): - bookmark_contr = BookmarkController(current_user.id) - href = (request.args if request.method == 'GET' else request.form)\ - .get('href', None) - if not href: - flash(gettext("Couldn't add bookmark: url missing."), "error") - raise BadRequest("url is missing") - title = (request.args if request.method == 'GET' else request.form)\ - .get('title', None) - if not title: - title = href - - bookmark_exists = bookmark_contr.read(**{'href': href}).all() - if bookmark_exists: - flash(gettext("Couldn't add bookmark: bookmark already exists."), - "warning") - return redirect(url_for('bookmark.form', - bookmark_id=bookmark_exists[0].id)) - - bookmark_attr = {'href': href, - 'description': '', - 'title': title, - 'shared': True, - 'to_read': True} - - new_bookmark = bookmark_contr.create(**bookmark_attr) - flash(gettext('Bookmark successfully created.'), 'success') - return redirect(url_for('bookmark.form', bookmark_id=new_bookmark.id)) - - -@bookmark_bp.route('/import_pinboard', methods=['POST']) -@login_required -def import_pinboard(): - bookmarks = request.files.get('jsonfile', None) - if bookmarks: - try: - nb_bookmarks = import_pinboard_json(current_user, bookmarks.read()) - flash(gettext("%(nb_bookmarks)s bookmarks successfully imported.", - nb_bookmarks=nb_bookmarks), 'success') - except Exception as e: - flash(gettext('Error when importing bookmarks.'), 'error') - - return redirect(redirect_url()) - - -@bookmarks_bp.route('/export', methods=['GET']) -@login_required -def export(): - bookmarks = export_bookmarks(current_user) - response = make_response(bookmarks) - response.mimetype = 'application/json' - response.headers["Content-Disposition"] \ - = 'attachment; filename=newspipe_bookmarks_export.json' - return response diff --git a/src/web/views/category.py b/src/web/views/category.py deleted file mode 100644 index 138561dd..00000000 --- a/src/web/views/category.py +++ /dev/null @@ -1,86 +0,0 @@ -from flask import Blueprint, render_template, flash, redirect, url_for -from flask_babel import gettext -from flask_login import login_required, current_user - -from web.forms import CategoryForm -from lib.utils import redirect_url -from web.lib.view_utils import etag_match -from web.controllers import ArticleController, FeedController, \ - CategoryController - -categories_bp = Blueprint('categories', __name__, url_prefix='/categories') -category_bp = Blueprint('category', __name__, url_prefix='/category') - - -@categories_bp.route('/', methods=['GET']) -@login_required -@etag_match -def list_(): - "Lists the subscribed feeds in a table." - art_contr = ArticleController(current_user.id) - return render_template('categories.html', - categories=list(CategoryController(current_user.id).read().order_by('name')), - feeds_count=FeedController(current_user.id).count_by_category(), - unread_article_count=art_contr.count_by_category(readed=False), - article_count=art_contr.count_by_category()) - - -@category_bp.route('/create', methods=['GET']) -@category_bp.route('/edit/', methods=['GET']) -@login_required -@etag_match -def form(category_id=None): - action = gettext("Add a category") - head_titles = [action] - if category_id is None: - return render_template('edit_category.html', action=action, - head_titles=head_titles, form=CategoryForm()) - category = CategoryController(current_user.id).get(id=category_id) - action = gettext('Edit category') - head_titles = [action] - if category.name: - head_titles.append(category.name) - return render_template('edit_category.html', action=action, - head_titles=head_titles, category=category, - form=CategoryForm(obj=category)) - - -@category_bp.route('/delete/', methods=['GET']) -@login_required -def delete(category_id=None): - category = CategoryController(current_user.id).delete(category_id) - flash(gettext("Category %(category_name)s successfully deleted.", - category_name=category.name), 'success') - return redirect(redirect_url()) - - -@category_bp.route('/create', methods=['POST']) -@category_bp.route('/edit/', methods=['POST']) -@login_required -def process_form(category_id=None): - form = CategoryForm() - cat_contr = CategoryController(current_user.id) - - if not form.validate(): - return render_template('edit_category.html', form=form) - existing_cats = list(cat_contr.read(name=form.name.data)) - if existing_cats and category_id is None: - flash(gettext("Couldn't add category: already exists."), "warning") - return redirect(url_for('category.form', - category_id=existing_cats[0].id)) - # Edit an existing category - category_attr = {'name': form.name.data} - - if category_id is not None: - cat_contr.update({'id': category_id}, category_attr) - flash(gettext('Category %(cat_name)r successfully updated.', - cat_name=category_attr['name']), 'success') - return redirect(url_for('category.form', category_id=category_id)) - - # Create a new category - new_category = cat_contr.create(**category_attr) - - flash(gettext('Category %(category_name)r successfully created.', - category_name=new_category.name), 'success') - - return redirect(url_for('category.form', category_id=new_category.id)) diff --git a/src/web/views/common.py b/src/web/views/common.py deleted file mode 100644 index e422fd57..00000000 --- a/src/web/views/common.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -from functools import wraps -from datetime import datetime -from flask import current_app, Response -from flask_login import login_user -from flask_principal import (Identity, Permission, RoleNeed, - session_identity_loader, identity_changed) -from web.controllers import UserController -from lib.utils import default_handler - -admin_role = RoleNeed('admin') -api_role = RoleNeed('api') - -admin_permission = Permission(admin_role) -api_permission = Permission(api_role) - - -def scoped_default_handler(): - if admin_permission.can(): - role = 'admin' - elif api_permission.can(): - role = 'api' - else: - role = 'user' - - @wraps(default_handler) - def wrapper(obj): - return default_handler(obj, role=role) - return wrapper - - -def jsonify(func): - """Will cast results of func as a result, and try to extract - a status_code for the Response object""" - @wraps(func) - def wrapper(*args, **kwargs): - status_code = 200 - result = func(*args, **kwargs) - if isinstance(result, Response): - return result - elif isinstance(result, tuple): - result, status_code = result - return Response(json.dumps(result, default=scoped_default_handler()), - mimetype='application/json', status=status_code) - return wrapper - - -def login_user_bundle(user): - login_user(user) - identity_changed.send(current_app, identity=Identity(user.id)) - session_identity_loader() - UserController(user.id).update( - {'id': user.id}, {'last_seen': datetime.utcnow()}) diff --git a/src/web/views/feed.py b/src/web/views/feed.py deleted file mode 100644 index b98a005a..00000000 --- a/src/web/views/feed.py +++ /dev/null @@ -1,306 +0,0 @@ -import logging -import requests.exceptions -from datetime import datetime, timedelta -from sqlalchemy import desc -from werkzeug.exceptions import BadRequest - -from flask import Blueprint, render_template, flash, \ - redirect, request, url_for, make_response -from flask_babel import gettext -from flask_login import login_required, current_user -from flask_paginate import Pagination, get_page_args - -import conf -from lib import misc_utils, utils -from lib.feed_utils import construct_feed_from -from web.lib.view_utils import etag_match -from web.forms import AddFeedForm -from web.controllers import (UserController, CategoryController, - FeedController, ArticleController) - -logger = logging.getLogger(__name__) -feeds_bp = Blueprint('feeds', __name__, url_prefix='/feeds') -feed_bp = Blueprint('feed', __name__, url_prefix='/feed') - - -@feeds_bp.route('/', methods=['GET']) -@login_required -@etag_match -def feeds(): - "Lists the subscribed feeds in a table." - art_contr = ArticleController(current_user.id) - return render_template('feeds.html', - feeds=FeedController(current_user.id).read().order_by('title'), - unread_article_count=art_contr.count_by_feed(readed=False), - article_count=art_contr.count_by_feed()) - - -def feed_view(feed_id=None, user_id=None): - feed = FeedController(user_id).get(id=feed_id) - word_size = 6 - category = None - if feed.category_id: - category = CategoryController(user_id).get(id=feed.category_id) - filters = {} - filters['feed_id'] = feed_id - articles = ArticleController(user_id).read_light(**filters) - - # Server-side pagination - page, per_page, offset = get_page_args(per_page_parameter='per_page') - pagination = Pagination(page=page, total=articles.count(), - css_framework='bootstrap3', - search=False, record_name='articles', - per_page=per_page) - - today = datetime.now() - try: - last_article = articles[0].date - first_article = articles[-1].date - delta = last_article - first_article - average = round(float(articles.count()) / abs(delta.days), 2) - except Exception as e: - last_article = datetime.fromtimestamp(0) - first_article = datetime.fromtimestamp(0) - delta = last_article - first_article - average = 0 - elapsed = today - last_article - - return render_template('feed.html', - head_titles=[utils.clear_string(feed.title)], - feed=feed, category=category, - articles=articles.offset(offset).limit(per_page), - pagination=pagination, - first_post_date=first_article, - end_post_date=last_article, - average=average, delta=delta, elapsed=elapsed) - - -@feed_bp.route('/', methods=['GET']) -@login_required -@etag_match -def feed(feed_id=None): - "Presents detailed information about a feed." - return feed_view(feed_id, current_user.id) - - -@feed_bp.route('/public/', methods=['GET']) -@etag_match -def feed_pub(feed_id=None): - """ - Presents details of a pubic feed if the profile of the owner is also - public. - """ - feed = FeedController(None).get(id=feed_id) - if feed.private or not feed.user.is_public_profile: - return render_template('errors/404.html'), 404 - return feed_view(feed_id, None) - - -@feed_bp.route('/delete/', methods=['GET']) -@login_required -def delete(feed_id=None): - feed_contr = FeedController(current_user.id) - feed = feed_contr.get(id=feed_id) - feed_contr.delete(feed_id) - flash(gettext("Feed %(feed_title)s successfully deleted.", - feed_title=feed.title), 'success') - return redirect(url_for('home')) - - -@feed_bp.route('/reset_errors/', methods=['GET', 'POST']) -@login_required -def reset_errors(feed_id): - feed_contr = FeedController(current_user.id) - feed = feed_contr.get(id=feed_id) - feed_contr.update({'id': feed_id}, {'error_count': 0, 'last_error': ''}) - flash(gettext('Feed %(feed_title)r successfully updated.', - feed_title=feed.title), 'success') - return redirect(request.referrer or url_for('home')) - - -@feed_bp.route('/bookmarklet', methods=['GET', 'POST']) -@login_required -def bookmarklet(): - feed_contr = FeedController(current_user.id) - url = (request.args if request.method == 'GET' else request.form)\ - .get('url', None) - if not url: - flash(gettext("Couldn't add feed: url missing."), "error") - raise BadRequest("url is missing") - - feed_exists = list(feed_contr.read(__or__={'link': url, 'site_link': url})) - if feed_exists: - flash(gettext("Couldn't add feed: feed already exists."), - "warning") - return redirect(url_for('feed.form', feed_id=feed_exists[0].id)) - - try: - feed = construct_feed_from(url) - except requests.exceptions.ConnectionError: - flash(gettext("Impossible to connect to the address: {}.".format(url)), - "danger") - return redirect(url_for('home')) - except Exception: - logger.exception('something bad happened when fetching %r', url) - return redirect(url_for('home')) - if not feed.get('link'): - feed['enabled'] = False - flash(gettext("Couldn't find a feed url, you'll need to find a Atom or" - " RSS link manually and reactivate this feed"), - 'warning') - feed = feed_contr.create(**feed) - flash(gettext('Feed was successfully created.'), 'success') - if feed.enabled and conf.CRAWLING_METHOD == "default": - misc_utils.fetch(current_user.id, feed.id) - flash(gettext("Downloading articles for the new feed..."), 'info') - return redirect(url_for('feed.form', feed_id=feed.id)) - - -@feed_bp.route('/update//', methods=['GET', 'POST']) -@feeds_bp.route('/update/', methods=['GET', 'POST']) -@login_required -def update(action, feed_id=None): - readed = action == 'read' - filters = {'readed__ne': readed} - - nb_days = request.args.get('nb_days', 0, type=int) - if nb_days != 0: - filters['date__lt'] = datetime.now() - timedelta(days=nb_days) - - if feed_id: - filters['feed_id'] = feed_id - ArticleController(current_user.id).update(filters, {'readed': readed}) - flash(gettext('Feed successfully updated.'), 'success') - return redirect(request.referrer or url_for('home')) - - -@feed_bp.route('/create', methods=['GET']) -@feed_bp.route('/edit/', methods=['GET']) -@login_required -@etag_match -def form(feed_id=None): - action = gettext("Add a feed") - categories = CategoryController(current_user.id).read() - head_titles = [action] - if feed_id is None: - form = AddFeedForm() - form.set_category_choices(categories) - return render_template('edit_feed.html', action=action, - head_titles=head_titles, form=form) - feed = FeedController(current_user.id).get(id=feed_id) - form = AddFeedForm(obj=feed) - form.set_category_choices(categories) - action = gettext('Edit feed') - head_titles = [action] - if feed.title: - head_titles.append(feed.title) - return render_template('edit_feed.html', action=action, - head_titles=head_titles, categories=categories, - form=form, feed=feed) - - -@feed_bp.route('/create', methods=['POST']) -@feed_bp.route('/edit/', methods=['POST']) -@login_required -def process_form(feed_id=None): - form = AddFeedForm() - feed_contr = FeedController(current_user.id) - form.set_category_choices(CategoryController(current_user.id).read()) - - if not form.validate(): - return render_template('edit_feed.html', form=form) - existing_feeds = list(feed_contr.read(link=form.link.data)) - if existing_feeds and feed_id is None: - flash(gettext("Couldn't add feed: feed already exists."), "warning") - return redirect(url_for('feed.form', feed_id=existing_feeds[0].id)) - # Edit an existing feed - feed_attr = {'title': form.title.data, 'enabled': form.enabled.data, - 'link': form.link.data, 'site_link': form.site_link.data, - 'filters': [], 'category_id': form.category_id.data, - 'private': form.private.data} - if not feed_attr['category_id'] or feed_attr['category_id'] == '0': - del feed_attr['category_id'] - - for filter_attr in ('type', 'pattern', 'action on', 'action'): - for i, value in enumerate( - request.form.getlist(filter_attr.replace(' ', '_'))): - if i >= len(feed_attr['filters']): - feed_attr['filters'].append({}) - feed_attr['filters'][i][filter_attr] = value - - if feed_id is not None: - feed_contr.update({'id': feed_id}, feed_attr) - flash(gettext('Feed %(feed_title)r successfully updated.', - feed_title=feed_attr['title']), 'success') - return redirect(url_for('feed.form', feed_id=feed_id)) - - # Create a new feed - new_feed = feed_contr.create(**feed_attr) - - flash(gettext('Feed %(feed_title)r successfully created.', - feed_title=new_feed.title), 'success') - - if conf.CRAWLING_METHOD == "default": - misc_utils.fetch(current_user.id, new_feed.id) - flash(gettext("Downloading articles for the new feed..."), 'info') - - return redirect(url_for('feed.form', feed_id=new_feed.id)) - - -@feeds_bp.route('/inactives', methods=['GET']) -@login_required -def inactives(): - """ - List of inactive feeds. - """ - nb_days = int(request.args.get('nb_days', 365)) - inactives = FeedController(current_user.id).get_inactives(nb_days) - return render_template('inactives.html', - inactives=inactives, nb_days=nb_days) - - -@feed_bp.route('/duplicates/', methods=['GET']) -@login_required -def duplicates(feed_id): - """ - Return duplicates article for a feed. - """ - feed, duplicates = FeedController(current_user.id).get_duplicates(feed_id) - if len(duplicates) == 0: - flash(gettext('No duplicates in the feed "{}".').format(feed.title), - 'info') - return redirect(url_for('home')) - return render_template('duplicates.html', duplicates=duplicates, feed=feed) - - -@feeds_bp.route('/export', methods=['GET']) -@login_required -def export(): - """ - Export feeds to OPML. - """ - include_disabled = request.args.get('includedisabled', '') == 'on' - include_private = request.args.get('includeprivate', '') == 'on' - include_exceeded_error_count = request.args. \ - get('includeexceedederrorcount', '') == 'on' - - filter = {} - if not include_disabled: - filter['enabled'] = True - if not include_private: - filter['private'] = False - if not include_exceeded_error_count: - filter['error_count__lt'] = conf.DEFAULT_MAX_ERROR - - user = UserController(current_user.id).get(id=current_user.id) - feeds = FeedController(current_user.id).read(**filter) - categories = {cat.id: cat.dump() - for cat in CategoryController(user.id).read()} - - response = make_response(render_template('opml.xml', - user=user, feeds=feeds, - categories=categories, - now=datetime.now())) - response.headers['Content-Type'] = 'application/xml' - response.headers['Content-Disposition'] = 'attachment; filename=feeds.opml' - return response diff --git a/src/web/views/home.py b/src/web/views/home.py deleted file mode 100644 index 34ecb9fa..00000000 --- a/src/web/views/home.py +++ /dev/null @@ -1,172 +0,0 @@ -import pytz -import logging -from datetime import datetime - -from flask import current_app, render_template, \ - request, flash, url_for, redirect -from flask_login import login_required, current_user -from flask_babel import gettext, get_locale -from babel.dates import format_datetime, format_timedelta - -import conf -from lib.utils import redirect_url -from lib import misc_utils -from web.lib.view_utils import etag_match -from web.views.common import jsonify - -from web.controllers import FeedController, \ - ArticleController, CategoryController - -localize = pytz.utc.localize -logger = logging.getLogger(__name__) - - -@current_app.route('/') -@login_required -@etag_match -def home(): - return render_template('home.html', cdn=conf.CDN_ADDRESS) - - -@current_app.route('/menu') -@login_required -@etag_match -@jsonify -def get_menu(): - now, locale = datetime.now(), get_locale() - categories_order = [0] - categories = {0: {'name': 'No category', 'id': 0}} - for cat in CategoryController(current_user.id).read().order_by('name'): - categories_order.append(cat.id) - categories[cat.id] = cat - unread = ArticleController(current_user.id).count_by_feed(readed=False) - for cat_id in categories: - categories[cat_id]['unread'] = 0 - categories[cat_id]['feeds'] = [] - feeds = {feed.id: feed for feed in FeedController(current_user.id).read()} - for feed_id, feed in feeds.items(): - feed['created_rel'] = format_timedelta(feed.created_date - now, - add_direction=True, locale=locale) - feed['last_rel'] = format_timedelta(feed.last_retrieved - now, - add_direction=True, locale=locale) - feed['created_date'] = format_datetime(localize(feed.created_date), - locale=locale) - feed['last_retrieved'] = format_datetime(localize(feed.last_retrieved), - locale=locale) - feed['category_id'] = feed.category_id or 0 - feed['unread'] = unread.get(feed.id, 0) - if not feed.filters: - feed['filters'] = [] - if feed.icon_url: - feed['icon_url'] = url_for('icon.icon', url=feed.icon_url) - categories[feed['category_id']]['unread'] += feed['unread'] - categories[feed['category_id']]['feeds'].append(feed_id) - return {'feeds': feeds, 'categories': categories, - 'categories_order': categories_order, - 'crawling_method': conf.CRAWLING_METHOD, - 'max_error': conf.DEFAULT_MAX_ERROR, - 'error_threshold': conf.ERROR_THRESHOLD, - 'is_admin': current_user.is_admin, - 'all_unread_count': sum(unread.values())} - - -def _get_filters(in_dict): - filters = {} - query = in_dict.get('query') - if query: - search_title = in_dict.get('search_title') == 'true' - search_content = in_dict.get('search_content') == 'true' - if search_title: - filters['title__ilike'] = "%%%s%%" % query - if search_content: - filters['content__ilike'] = "%%%s%%" % query - if len(filters) == 0: - filters['title__ilike'] = "%%%s%%" % query - if len(filters) > 1: - filters = {"__or__": filters} - if in_dict.get('filter') == 'unread': - filters['readed'] = False - elif in_dict.get('filter') == 'liked': - filters['like'] = True - filter_type = in_dict.get('filter_type') - if filter_type in {'feed_id', 'category_id'} and in_dict.get('filter_id'): - filters[filter_type] = int(in_dict['filter_id']) or None - return filters - - -@jsonify -def _articles_to_json(articles, fd_hash=None): - now, locale = datetime.now(), get_locale() - fd_hash = {feed.id: {'title': feed.title, - 'icon_url': url_for('icon.icon', url=feed.icon_url) - if feed.icon_url else None} - for feed in FeedController(current_user.id).read()} - - return {'articles': [{'title': art.title, 'liked': art.like, - 'read': art.readed, 'article_id': art.id, 'selected': False, - 'feed_id': art.feed_id, 'category_id': art.category_id or 0, - 'feed_title': fd_hash[art.feed_id]['title'] if fd_hash else None, - 'icon_url': fd_hash[art.feed_id]['icon_url'] if fd_hash else None, - 'date': format_datetime(localize(art.date), locale=locale), - 'rel_date': format_timedelta(art.date - now, - threshold=1.1, add_direction=True, - locale=locale)} - for art in articles.limit(1000)]} - - -@current_app.route('/middle_panel') -@login_required -@etag_match -def get_middle_panel(): - filters = _get_filters(request.args) - art_contr = ArticleController(current_user.id) - articles = art_contr.read_light(**filters) - return _articles_to_json(articles) - - -@current_app.route('/getart/') -@current_app.route('/getart//') -@login_required -@etag_match -@jsonify -def get_article(article_id, parse=False): - locale = get_locale() - contr = ArticleController(current_user.id) - article = contr.get(id=article_id) - if not article.readed: - article['readed'] = True - contr.update({'id': article_id}, {'readed': True}) - article['category_id'] = article.category_id or 0 - feed = FeedController(current_user.id).get(id=article.feed_id) - article['icon_url'] = url_for('icon.icon', url=feed.icon_url) \ - if feed.icon_url else None - article['date'] = format_datetime(localize(article.date), locale=locale) - return article - - -@current_app.route('/mark_all_as_read', methods=['PUT']) -@login_required -def mark_all_as_read(): - filters = _get_filters(request.json) - acontr = ArticleController(current_user.id) - processed_articles = _articles_to_json(acontr.read_light(**filters)) - acontr.update(filters, {'readed': True}) - return processed_articles - - -@current_app.route('/fetch', methods=['GET']) -@current_app.route('/fetch/', methods=['GET']) -@login_required -def fetch(feed_id=None): - """ - Triggers the download of news. - News are downloaded in a separated process. - """ - if conf.CRAWLING_METHOD == "default" \ - and (not conf.ON_HEROKU or current_user.is_admin): - misc_utils.fetch(current_user.id, feed_id) - flash(gettext("Downloading articles..."), "info") - else: - flash(gettext("The manual retrieving of news is only available " + - "for administrator, on the Heroku platform."), "info") - return redirect(redirect_url()) diff --git a/src/web/views/icon.py b/src/web/views/icon.py deleted file mode 100644 index 64e54cab..00000000 --- a/src/web/views/icon.py +++ /dev/null @@ -1,15 +0,0 @@ -import base64 -from flask import Blueprint, Response, request -from web.controllers import IconController -from web.lib.view_utils import etag_match - -icon_bp = Blueprint('icon', __name__, url_prefix='/icon') - - -@icon_bp.route('/', methods=['GET']) -@etag_match -def icon(): - icon = IconController().get(url=request.args['url']) - headers = {'Cache-Control': 'max-age=86400', - 'Content-Type': icon.mimetype} - return Response(base64.b64decode(icon.content), headers=headers) diff --git a/src/web/views/session_mgmt.py b/src/web/views/session_mgmt.py deleted file mode 100644 index 0db76115..00000000 --- a/src/web/views/session_mgmt.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -import logging - -from datetime import datetime -from werkzeug.security import generate_password_hash -from werkzeug.exceptions import NotFound -from flask import (render_template, flash, session, request, - url_for, redirect, current_app) -from flask_babel import gettext, lazy_gettext -from flask_login import LoginManager, logout_user, \ - login_required, current_user -from flask_principal import (Principal, AnonymousIdentity, UserNeed, - identity_changed, identity_loaded, - session_identity_loader) - -import conf -from web.views.common import admin_role, api_role, login_user_bundle -from web.controllers import UserController -from web.forms import SignupForm, SigninForm -from notifications import notifications - -Principal(current_app) -# Create a permission with a single Need, in this case a RoleNeed. - -login_manager = LoginManager() -login_manager.init_app(current_app) -login_manager.login_view = 'login' -login_manager.login_message = lazy_gettext('Please log in to access this page.') -login_manager.login_message_category = 'info' - -logger = logging.getLogger(__name__) - - -@identity_loaded.connect_via(current_app._get_current_object()) -def on_identity_loaded(sender, identity): - # Set the identity user object - identity.user = current_user - - # Add the UserNeed to the identity - if current_user.is_authenticated: - identity.provides.add(UserNeed(current_user.id)) - if current_user.is_admin: - identity.provides.add(admin_role) - if current_user.is_api: - identity.provides.add(api_role) - - -@login_manager.user_loader -def load_user(user_id): - return UserController(user_id, ignore_context=True).get( - id=user_id, is_active=True) - -@current_app.before_request -def before_request(): - if current_user.is_authenticated: - UserController(current_user.id).update( - {'id': current_user.id}, {'last_seen': datetime.utcnow()}) - -@current_app.route('/login', methods=['GET', 'POST']) -def login(): - if current_user.is_authenticated: - return redirect(url_for('home')) - form = SigninForm() - if form.validate_on_submit(): - login_user_bundle(form.user) - return form.redirect('home') - return render_template('login.html', form=form) - - -@current_app.route('/logout') -@login_required -def logout(): - # Remove the user information from the session - logout_user() - - # Remove session keys set by Flask-Principal - for key in ('identity.name', 'identity.auth_type'): - session.pop(key, None) - - # Tell Flask-Principal the user is anonymous - identity_changed.send(current_app, identity=AnonymousIdentity()) - session_identity_loader() - - return redirect(url_for('login')) - - -@current_app.route('/signup', methods=['GET', 'POST']) -def signup(): - if not conf.SELF_REGISTRATION: - flash(gettext('Self-registration is disabled.'), 'warning') - return redirect(url_for('home')) - if current_user.is_authenticated: - return redirect(url_for('home')) - - form = SignupForm() - if form.validate_on_submit(): - user = UserController().create(nickname=form.nickname.data, - pwdhash=generate_password_hash(form.password.data)) - - # Send the confirmation email - try: - notifications.new_account_notification(user, form.email.data) - except Exception as error: - flash(gettext('Problem while sending activation email: %(error)s', - error=error), 'danger') - return redirect(url_for('home')) - - flash(gettext('Your account has been created. ' - 'Check your mail to confirm it.'), 'success') - - return redirect(url_for('home')) - - return render_template('signup.html', form=form) diff --git a/src/web/views/user.py b/src/web/views/user.py deleted file mode 100644 index 24b73a60..00000000 --- a/src/web/views/user.py +++ /dev/null @@ -1,203 +0,0 @@ -import string -import random -from datetime import datetime, timedelta -from flask import (Blueprint, g, render_template, redirect, - flash, url_for, request) -from flask_babel import gettext -from flask_login import login_required, current_user -from flask_paginate import Pagination, get_page_args - -import conf -from notifications import notifications -from lib import misc_utils -from lib.data import import_opml, import_json -from web.lib.user_utils import confirm_token -from web.controllers import (UserController, FeedController, ArticleController, - CategoryController, BookmarkController) - -from web.forms import ProfileForm - -users_bp = Blueprint('users', __name__, url_prefix='/users') -user_bp = Blueprint('user', __name__, url_prefix='/user') - - -@user_bp.route('/', methods=['GET']) -def profile_public(nickname=None): - """ - Display the public profile of the user. - """ - category_id = int(request.args.get('category_id', 0)) - user_contr = UserController() - user = user_contr.get(nickname=nickname) - if not user.is_public_profile: - if current_user.is_authenticated and current_user.id == user.id: - flash(gettext('You must set your profile to public.'), 'info') - return redirect(url_for('user.profile')) - - filters = {} - filters['private'] = False - if category_id: - filters['category_id'] = category_id - feeds = FeedController(user.id).read(**filters) - - return render_template('profile_public.html', user=user, feeds=feeds, - selected_category_id=category_id) - - -@user_bp.route('//stream', defaults={'per_page': '25'}, methods=['GET']) -def user_stream(per_page, nickname=None): - """ - Display the stream of a user (list of articles of public feed). - """ - user_contr = UserController() - user = user_contr.get(nickname=nickname) - if not user.is_public_profile: - if current_user.is_authenticated and current_user.id == user.id: - flash(gettext('You must set your profile to public.'), 'info') - return redirect(url_for('user.profile')) - - category_id = int(request.args.get('category_id', 0)) - category = CategoryController().read(id=category_id).first() - - # Load the public feeds - filters = {} - filters['private'] = False - if category_id: - filters['category_id'] = category_id - feeds = FeedController().read(**filters).all() - - # Re-initializes the filters to load the articles - filters = {} - filters['feed_id__in'] = [feed.id for feed in feeds] - if category: - filters['category_id'] = category_id - articles = ArticleController(user.id).read_light(**filters) - - # Server-side pagination - page, per_page, offset = get_page_args(per_page_parameter='per_page') - pagination = Pagination(page=page, total=articles.count(), - css_framework='bootstrap3', - search=False, record_name='articles', - per_page=per_page) - - return render_template('user_stream.html', user=user, - articles=articles.offset(offset).limit(per_page), - category=category, - pagination=pagination) - - -@user_bp.route('/management', methods=['GET', 'POST']) -@login_required -def management(): - """ - Display the management page. - """ - if request.method == 'POST': - if None != request.files.get('opmlfile', None): - # Import an OPML file - data = request.files.get('opmlfile', None) - if not misc_utils.allowed_file(data.filename): - flash(gettext('File not allowed.'), 'danger') - else: - try: - nb = import_opml(current_user.nickname, data.read()) - if conf.CRAWLING_METHOD == "classic": - misc_utils.fetch(current_user.id, None) - flash(str(nb) + ' ' + gettext('feeds imported.'), - "success") - flash(gettext("Downloading articles..."), 'info') - except: - flash(gettext("Impossible to import the new feeds."), - "danger") - elif None != request.files.get('jsonfile', None): - # Import an account - data = request.files.get('jsonfile', None) - if not misc_utils.allowed_file(data.filename): - flash(gettext('File not allowed.'), 'danger') - else: - try: - nb = import_json(current_user.nickname, data.read()) - flash(gettext('Account imported.'), "success") - except: - flash(gettext("Impossible to import the account."), - "danger") - else: - flash(gettext('File not allowed.'), 'danger') - - nb_feeds = FeedController(current_user.id).read().count() - art_contr = ArticleController(current_user.id) - nb_articles = art_contr.read().count() - nb_unread_articles = art_contr.read(readed=False).count() - nb_categories = CategoryController(current_user.id).read().count() - nb_bookmarks = BookmarkController(current_user.id).read().count() - return render_template('management.html', user=current_user, - nb_feeds=nb_feeds, nb_articles=nb_articles, - nb_unread_articles=nb_unread_articles, - nb_categories=nb_categories, - nb_bookmarks=nb_bookmarks) - - -@user_bp.route('/profile', methods=['GET', 'POST']) -@login_required -def profile(): - """ - Edit the profile of the currently logged user. - """ - user_contr = UserController(current_user.id) - user = user_contr.get(id=current_user.id) - form = ProfileForm() - - if request.method == 'POST': - if form.validate(): - try: - user_contr.update({'id': current_user.id}, - {'nickname': form.nickname.data, - 'password': form.password.data, - 'automatic_crawling': form.automatic_crawling.data, - 'is_public_profile': form.is_public_profile.data, - 'bio': form.bio.data, - 'webpage': form.webpage.data, - 'twitter': form.twitter.data}) - except Exception as error: - flash(gettext('Problem while updating your profile: ' - '%(error)s', error=error), 'danger') - else: - flash(gettext('User %(nick)s successfully updated', - nick=user.nickname), 'success') - return redirect(url_for('user.profile')) - else: - return render_template('profile.html', user=user, form=form) - - if request.method == 'GET': - form = ProfileForm(obj=user) - return render_template('profile.html', user=user, form=form) - - -@user_bp.route('/delete_account', methods=['GET']) -@login_required -def delete_account(): - """ - Delete the account of the user (with all its data). - """ - UserController(current_user.id).delete(current_user.id) - flash(gettext('Your account has been deleted.'), 'success') - return redirect(url_for('login')) - - -@user_bp.route('/confirm_account/', methods=['GET']) -def confirm_account(token=None): - """ - Confirm the account of a user. - """ - user_contr = UserController() - user, nickname = None, None - if token != "": - nickname = confirm_token(token) - if nickname: - user = user_contr.read(nickname=nickname).first() - if user is not None: - user_contr.update({'id': user.id}, {'is_active': True}) - flash(gettext('Your account has been confirmed.'), 'success') - else: - flash(gettext('Impossible to confirm this account.'), 'danger') - return redirect(url_for('login')) diff --git a/src/web/views/views.py b/src/web/views/views.py deleted file mode 100644 index 57f790b1..00000000 --- a/src/web/views/views.py +++ /dev/null @@ -1,95 +0,0 @@ -import sys -import logging -import operator -from datetime import datetime, timedelta -from flask import (request, render_template, flash, - url_for, redirect, current_app) -from flask_babel import gettext -from sqlalchemy import desc - -import conf -from web import __version__ -from conf import API_ROOT, ADMIN_EMAIL -from web.controllers import FeedController, UserController -from web.lib.view_utils import etag_match - -logger = logging.getLogger(__name__) - - -@current_app.errorhandler(401) -def authentication_required(error): - if API_ROOT in request.url: - return error - flash(gettext('Authentication required.'), 'info') - return redirect(url_for('login')) - - -@current_app.errorhandler(403) -def authentication_failed(error): - if API_ROOT in request.url: - return error - flash(gettext('Forbidden.'), 'danger') - return redirect(url_for('login')) - - -@current_app.errorhandler(404) -def page_not_found(error): - return render_template('errors/404.html'), 404 - - -@current_app.errorhandler(500) -def internal_server_error(error): - return render_template('errors/500.html'), 500 - - -@current_app.errorhandler(AssertionError) -def handle_sqlalchemy_assertion_error(error): - return error.args[0], 400 - - -@current_app.route('/popular', methods=['GET']) -@etag_match -def popular(): - """ - Return the most popular feeds for the last nb_days days. - """ - # try to get the 'recent' popular websites, created after - # 'not_created_before' - # ie: not_added_before = date_last_added_feed - nb_days - try: - nb_days = int(request.args.get('nb_days', 365)) - except ValueError: - nb_days = 10000 - last_added_feed = FeedController().read().\ - order_by(desc('created_date')).limit(1).all() - if last_added_feed: - date_last_added_feed = last_added_feed[0].created_date - else: - date_last_added_feed = datetime.now() - not_added_before = date_last_added_feed - timedelta(days=nb_days) - - filters = {} - filters['created_date__gt'] = not_added_before - filters['private'] = False - filters['error_count__lt'] = conf.DEFAULT_MAX_ERROR - feeds = FeedController().count_by_link(**filters) - sorted_feeds = sorted(list(feeds.items()), key=operator.itemgetter(1), - reverse=True) - return render_template('popular.html', popular=sorted_feeds) - - -@current_app.route('/about', methods=['GET']) -@etag_match -def about(): - return render_template('about.html', contact=ADMIN_EMAIL) - -@current_app.route('/about/more', methods=['GET']) -@etag_match -def about_more(): - return render_template('about_more.html', - newspipe_version=__version__.split()[1], - on_heroku=[conf.ON_HEROKU and 'Yes' or 'No'][0], - registration=[conf.SELF_REGISTRATION and 'Open' or 'Closed'][0], - python_version="{}.{}.{}".format(*sys.version_info[:3]), - nb_users=UserController().read().count()) - -- cgit