diff options
Diffstat (limited to 'newspipe/web')
42 files changed, 1412 insertions, 997 deletions
diff --git a/newspipe/web/controllers/__init__.py b/newspipe/web/controllers/__init__.py index 5fbc2619..9b193cc5 100644 --- a/newspipe/web/controllers/__init__.py +++ b/newspipe/web/controllers/__init__.py @@ -7,6 +7,12 @@ from .bookmark import BookmarkController from .tag import BookmarkTagController -__all__ = ['FeedController', 'CategoryController', 'ArticleController', - 'UserController', 'IconController', 'BookmarkController', - 'BookmarkTagController'] +__all__ = [ + "FeedController", + "CategoryController", + "ArticleController", + "UserController", + "IconController", + "BookmarkController", + "BookmarkTagController", +] diff --git a/newspipe/web/controllers/abstract.py b/newspipe/web/controllers/abstract.py index 764ff305..9d9e84f2 100644 --- a/newspipe/web/controllers/abstract.py +++ b/newspipe/web/controllers/abstract.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) class AbstractController: _db_cls = None # reference to the database class - _user_id_key = 'user_id' + _user_id_key = "user_id" def __init__(self, user_id=None, ignore_context=False): """User id is a right management mechanism that should be used to @@ -36,25 +36,25 @@ class AbstractController: """ db_filters = set() for key, value in filters.items(): - if key == '__or__': + if key == "__or__": db_filters.add(or_(*self._to_filters(**value))) - elif key.endswith('__gt'): + elif key.endswith("__gt"): db_filters.add(getattr(self._db_cls, key[:-4]) > value) - elif key.endswith('__lt'): + elif key.endswith("__lt"): db_filters.add(getattr(self._db_cls, key[:-4]) < value) - elif key.endswith('__ge'): + elif key.endswith("__ge"): db_filters.add(getattr(self._db_cls, key[:-4]) >= value) - elif key.endswith('__le'): + elif key.endswith("__le"): db_filters.add(getattr(self._db_cls, key[:-4]) <= value) - elif key.endswith('__ne'): + elif key.endswith("__ne"): db_filters.add(getattr(self._db_cls, key[:-4]) != value) - elif key.endswith('__in'): + elif key.endswith("__in"): db_filters.add(getattr(self._db_cls, key[:-4]).in_(value)) - elif key.endswith('__contains'): + elif key.endswith("__contains"): db_filters.add(getattr(self._db_cls, key[:-10]).contains(value)) - elif key.endswith('__like'): + elif key.endswith("__like"): db_filters.add(getattr(self._db_cls, key[:-6]).like(value)) - elif key.endswith('__ilike'): + elif key.endswith("__ilike"): db_filters.add(getattr(self._db_cls, key[:-7]).ilike(value)) else: db_filters.add(getattr(self._db_cls, key) == value) @@ -66,8 +66,11 @@ class AbstractController: dependent) and the user is not an admin and the filters doesn't already contains a filter for that user. """ - if self._user_id_key is not None and self.user_id \ - and filters.get(self._user_id_key) != self.user_id: + if ( + self._user_id_key is not None + and self.user_id + and filters.get(self._user_id_key) != self.user_id + ): filters[self._user_id_key] = self.user_id return self._db_cls.query.filter(*self._to_filters(**filters)) @@ -76,20 +79,27 @@ class AbstractController: obj = self._get(**filters).first() if obj and not self._has_right_on(obj): - raise Forbidden({'message': 'No authorized to access %r (%r)' - % (self._db_cls.__class__.__name__, filters)}) + raise Forbidden( + { + "message": "No authorized to access %r (%r)" + % (self._db_cls.__class__.__name__, filters) + } + ) if not obj: - raise NotFound({'message': 'No %r (%r)' - % (self._db_cls.__class__.__name__, filters)}) + raise NotFound( + {"message": "No %r (%r)" % (self._db_cls.__class__.__name__, filters)} + ) return obj def create(self, **attrs): assert attrs, "attributes to update must not be empty" if self._user_id_key is not None and self._user_id_key not in attrs: attrs[self._user_id_key] = self.user_id - assert self._user_id_key is None or self._user_id_key in attrs \ - or self.user_id is None, \ - "You must provide user_id one way or another" + assert ( + self._user_id_key is None + or self._user_id_key in attrs + or self.user_id is None + ), "You must provide user_id one way or another" obj = self._db_cls(**attrs) db.session.add(obj) @@ -123,39 +133,45 @@ class AbstractController: # user_id == None is like being admin if self._user_id_key is None: return True - return self.user_id is None \ - or getattr(obj, self._user_id_key, None) == self.user_id + return ( + self.user_id is None + or getattr(obj, self._user_id_key, None) == self.user_id + ) def _count_by(self, elem_to_group_by, filters): if self.user_id: - filters['user_id'] = self.user_id - return dict(db.session.query(elem_to_group_by, func.count('id')) - .filter(*self._to_filters(**filters)) - .group_by(elem_to_group_by).all()) + filters["user_id"] = self.user_id + return dict( + db.session.query(elem_to_group_by, func.count("id")) + .filter(*self._to_filters(**filters)) + .group_by(elem_to_group_by) + .all() + ) @classmethod def _get_attrs_desc(cls, role, right=None): result = defaultdict(dict) - if role == 'admin': + if role == "admin": columns = cls._db_cls.__table__.columns.keys() else: - assert role in {'base', 'api'}, 'unknown role %r' % role - assert right in {'read', 'write'}, \ - "right must be 'read' or 'write' with role %r" % role - columns = getattr(cls._db_cls, 'fields_%s_%s' % (role, right))() + assert role in {"base", "api"}, "unknown role %r" % role + assert right in {"read", "write"}, ( + "right must be 'read' or 'write' with role %r" % role + ) + columns = getattr(cls._db_cls, "fields_%s_%s" % (role, right))() for column in columns: result[column] = {} db_col = getattr(cls._db_cls, column).property.columns[0] try: - result[column]['type'] = db_col.type.python_type + result[column]["type"] = db_col.type.python_type except NotImplementedError: if db_col.default: - result[column]['type'] = db_col.default.arg.__class__ + result[column]["type"] = db_col.default.arg.__class__ if column not in result: continue - if issubclass(result[column]['type'], datetime): - result[column]['default'] = datetime.utcnow() - result[column]['type'] = lambda x: dateutil.parser.parse(x) + if issubclass(result[column]["type"], datetime): + result[column]["default"] = datetime.utcnow() + result[column]["type"] = lambda x: dateutil.parser.parse(x) elif db_col.default: - result[column]['default'] = db_col.default.arg + result[column]["default"] = db_col.default.arg return result diff --git a/newspipe/web/controllers/article.py b/newspipe/web/controllers/article.py index d7058229..03342a1f 100644 --- a/newspipe/web/controllers/article.py +++ b/newspipe/web/controllers/article.py @@ -30,19 +30,24 @@ class ArticleController(AbstractController): return self._count_by(Article.feed_id, filters) def count_by_user_id(self, **filters): - return dict(db.session.query(Article.user_id, func.count(Article.id)) - .filter(*self._to_filters(**filters)) - .group_by(Article.user_id).all()) + return dict( + db.session.query(Article.user_id, func.count(Article.id)) + .filter(*self._to_filters(**filters)) + .group_by(Article.user_id) + .all() + ) def create(self, **attrs): # handling special denorm for article rights - assert 'feed_id' in attrs, "must provide feed_id when creating article" - feed = FeedController( - attrs.get('user_id', self.user_id)).get(id=attrs['feed_id']) - if 'user_id' in attrs: - assert feed.user_id == attrs['user_id'] or self.user_id is None, \ - "no right on feed %r" % feed.id - attrs['user_id'], attrs['category_id'] = feed.user_id, feed.category_id + assert "feed_id" in attrs, "must provide feed_id when creating article" + feed = FeedController(attrs.get("user_id", self.user_id)).get( + id=attrs["feed_id"] + ) + if "user_id" in attrs: + assert feed.user_id == attrs["user_id"] or self.user_id is None, ( + "no right on feed %r" % feed.id + ) + attrs["user_id"], attrs["category_id"] = feed.user_id, feed.category_id skipped, read, liked = process_filters(feed.filters, attrs) if skipped: @@ -51,15 +56,16 @@ class ArticleController(AbstractController): return article def update(self, filters, attrs): - user_id = attrs.get('user_id', self.user_id) - if 'feed_id' in attrs: - feed = FeedController().get(id=attrs['feed_id']) + user_id = attrs.get("user_id", self.user_id) + if "feed_id" in attrs: + feed = FeedController().get(id=attrs["feed_id"]) assert feed.user_id == user_id, "no right on feed %r" % feed.id - attrs['category_id'] = feed.category_id - if attrs.get('category_id'): - cat = CategoryController().get(id=attrs['category_id']) - assert self.user_id is None or cat.user_id == user_id, \ - "no right on cat %r" % cat.id + attrs["category_id"] = feed.category_id + if attrs.get("category_id"): + cat = CategoryController().get(id=attrs["category_id"]) + assert self.user_id is None or cat.user_id == user_id, ( + "no right on cat %r" % cat.id + ) return super().update(filters, attrs) def get_history(self, year=None, month=None): @@ -69,11 +75,11 @@ class ArticleController(AbstractController): articles_counter = Counter() articles = self.read() if year is not None: - articles = articles.filter( - sqlalchemy.extract('year', Article.date) == year) + articles = articles.filter(sqlalchemy.extract("year", Article.date) == year) if month is not None: articles = articles.filter( - sqlalchemy.extract('month', Article.date) == month) + sqlalchemy.extract("month", Article.date) == month + ) for article in articles.all(): if year is not None: articles_counter[article.date.month] += 1 @@ -82,6 +88,17 @@ class ArticleController(AbstractController): return articles_counter, articles def read_light(self, **filters): - return super().read(**filters).with_entities(Article.id, Article.title, - Article.readed, Article.like, Article.feed_id, Article.date, - Article.category_id).order_by(Article.date.desc()) + return ( + super() + .read(**filters) + .with_entities( + Article.id, + Article.title, + Article.readed, + Article.like, + Article.feed_id, + Article.date, + Article.category_id, + ) + .order_by(Article.date.desc()) + ) diff --git a/newspipe/web/controllers/bookmark.py b/newspipe/web/controllers/bookmark.py index b5413243..d1c1260c 100644 --- a/newspipe/web/controllers/bookmark.py +++ b/newspipe/web/controllers/bookmark.py @@ -17,16 +17,19 @@ class BookmarkController(AbstractController): return self._count_by(Bookmark.href, filters) def update(self, filters, attrs): - BookmarkTagController(self.user_id) \ - .read(**{'bookmark_id': filters["id"]}) \ - .delete() + BookmarkTagController(self.user_id).read( + **{"bookmark_id": filters["id"]} + ).delete() - for tag in attrs['tags']: + for tag in attrs["tags"]: BookmarkTagController(self.user_id).create( - **{'text': tag.text, - 'id': tag.id, - 'bookmark_id': tag.bookmark_id, - 'user_id': tag.user_id}) - - del attrs['tags'] + **{ + "text": tag.text, + "id": tag.id, + "bookmark_id": tag.bookmark_id, + "user_id": tag.user_id, + } + ) + + del attrs["tags"] return super().update(filters, attrs) diff --git a/newspipe/web/controllers/category.py b/newspipe/web/controllers/category.py index fef5ca81..ec54f5a3 100644 --- a/newspipe/web/controllers/category.py +++ b/newspipe/web/controllers/category.py @@ -7,6 +7,7 @@ class CategoryController(AbstractController): _db_cls = Category def delete(self, obj_id): - FeedController(self.user_id).update({'category_id': obj_id}, - {'category_id': None}) + FeedController(self.user_id).update( + {"category_id": obj_id}, {"category_id": None} + ) return super().delete(obj_id) diff --git a/newspipe/web/controllers/feed.py b/newspipe/web/controllers/feed.py index d75cd994..19ba463f 100644 --- a/newspipe/web/controllers/feed.py +++ b/newspipe/web/controllers/feed.py @@ -16,22 +16,26 @@ DEFAULT_MAX_ERROR = conf.DEFAULT_MAX_ERROR class FeedController(AbstractController): _db_cls = Feed - def list_late(self, max_last, max_error=DEFAULT_MAX_ERROR, - limit=DEFAULT_LIMIT): - return [feed for feed in self.read( - error_count__lt=max_error, enabled=True, - last_retrieved__lt=max_last) - .join(User).filter(User.is_active == True) - .order_by('last_retrieved') - .limit(limit)] + def list_late(self, max_last, max_error=DEFAULT_MAX_ERROR, limit=DEFAULT_LIMIT): + return [ + feed + for feed in self.read( + error_count__lt=max_error, enabled=True, last_retrieved__lt=max_last + ) + .join(User) + .filter(User.is_active == True) + .order_by("last_retrieved") + .limit(limit) + ] def list_fetchable(self, max_error=DEFAULT_MAX_ERROR, limit=DEFAULT_LIMIT): now = datetime.now() max_last = now - timedelta(minutes=60) feeds = self.list_late(max_last, max_error, limit) if feeds: - self.update({'id__in': [feed.id for feed in feeds]}, - {'last_retrieved': now}) + self.update( + {"id__in": [feed.id for feed in feeds]}, {"last_retrieved": now} + ) return feeds def get_duplicates(self, feed_id): @@ -43,8 +47,9 @@ class FeedController(AbstractController): duplicates = [] for pair in itertools.combinations(feed.articles[:1000], 2): date1, date2 = pair[0].date, pair[1].date - if clear_string(pair[0].title) == clear_string(pair[1].title) \ - and (date1 - date2) < timedelta(days=1): + if clear_string(pair[0].title) == clear_string(pair[1].title) and ( + date1 - date2 + ) < timedelta(days=1): if pair[0].retrieved_date < pair[1].retrieved_date: duplicates.append((pair[0], pair[1])) else: @@ -75,11 +80,11 @@ class FeedController(AbstractController): return self._count_by(Feed.link, filters) def _ensure_icon(self, attrs): - if not attrs.get('icon_url'): + if not attrs.get("icon_url"): return icon_contr = IconController() - if not icon_contr.read(url=attrs['icon_url']).count(): - icon_contr.create(**{'url': attrs['icon_url']}) + if not icon_contr.read(url=attrs["icon_url"]).count(): + icon_contr.create(**{"url": attrs["icon_url"]}) def create(self, **attrs): self._ensure_icon(attrs) @@ -87,12 +92,14 @@ class FeedController(AbstractController): def update(self, filters, attrs): from .article import ArticleController + self._ensure_icon(attrs) - if 'category_id' in attrs and attrs['category_id'] == 0: - del attrs['category_id'] - elif 'category_id' in attrs: + if "category_id" in attrs and attrs["category_id"] == 0: + del attrs["category_id"] + elif "category_id" in attrs: art_contr = ArticleController(self.user_id) for feed in self.read(**filters): - art_contr.update({'feed_id': feed.id}, - {'category_id': attrs['category_id']}) + art_contr.update( + {"feed_id": feed.id}, {"category_id": attrs["category_id"]} + ) return super().update(filters, attrs) diff --git a/newspipe/web/controllers/icon.py b/newspipe/web/controllers/icon.py index 07c4a4ef..de86b52f 100644 --- a/newspipe/web/controllers/icon.py +++ b/newspipe/web/controllers/icon.py @@ -9,11 +9,15 @@ class IconController(AbstractController): _user_id_key = None def _build_from_url(self, attrs): - if 'url' in attrs and 'content' not in attrs: - resp = requests.get(attrs['url'], verify=False) - attrs.update({'url': resp.url, - 'mimetype': resp.headers.get('content-type', None), - 'content': base64.b64encode(resp.content).decode('utf8')}) + if "url" in attrs and "content" not in attrs: + resp = requests.get(attrs["url"], verify=False) + attrs.update( + { + "url": resp.url, + "mimetype": resp.headers.get("content-type", None), + "content": base64.b64encode(resp.content).decode("utf8"), + } + ) return attrs def create(self, **attrs): diff --git a/newspipe/web/controllers/user.py b/newspipe/web/controllers/user.py index 6ab04d44..71eb7d08 100644 --- a/newspipe/web/controllers/user.py +++ b/newspipe/web/controllers/user.py @@ -8,13 +8,13 @@ logger = logging.getLogger(__name__) class UserController(AbstractController): _db_cls = User - _user_id_key = 'id' + _user_id_key = "id" def _handle_password(self, attrs): - if attrs.get('password'): - attrs['pwdhash'] = generate_password_hash(attrs.pop('password')) - elif 'password' in attrs: - del attrs['password'] + if attrs.get("password"): + attrs["pwdhash"] = generate_password_hash(attrs.pop("password")) + elif "password" in attrs: + del attrs["password"] def check_password(self, user, password): return check_password_hash(user.pwdhash, password) diff --git a/newspipe/web/decorators.py b/newspipe/web/decorators.py index 3835f646..8569a024 100644 --- a/newspipe/web/decorators.py +++ b/newspipe/web/decorators.py @@ -13,9 +13,11 @@ def async_maker(f): indexing the database) in background. This prevent the server to freeze. """ + def wrapper(*args, **kwargs): thr = Thread(target=f, args=args, kwargs=kwargs) thr.start() + return wrapper @@ -24,4 +26,5 @@ def pyagg_default_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + return wrapper diff --git a/newspipe/web/forms.py b/newspipe/web/forms.py index 7b1893e2..046ce1ec 100644 --- a/newspipe/web/forms.py +++ b/newspipe/web/forms.py @@ -30,8 +30,17 @@ from flask import flash, url_for, redirect from flask_wtf import FlaskForm from flask_babel import lazy_gettext from werkzeug.exceptions import NotFound -from wtforms import TextField, TextAreaField, PasswordField, BooleanField, \ - SubmitField, IntegerField, SelectField, validators, HiddenField +from wtforms import ( + TextField, + TextAreaField, + PasswordField, + BooleanField, + SubmitField, + IntegerField, + SelectField, + validators, + HiddenField, +) from wtforms.fields.html5 import EmailField, URLField from lib import misc_utils @@ -43,27 +52,44 @@ class SignupForm(FlaskForm): """ Sign up form (registration to newspipe). """ - nickname = TextField(lazy_gettext("Nickname"), - [validators.Required(lazy_gettext("Please enter your nickname."))]) - email = EmailField(lazy_gettext("Email"), - [validators.Length(min=6, max=35), - validators.Required( - lazy_gettext("Please enter your email address (only for account activation, won't be stored)."))]) - password = PasswordField(lazy_gettext("Password"), - [validators.Required(lazy_gettext("Please enter a password.")), - validators.Length(min=6, max=100)]) + + nickname = TextField( + lazy_gettext("Nickname"), + [validators.Required(lazy_gettext("Please enter your nickname."))], + ) + email = EmailField( + lazy_gettext("Email"), + [ + validators.Length(min=6, max=35), + validators.Required( + lazy_gettext( + "Please enter your email address (only for account activation, won't be stored)." + ) + ), + ], + ) + password = PasswordField( + lazy_gettext("Password"), + [ + validators.Required(lazy_gettext("Please enter a password.")), + validators.Length(min=6, max=100), + ], + ) submit = SubmitField(lazy_gettext("Sign up")) def validate(self): ucontr = UserController() validated = super().validate() if ucontr.read(nickname=self.nickname.data).count(): - self.nickname.errors.append('Nickname already taken') + self.nickname.errors.append("Nickname already taken") validated = False if self.nickname.data != User.make_valid_nickname(self.nickname.data): - self.nickname.errors.append(lazy_gettext( - 'This nickname has invalid characters. ' - 'Please use letters, numbers, dots and underscores only.')) + self.nickname.errors.append( + lazy_gettext( + "This nickname has invalid characters. " + "Please use letters, numbers, dots and underscores only." + ) + ) validated = False return validated @@ -72,14 +98,15 @@ class RedirectForm(FlaskForm): """ Secure back redirects with WTForms. """ + next = HiddenField() def __init__(self, *args, **kwargs): FlaskForm.__init__(self, *args, **kwargs) if not self.next.data: - self.next.data = misc_utils.get_redirect_target() or '' + self.next.data = misc_utils.get_redirect_target() or "" - def redirect(self, endpoint='home', **values): + def redirect(self, endpoint="home", **values): if misc_utils.is_safe_url(self.next.data): return redirect(self.next.data) target = misc_utils.get_redirect_target() @@ -90,13 +117,21 @@ class SigninForm(RedirectForm): """ Sign in form (connection to newspipe). """ - nickmane = TextField("Nickname", - [validators.Length(min=3, max=35), - validators.Required( - lazy_gettext("Please enter your nickname."))]) - password = PasswordField(lazy_gettext('Password'), - [validators.Required(lazy_gettext("Please enter a password.")), - validators.Length(min=6, max=100)]) + + nickmane = TextField( + "Nickname", + [ + validators.Length(min=3, max=35), + validators.Required(lazy_gettext("Please enter your nickname.")), + ], + ) + password = PasswordField( + lazy_gettext("Password"), + [ + validators.Required(lazy_gettext("Please enter a password.")), + validators.Length(min=6, max=100), + ], + ) submit = SubmitField(lazy_gettext("Log In")) def __init__(self, *args, **kwargs): @@ -109,15 +144,14 @@ class SigninForm(RedirectForm): try: user = ucontr.get(nickname=self.nickmane.data) except NotFound: - self.nickmane.errors.append( - 'Wrong nickname') + self.nickmane.errors.append("Wrong nickname") validated = False else: if not user.is_active: - self.nickmane.errors.append('Account not active') + self.nickmane.errors.append("Account not active") validated = False if not ucontr.check_password(user, self.password.data): - self.password.errors.append('Wrong password') + self.password.errors.append("Wrong password") validated = False self.user = user return validated @@ -127,19 +161,24 @@ class UserForm(FlaskForm): """ Create or edit a user (for the administrator). """ - nickname = TextField(lazy_gettext("Nickname"), - [validators.Required(lazy_gettext("Please enter your nickname."))]) + + nickname = TextField( + lazy_gettext("Nickname"), + [validators.Required(lazy_gettext("Please enter your nickname."))], + ) password = PasswordField(lazy_gettext("Password")) - automatic_crawling = BooleanField(lazy_gettext("Automatic crawling"), - default=True) + automatic_crawling = BooleanField(lazy_gettext("Automatic crawling"), default=True) submit = SubmitField(lazy_gettext("Save")) def validate(self): validated = super(UserForm, self).validate() if self.nickname.data != User.make_valid_nickname(self.nickname.data): - self.nickname.errors.append(lazy_gettext( - 'This nickname has invalid characters. ' - 'Please use letters, numbers, dots and underscores only.')) + self.nickname.errors.append( + lazy_gettext( + "This nickname has invalid characters. " + "Please use letters, numbers, dots and underscores only." + ) + ) validated = False return validated @@ -148,17 +187,18 @@ class ProfileForm(FlaskForm): """ Edit user information. """ - nickname = TextField(lazy_gettext("Nickname"), - [validators.Required(lazy_gettext("Please enter your nickname."))]) + + nickname = TextField( + lazy_gettext("Nickname"), + [validators.Required(lazy_gettext("Please enter your nickname."))], + ) password = PasswordField(lazy_gettext("Password")) password_conf = PasswordField(lazy_gettext("Password Confirmation")) - automatic_crawling = BooleanField(lazy_gettext("Automatic crawling"), - default=True) + automatic_crawling = BooleanField(lazy_gettext("Automatic crawling"), default=True) bio = TextAreaField(lazy_gettext("Bio")) webpage = URLField(lazy_gettext("Webpage")) twitter = URLField(lazy_gettext("Twitter")) - is_public_profile = BooleanField(lazy_gettext("Public profile"), - default=True) + is_public_profile = BooleanField(lazy_gettext("Public profile"), default=True) submit = SubmitField(lazy_gettext("Save")) def validate(self): @@ -169,28 +209,34 @@ class ProfileForm(FlaskForm): self.password_conf.errors.append(message) validated = False if self.nickname.data != User.make_valid_nickname(self.nickname.data): - self.nickname.errors.append(lazy_gettext('This nickname has ' - 'invalid characters. Please use letters, numbers, dots and' - ' underscores only.')) + self.nickname.errors.append( + lazy_gettext( + "This nickname has " + "invalid characters. Please use letters, numbers, dots and" + " underscores only." + ) + ) validated = False return validated class AddFeedForm(FlaskForm): title = TextField(lazy_gettext("Title"), [validators.Optional()]) - link = TextField(lazy_gettext("Feed link"), - [validators.Required(lazy_gettext("Please enter the URL."))]) + link = TextField( + lazy_gettext("Feed link"), + [validators.Required(lazy_gettext("Please enter the URL."))], + ) site_link = TextField(lazy_gettext("Site link"), [validators.Optional()]) enabled = BooleanField(lazy_gettext("Check for updates"), default=True) submit = SubmitField(lazy_gettext("Save")) - category_id = SelectField(lazy_gettext("Category of the feed"), - [validators.Optional()]) + category_id = SelectField( + lazy_gettext("Category of the feed"), [validators.Optional()] + ) private = BooleanField(lazy_gettext("Private"), default=False) def set_category_choices(self, categories): - self.category_id.choices = [('0', 'No Category')] - self.category_id.choices += [(str(cat.id), cat.name) - for cat in categories] + self.category_id.choices = [("0", "No Category")] + self.category_id.choices += [(str(cat.id), cat.name) for cat in categories] class CategoryForm(FlaskForm): @@ -199,13 +245,13 @@ class CategoryForm(FlaskForm): class BookmarkForm(FlaskForm): - href = TextField(lazy_gettext("URL"), - [validators.Required( - lazy_gettext("Please enter an URL."))]) - title = TextField(lazy_gettext("Title"), - [validators.Length(min=0, max=100)]) - description = TextField(lazy_gettext("Description"), - [validators.Length(min=0, max=500)]) + href = TextField( + lazy_gettext("URL"), [validators.Required(lazy_gettext("Please enter an URL."))] + ) + title = TextField(lazy_gettext("Title"), [validators.Length(min=0, max=100)]) + description = TextField( + lazy_gettext("Description"), [validators.Length(min=0, max=500)] + ) tags = TextField(lazy_gettext("Tags")) to_read = BooleanField(lazy_gettext("To read"), default=False) shared = BooleanField(lazy_gettext("Shared"), default=False) @@ -213,8 +259,12 @@ class BookmarkForm(FlaskForm): class InformationMessageForm(FlaskForm): - subject = TextField(lazy_gettext("Subject"), - [validators.Required(lazy_gettext("Please enter a subject."))]) - message = TextAreaField(lazy_gettext("Message"), - [validators.Required(lazy_gettext("Please enter a content."))]) + subject = TextField( + lazy_gettext("Subject"), + [validators.Required(lazy_gettext("Please enter a subject."))], + ) + message = TextAreaField( + lazy_gettext("Message"), + [validators.Required(lazy_gettext("Please enter a content."))], + ) submit = SubmitField(lazy_gettext("Send")) diff --git a/newspipe/web/lib/user_utils.py b/newspipe/web/lib/user_utils.py index f78a6ed6..84b1c75c 100644 --- a/newspipe/web/lib/user_utils.py +++ b/newspipe/web/lib/user_utils.py @@ -1,22 +1,20 @@ - - from itsdangerous import URLSafeTimedSerializer import conf from bootstrap import application def generate_confirmation_token(nickname): - serializer = URLSafeTimedSerializer(application.config['SECRET_KEY']) - return serializer.dumps(nickname, salt=application.config['SECURITY_PASSWORD_SALT']) + serializer = URLSafeTimedSerializer(application.config["SECRET_KEY"]) + return serializer.dumps(nickname, salt=application.config["SECURITY_PASSWORD_SALT"]) def confirm_token(token): - serializer = URLSafeTimedSerializer(application.config['SECRET_KEY']) + serializer = URLSafeTimedSerializer(application.config["SECRET_KEY"]) try: nickname = serializer.loads( token, - salt=application.config['SECURITY_PASSWORD_SALT'], - max_age=conf.TOKEN_VALIDITY_PERIOD + salt=application.config["SECURITY_PASSWORD_SALT"], + max_age=conf.TOKEN_VALIDITY_PERIOD, ) except: return False diff --git a/newspipe/web/lib/view_utils.py b/newspipe/web/lib/view_utils.py index 1d8c6aed..218ebb4c 100644 --- a/newspipe/web/lib/view_utils.py +++ b/newspipe/web/lib/view_utils.py @@ -15,12 +15,14 @@ def etag_match(func): headers = {} else: return response - if request.headers.get('if-none-match') == etag: + if request.headers.get("if-none-match") == etag: response = Response(status=304) - response.headers['Cache-Control'] \ - = headers.get('Cache-Control', 'pragma: no-cache') + response.headers["Cache-Control"] = headers.get( + "Cache-Control", "pragma: no-cache" + ) elif not isinstance(response, Response): response = make_response(response) - response.headers['etag'] = etag + response.headers["etag"] = etag return response + return wrapper diff --git a/newspipe/web/models/__init__.py b/newspipe/web/models/__init__.py index bfb1368c..09249368 100644 --- a/newspipe/web/models/__init__.py +++ b/newspipe/web/models/__init__.py @@ -36,18 +36,29 @@ from .tag import BookmarkTag from .tag import ArticleTag from .bookmark import Bookmark -__all__ = ['Feed', 'Role', 'User', 'Article', 'Icon', 'Category', - 'Bookmark', 'ArticleTag', 'BookmarkTag'] +__all__ = [ + "Feed", + "Role", + "User", + "Article", + "Icon", + "Category", + "Bookmark", + "ArticleTag", + "BookmarkTag", +] import os from sqlalchemy.engine import reflection from sqlalchemy.schema import ( - MetaData, - Table, - DropTable, - ForeignKeyConstraint, - DropConstraint) + MetaData, + Table, + DropTable, + ForeignKeyConstraint, + DropConstraint, +) + def db_empty(db): "Will drop every datas stocked in db." @@ -71,9 +82,9 @@ def db_empty(db): for table_name in inspector.get_table_names(): fks = [] for fk in inspector.get_foreign_keys(table_name): - if not fk['name']: + if not fk["name"]: continue - fks.append(ForeignKeyConstraint((), (), name=fk['name'])) + fks.append(ForeignKeyConstraint((), (), name=fk["name"])) t = Table(table_name, metadata, *fks) tbs.append(t) all_fks.extend(fks) diff --git a/newspipe/web/models/article.py b/newspipe/web/models/article.py index d55e59c1..c826a3c9 100644 --- a/newspipe/web/models/article.py +++ b/newspipe/web/models/article.py @@ -48,40 +48,54 @@ class Article(db.Model, RightMixin): retrieved_date = db.Column(db.DateTime(), default=datetime.utcnow) # foreign keys - user_id = db.Column(db.Integer(), db.ForeignKey('user.id')) - feed_id = db.Column(db.Integer(), db.ForeignKey('feed.id')) - category_id = db.Column(db.Integer(), db.ForeignKey('category.id')) + user_id = db.Column(db.Integer(), db.ForeignKey("user.id")) + feed_id = db.Column(db.Integer(), db.ForeignKey("feed.id")) + category_id = db.Column(db.Integer(), db.ForeignKey("category.id")) # relationships - tag_objs = db.relationship('ArticleTag', back_populates='article', - cascade='all,delete-orphan', - lazy=False, - foreign_keys='[ArticleTag.article_id]') - tags = association_proxy('tag_objs', 'text') + tag_objs = db.relationship( + "ArticleTag", + back_populates="article", + cascade="all,delete-orphan", + lazy=False, + foreign_keys="[ArticleTag.article_id]", + ) + tags = association_proxy("tag_objs", "text") # indexes - #__table_args__ = ( + # __table_args__ = ( # Index('user_id'), # Index('user_id', 'category_id'), # Index('user_id', 'feed_id'), # Index('ix_article_uid_fid_eid', user_id, feed_id, entry_id) - #) + # ) # api whitelists @staticmethod def _fields_base_write(): - return {'readed', 'like', 'feed_id', 'category_id'} + return {"readed", "like", "feed_id", "category_id"} @staticmethod def _fields_base_read(): - return {'id', 'entry_id', 'link', 'title', 'content', 'date', - 'retrieved_date', 'user_id', 'tags'} + return { + "id", + "entry_id", + "link", + "title", + "content", + "date", + "retrieved_date", + "user_id", + "tags", + } @staticmethod def _fields_api_write(): - return {'tags'} + return {"tags"} def __repr__(self): - return "<Article(id=%d, entry_id=%s, title=%r, " \ - "date=%r, retrieved_date=%r)>" % (self.id, self.entry_id, - self.title, self.date, self.retrieved_date) + return ( + "<Article(id=%d, entry_id=%s, title=%r, " + "date=%r, retrieved_date=%r)>" + % (self.id, self.entry_id, self.title, self.date, self.retrieved_date) + ) diff --git a/newspipe/web/models/bookmark.py b/newspipe/web/models/bookmark.py index eb6b73e3..c557225e 100644 --- a/newspipe/web/models/bookmark.py +++ b/newspipe/web/models/bookmark.py @@ -40,6 +40,7 @@ class Bookmark(db.Model, RightMixin): """ Represent a bookmark. """ + id = db.Column(db.Integer(), primary_key=True) href = db.Column(db.String(), default="") title = db.Column(db.String(), default="") @@ -47,22 +48,25 @@ class Bookmark(db.Model, RightMixin): shared = db.Column(db.Boolean(), default=False) to_read = db.Column(db.Boolean(), default=False) time = db.Column(db.DateTime(), default=datetime.utcnow) - user_id = db.Column(db.Integer(), db.ForeignKey('user.id')) + user_id = db.Column(db.Integer(), db.ForeignKey("user.id")) # relationships - tags = db.relationship(BookmarkTag, backref='of_bookmark', lazy='dynamic', - cascade='all,delete-orphan', - order_by=desc(BookmarkTag.text)) - tags_proxy = association_proxy('tags', 'text') - + tags = db.relationship( + BookmarkTag, + backref="of_bookmark", + lazy="dynamic", + cascade="all,delete-orphan", + order_by=desc(BookmarkTag.text), + ) + tags_proxy = association_proxy("tags", "text") - @validates('description') + @validates("description") def validates_title(self, key, value): return str(value).strip() - @validates('extended') + @validates("extended") def validates_description(self, key, value): return str(value).strip() def __repr__(self): - return '<Bookmark %r>' % (self.href) + return "<Bookmark %r>" % (self.href) diff --git a/newspipe/web/models/category.py b/newspipe/web/models/category.py index 2da7809a..bb47ce45 100644 --- a/newspipe/web/models/category.py +++ b/newspipe/web/models/category.py @@ -11,19 +11,18 @@ class Category(db.Model, RightMixin): name = db.Column(db.String()) # relationships - user_id = db.Column(db.Integer, db.ForeignKey('user.id')) - feeds = db.relationship('Feed', cascade='all,delete-orphan') - articles = db.relationship('Article', - cascade='all,delete-orphan') + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) + feeds = db.relationship("Feed", cascade="all,delete-orphan") + articles = db.relationship("Article", cascade="all,delete-orphan") # index - idx_category_uid = Index('user_id') + idx_category_uid = Index("user_id") # api whitelists @staticmethod def _fields_base_read(): - return {'id', 'user_id'} + return {"id", "user_id"} @staticmethod def _fields_base_write(): - return {'name'} + return {"name"} diff --git a/newspipe/web/models/feed.py b/newspipe/web/models/feed.py index fc0b64cb..f440a39c 100644 --- a/newspipe/web/models/feed.py +++ b/newspipe/web/models/feed.py @@ -38,6 +38,7 @@ class Feed(db.Model, RightMixin): """ Represent a feed. """ + id = db.Column(db.Integer(), primary_key=True) title = db.Column(db.String(), default="") description = db.Column(db.String(), default="FR") @@ -58,34 +59,47 @@ class Feed(db.Model, RightMixin): error_count = db.Column(db.Integer(), default=0) # relationship - icon_url = db.Column(db.String(), db.ForeignKey('icon.url'), default=None) - user_id = db.Column(db.Integer(), db.ForeignKey('user.id')) - category_id = db.Column(db.Integer(), db.ForeignKey('category.id')) - articles = db.relationship(Article, backref='source', lazy='dynamic', - cascade='all,delete-orphan', - order_by=desc(Article.date)) + icon_url = db.Column(db.String(), db.ForeignKey("icon.url"), default=None) + user_id = db.Column(db.Integer(), db.ForeignKey("user.id")) + category_id = db.Column(db.Integer(), db.ForeignKey("category.id")) + articles = db.relationship( + Article, + backref="source", + lazy="dynamic", + cascade="all,delete-orphan", + order_by=desc(Article.date), + ) # index - idx_feed_uid_cid = Index('user_id', 'category_id') - idx_feed_uid = Index('user_id') + idx_feed_uid_cid = Index("user_id", "category_id") + idx_feed_uid = Index("user_id") - # api whitelists + # api whitelists @staticmethod def _fields_base_write(): - return {'title', 'description', 'link', 'site_link', 'enabled', - 'filters', 'last_error', 'error_count', 'category_id'} + return { + "title", + "description", + "link", + "site_link", + "enabled", + "filters", + "last_error", + "error_count", + "category_id", + } @staticmethod def _fields_base_read(): - return {'id', 'user_id', 'icon_url', 'last_retrieved'} + return {"id", "user_id", "icon_url", "last_retrieved"} - @validates('title') + @validates("title") def validates_title(self, key, value): return str(value).strip() - @validates('description') + @validates("description") def validates_description(self, key, value): return str(value).strip() def __repr__(self): - return '<Feed %r>' % (self.title) + return "<Feed %r>" % (self.title) diff --git a/newspipe/web/models/right_mixin.py b/newspipe/web/models/right_mixin.py index 1c316f95..670beafa 100644 --- a/newspipe/web/models/right_mixin.py +++ b/newspipe/web/models/right_mixin.py @@ -2,14 +2,13 @@ from sqlalchemy.ext.associationproxy import _AssociationList class RightMixin: - @staticmethod def _fields_base_write(): return set() @staticmethod def _fields_base_read(): - return set(['id']) + return set(["id"]) @staticmethod def _fields_api_write(): @@ -17,7 +16,7 @@ class RightMixin: @staticmethod def _fields_api_read(): - return set(['id']) + return set(["id"]) @classmethod def fields_base_write(cls): @@ -36,26 +35,28 @@ class RightMixin: return cls.fields_base_read().union(cls._fields_api_read()) def __getitem__(self, key): - if not hasattr(self, '__dump__'): + if not hasattr(self, "__dump__"): self.__dump__ = {} return self.__dump__.get(key) def __setitem__(self, key, value): - if not hasattr(self, '__dump__'): + if not hasattr(self, "__dump__"): self.__dump__ = {} self.__dump__[key] = value - def dump(self, role='admin'): - if role == 'admin': - dico = {k: getattr(self, k) - for k in set(self.__table__.columns.keys()) - .union(self.fields_api_read()) - .union(self.fields_base_read())} - elif role == 'api': + def dump(self, role="admin"): + if role == "admin": + dico = { + k: getattr(self, k) + for k in set(self.__table__.columns.keys()) + .union(self.fields_api_read()) + .union(self.fields_base_read()) + } + elif role == "api": dico = {k: getattr(self, k) for k in self.fields_api_read()} else: dico = {k: getattr(self, k) for k in self.fields_base_read()} - if hasattr(self, '__dump__'): + if hasattr(self, "__dump__"): dico.update(self.__dump__) for key, value in dico.items(): # preventing association proxy to die if isinstance(value, _AssociationList): diff --git a/newspipe/web/models/role.py b/newspipe/web/models/role.py index 0a2ecd4a..15a08b73 100644 --- a/newspipe/web/models/role.py +++ b/newspipe/web/models/role.py @@ -33,7 +33,8 @@ class Role(db.Model): """ Represent a role. """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(), unique=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) diff --git a/newspipe/web/models/tag.py b/newspipe/web/models/tag.py index 76467c0b..9bd8afc5 100644 --- a/newspipe/web/models/tag.py +++ b/newspipe/web/models/tag.py @@ -8,12 +8,14 @@ class ArticleTag(db.Model): text = db.Column(db.String, primary_key=True, unique=False) # foreign keys - article_id = db.Column(db.Integer, db.ForeignKey('article.id', ondelete='CASCADE'), - primary_key=True) + article_id = db.Column( + db.Integer, db.ForeignKey("article.id", ondelete="CASCADE"), primary_key=True + ) # relationships - article = db.relationship('Article', back_populates='tag_objs', - foreign_keys=[article_id]) + article = db.relationship( + "Article", back_populates="tag_objs", foreign_keys=[article_id] + ) def __init__(self, text): self.text = text @@ -24,12 +26,18 @@ class BookmarkTag(db.Model): text = db.Column(db.String, unique=False) # foreign keys - user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE')) - bookmark_id = db.Column(db.Integer, db.ForeignKey('bookmark.id', ondelete='CASCADE')) + user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE")) + bookmark_id = db.Column( + db.Integer, db.ForeignKey("bookmark.id", ondelete="CASCADE") + ) # relationships - bookmark = db.relationship('Bookmark', back_populates='tags', - cascade="all,delete", foreign_keys=[bookmark_id]) + bookmark = db.relationship( + "Bookmark", + back_populates="tags", + cascade="all,delete", + foreign_keys=[bookmark_id], + ) # def __init__(self, text, user_id): # self.text = text diff --git a/newspipe/web/models/user.py b/newspipe/web/models/user.py index 4d65c3c5..a088c74e 100644 --- a/newspipe/web/models/user.py +++ b/newspipe/web/models/user.py @@ -44,6 +44,7 @@ class User(db.Model, UserMixin, RightMixin): """ Represent a user. """ + id = db.Column(db.Integer, primary_key=True) nickname = db.Column(db.String(), unique=True) pwdhash = db.Column(db.String()) @@ -64,29 +65,34 @@ class User(db.Model, UserMixin, RightMixin): is_api = db.Column(db.Boolean(), default=False) # relationships - categories = db.relationship('Category', backref='user', - cascade='all, delete-orphan', - foreign_keys=[Category.user_id]) - feeds = db.relationship('Feed', backref='user', - cascade='all, delete-orphan', - foreign_keys=[Feed.user_id]) + categories = db.relationship( + "Category", + backref="user", + cascade="all, delete-orphan", + foreign_keys=[Category.user_id], + ) + feeds = db.relationship( + "Feed", + backref="user", + cascade="all, delete-orphan", + foreign_keys=[Feed.user_id], + ) @staticmethod def _fields_base_write(): - return {'login', 'password'} + return {"login", "password"} @staticmethod def _fields_base_read(): - return {'date_created', 'last_connection'} + return {"date_created", "last_connection"} @staticmethod def make_valid_nickname(nickname): - return re.sub('[^a-zA-Z0-9_\.]', '', nickname) + return re.sub("[^a-zA-Z0-9_\.]", "", nickname) - @validates('bio') + @validates("bio") def validates_bio(self, key, value): - assert len(value) <= 5000, \ - AssertionError("maximum length for bio: 5000") + assert len(value) <= 5000, AssertionError("maximum length for bio: 5000") return value.strip() def get_id(self): @@ -105,4 +111,4 @@ class User(db.Model, UserMixin, RightMixin): return self.id == other.id def __repr__(self): - return '<User %r>' % (self.nickname) + return "<User %r>" % (self.nickname) diff --git a/newspipe/web/views/__init__.py b/newspipe/web/views/__init__.py index 41bb52f3..b8b415f9 100644 --- a/newspipe/web/views/__init__.py +++ b/newspipe/web/views/__init__.py @@ -8,10 +8,25 @@ 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'] +__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 diff --git a/newspipe/web/views/admin.py b/newspipe/web/views/admin.py index 73b2b668..fe1b389b 100644 --- a/newspipe/web/views/admin.py +++ b/newspipe/web/views/admin.py @@ -1,5 +1,5 @@ from datetime import datetime -from flask import (Blueprint, render_template, redirect, flash, url_for) +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 @@ -8,41 +8,45 @@ 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 = Blueprint("admin", __name__, url_prefix="/admin") -@admin_bp.route('/dashboard', methods=['GET', 'POST']) +@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')) + 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/<int:user_id>', methods=['GET']) + 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/<int:user_id>", 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 <i>%(nick)s</i>', nick=user.nickname) + message = gettext("Edit the user <i>%(nick)s</i>", nick=user.nickname) else: form = UserForm() - message = gettext('Add a new user') - return render_template('/admin/create_user.html', - form=form, message=message) + 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/<int:user_id>', methods=['POST']) +@admin_bp.route("/user/create", methods=["POST"]) +@admin_bp.route("/user/edit/<int:user_id>", methods=["POST"]) @login_required @admin_permission.require(http_exception=403) def process_user_form(user_id=None): @@ -53,31 +57,42 @@ def process_user_form(user_id=None): user_contr = UserController() if not form.validate(): - return render_template('/admin/create_user.html', form=form, - message=gettext('Some errors were found')) + 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_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') + 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)) + 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/<int:user_id>', methods=['GET']) +@admin_bp.route("/delete_user/<int:user_id>", methods=["GET"]) @login_required @admin_permission.require(http_exception=403) def delete_user(user_id=None): @@ -86,16 +101,21 @@ def delete_user(user_id=None): """ try: user = UserController().delete(user_id) - flash(gettext('User %(nick)s successfully deleted', - nick=user.nickname), 'success') + 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')) + 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/<int:user_id>', methods=['GET']) +@admin_bp.route("/toggle_user/<int:user_id>", methods=["GET"]) @login_required @admin_permission.require() def toggle_user(user_id=None): @@ -104,16 +124,18 @@ def toggle_user(user_id=None): """ ucontr = UserController() user = ucontr.get(id=user_id) - user_changed = ucontr.update({'id': user_id}, - {'is_active': not user.is_active}) + 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')) + 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')) + 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/newspipe/web/views/api/v2/__init__.py b/newspipe/web/views/api/v2/__init__.py index 46760261..ef587e72 100644 --- a/newspipe/web/views/api/v2/__init__.py +++ b/newspipe/web/views/api/v2/__init__.py @@ -1,3 +1,3 @@ from web.views.api.v2 import article, feed, category -__all__ = ['article', 'feed', 'category'] +__all__ = ["article", "feed", "category"] diff --git a/newspipe/web/views/api/v2/article.py b/newspipe/web/views/api/v2/article.py index 2be286c6..8da6c6dd 100644 --- a/newspipe/web/views/api/v2/article.py +++ b/newspipe/web/views/api/v2/article.py @@ -6,8 +6,12 @@ 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) +from web.views.api.v2.common import ( + PyAggAbstractResource, + PyAggResourceNew, + PyAggResourceExisting, + PyAggResourceMulti, +) class ArticleNewAPI(PyAggResourceNew): @@ -24,30 +28,32 @@ class ArticlesAPI(PyAggResourceMulti): class ArticlesChallenge(PyAggAbstractResource): controller_cls = ArticleController - attrs = {'ids': {'type': list, 'default': []}} + attrs = {"ids": {"type": list, "default": []}} @api_permission.require(http_exception=403) def get(self): - parsed_args = self.reqparse_args(right='read') + 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']: + 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): + 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'])) + 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/<int:obj_id>', endpoint='article.json') -api.add_resource(ArticlesAPI, '/articles', endpoint='articles.json') -api.add_resource(ArticlesChallenge, '/articles/challenge', - endpoint='articles_challenge.json') +api.add_resource(ArticleNewAPI, "/article", endpoint="article_new.json") +api.add_resource(ArticleAPI, "/article/<int:obj_id>", endpoint="article.json") +api.add_resource(ArticlesAPI, "/articles", endpoint="articles.json") +api.add_resource( + ArticlesChallenge, "/articles/challenge", endpoint="articles_challenge.json" +) diff --git a/newspipe/web/views/api/v2/category.py b/newspipe/web/views/api/v2/category.py index 70fda1ea..a830624d 100644 --- a/newspipe/web/views/api/v2/category.py +++ b/newspipe/web/views/api/v2/category.py @@ -3,9 +3,11 @@ 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) +from web.views.api.v2.common import ( + PyAggResourceNew, + PyAggResourceExisting, + PyAggResourceMulti, +) class CategoryNewAPI(PyAggResourceNew): @@ -21,7 +23,6 @@ class CategoriesAPI(PyAggResourceMulti): api = Api(current_app, prefix=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') +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") diff --git a/newspipe/web/views/api/v2/common.py b/newspipe/web/views/api/v2/common.py index 8a53d7e6..81248422 100644 --- a/newspipe/web/views/api/v2/common.py +++ b/newspipe/web/views/api/v2/common.py @@ -26,8 +26,12 @@ 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.views.common import ( + admin_permission, + api_permission, + login_user_bundle, + jsonify, +) from web.controllers import UserController logger = logging.getLogger(__name__) @@ -50,6 +54,7 @@ def authenticate(func): if current_user.is_authenticated: return func(*args, **kwargs) raise Unauthorized() + return wrapper @@ -64,8 +69,9 @@ class PyAggAbstractResource(Resource): 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): + 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 @@ -89,42 +95,41 @@ class PyAggAbstractResource(Resource): if self.attrs is not None: attrs = self.attrs elif admin_permission.can(): - attrs = self.controller_cls._get_attrs_desc('admin') + attrs = self.controller_cls._get_attrs_desc("admin") elif api_permission.can(): - attrs = self.controller_cls._get_attrs_desc('api', right) + attrs = self.controller_cls._get_attrs_desc("api", right) else: - attrs = self.controller_cls._get_attrs_desc('base', right) + 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]) + 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 + 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) + args = self.reqparse_args(right="write", default=False) if not args: raise BadRequest() - return self.controller.update({'id': obj_id}, args), 200 + return self.controller.update({"id": obj_id}, args), 200 def delete(self, obj_id=None): """delete a object""" @@ -133,19 +138,18 @@ class PyAggResourceExisting(PyAggAbstractResource): 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) + 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) + 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) @@ -163,10 +167,11 @@ class PyAggResourceMulti(PyAggAbstractResource): class Proxy: pass + for attrs in request.json: try: Proxy.json = attrs - args = self.reqparse_args('write', req=Proxy, default=False) + args = self.reqparse_args("write", req=Proxy, default=False) obj = self.controller.create(**args) results.append(obj) except Exception as error: @@ -188,20 +193,21 @@ class PyAggResourceMulti(PyAggAbstractResource): 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) + args = self.reqparse_args("write", req=Proxy, default=False) + result = self.controller.update({"id": obj_id}, args) if result: - results.append('ok') + results.append("ok") else: - results.append('nok') + results.append("nok") except Exception as error: results.append(str(error)) - if results.count('ok') == 0: # all failed => 500 + if results.count("ok") == 0: # all failed => 500 status = 500 - elif results.count('ok') != len(results): # some failed => 206 + elif results.count("ok") != len(results): # some failed => 206 status = 206 return results, status @@ -212,11 +218,11 @@ class PyAggResourceMulti(PyAggAbstractResource): for obj_id in request.json: try: self.controller.delete(obj_id) - results.append('ok') + 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: + if status == 206 and results.count("ok") == 0: status = 500 return results, status diff --git a/newspipe/web/views/api/v2/feed.py b/newspipe/web/views/api/v2/feed.py index a0691277..1e4fabf2 100644 --- a/newspipe/web/views/api/v2/feed.py +++ b/newspipe/web/views/api/v2/feed.py @@ -3,14 +3,14 @@ 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.controllers.feed import FeedController, DEFAULT_MAX_ERROR, DEFAULT_LIMIT -from web.views.api.v2.common import PyAggAbstractResource, \ - PyAggResourceNew, \ - PyAggResourceExisting, \ - PyAggResourceMulti +from web.views.api.v2.common import ( + PyAggAbstractResource, + PyAggResourceNew, + PyAggResourceExisting, + PyAggResourceMulti, +) class FeedNewAPI(PyAggResourceNew): @@ -27,21 +27,21 @@ class FeedsAPI(PyAggResourceMulti): class FetchableFeedAPI(PyAggAbstractResource): controller_cls = FeedController - attrs = {'max_error': {'type': int, 'default': DEFAULT_MAX_ERROR}, - 'limit': {'type': int, 'default': DEFAULT_LIMIT}} + 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)] + 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/<int:obj_id>', endpoint='feed.json') -api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json') -api.add_resource(FetchableFeedAPI, '/feeds/fetchable', - endpoint='fetchable_feed.json') +api.add_resource(FeedNewAPI, "/feed", endpoint="feed_new.json") +api.add_resource(FeedAPI, "/feed/<int:obj_id>", endpoint="feed.json") +api.add_resource(FeedsAPI, "/feeds", endpoint="feeds.json") +api.add_resource(FetchableFeedAPI, "/feeds/fetchable", endpoint="fetchable_feed.json") diff --git a/newspipe/web/views/api/v3/__init__.py b/newspipe/web/views/api/v3/__init__.py index 76aa1f19..5066b0b6 100644 --- a/newspipe/web/views/api/v3/__init__.py +++ b/newspipe/web/views/api/v3/__init__.py @@ -1,3 +1,3 @@ from web.views.api.v3 import article -__all__ = ['article'] +__all__ = ["article"] diff --git a/newspipe/web/views/api/v3/article.py b/newspipe/web/views/api/v3/article.py index 4cf35648..04d19552 100644 --- a/newspipe/web/views/api/v3/article.py +++ b/newspipe/web/views/api/v3/article.py @@ -35,6 +35,7 @@ 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. """ @@ -43,7 +44,7 @@ class ArticleProcessor(AbstractProcessor): try: article = ArticleController(current_user.id).get(id=instance_id) except NotFound: - raise ProcessingException(description='No such article.', code=404) + raise ProcessingException(description="No such article.", code=404) self.is_authorized(current_user, article) def post_preprocessor(self, data=None, **kw): @@ -52,7 +53,7 @@ class ArticleProcessor(AbstractProcessor): try: feed = FeedController(current_user.id).get(id=data["feed_id"]) except NotFound: - raise ProcessingException(description='No such feed.', code=404) + raise ProcessingException(description="No such feed.", code=404) self.is_authorized(current_user, feed) data["category_id"] = feed.category_id @@ -61,24 +62,23 @@ class ArticleProcessor(AbstractProcessor): try: article = ArticleController(current_user.id).get(id=instance_id) except NotFound: - raise ProcessingException(description='No such article.', code=404) + 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])) +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/newspipe/web/views/api/v3/common.py b/newspipe/web/views/api/v3/common.py index d5e94a3f..5467ee30 100644 --- a/newspipe/web/views/api/v3/common.py +++ b/newspipe/web/views/api/v3/common.py @@ -33,7 +33,8 @@ from werkzeug.exceptions import NotFound from web.controllers import ArticleController, UserController from web.views.common import login_user_bundle -url_prefix = '/api/v3' +url_prefix = "/api/v3" + def auth_func(*args, **kw): if request.authorization: @@ -41,24 +42,23 @@ def auth_func(*args, **kw): try: user = ucontr.get(nickname=request.authorization.username) except NotFound: - raise ProcessingException("Couldn't authenticate your user", - code=401) + 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) + 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) + raise ProcessingException(description="Not authenticated!", code=401) + -class AbstractProcessor(): +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) + 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 @@ -69,13 +69,11 @@ class AbstractProcessor(): """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) + 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"] = [] search_params["filters"].append(filt) @@ -95,13 +93,11 @@ class AbstractProcessor(): 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) + 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"] = [] search_params["filters"].append(filt) diff --git a/newspipe/web/views/api/v3/feed.py b/newspipe/web/views/api/v3/feed.py index 2cbbafd9..9d2c9180 100644 --- a/newspipe/web/views/api/v3/feed.py +++ b/newspipe/web/views/api/v3/feed.py @@ -33,6 +33,7 @@ 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. """ @@ -43,16 +44,19 @@ class FeedProcessor(AbstractProcessor): 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])) +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/newspipe/web/views/article.py b/newspipe/web/views/article.py index bf39795d..ba66726e 100644 --- a/newspipe/web/views/article.py +++ b/newspipe/web/views/article.py @@ -1,6 +1,14 @@ from datetime import datetime, timedelta -from flask import (Blueprint, g, render_template, redirect, - flash, url_for, make_response, request) +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 @@ -9,25 +17,24 @@ 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.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') +articles_bp = Blueprint("articles", __name__, url_prefix="/articles") +article_bp = Blueprint("article", __name__, url_prefix="/article") -@article_bp.route('/redirect/<int:article_id>', methods=['GET']) +@article_bp.route("/redirect/<int:article_id>", 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}) + contr.update({"id": article.id}, {"readed": True}) return redirect(article.link) -@article_bp.route('/<int:article_id>', methods=['GET']) +@article_bp.route("/<int:article_id>", methods=["GET"]) @login_required @etag_match def article(article_id=None): @@ -35,11 +42,12 @@ 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) + return render_template( + "article.html", head_titles=[clear_string(article.title)], article=article + ) -@article_bp.route('/public/<int:article_id>', methods=['GET']) + +@article_bp.route("/public/<int:article_id>", methods=["GET"]) @etag_match def article_pub(article_id=None): """ @@ -48,13 +56,13 @@ def article_pub(article_id=None): """ 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) + 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/<int:article_id>', methods=['GET']) +@article_bp.route("/like/<int:article_id>", methods=["GET"]) @login_required def like(article_id=None): """ @@ -62,80 +70,84 @@ def like(article_id=None): """ 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}) + art_contr = art_contr.update({"id": article_id}, {"like": not article.like}) return redirect(redirect_url()) -@article_bp.route('/delete/<int:article_id>', methods=['GET']) +@article_bp.route("/delete/<int:article_id>", 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')) + 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/<int:year>', methods=['GET']) -@articles_bp.route('/history/<int:year>/<int:month>', methods=['GET']) +@articles_bp.route("/history", methods=["GET"]) +@articles_bp.route("/history/<int:year>", methods=["GET"]) +@articles_bp.route("/history/<int:year>/<int:month>", 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) + return render_template( + "history.html", articles_counter=cntr, articles=artcles, year=year, month=month + ) -@article_bp.route('/mark_as/<string:new_value>', methods=['GET']) -@article_bp.route('/mark_as/<string:new_value>/article/<int:article_id>', - methods=['GET']) +@article_bp.route("/mark_as/<string:new_value>", methods=["GET"]) +@article_bp.route( + "/mark_as/<string:new_value>/article/<int:article_id>", methods=["GET"] +) @login_required -def mark_as(new_value='read', feed_id=None, article_id=None): +def mark_as(new_value="read", feed_id=None, article_id=None): """ Mark all unreaded articles as read. """ - readed = new_value == 'read' + readed = new_value == "read" art_contr = ArticleController(current_user.id) - filters = {'readed': not readed} + filters = {"readed": not readed} if feed_id is not None: - filters['feed_id'] = feed_id - message = 'Feed marked as %s.' + 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.' + filters["id"] = article_id + message = "Article marked as %s." else: - message = 'All article marked as %s.' + message = "All article marked as %s." art_contr.update(filters, {"readed": readed}) - flash(gettext(message % new_value), 'info') + flash(gettext(message % new_value), "info") if readed: return redirect(redirect_url()) - return redirect('home') + return redirect("home") -@articles_bp.route('/expire_articles', methods=['GET']) +@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))) + 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}) + 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') + flash(gettext("%(count)d articles deleted", count=count), "info") return redirect(redirect_url()) -@articles_bp.route('/export', methods=['GET']) +@articles_bp.route("/export", methods=["GET"]) @login_required def export(): """ @@ -145,10 +157,9 @@ def export(): try: json_result = export_json(user) except Exception as e: - flash(gettext("Error when exporting articles."), 'danger') + 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' + response.mimetype = "application/json" + response.headers["Content-Disposition"] = "attachment; filename=account.json" return response diff --git a/newspipe/web/views/bookmark.py b/newspipe/web/views/bookmark.py index 21d832d2..bceda631 100644 --- a/newspipe/web/views/bookmark.py +++ b/newspipe/web/views/bookmark.py @@ -1,5 +1,5 @@ #! /usr/bin/env python -#-*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # Newspipe - A Web based news aggregator. # Copyright (C) 2010-2017 Cédric Bonhomme - https://www.cedricbonhomme.org @@ -30,8 +30,15 @@ import logging import datetime from werkzeug.exceptions import BadRequest -from flask import Blueprint, render_template, flash, \ - redirect, request, url_for, make_response +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 @@ -46,93 +53,105 @@ 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 = 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('/<string:status>', defaults={'per_page': '50'}, - methods=['GET']) -def list_(per_page, status='all'): +@bookmarks_bp.route("/", defaults={"per_page": "50"}, methods=["GET"]) +@bookmarks_bp.route("/<string:status>", 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) + tag = request.args.get("tag", None) if tag: - filters['tags_proxy__contains'] = tag - query = request.args.get('query', None) + 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} + 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': + if status == "public": # load public bookmarks only - filters['shared'] = True - elif status == 'private': + filters["shared"] = True + elif status == "private": # load private bookmarks only - filters['shared'] = False + filters["shared"] = False else: # no filter: load shared and public bookmarks pass - if status == 'unread': - filters['to_read'] = True + 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 + 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')) + bookmarks = BookmarkController(user_id).read(**filters).order_by(desc("time")) - #tag_contr = BookmarkTagController(user_id) - #tag_contr.read().join(bookmarks).all() + # 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/<int:bookmark_id>', methods=['GET']) + 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/<int:bookmark_id>", 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()) + 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') + 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) + 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/<int:bookmark_id>', methods=['POST']) +@bookmark_bp.route("/create", methods=["POST"]) +@bookmark_bp.route("/edit/<int:bookmark_id>", methods=["POST"]) @login_required def process_form(bookmark_id=None): "Process the creation/edition of bookmarks." @@ -141,116 +160,131 @@ def process_form(bookmark_id=None): tag_contr = BookmarkTagController(current_user.id) if not form.validate(): - return render_template('edit_bookmark.html', form=form) + return render_template("edit_bookmark.html", form=form) - if form.title.data == '': + 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} + 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) + 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)) + 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) + 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_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/<int:bookmark_id>', methods=['GET']) +@bookmark_bp.route("/delete/<int:bookmark_id>", 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_')) + 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']) +@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') + flash(gettext("Bookmarks successfully deleted."), "success") return redirect(redirect_url()) -@bookmark_bp.route('/bookmarklet', methods=['GET', 'POST']) +@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) + 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) + 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() + 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)) + 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} + 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)) + flash(gettext("Bookmark successfully created."), "success") + return redirect(url_for("bookmark.form", bookmark_id=new_bookmark.id)) -@bookmark_bp.route('/import_pinboard', methods=['POST']) +@bookmark_bp.route("/import_pinboard", methods=["POST"]) @login_required def import_pinboard(): - bookmarks = request.files.get('jsonfile', None) + 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') + flash( + gettext( + "%(nb_bookmarks)s bookmarks successfully imported.", + nb_bookmarks=nb_bookmarks, + ), + "success", + ) except Exception as e: - flash(gettext('Error when importing bookmarks.'), 'error') + flash(gettext("Error when importing bookmarks."), "error") return redirect(redirect_url()) -@bookmarks_bp.route('/export', methods=['GET']) +@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' + response.mimetype = "application/json" + response.headers[ + "Content-Disposition" + ] = "attachment; filename=newspipe_bookmarks_export.json" return response diff --git a/newspipe/web/views/category.py b/newspipe/web/views/category.py index 138561dd..1c897058 100644 --- a/newspipe/web/views/category.py +++ b/newspipe/web/views/category.py @@ -5,82 +5,105 @@ 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 web.controllers import ArticleController, FeedController, CategoryController -categories_bp = Blueprint('categories', __name__, url_prefix='/categories') -category_bp = Blueprint('category', __name__, url_prefix='/category') +categories_bp = Blueprint("categories", __name__, url_prefix="/categories") +category_bp = Blueprint("category", __name__, url_prefix="/category") -@categories_bp.route('/', methods=['GET']) +@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()) + 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/<int:category_id>', methods=['GET']) +@category_bp.route("/create", methods=["GET"]) +@category_bp.route("/edit/<int:category_id>", 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()) + 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') + 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)) + return render_template( + "edit_category.html", + action=action, + head_titles=head_titles, + category=category, + form=CategoryForm(obj=category), + ) -@category_bp.route('/delete/<int:category_id>', methods=['GET']) +@category_bp.route("/delete/<int:category_id>", 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') + 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/<int:category_id>', methods=['POST']) +@category_bp.route("/create", methods=["POST"]) +@category_bp.route("/edit/<int:category_id>", 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) + 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)) + return redirect(url_for("category.form", category_id=existing_cats[0].id)) # Edit an existing category - category_attr = {'name': form.name.data} + 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)) + 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') + 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)) + return redirect(url_for("category.form", category_id=new_category.id)) diff --git a/newspipe/web/views/common.py b/newspipe/web/views/common.py index e422fd57..c2d8e2df 100644 --- a/newspipe/web/views/common.py +++ b/newspipe/web/views/common.py @@ -3,13 +3,18 @@ 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 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_role = RoleNeed("admin") +api_role = RoleNeed("api") admin_permission = Permission(admin_role) api_permission = Permission(api_role) @@ -17,21 +22,23 @@ api_permission = Permission(api_role) def scoped_default_handler(): if admin_permission.can(): - role = 'admin' + role = "admin" elif api_permission.can(): - role = 'api' + role = "api" else: - role = 'user' + 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 @@ -40,8 +47,12 @@ def jsonify(func): 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 Response( + json.dumps(result, default=scoped_default_handler()), + mimetype="application/json", + status=status_code, + ) + return wrapper @@ -49,5 +60,4 @@ 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()}) + UserController(user.id).update({"id": user.id}, {"last_seen": datetime.utcnow()}) diff --git a/newspipe/web/views/feed.py b/newspipe/web/views/feed.py index b98a005a..592e3cbf 100644 --- a/newspipe/web/views/feed.py +++ b/newspipe/web/views/feed.py @@ -4,8 +4,15 @@ 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 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 @@ -15,24 +22,30 @@ 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) +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 = Blueprint("feeds", __name__, url_prefix="/feeds") +feed_bp = Blueprint("feed", __name__, url_prefix="/feed") -@feeds_bp.route('/', methods=['GET']) +@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()) + 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): @@ -42,15 +55,19 @@ def feed_view(feed_id=None, user_id=None): if feed.category_id: category = CategoryController(user_id).get(id=feed.category_id) filters = {} - filters['feed_id'] = feed_id + 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) + 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: @@ -65,17 +82,22 @@ def feed_view(feed_id=None, user_id=None): 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('/<int:feed_id>', methods=['GET']) + 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("/<int:feed_id>", methods=["GET"]) @login_required @etag_match def feed(feed_id=None): @@ -83,7 +105,7 @@ def feed(feed_id=None): return feed_view(feed_id, current_user.id) -@feed_bp.route('/public/<int:feed_id>', methods=['GET']) +@feed_bp.route("/public/<int:feed_id>", methods=["GET"]) @etag_match def feed_pub(feed_id=None): """ @@ -92,90 +114,97 @@ def feed_pub(feed_id=None): """ 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 render_template("errors/404.html"), 404 return feed_view(feed_id, None) -@feed_bp.route('/delete/<feed_id>', methods=['GET']) +@feed_bp.route("/delete/<feed_id>", 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')) + flash( + gettext("Feed %(feed_title)s successfully deleted.", feed_title=feed.title), + "success", + ) + return redirect(url_for("home")) -@feed_bp.route('/reset_errors/<int:feed_id>', methods=['GET', 'POST']) +@feed_bp.route("/reset_errors/<int:feed_id>", 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_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']) +@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) + 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})) + 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)) + 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')) + 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') + 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') + 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)) + flash(gettext("Downloading articles for the new feed..."), "info") + return redirect(url_for("feed.form", feed_id=feed.id)) -@feed_bp.route('/update/<action>/<int:feed_id>', methods=['GET', 'POST']) -@feeds_bp.route('/update/<action>', methods=['GET', 'POST']) +@feed_bp.route("/update/<action>/<int:feed_id>", methods=["GET", "POST"]) +@feeds_bp.route("/update/<action>", methods=["GET", "POST"]) @login_required def update(action, feed_id=None): - readed = action == 'read' - filters = {'readed__ne': readed} + readed = action == "read" + filters = {"readed__ne": readed} - nb_days = request.args.get('nb_days', 0, type=int) + nb_days = request.args.get("nb_days", 0, type=int) if nb_days != 0: - filters['date__lt'] = datetime.now() - timedelta(days=nb_days) + 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')) + 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/<int:feed_id>', methods=['GET']) +@feed_bp.route("/create", methods=["GET"]) +@feed_bp.route("/edit/<int:feed_id>", methods=["GET"]) @login_required @etag_match def form(feed_id=None): @@ -185,22 +214,28 @@ def form(feed_id=None): 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) + 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') + 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/<int:feed_id>', methods=['POST']) + 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/<int:feed_id>", methods=["POST"]) @login_required def process_form(feed_id=None): form = AddFeedForm() @@ -208,58 +243,68 @@ def process_form(feed_id=None): form.set_category_choices(CategoryController(current_user.id).read()) if not form.validate(): - return render_template('edit_feed.html', form=form) + 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)) + 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 + 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)) + 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') + 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') + flash(gettext("Downloading articles for the new feed..."), "info") - return redirect(url_for('feed.form', feed_id=new_feed.id)) + return redirect(url_for("feed.form", feed_id=new_feed.id)) -@feeds_bp.route('/inactives', methods=['GET']) +@feeds_bp.route("/inactives", methods=["GET"]) @login_required def inactives(): """ List of inactive feeds. """ - nb_days = int(request.args.get('nb_days', 365)) + 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) + return render_template("inactives.html", inactives=inactives, nb_days=nb_days) -@feed_bp.route('/duplicates/<int:feed_id>', methods=['GET']) +@feed_bp.route("/duplicates/<int:feed_id>", methods=["GET"]) @login_required def duplicates(feed_id): """ @@ -267,40 +312,44 @@ def duplicates(feed_id): """ 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) + 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']) +@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' + 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 + filter["enabled"] = True if not include_private: - filter['private'] = False + filter["private"] = False if not include_exceeded_error_count: - filter['error_count__lt'] = conf.DEFAULT_MAX_ERROR + 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' + 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/newspipe/web/views/home.py b/newspipe/web/views/home.py index dc7a361a..1f51c55c 100644 --- a/newspipe/web/views/home.py +++ b/newspipe/web/views/home.py @@ -2,8 +2,7 @@ import pytz import logging from datetime import datetime -from flask import current_app, render_template, \ - request, flash, url_for, redirect +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 @@ -14,107 +13,128 @@ 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 +from web.controllers import FeedController, ArticleController, CategoryController localize = pytz.utc.localize logger = logging.getLogger(__name__) -@current_app.route('/') +@current_app.route("/") @login_required @etag_match def home(): - return render_template('home.html', cdn=conf.CDN_ADDRESS) + return render_template("home.html", cdn=conf.CDN_ADDRESS) -@current_app.route('/menu') +@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 = {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'] = [] + 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) + 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'] = [] + 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())} + 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') + query = in_dict.get("query") if query: - search_title = in_dict.get('search_title') == 'true' - search_content = in_dict.get('search_content') == 'true' + search_title = in_dict.get("search_title") == "true" + search_content = in_dict.get("search_content") == "true" if search_title: - filters['title__ilike'] = "%%%s%%" % query + filters["title__ilike"] = "%%%s%%" % query if search_content: - filters['content__ilike'] = "%%%s%%" % query + filters["content__ilike"] = "%%%s%%" % query if len(filters) == 0: - filters['title__ilike'] = "%%%s%%" % query + 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 + 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') + 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(): @@ -124,8 +144,8 @@ def get_middle_panel(): return _articles_to_json(articles) -@current_app.route('/getart/<int:article_id>') -@current_app.route('/getart/<int:article_id>/<parse>') +@current_app.route("/getart/<int:article_id>") +@current_app.route("/getart/<int:article_id>/<parse>") @login_required @etag_match @jsonify @@ -134,28 +154,29 @@ def get_article(article_id, parse=False): 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 + 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) + 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']) +@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}) + acontr.update(filters, {"readed": True}) return processed_articles -@current_app.route('/fetch', methods=['GET']) -@current_app.route('/fetch/<int:feed_id>', methods=['GET']) +@current_app.route("/fetch", methods=["GET"]) +@current_app.route("/fetch/<int:feed_id>", methods=["GET"]) @login_required def fetch(feed_id=None): """ @@ -166,6 +187,11 @@ def fetch(feed_id=None): 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") + flash( + gettext( + "The manual retrieving of news is only available " + + "for administrator, on the Heroku platform." + ), + "info", + ) return redirect(redirect_url()) diff --git a/newspipe/web/views/icon.py b/newspipe/web/views/icon.py index 64e54cab..e1de6402 100644 --- a/newspipe/web/views/icon.py +++ b/newspipe/web/views/icon.py @@ -3,13 +3,12 @@ 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 = Blueprint("icon", __name__, url_prefix="/icon") -@icon_bp.route('/', methods=['GET']) +@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} + 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/newspipe/web/views/session_mgmt.py b/newspipe/web/views/session_mgmt.py index 0db76115..809825d3 100644 --- a/newspipe/web/views/session_mgmt.py +++ b/newspipe/web/views/session_mgmt.py @@ -4,14 +4,25 @@ 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 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) +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 @@ -24,9 +35,9 @@ Principal(current_app) 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' +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__) @@ -47,67 +58,77 @@ def on_identity_loaded(sender, identity): @login_manager.user_loader def load_user(user_id): - return UserController(user_id, ignore_context=True).get( - id=user_id, is_active=True) + 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()}) + {"id": current_user.id}, {"last_seen": datetime.utcnow()} + ) + -@current_app.route('/login', methods=['GET', 'POST']) +@current_app.route("/login", methods=["GET", "POST"]) def login(): if current_user.is_authenticated: - return redirect(url_for('home')) + 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) + return form.redirect("home") + return render_template("login.html", form=form) -@current_app.route('/logout') +@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'): + 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')) + return redirect(url_for("login")) -@current_app.route('/signup', methods=['GET', 'POST']) +@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')) + flash(gettext("Self-registration is disabled."), "warning") + return redirect(url_for("home")) if current_user.is_authenticated: - return redirect(url_for('home')) + 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)) + 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) + 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/newspipe/web/views/user.py b/newspipe/web/views/user.py index 24b73a60..10974947 100644 --- a/newspipe/web/views/user.py +++ b/newspipe/web/views/user.py @@ -1,8 +1,7 @@ import string import random from datetime import datetime, timedelta -from flask import (Blueprint, g, render_template, redirect, - flash, url_for, request) +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 @@ -12,39 +11,47 @@ 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.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') +users_bp = Blueprint("users", __name__, url_prefix="/users") +user_bp = Blueprint("user", __name__, url_prefix="/user") -@user_bp.route('/<string:nickname>', methods=['GET']) +@user_bp.route("/<string:nickname>", methods=["GET"]) def profile_public(nickname=None): """ Display the public profile of the user. """ - category_id = int(request.args.get('category_id', 0)) + 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')) + flash(gettext("You must set your profile to public."), "info") + return redirect(url_for("user.profile")) filters = {} - filters['private'] = False + filters["private"] = False if category_id: - filters['category_id'] = 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) + return render_template( + "profile_public.html", user=user, feeds=feeds, selected_category_id=category_id + ) -@user_bp.route('/<string:nickname>/stream', defaults={'per_page': '25'}, methods=['GET']) +@user_bp.route( + "/<string:nickname>/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). @@ -53,76 +60,80 @@ def user_stream(per_page, nickname=None): 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')) + 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_id = int(request.args.get("category_id", 0)) category = CategoryController().read(id=category_id).first() # Load the public feeds filters = {} - filters['private'] = False + filters["private"] = False if category_id: - filters['category_id'] = 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] + filters["feed_id__in"] = [feed.id for feed in feeds] if category: - filters['category_id'] = category_id + 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']) + 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): + if request.method == "POST": + if None != request.files.get("opmlfile", None): # Import an OPML file - data = request.files.get('opmlfile', None) + data = request.files.get("opmlfile", None) if not misc_utils.allowed_file(data.filename): - flash(gettext('File not allowed.'), 'danger') + 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') + 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): + 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) + data = request.files.get("jsonfile", None) if not misc_utils.allowed_file(data.filename): - flash(gettext('File not allowed.'), 'danger') + flash(gettext("File not allowed."), "danger") else: try: nb = import_json(current_user.nickname, data.read()) - flash(gettext('Account imported.'), "success") + flash(gettext("Account imported."), "success") except: - flash(gettext("Impossible to import the account."), - "danger") + flash(gettext("Impossible to import the account."), "danger") else: - flash(gettext('File not allowed.'), 'danger') + flash(gettext("File not allowed."), "danger") nb_feeds = FeedController(current_user.id).read().count() art_contr = ArticleController(current_user.id) @@ -130,14 +141,18 @@ def management(): 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']) + 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(): """ @@ -147,44 +162,54 @@ def profile(): user = user_contr.get(id=current_user.id) form = ProfileForm() - if request.method == 'POST': + 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}) + 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') + 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')) + 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) + return render_template("profile.html", user=user, form=form) - if request.method == 'GET': + if request.method == "GET": form = ProfileForm(obj=user) - return render_template('profile.html', user=user, form=form) + return render_template("profile.html", user=user, form=form) -@user_bp.route('/delete_account', methods=['GET']) +@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')) + flash(gettext("Your account has been deleted."), "success") + return redirect(url_for("login")) -@user_bp.route('/confirm_account/<string:token>', methods=['GET']) +@user_bp.route("/confirm_account/<string:token>", methods=["GET"]) def confirm_account(token=None): """ Confirm the account of a user. @@ -196,8 +221,8 @@ def confirm_account(token=None): 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') + 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')) + flash(gettext("Impossible to confirm this account."), "danger") + return redirect(url_for("login")) diff --git a/newspipe/web/views/views.py b/newspipe/web/views/views.py index d587bd09..1fde12c7 100644 --- a/newspipe/web/views/views.py +++ b/newspipe/web/views/views.py @@ -2,8 +2,7 @@ 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 import request, render_template, flash, url_for, redirect, current_app from flask_babel import gettext from sqlalchemy import desc @@ -20,26 +19,26 @@ logger = logging.getLogger(__name__) def authentication_required(error): if API_ROOT in request.url: return error - flash(gettext('Authentication required.'), 'info') - return redirect(url_for('login')) + 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')) + 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 + return render_template("errors/404.html"), 404 @current_app.errorhandler(500) def internal_server_error(error): - return render_template('errors/500.html'), 500 + return render_template("errors/500.html"), 500 @current_app.errorhandler(AssertionError) @@ -47,7 +46,7 @@ def handle_sqlalchemy_assertion_error(error): return error.args[0], 400 -@current_app.route('/popular', methods=['GET']) +@current_app.route("/popular", methods=["GET"]) @etag_match def popular(): """ @@ -57,11 +56,12 @@ def popular(): # 'not_created_before' # ie: not_added_before = date_last_added_feed - nb_days try: - nb_days = int(request.args.get('nb_days', 365)) + 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() + 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: @@ -69,25 +69,27 @@ def popular(): 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 + 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) + 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']) +@current_app.route("/about", methods=["GET"]) @etag_match def about(): - return render_template('about.html', contact=ADMIN_EMAIL) + return render_template("about.html", contact=ADMIN_EMAIL) -@current_app.route('/about/more', methods=['GET']) + +@current_app.route("/about/more", methods=["GET"]) @etag_match def about_more(): - return render_template('about_more.html', - newspipe_version=__version__.split()[1], - registration=[conf.SELF_REGISTRATION and 'Open' or 'Closed'][0], - python_version="{}.{}.{}".format(*sys.version_info[:3]), - nb_users=UserController().read().count()) + return render_template( + "about_more.html", + newspipe_version=__version__.split()[1], + registration=[conf.SELF_REGISTRATION and "Open" or "Closed"][0], + python_version="{}.{}.{}".format(*sys.version_info[:3]), + nb_users=UserController().read().count(), + ) |