From 7cbbcb59f4c434fbd7e74e85c90e98fadd189b65 Mon Sep 17 00:00:00 2001 From: François Schmidts Date: Mon, 12 Oct 2015 17:48:41 +0200 Subject: adding, improving UI to manage categories --- pyaggr3g470r/controllers/category.py | 6 ++++++ pyaggr3g470r/templates/categories.html | 36 ++++++++++++++++++++++++++++++++++ pyaggr3g470r/views/category.py | 36 ++++++++++++++++++++++++++++------ src/web/controllers/abstract.py | 13 +++++++++--- src/web/controllers/article.py | 9 ++++----- src/web/controllers/feed.py | 8 +++++--- src/web/forms.py | 7 +++++-- src/web/templates/feed_list.html | 2 +- src/web/templates/feeds.html | 2 +- src/web/views/feed.py | 1 + 10 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 pyaggr3g470r/templates/categories.html diff --git a/pyaggr3g470r/controllers/category.py b/pyaggr3g470r/controllers/category.py index b6fc591c..a0f746e9 100644 --- a/pyaggr3g470r/controllers/category.py +++ b/pyaggr3g470r/controllers/category.py @@ -1,6 +1,12 @@ from .abstract import AbstractController from pyaggr3g470r.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/pyaggr3g470r/templates/categories.html b/pyaggr3g470r/templates/categories.html new file mode 100644 index 00000000..a61cc4b2 --- /dev/null +++ b/pyaggr3g470r/templates/categories.html @@ -0,0 +1,36 @@ +{% extends "layout.html" %} +{% block content %} +
+

{{ _("You have %(categories)d categories · Add a %(start_link)scategory%(end_link)s", categories=categories|count, start_link=("" % url_for("category.form"))|safe, end_link=""|safe) }}

+ {% if categories|count == 0 %} +

{{_("No category")}}

