aboutsummaryrefslogtreecommitdiff
path: root/src/web/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'src/web/controllers')
-rw-r--r--src/web/controllers/__init__.py8
-rw-r--r--src/web/controllers/abstract.py116
-rw-r--r--src/web/controllers/article.py73
-rw-r--r--src/web/controllers/feed.py70
-rw-r--r--src/web/controllers/icon.py23
-rw-r--r--src/web/controllers/user.py7
6 files changed, 297 insertions, 0 deletions
diff --git a/src/web/controllers/__init__.py b/src/web/controllers/__init__.py
new file mode 100644
index 00000000..ad77fa1d
--- /dev/null
+++ b/src/web/controllers/__init__.py
@@ -0,0 +1,8 @@
+from .feed import FeedController
+from .article import ArticleController
+from .user import UserController
+from .icon import IconController
+
+
+__all__ = ['FeedController', 'ArticleController', 'UserController',
+ 'IconController']
diff --git a/src/web/controllers/abstract.py b/src/web/controllers/abstract.py
new file mode 100644
index 00000000..f33d241e
--- /dev/null
+++ b/src/web/controllers/abstract.py
@@ -0,0 +1,116 @@
+import logging
+from flask import g
+from bootstrap import db
+from sqlalchemy import or_
+from werkzeug.exceptions import Forbidden, NotFound
+
+logger = logging.getLogger(__name__)
+
+
+class AbstractController(object):
+ _db_cls = None # reference to the database class
+ _user_id_key = 'user_id'
+
+ def __init__(self, user_id=None):
+ """User id is a right management mechanism that should be used to
+ filter objects in database on their denormalized "user_id" field
+ (or "id" field for users).
+ Should no user_id be provided, the Controller won't apply any filter
+ allowing for a kind of "super user" mode.
+ """
+ self.user_id = user_id
+ try:
+ if self.user_id is not None \
+ and self.user_id != g.user.id and not g.user.is_admin():
+ self.user_id = g.user.id
+ except RuntimeError: # passing on out of context errors
+ pass
+
+ def _to_filters(self, **filters):
+ """
+ Will translate filters to sqlalchemy filter.
+ This method will also apply user_id restriction if available.
+
+ each parameters of the function is treated as an equality unless the
+ name of the parameter ends with either "__gt", "__lt", "__ge", "__le",
+ "__ne", "__in" ir "__like".
+ """
+ db_filters = set()
+ for key, value in filters.items():
+ if key == '__or__':
+ db_filters.add(or_(*self._to_filters(**value)))
+ elif key.endswith('__gt'):
+ db_filters.add(getattr(self._db_cls, key[:-4]) > value)
+ elif key.endswith('__lt'):
+ db_filters.add(getattr(self._db_cls, key[:-4]) < value)
+ elif key.endswith('__ge'):
+ db_filters.add(getattr(self._db_cls, key[:-4]) >= value)
+ elif key.endswith('__le'):
+ db_filters.add(getattr(self._db_cls, key[:-4]) <= value)
+ elif key.endswith('__ne'):
+ db_filters.add(getattr(self._db_cls, key[:-4]) != value)
+ elif key.endswith('__in'):
+ db_filters.add(getattr(self._db_cls, key[:-4]).in_(value))
+ elif key.endswith('__like'):
+ db_filters.add(getattr(self._db_cls, key[:-6]).like(value))
+ 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)
+ return db_filters
+
+ def _get(self, **filters):
+ """ Will add the current user id if that one is not none (in which case
+ the decision has been made in the code that the query shouldn't be user
+ dependant) 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:
+ filters[self._user_id_key] = self.user_id
+ return self._db_cls.query.filter(*self._to_filters(**filters))
+
+ def get(self, **filters):
+ """Will return one single objects corresponding to filters"""
+ 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)})
+ if not obj:
+ raise NotFound({'message': 'No %r (%r)'
+ % (self._db_cls.__class__.__name__, filters)})
+ return obj
+
+ def create(self, **attrs):
+ assert self._user_id_key is None or self._user_id_key in attrs \
+ or self.user_id is not None, \
+ "You must provide user_id one way or another"
+
+ if self._user_id_key is not None and self._user_id_key not in attrs:
+ attrs[self._user_id_key] = self.user_id
+ obj = self._db_cls(**attrs)
+ db.session.add(obj)
+ db.session.commit()
+ return obj
+
+ def read(self, **filters):
+ return self._get(**filters)
+
+ def update(self, filters, attrs):
+ result = self._get(**filters).update(attrs, synchronize_session=False)
+ db.session.commit()
+ return result
+
+ def delete(self, obj_id):
+ obj = self.get(id=obj_id)
+ db.session.delete(obj)
+ db.session.commit()
+ return obj
+
+ def _has_right_on(self, obj):
+ # 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
diff --git a/src/web/controllers/article.py b/src/web/controllers/article.py
new file mode 100644
index 00000000..8b6926b7
--- /dev/null
+++ b/src/web/controllers/article.py
@@ -0,0 +1,73 @@
+import re
+import logging
+from sqlalchemy import func
+
+from bootstrap import db
+from .abstract import AbstractController
+from web.controllers import FeedController
+from web.models import Article
+
+logger = logging.getLogger(__name__)
+
+
+class ArticleController(AbstractController):
+ _db_cls = Article
+
+ def get(self, **filters):
+ article = super(ArticleController, self).get(**filters)
+ if not article.readed:
+ self.update({'id': article.id}, {'readed': True})
+ return article
+
+ def challenge(self, ids):
+ """Will return each id that wasn't found in the database."""
+ for id_ in ids:
+ if self.read(**id_).first():
+ continue
+ yield id_
+
+ def count_by_feed(self, **filters):
+ if self.user_id:
+ filters['user_id'] = self.user_id
+ return dict(db.session.query(Article.feed_id, func.count(Article.id))
+ .filter(*self._to_filters(**filters))
+ .group_by(Article.feed_id).all())
+
+ 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())
+
+ def create(self, **attrs):
+ # handling special denorm for article rights
+ assert 'feed_id' in attrs
+ 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
+ attrs['user_id'] = feed.user_id
+
+ # handling feed's filters
+ for filter_ in feed.filters or []:
+ match = False
+ if filter_.get('type') == 'regex':
+ match = re.match(filter_['pattern'], attrs.get('title', ''))
+ elif filter_.get('type') == 'simple match':
+ match = filter_['pattern'] in attrs.get('title', '')
+ take_action = match and filter_.get('action on') == 'match' \
+ or not match and filter_.get('action on') == 'no match'
+
+ if not take_action:
+ continue
+
+ if filter_.get('action') == 'mark as read':
+ attrs['readed'] = True
+ logger.warn("article %s will be created as read",
+ attrs['link'])
+ elif filter_.get('action') == 'mark as favorite':
+ attrs['like'] = True
+ logger.warn("article %s will be created as liked",
+ attrs['link'])
+
+ return super().create(**attrs)
diff --git a/src/web/controllers/feed.py b/src/web/controllers/feed.py
new file mode 100644
index 00000000..15be8663
--- /dev/null
+++ b/src/web/controllers/feed.py
@@ -0,0 +1,70 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# jarr - A Web based news aggregator.
+# Copyright (C) 2010-2015 Cédric Bonhomme - https://www.JARR-aggregator.org
+#
+# For more information : https://github.com/JARR-aggregator/JARR/
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+from datetime import datetime, timedelta
+
+import conf
+from .abstract import AbstractController
+from .icon import IconController
+from web.models import Feed
+
+logger = logging.getLogger(__name__)
+DEFAULT_LIMIT = 5
+DEFAULT_REFRESH_RATE = 60
+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)
+ .order_by('last_retrieved')
+ .limit(limit)]
+
+ def list_fetchable(self, max_error=DEFAULT_MAX_ERROR, limit=DEFAULT_LIMIT,
+ refresh_rate=DEFAULT_REFRESH_RATE):
+ now = datetime.now()
+ max_last = now - timedelta(minutes=refresh_rate)
+ feeds = self.list_late(max_last, max_error, limit)
+ if feeds:
+ self.update({'id__in': [feed.id for feed in feeds]},
+ {'last_retrieved': now})
+ return feeds
+
+ def _ensure_icon(self, attrs):
+ 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']})
+
+ def create(self, **attrs):
+ self._ensure_icon(attrs)
+ return super().create(**attrs)
+
+ def update(self, filters, attrs):
+ self._ensure_icon(attrs)
+ return super().update(filters, attrs)
diff --git a/src/web/controllers/icon.py b/src/web/controllers/icon.py
new file mode 100644
index 00000000..07c4a4ef
--- /dev/null
+++ b/src/web/controllers/icon.py
@@ -0,0 +1,23 @@
+import base64
+import requests
+from web.models import Icon
+from .abstract import AbstractController
+
+
+class IconController(AbstractController):
+ _db_cls = Icon
+ _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')})
+ return attrs
+
+ def create(self, **attrs):
+ return super().create(**self._build_from_url(attrs))
+
+ def update(self, filters, attrs):
+ return super().update(filters, self._build_from_url(attrs))
diff --git a/src/web/controllers/user.py b/src/web/controllers/user.py
new file mode 100644
index 00000000..3f96b185
--- /dev/null
+++ b/src/web/controllers/user.py
@@ -0,0 +1,7 @@
+from .abstract import AbstractController
+from web.models import User
+
+
+class UserController(AbstractController):
+ _db_cls = User
+ _user_id_key = 'id'
bgstack15