diff options
Diffstat (limited to 'pyaggr3g470r')
-rw-r--r-- | pyaggr3g470r/controllers/abstract.py | 9 | ||||
-rw-r--r-- | pyaggr3g470r/controllers/article.py | 9 | ||||
-rw-r--r-- | pyaggr3g470r/static/css/bootstrap.css | 1 | ||||
-rw-r--r-- | pyaggr3g470r/templates/about.html | 2 | ||||
-rw-r--r-- | pyaggr3g470r/templates/admin/user.html | 6 | ||||
-rw-r--r-- | pyaggr3g470r/templates/favorites.html | 48 | ||||
-rw-r--r-- | pyaggr3g470r/templates/feed.html | 5 | ||||
-rw-r--r-- | pyaggr3g470r/templates/feeds.html | 8 | ||||
-rw-r--r-- | pyaggr3g470r/templates/home.html | 30 | ||||
-rw-r--r-- | pyaggr3g470r/templates/layout.html | 73 | ||||
-rw-r--r-- | pyaggr3g470r/templates/management.html | 2 | ||||
-rw-r--r-- | pyaggr3g470r/templates/unread.html | 51 | ||||
-rw-r--r-- | pyaggr3g470r/views/feed.py | 150 | ||||
-rw-r--r-- | pyaggr3g470r/views/views.py | 186 |
14 files changed, 271 insertions, 309 deletions
diff --git a/pyaggr3g470r/controllers/abstract.py b/pyaggr3g470r/controllers/abstract.py index f1173817..8f0a8e3f 100644 --- a/pyaggr3g470r/controllers/abstract.py +++ b/pyaggr3g470r/controllers/abstract.py @@ -56,8 +56,8 @@ class AbstractController(object): if not obj: raise NotFound({'message': 'No %r (%r)' % (self._db_cls.__class__.__name__, filters)}) - if self.user_id is not None \ - and getattr(obj, self._user_id_key) != self.user_id: + + if not self._has_right_on(obj): raise Forbidden({'message': 'No authorized to access %r (%r)' % (self._db_cls.__class__.__name__, filters)}) return obj @@ -84,3 +84,8 @@ class AbstractController(object): db.session.delete(obj) db.session.commit() return obj + + def _has_right_on(self, obj): + # user_id == None is like being admin + return self.user_id is None \ + or getattr(obj, self._user_id_key, None) == self.user_id diff --git a/pyaggr3g470r/controllers/article.py b/pyaggr3g470r/controllers/article.py index 46ca0988..0ec53a2f 100644 --- a/pyaggr3g470r/controllers/article.py +++ b/pyaggr3g470r/controllers/article.py @@ -1,3 +1,6 @@ +from sqlalchemy import func + +from bootstrap import db import conf from .abstract import AbstractController from pyaggr3g470r.models import Article @@ -25,3 +28,9 @@ class ArticleController(AbstractController): if self.read(**id_).first(): continue yield id_ + + def get_unread(self): + return dict(db.session.query(Article.feed_id, func.count(Article.id)) + .filter(Article.readed == False, + Article.user_id == self.user_id) + .group_by(Article.feed_id).all()) diff --git a/pyaggr3g470r/static/css/bootstrap.css b/pyaggr3g470r/static/css/bootstrap.css index fb15e3d6..686809a5 100644 --- a/pyaggr3g470r/static/css/bootstrap.css +++ b/pyaggr3g470r/static/css/bootstrap.css @@ -6582,3 +6582,4 @@ button.close { } } /*# sourceMappingURL=bootstrap.css.map */ +.glyphicon.delete, .glyphicon.like, .glyphicon.readed { cursor: pointer; } diff --git a/pyaggr3g470r/templates/about.html b/pyaggr3g470r/templates/about.html index 08b80fbb..901b3b35 100644 --- a/pyaggr3g470r/templates/about.html +++ b/pyaggr3g470r/templates/about.html @@ -17,7 +17,7 @@ <h1>{{ _('Help') }}</h1> <p>{{ _('If you have any problem, <a href="http://wiki.cedricbonhomme.org/contact">contact</a> the administrator.') }}</p> <p>{{ _('The documentation of the RESTful API is <a href="https://pyaggr3g470r.readthedocs.org/en/latest/web-services.html">here</a>.') }}</p> - <p>{{ _('You can subscribe to new feeds with a bookmarklet. Drag <a href="%(bookmarklet)s">this link</a> to your browser bookmarks.', bookmarklet='javascript:window.location="https://pyaggr3g470r.herokuapp.com/bookmarklet?url="+encodeURIComponent(document.location)') }}</p> + <p>{{ _('You can subscribe to new feeds with a bookmarklet. Drag <a href="%(bookmarklet)s">this link</a> to your browser bookmarks.', bookmarklet='javascript:window.location="%s?url="+encodeURIComponent(document.location)' % url_for('feed.bookmarklet', _external=True)) }}</p> </div> <div class="well"> <h1>{{ _('Donation') }}</h1> diff --git a/pyaggr3g470r/templates/admin/user.html b/pyaggr3g470r/templates/admin/user.html index f20d53dd..317fef49 100644 --- a/pyaggr3g470r/templates/admin/user.html +++ b/pyaggr3g470r/templates/admin/user.html @@ -39,9 +39,9 @@ <td>{{ feed.site_link }}</td> <td>{{ feed.articles.all()|count }}</td> <td> - <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-th-list" title="{{ _('Feed') }}"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> - <a href="/delete_feed/{{ feed.id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> + <a href="{{ url_for("feed.feed", feed_id=feed.id) }}"><i class="glyphicon glyphicon-th-list" title="{{ _('Feed') }}"></i></a> + <a href="{{ url_for("feed.form", feed_id=feed.id) }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> + <a href="{{ url_for("feed.delete", feed_id=feed.id) }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> </td> {% endfor %} </tbody> diff --git a/pyaggr3g470r/templates/favorites.html b/pyaggr3g470r/templates/favorites.html deleted file mode 100644 index 210f7343..00000000 --- a/pyaggr3g470r/templates/favorites.html +++ /dev/null @@ -1,48 +0,0 @@ - {% extends "layout.html" %} -{% block content %} -<div class="container"> - {% if feeds|count == 0 %} - <div class="page-header"> - <h1>{{ _('No favorites') }}</h1> - </div> - {% else %} - <div class="page-header"> - <h1>{{ _('Favorites articles') }} <small>{{ nb_favorites }}</small></h1> - </div> - {% for feed in feeds|sort(attribute="title") %} - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <h1>{{ feed.title|safe }}</h1> - <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="{{ _('More articles') }}"></i></a> - <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-info-sign" title="{{ _('Details') }}"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> - </div> - </div> - {% for number in range(0, feed.articles|length-(feed.articles|length % 3), 3) %} - <div class="row"> - {% for n in range(number, number+3) %} - <div class="col-xs-6 col-sm-4 col-md-4"> - {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} - <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> - {% if feed.articles[n].readed %}</h3>{% else %}</h1>{% endif %} - <h6>{{ feed.articles[n].date | datetime }}</h6> - </div> - {% endfor %} - </div> - {% endfor %} - {% if feed.articles|length % 3 != 0 %} - <div class="row"> - {% for n in range(feed.articles|length-(feed.articles|length % 3), feed.articles|length) %} - <div class="col-xs-6 col-sm-4 col-md-4"> - {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} - <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> - {% if feed.articles[n].readed %}</h3>{% else %}</h1>{% endif %} - <h6>{{ feed.articles[n].date | datetime }}</h6> - </div> - {% endfor %} - </div> - {% endif %} - {% endfor %} - {% endif %} -</div><!-- /.container --> -{% endblock %} diff --git a/pyaggr3g470r/templates/feed.html b/pyaggr3g470r/templates/feed.html index 268cbf7d..c888e04f 100644 --- a/pyaggr3g470r/templates/feed.html +++ b/pyaggr3g470r/templates/feed.html @@ -4,8 +4,8 @@ <div class="well"> <h2>{{ feed.title }}</h2> {% if feed.description %} <p>{{ feed.description }}</p> {% endif %} - <a href="/delete_feed/{{ feed.id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> + <a href="{{ url_for("feed.delete", feed_id=feed.id) }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> + <a href="{{ url_for("feed.form", feed_id=feed.id) }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> </div> <div class="well"> <p> @@ -23,6 +23,7 @@ {% if feed.error_count > 2 %} <b>{{ _("That feed has encountered too much consecutive errors and won't be retrieved anymore.") }}</b><br /> + {{ _("You can click <a href='%(reset_error_url)s'>here</a> to reset the error count and reactivate the feed.", reset_error_url=url_for("feed.reset_errors", feed_id=feed.id)) }} {% elif feed.error_count > 0 %} {{ _("The download of this feed has encountered some problems. However its error counter will be reinitialized at the next successful retrieving.") }}<br /> {% endif %} diff --git a/pyaggr3g470r/templates/feeds.html b/pyaggr3g470r/templates/feeds.html index 460ae4a2..9c57bf42 100644 --- a/pyaggr3g470r/templates/feeds.html +++ b/pyaggr3g470r/templates/feeds.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block content %} <div class="container"> - <h1>{{ _('You are subscribed to') }} {{ feeds.count() }} {{ _('feeds') }} · {{ _('Add a') }} <a href="/create_feed">{{ _('feed') }}</a></h1> + <h1>{{ _('You are subscribed to') }} {{ feeds.count() }} {{ _('feeds') }} · {{ _('Add a') }} <a href="{{ url_for("feed.form") }}">{{ _('feed') }}</a></h1> <div class="table-responsive"> <table class="table table-striped"> <thead> @@ -25,14 +25,14 @@ <i class="glyphicon glyphicon-eye-close" title="{{ _('Feed disabled') }}"></i> {% endif %} </td> - <td><a href="/feed/{{ feed.id }}" {% if feed.description %}title="{{ feed.description }}"{% endif %}>{{ feed.title }}</a></td> + <td><a href="{{ url_for("feed.feed", feed_id=feed.id) }}" {% if feed.description %}title="{{ feed.description }}"{% endif %}>{{ feed.title }}</a></td> <td><a href="{{ feed.site_link }}">{{ feed.site_link }}</a></td> <td>{{ feed.articles.count() }}</td> <td> <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="{{ _('Articles') }}"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> + <a href="{{ url_for("feed.form", feed_id=feed.id) }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> <a href="/duplicates/{{ feed.id }}"><i class="glyphicon glyphicon-book" title="{{ _('Duplicate articles') }}"></i></a> - <a href="/delete_feed/{{ feed.id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> + <a href="{{ url_for("feed.delete", feed_id=feed.id) }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> </td> </tr> {% endfor %} diff --git a/pyaggr3g470r/templates/home.html b/pyaggr3g470r/templates/home.html index c20cacfa..e055b3e0 100644 --- a/pyaggr3g470r/templates/home.html +++ b/pyaggr3g470r/templates/home.html @@ -3,7 +3,7 @@ {% if feeds|count == 0 %} <div class="col-md-4 col-md-offset-4"> <h1>{{ _("You don't have any feeds.") }}</h1> - <h1><a href="/create_feed">{{ _('Add some') }}</a>, {{ _('or') }} <a href="/management">{{ _('upload an OPML file.') }}</a></h1> + <h1><a href="{{ url_for("feed.form") }}">{{ _('Add some') }}</a>, {{ _('or') }} <a href="/management">{{ _('upload an OPML file.') }}</a></h1> </div> {% else %} <div id="affix-nav" class="col-md-3 sidebar hidden-xs hidden-sm"> @@ -26,10 +26,10 @@ <li class="feed-commands"><span> <a href="/feed/{{ fid }}"><i class="glyphicon glyphicon-info-sign" title="{{ _('Details') }}"></i></a> <a href="/articles/{{ fid }}/100"><i class="glyphicon glyphicon-th-list" title="{{ _('Articles') }}"></i></a> - <a href="/edit_feed/{{ fid }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> - <a href="/delete_feed/{{ fid }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> - <a href="/mark_as/read/feed/{{ fid }}"><i class="glyphicon glyphicon-check" title="{{ _('Mark this feed as read') }}"></i></a> - <a href="/mark_as/unread/feed/{{ fid }}"><i class="glyphicon glyphicon-unchecked" title="{{ _('Mark this feed as unread') }}"></i></a> + <a href="{{ url_for("feed.form", feed_id=fid) }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> + <a href="{{ url_for("feed.delete", feed_id=fid) }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> + <a href="{{ url_for("feed.update", feed_id=fid, action="read") }}"><i class="glyphicon glyphicon-check" title="{{ _('Mark this feed as read') }}"></i></a> + <a href="{{ url_for("feed.update", feed_id=fid, action="unread") }}"><i class="glyphicon glyphicon-unchecked" title="{{ _('Mark this feed as unread') }}"></i></a> </span></li> {% endfor %} {% for fid, ftitle in feeds|dictsort(case_sensitive=False, by='value') if not fid in unread %} @@ -42,12 +42,12 @@ {% if feed_id == fid %}</b>{% endif %} </a></li> <li class="feed-commands"><span> - <a href="/feed/{{ fid }}"><i class="glyphicon glyphicon-info-sign" title="{{ _('Details') }}"></i></a> + <a href="{{ url_for("feed.feed", feed_id=fid) }}"><i class="glyphicon glyphicon-info-sign" title="{{ _('Details') }}"></i></a> <a href="/articles/{{ fid }}/100"><i class="glyphicon glyphicon-th-list" title="{{ _('Articles') }}"></i></a> - <a href="/edit_feed/{{ fid }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> - <a href="/delete_feed/{{ fid }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> - <a href="/mark_as/read/feed/{{ fid }}"><i class="glyphicon glyphicon-check" title="{{ _('Mark this feed as read') }}"></i></a> - <a href="/mark_as/unread/feed/{{ fid }}"><i class="glyphicon glyphicon-unchecked" title="{{ _('Mark this feed as unread') }}"></i></a> + <a href="{{ url_for("feed.form", feed_id=fid) }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> + <a href="{{ url_for("feed.delete", feed_id=fid) }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this feed') }}" onclick="return confirm('{{ _('You are going to delete this feed.') }}');"></i></a> + <a href="{{ url_for("feed.update", feed_id=fid, action="read") }}"><i class="glyphicon glyphicon-check" title="{{ _('Mark this feed as read') }}"></i></a> + <a href="{{ url_for("feed.update", feed_id=fid, action="unread") }}"><i class="glyphicon glyphicon-unchecked" title="{{ _('Mark this feed as unread') }}"></i></a> </span></li> {% endfor %} </ul> @@ -82,16 +82,16 @@ {% for article in articles %} <tr data-article="{{ article.id }}" data-feed="{{ article.feed_id }}"> <td> - <a href="#"><i class="glyphicon glyphicon-remove delete" title="{{ _('Delete this article') }}"></i></a> + <a><i class="glyphicon glyphicon-remove delete" title="{{ _('Delete this article') }}"></i></a> {% if article.like %} - <a href="#"><i class="glyphicon glyphicon-star like" title="{{ _('One of your favorites') }}"></i></a> + <a><i class="glyphicon glyphicon-star like" title="{{ _('One of your favorites') }}"></i></a> {% else %} - <a href="#"><i class="glyphicon glyphicon-star-empty like" title="{{ _('Click if you like this article') }}"></i></a> + <a><i class="glyphicon glyphicon-star-empty like" title="{{ _('Click if you like this article') }}"></i></a> {% endif %} {% if article.readed %} - <a href="#"><i class="glyphicon glyphicon-unchecked readed" title="{{ _('Mark this article as unread') }}"></i></a> + <a><i class="glyphicon glyphicon-unchecked readed" title="{{ _('Mark this article as unread') }}"></i></a> {% else %} - <a href="#"><i class="glyphicon glyphicon-check readed" title="{{ _('Mark this article as read') }}"></i></a> + <a><i class="glyphicon glyphicon-check readed" title="{{ _('Mark this article as read') }}"></i></a> {% if filter_ == 'all' %}</b>{% endif %} {% endif %} </td> diff --git a/pyaggr3g470r/templates/layout.html b/pyaggr3g470r/templates/layout.html index 60efa69e..e673a128 100644 --- a/pyaggr3g470r/templates/layout.html +++ b/pyaggr3g470r/templates/layout.html @@ -6,13 +6,13 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="description" content="pyAggr3g470r is a web-based news aggregator." /> <meta name="author" content="" /> - <title>{% if head_title %}{{ head_title }} - {% endif %}pyAggr3g470r</title> + <title>pyAggr3g470r{% if head_title %} - {{ head_title }}{% endif %}</title> <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.png') }}" /> <!-- Bootstrap core CSS --> - <link href="{{ url_for('static', filename = 'css/bootstrap.css') }}" rel="stylesheet" media="screen" /> + <link href="{{ url_for('static', filename='css/bootstrap.css') }}" rel="stylesheet" media="screen" /> <!-- Add custom CSS here --> - <link href="{{ url_for('static', filename = 'css/customized-bootstrap.css') }}" rel="stylesheet" media="screen" /> - <link href="{{ url_for('static', filename = 'css/side-nav.css') }}" rel="stylesheet" media="screen" /> + <link href="{{ url_for('static', filename='css/customized-bootstrap.css') }}" rel="stylesheet" media="screen" /> + <link href="{{ url_for('static', filename='css/side-nav.css') }}" rel="stylesheet" media="screen" /> {% endblock %} </head> <body> @@ -25,38 +25,44 @@ <span class="icon-bar"></span> <span class="icon-bar"></span> </button> - <a class="navbar-brand" href="/">pyAggr3g470r</a> + <a class="navbar-brand" href="{{ url_for("home") }}">pyAggr3g470r</a> + <span class="navbar-brand">{% if head_title %} - {{ head_title }}{% endif %}</span> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse navbar-ex1-collapse"> <ul class="nav navbar-nav navbar-right"> {% if g.user.is_authenticated() %} + <li><a href="{{ url_for("feed.form") }}"><span class="glyphicon glyphicon-plus-sign"></span>{{ _('Add a feed') }}</a></li> + {% if favorites %} + <li><a href="{{ url_for("home") }}"><span class="glyphicon glyphicon-home"></span> {{ _('Home') }}</a></li> + {% else %} + <li><a accesskey="f" href="{{ url_for("favorites") }}"><span class="glyphicon glyphicon-star"></span> {{ _('Favorites') }}</a></li> + {% endif %} + {% if conf.ON_HEROKU and g.user.is_admin() %} + <li><a accesskey="r" href="/fetch"><span class="glyphicon glyphicon-import"></span>{{ _('Fetch') }}</a></li> + {% endif %} <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ _('Articles') }} <b class="caret"></b></a> + <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ _('Feed') }} <b class="caret"></b></a> <ul class="dropdown-menu"> - <li><a accesskey="r" href="/fetch">{{ _('Fetch') }}</a></li> - <li><a href="/mark_as/read">{{ _('Mark all as read') }}</a></li> + <li><a href="{{ url_for("feeds.update", action="read") }}">{{ _('Mark all as read') }}</a></li> <li role="presentation" class="divider"></li> - <li><a href="/create_feed">{{ _('Add a feed') }}</a></li> + <li><a accesskey="i" href="{{ url_for("inactives") }}">{{ _('Inactive') }}</a></li> + <li><a href="{{ url_for("history") }}">{{ _('History') }}</a></li> + <li><a href="{{ url_for("feeds.feeds") }}">{{ _('All') }}</a></li> </ul> </li> <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-filter"></span> {{ _('Filter') }} <b class="caret"></b></a> + <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ _('Tools') }} <b class="caret"></b></a> <ul class="dropdown-menu"> - <li><a accesskey="u" href="/unread">{{ _('Unread') }}</a></li> - <li><a accesskey="f" href="/favorites">{{ _('Favorites') }}</a></li> - <li><a accesskey="i" href="/inactives">{{ _('Inactive feeds') }}</a></li> - <li><a href="/feeds">{{ _('All feeds') }}</a></li> - <li><a href="/history">{{ _('History') }}</a></li> +i <li><a accesskey="m" href="{{ url_for("management") }}"><span class="glyphicon glyphicon-cog"></span> {{ _('Management') }}</a></li> + {% if g.user.is_admin() %} + <li><a href="{{ url_for("dashboard") }}"><span class="glyphicon glyphicon-dashboard"></span> {{ _('Dashboard') }}</a></li> + {% endif %} + <li><a href="{{ url_for("about") }}"><span class="glyphicon glyphicon-question-sign"></span>{{ _('About') }}</a></li> </ul> </li> - <li><a accesskey="m" href="/management"><span class="glyphicon glyphicon-cog"></span> {{ _('Management') }}</a></li> - {% if g.user.is_admin() %} - <li><a href="{{ url_for('dashboard') }}"><span class="glyphicon glyphicon-dashboard"></span> {{ _('Dashboard') }}</a></li> - {% endif %} - <li><a href="/about">{{ _('About') }}</a></li> - <li><a href="{{ url_for('logout') }}"><span class="glyphicon glyphicon-log-out"></span> {{ _('Logout') }}</a></li> + <li><a href="{{ url_for("logout") }}"><span class="glyphicon glyphicon-log-out"></span> {{ _('Logout') }}</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <div><span class="glyphicon glyphicon-search"></span> <b class="caret"></b></div> @@ -72,7 +78,7 @@ </ul> </li> {% else %} - <li><a href="/about">{{ _('About') }}</a></li> + <li><a href="{{ url_for("about") }}"><span class="glyphicon glyphicon-question-sign"></span>{{ _('About') }}</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> @@ -102,18 +108,19 @@ <script src="{{ url_for('static', filename = 'js/bootstrap.js') }}"></script> <script src="{{ url_for('static', filename = 'js/articles.js') }}"></script> <script type="text/javascript" class="source"> - if (window.location.href.indexOf("filter_=all") > -1){ - $("#tab-all").attr('class', "active"); - } - else if (window.location.href.indexOf("filter_=unread") > -1) { - $("#tab-unread").attr('class', "active"); - } - else if (window.location.href.indexOf("filter_=read") > -1) { - $("#tab-read").attr('class', "active"); - } - else { - $("#tab-unread").attr('class', "active"); + var filter_ = {% if filter_ %}"{{ filter_ }}"{% else %}undefined{% endif %}; + if (filter_ == undefined) { + if (window.location.href.indexOf("filter_=all") > -1){ + filter_ = 'all'; + } + else if (window.location.href.indexOf("filter_=unread") > -1) { + filter_ = 'unread'; + } + else if (window.location.href.indexOf("filter_=read") > -1) { + filter_ = 'read'; + } } + $("#tab-" + filter_).attr('class', "active"); </script> </body> </html> diff --git a/pyaggr3g470r/templates/management.html b/pyaggr3g470r/templates/management.html index 722300af..b9d02de5 100644 --- a/pyaggr3g470r/templates/management.html +++ b/pyaggr3g470r/templates/management.html @@ -3,7 +3,7 @@ <div class="container"> <div class="well"> <h1>{{ _('Your subscriptions') }}</h1> - <p>{{ _('You are subscribed to') }} {{ nb_feeds }} <a href="/feeds">{{ _('feeds') }}</a>. {{ _('Add a') }} <a href="/create_feed">{{ _('feed') }}</a>.</p> + <p>{{ _('You are subscribed to') }} {{ nb_feeds }} <a href="/feeds">{{ _('feeds') }}</a>. {{ _('Add a') }} <a href="{{ url_for("feed.form") }}">{{ _('feed') }}</a>.</p> <p>{{ nb_articles }} {{ _('articles are stored in the database with') }} {{ nb_unread_articles }} <a href="/unread">{{ _('unread articles') }}</a>.</p> {% if not_on_heroku %} <a href="/index_database" class="btn btn-default">{{ _('Index database') }}</a> diff --git a/pyaggr3g470r/templates/unread.html b/pyaggr3g470r/templates/unread.html deleted file mode 100644 index 9808572b..00000000 --- a/pyaggr3g470r/templates/unread.html +++ /dev/null @@ -1,51 +0,0 @@ - {% extends "layout.html" %} -{% block content %} -<div class="container"> - {% if feeds|count == 0 %} - <div class="page-header"> - <h1>{{ _('No unread articles') }}</h1> - </div> - {% else %} - <div class="page-header"> - <h1>{{ _('Unread articles') }} <small>{{ nb_unread }}</small></h1> - </div> - {% for feed in feeds|sort(attribute="title") %} - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <h1>{{ feed.title|safe }}</h1> - <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="{{ _('More articles') }}"></i></a> - <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-info-sign" title="{{ _('Details') }}"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this feed') }}"></i></a> - <a href="/mark_as/read/feed/{{ feed.id }}"><i class="glyphicon glyphicon-check" title="{{ _('Mark all feed as read') }}"></i></a> - <a href="/mark_as/unread/feed/{{ feed.id }}"><i class="glyphicon glyphicon-unchecked" title="{{ _('Mark all feed as unread') }}"></i></a> - <h3>{{ feed.articles|length }} {{ _('unread articles') }}.</h3> - </div> - </div> - {% for number in range(0, feed.articles|length-(feed.articles|length % 3), 3) %} - <div class="row"> - {% for n in range(number, number+3) %} - <div class="col-xs-6 col-sm-4 col-md-4"> - {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} - <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> - {% if feed.articles[n].readed %}</h3>{% else %}</h1>{% endif %} - <h6>{{ feed.articles[n].date | datetime }}</h6> - </div> - {% endfor %} - </div> - {% endfor %} - {% if feed.articles|length % 3 != 0 %} - <div class="row"> - {% for n in range(feed.articles|length-(feed.articles|length % 3), feed.articles|length) %} - <div class="col-xs-6 col-sm-4 col-md-4"> - {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} - <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> - {% if feed.articles[n].readed %}</h3>{% else %}</h1>{% endif %} - <h6>{{ feed.articles[n].date | datetime }}</h6> - </div> - {% endfor %} - </div> - {% endif %} - {% endfor %} - {% endif %} -</div><!-- /.container --> -{% endblock %} diff --git a/pyaggr3g470r/views/feed.py b/pyaggr3g470r/views/feed.py index 4fe4e5da..e4c0dc9a 100644 --- a/pyaggr3g470r/views/feed.py +++ b/pyaggr3g470r/views/feed.py @@ -2,33 +2,40 @@ # -*- coding: utf-8 - from datetime import datetime -from flask import Blueprint, g, render_template from sqlalchemy import desc +from werkzeug.exceptions import BadRequest -from pyaggr3g470r import controllers, utils -from pyaggr3g470r.decorators import pyagg_default_decorator, \ - feed_access_required +from flask import Blueprint, g, render_template, flash, \ + redirect, request, url_for +from flask.ext.babel import gettext +from flask.ext.login import login_required + +import conf +from pyaggr3g470r import utils +from pyaggr3g470r.forms import AddFeedForm +from pyaggr3g470r.controllers import FeedController, ArticleController feeds_bp = Blueprint('feeds', __name__, url_prefix='/feeds') feed_bp = Blueprint('feed', __name__, url_prefix='/feed') + @feeds_bp.route('/', methods=['GET']) +@login_required def feeds(): "Lists the subscribed feeds in a table." return render_template('feeds.html', - feeds=controllers.FeedController(g.user.id).read()) + feeds=FeedController(g.user.id).read()) @feed_bp.route('/<int:feed_id>', methods=['GET']) -@pyagg_default_decorator -@feed_access_required +@login_required def feed(feed_id=None): "Presents detailed information about a feed." - feed = controllers.FeedController(g.user.id).get(id=feed_id) + feed = FeedController(g.user.id).get(id=feed_id) word_size = 6 - articles = controllers.ArticleController(g.user.id) \ - .read(feed_id=feed_id) \ - .order_by(desc("Article.date")).all() + articles = ArticleController(g.user.id) \ + .read(feed_id=feed_id) \ + .order_by(desc("Article.date")).all() top_words = utils.top_words(articles, n=50, size=int(word_size)) tag_cloud = utils.tag_cloud(top_words) @@ -51,3 +58,124 @@ def feed(feed_id=None): first_post_date=first_article, end_post_date=last_article, average=average, delta=delta, elapsed=elapsed) + + +@feed_bp.route('/delete/<feed_id>', methods=['GET']) +@login_required +def delete(feed_id=None): + feed_contr = FeedController(g.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')) + + +@feed_bp.route('/reset_errors/<int:feed_id>', methods=['GET', 'POST']) +@login_required +def reset_errors(feed_id): + feed_contr = FeedController(g.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_bp.route('/bookmarklet', methods=['GET']) +@login_required +def bookmarklet(): + feed_contr = FeedController(g.user.id) + url = request.args.get('url', None) + if not url: + flash(gettext("Couldn't add feed: url missing."), "error") + raise BadRequest("url is missing") + + existing_feeds = list(feed_contr.read(link=url)) + if existing_feeds: + flash(gettext("Couldn't add feed: feed already exists."), + "warning") + return redirect(url_for('feed.form', + feed_id=existing_feeds[0].id)) + + feed = feed_contr.create(link=url) + flash(gettext('Feed was successfully created.'), 'success') + return redirect(url_for('feed.form', feed_id=feed.id)) + + +@feed_bp.route('/read/<int:feed_id>', methods=['GET', 'POST']) +@login_required +def read(feed_id): + FeedController(g.user.id).update(readed=True) + flash(gettext('Feed successfully updated.', + feed_title=feed.title), 'success') + return redirect(request.referrer or url_for('home')) + + +@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} + if feed_id: + filters['feed_id'] = feed_id + ArticleController(g.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', 'POST', 'PUT']) +@feed_bp.route('/edit/<int:feed_id>', methods=['GET', 'POST']) +@login_required +def form(feed_id=None): + form = AddFeedForm() + feed_contr = FeedController(g.user.id) + + if request.method == 'POST': + if not form.validate(): + 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)) + + # Edit an existing feed + if feed_id is not None: + feed_contr.update({'id': feed_id}, + {'title': form.title.data, + 'link': form.link.data, + 'enabled': form.enabled.data, + 'site_link': form.site_link.data}) + flash(gettext('Feed %(feed_title)r successfully updated.', + feed_title=form.title.data), 'success') + return redirect(url_for('feed.form', feed_id=feed_id)) + + # Create a new feed + new_feed = FeedController(g.user.id).create( + title=form.title.data, + description="", + link=form.link.data, + site_link=form.site_link.data, + enabled=form.enabled.data) + + flash(gettext('Feed %(feed_title)r successfully created.', + feed_title=new_feed.title), 'success') + + return redirect(url_for('feed.form', + feed_id=new_feed.id)) + + # Getting the form for an existing feed + if feed_id is not None: + feed = FeedController(g.user.id).get(id=feed_id) + form = AddFeedForm(obj=feed) + return render_template('edit_feed.html', + action=gettext("Edit the feed"), + form=form, feed=feed, + not_on_heroku=not conf.ON_HEROKU) + + # Return an empty form in order to create a new feed + return render_template('edit_feed.html', action=gettext("Add a feed"), + form=form, not_on_heroku=not conf.ON_HEROKU) diff --git a/pyaggr3g470r/views/views.py b/pyaggr3g470r/views/views.py index 17d79cab..e06e1a9d 100644 --- a/pyaggr3g470r/views/views.py +++ b/pyaggr3g470r/views/views.py @@ -34,25 +34,25 @@ import datetime from collections import namedtuple from bootstrap import application as app, db from flask import render_template, request, flash, session, \ - url_for, redirect, g, current_app, make_response, jsonify + url_for, redirect, g, current_app, make_response from flask.ext.login import LoginManager, login_user, logout_user, \ login_required, current_user, AnonymousUserMixin from flask.ext.principal import Principal, Identity, AnonymousIdentity, \ identity_changed, identity_loaded, Permission,\ RoleNeed, UserNeed from flask.ext.babel import gettext -from sqlalchemy import func, or_ +from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from werkzeug import generate_password_hash import conf from pyaggr3g470r import utils, notifications, export -from pyaggr3g470r import controllers from pyaggr3g470r.models import User, Feed, Article, Role from pyaggr3g470r.decorators import feed_access_required from pyaggr3g470r.forms import SignupForm, SigninForm, AddFeedForm, \ ProfileForm, InformationMessageForm, RecoverPasswordForm -from pyaggr3g470r.controllers import FeedController +from pyaggr3g470r.controllers import UserController, FeedController, \ + ArticleController if not conf.ON_HEROKU: import pyaggr3g470r.search as fastsearch @@ -93,7 +93,7 @@ def before_request(): @login_manager.user_loader def load_user(email): # Return an instance of the User model - return controllers.UserController().get(email=email) + return UserController().get(email=email) # @@ -153,7 +153,7 @@ def login(): form = SigninForm() if form.validate_on_submit(): - user = controllers.UserController().get(email=form.email.data) + user = UserController().get(email=form.email.data) login_user(user) g.user = user session['email'] = form.email.data @@ -225,39 +225,60 @@ def signup(): @app.route('/') @login_required -def home(): +def home(favorites=False): """ Home page for connected users. Displays by default unread articles. """ - feeds = {feed.id: feed.title for feed in g.user.feeds} - articles = Article.query.filter(Article.feed_id.in_(feeds.keys()), - Article.user_id == g.user.id) - filter_ = request.args.get('filter_', 'unread') + head_title = gettext('Favorites') if favorites else '' + feed_contr = FeedController(g.user.id) + arti_contr = ArticleController(g.user.id) + feeds = {feed.id: feed.title for feed in feed_contr.read()} + + unread = arti_contr.get_unread() + in_error = {feed.id: feed.error_count for feed in + feed_contr.read(error_count__gt=2)} + + filter_ = request.args.get('filter_', 'all' if favorites else 'unread') feed_id = int(request.args.get('feed', 0)) limit = request.args.get('limit', 1000) + + filters = {} + if favorites: + filters['like'] = True if filter_ != 'all': - articles = articles.filter(Article.readed == (filter_ == 'read')) + filters['readed'] = filter_ == 'read' if feed_id: - articles = articles.filter(Article.feed_id == feed_id) + filters['feed_id'] = feed_id + if head_title: + head_title += ' - ' + head_title += feed_contr.get(id=feed_id).title - articles = articles.order_by(Article.date.desc()) + articles = arti_contr.read(**filters).order_by(Article.date.desc()) if limit != 'all': limit = int(limit) articles = articles.limit(limit) - unread = db.session.query(Article.feed_id, func.count(Article.id))\ - .filter(Article.readed == False, Article.user_id == g.user.id)\ - .group_by(Article.feed_id).all() - in_error = {feed.id: feed.error_count for feed in - FeedController(g.user.id).read(error_count__gt=2).all()} + def gen_url(filter_=filter_, limit=limit, feed=feed_id): - return '?filter_=%s&limit=%s&feed=%d' % (filter_, limit, feed) + return url_for('favorites' if favorites else 'home', + filter_=filter_, limit=limit, feed=feed) + + articles = list(articles) + if not articles and not favorites and feed_id: + return redirect(gen_url(filter_='all')) + return render_template('home.html', gen_url=gen_url, feed_id=feed_id, filter_=filter_, limit=limit, feeds=feeds, - unread=dict(unread), articles=articles.all(), - in_error=in_error, + unread=unread, articles=articles, in_error=in_error, + head_title=head_title, favorites=favorites, default_max_error = conf.DEFAULT_MAX_ERROR) +@app.route('/favorites') +@login_required +def favorites(): + return home(favorites=True) + + @app.route('/fetch', methods=['GET']) @app.route('/fetch/<int:feed_id>', methods=['GET']) @login_required @@ -283,7 +304,6 @@ def about(): @app.route('/mark_as/<string:new_value>', methods=['GET']) -@app.route('/mark_as/<string:new_value>/feed/<int:feed_id>', methods=['GET']) @app.route('/mark_as/<string:new_value>/article/<int:article_id>', methods=['GET']) @login_required @feed_access_required @@ -342,40 +362,6 @@ def delete(article_id=None): return redirect(url_for('home')) -@app.route('/favorites', methods=['GET']) -@login_required -def favorites(): - """ - List favorites articles. - """ - feeds_with_like = Feed.query.filter(Feed.user_id == g.user.id, Feed.articles.any(like=True)) - result, nb_favorites = [], 0 - light_feed = namedtuple('Feed', ['id', 'title', 'articles'], verbose=False, rename=False) - for feed in feeds_with_like: - articles = Article.query.filter(Article.user_id == g.user.id, Article.feed_id == feed.id, Article.like == True).all() - result.append(light_feed(feed.id, feed.title, articles)) - nb_favorites += len(articles) - return render_template('favorites.html', feeds=result, nb_favorites=nb_favorites) - -@app.route('/unread/<int:feed_id>', methods=['GET']) -@app.route('/unread', methods=['GET']) -@login_required -def unread(feed_id=None): - """ - List unread articles. - """ - if feed_id is not None: - feeds_with_unread = Feed.query.filter(Feed.user_id == g.user.id, Feed.id == feed_id) - else: - feeds_with_unread = Feed.query.filter(Feed.user_id == g.user.id, Feed.articles.any(readed=False)) - result, nb_unread = [], 0 - light_feed = namedtuple('Feed', ['id', 'title', 'articles'], verbose=False, rename=False) - for feed in feeds_with_unread: - articles = Article.query.filter(Article.user_id == g.user.id, Article.feed_id == feed.id, Article.readed == False).all() - result.append(light_feed(feed.id, feed.title, articles)) - nb_unread += len(articles) - return render_template('unread.html', feeds=result, nb_unread=nb_unread) - @app.route('/inactives', methods=['GET']) @login_required def inactives(): @@ -383,7 +369,7 @@ def inactives(): List of inactive feeds. """ nb_days = int(request.args.get('nb_days', 365)) - user = controllers.UserController(g.user.id).get(email=g.user.email) + user = UserController(g.user.id).get(email=g.user.email) today = datetime.datetime.now() inactives = [] for feed in user.feeds: @@ -430,7 +416,7 @@ def export_articles(): """ Export all articles to HTML or JSON. """ - user = controllers.UserController(g.user.id).get(id=g.user.id) + user = UserController(g.user.id).get(id=g.user.id) if request.args.get('format') == "HTML": # Export to HTML try: @@ -463,7 +449,7 @@ def export_opml(): """ Export all feeds to OPML. """ - user = controllers.UserController(g.user.id).get(id=g.user.id) + user = UserController(g.user.id).get(id=g.user.id) response = make_response(render_template('opml.xml', user=user, now=datetime.datetime.now())) response.headers['Content-Type'] = 'application/xml' @@ -557,82 +543,6 @@ def history(year=None, month=None): articles=articles, year=year, month=month) -@app.route('/bookmarklet', methods=['GET']) -@app.route('/create_feed', methods=['GET', 'POST']) -@app.route('/edit_feed/<int:feed_id>', methods=['GET', 'POST']) -@login_required -@feed_access_required -def edit_feed(feed_id=None): - """ - Add or edit a feed. - """ - form = AddFeedForm() - - if request.method == 'POST': - if form.validate() == False: - return render_template('edit_feed.html', form=form) - if feed_id is not None: - # Edit an existing feed - feed = FeedController(g.user.id).get(id=feed_id) - form.populate_obj(feed) - if feed.enabled: - # set the error count to 0 - feed.error_count = 0 - feed.last_error = "" - db.session.commit() - flash(gettext('Feed successfully updated.'), 'success') - return redirect('/edit_feed/' + str(feed_id)) - else: - # Create a new feed - existing_feed = [f for f in g.user.feeds if f.link == form.link.data] - if len(existing_feed) == 0: - new_feed = Feed(title=form.title.data, description="", link=form.link.data, \ - site_link=form.site_link.data, enabled=form.enabled.data) - g.user.feeds.append(new_feed) - #user.feeds = sorted(user.feeds, key=lambda t: t.title.lower()) - db.session.commit() - flash(gettext('Feed successfully created.'), 'success') - - utils.fetch(g.user.id, Feed.query.filter(Feed.link == form.link.data).first().id) - flash(gettext("Downloading articles for the new feed..."), 'info') - - return redirect('/edit_feed/' + str(new_feed.id)) - else: - flash(gettext('Feed already in the database.'), 'warning') - return redirect('/edit_feed/' + str(existing_feed[0].id)) - - if request.method == 'GET': - if feed_id is not None: - feed = FeedController(g.user.id).get(id=feed_id) - form = AddFeedForm(obj=feed) - return render_template('edit_feed.html', action=gettext("Edit the feed"), form=form, feed=feed, \ - not_on_heroku = not conf.ON_HEROKU) - - # Enable the user to add a feed with a bookmarklet - if None is not request.args.get('url', None): - existing_feed = [f for f in g.user.feeds if feed.link == request.args.get('url', None)] - if len(existing_feed) == 0: - g.user.feeds.append(Feed(link=request.args.get('url', None))) - db.session.commit() - return jsonify({"message":"ok"}) - return jsonify({"message":"Feed already in the database."}) - - # Return an empty form in order to create a new feed - return render_template('edit_feed.html', action=gettext("Add a feed"), form=form, \ - not_on_heroku = not conf.ON_HEROKU) - -@app.route('/delete_feed/<feed_id>', methods=['GET']) -@login_required -@feed_access_required -def delete_feed(feed_id=None): - """ - Delete a feed with all associated articles. - """ - feed = Feed.query.filter(Feed.id == feed_id).first() - db.session.delete(feed) - db.session.commit() - flash(gettext('Feed') + ' ' + feed.title + ' ' + gettext('successfully deleted.'), 'success') - return redirect(redirect_url()) @app.route('/profile', methods=['GET', 'POST']) @login_required @@ -640,7 +550,7 @@ def profile(): """ Edit the profile of the currently logged user. """ - user = controllers.UserController(g.user.id).get(id=g.user.id) + user = UserController(g.user.id).get(id=g.user.id) form = ProfileForm() if request.method == 'POST': @@ -666,7 +576,7 @@ def delete_account(): """ Delete the account of the user (with all its data). """ - user = controllers.UserController(g.user.id).get(id=g.user.id) + user = UserController(g.user.id).get(id=g.user.id) if user is not None: db.session.delete(user) db.session.commit() |