aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/conf.py2
-rw-r--r--src/web/controllers/article.py5
-rw-r--r--src/web/export.py2
-rw-r--r--src/web/js/actions/MenuActions.js5
-rw-r--r--src/web/js/actions/MiddlePanelActions.js6
-rw-r--r--src/web/js/components/MainApp.react.js2
-rw-r--r--src/web/js/components/Menu.react.js25
-rw-r--r--src/web/js/components/MiddlePanel.react.js19
-rw-r--r--src/web/js/constants/JarrConstants.js22
-rw-r--r--src/web/js/stores/MenuStore.js25
-rw-r--r--src/web/js/stores/MiddlePanelStore.js46
-rw-r--r--src/web/templates/management.html6
-rw-r--r--src/web/views/article.py50
-rw-r--r--src/web/views/home.py3
14 files changed, 145 insertions, 73 deletions
diff --git a/src/conf.py b/src/conf.py
index 9e7f1d13..f30b5701 100644
--- a/src/conf.py
+++ b/src/conf.py
@@ -47,7 +47,7 @@ DEFAULTS = {"platform_url": "https://jarr.herokuapp.com/",
"host": "0.0.0.0",
"port": "5000",
"crawling_method": "classic",
- "webzine_root": "/tmp",
+ "webzine_root": "~/tmp",
}
if not ON_HEROKU:
diff --git a/src/web/controllers/article.py b/src/web/controllers/article.py
index 8c6952cb..37a35023 100644
--- a/src/web/controllers/article.py
+++ b/src/web/controllers/article.py
@@ -96,3 +96,8 @@ class ArticleController(AbstractController):
else:
articles_counter[article.date.year] += 1
return articles_counter, articles
+
+ def read_light(self, **filters):
+ return super().read(**filters).with_entities(Article.id, Article.title,
+ Article.readed, Article.like, Article.feed_id, Article.date,
+ Article.category_id).order_by(Article.date.desc())
diff --git a/src/web/export.py b/src/web/export.py
index 220f8a42..41ee839c 100644
--- a/src/web/export.py
+++ b/src/web/export.py
@@ -139,7 +139,7 @@ def export_html(user):
"""
Export all articles of 'user' in Web pages.
"""
- webzine_root = conf.WEBZINE_ROOT + "webzine/"
+ webzine_root = conf.WEBZINE_ROOT + "/webzine/"
nb_articles = format(len(models.Article.query.filter(models.Article.user_id == user.id).all()), ",d")
index = HTML_HEADER("News archive")
index += "<h1>List of feeds</h1>\n"
diff --git a/src/web/js/actions/MenuActions.js b/src/web/js/actions/MenuActions.js
index b9154581..824610d8 100644
--- a/src/web/js/actions/MenuActions.js
+++ b/src/web/js/actions/MenuActions.js
@@ -5,7 +5,7 @@ var jquery = require('jquery');
var MenuActions = {
// PARENT FILTERS
- reload: function() {
+ reload: function(setFilterFunc, id) {
jquery.getJSON('/menu', function(payload) {
JarrDispatcher.dispatch({
type: ActionTypes.RELOAD_MENU,
@@ -18,6 +18,9 @@ var MenuActions = {
crawling_method: payload.crawling_method,
all_unread_count: payload.all_unread_count,
});
+ if(setFilterFunc && id) {
+ setFilterFunc(id);
+ }
});
},
setFilter: function(filter) {
diff --git a/src/web/js/actions/MiddlePanelActions.js b/src/web/js/actions/MiddlePanelActions.js
index f805b7b1..3704e7ec 100644
--- a/src/web/js/actions/MiddlePanelActions.js
+++ b/src/web/js/actions/MiddlePanelActions.js
@@ -140,11 +140,9 @@ var MiddlePanelActions = {
data: JSON.stringify(filters),
url: "/mark_all_as_read",
success: function (payload) {
+ console.log(payload);
JarrDispatcher.dispatch({
- type: ActionTypes.CHANGE_ATTR,
- attribute: 'read',
- value_num: -1,
- value_bool: true,
+ type: ActionTypes.MARK_ALL_AS_READ,
articles: payload.articles,
});
},
diff --git a/src/web/js/components/MainApp.react.js b/src/web/js/components/MainApp.react.js
index cbdc5833..ffb14589 100644
--- a/src/web/js/components/MainApp.react.js
+++ b/src/web/js/components/MainApp.react.js
@@ -15,7 +15,7 @@ var MainApp = React.createClass({
<Grid fluid id="jarr-container">
<Menu />
<Col id="middle-panel" mdOffset={3} lgOffset={2}
- xs={4} sm={4} md={4} lg={4}>
+ xs={12} sm={4} md={4} lg={4}>
<MiddlePanel.MiddlePanelFilter />
<MiddlePanel.MiddlePanel />
</Col>
diff --git a/src/web/js/components/Menu.react.js b/src/web/js/components/Menu.react.js
index 60578f8a..4537ee81 100644
--- a/src/web/js/components/Menu.react.js
+++ b/src/web/js/components/Menu.react.js
@@ -84,13 +84,15 @@ var CategoryGroup = React.createClass({
name: React.PropTypes.string.isRequired,
feeds: React.PropTypes.array.isRequired,
unread: React.PropTypes.number.isRequired,
- folded: React.PropTypes.bool.isRequired,
+ folded: React.PropTypes.bool,
},
getInitialState: function() {
- return {folded: this.props.folded};
+ return {folded: false};
},
componentWillReceiveProps: function(nextProps) {
- this.setState({folded: nextProps.folded});
+ if(nextProps.folded != null) {
+ this.setState({folded: nextProps.folded});
+ }
},
render: function() {
// hidden the no category if empty
@@ -265,7 +267,22 @@ var Menu = React.createClass({
);
},
componentDidMount: function() {
- MenuActions.reload();
+ var setFilterFunc = null;
+ var id = null;
+ if(window.location.search.substring(1)) {
+ var args = window.location.search.substring(1).split('&');
+ args.map(function(arg) {
+ if (arg.split('=')[0] == 'at' && arg.split('=')[1] == 'c') {
+ setFilterFunc = MiddlePanelActions.setCategoryFilter;
+ } else if (arg.split('=')[0] == 'at' && arg.split('=')[1] == 'f') {
+ setFilterFunc = MiddlePanelActions.setFeedFilter;
+
+ } else if (arg.split('=')[0] == 'ai') {
+ id = parseInt(arg.split('=')[1]);
+ }
+ });
+ }
+ MenuActions.reload(setFilterFunc, id);
MenuStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
diff --git a/src/web/js/components/MiddlePanel.react.js b/src/web/js/components/MiddlePanel.react.js
index dad33acc..f6e44777 100644
--- a/src/web/js/components/MiddlePanel.react.js
+++ b/src/web/js/components/MiddlePanel.react.js
@@ -35,7 +35,8 @@ var TableLine = React.createClass({
icon = <Glyphicon glyph="ban-circle" />;
}
var title = (<a href={'/article/redirect/' + this.props.article_id}
- onClick={this.openRedirectLink}>
+ onClick={this.openRedirectLink} target="_blank"
+ title={this.props.feed_title}>
{icon} {this.props.feed_title}
</a>);
var read = (<Glyphicon glyph={this.state.read?"check":"unchecked"}
@@ -43,21 +44,17 @@ var TableLine = React.createClass({
var liked = (<Glyphicon glyph={this.state.liked?"star":"star-empty"}
onClick={this.toogleLike} />);
icon = <Glyphicon glyph={"new-window"} />;
- var newTab = (<a href={'/article/redirect/' + this.props.article_id}
- onClick={this.openRedirectLink} target="_blank">
- {icon}
- </a>);
var clsses = "list-group-item";
if(this.props.selected) {
clsses += " active";
}
// FIXME https://github.com/yahoo/react-intl/issues/189
// use FormattedRelative when fixed, will have to upgrade to ReactIntlv2
- return (<div className={clsses} onClick={this.loadArticle}>
+ return (<div className={clsses} onClick={this.loadArticle} title={this.props.title}>
<h5><strong>{title}</strong></h5>
<JarrTime text={this.props.date}
stamp={this.props.timestamp} />
- <div>{read} {liked} {newTab} {this.props.title}</div>
+ <div>{read} {liked} {this.props.title}</div>
</div>
);
},
@@ -81,7 +78,7 @@ var TableLine = React.createClass({
evnt.stopPropagation();
},
loadArticle: function() {
- this.setState({active: true, read: true}, function() {
+ this.setState({selected: true, read: true}, function() {
RightPanelActions.loadArticle(
this.props.article_id, this.props.read);
}.bind(this));
@@ -232,7 +229,11 @@ var MiddlePanel = React.createClass({
return (<Row className="show-grid">
<div className="list-group">
{this.state.articles.map(function(article){
- return (<TableLine key={"a" + article.article_id}
+ var key = "a" + article.article_id;
+ if(article.read) {key+="r";}
+ if(article.liked) {key+="l";}
+ if(article.selected) {key+="s";}
+ return (<TableLine key={key}
title={article.title}
icon_url={article.icon_url}
read={article.read}
diff --git a/src/web/js/constants/JarrConstants.js b/src/web/js/constants/JarrConstants.js
index 0ea42aad..78e8bf04 100644
--- a/src/web/js/constants/JarrConstants.js
+++ b/src/web/js/constants/JarrConstants.js
@@ -1,23 +1,13 @@
-/*
- * Copyright (c) 2014-2015, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
- * TodoConstants
- */
-
var keyMirror = require('keymirror');
module.exports = keyMirror({
TOGGLE_MENU_FOLD: null,
RELOAD_MENU: null,
- PARENT_FILTER: null,
- MENU_FILTER: null,
- CHANGE_ATTR: null,
+ PARENT_FILTER: null, // set a feed or a category as filter in menu
+ MENU_FILTER: null, // change displayed feed in the menu
+ CHANGE_ATTR: null, // edit an attr on an article (like / read)
RELOAD_MIDDLE_PANEL: null,
- MIDDLE_PANEL_FILTER: null,
- LOAD_ARTICLE: null,
+ MIDDLE_PANEL_FILTER: null, // set a filter (read/like/all)
+ LOAD_ARTICLE: null, // load a single article in right panel
+ MARK_ALL_AS_READ: null,
});
diff --git a/src/web/js/stores/MenuStore.js b/src/web/js/stores/MenuStore.js
index 49c61bc1..1cbbbda7 100644
--- a/src/web/js/stores/MenuStore.js
+++ b/src/web/js/stores/MenuStore.js
@@ -17,6 +17,7 @@ var MenuStore = assign({}, EventEmitter.prototype, {
setFilter: function(value) {
if(this._datas.filter != value) {
this._datas.filter = value;
+ this._datas.all_folded = null;
this.emitChange();
}
},
@@ -24,12 +25,10 @@ var MenuStore = assign({}, EventEmitter.prototype, {
if(this._datas.active_id != value || this._datas.active_type != type) {
this._datas.active_type = type;
this._datas.active_id = value;
+ this._datas.all_folded = null;
this.emitChange();
}
},
- readFeedArticle: function(feed_id) {
- // TODO
- },
emitChange: function() {
this.emit(CHANGE_EVENT);
},
@@ -53,6 +52,7 @@ MenuStore.dispatchToken = JarrDispatcher.register(function(action) {
MenuStore._datas['error_threshold'] = action.error_threshold;
MenuStore._datas['crawling_method'] = action.crawling_method;
MenuStore._datas['all_unread_count'] = action.all_unread_count;
+ MenuStore._datas.all_folded = null;
MenuStore.emitChange();
break;
case ActionTypes.PARENT_FILTER:
@@ -81,10 +81,10 @@ MenuStore.dispatchToken = JarrDispatcher.register(function(action) {
MenuStore._datas.categories[cat_id].unread += new_unread[feed_id];
}
if(changed) {
+ MenuStore._datas.all_folded = null;
MenuStore.emitChange();
}
}
-
break;
case ActionTypes.MENU_FILTER:
MenuStore.setFilter(action.filter);
@@ -98,18 +98,35 @@ MenuStore.dispatchToken = JarrDispatcher.register(function(action) {
MenuStore._datas.categories[article.category_id].unread += val;
MenuStore._datas.feeds[article.feed_id].unread += val;
});
+ MenuStore._datas.all_folded = null;
MenuStore.emitChange();
break;
case ActionTypes.LOAD_ARTICLE:
if(!action.was_read_before) {
MenuStore._datas.categories[action.article.category_id].unread -= 1;
MenuStore._datas.feeds[action.article.feed_id].unread -= 1;
+ MenuStore._datas.all_folded = null;
MenuStore.emitChange();
}
break;
case ActionTypes.TOGGLE_MENU_FOLD:
MenuStore._datas.all_folded = action.all_folded;
MenuStore.emitChange();
+ break;
+ case ActionTypes.MARK_ALL_AS_READ:
+ action.articles.map(function(art) {
+ if(!art.read) {
+ MenuStore._datas.feeds[art.feed_id].unread -= 1;
+ if(art.category_id) {
+ MenuStore._datas.categories[art.category_id].unread -= 1;
+
+ }
+ }
+ });
+
+ MenuStore._datas.all_folded = null;
+ MenuStore.emitChange();
+ break;
default:
// do nothing
}
diff --git a/src/web/js/stores/MiddlePanelStore.js b/src/web/js/stores/MiddlePanelStore.js
index 4a5efd00..c554f929 100644
--- a/src/web/js/stores/MiddlePanelStore.js
+++ b/src/web/js/stores/MiddlePanelStore.js
@@ -82,20 +82,18 @@ var MiddlePanelStore = assign({}, EventEmitter.prototype, {
MiddlePanelStore.dispatchToken = JarrDispatcher.register(function(action) {
var changed = false;
- switch(action.type) {
- case ActionTypes.RELOAD_MIDDLE_PANEL:
- changed = MiddlePanelStore.registerFilter(action);
- changed = MiddlePanelStore.setArticles(action.articles) || changed;
- break;
- case ActionTypes.PARENT_FILTER:
- changed = MiddlePanelStore.registerFilter(action);
- changed = MiddlePanelStore.setArticles(action.articles) || changed;
- break;
- case ActionTypes.MIDDLE_PANEL_FILTER:
- changed = MiddlePanelStore.registerFilter(action);
- changed = MiddlePanelStore.setArticles(action.articles) || changed;
- break;
- case ActionTypes.CHANGE_ATTR:
+ if (action.type == ActionTypes.RELOAD_MIDDLE_PANEL
+ || action.type == ActionTypes.PARENT_FILTER
+ || action.type == ActionTypes.MIDDLE_PANEL_FILTER) {
+ changed = MiddlePanelStore.registerFilter(action);
+ changed = MiddlePanelStore.setArticles(action.articles) || changed;
+ } else if (action.type == ActionTypes.MARK_ALL_AS_READ) {
+ changed = MiddlePanelStore.registerFilter(action);
+ for(var i in action.articles) {
+ action.articles[i].read = true;
+ }
+ changed = MiddlePanelStore.setArticles(action.articles) || changed;
+ } else if (action.type == ActionTypes.CHANGE_ATTR) {
var attr = action.attribute;
var val = action.value_bool;
action.articles.map(function(article) {
@@ -112,19 +110,15 @@ MiddlePanelStore.dispatchToken = JarrDispatcher.register(function(action) {
}
}
});
- break;
- case ActionTypes.LOAD_ARTICLE:
- changed = true;
- MiddlePanelStore._datas.selected_article = action.article.id;
- for (var i in MiddlePanelStore._datas.articles) {
- if(MiddlePanelStore._datas.articles[i].article_id == action.article.id) {
- MiddlePanelStore._datas.articles[i].read = true;
- break;
- }
+ } else if (action.type == ActionTypes.LOAD_ARTICLE) {
+ changed = true;
+ MiddlePanelStore._datas.selected_article = action.article.id;
+ for (var i in MiddlePanelStore._datas.articles) {
+ if(MiddlePanelStore._datas.articles[i].article_id == action.article.id) {
+ MiddlePanelStore._datas.articles[i].read = true;
+ break;
}
- break;
- default:
- // pass
+ }
}
if(changed) {MiddlePanelStore.emitChange();}
});
diff --git a/src/web/templates/management.html b/src/web/templates/management.html
index 01179b5e..dc95a052 100644
--- a/src/web/templates/management.html
+++ b/src/web/templates/management.html
@@ -14,18 +14,18 @@
<button class="btn btn-default" type="submit">OK</button>
</form>
<br />
- <a href="/export_opml" class="btn btn-default">{{ _('Export feeds to OPML') }}</a>
+ <a href="{{ url_for('articles.export', format='OPML') }}" class="btn btn-default">{{ _('Export feeds to OPML') }}</a>
<h1>{{ _('Data liberation') }}</h1>
<form action="" method="post" id="formImportJSON" enctype="multipart/form-data">
<span class="btn btn-default btn-file">{{ _('Import account') }} (<span class="text-info">*.json</span>)<input type="file" name="jsonfile" /></span>
<button class="btn btn-default" type="submit">OK</button>
</form>
<br />
- <a href="/export?format=JSON" class="btn btn-default">{{ _('Export account to JSON') }}</a>
+ <a href="{{ url_for('articles.export', format='JSON') }}" class="btn btn-default">{{ _('Export account to JSON') }}</a>
</div>
<div class="well">
<h1>{{ _('Export articles') }}</h1>
- <a href="/export?format=HTML" class="btn btn-default">HTML</a>
+ <a href="{{ url_for('articles.export', format='HTML') }}" class="btn btn-default">HTML</a>
</div>
</div><!-- /.container -->
{% endblock %}
diff --git a/src/web/views/article.py b/src/web/views/article.py
index 416bb96c..94f661fa 100644
--- a/src/web/views/article.py
+++ b/src/web/views/article.py
@@ -2,12 +2,14 @@
# -*- coding: utf-8 -
from datetime import datetime, timedelta
from flask import (Blueprint, g, render_template, redirect,
- flash, url_for, request)
+ flash, url_for, make_response, request)
from flask.ext.babel import gettext
from flask.ext.login import login_required, current_user
+from web.export import export_json, export_html
from web.lib.utils import clear_string, redirect_url
-from web.controllers import ArticleController
+from web.controllers import (ArticleController, UserController,
+ CategoryController)
from web.lib.view_utils import etag_match
articles_bp = Blueprint('articles', __name__, url_prefix='/articles')
@@ -124,3 +126,47 @@ def expire():
query.delete()
flash(gettext('%(count)d articles deleted', count=count), 'info')
return redirect(redirect_url())
+
+
+@articles_bp.route('/export', methods=['GET'])
+@login_required
+def export():
+ """
+ Export all articles to HTML or JSON.
+ """
+ user = UserController(current_user.id).get(id=current_user.id)
+ if request.args.get('format') == "HTML":
+ # Export to HTML
+ try:
+ archive_file, archive_file_name = export_html(user)
+ except Exception as e:
+ print(e)
+ flash(gettext("Error when exporting articles."), 'danger')
+ return redirect(redirect_url())
+ response = make_response(archive_file)
+ response.headers['Content-Type'] = 'application/x-compressed'
+ response.headers['Content-Disposition'] = 'attachment; filename=%s' \
+ % archive_file_name
+ elif request.args.get('format') == "JSON":
+ # Export to JSON
+ try:
+ json_result = export_json(user)
+ except Exception as e:
+ flash(gettext("Error when exporting articles."), 'danger')
+ return redirect(redirect_url())
+ response = make_response(json_result)
+ response.mimetype = 'application/json'
+ response.headers["Content-Disposition"] \
+ = 'attachment; filename=account.json'
+ elif request.args.get('format') == "OPML":
+ categories = {cat.id: cat.dump()
+ for cat in CategoryController(user.id).read()}
+ response = make_response(render_template('opml.xml', user=user,
+ categories=categories,
+ now=datetime.now()))
+ response.headers['Content-Type'] = 'application/xml'
+ response.headers['Content-Disposition'] = 'attachment; filename=feeds.opml'
+ else:
+ flash(gettext('Export format not supported.'), 'warning')
+ return redirect(redirect_url())
+ return response
diff --git a/src/web/views/home.py b/src/web/views/home.py
index fd677b3c..12a06024 100644
--- a/src/web/views/home.py
+++ b/src/web/views/home.py
@@ -133,8 +133,9 @@ def get_article(article_id, parse=False):
def mark_all_as_read():
filters = _get_filters(request.json)
acontr = ArticleController(current_user.id)
+ processed_articles = _articles_to_json(acontr.read_light(**filters))
acontr.update(filters, {'readed': True})
- return _articles_to_json(acontr.read(**filters))
+ return processed_articles
@current_app.route('/fetch', methods=['GET'])
bgstack15