aboutsummaryrefslogtreecommitdiff
path: root/src/web/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/web/views')
-rw-r--r--src/web/views/__init__.py7
-rw-r--r--src/web/views/admin.py166
-rw-r--r--src/web/views/api/__init__.py4
-rw-r--r--src/web/views/api/article.py7
-rw-r--r--src/web/views/api/category.py31
-rw-r--r--src/web/views/api/feed.py14
-rw-r--r--src/web/views/article.py104
-rw-r--r--src/web/views/category.py86
-rw-r--r--src/web/views/feed.py68
-rw-r--r--src/web/views/icon.py1
-rw-r--r--src/web/views/user.py151
-rw-r--r--src/web/views/views.py623
12 files changed, 729 insertions, 533 deletions
diff --git a/src/web/views/__init__.py b/src/web/views/__init__.py
index 36d382bd..27370fc3 100644
--- a/src/web/views/__init__.py
+++ b/src/web/views/__init__.py
@@ -3,4 +3,11 @@ from .api import *
from .article import article_bp, articles_bp
from .feed import feed_bp, feeds_bp
+from .category import category_bp, categories_bp
from .icon import icon_bp
+from .admin import admin_bp
+from .user import user_bp, users_bp
+
+
+__all__ = ['article_bp', 'articles_bp', 'feed_bp', 'feeds_bp', 'category_bp',
+ 'categories_bp', 'icon_bp', 'admin_bp', 'user_bp', 'users_bp']
diff --git a/src/web/views/admin.py b/src/web/views/admin.py
new file mode 100644
index 00000000..ec79262d
--- /dev/null
+++ b/src/web/views/admin.py
@@ -0,0 +1,166 @@
+from flask import (Blueprint, g, render_template, redirect,
+ flash, url_for, request)
+from flask.ext.babel import gettext
+from flask.ext.login import login_required
+
+from flask.ext.principal import Permission, RoleNeed
+
+from web.lib.utils import redirect_url
+from web.models import Role
+from web.controllers import UserController, ArticleController
+
+from web.forms import InformationMessageForm, UserForm
+from web import notifications
+
+admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
+admin_permission = Permission(RoleNeed('admin'))
+
+
+@admin_bp.route('/dashboard', methods=['GET', 'POST'])
+@login_required
+@admin_permission.require(http_exception=403)
+def dashboard():
+ """
+ Adminstrator's dashboard.
+ """
+ form = InformationMessageForm()
+
+ if request.method == 'POST':
+ if form.validate():
+ try:
+ notifications.information_message(form.subject.data,
+ form.message.data)
+ except Exception as error:
+ flash(gettext(
+ 'Problem while sending email: %(error)s', error=error),
+ 'danger')
+
+ users = UserController().read()
+ return render_template('admin/dashboard.html',
+ users=users, current_user=g.user, form=form)
+
+
+@admin_bp.route('/user/create', methods=['GET'])
+@admin_bp.route('/user/edit/<int:user_id>', methods=['GET'])
+@login_required
+@admin_permission.require(http_exception=403)
+def user_form(user_id=None):
+ if user_id is not None:
+ user = UserController().get(id=user_id)
+ form = UserForm(obj=user)
+ message = gettext('Edit the user <i>%(nick)s</i>', nick=user.nickname)
+ else:
+ form = UserForm()
+ message = gettext('Add a new user')
+ return render_template('/admin/create_user.html',
+ form=form, message=message)
+
+
+@admin_bp.route('/user/create', methods=['POST'])
+@admin_bp.route('/user/edit/<int:user_id>', methods=['POST'])
+@login_required
+@admin_permission.require(http_exception=403)
+def process_user_form(user_id=None):
+ """
+ Create or edit a user.
+ """
+ form = UserForm()
+ user_contr = UserController()
+
+ if not form.validate():
+ return render_template('/admin/create_user.html', form=form,
+ message=gettext('Some errors were found'))
+
+ role_user = Role.query.filter(Role.name == "user").first()
+ if user_id is not None:
+ # Edit a user
+ user_contr.update({'id': user_id},
+ {'nickname': form.nickname.data,
+ 'email': form.email.data,
+ 'password': form.password.data,
+ 'refresh_rate': form.refresh_rate.data})
+ user = user_contr.get(id=user_id)
+ flash(gettext('User %(nick)s successfully updated',
+ nick=user.nickname), 'success')
+ else:
+ # Create a new user
+ user = user_contr.create(nickname=form.nickname.data,
+ email=form.email.data,
+ password=form.password.data,
+ roles=[role_user],
+ refresh_rate=form.refresh_rate.data,
+ activation_key="")
+ flash(gettext('User %(nick)s successfully created',
+ nick=user.nickname), 'success')
+ return redirect(url_for('admin.user_form', user_id=user.id))
+
+
+@admin_bp.route('/user/<int:user_id>', methods=['GET'])
+@login_required
+@admin_permission.require(http_exception=403)
+def user(user_id=None):
+ """
+ See information about a user (stations, etc.).
+ """
+ user = UserController().get(id=user_id)
+ if user is not None:
+ article_contr = ArticleController(user_id)
+ return render_template('/admin/user.html', user=user, feeds=user.feeds,
+ article_count=article_contr.count_by_feed(),
+ unread_article_count=article_contr.count_by_feed(readed=False))
+
+ else:
+ flash(gettext('This user does not exist.'), 'danger')
+ return redirect(redirect_url())
+
+
+@admin_bp.route('/delete_user/<int:user_id>', methods=['GET'])
+@login_required
+@admin_permission.require(http_exception=403)
+def delete_user(user_id=None):
+ """
+ Delete a user (with all its data).
+ """
+ try:
+ user = UserController().delete(user_id)
+ flash(gettext('User %(nick)s successfully deleted',
+ nick=user.nickname), 'success')
+ except Exception as error:
+ flash(gettext('An error occured while trying to delete a user: '
+ '%(error)', error=error), 'danger')
+ return redirect(redirect_url())
+
+
+@admin_bp.route('/toggle_user/<int:user_id>', methods=['GET'])
+@login_required
+@admin_permission.require()
+def toggle_user(user_id=None):
+ """
+ Enable or disable the account of a user.
+ """
+ user_contr = UserController()
+ user = user_contr.get(id=user_id)
+
+ if user is None:
+ flash(gettext('This user does not exist.'), 'danger')
+ return redirect(url_for('admin.dashboard'))
+
+ if user.activation_key != "":
+
+ # Send the confirmation email
+ try:
+ notifications.new_account_activation(user)
+ user_contr.unset_activation_key(user.id)
+ message = gettext('Account of the user %(nick)s successfully '
+ 'activated.', nick=user.nickname)
+ except Exception as error:
+ flash(gettext('Problem while sending activation email %(error)s:',
+ error=error), 'danger')
+ return redirect(url_for('admin.dashboard'))
+
+ else:
+ user_contr.set_activation_key(user.id)
+ message = gettext('Account of the user %(nick)s successfully disabled',
+ nick=user.nickname)
+ flash(message, 'success')
+ return redirect(url_for('admin.dashboard'))
diff --git a/src/web/views/api/__init__.py b/src/web/views/api/__init__.py
index 24472ebe..ed3bd702 100644
--- a/src/web/views/api/__init__.py
+++ b/src/web/views/api/__init__.py
@@ -26,6 +26,6 @@ __revision__ = "$Date: 2014/07/05 $"
__copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "AGPLv3"
-from web.views.api import article, feed
+from web.views.api import article, feed, category
-__all__ = ['article', 'feed']
+__all__ = ['article', 'feed', 'category']
diff --git a/src/web/views/api/article.py b/src/web/views/api/article.py
index 51844b20..23c5c495 100644
--- a/src/web/views/api/article.py
+++ b/src/web/views/api/article.py
@@ -13,12 +13,15 @@ from web.views.api.common import PyAggAbstractResource,\
ARTICLE_ATTRS = {'user_id': {'type': int},
'feed_id': {'type': int},
+ 'category_id': {'type': int},
'entry_id': {'type': str},
'link': {'type': str},
'title': {'type': str},
- 'readed': {'type': bool}, 'like': {'type': bool},
+ 'readed': {'type': bool},
+ 'like': {'type': bool},
'content': {'type': str},
- 'date': {'type': str}, 'retrieved_date': {'type': str}}
+ 'date': {'type': str},
+ 'retrieved_date': {'type': str}}
class ArticleNewAPI(PyAggResourceNew):
diff --git a/src/web/views/api/category.py b/src/web/views/api/category.py
new file mode 100644
index 00000000..7923279a
--- /dev/null
+++ b/src/web/views/api/category.py
@@ -0,0 +1,31 @@
+from flask import g
+
+from web.controllers.category import CategoryController
+from web.views.api.common import (PyAggResourceNew,
+ PyAggResourceExisting,
+ PyAggResourceMulti)
+
+
+CAT_ATTRS = {'name': {'type': str},
+ 'user_id': {'type': int}}
+
+
+class CategoryNewAPI(PyAggResourceNew):
+ controller_cls = CategoryController
+ attrs = CAT_ATTRS
+
+
+class CategoryAPI(PyAggResourceExisting):
+ controller_cls = CategoryController
+ attrs = CAT_ATTRS
+
+
+class CategoriesAPI(PyAggResourceMulti):
+ controller_cls = CategoryController
+ attrs = CAT_ATTRS
+
+
+g.api.add_resource(CategoryNewAPI, '/category', endpoint='category_new.json')
+g.api.add_resource(CategoryAPI, '/category/<int:obj_id>',
+ endpoint='category.json')
+g.api.add_resource(CategoriesAPI, '/categories', endpoint='categories.json')
diff --git a/src/web/views/api/feed.py b/src/web/views/api/feed.py
index 2bb9814f..604620b4 100644
--- a/src/web/views/api/feed.py
+++ b/src/web/views/api/feed.py
@@ -4,23 +4,25 @@
from flask import g
from web.controllers.feed import (FeedController,
- DEFAULT_MAX_ERROR,
- DEFAULT_LIMIT,
- DEFAULT_REFRESH_RATE)
+ DEFAULT_MAX_ERROR,
+ DEFAULT_LIMIT,
+ DEFAULT_REFRESH_RATE)
from web.views.api.common import PyAggAbstractResource, \
- PyAggResourceNew, \
- PyAggResourceExisting, \
- PyAggResourceMulti
+ PyAggResourceNew, \
+ PyAggResourceExisting, \
+ PyAggResourceMulti
FEED_ATTRS = {'title': {'type': str},
'description': {'type': str},
'link': {'type': str},
'user_id': {'type': int},
+ 'category_id': {'type': int},
'site_link': {'type': str},
'enabled': {'type': bool, 'default': True},
'etag': {'type': str, 'default': ''},
'icon_url': {'type': str, 'default': ''},
+ 'filters': {'type': list},
'last_modified': {'type': str},
'last_retrieved': {'type': str},
'last_error': {'type': str},
diff --git a/src/web/views/article.py b/src/web/views/article.py
index bb914a6b..5b04fe7a 100644
--- a/src/web/views/article.py
+++ b/src/web/views/article.py
@@ -1,31 +1,37 @@
#! /usr/bin/env python
# -*- coding: utf-8 -
+from datetime import datetime, timedelta
+from flask import (Blueprint, g, render_template, redirect,
+ flash, url_for, request)
+from flask.ext.babel import gettext
+from flask.ext.login import login_required
-from flask import Blueprint, g, render_template, redirect
-
-from web import controllers, utils
+from web.lib.utils import clear_string, redirect_url
+from web.controllers import ArticleController
from web.lib.view_utils import etag_match
-from web.decorators import pyagg_default_decorator
articles_bp = Blueprint('articles', __name__, url_prefix='/articles')
article_bp = Blueprint('article', __name__, url_prefix='/article')
@article_bp.route('/redirect/<int:article_id>', methods=['GET'])
-@pyagg_default_decorator
+@login_required
def redirect_to_article(article_id):
- article = controllers.ArticleController(g.user.id).get(id=article_id)
+ contr = ArticleController(g.user.id)
+ article = contr.get(id=article_id)
+ if not article.readed:
+ contr.update({'id': article.id}, {'readed': True})
return redirect(article.link)
@article_bp.route('/<int:article_id>', methods=['GET'])
-@pyagg_default_decorator
+@login_required
@etag_match
def article(article_id=None):
"""
Presents the content of an article.
"""
- article = controllers.ArticleController(g.user.id).get(id=article_id)
+ article = ArticleController(g.user.id).get(id=article_id)
previous_article = article.previous_article()
if previous_article is None:
previous_article = article.source.articles[0]
@@ -34,7 +40,87 @@ def article(article_id=None):
next_article = article.source.articles[-1]
return render_template('article.html',
- head_titles=[utils.clear_string(article.title)],
+ head_titles=[clear_string(article.title)],
article=article,
previous_article=previous_article,
next_article=next_article)
+
+
+@article_bp.route('/like/<int:article_id>', methods=['GET'])
+@login_required
+def like(article_id=None):
+ """
+ Mark or unmark an article as favorites.
+ """
+ art_contr = ArticleController(g.user.id)
+ article = art_contr.get(id=article_id)
+ art_contr = art_contr.update({'id': article_id},
+ {'like': not article.like})
+ return redirect(redirect_url())
+
+
+@article_bp.route('/delete/<int:article_id>', methods=['GET'])
+@login_required
+def delete(article_id=None):
+ """
+ Delete an article from the database.
+ """
+ article = ArticleController(g.user.id).delete(article_id)
+ flash(gettext('Article %(article_title)s deleted',
+ article_title=article.title), 'success')
+ return redirect(url_for('home'))
+
+
+@articles_bp.route('/history', methods=['GET'])
+@articles_bp.route('/history/<int:year>', methods=['GET'])
+@articles_bp.route('/history/<int:year>/<int:month>', methods=['GET'])
+@login_required
+def history(year=None, month=None):
+ counter, articles = ArticleController(g.user.id).get_history(year, month)
+ return render_template('history.html', articles_counter=counter,
+ articles=articles, year=year, month=month)
+
+
+@article_bp.route('/mark_as/<string:new_value>', methods=['GET'])
+@article_bp.route('/mark_as/<string:new_value>/article/<int:article_id>',
+ methods=['GET'])
+@login_required
+def mark_as(new_value='read', feed_id=None, article_id=None):
+ """
+ Mark all unreaded articles as read.
+ """
+ readed = new_value == 'read'
+ art_contr = ArticleController(g.user.id)
+ filters = {'readed': not readed}
+ if feed_id is not None:
+ filters['feed_id'] = feed_id
+ message = 'Feed marked as %s.'
+ elif article_id is not None:
+ filters['id'] = article_id
+ message = 'Article marked as %s.'
+ else:
+ message = 'All article marked as %s.'
+ art_contr.update(filters, {"readed": readed})
+ flash(gettext(message % new_value), 'info')
+
+ if readed:
+ return redirect(redirect_url())
+ return redirect('home')
+
+
+@articles_bp.route('/expire_articles', methods=['GET'])
+@login_required
+def expire():
+ """
+ Delete articles older than the given number of weeks.
+ """
+ current_time = datetime.utcnow()
+ weeks_ago = current_time - timedelta(int(request.args.get('weeks', 10)))
+ art_contr = ArticleController(g.user.id)
+
+ query = art_contr.read(__or__={'date__lt': weeks_ago,
+ 'retrieved_date__lt': weeks_ago})
+ count = query.count()
+ query.delete()
+ flash(gettext('%(count)d articles deleted', count=count), 'info')
+ return redirect(redirect_url())
diff --git a/src/web/views/category.py b/src/web/views/category.py
new file mode 100644
index 00000000..20b90caa
--- /dev/null
+++ b/src/web/views/category.py
@@ -0,0 +1,86 @@
+from flask import g, Blueprint, render_template, flash, redirect, url_for
+from flask.ext.babel import gettext
+from flask.ext.login import login_required
+
+from web.forms import CategoryForm
+from web.lib.utils import redirect_url
+from web.lib.view_utils import etag_match
+from web.controllers import ArticleController, FeedController, \
+ CategoryController
+
+categories_bp = Blueprint('categories', __name__, url_prefix='/categories')
+category_bp = Blueprint('category', __name__, url_prefix='/category')
+
+
+@categories_bp.route('/', methods=['GET'])
+@login_required
+@etag_match
+def list_():
+ "Lists the subscribed feeds in a table."
+ art_contr = ArticleController(g.user.id)
+ return render_template('categories.html',
+ categories=list(CategoryController(g.user.id).read()),
+ feeds_count=FeedController(g.user.id).count_by_category(),
+ unread_article_count=art_contr.count_by_category(readed=False),
+ article_count=art_contr.count_by_category())
+
+
+@category_bp.route('/create', methods=['GET'])
+@category_bp.route('/edit/<int:category_id>', methods=['GET'])
+@login_required
+@etag_match
+def form(category_id=None):
+ action = gettext("Add a category")
+ head_titles = [action]
+ if category_id is None:
+ return render_template('edit_category.html', action=action,
+ head_titles=head_titles, form=CategoryForm())
+ category = CategoryController(g.user.id).get(id=category_id)
+ action = gettext('Edit category')
+ head_titles = [action]
+ if category.name:
+ head_titles.append(category.name)
+ return render_template('edit_category.html', action=action,
+ head_titles=head_titles, category=category,
+ form=CategoryForm(obj=category))
+
+
+@category_bp.route('/delete/<int:category_id>', methods=['GET'])
+@login_required
+def delete(category_id=None):
+ category = CategoryController(g.user.id).delete(category_id)
+ flash(gettext("Category %(category_name)s successfully deleted.",
+ category_name=category.name), 'success')
+ return redirect(redirect_url())
+
+
+@category_bp.route('/create', methods=['POST'])
+@category_bp.route('/edit/<int:category_id>', methods=['POST'])
+@login_required
+def process_form(category_id=None):
+ form = CategoryForm()
+ cat_contr = CategoryController(g.user.id)
+
+ if not form.validate():
+ return render_template('edit_category.html', form=form)
+ existing_cats = list(cat_contr.read(name=form.name.data))
+ if existing_cats and category_id is None:
+ flash(gettext("Couldn't add category: already exists."), "warning")
+ return redirect(url_for('category.form',
+ category_id=existing_cats[0].id))
+ # Edit an existing category
+ category_attr = {'name': form.name.data}
+
+ if category_id is not None:
+ cat_contr.update({'id': category_id}, category_attr)
+ flash(gettext('Category %(cat_name)r successfully updated.',
+ cat_name=category_attr['name']), 'success')
+ return redirect(url_for('category.form', category_id=category_id))
+
+ # Create a new category
+ new_category = cat_contr.create(**category_attr)
+
+ flash(gettext('Category %(category_name)r successfully created.',
+ category_name=new_category.name), 'success')
+
+ return redirect(url_for('category.form', category_id=new_category.id))
diff --git a/src/web/views/feed.py b/src/web/views/feed.py
index 2a9b2da8..959179f9 100644
--- a/src/web/views/feed.py
+++ b/src/web/views/feed.py
@@ -1,14 +1,13 @@
#! /usr/bin/env python
# -*- coding: utf-8 -
-import base64
+import logging
import requests.exceptions
-from hashlib import md5
from datetime import datetime, timedelta
from sqlalchemy import desc
from werkzeug.exceptions import BadRequest
from flask import Blueprint, g, render_template, flash, \
- redirect, request, url_for, Response
+ redirect, request, url_for
from flask.ext.babel import gettext
from flask.ext.login import login_required
@@ -17,8 +16,10 @@ from web import utils
from web.lib.view_utils import etag_match
from web.lib.feed_utils import construct_feed_from
from web.forms import AddFeedForm
-from web.controllers import FeedController, ArticleController
+from web.controllers import (CategoryController, FeedController,
+ ArticleController)
+logger = logging.getLogger(__name__)
feeds_bp = Blueprint('feeds', __name__, url_prefix='/feeds')
feed_bp = Blueprint('feed', __name__, url_prefix='/feed')
@@ -42,9 +43,12 @@ def feed(feed_id=None):
"Presents detailed information about a feed."
feed = FeedController(g.user.id).get(id=feed_id)
word_size = 6
+ category = None
+ if feed.category_id:
+ category = CategoryController(g.user.id).get(id=feed.category_id)
articles = ArticleController(g.user.id) \
.read(feed_id=feed_id) \
- .order_by(desc("Article.date")).all()
+ .order_by(desc("date")).all()
top_words = utils.top_words(articles, n=50, size=int(word_size))
tag_cloud = utils.tag_cloud(top_words)
@@ -65,7 +69,7 @@ def feed(feed_id=None):
head_titles=[utils.clear_string(feed.title)],
feed=feed, tag_cloud=tag_cloud,
first_post_date=first_article,
- end_post_date=last_article,
+ end_post_date=last_article, category=category,
average=average, delta=delta, elapsed=elapsed)
@@ -91,11 +95,12 @@ def reset_errors(feed_id):
return redirect(request.referrer or url_for('home'))
-@feed_bp.route('/bookmarklet', methods=['GET'])
+@feed_bp.route('/bookmarklet', methods=['GET', 'POST'])
@login_required
def bookmarklet():
feed_contr = FeedController(g.user.id)
- url = request.args.get('url', None)
+ url = (request.args if request.method == 'GET' else request.form)\
+ .get('url', None)
if not url:
flash(gettext("Couldn't add feed: url missing."), "error")
raise BadRequest("url is missing")
@@ -112,6 +117,9 @@ def bookmarklet():
flash(gettext("Impossible to connect to the address: {}.".format(url)),
"danger")
return redirect(url_for('home'))
+ except Exception:
+ logger.exception('something bad happened when fetching %r', url)
+ return redirect(url_for('home'))
if not feed.get('link'):
feed['enabled'] = False
flash(gettext("Couldn't find a feed url, you'll need to find a Atom or"
@@ -149,18 +157,23 @@ def update(action, feed_id=None):
@etag_match
def form(feed_id=None):
action = gettext("Add a feed")
+ categories = CategoryController(g.user.id).read()
head_titles = [action]
if feed_id is None:
+ form = AddFeedForm()
+ form.set_category_choices(categories)
return render_template('edit_feed.html', action=action,
- head_titles=head_titles, form=AddFeedForm())
+ head_titles=head_titles, form=form)
feed = FeedController(g.user.id).get(id=feed_id)
+ form = AddFeedForm(obj=feed)
+ form.set_category_choices(categories)
action = gettext('Edit feed')
head_titles = [action]
if feed.title:
head_titles.append(feed.title)
return render_template('edit_feed.html', action=action,
- head_titles=head_titles,
- form=AddFeedForm(obj=feed), feed=feed)
+ head_titles=head_titles, categories=categories,
+ form=form, feed=feed)
@feed_bp.route('/create', methods=['POST'])
@@ -169,6 +182,7 @@ def form(feed_id=None):
def process_form(feed_id=None):
form = AddFeedForm()
feed_contr = FeedController(g.user.id)
+ form.set_category_choices(CategoryController(g.user.id).read())
if not form.validate():
return render_template('edit_feed.html', form=form)
@@ -179,7 +193,9 @@ def process_form(feed_id=None):
# Edit an existing feed
feed_attr = {'title': form.title.data, 'enabled': form.enabled.data,
'link': form.link.data, 'site_link': form.site_link.data,
- 'filters': []}
+ 'filters': [], 'category_id': form.category_id.data}
+ if not feed_attr['category_id']:
+ del feed_attr['category_id']
for filter_attr in ('type', 'pattern', 'action on', 'action'):
for i, value in enumerate(
@@ -195,7 +211,7 @@ def process_form(feed_id=None):
return redirect(url_for('feed.form', feed_id=feed_id))
# Create a new feed
- new_feed = FeedController(g.user.id).create(**feed_attr)
+ new_feed = feed_contr.create(**feed_attr)
flash(gettext('Feed %(feed_title)r successfully created.',
feed_title=new_feed.title), 'success')
@@ -205,3 +221,29 @@ def process_form(feed_id=None):
flash(gettext("Downloading articles for the new feed..."), 'info')
return redirect(url_for('feed.form', feed_id=new_feed.id))
+
+
+@feeds_bp.route('/inactives', methods=['GET'])
+@login_required
+def inactives():
+ """
+ List of inactive feeds.
+ """
+ nb_days = int(request.args.get('nb_days', 365))
+ inactives = FeedController(g.user.id).get_inactives(nb_days)
+ return render_template('inactives.html',
+ inactives=inactives, nb_days=nb_days)
+
+
+@feed_bp.route('/duplicates/<int:feed_id>', methods=['GET'])
+@login_required
+def duplicates(feed_id):
+ """
+ Return duplicates article for a feed.
+ """
+ feed, duplicates = FeedController(g.user.id).get_duplicates(feed_id)
+ if len(duplicates) == 0:
+ flash(gettext('No duplicates in the feed "{}".').format(feed.title),
+ 'info')
+ return redirect('home')
+ return render_template('duplicates.html', duplicates=duplicates, feed=feed)
diff --git a/src/web/views/icon.py b/src/web/views/icon.py
index 895b4740..64e54cab 100644
--- a/src/web/views/icon.py
+++ b/src/web/views/icon.py
@@ -5,6 +5,7 @@ from web.lib.view_utils import etag_match
icon_bp = Blueprint('icon', __name__, url_prefix='/icon')
+
@icon_bp.route('/', methods=['GET'])
@etag_match
def icon():
diff --git a/src/web/views/user.py b/src/web/views/user.py
new file mode 100644
index 00000000..754d3b9a
--- /dev/null
+++ b/src/web/views/user.py
@@ -0,0 +1,151 @@
+import string
+import random
+from flask import (Blueprint, g, render_template, redirect,
+ flash, url_for, request)
+from flask.ext.babel import gettext
+from flask.ext.login import login_required
+
+import conf
+from web import utils, notifications
+from web.controllers import (UserController, FeedController, ArticleController)
+
+from web.forms import ProfileForm, RecoverPasswordForm
+
+users_bp = Blueprint('users', __name__, url_prefix='/users')
+user_bp = Blueprint('user', __name__, url_prefix='/user')
+
+
+@user_bp.route('/management', methods=['GET', 'POST'])
+@login_required
+def management():
+ """
+ Display the management page.
+ """
+ if request.method == 'POST':
+ if None != request.files.get('opmlfile', None):
+ # Import an OPML file
+ data = request.files.get('opmlfile', None)
+ if not utils.allowed_file(data.filename):
+ flash(gettext('File not allowed.'), 'danger')
+ else:
+ try:
+ nb = utils.import_opml(g.user.email, data.read())
+ if conf.CRAWLING_METHOD == "classic":
+ utils.fetch(g.user.email, None)
+ flash(str(nb) + ' ' + gettext('feeds imported.'),
+ "success")
+ flash(gettext("Downloading articles..."), 'info')
+ except:
+ flash(gettext("Impossible to import the new feeds."),
+ "danger")
+ elif None != request.files.get('jsonfile', None):
+ # Import an account
+ data = request.files.get('jsonfile', None)
+ if not utils.allowed_file(data.filename):
+ flash(gettext('File not allowed.'), 'danger')
+ else:
+ try:
+ nb = utils.import_json(g.user.email, data.read())
+ flash(gettext('Account imported.'), "success")
+ except:
+ flash(gettext("Impossible to import the account."),
+ "danger")
+ else:
+ flash(gettext('File not allowed.'), 'danger')
+
+ nb_feeds = FeedController(g.user.id).read().count()
+ art_contr = ArticleController(g.user.id)
+ nb_articles = art_contr.read().count()
+ nb_unread_articles = art_contr.read(readed=False).count()
+ return render_template('management.html', user=g.user,
+ nb_feeds=nb_feeds, nb_articles=nb_articles,
+ nb_unread_articles=nb_unread_articles)
+
+
+@user_bp.route('/profile', methods=['GET', 'POST'])
+@login_required
+def profile():
+ """
+ Edit the profile of the currently logged user.
+ """
+ user_contr = UserController(g.user.id)
+ user = user_contr.get(id=g.user.id)
+ form = ProfileForm()
+
+ if request.method == 'POST':
+ if form.validate():
+ user_contr.update({'id': g.user.id},
+ {'nickname': form.nickname.data,
+ 'email': form.email.data,
+ 'password': form.password.data,
+ 'refresh_rate': form.refresh_rate.data})
+
+ flash(gettext('User %(nick)s successfully updated',
+ nick=user.nickname), 'success')
+ return redirect(url_for('user.profile'))
+ else:
+ return render_template('profile.html', user=user, form=form)
+
+ if request.method == 'GET':
+ form = ProfileForm(obj=user)
+ return render_template('profile.html', user=user, form=form)
+
+
+@user_bp.route('/delete_account', methods=['GET'])
+@login_required
+def delete_account():
+ """
+ Delete the account of the user (with all its data).
+ """
+ UserController(g.user.id).delete(g.user.id)
+ flash(gettext('Your account has been deleted.'), 'success')
+ return redirect(url_for('login'))
+
+
+@user_bp.route('/confirm_account/<string:activation_key>', methods=['GET'])
+def confirm_account(activation_key=None):
+ """
+ Confirm the account of a user.
+ """
+ user_contr = UserController()
+ if activation_key != "":
+ user = user_contr.read(activation_key=activation_key).first()
+ if user is not None:
+ user_contr.update({'id': user.id}, {'activation_key': ''})
+ flash(gettext('Your account has been confirmed.'), 'success')
+ else:
+ flash(gettext('Impossible to confirm this account.'), 'danger')
+ return redirect(url_for('login'))
+
+
+@user_bp.route('/recover', methods=['GET', 'POST'])
+def recover():
+ """
+ Enables the user to recover its account when he has forgotten
+ its password.
+ """
+ form = RecoverPasswordForm()
+ user_contr = UserController()
+
+ if request.method == 'POST':
+ if form.validate():
+ user = user_contr.get(email=form.email.data)
+ characters = string.ascii_letters + string.digits
+ password = "".join(random.choice(characters)
+ for x in range(random.randint(8, 16)))
+ user.set_password(password)
+ user_contr.update({'id': user.id}, {'password': password})
+
+ # Send the confirmation email
+ try:
+ notifications.new_password_notification(user, password)
+ flash(gettext('New password sent to your address.'), 'success')
+ except Exception as error:
+ flash(gettext('Problem while sending your new password: '
+ '%(error)s', error=error), 'danger')
+
+ return redirect(url_for('login'))
+ return render_template('recover.html', form=form)
+
+ if request.method == 'GET':
+ return render_template('recover.html', form=form)
diff --git a/src/web/views/views.py b/src/web/views/views.py
index ed4ae8a7..223f96e9 100644
--- a/src/web/views/views.py
+++ b/src/web/views/views.py
@@ -27,37 +27,30 @@ __copyright__ = "Copyright (c) Cedric Bonhomme"
__license__ = "AGPLv3"
import os
-import string
-import random
-import hashlib
import logging
import datetime
-from collections import OrderedDict
from bootstrap import application as app, db
from flask import render_template, request, flash, session, \
url_for, redirect, g, current_app, make_response
from flask.ext.login import LoginManager, login_user, logout_user, \
- login_required, current_user, AnonymousUserMixin, \
- login_url
+ 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 or_, and_
from sqlalchemy.exc import IntegrityError
from werkzeug import generate_password_hash
import conf
+from web.lib.utils import redirect_url
from web import utils, notifications, export
from web.lib.view_utils import etag_match
-from web.models import User, Feed, Article, Role
-from web.decorators import feed_access_required
-from web.forms import SignupForm, SigninForm, InformationMessageForm,\
- ProfileForm, UserForm, RecoverPasswordForm \
+from web.models import User, Article, Role
+from web.forms import SignupForm, SigninForm
from web.controllers import UserController, FeedController, \
- ArticleController
+ ArticleController, CategoryController
Principal(app)
@@ -72,6 +65,7 @@ login_manager.login_view = 'login'
logger = logging.getLogger(__name__)
+
#
# Management of the user's session.
#
@@ -90,6 +84,7 @@ def on_identity_loaded(sender, identity):
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
+
@app.before_request
def before_request():
g.user = current_user
@@ -98,11 +93,13 @@ def before_request():
db.session.add(g.user)
db.session.commit()
+
@login_manager.user_loader
def load_user(id):
# Return an instance of the User model
return UserController().get(id=id)
+
#
# Custom error pages.
#
@@ -111,25 +108,23 @@ def authentication_required(e):
flash(gettext('Authentication required.'), 'info')
return redirect(url_for('login'))
+
@app.errorhandler(403)
def authentication_failed(e):
flash(gettext('Forbidden.'), 'danger')
return redirect(url_for('login'))
+
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
+
@app.errorhandler(500)
def internal_server_error(e):
return render_template('errors/500.html'), 500
-def redirect_url(default='home'):
- return request.args.get('next') or \
- request.referrer or \
- url_for(default)
-
@g.babel.localeselector
def get_locale():
"""
@@ -138,6 +133,7 @@ def get_locale():
"""
return request.accept_languages.best_match(conf.LANGUAGES.keys())
+
@g.babel.timezoneselector
def get_timezone():
try:
@@ -145,6 +141,7 @@ def get_timezone():
except:
return conf.TIME_ZONE["en"]
+
#
# Views.
#
@@ -167,6 +164,7 @@ def login():
return form.redirect('home')
return render_template('login.html', form=form)
+
@app.route('/logout')
@login_required
def logout():
@@ -189,6 +187,7 @@ def logout():
flash(gettext("Logged out successfully."), 'success')
return redirect(url_for('login'))
+
@app.route('/signup', methods=['GET', 'POST'])
def signup():
"""
@@ -230,114 +229,118 @@ def signup():
return render_template('signup.html', form=form)
-@etag_match
-def render_home(filters=None, head_titles=None,
- page_to_render='home', **kwargs):
- if filters is None:
- filters = {}
- if head_titles is None:
- head_titles = []
- feed_contr = FeedController(g.user.id)
- arti_contr = ArticleController(g.user.id)
- feeds = {feed.id: feed.title for feed in feed_contr.read()}
-
- in_error = {feed.id: feed.error_count for feed in
- feed_contr.read(error_count__gt=2)}
-
- filter_ = request.args.get('filter_',
- 'unread' if page_to_render == 'home' else 'all')
- sort_ = request.args.get('sort_', 'date')
- feed_id = int(request.args.get('feed_id', 0))
- limit = request.args.get('limit', 1000)
-
- if filter_ != 'all':
- filters['readed'] = filter_ == 'read'
- if feed_id:
- filters['feed_id'] = feed_id
- head_titles.append(feed_contr.get(id=feed_id).title)
-
- sort_param = {"feed": Feed.title.desc(),
- "date": Article.date.desc(),
- "article": Article.title.desc(),
- "-feed": Feed.title.asc(),
- "-date": Article.date.asc(),
- "-article": Article.title.asc()
- }.get(sort_, Article.date.desc())
-
- articles = arti_contr.read(**filters).join(Article.source). \
- order_by(sort_param)
- if limit != 'all':
- limit = int(limit)
- articles = articles.limit(limit)
-
- def gen_url(filter_=filter_, sort_=sort_, limit=limit, feed_id=feed_id,
- **kwargs):
- o_kwargs = OrderedDict()
- for key in sorted(kwargs):
- o_kwargs[key] = kwargs[key]
- if page_to_render == 'search':
- o_kwargs['query'] = request.args.get('query', '')
- o_kwargs['search_title'] = request.args.get('search_title', 'off')
- o_kwargs['search_content'] = request.args.get(
- 'search_content', 'off')
- # if nor title and content are selected, selecting title
- if o_kwargs['search_title'] == o_kwargs['search_content'] == 'off':
- o_kwargs['search_title'] = 'on'
- o_kwargs['filter_'] = filter_
- o_kwargs['sort_'] = sort_
- o_kwargs['limit'] = limit
- o_kwargs['feed_id'] = feed_id
- return url_for(page_to_render, **o_kwargs)
-
- articles = list(articles)
- if (page_to_render == 'home' and feed_id or page_to_render == 'search') \
- and filter_ != 'all' and not articles:
- return redirect(gen_url(filter_='all'))
-
- return render_template('home.html', gen_url=gen_url,
- feed_id=feed_id, page_to_render=page_to_render,
- filter_=filter_, limit=limit, feeds=feeds,
- unread=arti_contr.count_by_feed(readed=False),
- articles=articles, in_error=in_error,
- head_titles=head_titles, sort_=sort_, **kwargs)
+from calendar import timegm
+from flask import jsonify
@app.route('/')
@login_required
+@etag_match
def home():
- "Home page for connected users. Displays by default unread articles."
- return render_home()
-
-
-@app.route('/favorites')
-@login_required
-def favorites():
- return render_home({'like': True}, [gettext('Favorites')], 'favorites')
+ return render_template('home.html')
-@app.route('/search', methods=['GET'])
+@app.route('/menu')
@login_required
-def search():
- "Search articles corresponding to the query."
- if 'query' not in request.args:
- flash(gettext("No text to search were provided."), "warning")
- return render_home()
- query = request.args['query']
+def get_menu():
+ categories_order = [0]
+ categories = {0: {'name': 'No category', 'id': 0}}
+ for cat in CategoryController(g.user.id).read().order_by('name'):
+ categories_order.append(cat.id)
+ categories[cat.id] = cat.dump()
+ unread = ArticleController(g.user.id).count_by_feed(readed=False)
+ for cat_id in categories:
+ categories[cat_id]['unread'] = 0
+ categories[cat_id]['feeds'] = []
+ feeds = {feed.id: feed.dump() for feed in FeedController(g.user.id).read()}
+ for feed_id, feed in feeds.items():
+ feed['created_stamp'] = timegm(feed['created_date'].timetuple()) * 1000
+ feed['last_stamp'] = timegm(feed['last_retrieved'].timetuple()) * 1000
+ feed['category_id'] = feed['category_id'] or 0
+ feed['unread'] = unread.get(feed['id'], 0)
+ if not feed['filters']:
+ feed['filters'] = []
+ if feed.get('icon_url'):
+ feed['icon_url'] = url_for('icon.icon', url=feed['icon_url'])
+ categories[feed['category_id']]['unread'] += feed['unread']
+ categories[feed['category_id']]['feeds'].append(feed_id)
+ return jsonify(**{'feeds': feeds, 'categories': categories,
+ 'categories_order': categories_order,
+ 'crawling_method': conf.CRAWLING_METHOD,
+ 'max_error': conf.DEFAULT_MAX_ERROR,
+ 'error_threshold': conf.ERROR_THRESHOLD,
+ 'is_admin': g.user.is_admin(),
+ 'all_unread_count': sum(unread.values())})
+
+
+def _get_filters(in_dict):
filters = {}
- search_title = request.args.get('search_title', 'off')
- search_content = request.args.get('search_content', 'off')
- if search_title == 'on':
- filters['title__ilike'] = "%%%s%%" % query
- if search_content == 'on':
- filters['content__ilike'] = "%%%s%%" % query
- if len(filters) == 0:
- search_title = 'on'
- filters['title__ilike'] = "%%%s%%" % query
- if len(filters) > 1:
- filters = {"__or__": filters}
- return render_home(filters, ["%s %s" % (gettext('Search:'), query)],
- 'search', search_query=query, search_title=search_title,
- search_content=search_content)
+ query = in_dict.get('query')
+ if query:
+ search_title = in_dict.get('search_title') == 'true'
+ search_content = in_dict.get('search_content') == 'true'
+ if search_title:
+ filters['title__ilike'] = "%%%s%%" % query
+ if search_content:
+ filters['content__ilike'] = "%%%s%%" % query
+ if len(filters) == 0:
+ filters['title__ilike'] = "%%%s%%" % query
+ if len(filters) > 1:
+ filters = {"__or__": filters}
+ if in_dict.get('filter') == 'unread':
+ filters['readed'] = False
+ elif in_dict.get('filter') == 'liked':
+ filters['like'] = True
+ filter_type = in_dict.get('filter_type')
+ if filter_type in {'feed_id', 'category_id'} and in_dict.get('filter_id'):
+ filters[filter_type] = int(in_dict['filter_id']) or None
+ return filters
+
+
+def _articles_to_json(articles, fd_hash=None):
+ return jsonify(**{'articles': [{'title': art.title, 'liked': art.like,
+ 'read': art.readed, 'article_id': art.id, 'selected': False,
+ 'feed_id': art.feed_id, 'category_id': art.category_id or 0,
+ 'feed_title': fd_hash[art.feed_id]['title'] if fd_hash else None,
+ 'icon_url': fd_hash[art.feed_id]['icon_url'] if fd_hash else None,
+ 'date': art.date, 'timestamp': timegm(art.date.timetuple()) * 1000}
+ for art in articles.limit(1000)]})
+
+
+@app.route('/middle_panel')
+@login_required
+def get_middle_panel():
+ filters = _get_filters(request.args)
+ art_contr = ArticleController(g.user.id)
+ fd_hash = {feed.id: {'title': feed.title,
+ 'icon_url': url_for('icon.icon', url=feed.icon_url)
+ if feed.icon_url else None}
+ for feed in FeedController(g.user.id).read()}
+ articles = art_contr.read(**filters).order_by(Article.date.desc())
+ return _articles_to_json(articles, fd_hash)
+
+
+@app.route('/getart/<int:article_id>')
+@login_required
+def get_article(article_id):
+ contr = ArticleController(g.user.id)
+ article = contr.get(id=article_id).dump()
+ if not article['readed']:
+ contr.update({'id': article_id}, {'readed': True})
+ article['category_id'] = article['category_id'] or 0
+ feed = FeedController(g.user.id).get(id=article['feed_id'])
+ article['icon_url'] = url_for('icon.icon', url=feed.icon_url) \
+ if feed.icon_url else None
+ return jsonify(**article)
+
+
+@app.route('/mark_all_as_read', methods=['PUT'])
+@login_required
+def mark_all_as_read():
+ filters, acontr = _get_filters(request.json), ArticleController(g.user.id)
+ articles = _articles_to_json(acontr.read(**filters))
+ acontr.update(filters, {'readed': True})
+ return articles
@app.route('/fetch', methods=['GET'])
@@ -367,97 +370,6 @@ def about():
return render_template('about.html')
-@app.route('/mark_as/<string:new_value>', methods=['GET'])
-@app.route('/mark_as/<string:new_value>/article/<int:article_id>', methods=['GET'])
-@login_required
-@feed_access_required
-def mark_as(new_value='read', feed_id=None, article_id=None):
- """
- Mark all unreaded articles as read.
- """
- readed = new_value == 'read'
- articles = Article.query.filter(Article.user_id == g.user.id)
- if feed_id is not None:
- articles = articles.filter(Article.feed_id == feed_id)
- message = 'Feed marked as %s.'
- elif article_id is not None:
- articles = articles.filter(Article.id == article_id)
- message = 'Article marked as %s.'
- else:
- message = 'All article marked as %s.'
- articles.filter(Article.readed == (not readed)).update({"readed": readed})
- flash(gettext(message % new_value), 'info')
- db.session.commit()
- if readed:
- return redirect(redirect_url())
- return redirect(url_for('home'))
-
-@app.route('/like/<int:article_id>', methods=['GET'])
-@login_required
-def like(article_id=None):
- """
- Mark or unmark an article as favorites.
- """
- Article.query.filter(Article.user_id == g.user.id, Article.id == article_id). \
- update({
- "like": not Article.query.filter(Article.id == article_id).first().like
- })
- db.session.commit()
- return redirect(redirect_url())
-
-@app.route('/delete/<int:article_id>', methods=['GET'])
-@login_required
-def delete(article_id=None):
- """
- Delete an article from the database.
- """
- article = Article.query.filter(Article.id == article_id).first()
- if article is not None and article.source.subscriber.id == g.user.id:
- db.session.delete(article)
- db.session.commit()
- flash(gettext('Article') + ' ' + article.title + ' ' + gettext('deleted.'), 'success')
- return redirect(redirect_url())
- else:
- flash(gettext('This article do not exist.'), 'danger')
- return redirect(url_for('home'))
-
-
-@app.route('/inactives', methods=['GET'])
-@login_required
-def inactives():
- """
- List of inactive feeds.
- """
- nb_days = int(request.args.get('nb_days', 365))
- user = UserController(g.user.id).get(email=g.user.email)
- today = datetime.datetime.now()
- inactives = []
- for feed in user.feeds:
- try:
- last_post = feed.articles[0].date
- except IndexError:
- continue
- elapsed = today - last_post
- if elapsed > datetime.timedelta(days=nb_days):
- inactives.append((feed, elapsed))
- inactives.sort(key=lambda tup: tup[1], reverse=True)
- return render_template('inactives.html', inactives=inactives, nb_days=nb_days)
-
-@app.route('/duplicates/<int:feed_id>', methods=['GET'])
-@login_required
-def duplicates(feed_id=None):
- """
- Return duplicates article for a feed.
- """
- feed = Feed.query.filter(Feed.user_id == g.user.id, Feed.id == feed_id).first()
- duplicates = []
- duplicates = utils.compare_documents(feed)
- if len(duplicates) == 0:
- flash(gettext('No duplicates in the feed "{}".').format(feed.title),
- 'info')
- return redirect(redirect_url())
- return render_template('duplicates.html', duplicates=duplicates, feed=feed)
-
@app.route('/export', methods=['GET'])
@login_required
def export_articles():
@@ -485,12 +397,14 @@ def export_articles():
return redirect(redirect_url())
response = make_response(json_result)
response.mimetype = 'application/json'
- response.headers["Content-Disposition"] = 'attachment; filename=account.json'
+ response.headers["Content-Disposition"] \
+ = 'attachment; filename=account.json'
else:
flash(gettext('Export format not supported.'), 'warning')
return redirect(redirect_url())
return response
+
@app.route('/export_opml', methods=['GET'])
@login_required
def export_opml():
@@ -503,296 +417,3 @@ def export_opml():
response.headers['Content-Type'] = 'application/xml'
response.headers['Content-Disposition'] = 'attachment; filename=feeds.opml'
return response
-
-
-@app.route('/management', methods=['GET', 'POST'])
-@login_required
-def management():
- """
- Display the management page.
- """
- if request.method == 'POST':
- if None != request.files.get('opmlfile', None):
- # Import an OPML file
- data = request.files.get('opmlfile', None)
- if not utils.allowed_file(data.filename):
- flash(gettext('File not allowed.'), 'danger')
- else:
- try:
- nb = utils.import_opml(g.user.email, data.read())
- if conf.CRAWLING_METHOD == "classic":
- utils.fetch(g.user.email, None)
- flash(str(nb) + ' ' + gettext('feeds imported.'),
- "success")
- flash(gettext("Downloading articles..."), 'info')
- except:
- flash(gettext("Impossible to import the new feeds."),
- "danger")
- elif None != request.files.get('jsonfile', None):
- # Import an account
- data = request.files.get('jsonfile', None)
- if not utils.allowed_file(data.filename):
- flash(gettext('File not allowed.'), 'danger')
- else:
- try:
- nb = utils.import_json(g.user.email, data.read())
- flash(gettext('Account imported.'), "success")
- except:
- flash(gettext("Impossible to import the account."),
- "danger")
- else:
- flash(gettext('File not allowed.'), 'danger')
-
- nb_feeds = len(g.user.feeds.all())
- articles = Article.query.filter(Article.user_id == g.user.id)
- nb_articles = articles.count()
- nb_unread_articles = articles.filter(Article.readed == False).count()
- return render_template('management.html', user=g.user,
- nb_feeds=nb_feeds, nb_articles=nb_articles,
- nb_unread_articles=nb_unread_articles)
-
-@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(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('/profile', methods=['GET', 'POST'])
-@login_required
-def profile():
- """
- Edit the profile of the currently logged user.
- """
- user = UserController(g.user.id).get(id=g.user.id)
- form = ProfileForm()
-
- if request.method == 'POST':
- if form.validate():
- form.populate_obj(user)
- if form.password.data != "":
- user.set_password(form.password.data)
- db.session.commit()
- flash("%s %s %s" % (gettext('User'), user.nickname,
- gettext('successfully updated.')),
- 'success')
- return redirect(url_for('profile'))
- else:
- return render_template('profile.html', user=user, form=form)
-
- if request.method == 'GET':
- form = ProfileForm(obj=user)
- return render_template('profile.html', user=user, form=form)
-
-@app.route('/delete_account', methods=['GET'])
-@login_required
-def delete_account():
- """
- Delete the account of the user (with all its data).
- """
- user = UserController(g.user.id).get(id=g.user.id)
- if user is not None:
- db.session.delete(user)
- db.session.commit()
- flash(gettext('Your account has been deleted.'), 'success')
- else:
- flash(gettext('This user does not exist.'), 'danger')
- return redirect(url_for('login'))
-
-@app.route('/expire_articles', methods=['GET'])
-@login_required
-def expire_articles():
- """
- Delete articles older than the given number of weeks.
- """
- weeks_ago = datetime.datetime.utcnow() - \
- datetime.timedelta(weeks=int(request.args.get('weeks', 10)))
- Article.query.filter(
- and_(Article.user_id == g.user.id,
- or_(Article.date < weeks_ago,
- Article.retrieved_date < weeks_ago))).delete()
- flash(gettext('Articles deleted.'), 'info')
- db.session.commit()
- return redirect(redirect_url())
-
-@app.route('/confirm_account/<string:activation_key>', methods=['GET'])
-def confirm_account(activation_key=None):
- """
- Confirm the account of a user.
- """
- if activation_key != "":
- user = User.query.filter(User.activation_key == activation_key).first()
- if user is not None:
- user.activation_key = ""
- db.session.commit()
- flash(gettext('Your account has been confirmed.'), 'success')
- else:
- flash(gettext('Impossible to confirm this account.'), 'danger')
- return redirect(url_for('login'))
-
-@app.route('/recover', methods=['GET', 'POST'])
-def recover():
- """
- Enables the user to recover its account when he has forgotten
- its password.
- """
- form = RecoverPasswordForm()
-
- if request.method == 'POST':
- if form.validate():
- user = User.query.filter(User.email == form.email.data).first()
- characters = string.ascii_letters + string.digits
- password = "".join(random.choice(characters) for x in range(random.randint(8, 16)))
- user.set_password(password)
- db.session.commit()
-
- # Send the confirmation email
- try:
- notifications.new_password_notification(user, password)
- flash(gettext('New password sent to your address.'), 'success')
- except Exception as e:
- flash(gettext('Problem while sending your new password.') + ': ' + str(e), 'danger')
-
- return redirect(url_for('login'))
- return render_template('recover.html', form=form)
-
- if request.method == 'GET':
- return render_template('recover.html', form=form)
-
-#
-# Views dedicated to administration tasks.
-#
-@app.route('/admin/dashboard', methods=['GET', 'POST'])
-@login_required
-@admin_permission.require(http_exception=403)
-def dashboard():
- """
- Adminstrator's dashboard.
- """
- form = InformationMessageForm()
-
- if request.method == 'POST':
- if form.validate():
- try:
- notifications.information_message(form.subject.data, form.message.data)
- except Exception as e:
- flash(gettext('Problem while sending email') + ': ' + str(e), 'danger')
-
- users = User.query.all()
- return render_template('admin/dashboard.html', users=users, current_user=g.user, form=form)
-
-@app.route('/admin/create_user', methods=['GET', 'POST'])
-@app.route('/admin/edit_user/<int:user_id>', methods=['GET', 'POST'])
-@login_required
-@admin_permission.require(http_exception=403)
-def create_user(user_id=None):
- """
- Create or edit a user.
- """
- form = UserForm()
-
- if request.method == 'POST':
- if form.validate():
- role_user = Role.query.filter(Role.name == "user").first()
- if user_id is not None:
- # Edit a user
- user = User.query.filter(User.id == user_id).first()
- form.populate_obj(user)
- if form.password.data != "":
- user.set_password(form.password.data)
- db.session.commit()
- flash(gettext('User') + ' ' + user.nickname + ' ' + gettext('successfully updated.'), 'success')
- else:
- # Create a new user
- user = User(nickname=form.nickname.data,
- email=form.email.data,
- pwdhash=generate_password_hash(form.password.data))
- user.roles.extend([role_user])
- user.activation_key = ""
- db.session.add(user)
- db.session.commit()
- flash("%s %s %s" % (gettext('User'), user.nickname,
- gettext('successfully created.')),
- 'success')
- return redirect(url_for('create_user', user_id=user.id))
- else:
- return redirect(url_for('create_user'))
-
- if request.method == 'GET':
- if user_id is not None:
- user = User.query.filter(User.id == user_id).first()
- form = UserForm(obj=user)
- message = "%s <i>%s</i>" % (gettext('Edit the user'),
- user.nickname)
- else:
- form = UserForm()
- message = gettext('Add a new user')
- return render_template('/admin/create_user.html',
- form=form, message=message)
-
-@app.route('/admin/user/<int:user_id>', methods=['GET'])
-@login_required
-@admin_permission.require(http_exception=403)
-def user(user_id=None):
- """
- See information about a user (stations, etc.).
- """
- user = UserController().get(id=user_id)
- if user is not None:
- article_contr = ArticleController(user_id)
- return render_template('/admin/user.html', user=user, feeds=user.feeds,
- article_count=article_contr.count_by_feed(),
- unread_article_count=article_contr.count_by_feed(readed=False))
-
- else:
- flash(gettext('This user does not exist.'), 'danger')
- return redirect(redirect_url())
-
-@app.route('/admin/delete_user/<int:user_id>', methods=['GET'])
-@login_required
-@admin_permission.require(http_exception=403)
-def delete_user(user_id=None):
- """
- Delete a user (with all its data).
- """
- user = User.query.filter(User.id == user_id).first()
- if user is not None:
- db.session.delete(user)
- db.session.commit()
- flash(gettext('User') + ' ' + user.nickname + ' ' + gettext('successfully deleted.'), 'success')
- else:
- flash(gettext('This user does not exist.'), 'danger')
- return redirect(redirect_url())
-
-@app.route('/admin/enable_user/<int:user_id>', methods=['GET'])
-@app.route('/admin/disable_user/<int:user_id>', methods=['GET'])
-@login_required
-@admin_permission.require()
-def disable_user(user_id=None):
- """
- Enable or disable the account of a user.
- """
- user = User.query.filter(User.id == user_id).first()
- if user is not None:
- if user.activation_key != "":
-
- # Send the confirmation email
- try:
- notifications.new_account_activation(user)
- user.activation_key = ""
- flash(gettext('Account of the user') + ' ' + user.nickname + ' ' + gettext('successfully activated.'), 'success')
- except Exception as e:
- flash(gettext('Problem while sending activation email') + ': ' + str(e), 'danger')
-
- else:
- user.activation_key = hashlib.sha512(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[:86]
- flash(gettext('Account of the user') + ' ' + user.nickname + ' ' + gettext('successfully disabled.'), 'success')
- db.session.commit()
- else:
- flash(gettext('This user does not exist.'), 'danger')
- return redirect(redirect_url())
bgstack15