From 62b3afeeedfe054345f86093e2d243e956c1e3c9 Mon Sep 17 00:00:00 2001 From: Cédric Bonhomme Date: Wed, 26 Feb 2020 11:27:31 +0100 Subject: The project is now using Poetry. --- src/web/controllers/__init__.py | 12 --- src/web/controllers/abstract.py | 161 ---------------------------------------- src/web/controllers/article.py | 87 ---------------------- src/web/controllers/bookmark.py | 32 -------- src/web/controllers/category.py | 12 --- src/web/controllers/feed.py | 98 ------------------------ src/web/controllers/icon.py | 23 ------ src/web/controllers/tag.py | 22 ------ src/web/controllers/user.py | 28 ------- 9 files changed, 475 deletions(-) delete mode 100644 src/web/controllers/__init__.py delete mode 100644 src/web/controllers/abstract.py delete mode 100644 src/web/controllers/article.py delete mode 100644 src/web/controllers/bookmark.py delete mode 100644 src/web/controllers/category.py delete mode 100644 src/web/controllers/feed.py delete mode 100644 src/web/controllers/icon.py delete mode 100644 src/web/controllers/tag.py delete mode 100644 src/web/controllers/user.py (limited to 'src/web/controllers') diff --git a/src/web/controllers/__init__.py b/src/web/controllers/__init__.py deleted file mode 100644 index 5fbc2619..00000000 --- a/src/web/controllers/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .feed import FeedController -from .category import CategoryController -from .article import ArticleController -from .user import UserController -from .icon import IconController -from .bookmark import BookmarkController -from .tag import BookmarkTagController - - -__all__ = ['FeedController', 'CategoryController', 'ArticleController', - 'UserController', 'IconController', 'BookmarkController', - 'BookmarkTagController'] diff --git a/src/web/controllers/abstract.py b/src/web/controllers/abstract.py deleted file mode 100644 index 764ff305..00000000 --- a/src/web/controllers/abstract.py +++ /dev/null @@ -1,161 +0,0 @@ -import logging -import dateutil.parser -from bootstrap import db -from datetime import datetime -from collections import defaultdict -from sqlalchemy import or_, func -from werkzeug.exceptions import Forbidden, NotFound - -logger = logging.getLogger(__name__) - - -class AbstractController: - _db_cls = None # reference to the database class - _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 - 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. - """ - try: - self.user_id = int(user_id) - except TypeError: - self.user_id = user_id - - 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('__contains'): - db_filters.add(getattr(self._db_cls, key[:-10]).contains(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 - 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: - 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 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" - - obj = self._db_cls(**attrs) - db.session.add(obj) - db.session.flush() - db.session.commit() - return obj - - def read(self, **filters): - return self._get(**filters) - - def update(self, filters, attrs, return_objs=False, commit=True): - assert attrs, "attributes to update must not be empty" - result = self._get(**filters).update(attrs, synchronize_session=False) - if commit: - db.session.flush() - db.session.commit() - if return_objs: - return self._get(**filters) - return result - - def delete(self, obj_id): - obj = self.get(id=obj_id) - db.session.delete(obj) - try: - db.session.commit() - except Exception as e: - db.session.rollback() - 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 - - 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()) - - @classmethod - def _get_attrs_desc(cls, role, right=None): - result = defaultdict(dict) - 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))() - 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 - except NotImplementedError: - if db_col.default: - 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) - elif db_col.default: - result[column]['default'] = db_col.default.arg - return result diff --git a/src/web/controllers/article.py b/src/web/controllers/article.py deleted file mode 100644 index d7058229..00000000 --- a/src/web/controllers/article.py +++ /dev/null @@ -1,87 +0,0 @@ -import re -import logging -import sqlalchemy -from sqlalchemy import func -from collections import Counter - -from bootstrap import db -from .abstract import AbstractController -from lib.article_utils import process_filters -from web.controllers import CategoryController, FeedController -from web.models import Article - -logger = logging.getLogger(__name__) - - -class ArticleController(AbstractController): - _db_cls = 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_category(self, **filters): - return self._count_by(Article.category_id, filters) - - def count_by_feed(self, **filters): - 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()) - - 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 - - skipped, read, liked = process_filters(feed.filters, attrs) - if skipped: - return None - article = super().create(**attrs) - 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']) - 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 - return super().update(filters, attrs) - - def get_history(self, year=None, month=None): - """ - Sort articles by year and month. - """ - articles_counter = Counter() - articles = self.read() - if year is not None: - articles = articles.filter( - sqlalchemy.extract('year', Article.date) == year) - if month is not None: - articles = articles.filter( - sqlalchemy.extract('month', Article.date) == month) - for article in articles.all(): - if year is not None: - articles_counter[article.date.month] += 1 - else: - articles_counter[article.date.year] += 1 - 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()) diff --git a/src/web/controllers/bookmark.py b/src/web/controllers/bookmark.py deleted file mode 100644 index b5413243..00000000 --- a/src/web/controllers/bookmark.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging -import itertools -from datetime import datetime, timedelta - -from bootstrap import db -from web.models import Bookmark -from .abstract import AbstractController -from .tag import BookmarkTagController - -logger = logging.getLogger(__name__) - - -class BookmarkController(AbstractController): - _db_cls = Bookmark - - def count_by_href(self, **filters): - return self._count_by(Bookmark.href, filters) - - def update(self, filters, attrs): - BookmarkTagController(self.user_id) \ - .read(**{'bookmark_id': filters["id"]}) \ - .delete() - - 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'] - return super().update(filters, attrs) diff --git a/src/web/controllers/category.py b/src/web/controllers/category.py deleted file mode 100644 index fef5ca81..00000000 --- a/src/web/controllers/category.py +++ /dev/null @@ -1,12 +0,0 @@ -from .abstract import AbstractController -from web.models import Category -from .feed import FeedController - - -class CategoryController(AbstractController): - _db_cls = Category - - def delete(self, obj_id): - FeedController(self.user_id).update({'category_id': obj_id}, - {'category_id': None}) - return super().delete(obj_id) diff --git a/src/web/controllers/feed.py b/src/web/controllers/feed.py deleted file mode 100644 index d75cd994..00000000 --- a/src/web/controllers/feed.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging -import itertools -from datetime import datetime, timedelta - -import conf -from .abstract import AbstractController -from .icon import IconController -from web.models import User, Feed -from lib.utils import clear_string - -logger = logging.getLogger(__name__) -DEFAULT_LIMIT = 5 -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_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}) - return feeds - - def get_duplicates(self, feed_id): - """ - Compare a list of documents by pair. - Pairs of duplicates are sorted by "retrieved date". - """ - feed = self.get(id=feed_id) - 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 pair[0].retrieved_date < pair[1].retrieved_date: - duplicates.append((pair[0], pair[1])) - else: - duplicates.append((pair[1], pair[0])) - return feed, duplicates - - def get_inactives(self, nb_days): - today = datetime.now() - inactives = [] - for feed in self.read(): - try: - last_post = feed.articles[0].date - except IndexError: - continue - except Exception as e: - logger.exception(e) - continue - elapsed = today - last_post - if elapsed > timedelta(days=nb_days): - inactives.append((feed, elapsed)) - inactives.sort(key=lambda tup: tup[1], reverse=True) - return inactives - - def count_by_category(self, **filters): - return self._count_by(Feed.category_id, filters) - - def count_by_link(self, **filters): - return self._count_by(Feed.link, filters) - - 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): - 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: - art_contr = ArticleController(self.user_id) - for feed in self.read(**filters): - art_contr.update({'feed_id': feed.id}, - {'category_id': attrs['category_id']}) - return super().update(filters, attrs) diff --git a/src/web/controllers/icon.py b/src/web/controllers/icon.py deleted file mode 100644 index 07c4a4ef..00000000 --- a/src/web/controllers/icon.py +++ /dev/null @@ -1,23 +0,0 @@ -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/tag.py b/src/web/controllers/tag.py deleted file mode 100644 index 35fd5613..00000000 --- a/src/web/controllers/tag.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging -import itertools -from datetime import datetime, timedelta - -from bootstrap import db -from .abstract import AbstractController -from web.models.tag import BookmarkTag - -logger = logging.getLogger(__name__) - - -class BookmarkTagController(AbstractController): - _db_cls = BookmarkTag - - def count_by_href(self, **filters): - return self._count_by(BookmarkTag.text, filters) - - def create(self, **attrs): - return super().create(**attrs) - - def update(self, filters, attrs): - return super().update(filters, attrs) diff --git a/src/web/controllers/user.py b/src/web/controllers/user.py deleted file mode 100644 index 6ab04d44..00000000 --- a/src/web/controllers/user.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -from werkzeug.security import generate_password_hash, check_password_hash -from .abstract import AbstractController -from web.models import User - -logger = logging.getLogger(__name__) - - -class UserController(AbstractController): - _db_cls = User - _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'] - - def check_password(self, user, password): - return check_password_hash(user.pwdhash, password) - - def create(self, **attrs): - self._handle_password(attrs) - return super().create(**attrs) - - def update(self, filters, attrs): - self._handle_password(attrs) - return super().update(filters, attrs) -- cgit