+ {% else %} +
+ + + + + + + + + + + + {% for category in categories|sort(attribute="name") %} + + + + + + + + {% endfor %} + +
#{{ _('Name') }}{{ _('Feeds') }}{{ _('Articles') }}{{ _('Actions') }}
{{ loop.index }}{{ category.name }}{{ feeds_count.get(category.id, 0) }}( {{ unread_article_count.get(category.id, 0) }} ) {{ article_count.get(category.id, 0) }} + + +
+
+ {% endif %} +{% endblock %} diff --git a/pyaggr3g470r/views/category.py b/pyaggr3g470r/views/category.py index c5defb7f..027a800f 100644 --- a/pyaggr3g470r/views/category.py +++ b/pyaggr3g470r/views/category.py @@ -2,14 +2,29 @@ from flask import g, Blueprint, render_template, flash, redirect, url_for from flask.ext.babel import gettext from flask.ext.login import login_required -from pyaggr3g470r.forms import AddCategoryForm +from pyaggr3g470r.forms import CategoryForm +from pyaggr3g470r.lib.utils import redirect_url from pyaggr3g470r.lib.view_utils import etag_match -from pyaggr3g470r.controllers.category import CategoryController +from pyaggr3g470r.controllers \ + import ArticleController, FeedController, CategoryController categories_bp = Blueprint('categories', __name__, url_prefix='/categories') category_bp = Blueprint('category', __name__, url_prefix='/category') +@categories_bp.route('/', methods=['GET']) +@login_required +@etag_match +def list_(): + "Lists the subscribed feeds in a table." + art_contr = ArticleController(g.user.id) + return render_template('categories.html', + categories=list(CategoryController(g.user.id).read()), + feeds_count=FeedController(g.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/', methods=['GET']) @login_required @@ -19,7 +34,7 @@ def form(category_id=None): head_titles = [action] if category_id is None: return render_template('edit_category.html', action=action, - head_titles=head_titles, form=AddCategoryForm()) + head_titles=head_titles, form=CategoryForm()) category = CategoryController(g.user.id).get(id=category_id) action = gettext('Edit category') head_titles = [action] @@ -27,14 +42,23 @@ def form(category_id=None): head_titles.append(category.name) return render_template('edit_category.html', action=action, head_titles=head_titles, category=category, - form=AddCategoryForm(obj=category)) + form=CategoryForm(obj=category)) + + +@category_bp.route('/delete/', methods=['GET']) +@login_required +def delete(category_id=None): + category = CategoryController(g.user.id).delete(category_id) + 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/', methods=['POST']) +@category_bp.route('/edit/', methods=['POST']) @login_required def process_form(category_id=None): - form = AddCategoryForm() + form = CategoryForm() cat_contr = CategoryController(g.user.id) if not form.validate(): diff --git a/src/web/controllers/abstract.py b/src/web/controllers/abstract.py index 99d92ff3..828e6a29 100644 --- a/src/web/controllers/abstract.py +++ b/src/web/controllers/abstract.py @@ -1,7 +1,7 @@ import logging from flask import g from bootstrap import db -from sqlalchemy import or_ +from sqlalchemy import or_, func from werkzeug.exceptions import Forbidden, NotFound logger = logging.getLogger(__name__) @@ -83,12 +83,12 @@ class AbstractController(object): return obj def create(self, **attrs): + 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" - 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() @@ -114,3 +114,10 @@ class AbstractController(object): 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()) diff --git a/src/web/controllers/article.py b/src/web/controllers/article.py index 72288a09..50e6757f 100644 --- a/src/web/controllers/article.py +++ b/src/web/controllers/article.py @@ -28,12 +28,11 @@ class ArticleController(AbstractController): continue yield id_ + def count_by_category(self, **filters): + return self._count_by(Article.category_id, filters) + 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()) + 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)) diff --git a/src/web/controllers/feed.py b/src/web/controllers/feed.py index b76c4e42..31a1ec41 100644 --- a/src/web/controllers/feed.py +++ b/src/web/controllers/feed.py @@ -87,6 +87,9 @@ class FeedController(AbstractController): 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 _ensure_icon(self, attrs): if not attrs.get('icon_url'): return @@ -101,10 +104,9 @@ class FeedController(AbstractController): def update(self, filters, attrs): from .article import ArticleController self._ensure_icon(attrs) - result = super().update(filters, attrs) if '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': feed.category_id}) - return result + {'category_id': attrs['category_id']}) + return super().update(filters, attrs) diff --git a/src/web/forms.py b/src/web/forms.py index f57b31d0..9dbf1b5f 100644 --- a/src/web/forms.py +++ b/src/web/forms.py @@ -38,6 +38,7 @@ from flask_wtf import RecaptchaField from web import utils from web.models import User + class SignupForm(Form): """ Sign up form (registration to jarr). @@ -63,6 +64,7 @@ class SignupForm(Form): validated = False return validated + class RedirectForm(Form): """ Secure back redirects with WTForms. @@ -80,6 +82,7 @@ class RedirectForm(Form): target = utils.get_redirect_target() return redirect(target or url_for(endpoint, **values)) + class SigninForm(RedirectForm): """ Sign in form (connection to jarr). @@ -179,9 +182,9 @@ class AddFeedForm(Form): for cat in categories] -class AddCategoryForm(Form): +class CategoryForm(Form): name = TextField(lazy_gettext("Name")) - submit = SubmitField(lazy_gettext("Sign up")) + submit = SubmitField(lazy_gettext("Submit")) class InformationMessageForm(Form): diff --git a/src/web/templates/feed_list.html b/src/web/templates/feed_list.html index 27815250..51561dee 100644 --- a/src/web/templates/feed_list.html +++ b/src/web/templates/feed_list.html @@ -1,4 +1,4 @@ -{% if feeds.all()| count == 0 %} +{% if feeds| count == 0 %}

{{_("No feed")}}

{% else %}
diff --git a/src/web/templates/feeds.html b/src/web/templates/feeds.html index 9ba16359..074957f7 100644 --- a/src/web/templates/feeds.html +++ b/src/web/templates/feeds.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block content %}
-

{{ _('You are subscribed to') }} {{ feeds.count() }} {{ _('feeds') }} · {{ _('Add a') }} {{ _('feed') }}

+

{{ _('You are subscribed to') }} {{ feeds|count }} {{ _('feeds') }} · {{ _('Add a') }} {{ _('feed') }}

{% include "feed_list.html" %}
{% endblock %} diff --git a/src/web/views/feed.py b/src/web/views/feed.py index 6569d1b7..e1c2fb55 100644 --- a/src/web/views/feed.py +++ b/src/web/views/feed.py @@ -14,6 +14,7 @@ from flask.ext.login import login_required import conf from web import utils +from web.lib.utils import redirect_url from web.lib.view_utils import etag_match from web.lib.feed_utils import construct_feed_from from web.forms import AddFeedForm -- cgit