summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2015-07-29 18:30:42 -0700
committerMike Crute <mcrute@gmail.com>2015-07-29 18:30:42 -0700
commit979633a50f11afbb6388ac0fd46e45aac64a286a (patch)
tree98ed772da65e8b37349e5abfca3c9f3b9da41760
parentc68030ada8a1c7bf0b90be17aa37315d8e2ad89b (diff)
downloadweb_rss_reader-979633a50f11afbb6388ac0fd46e45aac64a286a.tar.bz2
web_rss_reader-979633a50f11afbb6388ac0fd46e45aac64a286a.tar.xz
web_rss_reader-979633a50f11afbb6388ac0fd46e45aac64a286a.zip
Add more featuresHEADmaster
-rwxr-xr-xapp.py85
-rw-r--r--templates/items.html250
2 files changed, 213 insertions, 122 deletions
diff --git a/app.py b/app.py
index 1740f51..1bc6d93 100755
--- a/app.py
+++ b/app.py
@@ -43,8 +43,7 @@ class RSSItem(DataObject):
43 def __init__(self, id, title, author, feed_title, feed_url, url, 43 def __init__(self, id, title, author, feed_title, feed_url, url,
44 publish_date, content, read): 44 publish_date, content, read):
45 self.id = id 45 self.id = id
46 self.feed_url = RSSFeed(feed_url, None, None).self_link 46 self.feed = RSSFeed(feed_url, None, feed_title)
47 self.feed_title = feed_title
48 self.title = title 47 self.title = title
49 self.author = author 48 self.author = author
50 self.url = url 49 self.url = url
@@ -53,6 +52,10 @@ class RSSItem(DataObject):
53 self.read = not bool(read) 52 self.read = not bool(read)
54 self.no_content = len(content) < 100 53 self.no_content = len(content) < 100
55 54
55 def _update_json(self, data):
56 data['feed'] = self.feed.self_link
57 return data
58
56 59
57class RSSFeed(DataObject): 60class RSSFeed(DataObject):
58 61
@@ -64,7 +67,16 @@ class RSSFeed(DataObject):
64 self.title = title 67 self.title = title
65 68
66 def _update_json(self, data): 69 def _update_json(self, data):
67 data['items'] = '/feed/{}/items'.format(self.id) 70 data['items'] = self.items
71 data['unread_items'] = self.unread_items
72
73 @property
74 def items(self):
75 return '/feed/{}/items'.format(self.id)
76
77 @property
78 def unread_items(self):
79 return '/feed/{}/items/unread'.format(self.id)
68 80
69 @property 81 @property
70 def id(self): 82 def id(self):
@@ -77,6 +89,13 @@ class RSSFeed(DataObject):
77 '{}========'.format(token).encode('utf-8')).decode('utf-8') 89 '{}========'.format(token).encode('utf-8')).decode('utf-8')
78 90
79 91
92class UnreadRSSFeed(RSSFeed):
93
94 def __init__(self, feed_url, url, title, unread_count):
95 super(UnreadRSSFeed, self).__init__(feed_url, url, title)
96 self.unread_count = unread_count
97
98
80 99
81class DBReader(object): 100class DBReader(object):
82 101
@@ -95,6 +114,20 @@ class DBReader(object):
95 i.pubDate desc 114 i.pubDate desc
96 ''' 115 '''
97 116
117 UNREAD_FEEDS = '''
118 SELECT
119 f.rssurl, f.url, f.title,
120 count(*) as unread_count
121 FROM
122 rss_item i
123 LEFT JOIN rss_feed f
124 ON f.rssurl = i.feedurl
125 WHERE
126 i.unread = 1
127 GROUP BY
128 i.feedurl
129 '''
130
98 def __init__(self, db_path): 131 def __init__(self, db_path):
99 self.con = sqlite3.connect(db_path) 132 self.con = sqlite3.connect(db_path)
100 self.con.row_factory = RSSItem.from_db_row 133 self.con.row_factory = RSSItem.from_db_row
@@ -120,17 +153,26 @@ class DBReader(object):
120 [1 if unread else 0, id]) 153 [1 if unread else 0, id])
121 con.commit() 154 con.commit()
122 155
156 def update_feed_unread(self, token, unread=True):
157 with self.con as con:
158 con.execute(
159 'UPDATE rss_item SET unread = ? WHERE feedurl = ?',
160 [1 if unread else 0, RSSFeed.parse_token(token)])
161 con.commit()
162
123 def get_entry(self, id): 163 def get_entry(self, id):
124 return self._fetch("i.id = ?", [id])[0] 164 return self._fetch("i.id = ?", [id])[0]
125 165
126 def get_unread(self): 166 def get_unread(self):
127 data = defaultdict(list) 167 data = defaultdict(list)
128 unread = self._fetch("i.unread = ?", [1]) 168 return self._fetch("i.unread = ?", [1])
129
130 for record in unread:
131 data[record.feed_title].append(record)
132 169
133 return sorted(data.items()) 170 def get_unread_feeds(self):
171 with self.con as con:
172 con.row_factory = UnreadRSSFeed.from_db_row
173 curs = con.cursor()
174 curs.execute(self.UNREAD_FEEDS)
175 return curs.fetchall()
134 176
135 def get_unread_for_feed(self, token, only_unread=False): 177 def get_unread_for_feed(self, token, only_unread=False):
136 if only_unread: 178 if only_unread:
@@ -159,16 +201,21 @@ def json_list(data):
159 201
160@app.route('/') 202@app.route('/')
161def index(): 203def index():
162 reader = DBReader(DB_PATH) 204 return render_template('items.html')
163 return render_template('items.html', items=reader.get_unread())
164 205
165 206
166@app.route('/feed/') 207@app.route('/feed')
167def feed_list(): 208def feed_list():
168 reader = DBReader(DB_PATH) 209 reader = DBReader(DB_PATH)
169 return jsonify({ 'feeds': json_list(reader.get_feeds()) }) 210 return jsonify({ 'feeds': json_list(reader.get_feeds()) })
170 211
171 212
213@app.route('/feed/unread')
214def unread_feed_list():
215 reader = DBReader(DB_PATH)
216 return jsonify({ 'feeds': json_list(reader.get_unread_feeds()) })
217
218
172@app.route('/feed/<token>') 219@app.route('/feed/<token>')
173def feed(token): 220def feed(token):
174 reader = DBReader(DB_PATH) 221 reader = DBReader(DB_PATH)
@@ -182,13 +229,27 @@ def feed_items(token):
182 return jsonify({ 'items': json_list(unread), "count": len(unread) }) 229 return jsonify({ 'items': json_list(unread), "count": len(unread) })
183 230
184 231
185@app.route('/feed/<token>/items/unread') 232@app.route('/feed/<token>/items/unread', methods=["GET", "POST"])
186def unread_feed_items(token): 233def unread_feed_items(token):
187 reader = DBReader(DB_PATH) 234 reader = DBReader(DB_PATH)
235 if request.method == 'POST':
236 try:
237 read = bool(int(request.form.get('read')))
238 except:
239 return make_response('', 400)
240
241 reader.update_feed_unread(token, not read)
242
188 unread = reader.get_unread_for_feed(token, True) 243 unread = reader.get_unread_for_feed(token, True)
189 return jsonify({ 'items': json_list(unread), "count": len(unread) }) 244 return jsonify({ 'items': json_list(unread), "count": len(unread) })
190 245
191 246
247@app.route('/item/unread')
248def unread_item_list():
249 reader = DBReader(DB_PATH)
250 return jsonify({ 'items': json_list(reader.get_unread()) })
251
252
192@app.route("/item/<int:entry_id>", methods=["GET", "POST"]) 253@app.route("/item/<int:entry_id>", methods=["GET", "POST"])
193def item(entry_id): 254def item(entry_id):
194 #post read=1 255 #post read=1
diff --git a/templates/items.html b/templates/items.html
index 22adbba..e138ece 100644
--- a/templates/items.html
+++ b/templates/items.html
@@ -8,120 +8,80 @@
8 font: 12px Georgia,serif; 8 font: 12px Georgia,serif;
9 } 9 }
10 10
11 .entries li { 11 #feeds, #feed-items {
12 position: relative; 12 margin: 0;
13 padding: 0;
13 list-style: none; 14 list-style: none;
14 border: 1px solid #999; 15 width: 95%;
15 margin: 0.5em 0;
16 } 16 }
17 17
18 .entries { 18 #feeds-container {
19 margin: 0; 19 margin: 0;
20 padding: 0; 20 padding: 0;
21 display: inline-block;
22 vertical-align: top;
23 border: 1px solid #999;
21 } 24 }
22 25
23 .entries a.headline { 26 #feeds li {
24 display: block; 27 padding: 0.7em;
25 background: #ddd; 28 cursor: pointer;
26 padding: 1em;
27 color: black;
28 text-decoration: none;
29 font-weight: bolder;
30 } 29 }
31 30
32 .entries .highlighted a.headline { 31 #minimize-feeds {
33 background-color: #FFFF99; 32 cursor: pointer;
33 padding: 0.7em;
34 background-color: #ccc;
34 } 35 }
35 36
36 .entries li > input.read { 37 #feed-items li {
37 position: absolute; 38 position: relative;
38 top: 1em; 39 border: 1px solid gray;
39 right: 1em; 40 border-top: none;
41 width: 1000px;
40 } 42 }
41 43
42 .entries .content { 44 #feed-items li .title {
43 padding: 1em; 45 display: inline-block;
44 display: none; 46 cursor: pointer;
47 padding: 0.5em;
48 margin: 0.1em;
49 line-height: 1.5em;
45 } 50 }
46 51
47 h2 a.mark-all-read { 52 #feed-items li:first-child {
48 display: inline-block; 53 border-top: 1px solid gray;
49 position: absolute;
50 right: 1em;
51 } 54 }
52 </style>
53 55
54 <script src="//code.jquery.com/jquery-2.1.1.min.js"></script> 56 #feed-items li input {
57 margin: 0.5em;
58 }
55 59
56 <script type="text/javascript"> 60 #feeds li.active {
57 /** 61 background-color: #ff9;
58 * Copyright (c) 2007-2014 Ariel Flesler - aflesler<a>gmail<d>com | http://flesler.blogspot.com 62 border: 1px solid goldenrod;
59 * Licensed under MIT 63 border-right: none;
60 * @author Ariel Flesler 64 border-top-left-radius: 10px;
61 * @version 1.4.12 65 border-bottom-left-radius: 10px;
62 */ 66 }
63 ;(function(a){if(typeof define==='function'&&define.amd){define(['jquery'],a)}else{a(jQuery)}}(function($){var j=$.scrollTo=function(a,b,c){return $(window).scrollTo(a,b,c)};j.defaults={axis:'xy',duration:parseFloat($.fn.jquery)>=1.3?0:1,limit:true};j.window=function(a){return $(window)._scrollable()};$.fn._scrollable=function(){return this.map(function(){var a=this,isWin=!a.nodeName||$.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!isWin)return a;var b=(a.contentWindow||a).document||a.ownerDocument||a;return/webkit/i.test(navigator.userAgent)||b.compatMode=='BackCompat'?b.body:b.documentElement})};$.fn.scrollTo=function(f,g,h){if(typeof g=='object'){h=g;g=0}if(typeof h=='function')h={onAfter:h};if(f=='max')f=9e9;h=$.extend({},j.defaults,h);g=g||h.duration;h.queue=h.queue&&h.axis.length>1;if(h.queue)g/=2;h.offset=both(h.offset);h.over=both(h.over);return this._scrollable().each(function(){if(f==null)return;var d=this,$elem=$(d),targ=f,toff,attr={},win=$elem.is('html,body');switch(typeof targ){case'number':case'string':if(/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)){targ=both(targ);break}targ=win?$(targ):$(targ,this);if(!targ.length)return;case'object':if(targ.is||targ.style)toff=(targ=$(targ)).offset()}var e=$.isFunction(h.offset)&&h.offset(d,targ)||h.offset;$.each(h.axis.split(''),function(i,a){var b=a=='x'?'Left':'Top',pos=b.toLowerCase(),key='scroll'+b,old=d[key],max=j.max(d,a);if(toff){attr[key]=toff[pos]+(win?0:old-$elem.offset()[pos]);if(h.margin){attr[key]-=parseInt(targ.css('margin'+b))||0;attr[key]-=parseInt(targ.css('border'+b+'Width'))||0}attr[key]+=e[pos]||0;if(h.over[pos])attr[key]+=targ[a=='x'?'width':'height']()*h.over[pos]}else{var c=targ[pos];attr[key]=c.slice&&c.slice(-1)=='%'?parseFloat(c)/100*max:c}if(h.limit&&/^\d+$/.test(attr[key]))attr[key]=attr[key]<=0?0:Math.min(attr[key],max);if(!i&&h.queue){if(old!=attr[key])animate(h.onAfterFirst);delete attr[key]}});animate(h.onAfter);function animate(a){$elem.animate(attr,g,h.easing,a&&function(){a.call(this,targ,h)})}}).end()};j.max=function(a,b){var c=b=='x'?'Width':'Height',scroll='scroll'+c;if(!$(a).is('html,body'))return a[scroll]-$(a)[c.toLowerCase()]();var d='client'+c,html=a.ownerDocument.documentElement,body=a.ownerDocument.body;return Math.max(html[scroll],body[scroll])-Math.min(html[d],body[d])};function both(a){return $.isFunction(a)||typeof a=='object'?a:{top:a,left:a}};return j}));
64 </script>
65 67
66 <script type="text/javascript"> 68 .content-iframe {
67 /* 69 width: 100%;
68 * Viewport - jQuery selectors for finding elements in viewport 70 border: none;
69 * 71 }
70 * Copyright (c) 2008-2009 Mika Tuupola 72
71 * 73 .no-content {
72 * Licensed under the MIT license: 74 background: url(http://www.famfamfam.com/lab/icons/silk/icons/tab.png) no-repeat 99% center;
73 * http://www.opensource.org/licenses/mit-license.php 75 }
74 * 76 </style>
75 * Project home:
76 * http://www.appelsiini.net/projects/viewport
77 *
78 */
79 (function($) {
80
81 $.belowthefold = function(element, settings) {
82 var fold = $(window).height() + $(window).scrollTop();
83 return fold <= $(element).offset().top - settings.threshold;
84 };
85
86 $.abovethetop = function(element, settings) {
87 var top = $(window).scrollTop();
88 return top >= $(element).offset().top + $(element).height() - settings.threshold;
89 };
90
91 $.rightofscreen = function(element, settings) {
92 var fold = $(window).width() + $(window).scrollLeft();
93 return fold <= $(element).offset().left - settings.threshold;
94 };
95
96 $.leftofscreen = function(element, settings) {
97 var left = $(window).scrollLeft();
98 return left >= $(element).offset().left + $(element).width() - settings.threshold;
99 };
100
101 $.inviewport = function(element, settings) {
102 return !$.rightofscreen(element, settings) && !$.leftofscreen(element, settings) && !$.belowthefold(element, settings) && !$.abovethetop(element, settings);
103 };
104
105 $.extend($.expr[':'], {
106 "below-the-fold": function(a, i, m) {
107 return $.belowthefold(a, {threshold : 0});
108 },
109 "above-the-top": function(a, i, m) {
110 return $.abovethetop(a, {threshold : 0});
111 },
112 "left-of-screen": function(a, i, m) {
113 return $.leftofscreen(a, {threshold : 0});
114 },
115 "right-of-screen": function(a, i, m) {
116 return $.rightofscreen(a, {threshold : 0});
117 },
118 "in-viewport": function(a, i, m) {
119 return $.inviewport(a, {threshold : 0});
120 }
121 });
122 })(jQuery);
123 </script>
124 77
78 <script src="https://mike.crute.me/resources/modernizr-2.8.2.js"></script>
79 <script src="https://mike.crute.me/resources/jquery-2.1.1.js"></script>
80 <script src="https://mike.crute.me/resources/underscore-1.6.0.js"></script>
81 <script src="https://mike.crute.me/resources/jquery.scrollTo-1.4.12.js"></script>
82 <script src="https://mike.crute.me/resources/jquery.viewport.js"></script>
83
84 <!--
125 <script type="text/javascript"> 85 <script type="text/javascript">
126 $(document).ready(function() { 86 $(document).ready(function() {
127 $("a.headline").on("click", function(event) { 87 $("a.headline").on("click", function(event) {
@@ -162,13 +122,15 @@
162 }); 122 });
163 }); 123 });
164 124
165 $("h2 a.mark-all-read").on("click", function(event) { 125 $("h1 a.mark-all-read").on("click", function(event) {
166 event.preventDefault(); 126 event.preventDefault();
167 127
168 $(event.target).parents(".feed").find("a.headline").each(function(idx, box) { 128 $.ajax($(event.target).parents(".feed").attr("data-unread-items"), {
169 $(box).trigger("mark-read"); 129 data: { read: 1 },
130 type: "POST"
170 }); 131 });
171 132
133
172 return false; 134 return false;
173 }); 135 });
174 136
@@ -231,22 +193,90 @@
231 }); 193 });
232 }); 194 });
233 </script> 195 </script>
196 -->
197
198 <script type="text/javascript">
199 function loadFeedItems(url) {
200 var feed_item_template = _.template($("#feed_item_template").text());
201 $("#feed-items li").remove();
202
203 $.ajax(url).done(function(data) {
204 _.forEach(data.items, function(item) {
205 $("#feed-items").append(feed_item_template({ item: item }));
206 });
207
208 $("#feed-items .title").on("click", function(event) {
209 var target = $(event.target);
210 var template = _.template($("#item_contents_template").text());
211
212 $.ajax(target.parents("li").attr("data-item-href")).done(function(data) {
213 if (data.no_content) {
214 alert("No content");
215 /*
216 var win = window.open(data.url, "_blank");
217 if (!win) {
218 alert("Popups are blocked");
219 }
220 */
221 } else {
222 target.parent("li").find(".content-area").append(template({ item: data }));
223 var iframe = $("#" + data.id + "-item-contents");
224
225 iframe.contents().find("html").html(data.content);
226 iframe.height(iframe.contents().find("html").height());
227 }
228 });
229 });
230 });
231 }
232
233 $(document).ready(function() {
234 var feed_template = _.template($("#feed_template").text());
235
236 $.ajax("/feed/unread").done(function(data) {
237 _.forEach(data.feeds, function(item) {
238 $("#feeds").append(feed_template({ item: item }));
239 });
240
241 $("#feeds li").on("click", function(event) {
242 var target = $(event.currentTarget);
243 $("#feeds li").removeClass("active");
244 target.addClass("active");
245
246 loadFeedItems(target.attr("data-unread-href"));
247 });
248 });
249
250 $("#minimize-feeds").on("click", function() {
251 $("#feeds").slideToggle();
252 });
253
254 loadFeedItems("/item/unread");
255 });
256 </script>
234 </head> 257 </head>
235 <body> 258 <body>
236 <h1>RSS Entries</h1> 259 <div id="feeds-container">
237 {% for feed, records in items %} 260 <div id="minimize-feeds">Feeds</div>
238 <div class="feed"> 261 <ul id="feeds">
239 <h2>{{ feed }} ({{ records|length }}) <a href="#" class="mark-all-read">Mark All Read</a></h2>
240 <ul class="entries">
241 {% for record in records %}
242 <li data-url="{{ record.self_link }}" >
243 <a class="headline" href="{{ record.url }}">{{ record.title }}</a>
244 <input type="checkbox" class="read" />
245 <div class="content"></div>
246 </li>
247 {% endfor %}
248 </ul> 262 </ul>
249 </div> 263 </div>
250 {% endfor %} 264 <li id="feed-items"></li>
265
266
267 <script type="text/template" id="feed_template">
268 <li data-unread-href="<%= item.unread_items %>"><%= item.title %> (<%= item.unread_count %>)</li>
269 </script>
270
271 <script type="text/template" id="feed_item_template">
272 <li data-item-href="<%= item.self %>" <% if (item.no_content) { %>class="no-content"<% } %>>
273 <input type="checkbox" /><span class="title"><%= item.title %></span>
274 <div class="content-area"></div>
275 </li>
276 </script>
277
278 <script type="text/template" id="item_contents_template">
279 <iframe id="<%= item.id %>-item-contents" class="content-iframe"></iframe>
280 </script>
251 </body> 281 </body>
252</html> 282</html>