diff options
author | Cédric Bonhomme <cedric@cedricbonhomme.org> | 2017-02-12 16:41:09 +0100 |
---|---|---|
committer | Cédric Bonhomme <cedric@cedricbonhomme.org> | 2017-02-12 16:41:09 +0100 |
commit | 8bd82176356540b88eece24b96d7eb6a2260d718 (patch) | |
tree | 619caf62c174d09448e3da9c873b2df0d26dd5c2 | |
parent | New https certificate (diff) | |
download | newspipe-8bd82176356540b88eece24b96d7eb6a2260d718.tar.gz newspipe-8bd82176356540b88eece24b96d7eb6a2260d718.tar.bz2 newspipe-8bd82176356540b88eece24b96d7eb6a2260d718.zip |
prototypefor bookmarks
-rw-r--r-- | requirements.txt | 14 | ||||
-rwxr-xr-x | src/runserver.py | 2 | ||||
-rw-r--r-- | src/web/controllers/__init__.py | 3 | ||||
-rw-r--r-- | src/web/controllers/abstract.py | 12 | ||||
-rw-r--r-- | src/web/controllers/bookmark.py | 20 | ||||
-rw-r--r-- | src/web/forms.py | 10 | ||||
-rw-r--r-- | src/web/models/__init__.py | 6 | ||||
-rw-r--r-- | src/web/models/article.py | 8 | ||||
-rw-r--r-- | src/web/models/bookmark.py | 67 | ||||
-rw-r--r-- | src/web/models/tag.py | 40 | ||||
-rw-r--r-- | src/web/models/user.py | 4 | ||||
-rw-r--r-- | src/web/templates/bookmarks.html | 15 | ||||
-rw-r--r-- | src/web/templates/edit_bookmark.html | 58 | ||||
-rw-r--r-- | src/web/views/__init__.py | 3 | ||||
-rw-r--r-- | src/web/views/bookmark.py | 80 | ||||
-rw-r--r-- | src/web/views/category.py | 2 |
16 files changed, 317 insertions, 27 deletions
diff --git a/requirements.txt b/requirements.txt index 29453bfc..a6783af7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ -aiohttp==1.1.3 -requests==2.11.1 +aiohttp==1.2.0 +requests==2.12.4 requests-futures==0.9.7 feedparser==5.2.1 beautifulsoup4==4.5.1 -lxml==3.6.4 +lxml==3.7.1 opml==0.5 psycopg2==2.6.2 -SQLAlchemy==1.1.3 -alembic==0.8.8 -Flask==0.11.1 +SQLAlchemy==1.1.4 +alembic==0.8.9 +Flask==0.12 Flask-SQLAlchemy==2.1 Flask-Login==0.4.0 Flask-Principal==0.4.0 @@ -17,7 +17,7 @@ Flask-RESTful==0.3.5 Flask-Restless==0.17.0 Flask-Babel==0.11.1 Flask-SSLify==0.1.5 -Flask-Migrate==2.0.0 +Flask-Migrate==2.0.2 Flask-Script==2.0.5 WTForms==2.1 python-postmark==0.4.10 diff --git a/src/runserver.py b/src/runserver.py index 1099b483..ff4fe444 100755 --- a/src/runserver.py +++ b/src/runserver.py @@ -56,6 +56,8 @@ with application.app_context(): application.register_blueprint(views.admin_bp) application.register_blueprint(views.users_bp) application.register_blueprint(views.user_bp) + application.register_blueprint(views.bookmarks_bp) + application.register_blueprint(views.bookmark_bp) if __name__ == '__main__': diff --git a/src/web/controllers/__init__.py b/src/web/controllers/__init__.py index a1b89ea8..dd497e9a 100644 --- a/src/web/controllers/__init__.py +++ b/src/web/controllers/__init__.py @@ -3,7 +3,8 @@ from .category import CategoryController from .article import ArticleController from .user import UserController from .icon import IconController +from .bookmark import BookmarkController __all__ = ['FeedController', 'CategoryController', 'ArticleController', - 'UserController', 'IconController'] + 'UserController', 'IconController', 'BookmarkController'] diff --git a/src/web/controllers/abstract.py b/src/web/controllers/abstract.py index 3c91e08a..8563d5f2 100644 --- a/src/web/controllers/abstract.py +++ b/src/web/controllers/abstract.py @@ -98,14 +98,14 @@ class AbstractController: def read(self, **filters): return self._get(**filters) - def update(self, filters, attrs): + def update(self, filters, attrs, return_objs=False, commit=True): assert attrs, "attributes to update must not be empty" - result = None - try: - result = self._get(**filters).update(attrs, synchronize_session=False) + result = self._get(**filters).update(attrs, synchronize_session=False) + if commit: + db.session.flush() db.session.commit() - except Exception as e: - db.session.rollback() + if return_objs: + return self._get(**filters) return result def delete(self, obj_id): diff --git a/src/web/controllers/bookmark.py b/src/web/controllers/bookmark.py new file mode 100644 index 00000000..cf7815a8 --- /dev/null +++ b/src/web/controllers/bookmark.py @@ -0,0 +1,20 @@ +import logging +import itertools +from datetime import datetime, timedelta + +from bootstrap import db +from .abstract import AbstractController +from web.models import Bookmark + +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, *args, **kwargs): + self.tags = attrs['tags'] + return super().update(filters, attrs, *args, **kwargs) diff --git a/src/web/forms.py b/src/web/forms.py index 8088f27b..5957ceef 100644 --- a/src/web/forms.py +++ b/src/web/forms.py @@ -204,6 +204,16 @@ class CategoryForm(Form): submit = SubmitField(lazy_gettext("Save")) +class BookmarkForm(Form): + href = TextField(lazy_gettext("URL")) + description = TextField(lazy_gettext("Description")) + extended = TextField(lazy_gettext("Description long")) + tags = TextField(lazy_gettext("Tags")) + to_read = BooleanField(lazy_gettext("To read"), default=False) + shared = BooleanField(lazy_gettext("Shared"), default=False) + submit = SubmitField(lazy_gettext("Save")) + + class InformationMessageForm(Form): subject = TextField(lazy_gettext("Subject"), [validators.Required(lazy_gettext("Please enter a subject."))]) diff --git a/src/web/models/__init__.py b/src/web/models/__init__.py index 1fc0c3e0..c8fc1aef 100644 --- a/src/web/models/__init__.py +++ b/src/web/models/__init__.py @@ -33,8 +33,12 @@ from .article import Article from .icon import Icon from .category import Category from .tag import Tag +from .tag import BookmarkTag +from .tag import ArticleTag +from .bookmark import Bookmark -__all__ = ['Feed', 'Role', 'User', 'Article', 'Icon', 'Category', 'Tag'] +__all__ = ['Feed', 'Role', 'User', 'Article', 'Icon', 'Category', 'Tag', + 'Bookmark', 'ArticleTag', 'BookmarkTag'] import os diff --git a/src/web/models/article.py b/src/web/models/article.py index 87ad1a9a..923d6869 100644 --- a/src/web/models/article.py +++ b/src/web/models/article.py @@ -52,10 +52,10 @@ class Article(db.Model, RightMixin): category_id = db.Column(db.Integer(), db.ForeignKey('category.id')) # relationships - tag_objs = db.relationship('Tag', back_populates='article', - cascade='all,delete-orphan', - lazy=False, - foreign_keys='[Tag.article_id]') + 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') # index diff --git a/src/web/models/bookmark.py b/src/web/models/bookmark.py new file mode 100644 index 00000000..a796cffa --- /dev/null +++ b/src/web/models/bookmark.py @@ -0,0 +1,67 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Newspipe - A Web based news aggregator. +# Copyright (C) 2010-2016 Cédric Bonhomme - https://www.cedricbonhomme.org +# +# For more information : https://github.com/newspipe/newspipe +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +__author__ = "Cedric Bonhomme" +__version__ = "$Revision: 0.1 $" +__date__ = "$Date: 2016/12/07 $" +__revision__ = "$Date: 2016/12/07 $" +__copyright__ = "Copyright (c) Cedric Bonhomme" +__license__ = "GPLv3" + +from bootstrap import db +from datetime import datetime +from sqlalchemy.orm import validates +from sqlalchemy.ext.associationproxy import association_proxy + +from web.models.right_mixin import RightMixin + + +class Bookmark(db.Model, RightMixin): + """ + Represent a bookmark. + """ + id = db.Column(db.Integer(), primary_key=True) + href = db.Column(db.String(), default="") + description = db.Column(db.String(), default="FR") + extended = db.Column(db.String()) + 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')) + + # relationships + tag_objs = db.relationship('BookmarkTag', back_populates='bookmark', + cascade='all,delete-orphan', + lazy=False, + foreign_keys='[BookmarkTag.bookmark_id]') + tags = association_proxy('tag_objs', 'text') + + + @validates('description') + def validates_title(self, key, value): + return str(value).strip() + + @validates('extended') + def validates_description(self, key, value): + return str(value).strip() + + def __repr__(self): + return '<Bookmark %r>' % (self.href) diff --git a/src/web/models/tag.py b/src/web/models/tag.py index 8d7fe4d4..d43ce300 100644 --- a/src/web/models/tag.py +++ b/src/web/models/tag.py @@ -8,15 +8,43 @@ from bootstrap import db class Tag(db.Model): - text = Column(String, primary_key=True, unique=False) + id = db.Column(db.Integer(), primary_key=True) + text = Column(String, unique=False) + obj = db.Column(db.String, default='article') + user_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=False) - # foreign keys - article_id = Column(Integer, ForeignKey('article.id', ondelete='CASCADE'), - primary_key=True) + __mapper_args__ = { + 'polymorphic_on': obj + } + + + def __init__(self, text): + self.text = text + + +class ArticleTag(Tag): + __mapper_args__ = { + 'polymorphic_identity': 'article' + } + id = db.Column(db.Integer, db.ForeignKey('tag.id', ondelete='CASCADE'), + primary_key=True) # relationships + article_id = Column(Integer, ForeignKey('article.id', ondelete='CASCADE'), + nullable=False) article = relationship('Article', back_populates='tag_objs', foreign_keys=[article_id]) - def __init__(self, text): - self.text = text + +class BookmarkTag(Tag): + __mapper_args__ = { + 'polymorphic_identity': 'bookmark' + } + id = db.Column(db.Integer, db.ForeignKey('tag.id', ondelete='CASCADE'), + primary_key=True) + + # relationships + bookmark_id = Column(Integer, ForeignKey('bookmark.id', ondelete='CASCADE'), + nullable=False) + bookmark = relationship('Bookmark', back_populates='tag_objs', + foreign_keys=[bookmark_id]) diff --git a/src/web/models/user.py b/src/web/models/user.py index 460958e0..d2b3c52a 100644 --- a/src/web/models/user.py +++ b/src/web/models/user.py @@ -38,6 +38,7 @@ from bootstrap import db from web.models.right_mixin import RightMixin from web.models.category import Category from web.models.feed import Feed +from web.models.bookmark import Bookmark class User(db.Model, UserMixin, RightMixin): @@ -71,6 +72,9 @@ class User(db.Model, UserMixin, RightMixin): feeds = db.relationship('Feed', backref='user', cascade='all, delete-orphan', foreign_keys=[Feed.user_id]) + bookmarks = db.relationship('Bookmark', backref='user', + cascade='all, delete-orphan', + foreign_keys=[Bookmark.user_id]) @staticmethod def _fields_base_write(): diff --git a/src/web/templates/bookmarks.html b/src/web/templates/bookmarks.html new file mode 100644 index 00000000..9e6f0ab1 --- /dev/null +++ b/src/web/templates/bookmarks.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% block content %} +<div class="container"> + <h1>{{ _('Bookmarks') }}</h1> + <ul class="list-group"> + {% for bookmark in bookmarks %} + <li class="list-group-item"> + <a href="{{ bookmark.href }}">{{ bookmark.href }}</a> + <a href="{{ bookmark.href }}">{{ ", ".join(bookmark.tags) }}</a> + <a href="{{ url_for('bookmark.form', bookmark_id=bookmark.id) }}">edit</a> + </li> + {% endfor %} + </ul> +</div><!-- /.container --> +{% endblock %} diff --git a/src/web/templates/edit_bookmark.html b/src/web/templates/edit_bookmark.html new file mode 100644 index 00000000..8dc0cba3 --- /dev/null +++ b/src/web/templates/edit_bookmark.html @@ -0,0 +1,58 @@ +{% extends "layout.html" %} +{% block content %} +<div class="container"> + <div class="well"> + <h3>{{ action }}</h3> + <form action="" method="post" name="save" class="form-horizontal"> + {{ form.hidden_tag() }} + <div class="form-group"> + <label for="{{ form.href.id }}" class="col-sm-3 control-label">{{ form.href.label }}</label> + <div class="col-sm-9"> + {{ form.href(class_="form-control", size="100%") }} + </div> + {% for error in form.href.errors %} <span style="color: red;">{{ error }}<br /></span>{% endfor %} + </div> + + <div class="form-group"> + <label for="{{ form.description.id }}" class="col-sm-3 control-label">{{ form.description.label }}</label> + <div class="col-sm-9"> + {{ form.description(class_="form-control", size="100%") }} + </div> + {% for error in form.description.errors %} <span style="color: red;">{{ error }}<br /></span>{% endfor %} + </div> + + <div class="form-group"> + <label for="{{ form.tags.id }}" class="col-sm-3 control-label">{{ form.tags.label }}</label> + <div class="col-sm-9"> + {{ form.tags(class_="form-control", size="100%") }} + </div> + {% for error in form.tags.errors %} <span style="color: red;">{{ error }}<br /></span>{% endfor %} + </div> + + <div class="form-group"> + <label for="{{ form.shared.id }}" class="col-sm-3 control-label">{{ form.shared.label }}</label> + <div class="col-sm-9"> + <div class="checkbox"> + {{ form.shared(class_="checkbox", style="margin-left: 0px;") }} + </div> + </div> + </div> + + <div class="form-group"> + <label for="{{ form.to_read.id }}" class="col-sm-3 control-label">{{ form.to_read.label }}</label> + <div class="col-sm-9"> + <div class="checkbox"> + {{ form.to_read(class_="checkbox", style="margin-left: 0px;") }} + </div> + </div> + </div> + + <div class="form-group"> + <div class="col-sm-offset-3 col-sm-9"> + {{ form.submit(class_="btn btn-default") }} + </div> + </div> + </form> + </div> +</div><!-- /.container --> +{% endblock %} diff --git a/src/web/views/__init__.py b/src/web/views/__init__.py index e0e49927..41bb52f3 100644 --- a/src/web/views/__init__.py +++ b/src/web/views/__init__.py @@ -6,11 +6,12 @@ from web.views.category import category_bp, categories_bp from web.views.icon import icon_bp from web.views.admin import admin_bp from web.views.user import user_bp, users_bp +from web.views.bookmark import bookmark_bp, bookmarks_bp __all__ = ['views', 'home', 'session_mgmt', 'v2', 'v3', 'article_bp', 'articles_bp', 'feed_bp', 'feeds_bp', 'category_bp', 'categories_bp', 'icon_bp', - 'admin_bp', 'user_bp', 'users_bp'] + 'admin_bp', 'user_bp', 'users_bp', 'bookmark_bp', 'bookmarks_bp'] import conf from flask import request diff --git a/src/web/views/bookmark.py b/src/web/views/bookmark.py new file mode 100644 index 00000000..d780a3ae --- /dev/null +++ b/src/web/views/bookmark.py @@ -0,0 +1,80 @@ +import logging +from werkzeug.exceptions import BadRequest + +from flask import Blueprint, render_template, flash, \ + redirect, request, url_for +from flask_babel import gettext +from flask_login import login_required, current_user + +import conf +from bootstrap import db +from web.forms import BookmarkForm +from web.controllers import BookmarkController + +logger = logging.getLogger(__name__) +bookmarks_bp = Blueprint('bookmarks', __name__, url_prefix='/bookmarks') +bookmark_bp = Blueprint('bookmark', __name__, url_prefix='/bookmark') + + +@bookmarks_bp.route('/', methods=['GET']) +@login_required +def list(): + "Lists the bookmarks." + bookmark_contr = BookmarkController(current_user.id) + return render_template('bookmarks.html', + bookmarks=BookmarkController(current_user.id).read().order_by('time')) + + +@bookmark_bp.route('/create', methods=['GET']) +@bookmark_bp.route('/edit/<int:bookmark_id>', methods=['GET']) +@login_required +def form(bookmark_id=None): + action = gettext("Add a bookmark") + head_titles = [action] + if bookmark_id is None: + return render_template('edit_bookmark.html', action=action, + head_titles=head_titles, form=BookmarkForm()) + bookmark = BookmarkController(current_user.id).get(id=bookmark_id) + action = gettext('Edit bookmark') + head_titles = [action] + form = BookmarkForm(obj=bookmark) + form.tags.data = ", ".join(bookmark.tags) + 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']) +@login_required +def process_form(bookmark_id=None): + form = BookmarkForm() + bookmark_contr = BookmarkController(current_user.id) + + if not form.validate(): + return render_template('edit_bookmark.html', form=form) + + # Edit an existing bookmark + bookmark_attr = {'href': form.href.data, + 'description': form.description.data, + 'extended': form.extended.data, + 'tags': [tag.strip() for tag in form.tags.data.split(',')], + 'shared': form.shared.data, + 'to_read': form.to_read.data} + + if bookmark_id is not None: + bookmark = BookmarkController(current_user.id).get(id=bookmark_id) + form.populate_obj(bookmark) + bookmark.tags = [tag.strip() for tag in form.tags.data.split(',')] + db.session.commit() + + #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) + + flash(gettext('Bookmark successfully created.'), 'success') + + return redirect(url_for('bookmark.form', bookmark_id=new_bookmark.id)) diff --git a/src/web/views/category.py b/src/web/views/category.py index 2bdcf9cc..138561dd 100644 --- a/src/web/views/category.py +++ b/src/web/views/category.py @@ -16,7 +16,7 @@ category_bp = Blueprint('category', __name__, url_prefix='/category') @login_required @etag_match def list_(): - "Lists the subscribed feeds in a table." + "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')), |