@@ -345,7 +419,8 @@ class FetchFeed:
%s

""" % (
("https://www.youtube.com/embed/" + video['id']),
- channel_url, username,
+ channel_url,
+ username,
duration,
linkify(linebreaks(video['snippet']['description'])),
thumbnail['url'] if thumbnail else "",
@@ -362,49 +437,52 @@ class FetchFeed:
'pubdate': dateutil.parser.parse(video['snippet']['publishedAt']),
}
rss.add_item(**story_data)
-
+
return rss.writeString('utf-8')
-
-
+
+
class ProcessFeed:
def __init__(self, feed_id, fpf, options, raw_feed=None):
self.feed_id = feed_id
self.options = options
self.fpf = fpf
self.raw_feed = raw_feed
-
+
def refresh_feed(self):
self.feed = Feed.get_by_id(self.feed_id)
if self.feed_id != self.feed.pk:
logging.debug(" ***> Feed has changed: from %s to %s" % (self.feed_id, self.feed.pk))
self.feed_id = self.feed.pk
-
+
def process(self):
- """ Downloads and parses a feed.
- """
+ """Downloads and parses a feed."""
start = time.time()
self.refresh_feed()
-
+
ret_values = dict(new=0, updated=0, same=0, error=0)
if hasattr(self.fpf, 'status'):
if self.options['verbose']:
if self.fpf.bozo and self.fpf.status != 304:
- logging.debug(' ---> [%-30s] ~FRBOZO exception: %s ~SB(%s entries)' % (
- self.feed.log_title[:30], self.fpf.bozo_exception, len(self.fpf.entries)))
-
+ logging.debug(
+ ' ---> [%-30s] ~FRBOZO exception: %s ~SB(%s entries)'
+ % (self.feed.log_title[:30], self.fpf.bozo_exception, len(self.fpf.entries))
+ )
+
if self.fpf.status == 304:
self.feed = self.feed.save()
self.feed.save_feed_history(304, "Not modified")
return FEED_SAME, ret_values
-
+
# 302 and 307: Temporary redirect: ignore
# 301 and 308: Permanent redirect: save it (after 10 tries)
if self.fpf.status == 301 or self.fpf.status == 308:
if self.fpf.href.endswith('feedburner.com/atom.xml'):
return FEED_ERRHTTP, ret_values
redirects, non_redirects = self.feed.count_redirects_in_history('feed')
- self.feed.save_feed_history(self.fpf.status, "HTTP Redirect (%d to go)" % (10-len(redirects)))
+ self.feed.save_feed_history(
+ self.fpf.status, "HTTP Redirect (%d to go)" % (10 - len(redirects))
+ )
if len(redirects) >= 10 or len(non_redirects) == 0:
address = self.fpf.href
if self.options['force'] and address:
@@ -412,16 +490,20 @@ class ProcessFeed:
self.feed.feed_address = address
if not self.feed.known_good:
self.feed.fetched_once = True
- logging.debug(" ---> [%-30s] ~SB~SK~FRFeed is %s'ing. Refetching..." % (
- self.feed.log_title[:30], self.fpf.status))
+ logging.debug(
+ " ---> [%-30s] ~SB~SK~FRFeed is %s'ing. Refetching..."
+ % (self.feed.log_title[:30], self.fpf.status)
+ )
self.feed = self.feed.schedule_feed_fetch_immediately()
if not self.fpf.entries:
self.feed = self.feed.save()
self.feed.save_feed_history(self.fpf.status, "HTTP Redirect")
return FEED_ERRHTTP, ret_values
if self.fpf.status >= 400:
- logging.debug(" ---> [%-30s] ~SB~FRHTTP Status code: %s. Checking address..." % (
- self.feed.log_title[:30], self.fpf.status))
+ logging.debug(
+ " ---> [%-30s] ~SB~FRHTTP Status code: %s. Checking address..."
+ % (self.feed.log_title[:30], self.fpf.status)
+ )
fixed_feed = None
if not self.feed.known_good:
fixed_feed, feed = self.feed.check_feed_link_for_feed_address()
@@ -431,17 +513,21 @@ class ProcessFeed:
self.feed = feed
self.feed = self.feed.save()
return FEED_ERRHTTP, ret_values
-
+
if not self.fpf:
- logging.debug(" ---> [%-30s] ~SB~FRFeed is Non-XML. No feedparser feed either!" % (self.feed.log_title[:30]))
+ logging.debug(
+ " ---> [%-30s] ~SB~FRFeed is Non-XML. No feedparser feed either!"
+ % (self.feed.log_title[:30])
+ )
self.feed.save_feed_history(551, "Broken feed")
return FEED_ERRHTTP, ret_values
if self.fpf and not self.fpf.entries:
if self.fpf.bozo and isinstance(self.fpf.bozo_exception, feedparser.NonXMLContentType):
- logging.debug(" ---> [%-30s] ~SB~FRFeed is Non-XML. %s entries. Checking address..." % (
- self.feed.log_title[:30],
- len(self.fpf.entries)))
+ logging.debug(
+ " ---> [%-30s] ~SB~FRFeed is Non-XML. %s entries. Checking address..."
+ % (self.feed.log_title[:30], len(self.fpf.entries))
+ )
fixed_feed = None
if not self.feed.known_good:
fixed_feed, feed = self.feed.check_feed_link_for_feed_address()
@@ -452,8 +538,10 @@ class ProcessFeed:
self.feed = self.feed.save()
return FEED_ERRPARSE, ret_values
elif self.fpf.bozo and isinstance(self.fpf.bozo_exception, xml.sax._exceptions.SAXException):
- logging.debug(" ---> [%-30s] ~SB~FRFeed has SAX/XML parsing issues. %s entries. Checking address..." % (
- self.feed.log_title[:30], len(self.fpf.entries)))
+ logging.debug(
+ " ---> [%-30s] ~SB~FRFeed has SAX/XML parsing issues. %s entries. Checking address..."
+ % (self.feed.log_title[:30], len(self.fpf.entries))
+ )
fixed_feed = None
if not self.feed.known_good:
fixed_feed, feed = self.feed.check_feed_link_for_feed_address()
@@ -463,7 +551,7 @@ class ProcessFeed:
self.feed = feed
self.feed = self.feed.save()
return FEED_ERRPARSE, ret_values
-
+
# the feed has changed (or it is the first time we parse it)
# saving the etag and last_modified fields
original_etag = self.feed.etag
@@ -475,26 +563,28 @@ class ProcessFeed:
self.feed.etag = ''
if self.feed.etag != original_etag:
self.feed.save(update_fields=['etag'])
-
+
original_last_modified = self.feed.last_modified
if hasattr(self.fpf, 'modified') and self.fpf.modified:
try:
- self.feed.last_modified = datetime.datetime.strptime(self.fpf.modified, '%a, %d %b %Y %H:%M:%S %Z')
+ self.feed.last_modified = datetime.datetime.strptime(
+ self.fpf.modified, '%a, %d %b %Y %H:%M:%S %Z'
+ )
except Exception as e:
self.feed.last_modified = None
logging.debug("Broken mtime %s: %s" % (self.feed.last_modified, e))
pass
if self.feed.last_modified != original_last_modified:
self.feed.save(update_fields=['last_modified'])
-
+
self.fpf.entries = self.fpf.entries[:100]
-
+
original_title = self.feed.feed_title
if self.fpf.feed.get('title'):
self.feed.feed_title = strip_tags(self.fpf.feed.get('title'))
if self.feed.feed_title != original_title:
self.feed.save(update_fields=['feed_title'])
-
+
tagline = self.fpf.feed.get('tagline', self.feed.data.feed_tagline)
if tagline:
original_tagline = self.feed.data.feed_tagline
@@ -507,14 +597,16 @@ class ProcessFeed:
if self.options['force'] and new_feed_link:
new_feed_link = qurl(new_feed_link, remove=['_'])
if new_feed_link != self.feed.feed_link:
- logging.debug(" ---> [%-30s] ~SB~FRFeed's page is different: %s to %s" % (
- self.feed.log_title[:30], self.feed.feed_link, new_feed_link))
+ logging.debug(
+ " ---> [%-30s] ~SB~FRFeed's page is different: %s to %s"
+ % (self.feed.log_title[:30], self.feed.feed_link, new_feed_link)
+ )
redirects, non_redirects = self.feed.count_redirects_in_history('page')
- self.feed.save_page_history(301, "HTTP Redirect (%s to go)" % (10-len(redirects)))
+ self.feed.save_page_history(301, "HTTP Redirect (%s to go)" % (10 - len(redirects)))
if len(redirects) >= 10 or len(non_redirects) == 0:
self.feed.feed_link = new_feed_link
self.feed.save(update_fields=['feed_link'])
-
+
# Determine if stories aren't valid and replace broken guids
guids_seen = set()
permalinks_seen = set()
@@ -527,62 +619,76 @@ class ProcessFeed:
permalink_difference = len(permalinks_seen) != len(self.fpf.entries)
single_permalink = len(permalinks_seen) == 1
replace_permalinks = single_permalink and permalink_difference
-
+
# Compare new stories to existing stories, adding and updating
start_date = datetime.datetime.utcnow()
story_hashes = []
stories = []
for entry in self.fpf.entries:
story = pre_process_story(entry, self.fpf.encoding)
- if not story['title'] and not story['story_content']: continue
+ if not story['title'] and not story['story_content']:
+ continue
if story.get('published') < start_date:
start_date = story.get('published')
if replace_guids:
if replace_permalinks:
new_story_guid = str(story.get('published'))
if self.options['verbose']:
- logging.debug(' ---> [%-30s] ~FBReplacing guid (%s) with timestamp: %s' % (
- self.feed.log_title[:30], story.get('guid'), new_story_guid))
+ logging.debug(
+ ' ---> [%-30s] ~FBReplacing guid (%s) with timestamp: %s'
+ % (self.feed.log_title[:30], story.get('guid'), new_story_guid)
+ )
story['guid'] = new_story_guid
else:
new_story_guid = Feed.get_permalink(story)
if self.options['verbose']:
- logging.debug(' ---> [%-30s] ~FBReplacing guid (%s) with permalink: %s' % (
- self.feed.log_title[:30], story.get('guid'), new_story_guid))
+ logging.debug(
+ ' ---> [%-30s] ~FBReplacing guid (%s) with permalink: %s'
+ % (self.feed.log_title[:30], story.get('guid'), new_story_guid)
+ )
story['guid'] = new_story_guid
story['story_hash'] = MStory.feed_guid_hash_unsaved(self.feed.pk, story.get('guid'))
stories.append(story)
story_hashes.append(story.get('story_hash'))
-
+
original_story_hash_count = len(story_hashes)
story_hashes_in_unread_cutoff = self.feed.story_hashes_in_unread_cutoff[:original_story_hash_count]
story_hashes.extend(story_hashes_in_unread_cutoff)
story_hashes = list(set(story_hashes))
if self.options['verbose'] or settings.DEBUG:
- logging.debug(' ---> [%-30s] ~FBFound ~SB%s~SN guids, adding ~SB%s~SN/%s guids from db' % (
- self.feed.log_title[:30], original_story_hash_count,
- len(story_hashes)-original_story_hash_count,
- len(story_hashes_in_unread_cutoff)))
-
-
- existing_stories = dict((s.story_hash, s) for s in MStory.objects(
- story_hash__in=story_hashes,
- # story_date__gte=start_date,
- # story_feed_id=self.feed.pk
- ))
+ logging.debug(
+ ' ---> [%-30s] ~FBFound ~SB%s~SN guids, adding ~SB%s~SN/%s guids from db'
+ % (
+ self.feed.log_title[:30],
+ original_story_hash_count,
+ len(story_hashes) - original_story_hash_count,
+ len(story_hashes_in_unread_cutoff),
+ )
+ )
+
+ existing_stories = dict(
+ (s.story_hash, s)
+ for s in MStory.objects(
+ story_hash__in=story_hashes,
+ # story_date__gte=start_date,
+ # story_feed_id=self.feed.pk
+ )
+ )
# if len(existing_stories) == 0:
# existing_stories = dict((s.story_hash, s) for s in MStory.objects(
# story_date__gte=start_date,
# story_feed_id=self.feed.pk
# ))
- ret_values = self.feed.add_update_stories(stories, existing_stories,
- verbose=self.options['verbose'],
- updates_off=self.options['updates_off'])
+ ret_values = self.feed.add_update_stories(
+ stories,
+ existing_stories,
+ verbose=self.options['verbose'],
+ updates_off=self.options['updates_off'],
+ )
# PubSubHubbub
- if (hasattr(self.fpf, 'feed') and
- hasattr(self.fpf.feed, 'links') and self.fpf.feed.links):
+ if hasattr(self.fpf, 'feed') and hasattr(self.fpf.feed, 'links') and self.fpf.feed.links:
hub_url = None
self_url = self.feed.feed_address
for link in self.fpf.feed.links:
@@ -596,37 +702,52 @@ class ProcessFeed:
push_expired = self.feed.push.lease_expires < datetime.datetime.now()
except PushSubscription.DoesNotExist:
self.feed.is_push = False
- if (hub_url and self_url and not settings.DEBUG and
- self.feed.active_subscribers > 0 and
- (push_expired or not self.feed.is_push or self.options.get('force'))):
- logging.debug(' ---> [%-30s] ~BB~FW%sSubscribing to PuSH hub: %s' % (
- self.feed.log_title[:30],
- "~SKRe-~SN" if push_expired else "", hub_url))
+ if (
+ hub_url
+ and self_url
+ and not settings.DEBUG
+ and self.feed.active_subscribers > 0
+ and (push_expired or not self.feed.is_push or self.options.get('force'))
+ ):
+ logging.debug(
+ ' ---> [%-30s] ~BB~FW%sSubscribing to PuSH hub: %s'
+ % (self.feed.log_title[:30], "~SKRe-~SN" if push_expired else "", hub_url)
+ )
try:
if settings.ENABLE_PUSH:
PushSubscription.objects.subscribe(self_url, feed=self.feed, hub=hub_url)
except TimeoutError:
- logging.debug(' ---> [%-30s] ~BB~FW~FRTimed out~FW subscribing to PuSH hub: %s' % (
- self.feed.log_title[:30], hub_url))
- elif (self.feed.is_push and
- (self.feed.active_subscribers <= 0 or not hub_url)):
- logging.debug(' ---> [%-30s] ~BB~FWTurning off PuSH, no hub found' % (
- self.feed.log_title[:30]))
+ logging.debug(
+ ' ---> [%-30s] ~BB~FW~FRTimed out~FW subscribing to PuSH hub: %s'
+ % (self.feed.log_title[:30], hub_url)
+ )
+ elif self.feed.is_push and (self.feed.active_subscribers <= 0 or not hub_url):
+ logging.debug(
+ ' ---> [%-30s] ~BB~FWTurning off PuSH, no hub found' % (self.feed.log_title[:30])
+ )
self.feed.is_push = False
self.feed = self.feed.save()
-
+
# Push notifications
if ret_values['new'] > 0 and MUserFeedNotification.feed_has_users(self.feed.pk) > 0:
QueueNotifications.delay(self.feed.pk, ret_values['new'])
-
+
# All Done
- logging.debug(' ---> [%-30s] ~FYParsed Feed: %snew=%s~SN~FY %sup=%s~SN same=%s%s~SN %serr=%s~SN~FY total=~SB%s' % (
- self.feed.log_title[:30],
- '~FG~SB' if ret_values['new'] else '', ret_values['new'],
- '~FY~SB' if ret_values['updated'] else '', ret_values['updated'],
- '~SB' if ret_values['same'] else '', ret_values['same'],
- '~FR~SB' if ret_values['error'] else '', ret_values['error'],
- len(self.fpf.entries)))
+ logging.debug(
+ ' ---> [%-30s] ~FYParsed Feed: %snew=%s~SN~FY %sup=%s~SN same=%s%s~SN %serr=%s~SN~FY total=~SB%s'
+ % (
+ self.feed.log_title[:30],
+ '~FG~SB' if ret_values['new'] else '',
+ ret_values['new'],
+ '~FY~SB' if ret_values['updated'] else '',
+ ret_values['updated'],
+ '~SB' if ret_values['same'] else '',
+ ret_values['same'],
+ '~FR~SB' if ret_values['error'] else '',
+ ret_values['error'],
+ len(self.fpf.entries),
+ )
+ )
self.feed.update_all_statistics(has_new_stories=bool(ret_values['new']), force=self.options['force'])
fetch_date = datetime.datetime.now()
if ret_values['new']:
@@ -638,52 +759,64 @@ class ProcessFeed:
self.feed.save_feed_history(200, "OK", date=fetch_date)
if self.options['verbose']:
- logging.debug(' ---> [%-30s] ~FBTIME: feed parse in ~FM%.4ss' % (
- self.feed.log_title[:30], time.time() - start))
-
+ logging.debug(
+ ' ---> [%-30s] ~FBTIME: feed parse in ~FM%.4ss'
+ % (self.feed.log_title[:30], time.time() - start)
+ )
+
return FEED_OK, ret_values
+
class FeedFetcherWorker:
-
def __init__(self, options):
self.options = options
self.feed_stats = {
- FEED_OK:0,
- FEED_SAME:0,
- FEED_ERRPARSE:0,
- FEED_ERRHTTP:0,
- FEED_ERREXC:0}
+ FEED_OK: 0,
+ FEED_SAME: 0,
+ FEED_ERRPARSE: 0,
+ FEED_ERRHTTP: 0,
+ FEED_ERREXC: 0,
+ }
self.feed_trans = {
- FEED_OK:'ok',
- FEED_SAME:'unchanged',
- FEED_ERRPARSE:'cant_parse',
- FEED_ERRHTTP:'http_error',
- FEED_ERREXC:'exception'}
+ FEED_OK: 'ok',
+ FEED_SAME: 'unchanged',
+ FEED_ERRPARSE: 'cant_parse',
+ FEED_ERRHTTP: 'http_error',
+ FEED_ERREXC: 'exception',
+ }
self.feed_keys = sorted(self.feed_trans.keys())
self.time_start = datetime.datetime.utcnow()
def refresh_feed(self, feed_id):
"""Update feed, since it may have changed"""
return Feed.get_by_id(feed_id)
-
+
def reset_database_connections(self):
connection._connections = {}
- connection._connection_settings ={}
+ connection._connection_settings = {}
connection._dbs = {}
settings.MONGODB = connect(settings.MONGO_DB_NAME, **settings.MONGO_DB)
if 'username' in settings.MONGO_ANALYTICS_DB:
- settings.MONGOANALYTICSDB = connect(db=settings.MONGO_ANALYTICS_DB['name'], host=f"mongodb://{settings.MONGO_ANALYTICS_DB['username']}:{settings.MONGO_ANALYTICS_DB['password']}@{settings.MONGO_ANALYTICS_DB['host']}/?authSource=admin", alias="nbanalytics")
+ settings.MONGOANALYTICSDB = connect(
+ db=settings.MONGO_ANALYTICS_DB['name'],
+ host=f"mongodb://{settings.MONGO_ANALYTICS_DB['username']}:{settings.MONGO_ANALYTICS_DB['password']}@{settings.MONGO_ANALYTICS_DB['host']}/?authSource=admin",
+ alias="nbanalytics",
+ )
else:
- settings.MONGOANALYTICSDB = connect(db=settings.MONGO_ANALYTICS_DB['name'], host=f"mongodb://{settings.MONGO_ANALYTICS_DB['host']}/", alias="nbanalytics")
+ settings.MONGOANALYTICSDB = connect(
+ db=settings.MONGO_ANALYTICS_DB['name'],
+ host=f"mongodb://{settings.MONGO_ANALYTICS_DB['host']}/",
+ alias="nbanalytics",
+ )
def process_feed_wrapper(self, feed_queue):
self.reset_database_connections()
-
+
delta = None
current_process = multiprocessing.current_process()
identity = "X"
feed = None
-
+
if current_process._identity:
identity = current_process._identity[0]
@@ -702,15 +835,20 @@ class FeedFetcherWorker:
try:
feed = self.refresh_feed(feed_id)
set_user({"id": feed_id, "username": feed.feed_title})
-
+
skip = False
if self.options.get('fake'):
skip = True
weight = "-"
quick = "-"
rand = "-"
- elif (self.options.get('quick') and not self.options['force'] and
- feed.known_good and feed.fetched_once and not feed.is_push):
+ elif (
+ self.options.get('quick')
+ and not self.options['force']
+ and feed.known_good
+ and feed.fetched_once
+ and not feed.is_push
+ ):
weight = feed.stories_last_month * feed.num_subscribers
random_weight = random.randint(1, max(weight, 1))
quick = float(self.options.get('quick', 0))
@@ -723,25 +861,24 @@ class FeedFetcherWorker:
quick = "-"
rand = "-"
if skip:
- logging.debug(' ---> [%-30s] ~BGFaking fetch, skipping (%s/month, %s subs, %s < %s)...' % (
- feed.log_title[:30],
- weight,
- feed.num_subscribers,
- rand, quick))
+ logging.debug(
+ ' ---> [%-30s] ~BGFaking fetch, skipping (%s/month, %s subs, %s < %s)...'
+ % (feed.log_title[:30], weight, feed.num_subscribers, rand, quick)
+ )
continue
-
+
ffeed = FetchFeed(feed_id, self.options)
ret_feed, fetched_feed = ffeed.fetch()
feed_fetch_duration = time.time() - start_duration
raw_feed = ffeed.raw_feed
-
- if ((fetched_feed and ret_feed == FEED_OK) or self.options['force']):
+
+ if (fetched_feed and ret_feed == FEED_OK) or self.options['force']:
pfeed = ProcessFeed(feed_id, fetched_feed, self.options, raw_feed=raw_feed)
ret_feed, ret_entries = pfeed.process()
feed = pfeed.feed
feed_process_duration = time.time() - start_duration
-
+
if (ret_entries and ret_entries['new']) or self.options['force']:
start = time.time()
if not feed.known_good or not feed.fetched_once:
@@ -749,23 +886,34 @@ class FeedFetcherWorker:
feed.fetched_once = True
feed = feed.save()
if self.options['force'] or random.random() <= 0.02:
- logging.debug(' ---> [%-30s] ~FBPerforming feed cleanup...' % (feed.log_title[:30],))
+ logging.debug(
+ ' ---> [%-30s] ~FBPerforming feed cleanup...' % (feed.log_title[:30],)
+ )
start_cleanup = time.time()
feed.sync_redis()
- logging.debug(' ---> [%-30s] ~FBDone with feed cleanup. Took ~SB%.4s~SN sec.' % (feed.log_title[:30], time.time() - start_cleanup))
+ logging.debug(
+ ' ---> [%-30s] ~FBDone with feed cleanup. Took ~SB%.4s~SN sec.'
+ % (feed.log_title[:30], time.time() - start_cleanup)
+ )
try:
self.count_unreads_for_subscribers(feed)
except TimeoutError:
- logging.debug(' ---> [%-30s] Unread count took too long...' % (feed.log_title[:30],))
+ logging.debug(
+ ' ---> [%-30s] Unread count took too long...' % (feed.log_title[:30],)
+ )
if self.options['verbose']:
- logging.debug(' ---> [%-30s] ~FBTIME: unread count in ~FM%.4ss' % (
- feed.log_title[:30], time.time() - start))
+ logging.debug(
+ ' ---> [%-30s] ~FBTIME: unread count in ~FM%.4ss'
+ % (feed.log_title[:30], time.time() - start)
+ )
except (urllib.error.HTTPError, urllib.error.URLError) as e:
- logging.debug(' ---> [%-30s] ~FRFeed throws HTTP error: ~SB%s' % (str(feed_id)[:30], e.reason))
+ logging.debug(
+ ' ---> [%-30s] ~FRFeed throws HTTP error: ~SB%s' % (str(feed_id)[:30], e.reason)
+ )
feed_code = 404
feed.save_feed_history(feed_code, str(e.reason), e)
fetched_feed = None
- except Feed.DoesNotExist as e:
+ except Feed.DoesNotExist:
logging.debug(' ---> [%-30s] ~FRFeed is now gone...' % (str(feed_id)[:30]))
continue
except SoftTimeLimitExceeded as e:
@@ -784,19 +932,18 @@ class FeedFetcherWorker:
tb = traceback.format_exc()
logging.error(tb)
logging.debug('[%d] ! -------------------------' % (feed_id,))
- ret_feed = FEED_ERREXC
+ ret_feed = FEED_ERREXC
feed = Feed.get_by_id(getattr(feed, 'pk', feed_id))
- if not feed: continue
+ if not feed:
+ continue
feed.save_feed_history(500, "Error", tb)
feed_code = 500
fetched_feed = None
# mail_feed_error_to_admin(feed, e, local_vars=locals())
- if (not settings.DEBUG and hasattr(settings, 'SENTRY_DSN') and
- settings.SENTRY_DSN):
+ if not settings.DEBUG and hasattr(settings, 'SENTRY_DSN') and settings.SENTRY_DSN:
capture_exception(e)
flush()
-
if not feed_code:
if ret_feed == FEED_OK:
feed_code = 200
@@ -808,29 +955,36 @@ class FeedFetcherWorker:
feed_code = 500
elif ret_feed == FEED_ERRPARSE:
feed_code = 550
-
- if not feed: continue
+
+ if not feed:
+ continue
feed = self.refresh_feed(feed.pk)
- if not feed: continue
-
- if ((self.options['force']) or
- (random.random() > .9) or
- (fetched_feed and
- feed.feed_link and
- feed.has_page and
- (ret_feed == FEED_OK or
- (ret_feed == FEED_SAME and feed.stories_last_month > 10)))):
-
+ if not feed:
+ continue
+
+ if (
+ (self.options['force'])
+ or (random.random() > 0.9)
+ or (
+ fetched_feed
+ and feed.feed_link
+ and feed.has_page
+ and (ret_feed == FEED_OK or (ret_feed == FEED_SAME and feed.stories_last_month > 10))
+ )
+ ):
+
logging.debug(' ---> [%-30s] ~FYFetching page: %s' % (feed.log_title[:30], feed.feed_link))
page_importer = PageImporter(feed)
try:
page_data = page_importer.fetch_page()
page_duration = time.time() - start_duration
except SoftTimeLimitExceeded as e:
- logging.debug(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
+ logging.debug(
+ " ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed
+ )
page_data = None
feed.save_feed_history(557, 'Timeout', e)
- except TimeoutError as e:
+ except TimeoutError:
logging.debug(' ---> [%-30s] ~FRPage fetch timed out...' % (feed.log_title[:30]))
page_data = None
feed.save_page_history(555, 'Timeout', '')
@@ -843,24 +997,25 @@ class FeedFetcherWorker:
fetched_feed = None
page_data = None
# mail_feed_error_to_admin(feed, e, local_vars=locals())
- if (not settings.DEBUG and hasattr(settings, 'SENTRY_DSN') and
- settings.SENTRY_DSN):
+ if not settings.DEBUG and hasattr(settings, 'SENTRY_DSN') and settings.SENTRY_DSN:
capture_exception(e)
flush()
-
+
feed = self.refresh_feed(feed.pk)
logging.debug(' ---> [%-30s] ~FYFetching icon: %s' % (feed.log_title[:30], feed.feed_link))
force = self.options['force']
- if random.random() > .99:
+ if random.random() > 0.99:
force = True
icon_importer = IconImporter(feed, page_data=page_data, force=force)
try:
icon_importer.save()
icon_duration = time.time() - start_duration
except SoftTimeLimitExceeded as e:
- logging.debug(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
+ logging.debug(
+ " ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed
+ )
feed.save_feed_history(558, 'Timeout', e)
- except TimeoutError as e:
+ except TimeoutError:
logging.debug(' ---> [%-30s] ~FRIcon fetch timed out...' % (feed.log_title[:30]))
feed.save_page_history(556, 'Timeout', '')
except Exception as e:
@@ -870,61 +1025,84 @@ class FeedFetcherWorker:
logging.debug('[%d] ! -------------------------' % (feed_id,))
# feed.save_feed_history(560, "Icon Error", tb)
# mail_feed_error_to_admin(feed, e, local_vars=locals())
- if (not settings.DEBUG and hasattr(settings, 'SENTRY_DSN') and
- settings.SENTRY_DSN):
+ if not settings.DEBUG and hasattr(settings, 'SENTRY_DSN') and settings.SENTRY_DSN:
capture_exception(e)
flush()
else:
- logging.debug(' ---> [%-30s] ~FBSkipping page fetch: (%s on %s stories) %s' % (feed.log_title[:30], self.feed_trans[ret_feed], feed.stories_last_month, '' if feed.has_page else ' [HAS NO PAGE]'))
-
+ logging.debug(
+ ' ---> [%-30s] ~FBSkipping page fetch: (%s on %s stories) %s'
+ % (
+ feed.log_title[:30],
+ self.feed_trans[ret_feed],
+ feed.stories_last_month,
+ '' if feed.has_page else ' [HAS NO PAGE]',
+ )
+ )
+
feed = self.refresh_feed(feed.pk)
delta = time.time() - start_time
-
+
feed.last_load_time = round(delta)
feed.fetched_once = True
try:
feed = feed.save(update_fields=['last_load_time', 'fetched_once'])
except IntegrityError:
- logging.debug(" ***> [%-30s] ~FRIntegrityError on feed: %s" % (feed.log_title[:30], feed.feed_address,))
-
+ logging.debug(
+ " ***> [%-30s] ~FRIntegrityError on feed: %s"
+ % (
+ feed.log_title[:30],
+ feed.feed_address,
+ )
+ )
+
if ret_entries and ret_entries['new']:
self.publish_to_subscribers(feed, ret_entries['new'])
-
- done_msg = ('%2s ---> [%-30s] ~FYProcessed in ~FM~SB%.4ss~FY~SN (~FB%s~FY) [%s]' % (
- identity, feed.log_title[:30], delta,
- feed.pk, self.feed_trans[ret_feed],))
+
+ done_msg = '%2s ---> [%-30s] ~FYProcessed in ~FM~SB%.4ss~FY~SN (~FB%s~FY) [%s]' % (
+ identity,
+ feed.log_title[:30],
+ delta,
+ feed.pk,
+ self.feed_trans[ret_feed],
+ )
logging.debug(done_msg)
total_duration = time.time() - start_duration
- MAnalyticsFetcher.add(feed_id=feed.pk, feed_fetch=feed_fetch_duration,
- feed_process=feed_process_duration,
- page=page_duration, icon=icon_duration,
- total=total_duration, feed_code=feed_code)
-
+ MAnalyticsFetcher.add(
+ feed_id=feed.pk,
+ feed_fetch=feed_fetch_duration,
+ feed_process=feed_process_duration,
+ page=page_duration,
+ icon=icon_duration,
+ total=total_duration,
+ feed_code=feed_code,
+ )
+
self.feed_stats[ret_feed] += 1
-
+
if len(feed_queue) == 1:
return feed
-
+
# time_taken = datetime.datetime.utcnow() - self.time_start
-
+
def publish_to_subscribers(self, feed, new_count):
try:
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
listeners_count = r.publish(str(feed.pk), 'story:new_count:%s' % new_count)
if listeners_count:
- logging.debug(" ---> [%-30s] ~FMPublished to %s subscribers" % (feed.log_title[:30], listeners_count))
+ logging.debug(
+ " ---> [%-30s] ~FMPublished to %s subscribers" % (feed.log_title[:30], listeners_count)
+ )
except redis.ConnectionError:
logging.debug(" ***> [%-30s] ~BMRedis is unavailable for real-time." % (feed.log_title[:30],))
-
+
def count_unreads_for_subscribers(self, feed):
- user_subs = UserSubscription.objects.filter(feed=feed,
- active=True,
- user__profile__last_seen_on__gte=feed.unread_cutoff)\
- .order_by('-last_read_date')
-
+ user_subs = UserSubscription.objects.filter(
+ feed=feed, active=True, user__profile__last_seen_on__gte=feed.unread_cutoff
+ ).order_by('-last_read_date')
+
if not user_subs.count():
return
-
+
for sub in user_subs:
if not sub.needs_unread_recalc:
sub.needs_unread_recalc = True
@@ -932,34 +1110,55 @@ class FeedFetcherWorker:
if self.options['compute_scores']:
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
- stories = MStory.objects(story_feed_id=feed.pk,
- story_date__gte=feed.unread_cutoff)
+ stories = MStory.objects(story_feed_id=feed.pk, story_date__gte=feed.unread_cutoff)
stories = Feed.format_stories(stories, feed.pk)
- story_hashes = r.zrangebyscore('zF:%s' % feed.pk, int(feed.unread_cutoff.strftime('%s')),
- int(time.time() + 60*60*24))
+ story_hashes = r.zrangebyscore(
+ 'zF:%s' % feed.pk,
+ int(feed.unread_cutoff.strftime('%s')),
+ int(time.time() + 60 * 60 * 24),
+ )
missing_story_hashes = set(story_hashes) - set([s['story_hash'] for s in stories])
if missing_story_hashes:
- missing_stories = MStory.objects(story_feed_id=feed.pk,
- story_hash__in=missing_story_hashes)\
- .read_preference(pymongo.ReadPreference.PRIMARY)
+ missing_stories = MStory.objects(
+ story_feed_id=feed.pk, story_hash__in=missing_story_hashes
+ ).read_preference(pymongo.ReadPreference.PRIMARY)
missing_stories = Feed.format_stories(missing_stories, feed.pk)
stories = missing_stories + stories
- logging.debug(' ---> [%-30s] ~FYFound ~SB~FC%s(of %s)/%s~FY~SN un-secondaried stories while computing scores' % (feed.log_title[:30], len(missing_stories), len(missing_story_hashes), len(stories)))
+ logging.debug(
+ ' ---> [%-30s] ~FYFound ~SB~FC%s(of %s)/%s~FY~SN un-secondaried stories while computing scores'
+ % (
+ feed.log_title[:30],
+ len(missing_stories),
+ len(missing_story_hashes),
+ len(stories),
+ )
+ )
cache.set("S:v3:%s" % feed.pk, stories, 60)
- logging.debug(' ---> [%-30s] ~FYComputing scores: ~SB%s stories~SN with ~SB%s subscribers ~SN(%s/%s/%s)' % (
- feed.log_title[:30], len(stories), user_subs.count(),
- feed.num_subscribers, feed.active_subscribers, feed.premium_subscribers))
+ logging.debug(
+ ' ---> [%-30s] ~FYComputing scores: ~SB%s stories~SN with ~SB%s subscribers ~SN(%s/%s/%s)'
+ % (
+ feed.log_title[:30],
+ len(stories),
+ user_subs.count(),
+ feed.num_subscribers,
+ feed.active_subscribers,
+ feed.premium_subscribers,
+ )
+ )
self.calculate_feed_scores_with_stories(user_subs, stories)
elif self.options.get('mongodb_replication_lag'):
- logging.debug(' ---> [%-30s] ~BR~FYSkipping computing scores: ~SB%s seconds~SN of mongodb lag' % (
- feed.log_title[:30], self.options.get('mongodb_replication_lag')))
-
+ logging.debug(
+ ' ---> [%-30s] ~BR~FYSkipping computing scores: ~SB%s seconds~SN of mongodb lag'
+ % (feed.log_title[:30], self.options.get('mongodb_replication_lag'))
+ )
+
@timelimit(10)
def calculate_feed_scores_with_stories(self, user_subs, stories):
for sub in user_subs:
silent = False if getattr(self.options, 'verbose', 0) >= 2 else True
sub.calculate_feed_scores(silent=silent, stories=stories)
+
class Dispatcher:
def __init__(self, options, num_threads):
self.options = options
@@ -967,22 +1166,23 @@ class Dispatcher:
self.workers = []
def add_jobs(self, feeds_queue, feeds_count=1):
- """ adds a feed processing job to the pool
- """
+ """adds a feed processing job to the pool"""
self.feeds_queue = feeds_queue
self.feeds_count = feeds_count
-
+
def run_jobs(self):
if self.options['single_threaded'] or self.num_threads == 1:
return dispatch_workers(self.feeds_queue[0], self.options)
else:
for i in range(self.num_threads):
feed_queue = self.feeds_queue[i]
- self.workers.append(multiprocessing.Process(target=dispatch_workers,
- args=(feed_queue, self.options)))
+ self.workers.append(
+ multiprocessing.Process(target=dispatch_workers, args=(feed_queue, self.options))
+ )
for i in range(self.num_threads):
self.workers[i].start()
+
def dispatch_workers(feed_queue, options):
worker = FeedFetcherWorker(options)
return worker.process_feed_wrapper(feed_queue)
From d2097c333168da315c7f195913e8f0a3a15503ec Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Wed, 2 Mar 2022 10:58:15 -0500
Subject: [PATCH 46/65] Black formatting
---
apps/profile/middleware.py | 402 ++++++++++++++++++++++++++-----------
1 file changed, 288 insertions(+), 114 deletions(-)
diff --git a/apps/profile/middleware.py b/apps/profile/middleware.py
index 3496ba572..7ee736e48 100644
--- a/apps/profile/middleware.py
+++ b/apps/profile/middleware.py
@@ -11,32 +11,41 @@ from django.template import Template, Context
from apps.statistics.rstats import round_time
from utils import json_functions as json
+
class LastSeenMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
def process_response(self, request, response):
- if ((request.path == '/' or
- request.path.startswith('/reader/refresh_feeds') or
- request.path.startswith('/reader/load_feeds') or
- request.path.startswith('/reader/feeds'))
+ if (
+ (
+ request.path == '/'
+ or request.path.startswith('/reader/refresh_feeds')
+ or request.path.startswith('/reader/load_feeds')
+ or request.path.startswith('/reader/feeds')
+ )
and hasattr(request, 'user')
- and request.user.is_authenticated):
+ and request.user.is_authenticated
+ ):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=60)
ip = request.META.get('HTTP_X_FORWARDED_FOR', None) or request.META['REMOTE_ADDR']
if request.user.profile.last_seen_on < hour_ago:
- logging.user(request, "~FG~BBRepeat visitor: ~SB%s (%s)" % (
- request.user.profile.last_seen_on, ip))
+ logging.user(
+ request, "~FG~BBRepeat visitor: ~SB%s (%s)" % (request.user.profile.last_seen_on, ip)
+ )
from apps.profile.tasks import CleanupUser
+
CleanupUser.delay(user_id=request.user.pk)
elif settings.DEBUG:
- logging.user(request, "~FG~BBRepeat visitor (ignored): ~SB%s (%s)" % (
- request.user.profile.last_seen_on, ip))
+ logging.user(
+ request,
+ "~FG~BBRepeat visitor (ignored): ~SB%s (%s)" % (request.user.profile.last_seen_on, ip),
+ )
request.user.profile.last_seen_on = datetime.datetime.utcnow()
request.user.profile.last_seen_ip = ip[-15:]
request.user.profile.save()
-
+
return response
def __call__(self, request):
@@ -50,29 +59,30 @@ class LastSeenMiddleware(object):
return response
+
class DBProfilerMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
- def process_request(self, request):
+ def process_request(self, request):
setattr(request, 'activated_segments', [])
- if ((request.path.startswith('/reader/feed') or
- request.path.startswith('/reader/river')) and
- random.random() < .01):
+ if (
+ request.path.startswith('/reader/feed') or request.path.startswith('/reader/river')
+ ) and random.random() < 0.01:
request.activated_segments.append('db_profiler')
connection.use_debug_cursor = True
setattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG)
settings.DEBUG = True
- def process_celery(self):
- setattr(self, 'activated_segments', [])
- if random.random() < .01:
+ def process_celery(self):
+ setattr(self, 'activated_segments', [])
+ if random.random() < 0.01:
self.activated_segments.append('db_profiler')
connection.use_debug_cursor = True
setattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG)
settings.DEBUG = True
return self
-
+
def process_exception(self, request, exception):
if hasattr(request, 'sql_times_elapsed'):
self._save_times(request.sql_times_elapsed)
@@ -84,25 +94,25 @@ class DBProfilerMiddleware:
# logging.debug(" ---> ~FGProfiling~FB app: %s" % request.sql_times_elapsed)
self._save_times(request.sql_times_elapsed)
return response
-
+
def process_celery_finished(self):
middleware = SQLLogToConsoleMiddleware()
middleware.process_celery(self)
if hasattr(self, 'sql_times_elapsed'):
logging.debug(" ---> ~FGProfiling~FB task: %s" % self.sql_times_elapsed)
self._save_times(self.sql_times_elapsed, 'task_')
-
+
def process_request_finished(self):
middleware = SQLLogToConsoleMiddleware()
middleware.process_celery(self)
if hasattr(self, 'sql_times_elapsed'):
logging.debug(" ---> ~FGProfiling~FB app: %s" % self.sql_times_elapsed)
self._save_times(self.sql_times_elapsed, 'app_')
-
+
def _save_times(self, db_times, prefix=""):
- if not db_times:
+ if not db_times:
return
-
+
r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL)
pipe = r.pipeline()
minute = round_time(round_to=60)
@@ -126,17 +136,19 @@ class DBProfilerMiddleware:
return response
+
class SQLLogToConsoleMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
def activated(self, request):
- return (settings.DEBUG_QUERIES or
- (hasattr(request, 'activated_segments') and
- 'db_profiler' in request.activated_segments))
+ return settings.DEBUG_QUERIES or (
+ hasattr(request, 'activated_segments') and 'db_profiler' in request.activated_segments
+ )
- def process_response(self, request, response):
- if not self.activated(request): return response
+ def process_response(self, request, response):
+ if not self.activated(request):
+ return response
if connection.queries:
time_elapsed = sum([float(q['time']) for q in connection.queries])
queries = connection.queries
@@ -164,20 +176,37 @@ class SQLLogToConsoleMiddleware:
query['sql'] = re.sub(r'INSERT', '~FGINSERT', query['sql'])
query['sql'] = re.sub(r'UPDATE', '~FY~SBUPDATE', query['sql'])
query['sql'] = re.sub(r'DELETE', '~FR~SBDELETE', query['sql'])
- if settings.DEBUG and settings.DEBUG_QUERIES and not getattr(settings, 'DEBUG_QUERIES_SUMMARY_ONLY', False):
- t = Template("{% for sql in sqllog %}{% if not forloop.first %} {% endif %}[{{forloop.counter}}] ~FC{{sql.time}}s~FW: {{sql.sql|safe}}{% if not forloop.last %}\n{% endif %}{% endfor %}")
- logging.debug(t.render(Context({
- 'sqllog': queries,
- 'count': len(queries),
- 'time': time_elapsed,
- })))
+ if (
+ settings.DEBUG
+ and settings.DEBUG_QUERIES
+ and not getattr(settings, 'DEBUG_QUERIES_SUMMARY_ONLY', False)
+ ):
+ t = Template(
+ "{% for sql in sqllog %}{% if not forloop.first %} {% endif %}[{{forloop.counter}}] ~FC{{sql.time}}s~FW: {{sql.sql|safe}}{% if not forloop.last %}\n{% endif %}{% endfor %}"
+ )
+ logging.debug(
+ t.render(
+ Context(
+ {
+ 'sqllog': queries,
+ 'count': len(queries),
+ 'time': time_elapsed,
+ }
+ )
+ )
+ )
times_elapsed = {
- 'sql': sum([float(q['time'])
- for q in queries if not q.get('mongo') and
- not q.get('redis_user') and
- not q.get('redis_story') and
- not q.get('redis_session') and
- not q.get('redis_pubsub')]),
+ 'sql': sum(
+ [
+ float(q['time'])
+ for q in queries
+ if not q.get('mongo')
+ and not q.get('redis_user')
+ and not q.get('redis_story')
+ and not q.get('redis_session')
+ and not q.get('redis_pubsub')
+ ]
+ ),
'mongo': sum([float(q['time']) for q in queries if q.get('mongo')]),
'redis_user': sum([float(q['time']) for q in queries if q.get('redis_user')]),
'redis_story': sum([float(q['time']) for q in queries if q.get('redis_story')]),
@@ -191,7 +220,7 @@ class SQLLogToConsoleMiddleware:
settings.DEBUG = False
return response
-
+
def process_celery(self, profiler):
self.process_response(profiler, None)
if not getattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG):
@@ -208,110 +237,247 @@ class SQLLogToConsoleMiddleware:
return response
+
SIMPSONS_QUOTES = [
("Homer", "D'oh."),
("Ralph", "Me fail English? That's unpossible."),
- ("Lionel Hutz", "This is the greatest case of false advertising I've seen since I sued the movie \"The Never Ending Story.\""),
+ (
+ "Lionel Hutz",
+ "This is the greatest case of false advertising I've seen since I sued the movie \"The Never Ending Story.\"",
+ ),
("Sideshow Bob", "No children have ever meddled with the Republican Party and lived to tell about it."),
- ("Troy McClure", "Don't kid yourself, Jimmy. If a cow ever got the chance, he'd eat you and everyone you care about!"),
+ (
+ "Troy McClure",
+ "Don't kid yourself, Jimmy. If a cow ever got the chance, he'd eat you and everyone you care about!",
+ ),
("Comic Book Guy", "The Internet King? I wonder if he could provide faster nudity..."),
("Homer", "Oh, so they have Internet on computers now!"),
- ("Ned Flanders", "I've done everything the Bible says - even the stuff that contradicts the other stuff!"),
- ("Comic Book Guy", "Your questions have become more redundant and annoying than the last three \"Highlander\" movies."),
+ (
+ "Ned Flanders",
+ "I've done everything the Bible says - even the stuff that contradicts the other stuff!",
+ ),
+ (
+ "Comic Book Guy",
+ "Your questions have become more redundant and annoying than the last three \"Highlander\" movies.",
+ ),
("Chief Wiggum", "Uh, no, you got the wrong number. This is 9-1...2."),
- ("Sideshow Bob", "I'll be back. You can't keep the Democrats out of the White House forever, and when they get in, I'm back on the streets, with all my criminal buddies."),
- ("Homer", "When I held that gun in my hand, I felt a surge of power...like God must feel when he's holding a gun."),
- ("Nelson", "Dad didn't leave... When he comes back from the store, he's going to wave those pop-tarts right in your face!"),
- ("Milhouse", "Remember the time he ate my goldfish? And you lied and said I never had goldfish. Then why did I have the bowl, Bart? *Why did I have the bowl?*"),
- ("Lionel Hutz", "Well, he's kind of had it in for me ever since I accidentally ran over his dog. Actually, replace \"accidentally\" with \"repeatedly\" and replace \"dog\" with \"son.\""),
- ("Comic Book Guy", "Last night's \"Itchy and Scratchy Show\" was, without a doubt, the worst episode *ever.* Rest assured, I was on the Internet within minutes, registering my disgust throughout the world."),
+ (
+ "Sideshow Bob",
+ "I'll be back. You can't keep the Democrats out of the White House forever, and when they get in, I'm back on the streets, with all my criminal buddies.",
+ ),
+ (
+ "Homer",
+ "When I held that gun in my hand, I felt a surge of power...like God must feel when he's holding a gun.",
+ ),
+ (
+ "Nelson",
+ "Dad didn't leave... When he comes back from the store, he's going to wave those pop-tarts right in your face!",
+ ),
+ (
+ "Milhouse",
+ "Remember the time he ate my goldfish? And you lied and said I never had goldfish. Then why did I have the bowl, Bart? *Why did I have the bowl?*",
+ ),
+ (
+ "Lionel Hutz",
+ "Well, he's kind of had it in for me ever since I accidentally ran over his dog. Actually, replace \"accidentally\" with \"repeatedly\" and replace \"dog\" with \"son.\"",
+ ),
+ (
+ "Comic Book Guy",
+ "Last night's \"Itchy and Scratchy Show\" was, without a doubt, the worst episode *ever.* Rest assured, I was on the Internet within minutes, registering my disgust throughout the world.",
+ ),
("Homer", "I'm normally not a praying man, but if you're up there, please save me, Superman."),
("Homer", "Save me, Jeebus."),
("Mayor Quimby", "I stand by my racial slur."),
("Comic Book Guy", "Oh, loneliness and cheeseburgers are a dangerous mix."),
- ("Homer", "You don't like your job, you don't strike. You go in every day and do it really half-assed. That's the American way."),
- ("Chief Wiggum", "Fat Tony is a cancer on this fair city! He is the cancer and I am the...uh...what cures cancer?"),
- ("Homer", "Bart, with $10,000 we'd be millionaires! We could buy all kinds of useful things like...love!"),
+ (
+ "Homer",
+ "You don't like your job, you don't strike. You go in every day and do it really half-assed. That's the American way.",
+ ),
+ (
+ "Chief Wiggum",
+ "Fat Tony is a cancer on this fair city! He is the cancer and I am the...uh...what cures cancer?",
+ ),
+ (
+ "Homer",
+ "Bart, with $10,000 we'd be millionaires! We could buy all kinds of useful things like...love!",
+ ),
("Homer", "Fame was like a drug. But what was even more like a drug were the drugs."),
- ("Homer", "Books are useless! I only ever read one book, \"To Kill A Mockingbird,\" and it gave me absolutely no insight on how to kill mockingbirds! Sure it taught me not to judge a man by the color of his skin...but what good does *that* do me?"),
- ("Chief Wiggum", "Can't you people take the law into your own hands? I mean, we can't be policing the entire city!"),
- ("Homer", "Weaseling out of things is important to learn. It's what separates us from the animals...except the weasel."),
- ("Reverend Lovejoy", "Marge, just about everything's a sin. [holds up a Bible] Y'ever sat down and read this thing? Technically we're not supposed to go to the bathroom."),
- ("Homer", "You know, the one with all the well meaning rules that don't work out in real life, uh, Christianity."),
+ (
+ "Homer",
+ "Books are useless! I only ever read one book, \"To Kill A Mockingbird,\" and it gave me absolutely no insight on how to kill mockingbirds! Sure it taught me not to judge a man by the color of his skin...but what good does *that* do me?",
+ ),
+ (
+ "Chief Wiggum",
+ "Can't you people take the law into your own hands? I mean, we can't be policing the entire city!",
+ ),
+ (
+ "Homer",
+ "Weaseling out of things is important to learn. It's what separates us from the animals...except the weasel.",
+ ),
+ (
+ "Reverend Lovejoy",
+ "Marge, just about everything's a sin. [holds up a Bible] Y'ever sat down and read this thing? Technically we're not supposed to go to the bathroom.",
+ ),
+ (
+ "Homer",
+ "You know, the one with all the well meaning rules that don't work out in real life, uh, Christianity.",
+ ),
("Smithers", "Uh, no, they're saying \"Boo-urns, Boo-urns.\""),
("Hans Moleman", "I was saying \"Boo-urns.\""),
("Homer", "Kids, you tried your best and you failed miserably. The lesson is, never try."),
("Homer", "Here's to alcohol, the cause of - and solution to - all life's problems."),
- ("Homer", "When will I learn? The answers to life's problems aren't at the bottom of a bottle, they're on TV!"),
+ (
+ "Homer",
+ "When will I learn? The answers to life's problems aren't at the bottom of a bottle, they're on TV!",
+ ),
("Chief Wiggum", "I hope this has taught you kids a lesson: kids never learn."),
- ("Homer", "How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?"),
+ (
+ "Homer",
+ "How is education supposed to make me feel smarter? Besides, every time I learn something new, it pushes some old stuff out of my brain. Remember when I took that home winemaking course, and I forgot how to drive?",
+ ),
("Homer", "Homer no function beer well without."),
("Duffman", "Duffman can't breathe! OH NO!"),
- ("Grandpa Simpson", "Dear Mr. President, There are too many states nowadays. Please, eliminate three. P.S. I am not a crackpot."),
- ("Homer", "Old people don't need companionship. They need to be isolated and studied so it can be determined what nutrients they have that might be extracted for our personal use."),
- ("Troy McClure", "Hi. I'm Troy McClure. You may remember me from such self-help tapes as \"Smoke Yourself Thin\" and \"Get Some Confidence, Stupid!\""),
+ (
+ "Grandpa Simpson",
+ "Dear Mr. President, There are too many states nowadays. Please, eliminate three. P.S. I am not a crackpot.",
+ ),
+ (
+ "Homer",
+ "Old people don't need companionship. They need to be isolated and studied so it can be determined what nutrients they have that might be extracted for our personal use.",
+ ),
+ (
+ "Troy McClure",
+ "Hi. I'm Troy McClure. You may remember me from such self-help tapes as \"Smoke Yourself Thin\" and \"Get Some Confidence, Stupid!\"",
+ ),
("Homer", "A woman is a lot like a refrigerator. Six feet tall, 300 pounds...it makes ice."),
- ("Homer", "Son, a woman is like a beer. They smell good, they look good, you'd step over your own mother just to get one! But you can't stop at one. You wanna drink another woman!"),
+ (
+ "Homer",
+ "Son, a woman is like a beer. They smell good, they look good, you'd step over your own mother just to get one! But you can't stop at one. You wanna drink another woman!",
+ ),
("Homer", "Facts are meaningless. You could use facts to prove anything that's even remotely true!"),
- ("Mr Burns", "I'll keep it short and sweet - Family. Religion. Friendship. These are the three demons you must slay if you wish to succeed in business."),
- ("Kent Brockman", "...And the fluffy kitten played with that ball of string all through the night. On a lighter note, a Kwik-E-Mart clerk was brutally murdered last night."),
- ("Ralph", "Mrs. Krabappel and Principal Skinner were in the closet making babies and I saw one of the babies and then the baby looked at me."),
+ (
+ "Mr Burns",
+ "I'll keep it short and sweet - Family. Religion. Friendship. These are the three demons you must slay if you wish to succeed in business.",
+ ),
+ (
+ "Kent Brockman",
+ "...And the fluffy kitten played with that ball of string all through the night. On a lighter note, a Kwik-E-Mart clerk was brutally murdered last night.",
+ ),
+ (
+ "Ralph",
+ "Mrs. Krabappel and Principal Skinner were in the closet making babies and I saw one of the babies and then the baby looked at me.",
+ ),
("Apu", "Please do not offer my god a peanut."),
("Homer", "You don't win friends with salad."),
("Mr Burns", "I don't like being outdoors, Smithers. For one thing, there's too many fat children."),
- ("Sideshow Bob", "Attempted murder? Now honestly, what is that? Do they give a Nobel Prize for attempted chemistry?"),
+ (
+ "Sideshow Bob",
+ "Attempted murder? Now honestly, what is that? Do they give a Nobel Prize for attempted chemistry?",
+ ),
("Chief Wiggum", "They only come out in the night. Or in this case, the day."),
("Mr Burns", "Whoa, slow down there, maestro. There's a *New* Mexico?"),
("Homer", "He didn't give you gay, did he? Did he?!"),
- ("Comic Book Guy", "But, Aquaman, you cannot marry a woman without gills. You're from two different worlds... Oh, I've wasted my life."),
+ (
+ "Comic Book Guy",
+ "But, Aquaman, you cannot marry a woman without gills. You're from two different worlds... Oh, I've wasted my life.",
+ ),
("Homer", "Marge, it takes two to lie. One to lie and one to listen."),
- ("Superintendent Chalmers", "I've had it with this school, Skinner. Low test scores, class after class of ugly, ugly children..."),
+ (
+ "Superintendent Chalmers",
+ "I've had it with this school, Skinner. Low test scores, class after class of ugly, ugly children...",
+ ),
("Mr Burns", "What good is money if it can't inspire terror in your fellow man?"),
("Homer", "Oh, everything looks bad if you remember it."),
("Ralph", "Slow down, Bart! My legs don't know how to be as long as yours."),
("Homer", "Donuts. Is there anything they can't do?"),
- ("Frink", "Brace yourselves gentlemen. According to the gas chromatograph, the secret ingredient is... Love!? Who's been screwing with this thing?"),
- ("Apu", "Yes! I am a citizen! Now which way to the welfare office? I'm kidding, I'm kidding. I work, I work."),
+ (
+ "Frink",
+ "Brace yourselves gentlemen. According to the gas chromatograph, the secret ingredient is... Love!? Who's been screwing with this thing?",
+ ),
+ (
+ "Apu",
+ "Yes! I am a citizen! Now which way to the welfare office? I'm kidding, I'm kidding. I work, I work.",
+ ),
("Milhouse", "We started out like Romeo and Juliet, but it ended up in tragedy."),
- ("Mr Burns", "A lifetime of working with nuclear power has left me with a healthy green glow...and left me as impotent as a Nevada boxing commissioner."),
+ (
+ "Mr Burns",
+ "A lifetime of working with nuclear power has left me with a healthy green glow...and left me as impotent as a Nevada boxing commissioner.",
+ ),
("Homer", "Kids, kids. I'm not going to die. That only happens to bad people."),
("Milhouse", "Look out, Itchy! He's Irish!"),
- ("Homer", "I'm going to the back seat of my car, with the woman I love, and I won't be back for ten minutes!"),
+ (
+ "Homer",
+ "I'm going to the back seat of my car, with the woman I love, and I won't be back for ten minutes!",
+ ),
("Smithers", "I'm allergic to bee stings. They cause me to, uh, die."),
("Barney", "Aaah! Natural light! Get it off me! Get it off me!"),
- ("Principal Skinner", "That's why I love elementary school, Edna. The children believe anything you tell them."),
- ("Sideshow Bob", "Your guilty consciences may make you vote Democratic, but secretly you all yearn for a Republican president to lower taxes, brutalize criminals, and rule you like a king!"),
+ (
+ "Principal Skinner",
+ "That's why I love elementary school, Edna. The children believe anything you tell them.",
+ ),
+ (
+ "Sideshow Bob",
+ "Your guilty consciences may make you vote Democratic, but secretly you all yearn for a Republican president to lower taxes, brutalize criminals, and rule you like a king!",
+ ),
("Barney", "Jesus must be spinning in his grave!"),
- ("Superintendent Chalmers", "\"Thank the Lord\"? That sounded like a prayer. A prayer in a public school. God has no place within these walls, just like facts don't have a place within an organized religion."),
+ (
+ "Superintendent Chalmers",
+ "\"Thank the Lord\"? That sounded like a prayer. A prayer in a public school. God has no place within these walls, just like facts don't have a place within an organized religion.",
+ ),
("Mr Burns", "[answering the phone] Ahoy hoy?"),
("Comic Book Guy", "Oh, a *sarcasm* detector. Oh, that's a *really* useful invention!"),
("Marge", "Our differences are only skin deep, but our sames go down to the bone."),
("Homer", "What's the point of going out? We're just going to wind up back here anyway."),
("Marge", "Get ready, skanks! It's time for the truth train!"),
("Bill Gates", "I didn't get rich by signing checks."),
- ("Principal Skinner", "Fire can be our friend; whether it's toasting marshmallows or raining down on Charlie."),
- ("Homer", "Oh, I'm in no condition to drive. Wait a minute. I don't have to listen to myself. I'm drunk."),
+ (
+ "Principal Skinner",
+ "Fire can be our friend; whether it's toasting marshmallows or raining down on Charlie.",
+ ),
+ (
+ "Homer",
+ "Oh, I'm in no condition to drive. Wait a minute. I don't have to listen to myself. I'm drunk.",
+ ),
("Homer", "And here I am using my own lungs like a sucker."),
("Comic Book Guy", "Human contact: the final frontier."),
("Homer", "I hope I didn't brain my damage."),
- ("Krusty the Clown", "And now, in the spirit of the season: start shopping. And for every dollar of Krusty merchandise you buy, I will be nice to a sick kid. For legal purposes, sick kids may include hookers with a cold."),
+ (
+ "Krusty the Clown",
+ "And now, in the spirit of the season: start shopping. And for every dollar of Krusty merchandise you buy, I will be nice to a sick kid. For legal purposes, sick kids may include hookers with a cold.",
+ ),
("Homer", "I'm a Spalding Gray in a Rick Dees world."),
("Dr Nick", "Inflammable means flammable? What a country."),
("Homer", "Beer. Now there's a temporary solution."),
("Comic Book Guy", "Stan Lee never left. I'm afraid his mind is no longer in mint condition."),
("Nelson", "Shoplifting is a victimless crime. Like punching someone in the dark."),
- ("Krusty the Clown", "Kids, we need to talk for a moment about Krusty Brand Chew Goo Gum Like Substance. We all knew it contained spider eggs, but the hantavirus? That came out of left field. So if you're experiencing numbness and/or comas, send five dollars to antidote, PO box..."),
+ (
+ "Krusty the Clown",
+ "Kids, we need to talk for a moment about Krusty Brand Chew Goo Gum Like Substance. We all knew it contained spider eggs, but the hantavirus? That came out of left field. So if you're experiencing numbness and/or comas, send five dollars to antidote, PO box...",
+ ),
("Milhouse", "I can't go to juvie. They use guys like me as currency."),
- ("Homer", "Son, when you participate in sporting events, it's not whether you win or lose: it's how drunk you get."),
+ (
+ "Homer",
+ "Son, when you participate in sporting events, it's not whether you win or lose: it's how drunk you get.",
+ ),
("Homer", "I like my beer cold, my TV loud and my homosexuals flaming."),
("Apu", "Thank you, steal again."),
- ("Homer", "Marge, you being a cop makes you the man! Which makes me the woman - and I have no interest in that, besides occasionally wearing the underwear, which as we discussed, is strictly a comfort thing."),
- ("Ed Begley Jr", "I prefer a vehicle that doesn't hurt Mother Earth. It's a go-cart, powered by my own sense of self-satisfaction."),
+ (
+ "Homer",
+ "Marge, you being a cop makes you the man! Which makes me the woman - and I have no interest in that, besides occasionally wearing the underwear, which as we discussed, is strictly a comfort thing.",
+ ),
+ (
+ "Ed Begley Jr",
+ "I prefer a vehicle that doesn't hurt Mother Earth. It's a go-cart, powered by my own sense of self-satisfaction.",
+ ),
("Bart", "I didn't think it was physically possible, but this both sucks *and* blows."),
- ("Homer", "How could you?! Haven't you learned anything from that guy who gives those sermons at church? Captain Whatshisname? We live in a society of laws! Why do you think I took you to all those Police Academy movies? For fun? Well, I didn't hear anybody laughing, did you? Except at that guy who made sound effects. Makes sound effects and laughs. Where was I? Oh yeah! Stay out of my booze."),
+ (
+ "Homer",
+ "How could you?! Haven't you learned anything from that guy who gives those sermons at church? Captain Whatshisname? We live in a society of laws! Why do you think I took you to all those Police Academy movies? For fun? Well, I didn't hear anybody laughing, did you? Except at that guy who made sound effects. Makes sound effects and laughs. Where was I? Oh yeah! Stay out of my booze.",
+ ),
("Homer", "Lisa, vampires are make-believe, like elves, gremlins, and Eskimos."),
]
+
class SimpsonsMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
@@ -333,9 +499,9 @@ class SimpsonsMiddleware:
response = self.process_response(request, response)
return response
-
-class ServerHostnameMiddleware:
+
+class ServerHostnameMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
@@ -355,8 +521,8 @@ class ServerHostnameMiddleware:
return response
-class TimingMiddleware:
+class TimingMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
@@ -369,13 +535,15 @@ class TimingMiddleware:
response = self.get_response(request)
return response
+
+
BANNED_USER_AGENTS = (
'feed reader-background',
'missing',
)
-BANNED_USERNAMES = (
-)
+BANNED_USERNAMES = ()
+
class UserAgentBanMiddleware:
def __init__(self, get_response=None):
@@ -383,31 +551,38 @@ class UserAgentBanMiddleware:
def process_request(self, request):
user_agent = request.environ.get('HTTP_USER_AGENT', 'missing').lower()
-
- if 'profile' in request.path: return
- if 'haproxy' in request.path: return
- if 'dbcheck' in request.path: return
- if 'account' in request.path: return
- if 'push' in request.path: return
- if getattr(settings, 'TEST_DEBUG'): return
+
+ if 'profile' in request.path:
+ return
+ if 'haproxy' in request.path:
+ return
+ if 'dbcheck' in request.path:
+ return
+ if 'account' in request.path:
+ return
+ if 'push' in request.path:
+ return
+ if getattr(settings, 'TEST_DEBUG'):
+ return
if any(ua in user_agent for ua in BANNED_USER_AGENTS):
- data = {
- 'error': 'User agent banned: %s' % user_agent,
- 'code': -1
- }
- logging.user(request, "~FB~SN~BBBanned UA: ~SB%s / %s (%s)" % (user_agent, request.path, request.META))
-
+ data = {'error': 'User agent banned: %s' % user_agent, 'code': -1}
+ logging.user(
+ request, "~FB~SN~BBBanned UA: ~SB%s / %s (%s)" % (user_agent, request.path, request.META)
+ )
+
return HttpResponse(json.encode(data), status=403, content_type='text/json')
- if request.user.is_authenticated and any(username == request.user.username for username in BANNED_USERNAMES):
- data = {
- 'error': 'User banned: %s' % request.user.username,
- 'code': -1
- }
- logging.user(request, "~FB~SN~BBBanned Username: ~SB%s / %s (%s)" % (request.user, request.path, request.META))
-
+ if request.user.is_authenticated and any(
+ username == request.user.username for username in BANNED_USERNAMES
+ ):
+ data = {'error': 'User banned: %s' % request.user.username, 'code': -1}
+ logging.user(
+ request,
+ "~FB~SN~BBBanned Username: ~SB%s / %s (%s)" % (request.user, request.path, request.META),
+ )
+
return HttpResponse(json.encode(data), status=403, content_type='text/json')
-
+
def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
@@ -418,4 +593,3 @@ class UserAgentBanMiddleware:
response = self.process_response(request, response)
return response
-
From 698755a66ce23ee35cac3f62a952fb9d53ea2fb6 Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Wed, 2 Mar 2022 11:07:20 -0500
Subject: [PATCH 47/65] Syntax coloring for ansible files
---
.vscode/settings.json | 3 +++
apps/notifications/models.py | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9b398128e..2b39f07c4 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -30,4 +30,7 @@
"--line-length=110",
"--skip-string-normalization"
],
+ "files.associations": {
+ "*.yml": "ansible"
+ },
}
diff --git a/apps/notifications/models.py b/apps/notifications/models.py
index 8862f0006..190a95a96 100644
--- a/apps/notifications/models.py
+++ b/apps/notifications/models.py
@@ -176,7 +176,7 @@ class MUserFeedNotification(mongo.Document):
continue
classifiers = user_feed_notification.classifiers(usersub)
- if classifiers == None:
+ if classifiers is None:
if settings.DEBUG:
logging.debug("Has no usersubs")
continue
From 6f5b3b10e2b8e4ca60cb6f2afecd3c09dcef1978 Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Wed, 2 Mar 2022 11:26:27 -0500
Subject: [PATCH 48/65] Turning off beta, deploying magazine to master.
---
ansible/playbooks/deploy_app.yml | 2 +-
apps/reader/views.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/ansible/playbooks/deploy_app.yml b/ansible/playbooks/deploy_app.yml
index 642e6a063..5b73be6a3 100644
--- a/ansible/playbooks/deploy_app.yml
+++ b/ansible/playbooks/deploy_app.yml
@@ -110,7 +110,7 @@
git:
repo: https://github.com/samuelclay/NewsBlur.git
dest: /srv/newsblur/
- version: magazine
+ version: master
register: pulled
tags:
- static
diff --git a/apps/reader/views.py b/apps/reader/views.py
index 3c02af379..1806a90de 100644
--- a/apps/reader/views.py
+++ b/apps/reader/views.py
@@ -75,7 +75,7 @@ BANNED_URLS = [
ALLOWED_SUBDOMAINS = [
'dev',
'www',
- 'beta',
+ # 'beta', # Comment to redirect beta -> www, uncomment to allow beta -> staging (+ dns changes)
'staging',
'discovery',
'debug',
From 23695568175f30b2ba78cd916d3cf7aac0c38953 Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Wed, 2 Mar 2022 11:42:44 -0500
Subject: [PATCH 49/65] Adding gzip compression to static assets.
---
docker/nginx/nginx.consul.conf.j2 | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docker/nginx/nginx.consul.conf.j2 b/docker/nginx/nginx.consul.conf.j2
index c3d1170f5..9c8171463 100644
--- a/docker/nginx/nginx.consul.conf.j2
+++ b/docker/nginx/nginx.consul.conf.j2
@@ -62,6 +62,10 @@ server {
location /static/ {
gzip_static on;
+ gzip on;
+ gzip_comp_level 6;
+ gzip_vary on;
+ gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype;
expires max;
keepalive_timeout 1;
root /srv/newsblur;
From 2491e3bf86c1658436635e07ed327e31b2190bc1 Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Thu, 3 Mar 2022 11:21:06 -0500
Subject: [PATCH 50/65] Fixing youtube story content encoding bug.
---
ansible/playbooks/deploy_app.yml | 1 +
apps/rss_feeds/models.py | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/ansible/playbooks/deploy_app.yml b/ansible/playbooks/deploy_app.yml
index 5b73be6a3..49acb3b35 100644
--- a/ansible/playbooks/deploy_app.yml
+++ b/ansible/playbooks/deploy_app.yml
@@ -13,6 +13,7 @@
- name: Update Sentry release
connection: local
+ run_once: yes
shell: >
curl {{ sentry_web_release_webhook }}/ \
-X POST \
diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py
index 6ed3deaa8..1a17d6e5a 100755
--- a/apps/rss_feeds/models.py
+++ b/apps/rss_feeds/models.py
@@ -2501,7 +2501,9 @@ class MStory(mongo.Document):
story_content = self.story_content
if not story_content and self.story_content_z:
story_content = smart_str(zlib.decompress(self.story_content_z))
-
+ else:
+ story_content = smart_str(story_content)
+
return story_content
From af4b6b2b02a676461ed1a6a0523d04782e7e541a Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Fri, 4 Mar 2022 11:03:57 -0500
Subject: [PATCH 51/65] Adding blog to docker and in progress magazine post.
---
docker/haproxy/haproxy.docker-compose.cfg | 1 +
docker/nginx/nginx.local.conf | 16 +++++++++++++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/docker/haproxy/haproxy.docker-compose.cfg b/docker/haproxy/haproxy.docker-compose.cfg
index cbfd5dbef..fff7d8dac 100644
--- a/docker/haproxy/haproxy.docker-compose.cfg
+++ b/docker/haproxy/haproxy.docker-compose.cfg
@@ -59,6 +59,7 @@ frontend public
use_backend nginx if { path_beg /favicon }
use_backend nginx if { path_beg /crossdomain/ }
use_backend nginx if { path_beg /robots }
+ use_backend nginx if { hdr_sub(host) -i blog.localhost }
#use_backend self if { path_beg /munin/ }
# use_backend gunicorn_counts if is_unread_count
diff --git a/docker/nginx/nginx.local.conf b/docker/nginx/nginx.local.conf
index 41f917e35..23d7efe51 100644
--- a/docker/nginx/nginx.local.conf
+++ b/docker/nginx/nginx.local.conf
@@ -62,7 +62,7 @@ server {
keepalive_timeout 1;
root /srv/newsblur;
}
-
+
location /static/ {
gzip_static on;
expires max;
@@ -114,3 +114,17 @@ server {
}
}
+
+
+server {
+ listen 81;
+
+ server_name blog.localhost;
+
+ set_real_ip_from 0.0.0.0/0;
+ real_ip_header X-Forwarded-For;
+ real_ip_recursive on;
+
+ root /srv/newsblur/blog/_site;
+}
+
From 8bd50538c405de0cee75dc1ff9e7de68003e5b5c Mon Sep 17 00:00:00 2001
From: Samuel Clay
Date: Fri, 4 Mar 2022 11:04:14 -0500
Subject: [PATCH 52/65] In progress magazine post
---
blog/_posts/2022-03-04-magazine-view.md | 27 ++++++++++++++++++++++++
blog/assets/magazine-dark.png | Bin 0 -> 1692444 bytes
blog/assets/magazine-light.png | Bin 0 -> 1351522 bytes
blog/assets/magazine-views.png | Bin 0 -> 36183 bytes
4 files changed, 27 insertions(+)
create mode 100644 blog/_posts/2022-03-04-magazine-view.md
create mode 100644 blog/assets/magazine-dark.png
create mode 100644 blog/assets/magazine-light.png
create mode 100644 blog/assets/magazine-views.png
diff --git a/blog/_posts/2022-03-04-magazine-view.md b/blog/_posts/2022-03-04-magazine-view.md
new file mode 100644
index 000000000..57049fc55
--- /dev/null
+++ b/blog/_posts/2022-03-04-magazine-view.md
@@ -0,0 +1,27 @@
+---
+layout: post
+title: The Magazine view is a new perspective
+tags: ['web']
+---
+
+Here's a nice new feature that brings a new perspective to your stories. It's called the Magazine view and you can access it next to the other story views:
+
+
+
+
+
+
+
+
+
+Loads of new features:
+
+ * The dashboard now has multiple, customizable rivers of news
+ * Image previews are now customizable by size and layout
+ * Story previews are also customizable by length
+ * Images are now full bleed on the web (edge-to-edge)
+ * Controls have been re-styled and made more accessible
+ * Sizes, spaces, and text have all been tweaked for a more legible read
+ * Upgraded backend: Python 2 to Python 3, latest Django and libraries, containerized infrastructure
+ * Both Android and iOS apps have been updated with the new design
+
diff --git a/blog/assets/magazine-dark.png b/blog/assets/magazine-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..c68b5b49591b0353bca28e34eec328ba18ed3990
GIT binary patch
literal 1692444
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