aboutsummaryrefslogtreecommitdiff
path: root/pyaggr3g470r
diff options
context:
space:
mode:
Diffstat (limited to 'pyaggr3g470r')
-rw-r--r--pyaggr3g470r/controllers/abstract.py1
-rw-r--r--pyaggr3g470r/controllers/user.py2
-rw-r--r--pyaggr3g470r/crawler.py22
-rw-r--r--pyaggr3g470r/duplicate.py20
-rw-r--r--pyaggr3g470r/lib/crawler.py1
-rw-r--r--pyaggr3g470r/models/__init__.py68
-rw-r--r--pyaggr3g470r/models/article.py2
-rw-r--r--pyaggr3g470r/models/feed.py2
-rw-r--r--pyaggr3g470r/notifications.py18
-rw-r--r--pyaggr3g470r/static/css/customized-bootstrap.css44
-rw-r--r--pyaggr3g470r/static/css/side-nav.css50
-rw-r--r--pyaggr3g470r/static/js/articles.js35
-rw-r--r--pyaggr3g470r/templates/about.html6
-rw-r--r--pyaggr3g470r/templates/admin/create_user.html2
-rw-r--r--pyaggr3g470r/templates/admin/user.html4
-rw-r--r--pyaggr3g470r/templates/article.html8
-rw-r--r--pyaggr3g470r/templates/articles.html2
-rw-r--r--pyaggr3g470r/templates/duplicates.html12
-rw-r--r--pyaggr3g470r/templates/edit_feed.html2
-rw-r--r--pyaggr3g470r/templates/errors/404.html2
-rw-r--r--pyaggr3g470r/templates/errors/500.html2
-rw-r--r--pyaggr3g470r/templates/feed.html20
-rw-r--r--pyaggr3g470r/templates/history.html96
-rw-r--r--pyaggr3g470r/templates/home.html4
-rw-r--r--pyaggr3g470r/templates/inactives.html2
-rw-r--r--pyaggr3g470r/templates/layout.html71
-rw-r--r--pyaggr3g470r/templates/login.html4
-rw-r--r--pyaggr3g470r/templates/management.html8
-rw-r--r--pyaggr3g470r/templates/profile.html2
-rw-r--r--pyaggr3g470r/templates/recover.html4
-rw-r--r--pyaggr3g470r/templates/signup.html2
-rw-r--r--pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mobin14305 -> 15562 bytes
-rw-r--r--pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po610
-rwxr-xr-xpyaggr3g470r/utils.py146
-rw-r--r--pyaggr3g470r/views/api/article.py3
-rw-r--r--pyaggr3g470r/views/api/common.py26
-rw-r--r--pyaggr3g470r/views/api/feed.py9
-rw-r--r--pyaggr3g470r/views/article.py10
-rw-r--r--pyaggr3g470r/views/feed.py11
-rw-r--r--pyaggr3g470r/views/views.py45
40 files changed, 777 insertions, 601 deletions
diff --git a/pyaggr3g470r/controllers/abstract.py b/pyaggr3g470r/controllers/abstract.py
index a99e67f3..c084deb9 100644
--- a/pyaggr3g470r/controllers/abstract.py
+++ b/pyaggr3g470r/controllers/abstract.py
@@ -1,6 +1,5 @@
import logging
from bootstrap import db
-from sqlalchemy import update
from werkzeug.exceptions import Forbidden, NotFound
logger = logging.getLogger(__name__)
diff --git a/pyaggr3g470r/controllers/user.py b/pyaggr3g470r/controllers/user.py
index c6c1d545..ed46e1e7 100644
--- a/pyaggr3g470r/controllers/user.py
+++ b/pyaggr3g470r/controllers/user.py
@@ -4,4 +4,4 @@ from pyaggr3g470r.models import User
class UserController(AbstractController):
_db_cls = User
- _user_id_key = 'id'
+ _user_id_key = 'email'
diff --git a/pyaggr3g470r/crawler.py b/pyaggr3g470r/crawler.py
index ebcb8ce4..ded9df6f 100644
--- a/pyaggr3g470r/crawler.py
+++ b/pyaggr3g470r/crawler.py
@@ -56,7 +56,7 @@ def get(*args, **kwargs):
return (yield from response.read_and_close(decode=False))
except Exception as e:
#print(e)
- return None
+ raise e
@asyncio.coroutine
def parse_feed(user, feed):
@@ -66,14 +66,17 @@ def parse_feed(user, feed):
data = None
with (yield from sem):
- data = yield from get(feed.link)
-
- if data is None:
- feed.error_count += 1
- if feed.error_count > 2:
- feed.enabled = False
- db.session.commit()
- return
+ try:
+ data = yield from get(feed.link)
+ except Exception as e:
+ feed.last_error = str(e)
+ finally:
+ if data is None:
+ feed.error_count += 1
+ if feed.error_count > 2:
+ feed.enabled = False
+ db.session.commit()
+ return
a_feed = feedparser.parse(data)
if a_feed['bozo'] == 1:
@@ -88,6 +91,7 @@ def parse_feed(user, feed):
feed.last_retrieved = datetime.now(dateutil.tz.tzlocal())
feed.error_count = 0
+ feed.last_error = ""
# Feed informations
if feed.title == "":
diff --git a/pyaggr3g470r/duplicate.py b/pyaggr3g470r/duplicate.py
deleted file mode 100644
index d4c6e31a..00000000
--- a/pyaggr3g470r/duplicate.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#! /usr/bin/env python
-#-*- coding: utf-8 -*-
-
-import itertools
-from datetime import timedelta
-
-from pyaggr3g470r import utils
-
-def compare_documents(feed):
- """
- Compare a list of documents by pair.
- """
- duplicates = []
- for pair in itertools.combinations(feed.articles, 2):
- date1 = pair[0].date
- date2 = pair[1].date
- if utils.clear_string(pair[0].title) == utils.clear_string(pair[1].title) and \
- (date1 - date2) < timedelta(days = 1):
- duplicates.append(pair)
- return duplicates \ No newline at end of file
diff --git a/pyaggr3g470r/lib/crawler.py b/pyaggr3g470r/lib/crawler.py
index 1ac6029a..1cb61973 100644
--- a/pyaggr3g470r/lib/crawler.py
+++ b/pyaggr3g470r/lib/crawler.py
@@ -27,6 +27,7 @@ from requests_futures.sessions import FuturesSession
from pyaggr3g470r.lib.utils import default_handler
logger = logging.getLogger(__name__)
+logging.captureWarnings(True)
API_ROOT = "api/v2.0/"
diff --git a/pyaggr3g470r/models/__init__.py b/pyaggr3g470r/models/__init__.py
index 9584d1f2..42903f4e 100644
--- a/pyaggr3g470r/models/__init__.py
+++ b/pyaggr3g470r/models/__init__.py
@@ -31,5 +31,71 @@ from .role import Role
from .user import User
from .article import Article
-
__all__ = ['Feed', 'Role', 'User', 'Article']
+
+import os
+
+from werkzeug import generate_password_hash
+
+from sqlalchemy.engine import reflection
+from sqlalchemy.schema import (
+ MetaData,
+ Table,
+ DropTable,
+ ForeignKeyConstraint,
+ DropConstraint)
+
+def db_empty(db):
+ "Will drop every datas stocked in db."
+ # From http://www.sqlalchemy.org/trac/wiki/UsageRecipes/DropEverything
+ conn = db.engine.connect()
+
+ # the transaction only applies if the DB supports
+ # transactional DDL, i.e. Postgresql, MS SQL Server
+ trans = conn.begin()
+
+ inspector = reflection.Inspector.from_engine(db.engine)
+
+ # gather all data first before dropping anything.
+ # some DBs lock after things have been dropped in
+ # a transaction.
+ metadata = MetaData()
+
+ tbs = []
+ all_fks = []
+
+ for table_name in inspector.get_table_names():
+ fks = []
+ for fk in inspector.get_foreign_keys(table_name):
+ if not fk['name']:
+ continue
+ fks.append(ForeignKeyConstraint((), (), name=fk['name']))
+ t = Table(table_name, metadata, *fks)
+ tbs.append(t)
+ all_fks.extend(fks)
+
+ for fkc in all_fks:
+ conn.execute(DropConstraint(fkc))
+
+ for table in tbs:
+ conn.execute(DropTable(table))
+
+ trans.commit()
+
+def db_create(db):
+ "Will create the database from conf parameters."
+ db.create_all()
+
+ role_admin = Role(name="admin")
+ role_user = Role(name="user")
+
+ user1 = User(nickname="admin",
+ email=os.environ.get("ADMIN_EMAIL",
+ "root@pyAggr3g470r.localhost"),
+ pwdhash=generate_password_hash(
+ os.environ.get("ADMIN_PASSWORD", "password")),
+ activation_key="")
+ user1.roles.extend([role_admin, role_user])
+
+ db.session.add(user1)
+ db.session.commit() \ No newline at end of file
diff --git a/pyaggr3g470r/models/article.py b/pyaggr3g470r/models/article.py
index f8f9d2d8..58cd0384 100644
--- a/pyaggr3g470r/models/article.py
+++ b/pyaggr3g470r/models/article.py
@@ -4,7 +4,7 @@
# pyAggr3g470r - A Web based news aggregator.
# Copyright (C) 2010-2015 Cédric Bonhomme - https://www.cedricbonhomme.org
#
-# For more information : https://bitbucket.org/cedricbonhomme/pyaggr3g470r/
+# For more information : https://bitbucket.org/cedricbonhomme/pyaggr3g470r
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
diff --git a/pyaggr3g470r/models/feed.py b/pyaggr3g470r/models/feed.py
index a37744d6..a36d9573 100644
--- a/pyaggr3g470r/models/feed.py
+++ b/pyaggr3g470r/models/feed.py
@@ -33,7 +33,7 @@ from sqlalchemy import desc
class Feed(db.Model):
"""
- Represent a station.
+ Represent a feed.
"""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(), default="No title")
diff --git a/pyaggr3g470r/notifications.py b/pyaggr3g470r/notifications.py
index cf8fb723..006aa594 100644
--- a/pyaggr3g470r/notifications.py
+++ b/pyaggr3g470r/notifications.py
@@ -30,10 +30,12 @@ def information_message(subject, plaintext):
from pyaggr3g470r.models import User
users = User.query.all()
# Only send email for activated accounts.
- emails = [user.email for user in users if user.activation_key == ""]
+ user_emails = [user.email for user in users if user.activation_key == ""]
# Postmark has a limit of twenty recipients per message in total.
- for i in xrange(0, len(emails), 19):
- emails.send(to=conf.NOTIFICATION_EMAIL, bcc=", ".join(emails[i:i+19]), subject=subject, plaintext=plaintext)
+ for i in xrange(0, len(user_emails), 19):
+ emails.send(to=conf.NOTIFICATION_EMAIL,
+ bcc=", ".join(user_emails[i:i+19]),
+ subject=subject, plaintext=plaintext)
def new_account_notification(user):
"""
@@ -41,7 +43,8 @@ def new_account_notification(user):
"""
plaintext = """Hello,\n\nYour account has been created. Click on the following link to confirm it:\n%s\n\nSee you,""" % \
(conf.PLATFORM_URL + 'confirm_account/' + user.activation_key)
- emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL, subject="[pyAggr3g470r] Account creation", plaintext=plaintext)
+ emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL,
+ subject="[pyAggr3g470r] Account creation", plaintext=plaintext)
def new_account_activation(user):
"""
@@ -49,7 +52,8 @@ def new_account_activation(user):
"""
plaintext = """Hello,\n\nYour account has been activated. You can now connect to the platform:\n%s\n\nSee you,""" % \
(conf.PLATFORM_URL)
- emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL, subject="[pyAggr3g470r] Account activated", plaintext=plaintext)
+ emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL,
+ subject="[pyAggr3g470r] Account activated", plaintext=plaintext)
def new_password_notification(user, password):
"""
@@ -58,4 +62,6 @@ def new_password_notification(user, password):
plaintext = """Hello,\n\nA new password has been generated at your request:\n\n%s""" % \
(password, )
plaintext += "\n\nIt is advised to replace it as soon as connected to pyAggr3g470r.\n\nSee you,"
- emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL, subject="[pyAggr3g470r] New password", plaintext=plaintext)
+ emails.send(to=user.email,
+ bcc=conf.NOTIFICATION_EMAIL,
+ subject="[pyAggr3g470r] New password", plaintext=plaintext)
diff --git a/pyaggr3g470r/static/css/customized-bootstrap.css b/pyaggr3g470r/static/css/customized-bootstrap.css
new file mode 100644
index 00000000..58a9d182
--- /dev/null
+++ b/pyaggr3g470r/static/css/customized-bootstrap.css
@@ -0,0 +1,44 @@
+body {
+ margin-top: 50px;
+}
+div.top {
+ position: relative;
+ top: -50px;
+ display: block;
+ height: 0;
+}
+
+.navbar-custom {
+ background-color: #205081;
+ border: #205081;
+ color: #FFFFFF;
+ border-radius: 0;
+}
+
+.navbar-custom .navbar-nav > li > a {
+ color: #FFFFFF;
+}
+
+.navbar-custom .navbar-nav > li > a:hover {
+ background-color: #3572B0;
+}
+
+.navbar-custom .navbar-nav > .active > a,
+.navbar-nav > .active > a:hover,
+.navbar-nav > .active > a:focus {
+ color: #FFFFFF;
+ background-color: #3572B0;
+}
+.navbar-custom .navbar-brand {
+ color: #FFFFFF;
+}
+
+.navbar-custom .navbar-nav > .open > a,
+.navbar-custom .navbar-nav > .open > a:hover,
+.navbar-custom .navbar-nav > .open > a:focus {
+ color: #FFFFFF;
+ background-color: #3572B0;
+}
+a {
+ color: #3572B0;
+}
diff --git a/pyaggr3g470r/static/css/side-nav.css b/pyaggr3g470r/static/css/side-nav.css
new file mode 100644
index 00000000..5786bffa
--- /dev/null
+++ b/pyaggr3g470r/static/css/side-nav.css
@@ -0,0 +1,50 @@
+/* First level of nav */
+.sidenav {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ padding-top: 10px;
+ padding-bottom: 0px;
+ overflow-y: auto;
+ height: 90%;
+ z-index: 1000;
+ background-color: #FFFFFF;
+ border-radius: 2px;
+ font-size: 100%;
+}
+/* All levels of nav */
+.sidebar .nav > li > a {
+ display: block;
+ color: #3572B0;
+ padding: 5px 20px;
+}
+.sidebar .nav > li > a:hover,
+.sidebar .nav > li > a:focus {
+ text-decoration: none;
+ background-color: #F0FFFF;
+}
+
+.sidebar .nav > .active > a,
+.sidebar .nav > .active:hover > a,
+.sidebar .nav > .active:focus > a {
+ font-weight: bold;
+ color: #3572B0;
+ background-color: transparent;
+}
+
+.badge {
+ background-color: #3572B0;
+}
+
+/* Nav: second level */
+.sidebar .nav .nav {
+ margin-bottom: 8px;
+}
+.sidebar .nav .nav > li > a {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ font-size: 80%;
+}
+
+li.feed-commands {display: none; text-align: right;}
+li.feed-commands > span > a {margin-right: 10px;}
+li.feed-menu:hover + li.feed-commands, li.feed-commands:hover {display: block;}
diff --git a/pyaggr3g470r/static/js/articles.js b/pyaggr3g470r/static/js/articles.js
index 312a5cb6..a5ac82d0 100644
--- a/pyaggr3g470r/static/js/articles.js
+++ b/pyaggr3g470r/static/js/articles.js
@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-API_ROOT = 'api/v2.0/'
+API_ROOT = '/api/v2.0/'
if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') }
@@ -131,7 +131,7 @@ if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') }
var article_id = $(this).parent().parent().parent().attr("data-article");
$(this).parent().parent().parent().remove();
- // sends the updates to the server
+ // sends the updates to the server
$.ajax({
type: 'DELETE',
url: API_ROOT + "article/" + article_id,
@@ -144,4 +144,35 @@ if (typeof jQuery === 'undefined') { throw new Error('Requires jQuery') }
});
});
+
+ // Delete all duplicate articles (used in the page /duplicates)
+ $('.delete-all').click(function(){
+ var data = [];
+
+ var columnNo = $(this).parent().index();
+ $(this).closest("table")
+ .find("tr td:nth-child(" + (columnNo+1) + ")")
+ .each(function(line, column) {
+ data.push(parseInt(column.id));
+ }).remove();
+
+ data = JSON.stringify(data);
+
+ // sends the updates to the server
+ $.ajax({
+ type: 'DELETE',
+ // Provide correct Content-Type, so that Flask will know how to process it.
+ contentType: 'application/json',
+ data: data,
+ url: API_ROOT + "articles",
+ success: function (result) {
+ //console.log(result);
+ },
+ error: function(XMLHttpRequest, textStatus, errorThrown){
+ console.log(XMLHttpRequest.responseText);
+ }
+ });
+
+ });
+
}(jQuery);
diff --git a/pyaggr3g470r/templates/about.html b/pyaggr3g470r/templates/about.html
index 84315271..08b80fbb 100644
--- a/pyaggr3g470r/templates/about.html
+++ b/pyaggr3g470r/templates/about.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h1>{{ _('About') }}</h1>
<p>
{{ _('pyAggr3g470r is a news aggregator platform and can be shared between several users.') }}
@@ -13,13 +13,13 @@
according to the <a href="https://www.gnu.org/licenses/agpl-3.0.html">Affero GPL</a> license.') }}</p>
<p>{{ _('Found a bug? Report it <a href="https://bitbucket.org/cedricbonhomme/pyaggr3g470r/issues">here</a>.') }}</p>
</div>
- <div class="jumbotron">
+ <div class="well">
<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>
</div>
- <div class="jumbotron">
+ <div class="well">
<h1>{{ _('Donation') }}</h1>
<p>{{ _('If you wish and if you like pyAggr3g470r, you can donate via bitcoin <a href="https://blockexplorer.com/address/1GVmhR9fbBeEh7rP1qNq76jWArDdDQ3otZ">1GVmhR9fbBeEh7rP1qNq76jWArDdDQ3otZ</a>. Thank you!') }}</p>
</div>
diff --git a/pyaggr3g470r/templates/admin/create_user.html b/pyaggr3g470r/templates/admin/create_user.html
index 833ae601..1d6d6c11 100644
--- a/pyaggr3g470r/templates/admin/create_user.html
+++ b/pyaggr3g470r/templates/admin/create_user.html
@@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h2>{{ message | safe }}</h2>
<form action="" method="post" name="saveprofileform" id="profileform">
{{ form.hidden_tag() }}
diff --git a/pyaggr3g470r/templates/admin/user.html b/pyaggr3g470r/templates/admin/user.html
index 046093e1..f20d53dd 100644
--- a/pyaggr3g470r/templates/admin/user.html
+++ b/pyaggr3g470r/templates/admin/user.html
@@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<a href="/admin/edit_user/{{ user.id }}" class="btn btn-default">{{ _('Edit this user') }}</a>
<h2>{{ _('Membership') }}</h2>
<div class="row">
@@ -14,7 +14,7 @@
</div>
</div>
</div>
- <div class="jumbotron">
+ <div class="well">
{% if user.feeds.all()|count == 0 %}
<h1>{{ _('This user is not subscribed to any feed.') }}</h1>
{% else %}
diff --git a/pyaggr3g470r/templates/article.html b/pyaggr3g470r/templates/article.html
index 101cf628..92014599 100644
--- a/pyaggr3g470r/templates/article.html
+++ b/pyaggr3g470r/templates/article.html
@@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<div class="container" data-article="{{ article.id }}">
- <div class="jumbotron">
+ <div class="well">
<h2><a href="{{ article.link }}" target="_blank">{{ article.title|safe }}</a></h2>
<h3>{{ _('from') }} <a href="/feed/{{ article.source.id }}">{{ article.source.title }}</a></h3>
<a href="/delete/{{ article.id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this article') }}"></i></a>
@@ -21,10 +21,10 @@
{% endif %}
<h6>{{ article.date | datetime }}</h6>
</div>
- <div class="jumbotron">
+ <div class="well">
{{ article.content | safe }}
</div>
- <div class="jumbotron">
+ <div class="well">
<div class="row">
<div class="col-md-6">
{{ _('Next post:') }} <a href="/article/{{ next_article.id }}">{{ next_article.title }}</a>
@@ -34,7 +34,7 @@
</div>
</div>
</div>
- <div class="jumbotron">
+ <div class="well">
<a href="https://api.pinboard.in/v1/posts/add?url={{ article.link }}&description={{ article.title }}" rel="noreferrer" target="_blank">
<img src="/static/img/pinboard.png" title="{{ _('Share on') }} Pinboard" />
</a>
diff --git a/pyaggr3g470r/templates/articles.html b/pyaggr3g470r/templates/articles.html
index eecb9579..383c28a4 100644
--- a/pyaggr3g470r/templates/articles.html
+++ b/pyaggr3g470r/templates/articles.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h2><a href="{{ feed.site_link }}">{{ feed.title|safe }}</a></h2>
<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>
diff --git a/pyaggr3g470r/templates/duplicates.html b/pyaggr3g470r/templates/duplicates.html
index a7eff2d0..4d7ac650 100644
--- a/pyaggr3g470r/templates/duplicates.html
+++ b/pyaggr3g470r/templates/duplicates.html
@@ -8,16 +8,20 @@
<thead>
<tr>
<th>#</th>
- <th></th>
- <th></th>
+ <th align="center">
+ <span class="delete-all btn btn-default">{{ _('Delete all in this column') }}</span>
+ </th>
+ <th align="center">
+ <span class="delete-all btn btn-default">{{ _('Delete all in this column') }}</span>
+ </th>
</tr>
</thead>
<tbody>
{% for pair in duplicates %}
<tr>
<td>{{ loop.index }}</td>
- <td><a href="/delete/{{ pair[0].id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this article') }}"></i></a>&nbsp;<a href="/article/{{ pair[0].id }}">{{ pair[0].title }}</a></td>
- <td><a href="/delete/{{ pair[1].id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this article') }}"></i></a>&nbsp;<a href="/article/{{ pair[1].id }}">{{ pair[1].title }}</a></td>
+ <td id="{{ pair[0].id }}"><a href="/delete/{{ pair[0].id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this article') }}"></i></a>&nbsp;<a href="/article/{{ pair[0].id }}">{{ pair[0].title }}</a> ({{ pair[0].retrieved_date }})</td>
+ <td id="{{ pair[1].id }}"><a href="/delete/{{ pair[1].id }}"><i class="glyphicon glyphicon-remove" title="{{ _('Delete this article') }}"></i></a>&nbsp;<a href="/article/{{ pair[1].id }}">{{ pair[1].title }}</a> ({{ pair[1].retrieved_date }})</td>
</tr>
{% endfor %}
</tobdy>
diff --git a/pyaggr3g470r/templates/edit_feed.html b/pyaggr3g470r/templates/edit_feed.html
index a6b28ded..e9a90960 100644
--- a/pyaggr3g470r/templates/edit_feed.html
+++ b/pyaggr3g470r/templates/edit_feed.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h1>{{ action }}</h1>
<form action="" method="post" name="save">
{{ form.hidden_tag() }}
diff --git a/pyaggr3g470r/templates/errors/404.html b/pyaggr3g470r/templates/errors/404.html
index 49c9ef5a..c64a2be8 100644
--- a/pyaggr3g470r/templates/errors/404.html
+++ b/pyaggr3g470r/templates/errors/404.html
@@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h1>Page Not Found</h1>
<p>What you were looking for is just not there, go to the <a href="{{ url_for('home') }}">home page</a>.</p>
</div>
diff --git a/pyaggr3g470r/templates/errors/500.html b/pyaggr3g470r/templates/errors/500.html
index 1fa6acc9..417fc0c7 100644
--- a/pyaggr3g470r/templates/errors/500.html
+++ b/pyaggr3g470r/templates/errors/500.html
@@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h1>Internal Server Error</h1>
<p>Something bad just happened! Go to the <a href="{{ url_for('home') }}">home page</a>.</p>
</div>
diff --git a/pyaggr3g470r/templates/feed.html b/pyaggr3g470r/templates/feed.html
index 4b050573..268cbf7d 100644
--- a/pyaggr3g470r/templates/feed.html
+++ b/pyaggr3g470r/templates/feed.html
@@ -1,19 +1,22 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <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>
</div>
- <div class="jumbotron">
+ <div class="well">
<p>
- {{ _('This feed contains') }} {{ feed.articles.all()|count }} <a href="/articles/{{ feed.id }}/100">{{ _('articles') }}</a>
- {% if nb_articles != 0 %}
- ({{ ((feed.articles.all()|count * 100 ) / nb_articles) | round(2, 'floor') }}% {{ _('of the database') }}).<br />
+ {{ _('This feed contains') }} {{ feed.articles.all()|count }} <a href="/articles/{{ feed.id }}/100">{{ _('articles') }}</a>.<br />
+ {{ _('Address of the feed') }}: <a href="{{ feed.link }}" target="_blank">{{ feed.link }}</a><br />
+ {% if feed.site_link != "" %}
+ {{ _('Address of the site') }}: <a href="{{ feed.site_link }}" target="_blank">{{ feed.site_link }}</a><br />
{% endif %}
+ <br />
+
{% if feed.last_retrieved %}
{{ _("Last download:") }} {{ feed.last_retrieved | datetime }}<br />
{% endif %}
@@ -28,18 +31,13 @@
{{ _("Here's the last error encountered while retrieving this feed:") }} <pre>{{ feed.last_error }}</pre><br />
{% endif %}
- {{ _('Address of the feed') }}: <a href="{{ feed.link }}">{{ feed.link }}</a><br />
- {% if feed.site_link != "" %}
- {{ _('Address of the site') }}: <a href="{{ feed.site_link }}">{{ feed.site_link }}</a><br />
- {% endif %}
-
{% if feed.articles.all()|count != 0 %}
{{ _('The last article was posted') }} {{ elapsed.days }} {{ _('day(s) ago.') }}<br />
{{ _('Daily average') }}: {{ average }}, {{ _('between the') }} {{ first_post_date | datetime }} {{ _('and the') }} {{ end_post_date | datetime }}.
{% endif %}
</p>
</div>
- <div class="jumbotron">
+ <div class="well">
{% if feed.articles.all()|count != 0 %}
<div>{{ tag_cloud|safe }}</div>
{% endif %}
diff --git a/pyaggr3g470r/templates/history.html b/pyaggr3g470r/templates/history.html
index 0194cb89..6be54d71 100644
--- a/pyaggr3g470r/templates/history.html
+++ b/pyaggr3g470r/templates/history.html
@@ -1,74 +1,26 @@
- {% extends "layout.html" %}
- {% block head %}
-{{ super() }}
-<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
-<style type="text/css">
-rect {
- fill: purple ;
- padding: 7px;
- margin: 2px;
- color: white;
- }
-rect:hover
- {
- opacity : 0.5;
- }
-</style>
-{% endblock %}
-
+{% extends "layout.html" %}
{% block content %}
<div class="container">
-<h1>History</h1>
-<div align="center" id="pie_chart">
- <div class="chart">
- </div>
-
-
-<script type="text/javascript">
-function createGraph(dataset, w, h) {
- var barPadding = 1;
-
- //Create SVG element
- var svg = d3.select("#pie_chart .chart")
- .append("svg")
- .attr("width", w)
- .attr("height", h);
-
- svg.selectAll("rect")
- .data(dataset)
- .enter()
- .append("rect")
- .on("click", clickEvent)
- .attr("x", function(d, i) {
- return i * (w / dataset.length);
- })
- .attr("y", function(d) {
- return h - (d * 4);
- })
- .attr("width", w / dataset.length - barPadding)
- .attr("height", function(d) {
- return d * 4;
- });
-}
-
-function clickEvent(d){
- //var op = prompt("Please enter the value", "");
- alert(d);
-
-};
-
-
-
-var w = 800;
-var h = 100;
-var dataset = [ 12, 10, 13, 19, 21, 25, 22, 18, 15, 13,
- 11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];
-
-createGraph(dataset, w, h);
-
-
-</script>
-
-
-</div>
-{% endblock %} \ No newline at end of file
+ <h1>{{ _('History') }}</h1>
+ {% if month != None %}
+ <h2><a href="/history/{{ year }}"><span class="glyphicon glyphicon-chevron-left"></span> {{ year }}</a></h2>
+ <h3>{{ month | month_name }}</h3>
+ {% elif year != None %}
+ <h2><a href="/history"><span class="glyphicon glyphicon-chevron-left"></span>&nbsp{{ _('all years') }}</a></h2>
+ <h3>{{ year }}</h3>
+ {% endif %}
+ <ul class="list-group">
+ {% for article in articles_counter %}
+ {% if year == None %}
+ <li class="list-group-item"><a href="/history/{{ article }}">{{ article }}</a> : {{ articles_counter[article] }} articles</li>
+ {% elif month == None %}
+ <li class="list-group-item"><a href="/history/{{ year }}/{{ article }}">{{ article | month_name }}</a> : {{ articles_counter[article] }} articles</li>
+ {% else %}
+ {% for article in articles | sort(attribute="date", reverse = True) %}
+ <li class="list-group-item">{{ article.date | datetime }} - <a href="/article/{{ article.id }}">{{ article.title | safe }}</a></li>
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ </ul>
+</div><!-- /.container -->
+{% endblock %}
diff --git a/pyaggr3g470r/templates/home.html b/pyaggr3g470r/templates/home.html
index d2a961ab..69ca582b 100644
--- a/pyaggr3g470r/templates/home.html
+++ b/pyaggr3g470r/templates/home.html
@@ -22,7 +22,7 @@
<li class="feed-menu"><a href="{{ gen_url(feed=fid) }}">
{% if feed_id == fid %}<b>{% endif %}
{% if in_error.get(fid, 0) > 0 %}
- <span style="background-color: {{ "red" if in_error[fid] > 5 else "orange" }} ;" class="badge pull-right" title="Some errors occured while trying to retrieve that feed.">{{ in_error[fid] }}</span>
+ <span style="background-color: {{ "red" if in_error[fid] > 5 else "orange" }} ;" class="badge pull-right" title="Some errors occured while trying to retrieve that feed.">{{ in_error[fid] }} {{ _("error") }}{% if in_error[fid] > 1 %}s{% endif %}</span>
{% endif %}
<span id="unread-{{ fid }}" class="badge pull-right">{{ nbunread }}</span>
{{ feeds[fid]|safe }}
@@ -40,7 +40,7 @@
{% for fid, ftitle in feeds|dictsort(case_sensitive=False, by='value') if not fid in unread %}
<li class="feed-menu"><a href="{{ gen_url(feed=fid) }}">
{% if in_error.get(fid, 0) > 0 %}
- <span style="background-color: {{ "red" if in_error[fid] > 5 else "orange" }} ;" class="badge pull-right" title="Some errors occured while trying to retrieve that feed.">{{ in_error[fid] }}</span>
+ <span style="background-color: {{ "red" if in_error[fid] > 5 else "orange" }} ;" class="badge pull-right" title="Some errors occured while trying to retrieve that feed.">{{ in_error[fid] }} {{ _("error") }}{% if in_error[fid] > 1 %}s{% endif %}</span>
{% endif %}
{% if feed_id == fid %}<b>{% endif %}
{{ ftitle|safe }}
diff --git a/pyaggr3g470r/templates/inactives.html b/pyaggr3g470r/templates/inactives.html
index e6897281..6a4ff055 100644
--- a/pyaggr3g470r/templates/inactives.html
+++ b/pyaggr3g470r/templates/inactives.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<form method=get action="/inactives">
<p>{{ _('Days of inactivity') }}:</p>
<input type="number" name="nb_days" class="form-control" value="{{ nb_days }}" min="0" max="1000000" step="1" size="4" style="text-align: center" />
diff --git a/pyaggr3g470r/templates/layout.html b/pyaggr3g470r/templates/layout.html
index 6b929bf3..60efa69e 100644
--- a/pyaggr3g470r/templates/layout.html
+++ b/pyaggr3g470r/templates/layout.html
@@ -11,71 +11,12 @@
<!-- Bootstrap core CSS -->
<link href="{{ url_for('static', filename = 'css/bootstrap.css') }}" rel="stylesheet" media="screen" />
<!-- Add custom CSS here -->
- <style>
- body {
- margin-top: 60px;
- }
- div.top {
- position: relative;
- top:-60px;
- display: block;
- height: 0;
- }
- ul.affix {
- position: fixed;
- top: 0px;
- }
- ul.affix-top {
- position: static;
- }
- ul.affix-bottom {
- position: absolute;
- }
- /* First level of nav */
- .sidenav {
- margin-top: 10px;
- margin-bottom: 0px;
- padding-top: 10px;
- padding-bottom: 0px;
- overflow-y: auto;
- height: 90%;
- z-index: 1000;
- background-color: #ffffff;
- border-radius: 2px;
- font-size: 100%;
- }
- /* All levels of nav */
- .sidebar .nav > li > a {
- display: block;
- color: #716b7a;
- padding: 5px 20px;
- }
- .sidebar .nav > li > a:hover,
- .sidebar .nav > li > a:focus {
- text-decoration: none;
- background-color: #e5e3e9;
- }
- .sidebar .nav > .active > a,
- .sidebar .nav > .active:hover > a,
- .sidebar .nav > .active:focus > a {
- font-weight: bold;
- color: #563d7c;
- background-color: transparent;
- }
- /* Nav: second level */
- .sidebar .nav .nav {
- margin-bottom: 8px;
- }
- .sidebar .nav .nav > li > a {
- padding-top: 3px;
- padding-bottom: 3px;
- font-size: 80%;
- }
- </style>
+ <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 data-spy="scroll" data-target="#affix-nav">
- <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+ <body>
+ <nav class="navbar navbar-inverse navbar-fixed-top navbar-custom" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
@@ -118,7 +59,7 @@
<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">
- <button class="btn btn-default btn-xs" type="submit"><span class="glyphicon glyphicon-search"></span></button> <b class="caret"></b>
+ <div><span class="glyphicon glyphicon-search"></span>&nbsp;<b class="caret"></b></div>
</a>
<ul class="dropdown-menu">
<li>
@@ -138,6 +79,8 @@
</div><!-- /.container -->
</nav>
+ <br />
+
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
diff --git a/pyaggr3g470r/templates/login.html b/pyaggr3g470r/templates/login.html
index ae0797a5..c37d6937 100644
--- a/pyaggr3g470r/templates/login.html
+++ b/pyaggr3g470r/templates/login.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h2>{{ _('Log In') }}</h2>
<form action="{{ url_for('login') }}" method=post>
{{ form.hidden_tag() }}
@@ -27,4 +27,4 @@
&nbsp;
<a href="/recover" class="btn btn-default">{{ _('Forgot password') }}</a>
</div><!-- /.container -->
-{% endblock %} \ No newline at end of file
+{% endblock %}
diff --git a/pyaggr3g470r/templates/management.html b/pyaggr3g470r/templates/management.html
index 4a2de617..722300af 100644
--- a/pyaggr3g470r/templates/management.html
+++ b/pyaggr3g470r/templates/management.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <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>{{ nb_articles }} {{ _('articles are stored in the database with') }} {{ nb_unread_articles }} <a href="/unread">{{ _('unread articles') }}</a>.</p>
@@ -10,7 +10,7 @@
{% endif %}
<a href="/expire_articles?weeks=10" class="btn btn-default" onclick="return confirm('{{ _('You are going to delete old articles.') }}');">{{ _('Delete articles older than 10 weeks') }}</a>
</div>
- <div class="jumbotron">
+ <div class="well">
<h1>{{ _('Your Profile') }}</h1>
<div class="row">
<div class="col-md-6">
@@ -25,7 +25,7 @@
</div>
</div>
</div>
- <div class="jumbotron">
+ <div class="well">
<h1 id="import">{{ _('OPML import/export') }}</h1>
<form action="" method="post" id="formImportOPML" enctype="multipart/form-data">
<span class="btn btn-default btn-file">{{ _('Batch import feeds from OPML') }} (<span class="text-info">*.xml {{ _('or') }} *.opml</span>)<input type="file" name="opmlfile" /></span>
@@ -41,7 +41,7 @@
<br />
<a href="/export?format=JSON" class="btn btn-default">{{ _('Export account to JSON') }}</a>
</div>
- <div class="jumbotron">
+ <div class="well">
<h1>{{ _('Export articles') }}</h1>
<a href="/export?format=HTML" class="btn btn-default">HTML</a>
</div>
diff --git a/pyaggr3g470r/templates/profile.html b/pyaggr3g470r/templates/profile.html
index f879b054..971c4e64 100644
--- a/pyaggr3g470r/templates/profile.html
+++ b/pyaggr3g470r/templates/profile.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h2>Edit your profile</h2>
<form action="" method="post" name="save">
{{ form.hidden_tag() }}
diff --git a/pyaggr3g470r/templates/recover.html b/pyaggr3g470r/templates/recover.html
index 1098ffef..c1176d55 100644
--- a/pyaggr3g470r/templates/recover.html
+++ b/pyaggr3g470r/templates/recover.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="jumbotron">
+ <div class="well">
<h2>{{ _('Recover your account') }}</h2>
{% for message in form.email.errors %}
<div class="flash">{{ message }}</div>
@@ -15,4 +15,4 @@
</form>
</div>
</div><!-- /.container -->
-{% endblock %} \ No newline at end of file
+{% endblock %}
diff --git a/pyaggr3g470r/templates/signup.html b/pyaggr3g470r/templates/signup.html
index dd8154e2..3962c42a 100644
--- a/pyaggr3g470r/templates/signup.html
+++ b/pyaggr3g470r/templates/signup.html
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
- <div class="form jumbotron">
+ <div class="form well">
<form action="" method="post" name="save">
{{ form.hidden_tag() }}
<div class="form-group">
diff --git a/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo
index e3b7f0ff..6ecf87bd 100644
--- a/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo
+++ b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po
index 3bb25cb9..8a0a5ca8 100644
--- a/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po
+++ b/pyaggr3g470r/translations/fr/LC_MESSAGES/messages.po
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2015-01-09 22:12+0100\n"
-"PO-Revision-Date: 2015-01-09 22:13+0100\n"
+"POT-Creation-Date: 2015-03-28 11:26+0100\n"
+"PO-Revision-Date: 2015-03-28 11:27+0100\n"
"Last-Translator: Cédric Bonhomme <cedric@cedricbonhomme.org>\n"
"Language-Team: fr <LL@li.org>\n"
"Language: fr\n"
@@ -19,37 +19,37 @@ msgstr ""
"Generated-By: Babel 1.3\n"
"X-Generator: Poedit 1.5.4\n"
-#: pyaggr3g470r/forms.py:39 pyaggr3g470r/forms.py:99
+#: pyaggr3g470r/forms.py:40 pyaggr3g470r/forms.py:97
#: pyaggr3g470r/templates/admin/dashboard.html:12
msgid "Nickname"
msgstr "Pseudonyme"
-#: pyaggr3g470r/forms.py:39 pyaggr3g470r/forms.py:99
+#: pyaggr3g470r/forms.py:41 pyaggr3g470r/forms.py:98
msgid "Please enter your nickname."
msgstr "S'il vous plaît, entrez votre pseudonyme."
-#: pyaggr3g470r/forms.py:40 pyaggr3g470r/forms.py:100
-#: pyaggr3g470r/forms.py:121 pyaggr3g470r/templates/admin/dashboard.html:13
+#: pyaggr3g470r/forms.py:42 pyaggr3g470r/forms.py:99 pyaggr3g470r/forms.py:133
+#: pyaggr3g470r/templates/admin/dashboard.html:13
msgid "Email"
msgstr "Email"
-#: pyaggr3g470r/forms.py:40 pyaggr3g470r/forms.py:60 pyaggr3g470r/forms.py:121
+#: pyaggr3g470r/forms.py:45 pyaggr3g470r/forms.py:65 pyaggr3g470r/forms.py:136
msgid "Please enter your email address."
msgstr "S'il vous plaît, entrez votre adresse email."
-#: pyaggr3g470r/forms.py:41 pyaggr3g470r/forms.py:61 pyaggr3g470r/forms.py:101
+#: pyaggr3g470r/forms.py:46 pyaggr3g470r/forms.py:66 pyaggr3g470r/forms.py:102
msgid "Password"
msgstr "Mot de passe"
-#: pyaggr3g470r/forms.py:41 pyaggr3g470r/forms.py:61
+#: pyaggr3g470r/forms.py:47 pyaggr3g470r/forms.py:67
msgid "Please enter a password."
msgstr "S'il vous plaît entrer un mot de passe."
-#: pyaggr3g470r/forms.py:43 pyaggr3g470r/templates/login.html:26
+#: pyaggr3g470r/forms.py:50 pyaggr3g470r/templates/login.html:26
msgid "Sign up"
msgstr "S'inscrire"
-#: pyaggr3g470r/forms.py:52 pyaggr3g470r/forms.py:111
+#: pyaggr3g470r/forms.py:55 pyaggr3g470r/forms.py:117
msgid ""
"This nickname has invalid characters. Please use letters, numbers, dots and "
"underscores only."
@@ -57,272 +57,88 @@ msgstr ""
"Ce pseudonyme a des caractères non valides. Utilisez seulement des lettres, "
"des chiffres, des points et '_'."
-#: pyaggr3g470r/forms.py:62 pyaggr3g470r/templates/login.html:5
+#: pyaggr3g470r/forms.py:69 pyaggr3g470r/templates/login.html:5
msgid "Log In"
msgstr "Connexion"
-#: pyaggr3g470r/forms.py:75
+#: pyaggr3g470r/forms.py:80
msgid "Account not confirmed"
msgstr "Compte non confirmé"
-#: pyaggr3g470r/forms.py:78
+#: pyaggr3g470r/forms.py:83
msgid "Invalid email or password"
msgstr "E-mail ou mot de passe invalide"
-#: pyaggr3g470r/forms.py:83 pyaggr3g470r/templates/feeds.html:11
+#: pyaggr3g470r/forms.py:89 pyaggr3g470r/templates/feeds.html:11
msgid "Title"
msgstr "Titre"
-#: pyaggr3g470r/forms.py:84 pyaggr3g470r/templates/admin/user.html:27
+#: pyaggr3g470r/forms.py:90 pyaggr3g470r/templates/admin/user.html:27
msgid "Feed link"
msgstr "Lien du flux"
-#: pyaggr3g470r/forms.py:85 pyaggr3g470r/templates/admin/user.html:28
+#: pyaggr3g470r/forms.py:91 pyaggr3g470r/templates/admin/user.html:28
msgid "Site link"
msgstr "Lien du site"
-#: pyaggr3g470r/forms.py:86
-msgid "Email notification"
-msgstr "Notification par email"
-
-#: pyaggr3g470r/forms.py:87
+#: pyaggr3g470r/forms.py:92
msgid "Check for updates"
msgstr "Vérifier les mises à jour"
-#: pyaggr3g470r/forms.py:88 pyaggr3g470r/forms.py:102
+#: pyaggr3g470r/forms.py:93 pyaggr3g470r/forms.py:107
msgid "Save"
msgstr "Sauver"
-#: pyaggr3g470r/forms.py:100
+#: pyaggr3g470r/forms.py:101
msgid "Please enter your email."
msgstr "S'il vous plaît, entrez votre email."
-#: pyaggr3g470r/forms.py:116
+#: pyaggr3g470r/forms.py:103
+msgid "Password Confirmation"
+msgstr "Confirmation du mot de passe"
+
+#: pyaggr3g470r/forms.py:104
+msgid "Feeds refresh frequency (in minutes)"
+msgstr "Fréquence de rafraîchissement du flux (en minutes)"
+
+#: pyaggr3g470r/forms.py:112
+msgid "Passwords aren't the same."
+msgstr "Les mots de passe ne sont pas identiques."
+
+#: pyaggr3g470r/forms.py:125
msgid "Subject"
msgstr "Objet"
-#: pyaggr3g470r/forms.py:116
+#: pyaggr3g470r/forms.py:126
msgid "Please enter a subject."
msgstr "S'il vous plaît entrer un objet."
-#: pyaggr3g470r/forms.py:117
+#: pyaggr3g470r/forms.py:127
msgid "Message"
msgstr "Message"
-#: pyaggr3g470r/forms.py:117
+#: pyaggr3g470r/forms.py:128
msgid "Please enter a content."
msgstr "S'il vous plaît entrer un contenu."
-#: pyaggr3g470r/forms.py:118
+#: pyaggr3g470r/forms.py:129
msgid "Send"
msgstr "Envoyer"
-#: pyaggr3g470r/forms.py:122
+#: pyaggr3g470r/forms.py:137
msgid "Recover"
msgstr "Récupérer"
-#: pyaggr3g470r/forms.py:135
+#: pyaggr3g470r/forms.py:147
msgid "Account not confirmed."
msgstr "Compte non confirmé."
-#: pyaggr3g470r/forms.py:138
+#: pyaggr3g470r/forms.py:150
msgid "Invalid email."
msgstr "Email invalide."
-#: pyaggr3g470r/views.py:99
-msgid "Authentication required."
-msgstr "Authentification requise."
-
-#: pyaggr3g470r/views.py:104
-msgid "Forbidden."
-msgstr "Interdit."
-
-#: pyaggr3g470r/views.py:156
-msgid "Logged in successfully."
-msgstr "Connecté avec succès."
-
-#: pyaggr3g470r/views.py:178
-msgid "Logged out successfully."
-msgstr "Déconnecté avec succès."
-
-#: pyaggr3g470r/views.py:187
-msgid "Self-registration is disabled."
-msgstr "L'auto-enregistrement est désactivé."
-
-#: pyaggr3g470r/views.py:204
-msgid "Email already used."
-msgstr "Email déjà utilisé."
-
-#: pyaggr3g470r/views.py:211 pyaggr3g470r/views.py:921
-msgid "Problem while sending activation email"
-msgstr "Problème lors de l'envoi d'email d'activation"
-
-#: pyaggr3g470r/views.py:214
-msgid "Your account has been created. Check your mail to confirm it."
-msgstr "Votre compte a été créé. Vérifiez votre courrier pour le confirmer."
-
-#: pyaggr3g470r/views.py:271 pyaggr3g470r/views.py:601
-msgid "Downloading articles..."
-msgstr "Téléchargement des articles."
-
-#: pyaggr3g470r/views.py:343 pyaggr3g470r/views.py:403
-msgid "This article do not exist."
-msgstr "Cet article n'existe pas."
-
-#: pyaggr3g470r/views.py:400 pyaggr3g470r/templates/home.html:75
-msgid "Article"
-msgstr "Article"
-
-#: pyaggr3g470r/views.py:400
-msgid "deleted."
-msgstr "supprimé."
-
-#: pyaggr3g470r/views.py:499
-msgid "Indexing database..."
-msgstr "Indexation la base de données..."
-
-#: pyaggr3g470r/views.py:501 pyaggr3g470r/views.py:570
-msgid "An error occured"
-msgstr "Une erreur est survenue."
-
-#: pyaggr3g470r/views.py:504
-msgid "Option not available on Heroku."
-msgstr "Option non disponible sur Heroku."
-
-#: pyaggr3g470r/views.py:519 pyaggr3g470r/views.py:529
-msgid "Error when exporting articles."
-msgstr "Erreur lors de l'export des articles."
-
-#: pyaggr3g470r/views.py:535
-msgid "Export format not supported."
-msgstr "Ce format d'export n'est pas supporté."
-
-#: pyaggr3g470r/views.py:558
-msgid "Full text search is not yet implemented for Heroku."
-msgstr "La recherche rapide n'est pas supporté sur Heroku."
-
-#: pyaggr3g470r/views.py:595 pyaggr3g470r/views.py:608
-#: pyaggr3g470r/views.py:616
-msgid "File not allowed."
-msgstr "Fichier non autorisé."
-
-#: pyaggr3g470r/views.py:600
-msgid "feeds imported."
-msgstr "flux importés."
-
-#: pyaggr3g470r/views.py:603
-msgid "Impossible to import the new feeds."
-msgstr "Impossible d'importer les nouveaux flux."
-
-#: pyaggr3g470r/views.py:612
-msgid "Account imported."
-msgstr "Compte importé."
-
-#: pyaggr3g470r/views.py:614
-msgid "Impossible to import the account."
-msgstr "Impossible d'importer le compte."
-
-#: pyaggr3g470r/views.py:652
-msgid "Feed successfully updated."
-msgstr "Flux mis à jour avec succès."
-
-#: pyaggr3g470r/views.py:664
-msgid "Feed successfully created."
-msgstr "Flux créé avec succès."
-
-#: pyaggr3g470r/views.py:667
-msgid "Downloading articles for the new feed..."
-msgstr "Téléchargement des articles du nouveau flux..."
-
-#: pyaggr3g470r/views.py:671
-msgid "Feed already in the database."
-msgstr "Flux déjà dans la base de données."
-
-#: pyaggr3g470r/views.py:677
-msgid "Edit the feed"
-msgstr "Éditez ce flux"
-
-#: pyaggr3g470r/views.py:690 pyaggr3g470r/templates/layout.html:109
-msgid "Add a feed"
-msgstr "Ajouter un flux"
-
-#: pyaggr3g470r/views.py:703 pyaggr3g470r/templates/home.html:74
-#: pyaggr3g470r/templates/admin/user.html:42
-msgid "Feed"
-msgstr "Flux"
-
-#: pyaggr3g470r/views.py:703 pyaggr3g470r/views.py:898
-msgid "successfully deleted."
-msgstr "supprimé avec succès."
-
-#: pyaggr3g470r/views.py:721 pyaggr3g470r/views.py:848
-#: pyaggr3g470r/views.py:858 pyaggr3g470r/views.py:898
-msgid "User"
-msgstr "Utilisateur"
-
-#: pyaggr3g470r/views.py:721 pyaggr3g470r/views.py:848
-msgid "successfully updated."
-msgstr "mis à jour avec succès."
-
-#: pyaggr3g470r/views.py:740
-msgid "Your account has been deleted."
-msgstr "Votre compte a été supprimé."
-
-#: pyaggr3g470r/views.py:742 pyaggr3g470r/views.py:884
-#: pyaggr3g470r/views.py:900 pyaggr3g470r/views.py:929
-msgid "This user does not exist."
-msgstr "Cet utilisateur n'existe pas."
-
-#: pyaggr3g470r/views.py:756
-msgid "Articles deleted."
-msgstr "Articles supprimés."
-
-#: pyaggr3g470r/views.py:770
-msgid "Your account has been confirmed."
-msgstr "Votre compte a été confirmé."
-
-#: pyaggr3g470r/views.py:772
-msgid "Impossible to confirm this account."
-msgstr "Impossible de confirmer ce compte."
-
-#: pyaggr3g470r/views.py:796
-msgid "New password sent to your address."
-msgstr "Nouveau mot de passe envoyé à votre adresse."
-
-#: pyaggr3g470r/views.py:798
-msgid "Problem while sending your new password."
-msgstr "Problème lors de l'envoi de votre nouveau mot de passe."
-
-#: pyaggr3g470r/views.py:823
-msgid "Problem while sending email"
-msgstr "Problème lors de l'envoi de l'email"
-
-#: pyaggr3g470r/views.py:858
-msgid "successfully created."
-msgstr "créé avec succès."
-
-#: pyaggr3g470r/views.py:867
-msgid "Edit the user"
-msgstr "Éditer cet utilisateur"
-
-#: pyaggr3g470r/views.py:870 pyaggr3g470r/templates/admin/dashboard.html:45
-msgid "Add a new user"
-msgstr "Ajouter un nouvel utilisateur"
-
-#: pyaggr3g470r/views.py:919 pyaggr3g470r/views.py:926
-msgid "Account of the user"
-msgstr "Compte de l'utilisateur"
-
-#: pyaggr3g470r/views.py:919
-msgid "successfully activated."
-msgstr "activé avec succès."
-
-#: pyaggr3g470r/views.py:926
-msgid "successfully disabled."
-msgstr "désactivé avec succès."
-
-#: pyaggr3g470r/templates/about.html:5 pyaggr3g470r/templates/layout.html:126
-#: pyaggr3g470r/templates/layout.html:143
+#: pyaggr3g470r/templates/about.html:5 pyaggr3g470r/templates/layout.html:117
+#: pyaggr3g470r/templates/layout.html:134
msgid "About"
msgstr "À propos"
@@ -379,11 +195,11 @@ msgstr ""
#: pyaggr3g470r/templates/about.html:19
msgid ""
-"The documentation of the RESTful API is <a href=\"https://bitbucket.org/"
-"cedricbonhomme/pyaggr3g470r#rst-header-web-service\">here</a>."
+"The documentation of the RESTful API is <a href=\"https://pyaggr3g470r."
+"readthedocs.org/en/latest/web-services.html\">here</a>."
msgstr ""
-"La documentation de l'API RESTful est <a href=\"https://bitbucket.org/"
-"cedricbonhomme/pyaggr3g470r#rst-header-web-service\">ici</a>."
+"La documentation de l'API RESTful est <a href=\"https://pyaggr3g470r."
+"readthedocs.org/en/latest/web-services.html\">ici</a>."
#: pyaggr3g470r/templates/about.html:20
#, python-format
@@ -414,25 +230,25 @@ msgid "from"
msgstr "de"
#: pyaggr3g470r/templates/article.html:11
-#: pyaggr3g470r/templates/duplicates.html:19
-#: pyaggr3g470r/templates/duplicates.html:20
-#: pyaggr3g470r/templates/home.html:89
+#: pyaggr3g470r/templates/duplicates.html:23
+#: pyaggr3g470r/templates/duplicates.html:24
+#: pyaggr3g470r/templates/home.html:90
msgid "Delete this article"
msgstr "Supprimer cet article"
-#: pyaggr3g470r/templates/article.html:13 pyaggr3g470r/templates/home.html:91
+#: pyaggr3g470r/templates/article.html:13 pyaggr3g470r/templates/home.html:92
msgid "One of your favorites"
msgstr "Un de vos favoris"
-#: pyaggr3g470r/templates/article.html:15 pyaggr3g470r/templates/home.html:93
+#: pyaggr3g470r/templates/article.html:15 pyaggr3g470r/templates/home.html:94
msgid "Click if you like this article"
msgstr "Cliquez si vous aimez cet article"
-#: pyaggr3g470r/templates/article.html:18 pyaggr3g470r/templates/home.html:96
+#: pyaggr3g470r/templates/article.html:18 pyaggr3g470r/templates/home.html:97
msgid "Mark this article as unread"
msgstr "Marquer cet article comme non lu"
-#: pyaggr3g470r/templates/article.html:20 pyaggr3g470r/templates/home.html:98
+#: pyaggr3g470r/templates/article.html:20 pyaggr3g470r/templates/home.html:99
msgid "Mark this article as read"
msgstr "Marquer cet article comme lu"
@@ -453,7 +269,12 @@ msgstr "Partager sur"
msgid "Duplicates in the feed"
msgstr "Doublons dans le flux"
-#: pyaggr3g470r/templates/duplicates.html:27
+#: pyaggr3g470r/templates/duplicates.html:12
+#: pyaggr3g470r/templates/duplicates.html:15
+msgid "Delete all in this column"
+msgstr "Supprimer tout dans cette colonne"
+
+#: pyaggr3g470r/templates/duplicates.html:31
msgid "No duplicates in the feed"
msgstr "Pas de doublon dans ce flux"
@@ -471,26 +292,26 @@ msgid "More articles"
msgstr "Plus d'articles"
#: pyaggr3g470r/templates/favorites.html:17
-#: pyaggr3g470r/templates/home.html:29 pyaggr3g470r/templates/home.html:44
+#: pyaggr3g470r/templates/home.html:32 pyaggr3g470r/templates/home.html:50
#: pyaggr3g470r/templates/unread.html:17
msgid "Details"
msgstr "Détails"
#: pyaggr3g470r/templates/favorites.html:18 pyaggr3g470r/templates/feed.html:8
-#: pyaggr3g470r/templates/feeds.html:33 pyaggr3g470r/templates/home.html:31
-#: pyaggr3g470r/templates/home.html:46 pyaggr3g470r/templates/unread.html:18
+#: pyaggr3g470r/templates/feeds.html:33 pyaggr3g470r/templates/home.html:34
+#: pyaggr3g470r/templates/home.html:52 pyaggr3g470r/templates/unread.html:18
#: pyaggr3g470r/templates/admin/user.html:43
msgid "Edit this feed"
msgstr "Éditer ce flux"
#: pyaggr3g470r/templates/feed.html:7 pyaggr3g470r/templates/feeds.html:35
-#: pyaggr3g470r/templates/home.html:32 pyaggr3g470r/templates/home.html:47
+#: pyaggr3g470r/templates/home.html:35 pyaggr3g470r/templates/home.html:53
#: pyaggr3g470r/templates/admin/user.html:44
msgid "Delete this feed"
msgstr "Supprimer ce flux"
#: pyaggr3g470r/templates/feed.html:7 pyaggr3g470r/templates/feeds.html:35
-#: pyaggr3g470r/templates/home.html:32 pyaggr3g470r/templates/home.html:47
+#: pyaggr3g470r/templates/home.html:35 pyaggr3g470r/templates/home.html:53
#: pyaggr3g470r/templates/admin/user.html:44
msgid "You are going to delete this feed."
msgstr "Vous allez supprimer ce flux."
@@ -503,35 +324,55 @@ msgstr "Ce flux contient"
msgid "articles"
msgstr "articles"
-#: pyaggr3g470r/templates/feed.html:14
-msgid "of the database"
-msgstr "de la base de données"
-
-#: pyaggr3g470r/templates/feed.html:17
+#: pyaggr3g470r/templates/feed.html:13
msgid "Address of the feed"
msgstr "Adresse du flux"
-#: pyaggr3g470r/templates/feed.html:19
+#: pyaggr3g470r/templates/feed.html:15
msgid "Address of the site"
msgstr "Adresse du site"
-#: pyaggr3g470r/templates/feed.html:23
+#: pyaggr3g470r/templates/feed.html:21
+msgid "Last download:"
+msgstr "Dernier téléchargement:"
+
+#: pyaggr3g470r/templates/feed.html:25
+msgid ""
+"That feed has encountered too much consecutive errors and won't be retrieved "
+"anymore."
+msgstr ""
+"Ce flux a rencontré trop d'erreurs consécutives et ne sera plus récupéré."
+
+#: pyaggr3g470r/templates/feed.html:27
+msgid ""
+"The download of this feed has encountered some problems. However its error "
+"counter will be reinitialized at the next successful retrieving."
+msgstr ""
+"Le téléchargement de ce flux a rencontré quelques problèmes. Cependant, son "
+"compteur d'erreurs sera réinitialisé lors de la prochaine récupération "
+"réussie."
+
+#: pyaggr3g470r/templates/feed.html:31
+msgid "Here's the last error encountered while retrieving this feed:"
+msgstr "Voici la dernière erreur survenue lors de la récupération ce flux:"
+
+#: pyaggr3g470r/templates/feed.html:35
msgid "The last article was posted"
msgstr "Le dernier article a été posté il y a"
-#: pyaggr3g470r/templates/feed.html:23
+#: pyaggr3g470r/templates/feed.html:35
msgid "day(s) ago."
msgstr "jours."
-#: pyaggr3g470r/templates/feed.html:24
+#: pyaggr3g470r/templates/feed.html:36
msgid "Daily average"
msgstr "Moyenne journalière"
-#: pyaggr3g470r/templates/feed.html:24
+#: pyaggr3g470r/templates/feed.html:36
msgid "between the"
msgstr "entre le"
-#: pyaggr3g470r/templates/feed.html:24
+#: pyaggr3g470r/templates/feed.html:36
msgid "and the"
msgstr "et le"
@@ -564,8 +405,8 @@ msgid "Site"
msgstr "Site"
#: pyaggr3g470r/templates/feeds.html:13 pyaggr3g470r/templates/feeds.html:32
-#: pyaggr3g470r/templates/home.html:30 pyaggr3g470r/templates/home.html:45
-#: pyaggr3g470r/templates/layout.html:104
+#: pyaggr3g470r/templates/home.html:33 pyaggr3g470r/templates/home.html:51
+#: pyaggr3g470r/templates/layout.html:95
msgid "Articles"
msgstr "Articles"
@@ -587,6 +428,15 @@ msgstr "Flux désactivé"
msgid "Duplicate articles"
msgstr "Articles doublon"
+#: pyaggr3g470r/templates/history.html:4
+#: pyaggr3g470r/templates/layout.html:110
+msgid "History"
+msgstr "Historique"
+
+#: pyaggr3g470r/templates/history.html:9
+msgid "all years"
+msgstr "toutes les années"
+
#: pyaggr3g470r/templates/home.html:10
msgid "You don't have any feeds."
msgstr "Vous n'avez pas de flux."
@@ -604,31 +454,44 @@ msgstr "ou"
msgid "upload an OPML file."
msgstr "téléchargez un fichier OPML."
-#: pyaggr3g470r/templates/home.html:18 pyaggr3g470r/templates/layout.html:118
+#: pyaggr3g470r/templates/home.html:18 pyaggr3g470r/templates/layout.html:109
msgid "All feeds"
msgstr "Tous les flux"
-#: pyaggr3g470r/templates/home.html:33 pyaggr3g470r/templates/home.html:48
+#: pyaggr3g470r/templates/home.html:25 pyaggr3g470r/templates/home.html:43
+msgid "error"
+msgstr "erreur"
+
+#: pyaggr3g470r/templates/home.html:36 pyaggr3g470r/templates/home.html:54
msgid "Mark this feed as read"
msgstr "Marquer ce flux comme lu"
-#: pyaggr3g470r/templates/home.html:34 pyaggr3g470r/templates/home.html:49
+#: pyaggr3g470r/templates/home.html:37 pyaggr3g470r/templates/home.html:55
msgid "Mark this feed as unread"
msgstr "Marquer ce flux comme non lu"
-#: pyaggr3g470r/templates/home.html:57 pyaggr3g470r/templates/home.html:65
+#: pyaggr3g470r/templates/home.html:63 pyaggr3g470r/templates/home.html:71
msgid "All"
msgstr "Tout"
-#: pyaggr3g470r/templates/home.html:58
+#: pyaggr3g470r/templates/home.html:64
msgid "Read"
msgstr "Lus"
-#: pyaggr3g470r/templates/home.html:59 pyaggr3g470r/templates/layout.html:115
+#: pyaggr3g470r/templates/home.html:65 pyaggr3g470r/templates/layout.html:106
msgid "Unread"
msgstr "Non lus"
-#: pyaggr3g470r/templates/home.html:76
+#: pyaggr3g470r/templates/home.html:81
+#: pyaggr3g470r/templates/admin/user.html:42 pyaggr3g470r/views/views.py:630
+msgid "Feed"
+msgstr "Flux"
+
+#: pyaggr3g470r/templates/home.html:82 pyaggr3g470r/views/views.py:336
+msgid "Article"
+msgstr "Article"
+
+#: pyaggr3g470r/templates/home.html:83
msgid "Date"
msgstr "Date"
@@ -644,39 +507,39 @@ msgstr "jours"
msgid "No inactive feeds."
msgstr "Aucun flux inactifs."
-#: pyaggr3g470r/templates/layout.html:106
+#: pyaggr3g470r/templates/layout.html:97
msgid "Fetch"
msgstr "Télécharger"
-#: pyaggr3g470r/templates/layout.html:107
+#: pyaggr3g470r/templates/layout.html:98
msgid "Mark all as read"
msgstr "Marquer tout comme lu"
-#: pyaggr3g470r/templates/layout.html:113
+#: pyaggr3g470r/templates/layout.html:100 pyaggr3g470r/views/views.py:617
+msgid "Add a feed"
+msgstr "Ajouter un flux"
+
+#: pyaggr3g470r/templates/layout.html:104
msgid "Filter"
msgstr "Filtrer"
-#: pyaggr3g470r/templates/layout.html:116
+#: pyaggr3g470r/templates/layout.html:107
msgid "Favorites"
msgstr "Favoris"
-#: pyaggr3g470r/templates/layout.html:117
+#: pyaggr3g470r/templates/layout.html:108
msgid "Inactive feeds"
msgstr "Flux inactifs"
-#: pyaggr3g470r/templates/layout.html:119
-msgid "History"
-msgstr "Historique"
-
-#: pyaggr3g470r/templates/layout.html:122
+#: pyaggr3g470r/templates/layout.html:113
msgid "Management"
msgstr "Gestion"
-#: pyaggr3g470r/templates/layout.html:124
+#: pyaggr3g470r/templates/layout.html:115
msgid "Dashboard"
msgstr "Tableau de bord"
-#: pyaggr3g470r/templates/layout.html:127
+#: pyaggr3g470r/templates/layout.html:118
msgid "Logout"
msgstr "Déconnexion"
@@ -828,6 +691,11 @@ msgstr "Supprimer cet utilisateur"
msgid "You are going to delete this account."
msgstr "Vous allez supprimer ce compte."
+#: pyaggr3g470r/templates/admin/dashboard.html:45
+#: pyaggr3g470r/views/views.py:797
+msgid "Add a new user"
+msgstr "Ajouter un nouvel utilisateur"
+
#: pyaggr3g470r/templates/admin/dashboard.html:46
msgid "Send notification messages"
msgstr "Envoyer des messages de notification"
@@ -852,6 +720,195 @@ msgstr "Nom de famille"
msgid "Number of articles"
msgstr "Nombre d'articles"
+#: pyaggr3g470r/views/views.py:103
+msgid "Authentication required."
+msgstr "Authentification requise."
+
+#: pyaggr3g470r/views/views.py:108
+msgid "Forbidden."
+msgstr "Interdit."
+
+#: pyaggr3g470r/views/views.py:161
+msgid "Logged in successfully."
+msgstr "Connecté avec succès."
+
+#: pyaggr3g470r/views/views.py:184
+msgid "Logged out successfully."
+msgstr "Déconnecté avec succès."
+
+#: pyaggr3g470r/views/views.py:193
+msgid "Self-registration is disabled."
+msgstr "L'auto-enregistrement est désactivé."
+
+#: pyaggr3g470r/views/views.py:210
+msgid "Email already used."
+msgstr "Email déjà utilisé."
+
+#: pyaggr3g470r/views/views.py:217 pyaggr3g470r/views/views.py:848
+msgid "Problem while sending activation email"
+msgstr "Problème lors de l'envoi d'email d'activation"
+
+#: pyaggr3g470r/views/views.py:220
+msgid "Your account has been created. Check your mail to confirm it."
+msgstr "Votre compte a été créé. Vérifiez votre courrier pour le confirmer."
+
+#: pyaggr3g470r/views/views.py:269 pyaggr3g470r/views/views.py:519
+msgid "Downloading articles..."
+msgstr "Téléchargement des articles."
+
+#: pyaggr3g470r/views/views.py:271
+msgid ""
+"The manual retrieving of news is only available for administrator, on the "
+"Heroku platform."
+msgstr ""
+"La récupération manuelle de nouvelles est disponible uniquement pour "
+"l'administrateur, sur la plate-forme Heroku."
+
+#: pyaggr3g470r/views/views.py:336
+msgid "deleted."
+msgstr "supprimé."
+
+#: pyaggr3g470r/views/views.py:339
+msgid "This article do not exist."
+msgstr "Cet article n'existe pas."
+
+#: pyaggr3g470r/views/views.py:417
+msgid "Indexing database..."
+msgstr "Indexation la base de données..."
+
+#: pyaggr3g470r/views/views.py:419 pyaggr3g470r/views/views.py:488
+msgid "An error occured"
+msgstr "Une erreur est survenue."
+
+#: pyaggr3g470r/views/views.py:422
+msgid "Option not available on Heroku."
+msgstr "Option non disponible sur Heroku."
+
+#: pyaggr3g470r/views/views.py:437 pyaggr3g470r/views/views.py:447
+msgid "Error when exporting articles."
+msgstr "Erreur lors de l'export des articles."
+
+#: pyaggr3g470r/views/views.py:453
+msgid "Export format not supported."
+msgstr "Ce format d'export n'est pas supporté."
+
+#: pyaggr3g470r/views/views.py:476
+msgid "Full text search is not yet implemented for Heroku."
+msgstr "La recherche rapide n'est pas supporté sur Heroku."
+
+#: pyaggr3g470r/views/views.py:513 pyaggr3g470r/views/views.py:526
+#: pyaggr3g470r/views/views.py:534
+msgid "File not allowed."
+msgstr "Fichier non autorisé."
+
+#: pyaggr3g470r/views/views.py:518
+msgid "feeds imported."
+msgstr "flux importés."
+
+#: pyaggr3g470r/views/views.py:521
+msgid "Impossible to import the new feeds."
+msgstr "Impossible d'importer les nouveaux flux."
+
+#: pyaggr3g470r/views/views.py:530
+msgid "Account imported."
+msgstr "Compte importé."
+
+#: pyaggr3g470r/views/views.py:532
+msgid "Impossible to import the account."
+msgstr "Impossible d'importer le compte."
+
+#: pyaggr3g470r/views/views.py:579
+msgid "Feed successfully updated."
+msgstr "Flux mis à jour avec succès."
+
+#: pyaggr3g470r/views/views.py:590
+msgid "Feed successfully created."
+msgstr "Flux créé avec succès."
+
+#: pyaggr3g470r/views/views.py:593
+msgid "Downloading articles for the new feed..."
+msgstr "Téléchargement des articles du nouveau flux..."
+
+#: pyaggr3g470r/views/views.py:597
+msgid "Feed already in the database."
+msgstr "Flux déjà dans la base de données."
+
+#: pyaggr3g470r/views/views.py:604
+msgid "Edit the feed"
+msgstr "Éditez ce flux"
+
+#: pyaggr3g470r/views/views.py:630 pyaggr3g470r/views/views.py:825
+msgid "successfully deleted."
+msgstr "supprimé avec succès."
+
+#: pyaggr3g470r/views/views.py:648 pyaggr3g470r/views/views.py:775
+#: pyaggr3g470r/views/views.py:785 pyaggr3g470r/views/views.py:825
+msgid "User"
+msgstr "Utilisateur"
+
+#: pyaggr3g470r/views/views.py:649 pyaggr3g470r/views/views.py:775
+msgid "successfully updated."
+msgstr "mis à jour avec succès."
+
+#: pyaggr3g470r/views/views.py:669
+msgid "Your account has been deleted."
+msgstr "Votre compte a été supprimé."
+
+#: pyaggr3g470r/views/views.py:671 pyaggr3g470r/views/views.py:811
+#: pyaggr3g470r/views/views.py:827 pyaggr3g470r/views/views.py:855
+msgid "This user does not exist."
+msgstr "Cet utilisateur n'existe pas."
+
+#: pyaggr3g470r/views/views.py:685
+msgid "Articles deleted."
+msgstr "Articles supprimés."
+
+#: pyaggr3g470r/views/views.py:699
+msgid "Your account has been confirmed."
+msgstr "Votre compte a été confirmé."
+
+#: pyaggr3g470r/views/views.py:701
+msgid "Impossible to confirm this account."
+msgstr "Impossible de confirmer ce compte."
+
+#: pyaggr3g470r/views/views.py:723
+msgid "New password sent to your address."
+msgstr "Nouveau mot de passe envoyé à votre adresse."
+
+#: pyaggr3g470r/views/views.py:725
+msgid "Problem while sending your new password."
+msgstr "Problème lors de l'envoi de votre nouveau mot de passe."
+
+#: pyaggr3g470r/views/views.py:750
+msgid "Problem while sending email"
+msgstr "Problème lors de l'envoi de l'email"
+
+#: pyaggr3g470r/views/views.py:785
+msgid "successfully created."
+msgstr "créé avec succès."
+
+#: pyaggr3g470r/views/views.py:794
+msgid "Edit the user"
+msgstr "Éditer cet utilisateur"
+
+#: pyaggr3g470r/views/views.py:846 pyaggr3g470r/views/views.py:852
+msgid "Account of the user"
+msgstr "Compte de l'utilisateur"
+
+#: pyaggr3g470r/views/views.py:846
+msgid "successfully activated."
+msgstr "activé avec succès."
+
+#: pyaggr3g470r/views/views.py:852
+msgid "successfully disabled."
+msgstr "désactivé avec succès."
+
+#~ msgid "of the database"
+#~ msgstr "de la base de données"
+
+#~ msgid "Email notification"
+#~ msgstr "Notification par email"
+
#~ msgid "Database indexed."
#~ msgstr "Base de données indexée."
@@ -912,9 +969,6 @@ msgstr "Nombre d'articles"
#~ msgid "First name"
#~ msgstr "Prénom"
-#~ msgid "Last name"
-#~ msgstr "Nom de famille"
-
#~ msgid "Please enter your last name."
#~ msgstr "S'il vous plaît, entrez votre nom de famille."
diff --git a/pyaggr3g470r/utils.py b/pyaggr3g470r/utils.py
index 3ed89f55..ea8a87bf 100755
--- a/pyaggr3g470r/utils.py
+++ b/pyaggr3g470r/utils.py
@@ -4,7 +4,7 @@
# pyAggr3g470r - A Web based news aggregator.
# Copyright (C) 2010-2015 Cédric Bonhomme - https://www.cedricbonhomme.org
#
-# For more information : https://bitbucket.org/cedricbonhomme/pyaggr3g470r/
+# For more information : https://bitbucket.org/cedricbonhomme/pyaggr3g470r
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -20,15 +20,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__author__ = "Cedric Bonhomme"
-__version__ = "$Revision: 1.6 $"
+__version__ = "$Revision: 1.7 $"
__date__ = "$Date: 2010/12/07 $"
-__revision__ = "$Date: 2013/11/17 $"
+__revision__ = "$Date: 2015/03/28 $"
__copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "AGPLv3"
#
# This file provides functions used for:
-# - the database management;
+# - detection of duplicate articles;
+# - import from a JSON file;
# - generation of tags cloud;
# - HTML processing.
#
@@ -41,30 +42,34 @@ import logging
import datetime
import operator
import urllib
+import itertools
import subprocess
+import sqlalchemy
try:
from urlparse import urlparse, parse_qs, urlunparse
except:
from urllib.parse import urlparse, parse_qs, urlunparse
from bs4 import BeautifulSoup
+from datetime import timedelta
from collections import Counter
from contextlib import contextmanager
import conf
from flask import g
+from bootstrap import application as app, db
+from pyaggr3g470r import controllers
from pyaggr3g470r.models import User, Feed, Article
-
-# regular expression to check URL
-url_finders = [
- re.compile("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}|(((news|telnet|nttp|file|http|ftp|https)://)|(www|ftp)[-A-Za-z0-9]*\\.)[-A-Za-z0-9\\.]+)(:[0-9]*)?/[-A-Za-z0-9_\\$\\.\\+\\!\\*\\(\\),;:@&=\\?/~\\#\\%]*[^]'\\.}>\\),\\\"]"), \
- re.compile("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}|(((news|telnet|nttp|file|http|ftp|https)://)|(www|ftp)[-A-Za-z0-9]*\\.)[-A-Za-z0-9\\.]+)(:[0-9]*)?"), \
- re.compile("(~/|/|\\./)([-A-Za-z0-9_\\$\\.\\+\\!\\*\\(\\),;:@&=\\?/~\\#\\%]|\\\\)+"), \
- re.compile("'\\<((mailto:)|)[-A-Za-z0-9\\.]+@[-A-Za-z0-9\\.]+") \
-]
-
logger = logging.getLogger(__name__)
+ALLOWED_EXTENSIONS = set(['xml', 'opml', 'json'])
+
+def allowed_file(filename):
+ """
+ Check if the uploaded file is allowed.
+ """
+ return '.' in filename and \
+ filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
@contextmanager
def opened_w_error(filename, mode="r"):
@@ -79,9 +84,31 @@ def opened_w_error(filename, mode="r"):
f.close()
def fetch(id, feed_id=None):
- cmd = [conf.PYTHON, conf.basedir+'/manager.py', 'fetch_asyncio', str(id), str(feed_id)]
+ """
+ Fetch the feeds in a new processus.
+ The "asyncio" crawler is launched with the manager.
+ """
+ cmd = [conf.PYTHON, conf.basedir+'/manager.py', 'fetch_asyncio', str(id),
+ str(feed_id)]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+def history(user_id, year=None, month=None):
+ """
+ Sort articles by year and month.
+ """
+ articles_counter = Counter()
+ articles = controllers.ArticleController(user_id).read()
+ if None != year:
+ articles = articles.filter(sqlalchemy.extract('year', Article.date) == year)
+ if None != month:
+ articles = articles.filter(sqlalchemy.extract('month', Article.date) == month)
+ for article in articles.all():
+ if None != year:
+ articles_counter[article.date.month] += 1
+ else:
+ articles_counter[article.date.year] += 1
+ return articles_counter, articles
+
def import_opml(email, opml_content):
"""
Import new feeds from an OPML file.
@@ -98,45 +125,35 @@ def import_opml(email, opml_content):
Parse recursively through the categories and sub-categories.
"""
for subscription in subsubscription:
-
if len(subscription) != 0:
nb = read(subscription, nb)
else:
-
try:
title = subscription.text
-
except:
title = ""
-
try:
description = subscription.description
except:
description = ""
-
try:
link = subscription.xmlUrl
except:
continue
-
if None != Feed.query.filter(Feed.user_id == user.id, Feed.link == link).first():
continue
-
try:
site_link = subscription.htmlUrl
except:
site_link = ""
-
new_feed = Feed(title=title, description=description,
link=link, site_link=site_link,
enabled=True)
-
user.feeds.append(new_feed)
nb += 1
return nb
-
nb = read(subscriptions)
- g.db.session.commit()
+ db.session.commit()
return nb
def import_json(email, json_content):
@@ -146,44 +163,46 @@ def import_json(email, json_content):
user = User.query.filter(User.email == email).first()
json_account = json.loads(json_content)
nb_feeds, nb_articles = 0, 0
-
- # Create feeds
+ # Create feeds:
for feed in json_account["result"]:
-
- if None != Feed.query.filter(Feed.user_id == user.id, Feed.link == feed["link"]).first():
+ if None != Feed.query.filter(Feed.user_id == user.id,
+ Feed.link == feed["link"]).first():
continue
-
- new_feed = Feed(title=feed["title"], description="", link=feed["link"], \
- site_link=feed["site_link"], \
- created_date=datetime.datetime.fromtimestamp(int(feed["created_date"])),
- enabled=feed["enabled"])
+ new_feed = Feed(title=feed["title"],
+ description="",
+ link=feed["link"],
+ site_link=feed["site_link"],
+ created_date=datetime.datetime.\
+ fromtimestamp(int(feed["created_date"])),
+ enabled=feed["enabled"])
user.feeds.append(new_feed)
nb_feeds += 1
- g.db.session.commit()
-
- # Create articles
+ db.session.commit()
+ # Create articles:
for feed in json_account["result"]:
- user_feed = Feed.query.filter(Feed.user_id == user.id, Feed.link == feed["link"]).first()
+ user_feed = Feed.query.filter(Feed.user_id == user.id,
+ Feed.link == feed["link"]).first()
if None != user_feed:
for article in feed["articles"]:
-
if None == Article.query.filter(Article.user_id == user.id,
- Article.feed_id == user_feed.id,
- Article.link == article["link"]).first():
-
- new_article = Article(link=article["link"], title=article["title"], \
- content=article["content"], readed=article["readed"], like=article["like"], \
- retrieved_date=datetime.datetime.fromtimestamp(int(article["retrieved_date"])),
- date=datetime.datetime.fromtimestamp(int(article["date"])),
- user_id=user.id, feed_id=user_feed.id)
-
+ Article.feed_id == user_feed.id,
+ Article.link == article["link"]).first():
+ new_article = Article(link=article["link"],
+ title=article["title"],
+ content=article["content"],
+ readed=article["readed"],
+ like=article["like"], \
+ retrieved_date=datetime.datetime.\
+ fromtimestamp(int(article["retrieved_date"])),
+ date=datetime.datetime.\
+ fromtimestamp(int(article["date"])),
+ user_id=user.id,
+ feed_id=user_feed.id)
user_feed.articles.append(new_article)
nb_articles += 1
- g.db.session.commit()
-
+ db.session.commit()
return nb_feeds, nb_articles
-
def clean_url(url):
"""
Remove utm_* parameters
@@ -201,7 +220,6 @@ def clean_url(url):
parsed_url.fragment
]).rstrip('=')
-
def open_url(url):
"""
Open an URL with the proxy and the user-agent
@@ -220,7 +238,6 @@ def open_url(url):
# server couldn't fulfill the request
error = (url, e.code, \
http.server.BaseHTTPRequestHandler.responses[e.code][1])
- #pyaggr3g470r_log.error(url + " " + str(e.code) + " " + http.server.BaseHTTPRequestHandler.responses[e.code][1])
return (False, error)
except urllib.error.URLError as e:
# failed to reach the server
@@ -229,10 +246,8 @@ def open_url(url):
#pyaggr3g470r_log.error(url + " " + e.reason)
else:
error = (url, e.reason.errno, e.reason.strerror)
- #pyaggr3g470r_log.error(url + " " + str(e.reason.errno) + " " + e.reason.strerror)
return (False, error)
-
def clear_string(data):
"""
Clear a string by removing HTML tags, HTML special caracters
@@ -242,7 +257,6 @@ def clear_string(data):
q = re.compile('\s') # consecutive white spaces
return p.sub('', q.sub(' ', data))
-
def load_stop_words():
"""
Load the stop words and return them in a list.
@@ -258,7 +272,6 @@ def load_stop_words():
stop_words += stop_wods_file.read().split(";")
return stop_words
-
def top_words(articles, n=10, size=5):
"""
Return the n most frequent words in a list.
@@ -273,16 +286,30 @@ def top_words(articles, n=10, size=5):
words[word] += 1
return words.most_common(n)
-
def tag_cloud(tags):
"""
Generates a tags cloud.
"""
tags.sort(key=operator.itemgetter(0))
- return '\n'.join([('<font size=%d><a href="/search/?query=%s" title="Count: %s">%s</a></font>' % \
+ return '\n'.join([('<font size=%d><a href="/search?query=%s" title="Count: %s">%s</a></font>' % \
(min(1 + count * 7 / max([tag[1] for tag in tags]), 7), word, format(count, ',d'), word)) \
for (word, count) in tags])
+def compare_documents(feed):
+ """
+ Compare a list of documents by pair.
+ Pairs of duplicates are sorted by "retrieved date".
+ """
+ duplicates = []
+ for pair in itertools.combinations(feed.articles, 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 duplicates
def search_feed(url):
"""
@@ -306,7 +333,6 @@ def search_feed(url):
return feed_link['href']
return None
-
if __name__ == "__main__":
import_opml("root@pyAggr3g470r.localhost", "./var/feeds_test.opml")
#import_opml("root@pyAggr3g470r.localhost", "./var/pyAggr3g470r.opml")
diff --git a/pyaggr3g470r/views/api/article.py b/pyaggr3g470r/views/api/article.py
index 17881412..c3ec2d34 100644
--- a/pyaggr3g470r/views/api/article.py
+++ b/pyaggr3g470r/views/api/article.py
@@ -1,3 +1,6 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -
+
from flask import g
import dateutil.parser
diff --git a/pyaggr3g470r/views/api/common.py b/pyaggr3g470r/views/api/common.py
index 48a0d0ac..b8477d4b 100644
--- a/pyaggr3g470r/views/api/common.py
+++ b/pyaggr3g470r/views/api/common.py
@@ -1,7 +1,10 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -
+
"""For a given resources, classes in the module intend to create the following
routes :
GET resource/<id>
- -> to retreive one
+ -> to retrieve one
POST resource
-> to create one
PUT resource/<id>
@@ -10,7 +13,7 @@ routes :
-> to delete one
GET resources
- -> to retreive several
+ -> to retrieve several
POST resources
-> to create several
PUT resources
@@ -21,7 +24,6 @@ routes :
import json
import logging
import dateutil.parser
-from copy import deepcopy
from functools import wraps
from werkzeug.exceptions import Unauthorized, BadRequest
from flask import request, g, session, Response
@@ -54,13 +56,11 @@ def authenticate(func):
and user.activation_key == "":
g.user = user
logged_in = True
-
if logged_in:
return func(*args, **kwargs)
raise Unauthorized({'WWWAuthenticate': 'Basic realm="Login Required"'})
return wrapper
-
def to_response(func):
"""Will cast results of func as a result, and try to extract
a status_code for the Response object"""
@@ -146,12 +146,16 @@ class PyAggResourceExisting(PyAggAbstractResource):
class PyAggResourceMulti(PyAggAbstractResource):
def get(self):
- """retreive several objects. filters can be set in the payload on the
+ """retrieve several objects. filters can be set in the payload on the
different fields of the object, and a limit can be set in there as well
"""
- if 'application/json' != request.headers.get('Content-Type'):
+ if 'application/json' not in request.headers.get('Content-Type'):
raise BadRequest("Content-Type must be application/json")
- limit = request.json.pop('limit', 10)
+ limit = 10
+ try:
+ limit = request.json.pop('limit', 10)
+ except:
+ return [res for res in self.controller.read().limit(limit)]
if not limit:
return [res for res in self.controller.read(**request.json).all()]
return [res for res in self.controller.read(**request.json).limit(limit)]
@@ -159,7 +163,7 @@ class PyAggResourceMulti(PyAggAbstractResource):
def post(self):
"""creating several objects. payload should be a list of dict.
"""
- if 'application/json' != request.headers.get('Content-Type'):
+ if 'application/json' not in request.headers.get('Content-Type'):
raise BadRequest("Content-Type must be application/json")
status = 201
results = []
@@ -180,7 +184,7 @@ class PyAggResourceMulti(PyAggAbstractResource):
[[obj_id1, {attr1: val1, attr2: val2}]
[obj_id2, {attr1: val1, attr2: val2}]]
"""
- if 'application/json' != request.headers.get('Content-Type'):
+ if 'application/json' not in request.headers.get('Content-Type'):
raise BadRequest("Content-Type must be application/json")
status = 200
results = []
@@ -201,7 +205,7 @@ class PyAggResourceMulti(PyAggAbstractResource):
def delete(self):
"""will delete several objects,
a list of their ids should be in the payload"""
- if 'application/json' != request.headers.get('Content-Type'):
+ if 'application/json' not in request.headers.get('Content-Type'):
raise BadRequest("Content-Type must be application/json")
status = 204
results = []
diff --git a/pyaggr3g470r/views/api/feed.py b/pyaggr3g470r/views/api/feed.py
index 0d83ea43..7d0e2862 100644
--- a/pyaggr3g470r/views/api/feed.py
+++ b/pyaggr3g470r/views/api/feed.py
@@ -1,3 +1,6 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -
+
from flask import g
from pyaggr3g470r.controllers.feed import FeedController, \
@@ -8,7 +11,6 @@ from pyaggr3g470r.views.api.common import PyAggAbstractResource, \
PyAggResourceExisting, \
PyAggResourceMulti
-
FEED_ATTRS = {'title': {'type': str},
'description': {'type': str},
'link': {'type': str},
@@ -20,25 +22,21 @@ FEED_ATTRS = {'title': {'type': str},
'last_error': {'type': str},
'error_count': {'type': int, 'default': 0}}
-
class FeedNewAPI(PyAggResourceNew):
controller_cls = FeedController
attrs = FEED_ATTRS
to_date = ['date', 'last_retrieved']
-
class FeedAPI(PyAggResourceExisting):
controller_cls = FeedController
attrs = FEED_ATTRS
to_date = ['date', 'last_retrieved']
-
class FeedsAPI(PyAggResourceMulti):
controller_cls = FeedController
attrs = FEED_ATTRS
to_date = ['date', 'last_retrieved']
-
class FetchableFeedAPI(PyAggAbstractResource):
controller_cls = FeedController
to_date = ['date', 'last_retrieved']
@@ -49,7 +47,6 @@ class FetchableFeedAPI(PyAggAbstractResource):
return [feed for feed in self.controller.list_fetchable(
**self.reqparse_args())]
-
g.api.add_resource(FeedNewAPI, '/feed', endpoint='feed_new.json')
g.api.add_resource(FeedAPI, '/feed/<int:obj_id>', endpoint='feed.json')
g.api.add_resource(FeedsAPI, '/feeds', endpoint='feeds.json')
diff --git a/pyaggr3g470r/views/article.py b/pyaggr3g470r/views/article.py
index 66cc0f37..08c92686 100644
--- a/pyaggr3g470r/views/article.py
+++ b/pyaggr3g470r/views/article.py
@@ -1,3 +1,6 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -
+
from flask import Blueprint, g, render_template, redirect
from sqlalchemy import desc
@@ -18,10 +21,9 @@ def articles(feed_id=None, nb_articles=-1):
feed.articles = controllers.ArticleController(g.user.id)\
.read(feed_id=feed.id)\
.order_by(desc("Article.date"))
- if len(feed.articles.all()) <= nb_articles:
- nb_articles = -1
- if nb_articles == -1:
- feed.articles = feed.articles.limit(nb_articles)
+ if len(feed.articles.all()) <= nb_articles or nb_articles == -1:
+ nb_articles = int(1e9)
+ feed.articles = feed.articles.limit(nb_articles)
return render_template('articles.html', feed=feed, nb_articles=nb_articles)
diff --git a/pyaggr3g470r/views/feed.py b/pyaggr3g470r/views/feed.py
index 2af502a7..4fe4e5da 100644
--- a/pyaggr3g470r/views/feed.py
+++ b/pyaggr3g470r/views/feed.py
@@ -1,5 +1,9 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -
+
from datetime import datetime
from flask import Blueprint, g, render_template
+from sqlalchemy import desc
from pyaggr3g470r import controllers, utils
from pyaggr3g470r.decorators import pyagg_default_decorator, \
@@ -22,9 +26,9 @@ def feed(feed_id=None):
"Presents detailed information about a feed."
feed = controllers.FeedController(g.user.id).get(id=feed_id)
word_size = 6
- articles = controllers.ArticleController(g.user.id)\
- .read(feed_id=feed_id).all()
- nb_articles = controllers.ArticleController(g.user.id).read().count()
+ articles = controllers.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)
@@ -46,5 +50,4 @@ def feed(feed_id=None):
feed=feed, tag_cloud=tag_cloud,
first_post_date=first_article,
end_post_date=last_article,
- nb_articles=nb_articles,
average=average, delta=delta, elapsed=elapsed)
diff --git a/pyaggr3g470r/views/views.py b/pyaggr3g470r/views/views.py
index 7934eef8..e202ad4d 100644
--- a/pyaggr3g470r/views/views.py
+++ b/pyaggr3g470r/views/views.py
@@ -27,14 +27,13 @@ __copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "AGPLv3"
import os
-import json
import string
import random
import hashlib
import datetime
from collections import namedtuple
from bootstrap import application as app, db
-from flask import render_template, request, flash, session, Response, \
+from flask import render_template, request, flash, session, \
url_for, redirect, g, current_app, make_response, jsonify
from flask.ext.login import LoginManager, login_user, logout_user, \
login_required, current_user, AnonymousUserMixin
@@ -47,7 +46,8 @@ from sqlalchemy.exc import IntegrityError
from werkzeug import generate_password_hash
import conf
-from pyaggr3g470r import utils, notifications, export, duplicate
+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, \
@@ -93,7 +93,7 @@ def before_request():
@login_manager.user_loader
def load_user(email):
# Return an instance of the User model
- return User.query.filter(User.email == email).first()
+ return controllers.UserController(email).get(email=email)
#
@@ -153,7 +153,7 @@ def login():
form = SigninForm()
if form.validate_on_submit():
- user = User.query.filter(User.email == form.email.data).first()
+ user = controllers.UserController(form.email.data).get(email=form.email.data)
login_user(user)
g.user = user
session['email'] = form.email.data
@@ -265,8 +265,12 @@ def fetch(feed_id=None):
Triggers the download of news.
News are downloaded in a separated process, mandatory for Heroku.
"""
- utils.fetch(g.user.id, feed_id)
- flash(gettext("Downloading articles..."), 'info')
+ if not conf.ON_HEROKU or g.user.is_admin():
+ utils.fetch(g.user.id, feed_id)
+ flash(gettext("Downloading articles..."), "info")
+ else:
+ flash(gettext("The manual retrieving of news is only available " +
+ "for administrator, on the Heroku platform."), "info")
return redirect(redirect_url())
@app.route('/about', methods=['GET'])
@@ -378,7 +382,7 @@ def inactives():
List of inactive feeds.
"""
nb_days = int(request.args.get('nb_days', 365))
- user = User.query.filter(User.id == g.user.id).first()
+ user = controllers.UserController(g.user.email).get(email=g.user.email)
today = datetime.datetime.now()
inactives = []
for feed in user.feeds:
@@ -399,7 +403,7 @@ def duplicates(feed_id=None):
"""
feed = Feed.query.filter(Feed.user_id == g.user.id, Feed.id == feed_id).first()
duplicates = []
- duplicates = duplicate.compare_documents(feed)
+ duplicates = utils.compare_documents(feed)
return render_template('duplicates.html', duplicates=duplicates, feed=feed)
@app.route('/index_database', methods=['GET'])
@@ -425,7 +429,7 @@ def export_articles():
"""
Export all articles to HTML or JSON.
"""
- user = User.query.filter(User.id == g.user.id).first()
+ user = controllers.UserController(g.user.email).get(id=g.user.id)
if request.args.get('format') == "HTML":
# Export to HTML
try:
@@ -457,7 +461,7 @@ def export_opml():
"""
Export all feeds to OPML.
"""
- user = User.query.filter(User.id == g.user.id).first()
+ user = controllers.UserController(g.user.email).get(id=g.user.id)
response = make_response(render_template('opml.xml', user=user, now=datetime.datetime.now()))
response.headers['Content-Type'] = 'application/xml'
response.headers['Content-Disposition'] = 'attachment; filename=feeds.opml'
@@ -506,7 +510,7 @@ def management():
if None != request.files.get('opmlfile', None):
# Import an OPML file
data = request.files.get('opmlfile', None)
- if not g.allowed_file(data.filename):
+ if not utils.allowed_file(data.filename):
flash(gettext('File not allowed.'), 'danger')
else:
try:
@@ -519,7 +523,7 @@ def management():
elif None != request.files.get('jsonfile', None):
# Import an account
data = request.files.get('jsonfile', None)
- if not g.allowed_file(data.filename):
+ if not utils.allowed_file(data.filename):
flash(gettext('File not allowed.'), 'danger')
else:
try:
@@ -540,10 +544,15 @@ def management():
not_on_heroku = not conf.ON_HEROKU)
@app.route('/history', methods=['GET'])
+@app.route('/history/<int:year>', methods=['GET'])
+@app.route('/history/<int:year>/<int:month>', methods=['GET'])
@login_required
-def history():
- #user = User.query.filter(User.id == g.user.id).first()
- return render_template('history.html')
+def history(year=None, month=None):
+ articles_counter, articles = utils.history(g.user.id, year, month)
+ return render_template('history.html',
+ articles_counter=articles_counter,
+ articles=articles,
+ year=year, month=month)
@app.route('/bookmarklet', methods=['GET'])
@app.route('/create_feed', methods=['GET', 'POST'])
@@ -628,7 +637,7 @@ def profile():
"""
Edit the profile of the currently logged user.
"""
- user = User.query.filter(User.email == g.user.email).first()
+ user = controllers.UserController(g.user.email).get(id=g.user.id)
form = ProfileForm()
if request.method == 'POST':
@@ -654,7 +663,7 @@ def delete_account():
"""
Delete the account of the user (with all its data).
"""
- user = User.query.filter(User.email == g.user.email).first()
+ user = controllers.UserController(g.user.email).get(id=g.user.id)
if user is not None:
db.session.delete(user)
db.session.commit()
bgstack15