~cedric/newspipe

3ab6290d4994b33cdbf831523938cdb18a13bf49 — Cédric Bonhomme 1 year, 2 months ago c58e6a7
Refactoring the backend.
98 files changed, 276 insertions(+), 282 deletions(-)

M .gitignore
A instance/production.py
R newspipe/manager.py => manager.py
M newspipe/bootstrap.py
M newspipe/conf.py
D newspipe/conf/conf.cfg-sample
R newspipe/{web/controllers/__init__.py => controllers/__init__.py}
R newspipe/{web/controllers/abstract.py => controllers/abstract.py}
R newspipe/{web/controllers/article.py => controllers/article.py}
R newspipe/{web/controllers/bookmark.py => controllers/bookmark.py}
R newspipe/{web/controllers/category.py => controllers/category.py}
R newspipe/{web/controllers/feed.py => controllers/feed.py}
R newspipe/{web/controllers/icon.py => controllers/icon.py}
R newspipe/{web/controllers/tag.py => controllers/tag.py}
R newspipe/{web/controllers/user.py => controllers/user.py}
M newspipe/crawler/default_crawler.py
M newspipe/lib/article_utils.py
M newspipe/lib/data.py
M newspipe/lib/feed_utils.py
M newspipe/lib/misc_utils.py
M newspipe/lib/utils.py
R newspipe/{web/models/__init__.py => models/__init__.py}
R newspipe/{web/models/article.py => models/article.py}
R newspipe/{web/models/bookmark.py => models/bookmark.py}
R newspipe/{web/models/category.py => models/category.py}
R newspipe/{web/models/feed.py => models/feed.py}
R newspipe/{web/models/icon.py => models/icon.py}
R newspipe/{web/models/right_mixin.py => models/right_mixin.py}
R newspipe/{web/models/role.py => models/role.py}
R newspipe/{web/models/tag.py => models/tag.py}
R newspipe/{web/models/user.py => models/user.py}
M newspipe/notifications/emails.py
M newspipe/notifications/notifications.py
R newspipe/{web/static/css/custom.css => static/css/custom.css}
R newspipe/{web/static/img/favicon.ico => static/img/favicon.ico}
R newspipe/{web/static/img/newspipe.png => static/img/newspipe.png}
R newspipe/{web/static/img/newspipe.svg => static/img/newspipe.svg}
R newspipe/{web/static/img/pinboard.png => static/img/pinboard.png}
R newspipe/{web/static/img/reddit.png => static/img/reddit.png}
R newspipe/{web/static/img/twitter.png => static/img/twitter.png}
R newspipe/{web/static/js/articles.js => static/js/articles.js}
R newspipe/{web/static/js/feed.js => static/js/feed.js}
R newspipe/{web/templates/about.html => templates/about.html}
R newspipe/{web/templates/about_more.html => templates/about_more.html}
R newspipe/{web/templates/admin/create_user.html => templates/admin/create_user.html}
R newspipe/{web/templates/admin/dashboard.html => templates/admin/dashboard.html}
R newspipe/{web/templates/article.html => templates/article.html}
R newspipe/{web/templates/article_pub.html => templates/article_pub.html}
R newspipe/{web/templates/bookmarks.html => templates/bookmarks.html}
R newspipe/{web/templates/categories.html => templates/categories.html}
R newspipe/{web/templates/duplicates.html => templates/duplicates.html}
R newspipe/{web/templates/edit_bookmark.html => templates/edit_bookmark.html}
R newspipe/{web/templates/edit_category.html => templates/edit_category.html}
R newspipe/{web/templates/edit_feed.html => templates/edit_feed.html}
R newspipe/{web/templates/emails/account_activation.txt => templates/emails/account_activation.txt}
R newspipe/{web/templates/emails/new_password.txt => templates/emails/new_password.txt}
R newspipe/{web/templates/errors/404.html => templates/errors/404.html}
R newspipe/{web/templates/errors/500.html => templates/errors/500.html}
R newspipe/{web/templates/feed.html => templates/feed.html}
R newspipe/{web/templates/feed_list.html => templates/feed_list.html}
R newspipe/{web/templates/feed_list_per_categories.html => templates/feed_list_per_categories.html}
R newspipe/{web/templates/feed_list_simple.html => templates/feed_list_simple.html}
R newspipe/{web/templates/feeds.html => templates/feeds.html}
R newspipe/{web/templates/history.html => templates/history.html}
R newspipe/{web/templates/home.html => templates/home.html}
R newspipe/{web/templates/inactives.html => templates/inactives.html}
R newspipe/{web/templates/layout.html => templates/layout.html}
R newspipe/{web/templates/login.html => templates/login.html}
R newspipe/{web/templates/management.html => templates/management.html}
R newspipe/{web/templates/opml.xml => templates/opml.xml}
R newspipe/{web/templates/popular.html => templates/popular.html}
R newspipe/{web/templates/profile.html => templates/profile.html}
R newspipe/{web/templates/profile_public.html => templates/profile_public.html}
R newspipe/{web/templates/signup.html => templates/signup.html}
R newspipe/{web/templates/user_stream.html => templates/user_stream.html}
M newspipe/web/forms.py
M newspipe/web/lib/user_utils.py
M newspipe/web/lib/view_utils.py
M newspipe/web/views/__init__.py
M newspipe/web/views/admin.py
M newspipe/web/views/api/v2/__init__.py
M newspipe/web/views/api/v2/article.py
M newspipe/web/views/api/v2/category.py
M newspipe/web/views/api/v2/common.py
M newspipe/web/views/api/v2/feed.py
M newspipe/web/views/article.py
M newspipe/web/views/bookmark.py
M newspipe/web/views/category.py
M newspipe/web/views/common.py
M newspipe/web/views/feed.py
M newspipe/web/views/home.py
M newspipe/web/views/icon.py
M newspipe/web/views/session_mgmt.py
M newspipe/web/views/user.py
M newspipe/web/views/views.py
M package-lock.json
M package.json
R newspipe/runserver.py => runserver.py
M .gitignore => .gitignore +3 -3
@@ 24,10 24,10 @@ build

newspipe.egg-info/

newspipe/conf/conf.cfg
instance/development.py
.coverage

# js and node files
node_modules
newspipe/web/static/npm_components
newspipe/web/static/js/bundle.min.js
newspipe/static/npm_components
newspipe/static/js/bundle.min.js

A instance/production.py => instance/production.py +56 -0
@@ 0,0 1,56 @@
# [webserver]
HOST = '127.0.0.1'
PORT = 5000
SECRET_KEY = 'a secret only you know'
DEBUG = False
TESTING = False
API_ROOT = "/api/v2.0"

