From 637f4d69551c0a57b54d9c4f607267d881d84b1c Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Mon, 14 Nov 2016 11:12:13 -0800 Subject: [PATCH] Stubbing in notifications dialog. --- apps/newsletters/views.py | 2 +- apps/notifications/__init__.py | 0 apps/notifications/models.py | 54 ++ apps/notifications/tests.py | 16 + apps/notifications/views.py | 1 + media/css/reader.css | 17 + .../newsblur/reader/reader_notifications.js | 567 ++++++++++++++++++ utils/mongoengine_fields.py | 111 ++++ 8 files changed, 767 insertions(+), 1 deletion(-) create mode 100644 apps/notifications/__init__.py create mode 100644 apps/notifications/models.py create mode 100644 apps/notifications/tests.py create mode 100644 apps/notifications/views.py create mode 100644 media/js/newsblur/reader/reader_notifications.js create mode 100644 utils/mongoengine_fields.py diff --git a/apps/newsletters/views.py b/apps/newsletters/views.py index 8522ea513..16c286cf1 100644 --- a/apps/newsletters/views.py +++ b/apps/newsletters/views.py @@ -41,7 +41,7 @@ def newsletter_receive(request): response = HttpResponse('OK') if settings.DEBUG or 'samuel' in params.get('To', ''): - logging.debug(" ---> Email newsletter: %s" % pprint(params)) + logging.debug(" ---> Email newsletter: %s" % params) email_newsletter = EmailNewsletter() story = email_newsletter.receive_newsletter(params) diff --git a/apps/notifications/__init__.py b/apps/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/notifications/models.py b/apps/notifications/models.py new file mode 100644 index 000000000..105ebafd7 --- /dev/null +++ b/apps/notifications/models.py @@ -0,0 +1,54 @@ +import datetime +import enum +import pymongo +import redis +import mongoengine as mongo +from django.conf import settings +from django.contrib.auth.models import User +from utils import log as logging +from utils import mongoengine_fields + + +class NotificationFrequency(enum.Enum): + immediately = 1 + hour_1 = 2 + hour_6 = 3 + hour_12 = 4 + hour_24 = 5 + + +class MUserFeedNotification(mongo.Document): + '''A user's notifications of a single feed.''' + user_id = mongo.IntField() + feed_id = mongo.IntField() + frequency = mongoengine_fields.IntEnumField(NotificationFrequency) + last_notification_date = mongo.DateTimeField(default=datetime.datetime.now) + is_email = mongo.BooleanField() + is_web = mongo.BooleanField() + is_ios = mongo.BooleanField() + is_android = mongo.BooleanField() + + meta = { + 'collection': 'notifications', + 'indexes': ['feed_id', + {'fields': ['user_id', 'feed_id'], + 'unique': True, + 'types': False, }], + 'allow_inheritance': False, + } + + def __unicode__(self): + notification_types = [] + if self.is_email: notification_types.append('email') + if self.is_web: notification_types.append('web') + if self.is_ios: notification_types.append('ios') + if self.is_android: notification_types.append('android') + + return "%s/%s: %s -> %s" % ( + User.objects.get(pk=self.user_id).username + Feed.get_feed_by_id(self.feed_id) + ','.join(notification_types), + self.last_notification_date + ) + + diff --git a/apps/notifications/tests.py b/apps/notifications/tests.py new file mode 100644 index 000000000..501deb776 --- /dev/null +++ b/apps/notifications/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/apps/notifications/views.py b/apps/notifications/views.py new file mode 100644 index 000000000..60f00ef0e --- /dev/null +++ b/apps/notifications/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/media/css/reader.css b/media/css/reader.css index 6b86c8dc5..9ce7b6b48 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -8809,6 +8809,23 @@ form.opml_import_form input { font-size: 11px; } +/* ======================= */ +/* = Notifications Modal = */ +/* ======================= */ + +.NB-modal-notifications .NB-preference-label { + float: left; + margin: 10px 0; + width: 102px; + clear: both; +} +.NB-modal-notifications .NB-preference-options { + margin: 6px 0; + float: left; + overflow: hidden; +} + + /* ===================== */ /* = Feedchooser Modal = */ /* ===================== */ diff --git a/media/js/newsblur/reader/reader_notifications.js b/media/js/newsblur/reader/reader_notifications.js new file mode 100644 index 000000000..e548c28e9 --- /dev/null +++ b/media/js/newsblur/reader/reader_notifications.js @@ -0,0 +1,567 @@ +NEWSBLUR.ReaderNotifications = function(feed_id, options) { + var defaults = { + 'onOpen': function() { + $(window).trigger('resize.simplemodal'); + } + }; + + this.options = $.extend({}, defaults, options); + this.model = NEWSBLUR.assets; + this.feed_id = feed_id; + this.feed = this.model.get_feed(feed_id); + this.folder_title = this.options.folder_title; + this.folder = this.folder_title && NEWSBLUR.assets.get_folder(this.folder_title); + + this.runner(); +}; + +NEWSBLUR.ReaderNotifications.prototype = new NEWSBLUR.Modal; +NEWSBLUR.ReaderNotifications.prototype.constructor = NEWSBLUR.ReaderNotifications; + +_.extend(NEWSBLUR.ReaderNotifications.prototype, { + + runner: function() { + if (this.folder) { + NEWSBLUR.Modal.prototype.initialize_folder.call(this, this.folder_title); + } else { + NEWSBLUR.Modal.prototype.initialize_feed.call(this, this.feed_id); + } + this.make_modal(); + this.handle_cancel(); + this.open_modal(); + this.initialize_feed(this.feed_id); + + this.$modal.bind('click', $.rescope(this.handle_click, this)); + this.$modal.bind('change', $.rescope(this.handle_change, this)); + }, + + initialize_feed: function(feed_id) { + var frequency = this.feed.get('notification_frequency'); + var notifications = this.feed.get('notifications'); + + if (this.feed) { + NEWSBLUR.Modal.prototype.initialize_feed.call(this, feed_id); + } else if (this.folder) { + NEWSBLUR.Modal.prototype.initialize_folder.call(this, feed_id); + } + + $('input[name=frequency]', this.$modal).each(function() { + if ($(this).val() == frequency) { + $(this).attr('checked', true); + return false; + } + }); + $('input[name=notifications]', this.$modal).each(function() { + if (_.contains(notifications, $(this).val())) { + $(this).attr('checked', true); + return false; + } + }); + + if (this.folder) { + this.$modal.addClass('NB-modal-folder-settings'); + this.$modal.removeClass('NB-modal-feed-settings'); + $(".NB-modal-title", this.$modal).text("Folder Settings"); + } else if (this.feed.get('exception_type')) { + this.$modal.removeClass('NB-modal-folder-settings'); + this.$modal.removeClass('NB-modal-feed-settings'); + $(".NB-modal-title", this.$modal).text("Fix a misbehaving site"); + } else { + this.$modal.removeClass('NB-modal-folder-settings'); + this.$modal.addClass('NB-modal-feed-settings'); + $(".NB-modal-title", this.$modal).text("Site Settings"); + } + + this.resize(); + }, + + get_feed_settings: function() { + if (this.feed.is_starred()) return; + + var $loading = $('.NB-modal-loading', this.$modal); + $loading.addClass('NB-active'); + + var settings_fn = this.options.social_feed ? this.model.get_social_settings : + this.model.get_feed_settings; + settings_fn.call(this.model, this.feed_id, _.bind(this.populate_settings, this)); + }, + + populate_settings: function(data) { + var $submit = $('.NB-modal-submit-save', this.$modal); + var $loading = $('.NB-modal-loading', this.$modal); + var $page_history = $(".NB-exception-page-history", this.$modal); + var $feed_history = $(".NB-exception-feed-history", this.$modal); + + $feed_history.html(this.make_history(data, 'feed_fetch')); + $page_history.html(this.make_history(data, 'page_fetch')); + + $loading.removeClass('NB-active'); + this.resize(); + }, + + make_modal: function() { + var self = this; + + this.$modal = $.make('div', { className: 'NB-modal-exception NB-modal' }, [ + (this.feed && $.make('div', { className: 'NB-modal-feed-chooser-container'}, [ + this.make_feed_chooser() + ])), + $.make('div', { className: 'NB-modal-loading' }), + $.make('h2', { className: 'NB-modal-title NB-exception-block-only' }, 'Fix a misbehaving site'), + $.make('h2', { className: 'NB-modal-title' }, 'Site settings'), + $.make('h2', { className: 'NB-modal-subtitle' }, [ + $.make('img', { className: 'NB-modal-feed-image feed_favicon' }), + $.make('div', { className: 'NB-modal-feed-heading' }, [ + $.make('span', { className: 'NB-modal-feed-title' }), + $.make('span', { className: 'NB-modal-feed-subscribers' }) + ]) + ]), + $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-view NB-modal-submit NB-settings-only' }, [ + $.make('h5', [ + $.make('div', { className: 'NB-exception-option-status NB-right' }), + $.make('div', { className: 'NB-exception-option-meta' }), + 'View settings' + ]), + $.make('div', { className: 'NB-fieldset-fields' }, [ + $.make('div', { className: 'NB-exception-input-wrapper' }, [ + $.make('div', { className: 'NB-preference-label'}, [ + 'Reading view' + ]), + $.make('div', { className: 'NB-preference-options NB-view-settings' }, [ + $.make('div', { className: "NB-view-setting-original" }, [ + $.make('label', { 'for': 'NB-preference-view-1' }, [ + $.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'view_settings', value: 'page' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_original_active.png' }), + $.make("div", { className: "NB-view-title" }, "Original") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-view-2' }, [ + $.make('input', { id: 'NB-preference-view-2', type: 'radio', name: 'view_settings', value: 'feed' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_feed_active.png' }), + $.make("div", { className: "NB-view-title" }, "Feed") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-view-3' }, [ + $.make('input', { id: 'NB-preference-view-3', type: 'radio', name: 'view_settings', value: 'text' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_text_active.png' }), + $.make("div", { className: "NB-view-title" }, "Text") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-view-4' }, [ + $.make('input', { id: 'NB-preference-view-4', type: 'radio', name: 'view_settings', value: 'story' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_story_active.png' }), + $.make("div", { className: "NB-view-title" }, "Story") + ]) + ]) + ]), + $.make('div', { className: 'NB-preference-label'}, [ + 'Story layout' + ]), + $.make('div', { className: 'NB-preference-options NB-view-settings' }, [ + $.make('div', { className: "" }, [ + $.make('label', { 'for': 'NB-preference-layout-1' }, [ + $.make('input', { id: 'NB-preference-layout-1', type: 'radio', name: 'story_layout', value: 'full' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_full_active.png' }), + $.make("div", { className: "NB-layout-title" }, "Full") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-layout-2' }, [ + $.make('input', { id: 'NB-preference-layout-2', type: 'radio', name: 'story_layout', value: 'split' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_split_active.png' }), + $.make("div", { className: "NB-layout-title" }, "Split") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-layout-3' }, [ + $.make('input', { id: 'NB-preference-layout-3', type: 'radio', name: 'story_layout', value: 'list' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_list_active.png' }), + $.make("div", { className: "NB-layout-title" }, "List") + ]) + ]), + $.make('div', [ + $.make('label', { 'for': 'NB-preference-layout-4' }, [ + $.make('input', { id: 'NB-preference-layout-4', type: 'radio', name: 'story_layout', value: 'grid' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_grid_active.png' }), + $.make("div", { className: "NB-layout-title" }, "Grid") + ]) + ]) + ]) + ]) + ]) + ]), + $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-retry NB-modal-submit NB-exception-block-only' }, [ + $.make('h5', [ + $.make('div', { className: 'NB-exception-option-meta' }), + $.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 1:'), + 'Retry' + ]), + $.make('div', { className: 'NB-fieldset-fields' }, [ + $.make('div', [ + $.make('div', { className: 'NB-loading' }), + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-retry' }, 'Retry fetching and parsing'), + $.make('div', { className: 'NB-error' }) + ]) + ]) + ]), + (this.feed && $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-feed NB-modal-submit' }, [ + $.make('h5', [ + $.make('div', { className: 'NB-exception-option-meta' }), + $.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 2:'), + 'Change RSS Feed Address' + ]), + $.make('div', { className: 'NB-fieldset-fields' }, [ + $.make('div', { className: 'NB-exception-input-wrapper' }, [ + $.make('div', { className: 'NB-loading' }), + $.make('label', { 'for': 'NB-exception-input-address', className: 'NB-exception-label' }, [ + $.make('div', { className: 'NB-folder-icon' }), + 'RSS/XML URL: ' + ]), + $.make('input', { type: 'text', id: 'NB-exception-input-address', className: 'NB-exception-input-address NB-input', name: 'feed_address', value: this.feed.get('feed_address') }) + ]), + (this.feed.is_feed() && $.make('div', { className: 'NB-exception-submit-wrapper' }, [ + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-address' }, 'Parse this RSS/XML Feed'), + $.make('div', { className: 'NB-error' }), + $.make('div', { className: 'NB-exception-feed-history' }) + ])) + ]) + ])), + (this.feed && $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-page NB-modal-submit' }, [ + $.make('h5', [ + $.make('div', { className: 'NB-exception-option-meta' }), + $.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 3:'), + 'Change Website Address' + ]), + $.make('div', { className: 'NB-fieldset-fields' }, [ + $.make('div', { className: 'NB-exception-input-wrapper' }, [ + $.make('div', { className: 'NB-loading' }), + $.make('label', { 'for': 'NB-exception-input-link', className: 'NB-exception-label' }, [ + $.make('div', { className: 'NB-folder-icon' }), + 'Website URL: ' + ]), + $.make('input', { type: 'text', id: 'NB-exception-input-link', className: 'NB-exception-input-link NB-input', name: 'feed_link', value: this.feed.get('feed_link') }) + ]), + (this.feed.is_feed() && $.make('div', { className: 'NB-exception-submit-wrapper' }, [ + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-link' }, 'Fetch Feed From Website'), + $.make('div', { className: 'NB-error' }), + $.make('div', { className: 'NB-exception-page-history' }) + ])) + ]) + ])), + (this.folder && $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-feed NB-modal-submit' }, [ + $.make('h5', [ + $.make('div', { className: 'NB-exception-option-meta' }), + 'Folder RSS Feed Address' + ]), + $.make('div', { className: 'NB-fieldset-fields' }, [ + $.make('div', { className: 'NB-exception-input-wrapper' }, [ + $.make('div', { className: 'NB-loading' }), + $.make('label', { 'for': 'NB-exception-input-unread', className: 'NB-exception-label' }, [ + $.make('div', { className: 'NB-folder-icon' }), + 'Unread+Focus:' + ]), + $.make('input', { type: 'text', id: 'NB-exception-input-unread', className: 'NB-exception-input-unread NB-input', name: 'folder_rss_unread_url', value: this.folder.rss_url('unread') }) + ]), + $.make('div', { className: 'NB-exception-input-wrapper' }, [ + $.make('div', { className: 'NB-loading' }), + $.make('label', { 'for': 'NB-exception-input-focus', className: 'NB-exception-label' }, [ + $.make('div', { className: 'NB-folder-icon' }), + 'Only Focus:' + ]), + $.make('input', { type: 'text', id: 'NB-exception-input-focus', className: 'NB-exception-input-focus NB-input', name: 'folder_rss_focus_url', value: this.folder.rss_url('focus') }) + ]), + (!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-premium-only' }, [ + $.make('div', { className: 'NB-premium-only-divider'}), + $.make('div', { className: 'NB-premium-only-text'}, [ + 'RSS feeds for folders is a ', + $.make('a', { href: '#', className: 'NB-premium-only-link NB-splash-link' }, 'premium feature'), + '.' + ]) + ])) + ]) + ])), + $.make('div', { className: 'NB-fieldset NB-exception-option NB-exception-option-delete NB-exception-block-only NB-modal-submit' }, [ + $.make('h5', [ + $.make('span', { className: 'NB-exception-option-option NB-exception-only' }, 'Option 4:'), + 'Just Delete This Feed' + ]), + $.make('div', { className: 'NB-fieldset-fields' }, [ + $.make('div', [ + $.make('div', { className: 'NB-loading' }), + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-red NB-modal-submit-delete' }, 'Delete It. It Just Won\'t Work!'), + $.make('div', { className: 'NB-error' }) + ]) + ]) + ]) + ]); + }, + + make_history: function(data, fetch_type) { + var fetches = data[fetch_type+'_history']; + var $history; + + if (fetches && fetches.length) { + $history = _.map(fetches, function(fetch) { + var feed_ok = _.contains([200, 304], fetch.status_code) || !fetch.status_code; + var status_class = feed_ok ? ' NB-ok ' : ' NB-errorcode '; + return $.make('div', { className: 'NB-history-fetch' + status_class, title: feed_ok ? '' : fetch.exception }, [ + $.make('div', { className: 'NB-history-fetch-date' }, fetch.fetch_date || fetch.push_date), + $.make('div', { className: 'NB-history-fetch-message' }, [ + fetch.message, + (fetch.status_code && $.make('div', { className: 'NB-history-fetch-code' }, ' ('+fetch.status_code+')')) + ]) + ]); + }); + } + + return $.make('div', $history); + }, + + show_recommended_options_meta: function() { + var $meta_retry = $('.NB-exception-option-retry .NB-exception-option-meta', this.$modal); + var $meta_page = $('.NB-exception-option-page .NB-exception-option-meta', this.$modal); + var $meta_feed = $('.NB-exception-option-feed .NB-exception-option-meta', this.$modal); + var is_400 = (400 <= this.feed.get('exception_code') && this.feed.get('exception_code') < 500); + + if (!is_400) { + $meta_retry.addClass('NB-exception-option-meta-recommended'); + $meta_retry.text('Recommended'); + return; + } + if (this.feed.get('exception_type') == 'feed') { + $meta_page.addClass('NB-exception-option-meta-recommended'); + $meta_page.text('Recommended'); + } + if (this.feed.get('exception_type') == 'page') { + if (is_400) { + $meta_feed.addClass('NB-exception-option-meta-recommended'); + $meta_feed.text('Recommended'); + } else { + $meta_page.addClass('NB-exception-option-meta-recommended'); + $meta_page.text('Recommended'); + } + } + }, + + handle_cancel: function() { + var $cancel = $('.NB-modal-cancel', this.$modal); + + $cancel.click(function(e) { + e.preventDefault(); + $.modal.close(); + }); + }, + + save_retry_feed: function() { + var $loading = $('.NB-modal-loading', this.$modal); + $loading.addClass('NB-active'); + var feed_id = this.feed_id; + + $('.NB-modal-submit-retry', this.$modal).addClass('NB-disabled').attr('value', 'Fetching...'); + + this.model.save_exception_retry(feed_id, function() { + NEWSBLUR.reader.force_feed_refresh(feed_id); + $.modal.close(); + }); + }, + + delete_feed: function() { + var $loading = $('.NB-modal-loading', this.$modal); + $loading.addClass('NB-active'); + + $('.NB-modal-submit-delete', this.$modal).addClass('NB-disabled').attr('value', 'Deleting...'); + + var feed_id = this.feed_id; + + // this.model.delete_feed(feed_id, function() { + NEWSBLUR.reader.manage_menu_delete_feed(feed_id); + _.delay(function() { $.modal.close(); }, 500); + // }); + }, + + change_feed_address: function() { + var feed_id = this.feed_id; + var $loading = $('.NB-modal-loading', this.$modal); + var $feed_address = $('input[name=feed_address]', this.$modal); + var $submit = $('.NB-modal-submit-address', this.$modal); + var $error = $feed_address.closest('.NB-exception-option').find('.NB-error'); + var feed_address = $feed_address.val(); + + $loading.addClass('NB-active'); + $submit.addClass('NB-disabled').attr('value', 'Parsing...'); + $error.hide().html(''); + + if (feed_address.length) { + this.model.save_exception_change_feed_address(feed_id, feed_address, _.bind(function(data) { + console.log(["return to change address", data]); + NEWSBLUR.assets.feeds.add(_.values(data.feeds)); + var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id); + var old_feed = NEWSBLUR.assets.get_feed(feed_id); + if (data.new_feed_id != feed_id && old_feed.get('selected')) { + old_feed.set('selected', false); + } + + if (data && data.new_feed_id) { + NEWSBLUR.assets.load_feeds(function() { + var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id); + console.log(["Loading feed", data.new_feed_id || feed_id, feed]); + NEWSBLUR.reader.open_feed(feed.id); + }); + } + + console.log(["feed address", feed, NEWSBLUR.assets.get_feed(feed_id)]); + if (!data || data.code < 0 || !data.new_feed_id) { + var error = data.message || "There was a problem fetching the feed from this URL."; + if (parseInt(feed.get('exception_code'), 10) == 404) { + error = "URL gives a 404 - page not found."; + } + $error.show().html((data && data.message) || error); + } + $loading.removeClass('NB-active'); + $submit.removeClass('NB-disabled').attr('value', 'Parse this RSS/XML Feed'); + this.populate_settings(data); + }, this)); + } + }, + + change_feed_link: function() { + var feed_id = this.feed_id; + var $feed_link = $('input[name=feed_link]', this.$modal); + var $loading = $('.NB-modal-loading', this.$modal); + var $submit = $('.NB-modal-submit-link', this.$modal); + var $error = $feed_link.closest('.NB-exception-option').find('.NB-error'); + var feed_link = $feed_link.val(); + + $loading.addClass('NB-active'); + $submit.addClass('NB-disabled').attr('value', 'Fetching...'); + $error.hide().html(''); + + if (feed_link.length) { + this.model.save_exception_change_feed_link(feed_id, feed_link, _.bind(function(data) { + var old_feed = NEWSBLUR.assets.get_feed(feed_id); + if (data.new_feed_id != feed_id && old_feed.get('selected')) { + old_feed.set('selected', false); + } + + if (data && data.new_feed_id) { + NEWSBLUR.assets.load_feeds(function() { + var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id); + console.log(["Loading feed", data.new_feed_id || feed_id, feed]); + NEWSBLUR.reader.open_feed(feed.id); + }); + } + + var feed = NEWSBLUR.assets.get_feed(data.new_feed_id) || NEWSBLUR.assets.get_feed(feed_id); + + if (!data || data.code < 0 || !data.new_feed_id) { + var error = data.message || "There was a problem fetching the feed from this URL."; + if (feed.get('exception_code') == '404') { + error = "URL gives a 404 - page not found."; + } + $error.show().html((data && data.message) || error); + } + $loading.removeClass('NB-active'); + $submit.removeClass('NB-disabled').attr('value', 'Fetch Feed from Website'); + this.populate_settings(data); + }, this)); + } + }, + + // =========== + // = Actions = + // =========== + + handle_click: function(elem, e) { + var self = this; + + $.targetIs(e, { tagSelector: '.NB-modal-submit-retry' }, function($t, $p) { + e.preventDefault(); + + self.save_retry_feed(); + }); + $.targetIs(e, { tagSelector: '.NB-modal-submit-delete' }, function($t, $p) { + e.preventDefault(); + + self.delete_feed(); + }); + $.targetIs(e, { tagSelector: '.NB-modal-submit-address' }, function($t, $p) { + e.preventDefault(); + + self.change_feed_address(); + }); + $.targetIs(e, { tagSelector: '.NB-modal-submit-link' }, function($t, $p) { + e.preventDefault(); + + self.change_feed_link(); + }); + $.targetIs(e, { tagSelector: '.NB-premium-only-link' }, function($t, $p){ + e.preventDefault(); + + self.close(function() { + NEWSBLUR.reader.open_feedchooser_modal({premium_only: true}); + }); + }); + }, + + animate_saved: function() { + var $status = $('.NB-exception-option-view .NB-exception-option-status', this.$modal); + $status.text('Saved').animate({ + 'opacity': 1 + }, { + 'queue': false, + 'duration': 600, + 'complete': function() { + _.delay(function() { + $status.animate({'opacity': 0}, {'queue': false, 'duration': 1000}); + }, 300); + } + }); + }, + + animate_saved: function() { + var $status = $('.NB-exception-option-view .NB-exception-option-status', this.$modal); + $status.text('Saved').animate({ + 'opacity': 1 + }, { + 'queue': false, + 'duration': 600, + 'complete': function() { + _.delay(function() { + $status.animate({'opacity': 0}, {'queue': false, 'duration': 1000}); + }, 300); + } + }); + }, + + handle_change: function(elem, e) { + var self = this; + + $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function($t, $p){ + var feed_id = $t.val(); + self.first_load = false; + self.initialize_feed(feed_id); + self.get_feed_settings(); + }); + + $.targetIs(e, { tagSelector: 'input[name=view_settings]' }, function($t, $p){ + if (self.folder) { + self.folder.view_setting({'view': $t.val()}); + } else { + NEWSBLUR.assets.view_setting(self.feed_id, {'view': $t.val()}); + } + self.animate_saved(); + }); + $.targetIs(e, { tagSelector: 'input[name=story_layout]' }, function($t, $p){ + if (self.folder) { + self.folder.view_setting({'layout': $t.val()}); + } else { + NEWSBLUR.assets.view_setting(self.feed_id, {'layout': $t.val()}); + } + self.animate_saved(); + }); + } + +}); \ No newline at end of file diff --git a/utils/mongoengine_fields.py b/utils/mongoengine_fields.py new file mode 100644 index 000000000..c22c5140a --- /dev/null +++ b/utils/mongoengine_fields.py @@ -0,0 +1,111 @@ +from datetime import timedelta +from mongoengine.base import BaseField +from mongoengine.fields import IntField, StringField, EmailField + + +class TimedeltaField(BaseField): + """A timedelta field. + + Looks to the outside world like a datatime.timedelta, but stores + in the database as an integer (or float) number of seconds. + + """ + def validate(self, value): + if not isinstance(value, (timedelta, int, float)): + self.error(u'cannot parse timedelta "%r"' % value) + + def to_mongo(self, value): + return self.prepare_query_value(None, value) + + def to_python(self, value): + return timedelta(seconds=value) + + def prepare_query_value(self, op, value): + if value is None: + return value + if isinstance(value, timedelta): + return self.total_seconds(value) + if isinstance(value, (int, float)): + return value + + @staticmethod + def total_seconds(value): + """Implements Python 2.7's datetime.timedelta.total_seconds() + for backwards compatibility with Python 2.5 and 2.6. + + """ + try: + return value.total_seconds() + except AttributeError: + return (value.days * 24 * 3600) + \ + (value.seconds) + \ + (value.microseconds / 1000000.0) + + +class LowerStringField(StringField): + def __set__(self, instance, value): + value = self.to_python(value) + return super(LowerStringField, self).__set__(instance, value) + + def to_python(self, value): + if value: + value = value.lower() + return value + + def prepare_query_value(self, op, value): + value = value.lower() if value else value + return super(LowerStringField, self).prepare_query_value(op, value) + + +class LowerEmailField(LowerStringField): + + def validate(self, value): + if not EmailField.EMAIL_REGEX.match(value): + self.error('Invalid Mail-address: %s' % value) + super(LowerEmailField, self).validate(value) + + +class EnumField(object): + """ + A class to register Enum type (from the package enum34) into mongo + + :param choices: must be of :class:`enum.Enum`: type + and will be used as possible choices + """ + + def __init__(self, enum, *args, **kwargs): + self.enum = enum + kwargs['choices'] = [choice for choice in enum] + super(EnumField, self).__init__(*args, **kwargs) + + def __get_value(self, enum): + return enum.value if hasattr(enum, 'value') else enum + + def to_python(self, value): + return self.enum(super(EnumField, self).to_python(value)) + + def to_mongo(self, value): + return self.__get_value(value) + + def prepare_query_value(self, op, value): + return super(EnumField, self).prepare_query_value( + op, self.__get_value(value)) + + def validate(self, value): + return super(EnumField, self).validate(self.__get_value(value)) + + def _validate(self, value, **kwargs): + return super(EnumField, self)._validate( + self.enum(self.__get_value(value)), **kwargs) + + +class IntEnumField(EnumField, IntField): + """A variation on :class:`EnumField` for only int containing enumeration. + """ + pass + + +class StringEnumField(EnumField, StringField): + """A variation on :class:`EnumField` for only string containing enumeration. + """ + pass \ No newline at end of file