SECRET_KEY = 'LCx3BchmHRxFzkEv4BqQJyeXRLXenf'
SECURITY_PASSWORD_SALT = 'L8gTsyrpRQEF8jNWQPyvRfv7U5kJkD'


# [misc]
ADMIN_EMAIL = 'admin@admin.localhost'
SECURITY_PASSWORD_SALT = 'a secret to confirm user account'
TOKEN_VALIDITY_PERIOD = 3600
LOG_PATH = './var/newspipe.log'
NB_WORKER = 5
DEBUG = False
TESTING = False
LOG_LEVEL = 'info'
SELF_REGISTRATION = True


# [database]
DB_CONFIG_DICT = {
    'user': 'user',
    'password': 'password',
    'host': 'localhost',
    'port': 5432
}
DATABASE_NAME = 'newspipe'
SQLALCHEMY_DATABASE_URI = 'postgres://{user}:{password}@{host}:{port}/{name}'.format(name=DATABASE_NAME, **DB_CONFIG_DICT)
SQLALCHEMY_TRACK_MODIFICATIONS = False


# [crawler]
CRAWLING_METHOD = 'default'
DEFAULT_MAX_ERROR = 3
HTTP_PROXY = ''
USER_AGENT = 'JARR (https://github.com/JARR/JARR)'
RESOLVE_ARTICLE_URL = False
TIMEOUT = 30
RESOLV = False
FEED_REFRESH_INTERVAL = 0


# [notification]
MAIL_SERVER = 'localhost'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_DEBUG = DEBUG
MAIL_USERNAME = None
MAIL_PASSWORD = None
MAIL_DEFAULT_SENDER = ADMIN_EMAIL

R newspipe/manager.py => manager.py +4 -4
@@ 5,12 5,12 @@ import os
import logging
from datetime import datetime
from werkzeug.security import generate_password_hash
from bootstrap import application, db, conf, set_logging
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

import web.models
from web.controllers import UserController
import newspipe.models
from newspipe.controllers import UserController
from newspipe.bootstrap import application, db, set_logging

logger = logging.getLogger("manager")



@@ 62,7 62,7 @@ def fetch_asyncio(user_id=None, feed_id=None):
    import asyncio

    with application.app_context():
        from crawler import default_crawler
        from newspipe.crawler import default_crawler

        filters = {}
        filters["is_active"] = True

M newspipe/bootstrap.py => newspipe/bootstrap.py +22 -27
@@ 4,9 4,9 @@
# required imports and code execution for basic functionning

import os
import conf
import logging
from urllib.parse import urlsplit
from flask_babel import Babel, format_datetime


def set_logging(


@@ 47,41 47,36 @@ from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# Create Flask application
application = Flask("web")
application = Flask(__name__, instance_relative_config=True)
if os.environ.get("Newspipe_TESTING", False) == "true":
    application.debug = logging.DEBUG
    application.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
    application.config["TESTING"] = True
else:
    application.debug = conf.LOG_LEVEL <= logging.DEBUG
    application.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    application.config["SQLALCHEMY_DATABASE_URI"] = conf.SQLALCHEMY_DATABASE_URI
    if "postgres" in conf.SQLALCHEMY_DATABASE_URI:
        application.config["SQLALCHEMY_POOL_SIZE"] = 15
        application.config["SQLALCHEMY_MAX_OVERFLOW"] = 0
    try:
        application.config.from_pyfile("development.py", silent=False)
    except Exception:
        application.config.from_pyfile("production.py", silent=False)

scheme, domain, _, _, _ = urlsplit(conf.PLATFORM_URL)
application.config["SERVER_NAME"] = domain
application.config["PREFERRED_URL_SCHEME"] = scheme
# scheme, domain, _, _, _ = urlsplit(conf.PLATFORM_URL)
# application.config["SERVER_NAME"] = domain
# application.config["PREFERRED_URL_SCHEME"] = scheme

set_logging(conf.LOG_PATH, log_level=conf.LOG_LEVEL)

# Create secrey key so we can use sessions
application.config["SECRET_KEY"] = getattr(conf, "WEBSERVER_SECRET", None)
if not application.config["SECRET_KEY"]:
    application.config["SECRET_KEY"] = os.urandom(12)

application.config["SECURITY_PASSWORD_SALT"] = getattr(
    conf, "SECURITY_PASSWORD_SALT", None
)
if not application.config["SECURITY_PASSWORD_SALT"]:
    application.config["SECURITY_PASSWORD_SALT"] = os.urandom(12)
set_logging(application.config['LOG_PATH'])

db = SQLAlchemy(application)


def populate_g():
    from flask import g
babel = Babel(application)


    g.db = db
    g.app = application
@babel.localeselector
def get_locale():
    # if a user is logged in, use the locale from the user settings
    # user = getattr(g, 'user', None)
    # if user is not None:
    #     return user.locale
    # otherwise try to guess the language from the user accept
    # header the browser transmits.  We support de/fr/en in this
    # example.  The best match wins.
    return request.accept_languages.best_match(["fr", "en"])

M newspipe/conf.py => newspipe/conf.py +1 -1
@@ 10,7 10,7 @@ import logging

BASE_DIR = os.path.abspath(os.path.dirname(__file__))
PATH = os.path.abspath(".")
API_ROOT = "/api/v2.0"


# available languages
LANGUAGES = {"en": "English", "fr": "French"}

D newspipe/conf/conf.cfg-sample => newspipe/conf/conf.cfg-sample +0 -31
@@ 1,31 0,0 @@
[webserver]
host = 0.0.0.0
port = 5000
secret_key = a secret only you know
debug = true
[cdn]
cdn_address = https://cdn.cedricbonhomme.org/
[misc]
platform_url = http://127.0.0.1:5000/
admin_email =
security_password_salt = a secret to confirm user account
token_validity_period = 3600
log_path = ./var/log/newspipe.log
log_level = info
[database]
database_url = sqlite:///newspipe.db
[crawler]
crawling_method = default
default_max_error = 6
user_agent = Newspipe (https://git.sr.ht/~cedric/newspipe)
timeout = 30
resolv = false
feed_refresh_interval = 120
[notification]
notification_email = Newspipe@no-reply.com
host = smtp.googlemail.com
port = 465
tls = false
ssl = true
username = your-gmail-username
password = your-gmail-password

R newspipe/web/controllers/__init__.py => newspipe/controllers/__init__.py +0 -0
R newspipe/web/controllers/abstract.py => newspipe/controllers/abstract.py +1 -1
@@ 1,6 1,6 @@
import logging
import dateutil.parser
from bootstrap import db
from newspipe.bootstrap import db
from datetime import datetime
from collections import defaultdict
from sqlalchemy import or_, func

R newspipe/web/controllers/article.py => newspipe/controllers/article.py +4 -4
@@ 4,11 4,11 @@ import sqlalchemy
from sqlalchemy import func
from collections import Counter

from bootstrap import db
from newspipe.bootstrap import db
from .abstract import AbstractController
from lib.article_utils import process_filters
from web.controllers import CategoryController, FeedController
from web.models import Article
from newspipe.lib.article_utils import process_filters
from newspipe.controllers import CategoryController, FeedController
from newspipe.models import Article

logger = logging.getLogger(__name__)


R newspipe/web/controllers/bookmark.py => newspipe/controllers/bookmark.py +2 -2
@@ 2,8 2,8 @@ import logging
import itertools
from datetime import datetime, timedelta

from bootstrap import db
from web.models import Bookmark
from newspipe.bootstrap import db
from newspipe.models import Bookmark
from .abstract import AbstractController
from .tag import BookmarkTagController


R newspipe/web/controllers/category.py => newspipe/controllers/category.py +1 -1
@@ 1,5 1,5 @@
from .abstract import AbstractController
from web.models import Category
from newspipe.models import Category
from .feed import FeedController



R newspipe/web/controllers/feed.py => newspipe/controllers/feed.py +4 -4
@@ 2,15 2,15 @@ import logging
import itertools
from datetime import datetime, timedelta

import conf
from newspipe.bootstrap import application
from .abstract import AbstractController
from .icon import IconController
from web.models import User, Feed
from lib.utils import clear_string
from newspipe.models import User, Feed
from newspipe.lib.utils import clear_string

logger = logging.getLogger(__name__)
DEFAULT_LIMIT = 5
DEFAULT_MAX_ERROR = conf.DEFAULT_MAX_ERROR
DEFAULT_MAX_ERROR = application.config['DEFAULT_MAX_ERROR']


class FeedController(AbstractController):

R newspipe/web/controllers/icon.py => newspipe/controllers/icon.py +1 -1
@@ 1,6 1,6 @@
import base64
import requests
from web.models import Icon
from newspipe.models import Icon
from .abstract import AbstractController



R newspipe/web/controllers/tag.py => newspipe/controllers/tag.py +2 -2
@@ 2,9 2,9 @@ import logging
import itertools
from datetime import datetime, timedelta

from bootstrap import db
from newspipe.bootstrap import db
from .abstract import AbstractController
from web.models.tag import BookmarkTag
from newspipe.models.tag import BookmarkTag

logger = logging.getLogger(__name__)


R newspipe/web/controllers/user.py => newspipe/controllers/user.py +1 -1
@@ 1,7 1,7 @@
import logging
from werkzeug.security import generate_password_hash, check_password_hash
from .abstract import AbstractController
from web.models import User
from newspipe.models import User

logger = logging.getLogger(__name__)


M newspipe/crawler/default_crawler.py => newspipe/crawler/default_crawler.py +12 -12
@@ 1,8 1,8 @@
#! /usr/bin/env python
# -*- coding: utf-8 -

# newspipe - A Web based news aggregator.
# Copyright (C) 2010-2019  Cédric Bonhomme - https://www.cedricbonhomme.org
# Newspipe - A Web based news aggregator.
# Copyright (C) 2010-2020 Cédric Bonhomme - https://www.cedricbonhomme.org
#
# For more information: https://git.sr.ht/~cedric/newspipe
#


@@ 34,13 34,13 @@ import dateutil.parser
from datetime import datetime, timezone, timedelta
from sqlalchemy import or_

import conf
from bootstrap import db
from web.models import User
from web.controllers import FeedController, ArticleController
from lib.utils import jarr_get
from lib.feed_utils import construct_feed_from, is_parsing_ok
from lib.article_utils import construct_article, extract_id, get_article_content
from newspipe.bootstrap import application
from newspipe.bootstrap import db
from newspipe.models import User
from newspipe.controllers import FeedController, ArticleController
from newspipe.lib.utils import newspipe_get
from newspipe.lib.feed_utils import construct_feed_from, is_parsing_ok
from newspipe.lib.article_utils import construct_article, extract_id, get_article_content

logger = logging.getLogger(__name__)



@@ 59,7 59,7 @@ async def parse_feed(user, feed):
    # with (await sem):
    try:
        logger.info("Retrieving feed {}".format(feed.link))
        resp = await jarr_get(feed.link, timeout=5)
        resp = await newspipe_get(feed.link, timeout=5)
    except Exception as e:
        logger.info("Problem when reading feed {}".format(feed.link))
        return


@@ 163,9 163,9 @@ async def retrieve_feed(queue, users, feed_id=None):
        if feed_id is not None:
            filters["id"] = feed_id
        filters["enabled"] = True
        filters["error_count__lt"] = conf.DEFAULT_MAX_ERROR
        filters["error_count__lt"] = application.config['DEFAULT_MAX_ERROR']
        filters["last_retrieved__lt"] = datetime.now() - timedelta(
            minutes=conf.FEED_REFRESH_INTERVAL
            minutes=application.config['FEED_REFRESH_INTERVAL']
        )
        feeds = FeedController().read(**filters).all()


M newspipe/lib/article_utils.py => newspipe/lib/article_utils.py +5 -5
@@ 9,8 9,8 @@ import dateutil.parser
from bs4 import BeautifulSoup, SoupStrainer
from requests.exceptions import MissingSchema

import conf
from lib.utils import jarr_get
from newspipe.bootstrap import application
from newspipe.lib.utils import newspipe_get

logger = logging.getLogger(__name__)
PROCESSED_DATE_KEYS = {"published", "created", "updated"}


@@ 77,16 77,16 @@ def get_article_content(entry):
async def get_article_details(entry, fetch=True):
    article_link = entry.get("link")
    article_title = html.unescape(entry.get("title", ""))
    if fetch and conf.CRAWLER_RESOLV and article_link or not article_title:
    if fetch and application.config['CRAWLER_RESOLV'] and article_link or not article_title:
        try:
            # resolves URL behind proxies (like feedproxy.google.com)
            response = await jarr_get(article_link, timeout=5)
            response = await newspipe_get(article_link, timeout=5)
        except MissingSchema:
            split, failed = urlsplit(article_link), False
            for scheme in "https", "http":
                new_link = urlunsplit(SplitResult(scheme, *split[1:]))
                try:
                    response = await jarr_get(new_link, timeout=5)
                    response = await newspipe_get(new_link, timeout=5)
                except Exception as error:
                    failed = True
                    continue

M newspipe/lib/data.py => newspipe/lib/data.py +4 -4
@@ 35,10 35,10 @@ import opml
import datetime
from flask import jsonify

from bootstrap import db
from web.models import User, Feed, Article
from web.models.tag import BookmarkTag
from web.controllers import BookmarkController, BookmarkTagController
from newspipe.bootstrap import db
from newspipe.models import User, Feed, Article
from newspipe.models.tag import BookmarkTag
from newspipe.controllers import BookmarkController, BookmarkTagController


def import_opml(nickname, opml_content):

M newspipe/lib/feed_utils.py => newspipe/lib/feed_utils.py +3 -3
@@ 3,10 3,10 @@ import urllib
import logging
import requests
import feedparser
from conf import CRAWLER_USER_AGENT
from bs4 import BeautifulSoup, SoupStrainer

from lib.utils import try_keys, try_get_icon_url, rebuild_url
from newspipe.bootstrap import application
from newspipe.lib.utils import try_keys, try_get_icon_url, rebuild_url

logger = logging.getLogger(__name__)
logging.captureWarnings(True)


@@ 39,7 39,7 @@ def escape_keys(*keys):

@escape_keys("title", "description")
def construct_feed_from(url=None, fp_parsed=None, feed=None, query_site=True):
    requests_kwargs = {"headers": {"User-Agent": CRAWLER_USER_AGENT}, "verify": False}
    requests_kwargs = {"headers": {"User-Agent": application.config['CRAWLER_USER_AGENT']}, "verify": False}
    if url is None and fp_parsed is not None:
        url = fp_parsed.get("url")
    if url is not None and fp_parsed is None:

M newspipe/lib/misc_utils.py => newspipe/lib/misc_utils.py +5 -5
@@ 45,9 45,9 @@ from collections import Counter
from contextlib import contextmanager
from flask import request

import conf
from web.controllers import ArticleController
from lib.utils import clear_string
from newspipe.bootstrap import application
from newspipe.controllers import ArticleController
from newspipe.lib.utils import clear_string

logger = logging.getLogger(__name__)



@@ 101,7 101,7 @@ def fetch(id, feed_id=None):
    """
    cmd = [
        sys.executable,
        conf.BASE_DIR + "/manager.py",
        application.config['BASE_DIR'] + "/manager.py",
        "fetch_asyncio",
        "--user_id=" + str(id),
    ]


@@ 154,7 154,7 @@ def load_stop_words():
    Load the stop words and return them in a list.
    """
    stop_words_lists = glob.glob(
        os.path.join(conf.BASE_DIR, "web/var/stop_words/*.txt")
        os.path.join(application.config['BASE_DIR'], "web/var/stop_words/*.txt")
    )
    stop_words = []


M newspipe/lib/utils.py => newspipe/lib/utils.py +5 -5
@@ 6,7 6,7 @@ import requests
from hashlib import md5
from flask import request, url_for

import conf
from newspipe.bootstrap import application

logger = logging.getLogger(__name__)



@@ 56,7 56,7 @@ def try_get_icon_url(url, *splits):
        response = None
        # if html in content-type, we assume it's a fancy 404 page
        try:
            response = jarr_get(rb_url)
            response = newspipe_get(rb_url)
            content_type = response.headers.get("content-type", "")
        except Exception:
            pass


@@ 89,12 89,12 @@ def redirect_url(default="home"):
    return request.args.get("next") or request.referrer or url_for(default)


async def jarr_get(url, **kwargs):
async def newspipe_get(url, **kwargs):
    request_kwargs = {
        "verify": False,
        "allow_redirects": True,
        "timeout": conf.CRAWLER_TIMEOUT,
        "headers": {"User-Agent": conf.CRAWLER_USER_AGENT},
        "timeout": application.config['CRAWLER_TIMEOUT'],
        "headers": {"User-Agent": application.config['CRAWLER_USER_AGENT']},
    }
    request_kwargs.update(kwargs)
    return requests.get(url, **request_kwargs)

R newspipe/web/models/__init__.py => newspipe/models/__init__.py +0 -0
R newspipe/web/models/article.py => newspipe/models/article.py +2 -2
@@ 26,12 26,12 @@ __revision__ = "$Date: 2016/10/04 $"
__copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "GPLv3"

from bootstrap import db
from newspipe.bootstrap import db
from datetime import datetime
from sqlalchemy import Index
from sqlalchemy.ext.associationproxy import association_proxy

from web.models.right_mixin import RightMixin
from newspipe.models.right_mixin import RightMixin


class Article(db.Model, RightMixin):

R newspipe/web/models/bookmark.py => newspipe/models/bookmark.py +3 -3
@@ 26,14 26,14 @@ __revision__ = "$Date: 2016/12/07 $"
__copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "GPLv3"

from bootstrap import db
from newspipe.bootstrap import db
from datetime import datetime
from sqlalchemy import desc
from sqlalchemy.orm import validates
from sqlalchemy.ext.associationproxy import association_proxy

from web.models.tag import BookmarkTag
from web.models.right_mixin import RightMixin
from newspipe.models.tag import BookmarkTag
from newspipe.models.right_mixin import RightMixin


class Bookmark(db.Model, RightMixin):

R newspipe/web/models/category.py => newspipe/models/category.py +2 -2
@@ 1,9 1,9 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from bootstrap import db
from newspipe.bootstrap import db
from sqlalchemy import Index
from web.models.right_mixin import RightMixin
from newspipe.models.right_mixin import RightMixin


class Category(db.Model, RightMixin):

R newspipe/web/models/feed.py => newspipe/models/feed.py +3 -3
@@ 26,12 26,12 @@ __revision__ = "$Date: 2014/04/12 $"
__copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "GPLv3"

from bootstrap import db
from newspipe.bootstrap import db
from datetime import datetime
from sqlalchemy import desc, Index
from sqlalchemy.orm import validates
from web.models.right_mixin import RightMixin
from web.models.article import Article
from newspipe.models.right_mixin import RightMixin
from newspipe.models.article import Article


class Feed(db.Model, RightMixin):

R newspipe/web/models/icon.py => newspipe/models/icon.py +1 -1
@@ 1,7 1,7 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from bootstrap import db
from newspipe.bootstrap import db


class Icon(db.Model):

R newspipe/web/models/right_mixin.py => newspipe/models/right_mixin.py +0 -0
R newspipe/web/models/role.py => newspipe/models/role.py +1 -1
@@ 26,7 26,7 @@ __revision__ = "$Date: 2014/04/12 $"
__copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "GPLv3"

from bootstrap import db
from newspipe.bootstrap import db


class Role(db.Model):

R newspipe/web/models/tag.py => newspipe/models/tag.py +1 -1
@@ 1,7 1,7 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from bootstrap import db
from newspipe.bootstrap import db


class ArticleTag(db.Model):

R newspipe/web/models/user.py => newspipe/models/user.py +4 -4
@@ 34,10 34,10 @@ from werkzeug.security import check_password_hash
from flask_login import UserMixin
from sqlalchemy.orm import validates

from bootstrap import db
from web.models.right_mixin import RightMixin
from web.models.category import Category
from web.models.feed import Feed
from newspipe.bootstrap import db
from newspipe.models.right_mixin import RightMixin
from newspipe.models.category import Category
from newspipe.models.feed import Feed


class User(db.Model, UserMixin, RightMixin):

M newspipe/notifications/emails.py => newspipe/notifications/emails.py +8 -8
@@ 24,8 24,8 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

import conf
from web.decorators import async_maker
from newspipe.bootstrap import application
from newspipe.web.decorators import async_maker

logger = logging.getLogger(__name__)



@@ 33,8 33,8 @@ logger = logging.getLogger(__name__)
@async_maker
def send_async_email(mfrom, mto, msg):
    try:
        s = smtplib.SMTP(conf.NOTIFICATION_HOST)
        s.login(conf.NOTIFICATION_USERNAME, conf.NOTIFICATION_PASSWORD)
        s = smtplib.SMTP(application.config['NOTIFICATION_HOST'])
        s.login(application.config['NOTIFICATION_USERNAME'], application.config['NOTIFICATION_PASSWORD'])
    except Exception:
        logger.exception("send_async_email raised:")
    else:


@@ 56,7 56,7 @@ def send_smtp(to="", bcc="", subject="", plaintext="", html=""):
    # Create message container - the correct MIME type is multipart/alternative.
    msg = MIMEMultipart("alternative")
    msg["Subject"] = subject
    msg["From"] = conf.NOTIFICATION_EMAIL
    msg["From"] = application.config['NOTIFICATION_EMAIL']
    msg["To"] = to
    msg["BCC"] = bcc



@@ 71,12 71,12 @@ def send_smtp(to="", bcc="", subject="", plaintext="", html=""):
    msg.attach(part2)

    try:
        s = smtplib.SMTP(conf.NOTIFICATION_HOST)
        s.login(conf.NOTIFICATION_USERNAME, conf.NOTIFICATION_PASSWORD)
        s = smtplib.SMTP(application.config['NOTIFICATION_HOST'])
        s.login(application.config['NOTIFICATION_USERNAME'], application.config['NOTIFICATION_PASSWORD'])
    except Exception:
        logger.exception("send_smtp raised:")
    else:
        s.sendmail(
            conf.NOTIFICATION_EMAIL, msg["To"] + ", " + msg["BCC"], msg.as_string()
            application.config['NOTIFICATION_EMAIL'], msg["To"] + ", " + msg["BCC"], msg.as_string()
        )
        s.quit()

M newspipe/notifications/notifications.py => newspipe/notifications/notifications.py +8 -7
@@ 21,9 21,10 @@

import datetime
from flask import render_template
import conf
from notifications import emails
from web.lib.user_utils import generate_confirmation_token

from newspipe.bootstrap import application
from newspipe.notifications import emails
from newspipe.web.lib.user_utils import generate_confirmation_token


def new_account_notification(user, email):


@@ 32,20 33,20 @@ def new_account_notification(user, email):
    """
    token = generate_confirmation_token(user.nickname)
    expire_time = datetime.datetime.now() + datetime.timedelta(
        seconds=conf.TOKEN_VALIDITY_PERIOD
        seconds=application.config['TOKEN_VALIDITY_PERIOD']
    )

    plaintext = render_template(
        "emails/account_activation.txt",
        user=user,
        platform_url=conf.PLATFORM_URL,
        platform_url=application.config['PLATFORM_URL'],
        token=token,
        expire_time=expire_time,
    )

    emails.send(
        to=email,
        bcc=conf.NOTIFICATION_EMAIL,
        bcc=application.config['NOTIFICATION_EMAIL'],
        subject="[Newspipe] Account creation",
        plaintext=plaintext,
    )


@@ 58,7 59,7 @@ def new_password_notification(user, password):
    plaintext = render_template("emails/new_password.txt", user=user, password=password)
    emails.send(
        to=user.email,
        bcc=conf.NOTIFICATION_EMAIL,
        bcc=application.config['NOTIFICATION_EMAIL'],
        subject="[Newspipe] New password",
        plaintext=plaintext,
    )

R newspipe/web/static/css/custom.css => newspipe/static/css/custom.css +0 -0
R newspipe/web/static/img/favicon.ico => newspipe/static/img/favicon.ico +0 -0
R newspipe/web/static/img/newspipe.png => newspipe/static/img/newspipe.png +0 -0
R newspipe/web/static/img/newspipe.svg => newspipe/static/img/newspipe.svg +0 -0
R newspipe/web/static/img/pinboard.png => newspipe/static/img/pinboard.png +0 -0
R newspipe/web/static/img/reddit.png => newspipe/static/img/reddit.png +0 -0
R newspipe/web/static/img/twitter.png => newspipe/static/img/twitter.png +0 -0
R newspipe/web/static/js/articles.js => newspipe/static/js/articles.js +0 -0
R newspipe/web/static/js/feed.js => newspipe/static/js/feed.js +0 -0
R newspipe/web/templates/about.html => newspipe/templates/about.html +0 -0
R newspipe/web/templates/about_more.html => newspipe/templates/about_more.html +0 -0
R newspipe/web/templates/admin/create_user.html => newspipe/templates/admin/create_user.html +0 -0
R newspipe/web/templates/admin/dashboard.html => newspipe/templates/admin/dashboard.html +0 -0
R newspipe/web/templates/article.html => newspipe/templates/article.html +0 -0
R newspipe/web/templates/article_pub.html => newspipe/templates/article_pub.html +0 -0
R newspipe/web/templates/bookmarks.html => newspipe/templates/bookmarks.html +0 -0
R newspipe/web/templates/categories.html => newspipe/templates/categories.html +0 -0
R newspipe/web/templates/duplicates.html => newspipe/templates/duplicates.html +0 -0
R newspipe/web/templates/edit_bookmark.html => newspipe/templates/edit_bookmark.html +0 -0
R newspipe/web/templates/edit_category.html => newspipe/templates/edit_category.html +0 -0
R newspipe/web/templates/edit_feed.html => newspipe/templates/edit_feed.html +0 -0
R newspipe/web/templates/emails/account_activation.txt => newspipe/templates/emails/account_activation.txt +0 -0
R newspipe/web/templates/emails/new_password.txt => newspipe/templates/emails/new_password.txt +0 -0
R newspipe/web/templates/errors/404.html => newspipe/templates/errors/404.html +0 -0
R newspipe/web/templates/errors/500.html => newspipe/templates/errors/500.html +0 -0
R newspipe/web/templates/feed.html => newspipe/templates/feed.html +0 -0
R newspipe/web/templates/feed_list.html => newspipe/templates/feed_list.html +1 -1
@@ 21,7 21,7 @@
                    {% else %}
                        <i class="fa fa-eye-slash" aria-hidden="true" title="{{ _('Feed disabled') }}"></i>
                    {% endif %}
                    {% if feed.error_count >= conf.DEFAULT_MAX_ERROR %}
                    {% if feed.error_count >= application.config['DEFAULT_MAX_ERROR'] %}
                        <i class="fa fa-exclamation" aria-hidden="true" title="{{ _('Feed encountered too much errors.') }}"></i>
                    {% endif %}
                </td>

R newspipe/web/templates/feed_list_per_categories.html => newspipe/templates/feed_list_per_categories.html +0 -0
R newspipe/web/templates/feed_list_simple.html => newspipe/templates/feed_list_simple.html +0 -0
R newspipe/web/templates/feeds.html => newspipe/templates/feeds.html +0 -0
R newspipe/web/templates/history.html => newspipe/templates/history.html +0 -0
R newspipe/web/templates/home.html => newspipe/templates/home.html +0 -0
R newspipe/web/templates/inactives.html => newspipe/templates/inactives.html +0 -0
R newspipe/web/templates/layout.html => newspipe/templates/layout.html +0 -0
R newspipe/web/templates/login.html => newspipe/templates/login.html +0 -0
R newspipe/web/templates/management.html => newspipe/templates/management.html +0 -0
R newspipe/web/templates/opml.xml => newspipe/templates/opml.xml +0 -0
R newspipe/web/templates/popular.html => newspipe/templates/popular.html +0 -0
R newspipe/web/templates/profile.html => newspipe/templates/profile.html +0 -0
R newspipe/web/templates/profile_public.html => newspipe/templates/profile_public.html +0 -0
R newspipe/web/templates/signup.html => newspipe/templates/signup.html +0 -0
R newspipe/web/templates/user_stream.html => newspipe/templates/user_stream.html +0 -0
M newspipe/web/forms.py => newspipe/web/forms.py +3 -3
@@ 43,9 43,9 @@ from wtforms import (
)
from wtforms.fields.html5 import EmailField, URLField

from lib import misc_utils
from web.controllers import UserController
from web.models import User
from newspipe.lib import misc_utils
from newspipe.controllers import UserController
from newspipe.models import User


class SignupForm(FlaskForm):

M newspipe/web/lib/user_utils.py => newspipe/web/lib/user_utils.py +3 -3
@@ 1,6 1,6 @@
from itsdangerous import URLSafeTimedSerializer
import conf
from bootstrap import application

from newspipe.bootstrap import application


def generate_confirmation_token(nickname):


@@ 14,7 14,7 @@ def confirm_token(token):
        nickname = serializer.loads(
            token,
            salt=application.config["SECURITY_PASSWORD_SALT"],
            max_age=conf.TOKEN_VALIDITY_PERIOD,
            max_age=application.config['TOKEN_VALIDITY_PERIOD'],
        )
    except:
        return False

M newspipe/web/lib/view_utils.py => newspipe/web/lib/view_utils.py +1 -1
@@ 1,6 1,6 @@
from functools import wraps
from flask import request, Response, make_response
from lib.utils import to_hash
from newspipe.lib.utils import to_hash


def etag_match(func):

M newspipe/web/views/__init__.py => newspipe/web/views/__init__.py +9 -37
@@ 1,37 1,9 @@
from web.views.api import v2
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",
    "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())
from newspipe.web.views.api import v2
from newspipe.web.views import views, home, session_mgmt
from newspipe.web.views.article import article_bp, articles_bp
from newspipe.web.views.feed import feed_bp, feeds_bp
from newspipe.web.views.category import category_bp, categories_bp
from newspipe.web.views.icon import icon_bp
from newspipe.web.views.admin import admin_bp
from newspipe.web.views.user import user_bp, users_bp
from newspipe.web.views.bookmark import bookmark_bp, bookmarks_bp

M newspipe/web/views/admin.py => newspipe/web/views/admin.py +4 -4
@@ 3,10 3,10 @@ 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
from newspipe.lib.utils import redirect_url
from newspipe.controllers import UserController
from newspipe.web.views.common import admin_permission
from newspipe.web.forms import InformationMessageForm, UserForm

admin_bp = Blueprint("admin", __name__, url_prefix="/admin")


M newspipe/web/views/api/v2/__init__.py => newspipe/web/views/api/v2/__init__.py +1 -1
@@ 1,3 1,3 @@
from web.views.api.v2 import article, feed, category
from newspipe.web.views.api.v2 import article, feed, category

__all__ = ["article", "feed", "category"]

M newspipe/web/views/api/v2/article.py => newspipe/web/views/api/v2/article.py +5 -5
@@ 1,12 1,12 @@
from conf import API_ROOT
from newspipe.bootstrap import application
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 (
from newspipe.web.views.common import api_permission
from newspipe.controllers import ArticleController
from newspipe.web.views.api.v2.common import (
    PyAggAbstractResource,
    PyAggResourceNew,
    PyAggResourceExisting,


@@ 49,7 49,7 @@ class ArticlesChallenge(PyAggAbstractResource):
        return result or None, 200 if result else 204


api = Api(current_app, prefix=API_ROOT)
api = Api(current_app, prefix=application.config['API_ROOT'])

api.add_resource(ArticleNewAPI, "/article", endpoint="article_new.json")
api.add_resource(ArticleAPI, "/article/<int:obj_id>", endpoint="article.json")

M newspipe/web/views/api/v2/category.py => newspipe/web/views/api/v2/category.py +4 -4
@@ 1,9 1,9 @@
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 (
from newspipe.bootstrap import application
from newspipe.controllers.category import CategoryController
from newspipe.web.views.api.v2.common import (
    PyAggResourceNew,
    PyAggResourceExisting,
    PyAggResourceMulti,


@@ 22,7 22,7 @@ class CategoriesAPI(PyAggResourceMulti):
    controller_cls = CategoryController


api = Api(current_app, prefix=API_ROOT)
api = Api(current_app, prefix=application.config['API_ROOT'])
api.add_resource(CategoryNewAPI, "/category", endpoint="category_new.json")
api.add_resource(CategoryAPI, "/category/<int:obj_id>", endpoint="category.json")
api.add_resource(CategoriesAPI, "/categories", endpoint="categories.json")

M newspipe/web/views/api/v2/common.py => newspipe/web/views/api/v2/common.py +2 -2
@@ 26,13 26,13 @@ from flask import request
from flask_restful import Resource, reqparse
from flask_login import current_user

from web.views.common import (
from newspipe.web.views.common import (
    admin_permission,
    api_permission,
    login_user_bundle,
    jsonify,
)
from web.controllers import UserController
from newspipe.controllers import UserController

logger = logging.getLogger(__name__)


M newspipe/web/views/api/v2/feed.py => newspipe/web/views/api/v2/feed.py +5 -5
@@ 1,11 1,11 @@
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 newspipe.bootstrap import application
from newspipe.web.views.common import api_permission
from newspipe.controllers.feed import FeedController, DEFAULT_MAX_ERROR, DEFAULT_LIMIT

from web.views.api.v2.common import (
from newspipe.web.views.api.v2.common import (
    PyAggAbstractResource,
    PyAggResourceNew,
    PyAggResourceExisting,


@@ 39,7 39,7 @@ class FetchableFeedAPI(PyAggAbstractResource):
        return result or None, 200 if result else 204


api = Api(current_app, prefix=API_ROOT)
api = Api(current_app, prefix=application.config['API_ROOT'])

api.add_resource(FeedNewAPI, "/feed", endpoint="feed_new.json")
api.add_resource(FeedAPI, "/feed/<int:obj_id>", endpoint="feed.json")

M newspipe/web/views/article.py => newspipe/web/views/article.py +5 -5
@@ 14,11 14,11 @@ 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
from newspipe.bootstrap import db
from newspipe.lib.utils import clear_string, redirect_url
from newspipe.lib.data import export_json
from newspipe.controllers import ArticleController, UserController, CategoryController
from newspipe.web.lib.view_utils import etag_match

articles_bp = Blueprint("articles", __name__, url_prefix="/articles")
article_bp = Blueprint("article", __name__, url_prefix="/article")

M newspipe/web/views/bookmark.py => newspipe/web/views/bookmark.py +6 -7
@@ 44,13 44,12 @@ 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
from newspipe.lib.utils import redirect_url
from newspipe.lib.data import import_pinboard_json, export_bookmarks
from newspipe.bootstrap import db
from newspipe.web.forms import BookmarkForm
from newspipe.controllers import BookmarkController, BookmarkTagController
from newspipe.models import BookmarkTag

logger = logging.getLogger(__name__)
bookmarks_bp = Blueprint("bookmarks", __name__, url_prefix="/bookmarks")

M newspipe/web/views/category.py => newspipe/web/views/category.py +4 -4
@@ 2,10 2,10 @@ 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
from newspipe.web.forms import CategoryForm
from newspipe.lib.utils import redirect_url
from newspipe.web.lib.view_utils import etag_match
from newspipe.controllers import ArticleController, FeedController, CategoryController

categories_bp = Blueprint("categories", __name__, url_prefix="/categories")
category_bp = Blueprint("category", __name__, url_prefix="/category")

M newspipe/web/views/common.py => newspipe/web/views/common.py +2 -2
@@ 10,8 10,8 @@ from flask_principal import (
    session_identity_loader,
    identity_changed,
)
from web.controllers import UserController
from lib.utils import default_handler
from newspipe.controllers import UserController
from newspipe.lib.utils import default_handler

admin_role = RoleNeed("admin")
api_role = RoleNeed("api")

M newspipe/web/views/feed.py => newspipe/web/views/feed.py +9 -9
@@ 17,12 17,12 @@ 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 (
from newspipe.bootstrap import application
from newspipe.lib import misc_utils, utils
from newspipe.lib.feed_utils import construct_feed_from
from newspipe.web.lib.view_utils import etag_match
from newspipe.web.forms import AddFeedForm
from newspipe.controllers import (
    UserController,
    CategoryController,
    FeedController,


@@ 179,7 179,7 @@ def bookmarklet():
        )
    feed = feed_contr.create(**feed)
    flash(gettext("Feed was successfully created."), "success")
    if feed.enabled and conf.CRAWLING_METHOD == "default":
    if feed.enabled and application.confg['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))


@@ 286,7 286,7 @@ def process_form(feed_id=None):
        "success",
    )

    if conf.CRAWLING_METHOD == "default":
    if application.confg['CRAWLING_METHOD'] == "default":
        misc_utils.fetch(current_user.id, new_feed.id)
        flash(gettext("Downloading articles for the new feed..."), "info")



@@ 335,7 335,7 @@ def export():
    if not include_private:
        filter["private"] = False
    if not include_exceeded_error_count:
        filter["error_count__lt"] = conf.DEFAULT_MAX_ERROR
        filter["error_count__lt"] = application.confg['DEFAULT_MAX_ERROR']

    user = UserController(current_user.id).get(id=current_user.id)
    feeds = FeedController(current_user.id).read(**filter)

M newspipe/web/views/home.py => newspipe/web/views/home.py +7 -7
@@ 7,13 7,13 @@ 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 newspipe.bootstrap import application
from newspipe.lib.utils import redirect_url
from newspipe.lib import misc_utils
from newspipe.web.lib.view_utils import etag_match
from newspipe.web.views.common import jsonify

from web.controllers import FeedController, ArticleController, CategoryController
from newspipe.controllers import FeedController, ArticleController, CategoryController

localize = pytz.utc.localize
logger = logging.getLogger(__name__)


@@ 181,7 181,7 @@ def fetch(feed_id=None):
    Triggers the download of news.
    News are downloaded in a separated process.
    """
    if conf.CRAWLING_METHOD == "default" and current_user.is_admin:
    if application.config['CRAWLING_METHOD'] == "default" and current_user.is_admin:
        misc_utils.fetch(current_user.id, feed_id)
        flash(gettext("Downloading articles..."), "info")
    else:

M newspipe/web/views/icon.py => newspipe/web/views/icon.py +3 -2
@@ 1,7 1,8 @@
import base64
from flask import Blueprint, Response, request
from web.controllers import IconController
from web.lib.view_utils import etag_match

from newspipe.controllers import IconController
from newspipe.web.lib.view_utils import etag_match

icon_bp = Blueprint("icon", __name__, url_prefix="/icon")


M newspipe/web/views/session_mgmt.py => newspipe/web/views/session_mgmt.py +6 -6
@@ 24,11 24,11 @@ from flask_principal import (
    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
from newspipe.bootstrap import application
from newspipe.web.views.common import admin_role, api_role, login_user_bundle
from newspipe.controllers import UserController
from newspipe.web.forms import SignupForm, SigninForm
from newspipe.notifications import notifications

Principal(current_app)
# Create a permission with a single Need, in this case a RoleNeed.


@@ 99,7 99,7 @@ def logout():

@current_app.route("/signup", methods=["GET", "POST"])
def signup():
    if not conf.SELF_REGISTRATION:
    if not application.config['SELF_REGISTRATION']:
        flash(gettext("Self-registration is disabled."), "warning")
        return redirect(url_for("home"))
    if current_user.is_authenticated:

M newspipe/web/views/user.py => newspipe/web/views/user.py +8 -8
@@ 6,12 6,12 @@ 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 (
from newspipe.bootstrap import application
from newspipe.notifications import notifications
from newspipe.lib import misc_utils
from newspipe.lib.data import import_opml, import_json
from newspipe.web.lib.user_utils import confirm_token
from newspipe.controllers import (
    UserController,
    FeedController,
    ArticleController,


@@ 19,7 19,7 @@ from web.controllers import (
    BookmarkController,
)

from web.forms import ProfileForm
from newspipe.web.forms import ProfileForm

users_bp = Blueprint("users", __name__, url_prefix="/users")
user_bp = Blueprint("user", __name__, url_prefix="/user")


@@ 115,7 115,7 @@ def management():
            else:
                try:
                    nb = import_opml(current_user.nickname, data.read())
                    if conf.CRAWLING_METHOD == "classic":
                    if application.config['CRAWLING_METHOD'] == "classic":
                        misc_utils.fetch(current_user.id, None)
                        flash(str(nb) + "  " + gettext("feeds imported."), "success")
                        flash(gettext("Downloading articles..."), "info")

M newspipe/web/views/views.py => newspipe/web/views/views.py +8 -9
@@ 6,11 6,10 @@ from flask import request, render_template, flash, url_for, redirect, current_ap
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
from newspipe.bootstrap import application
from newspipe.web import __version__
from newspipe.controllers import FeedController, UserController
from newspipe.web.lib.view_utils import etag_match

logger = logging.getLogger(__name__)



@@ 25,7 24,7 @@ def authentication_required(error):

@current_app.errorhandler(403)
def authentication_failed(error):
    if API_ROOT in request.url:
    if application.conf['API_ROOT'] in request.url:
        return error
    flash(gettext("Forbidden."), "danger")
    return redirect(url_for("login"))


@@ 71,7 70,7 @@ def popular():
    filters = {}
    filters["created_date__gt"] = not_added_before
    filters["private"] = False
    filters["error_count__lt"] = conf.DEFAULT_MAX_ERROR
    filters["error_count__lt"] = application.config['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)


@@ 80,7 79,7 @@ def popular():
@current_app.route("/about", methods=["GET"])
@etag_match
def about():
    return render_template("about.html", contact=ADMIN_EMAIL)
    return render_template("about.html", contact=application.config['ADMIN_EMAIL'])


@current_app.route("/about/more", methods=["GET"])


@@ 102,7 101,7 @@ def about_more():
        "about_more.html",
        newspipe_version=newspipe_version,
        version_url=version_url,
        registration=[conf.SELF_REGISTRATION and "Open" or "Closed"][0],
        registration=[application.config['SELF_REGISTRATION'] and "Open" or "Closed"][0],
        python_version="{}.{}.{}".format(*sys.version_info[:3]),
        nb_users=UserController().read().count(),
    )

M package-lock.json => package-lock.json +3 -3
@@ 14,7 14,7 @@
      "resolved": "https://registry.npmjs.org/datatables/-/datatables-1.10.18.tgz",
      "integrity": "sha512-ntatMgS9NN6UMpwbmO+QkYJuKlVeMA2Mi0Gu/QxyIh+dW7ZjLSDhPT2tWlzjpIWEkDYgieDzS9Nu7bdQCW0sbQ==",
      "requires": {
        "jquery": ">=1.7"
        "jquery": "3.4.1"
      }
    },
    "datatables.net": {


@@ 22,7 22,7 @@
      "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.20.tgz",
      "integrity": "sha512-4E4S7tTU607N3h0fZPkGmAtr9mwy462u+VJ6gxYZ8MxcRIjZqHy3Dv1GNry7i3zQCktTdWbULVKBbkAJkuHEnQ==",
      "requires": {
        "jquery": ">=1.7"
        "jquery": "3.4.1"
      }
    },
    "datatables.net-bs4": {


@@ 31,7 31,7 @@
      "integrity": "sha512-kQmMUMsHMOlAW96ztdoFqjSbLnlGZQ63iIM82kHbmldsfYdzuyhbb4hTx6YNBi481WCO3iPSvI6YodNec46ZAw==",
      "requires": {
        "datatables.net": "1.10.20",
        "jquery": ">=1.7"
        "jquery": "3.4.1"
      }
    },
    "fork-awesome": {

M package.json => package.json +1 -1
@@ 17,6 17,6 @@
    "yarn": ">= 1.0.0"
  },
  "scripts": {
    "postinstall": "cd newspipe/web/static/ ; ln -sf ../../../node_modules npm_components"
    "postinstall": "cd newspipe/static/ ; ln -sf ../../node_modules npm_components"
  }
}

R newspipe/runserver.py => runserver.py +7 -5
@@ 19,7 19,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import calendar
from bootstrap import conf, application, populate_g
from newspipe.bootstrap import application
from flask_babel import Babel, format_datetime




@@ 33,18 33,18 @@ def month_name(month_number):

application.jinja_env.filters["month_name"] = month_name
application.jinja_env.filters["datetime"] = format_datetime
application.jinja_env.globals["conf"] = conf
# inject application in Jinja env
application.jinja_env.globals["application"] = application

# Views
from flask_restful import Api
from flask import g

with application.app_context():
    populate_g()
    g.api = Api(application, prefix="/api/v2.0")
    g.babel = babel

    from web import views
    from newspipe.web import views

    application.register_blueprint(views.articles_bp)
    application.register_blueprint(views.article_bp)


@@ 62,5 62,7 @@ with application.app_context():

if __name__ == "__main__":
    application.run(
        host=conf.WEBSERVER_HOST, port=conf.WEBSERVER_PORT, debug=conf.WEBSERVER_DEBUG
        host=application.config['HOST'],
        port=application.config['PORT'],
        debug=application.config['DEBUG']
    )