diff --git a/ansible/playbooks/setup_mongo.yml b/ansible/playbooks/setup_mongo.yml index 0001a890eb..da343479ed 100644 --- a/ansible/playbooks/setup_mongo.yml +++ b/ansible/playbooks/setup_mongo.yml @@ -6,18 +6,18 @@ - motd_role: db vars_files: - ../env_vars/base.yml - + roles: - - {role: 'base', tags: 'base'} - - {role: 'ufw', tags: 'ufw'} - - {role: 'docker', tags: 'docker'} - - {role: 'repo', tags: ['repo', 'pull']} - - {role: 'dnsmasq', tags: 'dnsmasq'} - - {role: 'consul', tags: 'consul'} - - {role: 'consul-client', tags: 'consul'} - - {role: 'mongo', tags: 'mongo'} - - {role: 'node-exporter', tags: ['node-exporter', 'metrics']} - - {role: 'mongo-exporter', tags: ['mongo-exporter', 'metrics']} - - {role: 'monitor', tags: 'monitor'} - - {role: 'flask_metrics', tags: ['flask-metrics', 'metrics']} + - { role: "base", tags: "base" } + - { role: "ufw", tags: "ufw" } + - { role: "docker", tags: "docker" } + - { role: "repo", tags: ["repo", "pull"] } + - { role: "dnsmasq", tags: "dnsmasq" } + - { role: "consul", tags: "consul" } + - { role: "consul-client", tags: "consul" } + - { role: "mongo", tags: "mongo" } + - { role: "node-exporter", tags: ["node-exporter", "metrics"] } + - { role: "mongo-exporter", tags: ["mongo-exporter", "metrics"] } + - { role: "monitor", tags: "monitor" } + - { role: "flask_metrics", tags: ["flask-metrics", "metrics"] } # - {role: 'benchmark', tags: 'benchmark'} diff --git a/ansible/roles/consul/tasks/get_consul_manager_ip.py b/ansible/roles/consul/tasks/get_consul_manager_ip.py index e98eb6b3f0..659d06a478 100755 --- a/ansible/roles/consul/tasks/get_consul_manager_ip.py +++ b/ansible/roles/consul/tasks/get_consul_manager_ip.py @@ -14,24 +14,31 @@ def get_host_ips_from_group(group_name): :param inventory_base_path: Base path to the inventory directories. Defaults to the path in ansible.cfg. :return: A list of IP addresses belonging to the specified group. """ - cmd = ['ansible-inventory', '-i', '/srv/newsblur/ansible/inventories/hetzner.ini', '-i', '/srv/newsblur/ansible/inventories/hetzner.yml', '--list'] - + cmd = [ + "ansible-inventory", + "-i", + "/srv/newsblur/ansible/inventories/hetzner.ini", + "-i", + "/srv/newsblur/ansible/inventories/hetzner.yml", + "--list", + ] + try: # Execute the ansible-inventory command result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) - + # Parse the JSON output from ansible-inventory inventory_data = json.loads(result.stdout) - + host_ips = [] # Check if the group exists if group_name in inventory_data: # Get the list of hosts in the specified group - if 'hosts' in inventory_data[group_name]: - for host in inventory_data[group_name]['hosts']: + if "hosts" in inventory_data[group_name]: + for host in inventory_data[group_name]["hosts"]: # Fetch the host details, specifically looking for the ansible_host variable for the IP - host_vars = inventory_data['_meta']['hostvars'][host] - ip_address = host_vars.get('ansible_host', None) + host_vars = inventory_data["_meta"]["hostvars"][host] + ip_address = host_vars.get("ansible_host", None) if ip_address: host_ips.append(ip_address) else: @@ -50,16 +57,19 @@ def get_host_ips_from_group(group_name): with open(TOKEN_FILE) as f: token = f.read().strip() - os.environ['DO_API_TOKEN'] = token + os.environ["DO_API_TOKEN"] = token manager = digitalocean.Manager(token=token) my_droplets = manager.get_all_droplets() consul_manager_droplets = [d for d in my_droplets if "db-consul" in d.name] # Use ansible-inventory to get the consul-manager ip -group_name = 'hconsul' +group_name = "hconsul" hetzner_hosts = get_host_ips_from_group(group_name) -consul_manager_ip_address = ','.join([f"\"{droplet.ip_address}\"" for droplet in consul_manager_droplets] + [f"\"{host}\"" for host in hetzner_hosts]) +consul_manager_ip_address = ",".join( + [f'"{droplet.ip_address}"' for droplet in consul_manager_droplets] + + [f'"{host}"' for host in hetzner_hosts] +) print(consul_manager_ip_address) diff --git a/ansible/roles/mongo/tasks/main.yml b/ansible/roles/mongo/tasks/main.yml index eb936152b8..e12957783c 100644 --- a/ansible/roles/mongo/tasks/main.yml +++ b/ansible/roles/mongo/tasks/main.yml @@ -49,7 +49,7 @@ - name: Create the mount point become: yes - file: + file: path: "/mnt/{{ inventory_hostname | regex_replace('db-|-', '') }}" state: directory owner: "{{ ansible_effective_user_id|int }}" @@ -64,7 +64,6 @@ opts: defaults,discard state: mounted - - name: Set permissions on mongo volume # become: yes file: @@ -93,7 +92,6 @@ force: yes when: (inventory_hostname | regex_replace('[0-9]+', '')) in ['db-mongo-secondary', 'db-mongo-analytics'] - - name: Block for mongo volume on hetzner block: - name: Create backup directory @@ -119,7 +117,7 @@ # network_mode: default # networks: # - name: newsblurnet - # aliases: + # aliases: # - mongo # ports: # - "27017:27017" @@ -148,7 +146,7 @@ # network_mode: default # networks: # - name: newsblurnet - # aliases: + # aliases: # - mongo # ports: # - "27017:27017" @@ -186,7 +184,7 @@ network_mode: default networks: - name: newsblurnet - aliases: + aliases: - mongo ports: - "27017:27017" @@ -214,7 +212,7 @@ network_mode: default networks: - name: newsblurnet - aliases: + aliases: - mongo ports: - "27017:27017" @@ -231,7 +229,7 @@ - name: Create mongo database user shell: # Don't use this line below as it means there is already a username and password, so no need to set one - # sleep 2; docker exec mongo mongo -u "{{ mongodb_username }}" -p "{{ mongodb_password }}" --eval ' + # sleep 2; docker exec mongo mongo -u "{{ mongodb_username }}" -p "{{ mongodb_password }}" --eval ' cmd: >- sleep 2; docker exec mongo mongo --eval ' db.createUser( @@ -252,8 +250,9 @@ - "'there are no users authenticated' not in auth_result.stdout" tags: - mongoauth + - never -# - debug: +# - debug: # msg: "{{ auth_result }}" # tags: # - mongoauth @@ -281,9 +280,9 @@ - name: Setup logrotate for mongo become: yes copy: src=logrotate.conf dest=/etc/logrotate.d/mongodb mode=0755 - tags: + tags: - logrotate - + - name: Add sanity checkers cronjob for disk usage become: yes cron: @@ -326,7 +325,6 @@ tags: - mongo-backup - cron - # - name: Add mongo starred_stories+stories backup # cron: # name: mongo starred/shared/all stories backup @@ -338,7 +336,7 @@ # - mongo-backup # Renaming a db-mongo-primary3 to db-mongo-primary2: -# - Change hostname to db-mongo-primary2 on Digital Ocean +# - Change hostname to db-mongo-primary2 on Digital Ocean # - make list; doctl compute droplet-action rename --droplet-name db-mongo-primary2 # - Change hostname to db-mongo-primary2 in /etc/hostname # - make inventory @@ -352,7 +350,7 @@ # - doctl compute droplet delete db-mongo3 # - tf state rm "digitalocean_droplet.db-mongo-primary-s[1]" # - tf state rm "digitalocean_droplet.db-mongo-primary-s[2]" -# - tf state mv "digitalocean_droplet.db-mongo-primary-s[3]" "digitalocean_droplet.db-mongo-primary-s[1]" +# - tf state mv "digitalocean_droplet.db-mongo-primary-s[3]" "digitalocean_droplet.db-mongo-primary-s[1]" # - Change hostname to db-mongo2 in /etc/hostname # - sudo hostname db-mongo-primary2 diff --git a/ansible/roles/postgres-exporter/tasks/get_credentials.py b/ansible/roles/postgres-exporter/tasks/get_credentials.py index 862c46d897..85fa38df88 100755 --- a/ansible/roles/postgres-exporter/tasks/get_credentials.py +++ b/ansible/roles/postgres-exporter/tasks/get_credentials.py @@ -1,12 +1,13 @@ #!/srv/newsblur/venv/newsblur3/bin/python import sys -sys.path.append('/srv/newsblur') + +sys.path.append("/srv/newsblur") from newsblur_web import settings -username = settings.DATABASES['default']['USER'] -password = settings.DATABASES['default']['PASSWORD'] +username = settings.DATABASES["default"]["USER"] +password = settings.DATABASES["default"]["PASSWORD"] -if sys.argv[1] =='postgres_credentials': +if sys.argv[1] == "postgres_credentials": print(f"{username}:{password}") -if sys.argv[1] =='s3_bucket': - print(settings.S3_BACKUP_BUCKET) \ No newline at end of file +if sys.argv[1] == "s3_bucket": + print(settings.S3_BACKUP_BUCKET) diff --git a/ansible/roles/postgres-exporter/tasks/main.yml b/ansible/roles/postgres-exporter/tasks/main.yml index 90b1f7194d..7f6beeb353 100644 --- a/ansible/roles/postgres-exporter/tasks/main.yml +++ b/ansible/roles/postgres-exporter/tasks/main.yml @@ -1,14 +1,14 @@ - - name: Register Postgres user and password become: no run_once: yes register: postgres_credentials local_action: command /srv/newsblur/ansible/roles/postgres-exporter/tasks/get_credentials.py postgres_credentials + - name: Start postgres-exporter container become: yes docker_container: name: postgres-exporter - image: prometheuscommunity/postgres-exporter + image: prometheuscommunity/postgres-exporter restart_policy: unless-stopped container_default_behavior: no_defaults networks_cli_compatible: yes @@ -16,9 +16,9 @@ networks: - name: newsblurnet env: - DATA_SOURCE_NAME: 'postgresql://{{ postgres_credentials.stdout }}@db-postgres.service.nyc1.consul:5432/postgres?sslmode=disable' + DATA_SOURCE_NAME: "postgresql://{{ postgres_credentials.stdout }}@db-postgres.service.nyc1.consul:5432/postgres?sslmode=disable" ports: - - '9187:9187' + - "9187:9187" - name: Register postgres-exporter in consul tags: consul diff --git a/ansible/roles/ufw/templates/ufw_rules.sh.j2 b/ansible/roles/ufw/templates/ufw_rules.sh.j2 index 379118bf10..8d6b777cf8 100644 --- a/ansible/roles/ufw/templates/ufw_rules.sh.j2 +++ b/ansible/roles/ufw/templates/ufw_rules.sh.j2 @@ -41,3 +41,7 @@ apply_rule "route allow from {{ host }}" "FWD" "{{ host }}" apply_rule "allow from {{ host }}" "IN" "{{ host }}" apply_rule "route allow from {{ host }}" "FWD" "{{ host }}" {% endfor %} + +# Allow traffic on docker0 interface +apply_rule "allow in on docker0" "IN" "docker0" +apply_rule "allow out on docker0" "IN" "docker0" diff --git a/ansible/utils/check_droplet.py b/ansible/utils/check_droplet.py index b231777aaa..09747a9bee 100644 --- a/ansible/utils/check_droplet.py +++ b/ansible/utils/check_droplet.py @@ -1,7 +1,9 @@ +import subprocess import sys import time + import digitalocean -import subprocess + def test_ssh(drop): droplet_ip_address = drop.ip_address @@ -10,6 +12,7 @@ def test_ssh(drop): return True return False + TOKEN_FILE = "/srv/secrets-newsblur/keys/digital_ocean.token" droplet_name = sys.argv[1] @@ -25,7 +28,7 @@ def test_ssh(drop): while not ssh_works: if timer > timeout: raise Exception(f"The {droplet_name} droplet was not created.") - + droplets = [drop for drop in manager.get_all_droplets() if drop.name == droplet_name] if droplets: droplet = droplets[0] @@ -33,4 +36,4 @@ def test_ssh(drop): ssh_works = test_ssh(droplet) time.sleep(3) timer += 3 -print("Success!") \ No newline at end of file +print("Success!") diff --git a/ansible/utils/generate_inventory.py b/ansible/utils/generate_inventory.py index d9e26f4a30..4f74ffa92e 100755 --- a/ansible/utils/generate_inventory.py +++ b/ansible/utils/generate_inventory.py @@ -1,14 +1,15 @@ #!/usr/bin/env python import os -import time -import sys import subprocess +import sys +import time + import digitalocean OLD = False # Set env var OLD=1 to use existing servers -if os.environ.get('OLD', False): +if os.environ.get("OLD", False): OLD = True if OLD: @@ -17,7 +18,7 @@ TOKEN_FILE = "/srv/secrets-newsblur/keys/digital_ocean.token" try: - api_token = open(TOKEN_FILE, 'r').read().strip() + api_token = open(TOKEN_FILE, "r").read().strip() except IOError: print(f" ---> Missing Digital Ocean API token: {TOKEN_FILE}") exit() @@ -25,20 +26,20 @@ outfile = f"/srv/newsblur/ansible/inventories/digital_ocean{'.old' if OLD else ''}.ini" # Install from https://github.com/do-community/do-ansible-inventory/releases -ansible_inventory_cmd = f'do-ansible-inventory -t {api_token} --out {outfile}' +ansible_inventory_cmd = f"do-ansible-inventory -t {api_token} --out {outfile}" subprocess.call(ansible_inventory_cmd, shell=True) -with open(outfile, 'r') as original: +with open(outfile, "r") as original: data = original.read() -with open(outfile, 'w') as modified: +with open(outfile, "w") as modified: modified.write("127.0.0.1 ansible_connection=local\n" + data) -exit() # Too many requests if we run the below code +exit() # Too many requests if we run the below code do = digitalocean.Manager(token=api_token) droplets = do.get_all_droplets() -print("\n ---> Checking droplets: %s\n" % (' '.join([d.name for d in droplets]))) +print("\n ---> Checking droplets: %s\n" % (" ".join([d.name for d in droplets]))) def check_droplets_created(): @@ -46,8 +47,8 @@ def check_droplets_created(): droplets = do.get_all_droplets() for instance in droplets: - if instance.status == 'new': - print(".", end=' ') + if instance.status == "new": + print(".", end=" ") sys.stdout.flush() i += 1 time.sleep(i) @@ -56,6 +57,7 @@ def check_droplets_created(): print(" ---> All booted!") return True + i = 0 while True: if check_droplets_created(): diff --git a/api/newsblur.py b/api/newsblur.py index acdd2e437d..afa15341de 100644 --- a/api/newsblur.py +++ b/api/newsblur.py @@ -2,9 +2,10 @@ # Retooled by Samuel Clay, August 2011 # Modified by Luke Hagan, 2011-11-05 -import urllib.request, urllib.parse import http.cookiejar import json +import urllib.parse +import urllib.request __author__ = "Dananjaya Ramanayake , Samuel Clay " __version__ = "1.0" @@ -13,342 +14,318 @@ # API_URL = "https://nb.local.host:8000/" -class request(): - +class request: opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) - - def __init__(self, endpoint=None, method='get'): + + def __init__(self, endpoint=None, method="get"): self.endpoint = endpoint self.method = method def __call__(self, func): def wrapped(*args, **kwargs): params = func(*args, **kwargs) or {} - url = self.endpoint if self.endpoint else params.pop('url') + url = self.endpoint if self.endpoint else params.pop("url") params = urllib.parse.urlencode(params) - url = "%s%s" % (API_URL, url) - + url = "%s%s" % (API_URL, url) + response = self.opener.open(url, params).read() - + return json.loads(response) + return wrapped -class API: - @request('api/login', method='post') +class API: + @request("api/login", method="post") def login(self, username, password): - ''' + """ Login as an existing user. - If a user has no password set, you cannot just send any old password. + If a user has no password set, you cannot just send any old password. Required parameters, username and password, must be of string type. - ''' - return { - 'username': username, - 'password': password - } + """ + return {"username": username, "password": password} - @request('api/logout') + @request("api/logout") def logout(self): - ''' + """ Logout the currently logged in user. - ''' + """ return - @request('api/signup') + @request("api/signup") def signup(self, username, password, email): - ''' + """ Create a new user. All three required parameters must be of type string. - ''' - return { - 'signup_username': username, - 'signup_password': password, - 'signup_email': email - } + """ + return {"signup_username": username, "signup_password": password, "signup_email": email} - @request('rss_feeds/search_feed') + @request("rss_feeds/search_feed") def search_feed(self, address, offset=0): - ''' + """ Retrieve information about a feed from its website or RSS address. Parameter address must be of type string while parameter offset must be an integer. Will return a feed. - ''' - return { - 'address': address, - 'offset': offset - } + """ + return {"address": address, "offset": offset} - @request('reader/feeds') + @request("reader/feeds") def feeds(self, include_favicons=True, flat=False): - ''' + """ Retrieve a list of feeds to which a user is actively subscribed. Includes the 3 unread counts (positive, neutral, negative), as well as optional favicons. - ''' - return { - 'include_favicons': include_favicons, - 'flat': flat - } + """ + return {"include_favicons": include_favicons, "flat": flat} - @request('reader/favicons') + @request("reader/favicons") def favicons(self, feeds=None): - ''' - Retrieve a list of favicons for a list of feeds. - Used when combined with /reader/feeds and include_favicons=false, so the feeds request contains far less data. - Useful for mobile devices, but requires a second request. - ''' + """ + Retrieve a list of favicons for a list of feeds. + Used when combined with /reader/feeds and include_favicons=false, so the feeds request contains far less data. + Useful for mobile devices, but requires a second request. + """ data = [] for feed in feeds: - data.append( ("feeds", feed) ) + data.append(("feeds", feed)) return data @request() def page(self, feed_id): - ''' + """ Retrieve the original page from a single feed. - ''' - return { - 'url': 'reader/page/%s' % feed_id - } + """ + return {"url": "reader/page/%s" % feed_id} @request() def feed(self, feed_id, page=1): - ''' + """ Retrieve the stories from a single feed. - ''' + """ return { - 'url': 'reader/feed/%s' % feed_id, - 'page': page, + "url": "reader/feed/%s" % feed_id, + "page": page, } - @request('reader/refresh_feeds') + @request("reader/refresh_feeds") def refresh_feeds(self): - ''' + """ Up-to-the-second unread counts for each active feed. Poll for these counts no more than once a minute. - ''' + """ return - @request('reader/feeds_trainer') + @request("reader/feeds_trainer") def feeds_trainer(self, feed_id=None): - ''' - Retrieves all popular and known intelligence classifiers. - Also includes user's own classifiers. - ''' + """ + Retrieves all popular and known intelligence classifiers. + Also includes user's own classifiers. + """ return { - 'feed_id': feed_id, + "feed_id": feed_id, } - + @request() def statistics(self, feed_id=None): - ''' + """ If you only want a user's classifiers, use /classifiers/:id. Omit the feed_id to get all classifiers for all subscriptions. - ''' - return { - 'url': 'rss_feeds/statistics/%d' % feed_id - } - - @request('rss_feeds/feed_autocomplete') + """ + return {"url": "rss_feeds/statistics/%d" % feed_id} + + @request("rss_feeds/feed_autocomplete") def feed_autocomplete(self, term): - ''' + """ Get a list of feeds that contain a search phrase. Searches by feed address, feed url, and feed title, in that order. Will only show sites with 2+ subscribers. - ''' - return { - 'term': term - } + """ + return {"term": term} - @request('reader/starred_stories') + @request("reader/starred_stories") def starred_stories(self, page=1): - ''' + """ Retrieve a user's starred stories. - ''' + """ return { - 'page': page, + "page": page, } - @request('reader/river_stories') + @request("reader/river_stories") def river_stories(self, feeds, page=1, read_stories_count=0): - ''' + """ Retrieve stories from a collection of feeds. This is known as the River of News. Stories are ordered in reverse chronological order. `read_stories_count` is the number of stories that have been read in this continuation, so NewsBlur can efficiently skip those stories when retrieving new stories. Takes an array of feed ids. - ''' - - data = [ ('page', page), ('read_stories_count', read_stories_count) ] + """ + + data = [("page", page), ("read_stories_count", read_stories_count)] for feed in feeds: - data.append( ("feeds", feed) ) + data.append(("feeds", feed)) return data - - @request('reader/mark_story_hashes_as_read') + + @request("reader/mark_story_hashes_as_read") def mark_story_hashes_as_read(self, story_hashes): - ''' - Mark stories as read using their unique story_hash. - ''' + """ + Mark stories as read using their unique story_hash. + """ data = [] for hash in story_hashes: - data.append( ("story_hash", hash) ) + data.append(("story_hash", hash)) return data - @request('reader/mark_story_as_read') + @request("reader/mark_story_as_read") def mark_story_as_read(self, feed_id, story_ids): - ''' - Mark stories as read. - Multiple story ids can be sent at once. - Each story must be from the same feed. - Takes an array of story ids. - ''' - - data = [ ('feed_id', feed_id) ] + """ + Mark stories as read. + Multiple story ids can be sent at once. + Each story must be from the same feed. + Takes an array of story ids. + """ + + data = [("feed_id", feed_id)] for story_id in story_ids: - data.append( ("story_id", story_id) ) + data.append(("story_id", story_id)) return data - @request('reader/mark_story_as_starred') + @request("reader/mark_story_as_starred") def mark_story_as_starred(self, feed_id, story_id): - ''' + """ Mark a story as starred (saved). - ''' + """ return { - 'feed_id': feed_id, - 'story_id': story_id, + "feed_id": feed_id, + "story_id": story_id, } - @request('reader/mark_all_as_read') + @request("reader/mark_all_as_read") def mark_all_as_read(self, days=0): - ''' + """ Mark all stories in a feed or list of feeds as read. - ''' + """ return { - 'days': days, + "days": days, } - @request('reader/add_url') - def add_url(self, url, folder=''): - ''' - Add a feed by its URL. + @request("reader/add_url") + def add_url(self, url, folder=""): + """ + Add a feed by its URL. Can be either the RSS feed or the website itself. - ''' + """ return { - 'url': url, - 'folder': folder, + "url": url, + "folder": folder, } - @request('reader/add_folder') - def add_folder(self, folder, parent_folder=''): - ''' + @request("reader/add_folder") + def add_folder(self, folder, parent_folder=""): + """ Add a new folder. - ''' + """ return { - 'folder': folder, - 'parent_folder': parent_folder, + "folder": folder, + "parent_folder": parent_folder, } - - @request('reader/rename_feed') + + @request("reader/rename_feed") def rename_feed(self, feed_id, feed_title): - ''' + """ Rename a feed title. Only the current user will see the new title. - ''' + """ return { - 'feed_id': feed_id, - 'feed_title': feed_title, + "feed_id": feed_id, + "feed_title": feed_title, } - - @request('reader/delete_feed') + + @request("reader/delete_feed") def delete_feed(self, feed_id, in_folder): - ''' + """ Unsubscribe from a feed. Removes it from the folder. - Set the in_folder parameter to remove a feed from the correct + Set the in_folder parameter to remove a feed from the correct folder, in case the user is subscribed to the feed in multiple folders. - ''' + """ return { - 'feed_id': feed_id, - 'in_folder': in_folder, + "feed_id": feed_id, + "in_folder": in_folder, } - - @request('reader/rename_folder') + + @request("reader/rename_folder") def rename_folder(self, folder_to_rename, new_folder_name, in_folder): - ''' + """ Rename a folder. - ''' + """ return { - 'folder_to_rename': folder_to_rename, - 'new_folder_name': new_folder_name, - 'in_folder': in_folder, + "folder_to_rename": folder_to_rename, + "new_folder_name": new_folder_name, + "in_folder": in_folder, } - - @request('reader/delete_folder') + + @request("reader/delete_folder") def delete_folder(self, folder_to_delete, in_folder): - ''' + """ Delete a folder and unsubscribe from all feeds inside. - ''' + """ return { - 'folder_to_delete': folder_to_delete, - 'in_folder': in_folder, + "folder_to_delete": folder_to_delete, + "in_folder": in_folder, } - - @request('reader/mark_feed_as_read') + + @request("reader/mark_feed_as_read") def mark_feed_as_read(self, feed_ids): - ''' + """ Mark a list of feeds as read. Takes an array of feeds. - ''' + """ data = [] for feed in feed_ids: - data.append( ("feed_id", feed) ) + data.append(("feed_id", feed)) return data - @request('reader/save_feed_order') + @request("reader/save_feed_order") def save_feed_order(self, folders): - ''' + """ Reorder feeds and move them around between folders. The entire folder structure needs to be serialized. - ''' + """ return { - 'folders': folders, + "folders": folders, } @request() def classifier(self, feed_id): - ''' - Get the intelligence classifiers for a user's site. - Only includes the user's own classifiers. - Use /reader/feeds_trainer for popular classifiers. - ''' + """ + Get the intelligence classifiers for a user's site. + Only includes the user's own classifiers. + Use /reader/feeds_trainer for popular classifiers. + """ return { - 'url': '/classifier/%d' % feed_id, + "url": "/classifier/%d" % feed_id, } - @request('classifier/save') + @request("classifier/save") def classifier_save(self, like_type, dislike_type, remove_like_type, remove_dislike_type): - ''' + """ Save intelligence classifiers (tags, titles, authors, and the feed) for a feed. - + TODO: Make this usable. - ''' + """ raise NotImplemented - - @request('import/opml_export') + @request("import/opml_export") def opml_export(self): - ''' + """ Download a backup of feeds and folders as an OPML file. Contains folders and feeds in XML; useful for importing in another RSS reader. - ''' + """ return - - @request('import/opml_upload') + + @request("import/opml_upload") def opml_upload(self, opml_file): - ''' + """ Upload an OPML file. - ''' + """ f = open(opml_file) - return { - 'file': f - } - - + return {"file": f} diff --git a/apps/analyzer/classifier.py b/apps/analyzer/classifier.py index 686d3720b0..8e6c39c6f4 100644 --- a/apps/analyzer/classifier.py +++ b/apps/analyzer/classifier.py @@ -1,9 +1,11 @@ -from apps.analyzer.models import Category, FeatureCategory -from django.db.models.aggregates import Sum import math +from django.db.models.aggregates import Sum + +from apps.analyzer.models import Category, FeatureCategory + + class Classifier: - def __init__(self, user, feed, phrases): self.user = user self.feed = feed @@ -11,7 +13,7 @@ def __init__(self, user, feed, phrases): def get_features(self, doc): found = {} - + for phrase in self.phrases: if phrase in doc: if phrase in found: @@ -20,36 +22,40 @@ def get_features(self, doc): found[phrase] = 1 return found - + def increment_feature(self, feature, category): - count = self.feature_count(feature,category) - if count==0: + count = self.feature_count(feature, category) + if count == 0: fc = FeatureCategory(user=self.user, feed=self.feed, feature=feature, category=category, count=1) fc.save() else: - fc = FeatureCategory.objects.get(user=self.user, feed=self.feed, feature=feature, category=category) + fc = FeatureCategory.objects.get( + user=self.user, feed=self.feed, feature=feature, category=category + ) fc.count = count + 1 fc.save() - + def feature_count(self, feature, category): if isinstance(category, Category): category = category.category - + try: - feature_count = FeatureCategory.objects.get(user=self.user, feed=self.feed, feature=feature, category=category) + feature_count = FeatureCategory.objects.get( + user=self.user, feed=self.feed, feature=feature, category=category + ) except FeatureCategory.DoesNotExist: return 0 else: return float(feature_count.count) - def increment_category(self,category): + def increment_category(self, category): count = self.category_count(category) - if count==0: + if count == 0: category = Category(user=self.user, feed=self.feed, category=category, count=1) category.save() else: category = Category.objects.get(user=self.user, feed=self.feed, category=category) - category.count = count+1 + category.count = count + 1 category.save() def category_count(self, category): @@ -68,12 +74,12 @@ def categories(self): return categories def totalcount(self): - categories = Category.objects.filter(user=self.user, feed=self.feed).aggregate(sum=Sum('count')) - return categories['sum'] + categories = Category.objects.filter(user=self.user, feed=self.feed).aggregate(sum=Sum("count")) + return categories["sum"] def train(self, item, category): features = self.get_features(item) - + # Increment the count for every feature with this category for feature in features: self.increment_feature(feature, category) @@ -84,7 +90,7 @@ def train(self, item, category): def feature_probability(self, feature, category): if self.category_count(category) == 0: return 0 - # The total number of times this feature appeared in this + # The total number of times this feature appeared in this # category divided by the total number of items in this category return self.feature_count(feature, category) / self.category_count(category) @@ -96,21 +102,20 @@ def weighted_probability(self, feature, category, prf, weight=1.0, ap=0.5): totals = sum([self.feature_count(feature, c) for c in self.categories()]) # Calculate the weighted average - bp = ((weight*ap) + (totals*basic_prob)) / (weight+totals) + bp = ((weight * ap) + (totals * basic_prob)) / (weight + totals) print(feature, category, basic_prob, totals, bp) return bp class FisherClassifier(Classifier): - def __init__(self, user, feed, phrases): Classifier.__init__(self, user, feed, phrases) self.minimums = {} - + def category_probability(self, feature, category): - # The frequency of this feature in this category + # The frequency of this feature in this category clf = self.feature_probability(feature, category) - if clf==0: + if clf == 0: return 0 # The frequency of this feature in all the categories @@ -119,54 +124,53 @@ def category_probability(self, feature, category): # The probability is the frequency in this category divided by # the overall frequency p = clf / freqsum - + return p - + def fisher_probability(self, item, category): # Multiply all the probabilities together - p = .5 + p = 0.5 features = self.get_features(item) if features: p = 1 - + for feature in features: - p *= (self.weighted_probability(feature, category, self.category_probability)) + p *= self.weighted_probability(feature, category, self.category_probability) # Take the natural log and multiply by -2 - fscore = -2*math.log(p) + fscore = -2 * math.log(p) # Use the inverse chi2 function to get a probability - return self.invchi2(fscore,len(features)*2) - + return self.invchi2(fscore, len(features) * 2) + def invchi2(self, chi, df): m = chi / 2.0 sum = term = math.exp(-m) - for i in range(1, df//2): + for i in range(1, df // 2): term *= m / i sum += term return min(sum, 1.0) - def setminimum(self, category, min): self.minimums[category] = min - + def getminimum(self, category): if category not in self.minimums: return 0 return self.minimums[category] - - def classify(self,item,default=None): + + def classify(self, item, default=None): # Loop through looking for the best result best = default max = 0.0 print(self.categories(), item) for category in self.categories(): - p=self.fisher_probability(item, category) + p = self.fisher_probability(item, category) # Make sure it exceeds its minimum if p > self.getminimum(category) and p > max: best = category max = p - - return best \ No newline at end of file + + return best diff --git a/apps/analyzer/feed_filter.py b/apps/analyzer/feed_filter.py index b900ec989e..ac5cba6786 100644 --- a/apps/analyzer/feed_filter.py +++ b/apps/analyzer/feed_filter.py @@ -1,41 +1,45 @@ -from django.contrib.auth.models import User -from apps.rss_feeds.models import Feed -from apps.reader.models import UserSubscription -from apps.analyzer.models import Category, FeatureCategory import datetime -import re import math +import re + +from django.contrib.auth.models import User + +from apps.analyzer.models import Category, FeatureCategory +from apps.reader.models import UserSubscription +from apps.rss_feeds.models import Feed + def entry_features(self, entry): - splitter=re.compile('\\W*') - f={} + splitter = re.compile("\\W*") + f = {} # Extract the title words and annotate - titlewords=[s.lower() for s in splitter.split(entry['title']) - if len(s)>2 and len(s)<20] - - for w in titlewords: f['Title:'+w]=1 + titlewords = [s.lower() for s in splitter.split(entry["title"]) if len(s) > 2 and len(s) < 20] + + for w in titlewords: + f["Title:" + w] = 1 # Extract the summary words - summarywords=[s.lower() for s in splitter.split(entry['summary']) - if len(s)>2 and len(s)<20] + summarywords = [s.lower() for s in splitter.split(entry["summary"]) if len(s) > 2 and len(s) < 20] # Count uppercase words - uc=0 + uc = 0 for i in range(len(summarywords)): - w=summarywords[i] - f[w]=1 - if w.isupper(): uc+=1 + w = summarywords[i] + f[w] = 1 + if w.isupper(): + uc += 1 # Get word pairs in summary as features - if i0.3: f['UPPERCASE']=1 + # UPPERCASE is a virtual word flagging too much shouting + if float(uc) / len(summarywords) > 0.3: + f["UPPERCASE"] = 1 return f diff --git a/apps/analyzer/forms.py b/apps/analyzer/forms.py index 5377f3b0cb..de8d29dcc1 100644 --- a/apps/analyzer/forms.py +++ b/apps/analyzer/forms.py @@ -1,32 +1,31 @@ import re + import requests from django import forms -from vendor.zebra.forms import StripePaymentForm -from django.utils.safestring import mark_safe from django.contrib.auth import authenticate from django.contrib.auth.models import User -from apps.profile.models import change_password, blank_authenticate, MGiftCode +from django.utils.safestring import mark_safe + +from apps.profile.models import MGiftCode, blank_authenticate, change_password from apps.social.models import MSocialProfile +from vendor.zebra.forms import StripePaymentForm + class PopularityQueryForm(forms.Form): - email = forms.CharField(widget=forms.TextInput(), - label="Your email address", - required=False) - query = forms.CharField(widget=forms.TextInput(), - label="Keywords", - required=False) + email = forms.CharField(widget=forms.TextInput(), label="Your email address", required=False) + query = forms.CharField(widget=forms.TextInput(), label="Keywords", required=False) def __init__(self, *args, **kwargs): super(PopularityQueryForm, self).__init__(*args, **kwargs) - + def clean_email(self): - if not self.cleaned_data['email']: - raise forms.ValidationError('Please enter in an email address.') + if not self.cleaned_data["email"]: + raise forms.ValidationError("Please enter in an email address.") + + return self.cleaned_data["email"] - return self.cleaned_data['email'] - def clean_query(self): - if not self.cleaned_data['query']: - raise forms.ValidationError('Please enter in a keyword search query.') + if not self.cleaned_data["query"]: + raise forms.ValidationError("Please enter in a keyword search query.") - return self.cleaned_data['query'] + return self.cleaned_data["query"] diff --git a/apps/analyzer/lda.py b/apps/analyzer/lda.py index b354d17145..5269f3c7e0 100644 --- a/apps/analyzer/lda.py +++ b/apps/analyzer/lda.py @@ -1,234 +1,244 @@ -from bs4 import BeautifulSoup -from glob import glob +import zlib from collections import defaultdict -from math import log, exp +from glob import glob +from math import exp, log from random import random -import zlib -from apps.rss_feeds.models import MStory + +from bs4 import BeautifulSoup from nltk import FreqDist +from apps.rss_feeds.models import MStory + def lgammln(xx): - """ - Returns the gamma function of xx. - Gamma(z) = Integral(0,infinity) of t^(z-1)exp(-t) dt. - (Adapted from: Numerical Recipies in C.) - - Usage: lgammln(xx) - - Copied from stats.py by strang@nmr.mgh.harvard.edu - """ - - coeff = [76.18009173, -86.50532033, 24.01409822, -1.231739516, - 0.120858003e-2, -0.536382e-5] - x = xx - 1.0 - tmp = x + 5.5 - tmp = tmp - (x+0.5)*log(tmp) - ser = 1.0 - for j in range(len(coeff)): - x = x + 1 - ser = ser + coeff[j]/x - return -tmp + log(2.50662827465*ser) + """ + Returns the gamma function of xx. + Gamma(z) = Integral(0,infinity) of t^(z-1)exp(-t) dt. + (Adapted from: Numerical Recipies in C.) -def log_sum(log_a, log_b): - if log_a < log_b: - return log_b + log(1 + exp(log_a - log_b)) - else: - return log_a + log(1 + exp(log_b - log_a)) + Usage: lgammln(xx) -def log_normalize(dist): - normalizer = reduce(log_sum, dist) - for ii in xrange(len(dist)): - dist[ii] -= normalizer - return dist + Copied from stats.py by strang@nmr.mgh.harvard.edu + """ -def log_sample(dist): - """ - Sample a key from a dictionary using the values as probabilities (unnormalized) - """ - cutoff = random() - dist = log_normalize(dist) - #print "Normalizer: ", normalizer - - current = 0 - for ii in xrange(len(dist)): - current += exp(dist[ii]) - if current >= cutoff: - #print "Chose", i - return ii - assert False, "Didn't choose anything: %f %f" % (cutoff, current) + coeff = [76.18009173, -86.50532033, 24.01409822, -1.231739516, 0.120858003e-2, -0.536382e-5] + x = xx - 1.0 + tmp = x + 5.5 + tmp = tmp - (x + 0.5) * log(tmp) + ser = 1.0 + for j in range(len(coeff)): + x = x + 1 + ser = ser + coeff[j] / x + return -tmp + log(2.50662827465 * ser) -def create_data(stories, lang="english", doc_limit=-1, delimiter=""): - from nltk.tokenize.treebank import TreebankWordTokenizer - tokenizer = TreebankWordTokenizer() - - from nltk.corpus import stopwords - stop = stopwords.words('english') - - from string import ascii_lowercase - - docs = {} - print("Found %i stories" % stories.count()) - for story in stories: - text = zlib.decompress(story.story_content_z) - # text = story.story_title - text = ''.join(BeautifulSoup(text, features="lxml").findAll(text=True)).lower() - if delimiter: - sections = text.split(delimiter) + +def log_sum(log_a, log_b): + if log_a < log_b: + return log_b + log(1 + exp(log_a - log_b)) else: - sections = [text] - - if doc_limit > 0 and len(docs) > doc_limit: - print("Passed doc limit %i" % len(docs)) - break - print(story.story_title, len(sections)) - - for jj in xrange(len(sections)): - docs["%s-%i" % (story.story_title, jj)] = [x for x in tokenizer.tokenize(sections[jj]) \ - if (not x in stop) and \ - (min(y in ascii_lowercase for y in x))] - return docs + return log_a + log(1 + exp(log_b - log_a)) -class LdaSampler: - def __init__(self, num_topics, doc_smoothing = 0.1, topic_smoothing = 0.01): - self._docs = defaultdict(FreqDist) - self._topics = defaultdict(FreqDist) - self._K = num_topics - self._state = None - - self._alpha = doc_smoothing - self._lambda = topic_smoothing - - def optimize_hyperparameters(self, samples=5, step = 3.0): - rawParam = [log(self._alpha), log(self._lambda)] - - for ii in xrange(samples): - lp_old = self.lhood(self._alpha, self._lambda) - lp_new = log(random()) + lp_old - print("OLD: %f\tNEW: %f at (%f, %f)" % (lp_old, lp_new, self._alpha, self._lambda)) - - l = [x - random() * step for x in rawParam] - r = [x + step for x in rawParam] - - for jj in xrange(100): - rawParamNew = [l[x] + random() * (r[x] - l[x]) for x in xrange(len(rawParam))] - trial_alpha, trial_lambda = [exp(x) for x in rawParamNew] - lp_test = self.lhood(trial_alpha, trial_lambda) - #print("TRYING: %f (need %f) at (%f, %f)" % (lp_test - lp_old, lp_new - lp_old, trial_alpha, trial_lambda)) - - if lp_test > lp_new: - print(jj) - self._alpha = exp(rawParamNew[0]) - self._lambda = exp(rawParamNew[1]) - self._alpha_sum = self._alpha * self._K - self._lambda_sum = self._lambda * self._W - rawParam = [log(self._alpha), log(self._lambda)] - break - else: - for dd in xrange(len(rawParamNew)): - if rawParamNew[dd] < rawParam[dd]: - l[dd] = rawParamNew[dd] - else: - r[dd] = rawParamNew[dd] - assert l[dd] <= rawParam[dd] - assert r[dd] >= rawParam[dd] - - print("\nNew hyperparameters (%i): %f %f" % (jj, self._alpha, self._lambda)) - - def lhood(self, doc_smoothing, voc_smoothing): - doc_sum = doc_smoothing * self._K - voc_sum = voc_smoothing * self._W - - val = 0.0 - val += lgammln(doc_sum) * len(self._docs) - val -= lgammln(doc_smoothing) * self._K * len(self._docs) - for ii in self._docs: - for jj in xrange(self._K): - val += lgammln(doc_smoothing + self._docs[ii][jj]) - val -= lgammln(doc_sum + self._docs[ii].N()) - - val += lgammln(voc_sum) * self._K - val -= lgammln(voc_smoothing) * self._W * self._K - for ii in self._topics: - for jj in self._vocab: - val += lgammln(voc_smoothing + self._topics[ii][jj]) - val -= lgammln(voc_sum + self._topics[ii].N()) - return val - - def initialize(self, data): - """ - Data should be keyed by doc-id, values should be iterable - """ - self._alpha_sum = self._alpha * self._K - self._state = defaultdict(dict) +def log_normalize(dist): + normalizer = reduce(log_sum, dist) + for ii in xrange(len(dist)): + dist[ii] -= normalizer + return dist - self._vocab = set([]) - for dd in data: - for ww in xrange(len(data[dd])): - # Learn all the words we'll see - self._vocab.add(data[dd][ww]) - # Initialize the state to unassigned - self._state[dd][ww] = -1 +def log_sample(dist): + """ + Sample a key from a dictionary using the values as probabilities (unnormalized) + """ + cutoff = random() + dist = log_normalize(dist) + # print "Normalizer: ", normalizer - self._W = len(self._vocab) - self._lambda_sum = float(self._W) * self._lambda + current = 0 + for ii in xrange(len(dist)): + current += exp(dist[ii]) + if current >= cutoff: + # print "Chose", i + return ii + assert False, "Didn't choose anything: %f %f" % (cutoff, current) - self._data = data - print("Initialized vocab of size %i" % len(self._vocab)) +def create_data(stories, lang="english", doc_limit=-1, delimiter=""): + from nltk.tokenize.treebank import TreebankWordTokenizer - def prob(self, doc, word, topic): - val = log(self._docs[doc][topic] + self._alpha) - # This is constant across a document, so we don't need to compute this term - # val -= log(self._docs[doc].N() + self._alpha_sum) - - val += log(self._topics[topic][word] + self._lambda) - val -= log(self._topics[topic].N() + self._lambda_sum) + tokenizer = TreebankWordTokenizer() - # print doc, word, topic, self._docs[doc][topic], self._topics[topic][word] - - return val + from nltk.corpus import stopwords - def sample_word(self, doc, position): - word = self._data[doc][position] + stop = stopwords.words("english") - old_topic = self._state[doc][position] - if old_topic != -1: - self.change_count(doc, word, old_topic, -1) + from string import ascii_lowercase - probs = [self.prob(doc, self._data[doc][position], x) for x in xrange(self._K)] - new_topic = log_sample(probs) - #print doc, word, new_topic + docs = {} + print("Found %i stories" % stories.count()) + for story in stories: + text = zlib.decompress(story.story_content_z) + # text = story.story_title + text = "".join(BeautifulSoup(text, features="lxml").findAll(text=True)).lower() + if delimiter: + sections = text.split(delimiter) + else: + sections = [text] - self.change_count(doc, word, new_topic, 1) - self._state[doc][position] = new_topic + if doc_limit > 0 and len(docs) > doc_limit: + print("Passed doc limit %i" % len(docs)) + break + print(story.story_title, len(sections)) - def change_count(self, doc, word, topic, delta): - self._docs[doc].inc(topic, delta) - self._topics[topic].inc(word, delta) + for jj in xrange(len(sections)): + docs["%s-%i" % (story.story_title, jj)] = [ + x + for x in tokenizer.tokenize(sections[jj]) + if (not x in stop) and (min(y in ascii_lowercase for y in x)) + ] + return docs - def sample(self, iterations = 100, hyper_delay = 10): - assert self._state - for ii in xrange(iterations): - for dd in self._data: - for ww in xrange(len(self._data[dd])): - self.sample_word(dd, ww) - print("Iteration %i %f" % (ii, self.lhood(self._alpha, self._lambda))) - if hyper_delay >= 0 and ii % hyper_delay == 0: - self.optimize_hyperparameters() - def print_topics(self, num_words=15): - for ii in self._topics: - print("%i:%s\n" % (ii, "\t".join(self._topics[ii].keys()[:num_words]))) +class LdaSampler: + def __init__(self, num_topics, doc_smoothing=0.1, topic_smoothing=0.01): + self._docs = defaultdict(FreqDist) + self._topics = defaultdict(FreqDist) + self._K = num_topics + self._state = None + + self._alpha = doc_smoothing + self._lambda = topic_smoothing + + def optimize_hyperparameters(self, samples=5, step=3.0): + rawParam = [log(self._alpha), log(self._lambda)] + + for ii in xrange(samples): + lp_old = self.lhood(self._alpha, self._lambda) + lp_new = log(random()) + lp_old + print("OLD: %f\tNEW: %f at (%f, %f)" % (lp_old, lp_new, self._alpha, self._lambda)) + + l = [x - random() * step for x in rawParam] + r = [x + step for x in rawParam] + + for jj in xrange(100): + rawParamNew = [l[x] + random() * (r[x] - l[x]) for x in xrange(len(rawParam))] + trial_alpha, trial_lambda = [exp(x) for x in rawParamNew] + lp_test = self.lhood(trial_alpha, trial_lambda) + # print("TRYING: %f (need %f) at (%f, %f)" % (lp_test - lp_old, lp_new - lp_old, trial_alpha, trial_lambda)) + + if lp_test > lp_new: + print(jj) + self._alpha = exp(rawParamNew[0]) + self._lambda = exp(rawParamNew[1]) + self._alpha_sum = self._alpha * self._K + self._lambda_sum = self._lambda * self._W + rawParam = [log(self._alpha), log(self._lambda)] + break + else: + for dd in xrange(len(rawParamNew)): + if rawParamNew[dd] < rawParam[dd]: + l[dd] = rawParamNew[dd] + else: + r[dd] = rawParamNew[dd] + assert l[dd] <= rawParam[dd] + assert r[dd] >= rawParam[dd] + + print("\nNew hyperparameters (%i): %f %f" % (jj, self._alpha, self._lambda)) + + def lhood(self, doc_smoothing, voc_smoothing): + doc_sum = doc_smoothing * self._K + voc_sum = voc_smoothing * self._W + + val = 0.0 + val += lgammln(doc_sum) * len(self._docs) + val -= lgammln(doc_smoothing) * self._K * len(self._docs) + for ii in self._docs: + for jj in xrange(self._K): + val += lgammln(doc_smoothing + self._docs[ii][jj]) + val -= lgammln(doc_sum + self._docs[ii].N()) + + val += lgammln(voc_sum) * self._K + val -= lgammln(voc_smoothing) * self._W * self._K + for ii in self._topics: + for jj in self._vocab: + val += lgammln(voc_smoothing + self._topics[ii][jj]) + val -= lgammln(voc_sum + self._topics[ii].N()) + return val + + def initialize(self, data): + """ + Data should be keyed by doc-id, values should be iterable + """ + + self._alpha_sum = self._alpha * self._K + self._state = defaultdict(dict) + + self._vocab = set([]) + for dd in data: + for ww in xrange(len(data[dd])): + # Learn all the words we'll see + self._vocab.add(data[dd][ww]) + + # Initialize the state to unassigned + self._state[dd][ww] = -1 + + self._W = len(self._vocab) + self._lambda_sum = float(self._W) * self._lambda + + self._data = data + + print("Initialized vocab of size %i" % len(self._vocab)) + + def prob(self, doc, word, topic): + val = log(self._docs[doc][topic] + self._alpha) + # This is constant across a document, so we don't need to compute this term + # val -= log(self._docs[doc].N() + self._alpha_sum) + + val += log(self._topics[topic][word] + self._lambda) + val -= log(self._topics[topic].N() + self._lambda_sum) + + # print doc, word, topic, self._docs[doc][topic], self._topics[topic][word] + + return val + + def sample_word(self, doc, position): + word = self._data[doc][position] + + old_topic = self._state[doc][position] + if old_topic != -1: + self.change_count(doc, word, old_topic, -1) + + probs = [self.prob(doc, self._data[doc][position], x) for x in xrange(self._K)] + new_topic = log_sample(probs) + # print doc, word, new_topic + + self.change_count(doc, word, new_topic, 1) + self._state[doc][position] = new_topic + + def change_count(self, doc, word, topic, delta): + self._docs[doc].inc(topic, delta) + self._topics[topic].inc(word, delta) + + def sample(self, iterations=100, hyper_delay=10): + assert self._state + for ii in xrange(iterations): + for dd in self._data: + for ww in xrange(len(self._data[dd])): + self.sample_word(dd, ww) + print("Iteration %i %f" % (ii, self.lhood(self._alpha, self._lambda))) + if hyper_delay >= 0 and ii % hyper_delay == 0: + self.optimize_hyperparameters() + + def print_topics(self, num_words=15): + for ii in self._topics: + print("%i:%s\n" % (ii, "\t".join(self._topics[ii].keys()[:num_words]))) if __name__ == "__main__": - stories = MStory.objects(story_feed_id=199) - d = create_data(stories, doc_limit=250, delimiter="") - lda = LdaSampler(5) - lda.initialize(d) + stories = MStory.objects(story_feed_id=199) + d = create_data(stories, doc_limit=250, delimiter="") + lda = LdaSampler(5) + lda.initialize(d) - lda.sample(50) - lda.print_topics() \ No newline at end of file + lda.sample(50) + lda.print_topics() diff --git a/apps/analyzer/migrations/0001_initial.py b/apps/analyzer/migrations/0001_initial.py index b83bf543f9..19af099c66 100644 --- a/apps/analyzer/migrations/0001_initial.py +++ b/apps/analyzer/migrations/0001_initial.py @@ -1,39 +1,54 @@ # Generated by Django 2.0 on 2020-06-16 06:52 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ - ('rss_feeds', '0001_initial'), + ("rss_feeds", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('category', models.CharField(max_length=255)), - ('count', models.IntegerField(default=0)), - ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rss_feeds.Feed')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("category", models.CharField(max_length=255)), + ("count", models.IntegerField(default=0)), + ("feed", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="rss_feeds.Feed")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.CreateModel( - name='FeatureCategory', + name="FeatureCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('feature', models.CharField(max_length=255)), - ('category', models.CharField(max_length=255)), - ('count', models.IntegerField(default=0)), - ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rss_feeds.Feed')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("feature", models.CharField(max_length=255)), + ("category", models.CharField(max_length=255)), + ("count", models.IntegerField(default=0)), + ("feed", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="rss_feeds.Feed")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), ], ), ] diff --git a/apps/analyzer/models.py b/apps/analyzer/models.py index dc3d01f551..3bdd16e997 100644 --- a/apps/analyzer/models.py +++ b/apps/analyzer/models.py @@ -1,33 +1,37 @@ import datetime -import mongoengine as mongo from collections import defaultdict -from django.db import models + +import mongoengine as mongo +from django.conf import settings from django.contrib.auth.models import User -from django.template.loader import render_to_string from django.core.mail import EmailMultiAlternatives -from django.conf import settings -from apps.rss_feeds.models import Feed +from django.db import models +from django.template.loader import render_to_string + from apps.analyzer.tasks import EmailPopularityQuery +from apps.rss_feeds.models import Feed from utils import log as logging + class FeatureCategory(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) feed = models.ForeignKey(Feed, on_delete=models.CASCADE) feature = models.CharField(max_length=255) category = models.CharField(max_length=255) count = models.IntegerField(default=0) - + def __str__(self): - return '%s - %s (%s)' % (self.feature, self.category, self.count) + return "%s - %s (%s)" % (self.feature, self.category, self.count) + class Category(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) feed = models.ForeignKey(Feed, on_delete=models.CASCADE) category = models.CharField(max_length=255) count = models.IntegerField(default=0) - + def __str__(self): - return '%s (%s)' % (self.category, self.count) + return "%s (%s)" % (self.category, self.count) class MPopularityQuery(mongo.Document): @@ -35,55 +39,53 @@ class MPopularityQuery(mongo.Document): query = mongo.StringField() is_emailed = mongo.BooleanField() creation_date = mongo.DateTimeField(default=datetime.datetime.now) - + meta = { - 'collection': 'popularity_query', - 'allow_inheritance': False, + "collection": "popularity_query", + "allow_inheritance": False, } - + def __str__(self): - return "%s - \"%s\"" % (self.email, self.query) + return '%s - "%s"' % (self.email, self.query) def queue_email(self): EmailPopularityQuery.delay(pk=str(self.pk)) - + @classmethod def ensure_all_sent(cls, queue=True): - for query in cls.objects.all().order_by('creation_date'): + for query in cls.objects.all().order_by("creation_date"): query.ensure_sent(queue=queue) - + def ensure_sent(self, queue=True): if self.is_emailed: logging.debug(" ---> Already sent %s" % self) return - + if queue: self.queue_email() else: self.send_email() - + def send_email(self, limit=5000): filename = Feed.xls_query_popularity(self.query, limit=limit) xlsx = open(filename, "r") - - params = { - 'query': self.query - } - text = render_to_string('mail/email_popularity_query.txt', params) - html = render_to_string('mail/email_popularity_query.xhtml', params) - subject = "Keyword popularity spreadsheet: \"%s\"" % self.query - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['<%s>' % (self.email)]) + + params = {"query": self.query} + text = render_to_string("mail/email_popularity_query.txt", params) + html = render_to_string("mail/email_popularity_query.xhtml", params) + subject = 'Keyword popularity spreadsheet: "%s"' % self.query + msg = EmailMultiAlternatives( + subject, text, from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, to=["<%s>" % (self.email)] + ) msg.attach_alternative(html, "text/html") - msg.attach(filename, xlsx.read(), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + msg.attach(filename, xlsx.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") msg.send() - + self.is_emailed = True self.save() - + logging.debug(" -> ~BB~FM~SBSent email for popularity query: %s" % self) - + class MClassifierTitle(mongo.Document): user_id = mongo.IntField() @@ -92,68 +94,69 @@ class MClassifierTitle(mongo.Document): title = mongo.StringField(max_length=255) score = mongo.IntField() creation_date = mongo.DateTimeField() - + meta = { - 'collection': 'classifier_title', - 'indexes': [('user_id', 'feed_id'), 'feed_id', ('user_id', 'social_user_id'), 'social_user_id'], - 'allow_inheritance': False, + "collection": "classifier_title", + "indexes": [("user_id", "feed_id"), "feed_id", ("user_id", "social_user_id"), "social_user_id"], + "allow_inheritance": False, } - + def __str__(self): user = User.objects.get(pk=self.user_id) return "%s - %s/%s: (%s) %s" % (user, self.feed_id, self.social_user_id, self.score, self.title[:30]) - - + + class MClassifierAuthor(mongo.Document): - user_id = mongo.IntField(unique_with=('feed_id', 'social_user_id', 'author')) + user_id = mongo.IntField(unique_with=("feed_id", "social_user_id", "author")) feed_id = mongo.IntField() social_user_id = mongo.IntField() author = mongo.StringField(max_length=255) score = mongo.IntField() creation_date = mongo.DateTimeField() - + meta = { - 'collection': 'classifier_author', - 'indexes': [('user_id', 'feed_id'), 'feed_id', ('user_id', 'social_user_id'), 'social_user_id'], - 'allow_inheritance': False, + "collection": "classifier_author", + "indexes": [("user_id", "feed_id"), "feed_id", ("user_id", "social_user_id"), "social_user_id"], + "allow_inheritance": False, } - + def __str__(self): user = User.objects.get(pk=self.user_id) return "%s - %s/%s: (%s) %s" % (user, self.feed_id, self.social_user_id, self.score, self.author[:30]) + class MClassifierTag(mongo.Document): - user_id = mongo.IntField(unique_with=('feed_id', 'social_user_id', 'tag')) + user_id = mongo.IntField(unique_with=("feed_id", "social_user_id", "tag")) feed_id = mongo.IntField() social_user_id = mongo.IntField() tag = mongo.StringField(max_length=255) score = mongo.IntField() creation_date = mongo.DateTimeField() - + meta = { - 'collection': 'classifier_tag', - 'indexes': [('user_id', 'feed_id'), 'feed_id', ('user_id', 'social_user_id'), 'social_user_id'], - 'allow_inheritance': False, + "collection": "classifier_tag", + "indexes": [("user_id", "feed_id"), "feed_id", ("user_id", "social_user_id"), "social_user_id"], + "allow_inheritance": False, } - + def __str__(self): user = User.objects.get(pk=self.user_id) return "%s - %s/%s: (%s) %s" % (user, self.feed_id, self.social_user_id, self.score, self.tag[:30]) - + class MClassifierFeed(mongo.Document): - user_id = mongo.IntField(unique_with=('feed_id', 'social_user_id')) + user_id = mongo.IntField(unique_with=("feed_id", "social_user_id")) feed_id = mongo.IntField() social_user_id = mongo.IntField() score = mongo.IntField() creation_date = mongo.DateTimeField() - + meta = { - 'collection': 'classifier_feed', - 'indexes': [('user_id', 'feed_id'), 'feed_id', ('user_id', 'social_user_id'), 'social_user_id'], - 'allow_inheritance': False, + "collection": "classifier_feed", + "indexes": [("user_id", "feed_id"), "feed_id", ("user_id", "social_user_id"), "social_user_id"], + "allow_inheritance": False, } - + def __str__(self): user = User.objects.get(pk=self.user_id) if self.feed_id: @@ -161,94 +164,105 @@ def __str__(self): else: feed = User.objects.get(pk=self.social_user_id) return "%s - %s/%s: (%s) %s" % (user, self.feed_id, self.social_user_id, self.score, feed) - + def compute_story_score(story, classifier_titles, classifier_authors, classifier_tags, classifier_feeds): intelligence = { - 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']), - 'author': apply_classifier_authors(classifier_authors, story), - 'tags': apply_classifier_tags(classifier_tags, story), - 'title': apply_classifier_titles(classifier_titles, story), + "feed": apply_classifier_feeds(classifier_feeds, story["story_feed_id"]), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } score = 0 - score_max = max(intelligence['title'], - intelligence['author'], - intelligence['tags']) - score_min = min(intelligence['title'], - intelligence['author'], - intelligence['tags']) + score_max = max(intelligence["title"], intelligence["author"], intelligence["tags"]) + score_min = min(intelligence["title"], intelligence["author"], intelligence["tags"]) if score_max > 0: score = score_max elif score_min < 0: score = score_min if score == 0: - score = intelligence['feed'] - + score = intelligence["feed"] + return score - + + def apply_classifier_titles(classifiers, story): score = 0 for classifier in classifiers: - if classifier.feed_id != story['story_feed_id']: + if classifier.feed_id != story["story_feed_id"]: continue - if classifier.title.lower() in story['story_title'].lower(): + if classifier.title.lower() in story["story_title"].lower(): # print 'Titles: (%s) %s -- %s' % (classifier.title in story['story_title'], classifier.title, story['story_title']) score = classifier.score - if score > 0: return score + if score > 0: + return score return score - + + def apply_classifier_authors(classifiers, story): score = 0 for classifier in classifiers: - if classifier.feed_id != story['story_feed_id']: + if classifier.feed_id != story["story_feed_id"]: continue - if story.get('story_authors') and classifier.author == story.get('story_authors'): + if story.get("story_authors") and classifier.author == story.get("story_authors"): # print 'Authors: %s -- %s' % (classifier.author, story['story_authors']) score = classifier.score - if score > 0: return classifier.score + if score > 0: + return classifier.score return score - + + def apply_classifier_tags(classifiers, story): score = 0 for classifier in classifiers: - if classifier.feed_id != story['story_feed_id']: + if classifier.feed_id != story["story_feed_id"]: continue - if story['story_tags'] and classifier.tag in story['story_tags']: + if story["story_tags"] and classifier.tag in story["story_tags"]: # print 'Tags: (%s-%s) %s -- %s' % (classifier.tag in story['story_tags'], classifier.score, classifier.tag, story['story_tags']) score = classifier.score - if score > 0: return classifier.score + if score > 0: + return classifier.score return score - + + def apply_classifier_feeds(classifiers, feed, social_user_ids=None): - if not feed and not social_user_ids: return 0 + if not feed and not social_user_ids: + return 0 feed_id = None if feed: feed_id = feed if isinstance(feed, int) else feed.pk - + if social_user_ids and not isinstance(social_user_ids, list): social_user_ids = [social_user_ids] - + for classifier in classifiers: if classifier.feed_id == feed_id: # print 'Feeds: %s -- %s' % (classifier.feed_id, feed.pk) return classifier.score - if (social_user_ids and not classifier.feed_id and - classifier.social_user_id in social_user_ids): + if social_user_ids and not classifier.feed_id and classifier.social_user_id in social_user_ids: return classifier.score return 0 - -def get_classifiers_for_user(user, feed_id=None, social_user_id=None, classifier_feeds=None, classifier_authors=None, - classifier_titles=None, classifier_tags=None): + + +def get_classifiers_for_user( + user, + feed_id=None, + social_user_id=None, + classifier_feeds=None, + classifier_authors=None, + classifier_titles=None, + classifier_tags=None, +): params = dict(user_id=user.pk) if isinstance(feed_id, list): - params['feed_id__in'] = feed_id + params["feed_id__in"] = feed_id elif feed_id: - params['feed_id'] = feed_id + params["feed_id"] = feed_id if social_user_id: if isinstance(social_user_id, str): - social_user_id = int(social_user_id.replace('social:', '')) - params['social_user_id'] = social_user_id + social_user_id = int(social_user_id.replace("social:", "")) + params["social_user_id"] = social_user_id if classifier_authors is None: classifier_authors = list(MClassifierAuthor.objects(**params)) @@ -258,49 +272,56 @@ def get_classifiers_for_user(user, feed_id=None, social_user_id=None, classifier classifier_tags = list(MClassifierTag.objects(**params)) if classifier_feeds is None: if not social_user_id and feed_id: - params['social_user_id'] = 0 + params["social_user_id"] = 0 classifier_feeds = list(MClassifierFeed.objects(**params)) - + feeds = [] for f in classifier_feeds: if f.social_user_id and not f.feed_id: - feeds.append(('social:%s' % f.social_user_id, f.score)) + feeds.append(("social:%s" % f.social_user_id, f.score)) else: feeds.append((f.feed_id, f.score)) - + payload = { - 'feeds': dict(feeds), - 'authors': dict([(a.author, a.score) for a in classifier_authors]), - 'titles': dict([(t.title, t.score) for t in classifier_titles]), - 'tags': dict([(t.tag, t.score) for t in classifier_tags]), + "feeds": dict(feeds), + "authors": dict([(a.author, a.score) for a in classifier_authors]), + "titles": dict([(t.title, t.score) for t in classifier_titles]), + "tags": dict([(t.tag, t.score) for t in classifier_tags]), } - + return payload - -def sort_classifiers_by_feed(user, feed_ids=None, - classifier_feeds=None, - classifier_authors=None, - classifier_titles=None, - classifier_tags=None): + + +def sort_classifiers_by_feed( + user, + feed_ids=None, + classifier_feeds=None, + classifier_authors=None, + classifier_titles=None, + classifier_tags=None, +): def sort_by_feed(classifiers): feed_classifiers = defaultdict(list) for classifier in classifiers: feed_classifiers[classifier.feed_id].append(classifier) return feed_classifiers - + classifiers = {} if feed_ids: - classifier_feeds = sort_by_feed(classifier_feeds) + classifier_feeds = sort_by_feed(classifier_feeds) classifier_authors = sort_by_feed(classifier_authors) - classifier_titles = sort_by_feed(classifier_titles) - classifier_tags = sort_by_feed(classifier_tags) + classifier_titles = sort_by_feed(classifier_titles) + classifier_tags = sort_by_feed(classifier_tags) for feed_id in feed_ids: - classifiers[feed_id] = get_classifiers_for_user(user, feed_id=feed_id, - classifier_feeds=classifier_feeds[feed_id], - classifier_authors=classifier_authors[feed_id], - classifier_titles=classifier_titles[feed_id], - classifier_tags=classifier_tags[feed_id]) - + classifiers[feed_id] = get_classifiers_for_user( + user, + feed_id=feed_id, + classifier_feeds=classifier_feeds[feed_id], + classifier_authors=classifier_authors[feed_id], + classifier_titles=classifier_titles[feed_id], + classifier_tags=classifier_tags[feed_id], + ) + return classifiers diff --git a/apps/analyzer/phrase_filter.py b/apps/analyzer/phrase_filter.py index 70fe77c01f..fe025f84bb 100644 --- a/apps/analyzer/phrase_filter.py +++ b/apps/analyzer/phrase_filter.py @@ -1,39 +1,39 @@ import re from pprint import pprint + class PhraseFilter: - def __init__(self): self.phrases = {} - + def run(self, text, storyid): chunks = self.chunk(text) self.count_phrases(chunks, storyid) - + def print_phrases(self): pprint(self.phrases) - + def get_phrases(self): return self.phrases.keys() - + # =========== # = Chunker = # =========== - + def chunk(self, text): - chunks = [t.strip() for t in re.split('[^a-zA-Z-]+', text) if t] + chunks = [t.strip() for t in re.split("[^a-zA-Z-]+", text) if t] # chunks = self._lowercase(chunks) return chunks - + def _lowercase(self, chunks): return [c.lower() for c in chunks] - + # ================== # = Phrase Counter = # ================== - + def count_phrases(self, chunks, storyid): - for l in range(1, len(chunks)+1): + for l in range(1, len(chunks) + 1): combinations = self._get_combinations(chunks, l) # print "Combinations: %s" % combinations for phrase in combinations: @@ -41,23 +41,23 @@ def count_phrases(self, chunks, storyid): self.phrases[phrase] = [] if storyid not in self.phrases[phrase]: self.phrases[phrase].append(storyid) - + def _get_combinations(self, chunks, length): combinations = [] for i, chunk in enumerate(chunks): # 0,1,2,3,4,5,6 = 01 12 23 34 45 56 combination = [] for l in range(length): - if i+l < len(chunks): + if i + l < len(chunks): # print i, l, chunks[i+l], len(chunks) - combination.append(chunks[i+l]) - combinations.append(' '.join(combination)) + combination.append(chunks[i + l]) + combinations.append(" ".join(combination)) return combinations - + # ================= # = Phrase Paring = # ================= - + def pare_phrases(self): # Kill singles for phrase, counts in self.phrases.items(): @@ -67,27 +67,32 @@ def pare_phrases(self): if len(phrase) < 4: del self.phrases[phrase] continue - + # Kill repeats for phrase in self.phrases.keys(): for phrase2 in self.phrases.keys(): - if phrase in self.phrases and len(phrase2) > len(phrase) and phrase in phrase2 and phrase != phrase2: + if ( + phrase in self.phrases + and len(phrase2) > len(phrase) + and phrase in phrase2 + and phrase != phrase2 + ): del self.phrases[phrase] - -if __name__ == '__main__': + + +if __name__ == "__main__": phrasefilter = PhraseFilter() - phrasefilter.run('House of the Day: 123 Atlantic Ave. #3', 1) - phrasefilter.run('House of the Day: 456 Plankton St. #3', 4) - phrasefilter.run('Coop of the Day: 321 Pacific St.', 2) - phrasefilter.run('Streetlevel: 393 Pacific St.', 11) - phrasefilter.run('Coop of the Day: 456 Jefferson Ave.', 3) - phrasefilter.run('Extra, Extra', 5) - phrasefilter.run('Extra, Extra', 6) - phrasefilter.run('Early Addition', 7) - phrasefilter.run('Early Addition', 8) - phrasefilter.run('Development Watch', 9) - phrasefilter.run('Streetlevel', 10) - + phrasefilter.run("House of the Day: 123 Atlantic Ave. #3", 1) + phrasefilter.run("House of the Day: 456 Plankton St. #3", 4) + phrasefilter.run("Coop of the Day: 321 Pacific St.", 2) + phrasefilter.run("Streetlevel: 393 Pacific St.", 11) + phrasefilter.run("Coop of the Day: 456 Jefferson Ave.", 3) + phrasefilter.run("Extra, Extra", 5) + phrasefilter.run("Extra, Extra", 6) + phrasefilter.run("Early Addition", 7) + phrasefilter.run("Early Addition", 8) + phrasefilter.run("Development Watch", 9) + phrasefilter.run("Streetlevel", 10) + phrasefilter.pare_phrases() phrasefilter.print_phrases() - \ No newline at end of file diff --git a/apps/analyzer/tasks.py b/apps/analyzer/tasks.py index c41736d12d..5741e15a90 100644 --- a/apps/analyzer/tasks.py +++ b/apps/analyzer/tasks.py @@ -1,12 +1,12 @@ from newsblur_web.celeryapp import app from utils import log as logging + @app.task() def EmailPopularityQuery(pk): from apps.analyzer.models import MPopularityQuery - + query = MPopularityQuery.objects.get(pk=pk) logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query) - + query.send_email() - diff --git a/apps/analyzer/tests.py b/apps/analyzer/tests.py index a69739247d..8cbf1513af 100644 --- a/apps/analyzer/tests.py +++ b/apps/analyzer/tests.py @@ -1,26 +1,29 @@ -from django.test.client import Client -from apps.rss_feeds.models import MStory -from django.test import TestCase -from django.core import management +from itertools import groupby + # from apps.analyzer.classifier import FisherClassifier import nltk -from itertools import groupby +from django.core import management +from django.test import TestCase +from django.test.client import Client + +from apps.analyzer.phrase_filter import PhraseFilter from apps.analyzer.tokenizer import Tokenizer +from apps.rss_feeds.models import MStory from vendor.reverend.thomas import Bayes -from apps.analyzer.phrase_filter import PhraseFilter class QuadgramCollocationFinder(nltk.collocations.AbstractCollocationFinder): - """A tool for the finding and ranking of quadgram collocations or other association measures. + """A tool for the finding and ranking of quadgram collocations or other association measures. It is often useful to use from_words() rather thanconstructing an instance directly. """ + def __init__(self, word_fd, quadgram_fd, trigram_fd, bigram_fd, wildcard_fd): """Construct a TrigramCollocationFinder, given FreqDists for appearances of words, bigrams, two words with any word between them,and trigrams.""" nltk.collocations.AbstractCollocationFinder.__init__(self, word_fd, quadgram_fd) self.trigram_fd = trigram_fd self.bigram_fd = bigram_fd self.wildcard_fd = wildcard_fd - + @classmethod def from_words(cls, words): wfd = nltk.probability.FreqDist() @@ -28,20 +31,20 @@ def from_words(cls, words): tfd = nltk.probability.FreqDist() bfd = nltk.probability.FreqDist() wildfd = nltk.probability.FreqDist() - - for w1, w2, w3 ,w4 in nltk.util.ingrams(words, 4, pad_right=True): + + for w1, w2, w3, w4 in nltk.util.ingrams(words, 4, pad_right=True): wfd.inc(w1) if w4 is None: continue else: - qfd.inc((w1,w2,w3,w4)) - bfd.inc((w1,w2)) - tfd.inc((w1,w2,w3)) - wildfd.inc((w1,w3,w4)) - wildfd.inc((w1,w2,w4)) - + qfd.inc((w1, w2, w3, w4)) + bfd.inc((w1, w2)) + tfd.inc((w1, w2, w3)) + wildfd.inc((w1, w3, w4)) + wildfd.inc((w1, w2, w4)) + return cls(wfd, qfd, tfd, bfd, wildfd) - + def score_ngram(self, score_fn, w1, w2, w3, w4): n_all = self.word_fd.N() n_iiii = self.ngram_fd[(w1, w2, w3, w4)] @@ -59,63 +62,78 @@ def score_ngram(self, score_fn, w1, w2, w3, w4): n_xixi = self.trigram_fd[(w2, w3)] n_xxii = self.trigram_fd[(w3, w4)] n_xxxi = self.trigram_fd[(w3, w4)] - return score_fn(n_iiii, - (n_iiix, n_iixi, n_ixii, n_xiii), - (n_iixx, n_ixix, n_ixxi, n_ixxx), - (n_xiix, n_xixi, n_xxii, n_xxxi), - n_all) + return score_fn( + n_iiii, + (n_iiix, n_iixi, n_ixii, n_xiii), + (n_iixx, n_ixix, n_ixxi, n_ixxx), + (n_xiix, n_xixi, n_xxii, n_xxxi), + n_all, + ) + - class CollocationTest(TestCase): - - fixtures = ['brownstoner.json'] - + fixtures = ["brownstoner.json"] + def setUp(self): self.client = Client() - + def test_bigrams(self): # bigram_measures = nltk.collocations.BigramAssocMeasures() trigram_measures = nltk.collocations.TrigramAssocMeasures() tokens = [ - 'Co-op', 'of', 'the', 'day', - 'House', 'of', 'the', 'day', - 'Condo', 'of', 'the', 'day', - 'Development', 'Watch', - 'Co-op', 'of', 'the', 'day', + "Co-op", + "of", + "the", + "day", + "House", + "of", + "the", + "day", + "Condo", + "of", + "the", + "day", + "Development", + "Watch", + "Co-op", + "of", + "the", + "day", ] finder = nltk.collocations.TrigramCollocationFinder.from_words(tokens) - + finder.apply_freq_filter(2) - + # return the 10 n-grams with the highest PMI print(finder.nbest(trigram_measures.pmi, 10)) titles = [ - 'Co-op of the day', - 'Condo of the day', - 'Co-op of the day', - 'House of the day', - 'Development Watch', - 'Streetlevel', + "Co-op of the day", + "Condo of the day", + "Co-op of the day", + "House of the day", + "Development Watch", + "Streetlevel", ] - tokens = nltk.tokenize.word(' '.join(titles)) + tokens = nltk.tokenize.word(" ".join(titles)) ngrams = nltk.ngrams(tokens, 4) d = [key for key, group in groupby(sorted(ngrams)) if len(list(group)) >= 2] print(d) + class ClassifierTest(TestCase): - - fixtures = ['classifiers.json', 'brownstoner.json'] - + fixtures = ["classifiers.json", "brownstoner.json"] + def setUp(self): self.client = Client() - # + + # # def test_filter(self): # user = User.objects.all() # feed = Feed.objects.all() - # + # # management.call_command('loaddata', 'brownstoner.json', verbosity=0) # response = self.client.get('/reader/refresh_feed', { "feed_id": 1, "force": True }) # management.call_command('loaddata', 'brownstoner2.json', verbosity=0) @@ -124,28 +142,32 @@ def setUp(self): # response = self.client.get('/reader/refresh_feed', { "feed_id": 4, "force": True }) # management.call_command('loaddata', 'gothamist2.json', verbosity=0) # response = self.client.get('/reader/refresh_feed', { "feed_id": 4, "force": True }) - # + # # stories = Story.objects.filter(story_feed=feed[1]).order_by('-story_date')[:100] - # + # # phrasefilter = PhraseFilter() # for story in stories: # # print story.story_title, story.id # phrasefilter.run(story.story_title, story.id) - # + # # phrasefilter.pare_phrases() # phrasefilter.print_phrases() - # + # def test_train(self): # user = User.objects.all() # feed = Feed.objects.all() - - management.call_command('loaddata', 'brownstoner.json', verbosity=0, commit=False, skip_checks=False) - management.call_command('refresh_feed', force=1, feed=1, single_threaded=True, daemonize=False, skip_checks=False) - management.call_command('loaddata', 'brownstoner2.json', verbosity=0, commit=False, skip_checks=False) - management.call_command('refresh_feed', force=1, feed=1, single_threaded=True, daemonize=False, skip_checks=False) - + + management.call_command("loaddata", "brownstoner.json", verbosity=0, commit=False, skip_checks=False) + management.call_command( + "refresh_feed", force=1, feed=1, single_threaded=True, daemonize=False, skip_checks=False + ) + management.call_command("loaddata", "brownstoner2.json", verbosity=0, commit=False, skip_checks=False) + management.call_command( + "refresh_feed", force=1, feed=1, single_threaded=True, daemonize=False, skip_checks=False + ) + stories = MStory.objects(story_feed_id=1)[:53] - + phrasefilter = PhraseFilter() for story in stories: # print story.story_title, story.id @@ -154,46 +176,45 @@ def test_train(self): phrasefilter.pare_phrases() phrases = phrasefilter.get_phrases() print(phrases) - + tokenizer = Tokenizer(phrases) - classifier = Bayes(tokenizer) # FisherClassifier(user[0], feed[0], phrases) - - classifier.train('good', 'House of the Day: 393 Pacific St.') - classifier.train('good', 'House of the Day: 393 Pacific St.') - classifier.train('good', 'Condo of the Day: 393 Pacific St.') - classifier.train('good', 'Co-op of the Day: 393 Pacific St. #3') - classifier.train('good', 'Co-op of the Day: 393 Pacific St. #3') - classifier.train('good', 'Development Watch: 393 Pacific St. #3') - classifier.train('bad', 'Development Watch: 393 Pacific St. #3') - classifier.train('bad', 'Development Watch: 393 Pacific St. #3') - classifier.train('bad', 'Development Watch: 393 Pacific St. #3') - classifier.train('bad', 'Streetlevel: 393 Pacific St. #3') - - guess = dict(classifier.guess('Co-op of the Day: 413 Atlantic')) - self.assertTrue(guess['good'] > .99) - self.assertTrue('bad' not in guess) - - guess = dict(classifier.guess('House of the Day: 413 Atlantic')) - self.assertTrue(guess['good'] > .99) - self.assertTrue('bad' not in guess) - - guess = dict(classifier.guess('Development Watch: Yatta')) - self.assertTrue(guess['bad'] > .7) - self.assertTrue(guess['good'] < .3) - - guess = dict(classifier.guess('Development Watch: 393 Pacific St.')) - self.assertTrue(guess['bad'] > .7) - self.assertTrue(guess['good'] < .3) - - guess = dict(classifier.guess('Streetlevel: 123 Carlton St.')) - self.assertTrue(guess['bad'] > .99) - self.assertTrue('good' not in guess) - - guess = classifier.guess('Extra, Extra') - self.assertTrue('bad' not in guess) - self.assertTrue('good' not in guess) - - guess = classifier.guess('Nothing doing: 393 Pacific St.') - self.assertTrue('bad' not in guess) - self.assertTrue('good' not in guess) - \ No newline at end of file + classifier = Bayes(tokenizer) # FisherClassifier(user[0], feed[0], phrases) + + classifier.train("good", "House of the Day: 393 Pacific St.") + classifier.train("good", "House of the Day: 393 Pacific St.") + classifier.train("good", "Condo of the Day: 393 Pacific St.") + classifier.train("good", "Co-op of the Day: 393 Pacific St. #3") + classifier.train("good", "Co-op of the Day: 393 Pacific St. #3") + classifier.train("good", "Development Watch: 393 Pacific St. #3") + classifier.train("bad", "Development Watch: 393 Pacific St. #3") + classifier.train("bad", "Development Watch: 393 Pacific St. #3") + classifier.train("bad", "Development Watch: 393 Pacific St. #3") + classifier.train("bad", "Streetlevel: 393 Pacific St. #3") + + guess = dict(classifier.guess("Co-op of the Day: 413 Atlantic")) + self.assertTrue(guess["good"] > 0.99) + self.assertTrue("bad" not in guess) + + guess = dict(classifier.guess("House of the Day: 413 Atlantic")) + self.assertTrue(guess["good"] > 0.99) + self.assertTrue("bad" not in guess) + + guess = dict(classifier.guess("Development Watch: Yatta")) + self.assertTrue(guess["bad"] > 0.7) + self.assertTrue(guess["good"] < 0.3) + + guess = dict(classifier.guess("Development Watch: 393 Pacific St.")) + self.assertTrue(guess["bad"] > 0.7) + self.assertTrue(guess["good"] < 0.3) + + guess = dict(classifier.guess("Streetlevel: 123 Carlton St.")) + self.assertTrue(guess["bad"] > 0.99) + self.assertTrue("good" not in guess) + + guess = classifier.guess("Extra, Extra") + self.assertTrue("bad" not in guess) + self.assertTrue("good" not in guess) + + guess = classifier.guess("Nothing doing: 393 Pacific St.") + self.assertTrue("bad" not in guess) + self.assertTrue("good" not in guess) diff --git a/apps/analyzer/tfidf.py b/apps/analyzer/tfidf.py index 08fe0e4f0e..9fdd98f601 100755 --- a/apps/analyzer/tfidf.py +++ b/apps/analyzer/tfidf.py @@ -6,8 +6,9 @@ See the README for a usage example. """ -import sys import os +import sys + class tfidf: def __init__(self): @@ -19,7 +20,7 @@ def addDocument(self, doc_name, list_of_words): # building a dictionary doc_dict = {} for w in list_of_words: - doc_dict[w] = doc_dict.get(w, 0.) + 1.0 + doc_dict[w] = doc_dict.get(w, 0.0) + 1.0 self.corpus_dict[w] = self.corpus_dict.get(w, 0.0) + 1.0 # normalizing the dictionary @@ -53,4 +54,4 @@ def similarities(self, list_of_words): score += (query_dict[k] / self.corpus_dict[k]) + (doc_dict[k] / self.corpus_dict[k]) sims.append([doc[0], score]) - return sims \ No newline at end of file + return sims diff --git a/apps/analyzer/tokenizer.py b/apps/analyzer/tokenizer.py index 83885398b6..0ca40f7e00 100644 --- a/apps/analyzer/tokenizer.py +++ b/apps/analyzer/tokenizer.py @@ -1,28 +1,30 @@ import re + class Tokenizer: """A simple regex-based whitespace tokenizer. It expects a string and can return all tokens lower-cased or in their existing case. """ - - WORD_RE = re.compile('[^a-zA-Z-]+') + + WORD_RE = re.compile("[^a-zA-Z-]+") def __init__(self, phrases, lower=False): self.phrases = phrases self.lower = lower - + def tokenize(self, doc): print(doc) - formatted_doc = ' '.join(self.WORD_RE.split(doc)) + formatted_doc = " ".join(self.WORD_RE.split(doc)) print(formatted_doc) for phrase in self.phrases: if phrase in formatted_doc: yield phrase - -if __name__ == '__main__': - phrases = ['Extra Extra', 'Streetlevel', 'House of the Day'] + + +if __name__ == "__main__": + phrases = ["Extra Extra", "Streetlevel", "House of the Day"] tokenizer = Tokenizer(phrases) - doc = 'Extra, Extra' - tokenizer.tokenize(doc) \ No newline at end of file + doc = "Extra, Extra" + tokenizer.tokenize(doc) diff --git a/apps/analyzer/urls.py b/apps/analyzer/urls.py index 3812755628..21a91189fc 100644 --- a/apps/analyzer/urls.py +++ b/apps/analyzer/urls.py @@ -1,9 +1,10 @@ from django.conf.urls import url + from apps.analyzer import views urlpatterns = [ - url(r'^$', views.index), - url(r'^save/?', views.save_classifier), - url(r'^popularity/?', views.popularity_query), - url(r'^(?P\d+)', views.get_classifiers_feed), + url(r"^$", views.index), + url(r"^save/?", views.save_classifier), + url(r"^popularity/?", views.popularity_query), + url(r"^(?P\d+)", views.get_classifiers_feed), ] diff --git a/apps/analyzer/views.py b/apps/analyzer/views.py index d06b72625b..66e738c698 100644 --- a/apps/analyzer/views.py +++ b/apps/analyzer/views.py @@ -1,48 +1,57 @@ import redis -from utils import log as logging -from django.shortcuts import get_object_or_404 -from django.views.decorators.http import require_POST from django.conf import settings -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render +from django.views.decorators.http import require_POST from mongoengine.queryset import NotUniqueError -from apps.rss_feeds.models import Feed -from apps.reader.models import UserSubscription -from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag -from apps.analyzer.models import get_classifiers_for_user, MPopularityQuery + from apps.analyzer.forms import PopularityQueryForm +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + MPopularityQuery, + get_classifiers_for_user, +) +from apps.reader.models import UserSubscription +from apps.rss_feeds.models import Feed from apps.social.models import MSocialSubscription from utils import json_functions as json -from utils.user_functions import get_user -from utils.user_functions import ajax_login_required +from utils import log as logging +from utils.user_functions import ajax_login_required, get_user + def index(requst): pass - + + @require_POST @ajax_login_required @json.json_view def save_classifier(request): post = request.POST - feed_id = post['feed_id'] + feed_id = post["feed_id"] feed = None social_user_id = None - if feed_id.startswith('social:'): - social_user_id = int(feed_id.replace('social:', '')) + if feed_id.startswith("social:"): + social_user_id = int(feed_id.replace("social:", "")) feed_id = None else: feed_id = int(feed_id) feed = get_object_or_404(Feed, pk=feed_id) code = 0 - message = 'OK' + message = "OK" payload = {} logging.user(request, "~FGSaving classifier: ~SB%s~SN ~FW%s" % (feed, post)) - + # Mark subscription as dirty, so unread counts can be recalculated usersub = None socialsub = None if social_user_id: - socialsub = MSocialSubscription.objects.get(user_id=request.user.pk, subscription_user_id=social_user_id) + socialsub = MSocialSubscription.objects.get( + user_id=request.user.pk, subscription_user_id=social_user_id + ) if not socialsub.needs_unread_recalc: socialsub.needs_unread_recalc = True socialsub.save() @@ -55,31 +64,31 @@ def save_classifier(request): usersub.needs_unread_recalc = True usersub.is_trained = True usersub.save() - - + def _save_classifier(ClassifierCls, content_type): classifiers = { - 'like_'+content_type: 1, - 'dislike_'+content_type: -1, - 'remove_like_'+content_type: 0, - 'remove_dislike_'+content_type: 0, + "like_" + content_type: 1, + "dislike_" + content_type: -1, + "remove_like_" + content_type: 0, + "remove_dislike_" + content_type: 0, } for opinion, score in classifiers.items(): if opinion in post: post_contents = post.getlist(opinion) for post_content in post_contents: - if not post_content: continue + if not post_content: + continue classifier_dict = { - 'user_id': request.user.pk, - 'feed_id': feed_id or 0, - 'social_user_id': social_user_id or 0, + "user_id": request.user.pk, + "feed_id": feed_id or 0, + "social_user_id": social_user_id or 0, } - if content_type in ('author', 'tag', 'title'): + if content_type in ("author", "tag", "title"): max_length = ClassifierCls._fields[content_type].max_length classifier_dict.update({content_type: post_content[:max_length]}) - if content_type == 'feed': - if not post_content.startswith('social:'): - classifier_dict['feed_id'] = post_content + if content_type == "feed": + if not post_content.startswith("social:"): + classifier_dict["feed_id"] = post_content try: classifier = ClassifierCls.objects.get(**classifier_dict) except ClassifierCls.DoesNotExist: @@ -94,59 +103,77 @@ def _save_classifier(ClassifierCls, content_type): classifier.delete() elif classifier.score != score: if score == 0: - if ((classifier.score == 1 and opinion.startswith('remove_like')) - or (classifier.score == -1 and opinion.startswith('remove_dislike'))): + if (classifier.score == 1 and opinion.startswith("remove_like")) or ( + classifier.score == -1 and opinion.startswith("remove_dislike") + ): classifier.delete() else: classifier.score = score classifier.save() - - _save_classifier(MClassifierAuthor, 'author') - _save_classifier(MClassifierTag, 'tag') - _save_classifier(MClassifierTitle, 'title') - _save_classifier(MClassifierFeed, 'feed') + + _save_classifier(MClassifierAuthor, "author") + _save_classifier(MClassifierTag, "tag") + _save_classifier(MClassifierTitle, "title") + _save_classifier(MClassifierFeed, "feed") r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'feed:%s' % feed_id) + r.publish(request.user.username, "feed:%s" % feed_id) response = dict(code=code, message=message, payload=payload) return response - + + @json.json_view def get_classifiers_feed(request, feed_id): user = get_user(request) code = 0 - + payload = get_classifiers_for_user(user, feed_id=feed_id) - + response = dict(code=code, payload=payload) - + return response + def popularity_query(request): - if request.method == 'POST': + if request.method == "POST": form = PopularityQueryForm(request.POST) if form.is_valid(): - logging.user(request.user, "~BC~FRPopularity query: ~SB%s~SN requests \"~SB~FM%s~SN~FR\"" % (request.POST['email'], request.POST['query'])) - query = MPopularityQuery.objects.create(email=request.POST['email'], - query=request.POST['query']) + logging.user( + request.user, + '~BC~FRPopularity query: ~SB%s~SN requests "~SB~FM%s~SN~FR"' + % (request.POST["email"], request.POST["query"]), + ) + query = MPopularityQuery.objects.create(email=request.POST["email"], query=request.POST["query"]) query.queue_email() - - response = render(request, 'analyzer/popularity_query.xhtml', { - 'success': True, - 'popularity_query_form': form, - }) - response.set_cookie('newsblur_popularity_query', request.POST['query']) - + + response = render( + request, + "analyzer/popularity_query.xhtml", + { + "success": True, + "popularity_query_form": form, + }, + ) + response.set_cookie("newsblur_popularity_query", request.POST["query"]) + return response else: - logging.user(request.user, "~BC~FRFailed popularity query: ~SB%s~SN requests \"~SB~FM%s~SN~FR\"" % (request.POST['email'], request.POST['query'])) + logging.user( + request.user, + '~BC~FRFailed popularity query: ~SB%s~SN requests "~SB~FM%s~SN~FR"' + % (request.POST["email"], request.POST["query"]), + ) else: logging.user(request.user, "~BC~FRPopularity query form loading") - form = PopularityQueryForm(initial={'query': request.COOKIES.get('newsblur_popularity_query', "")}) - - response = render(request, 'analyzer/popularity_query.xhtml', { - 'popularity_query_form': form, - }) + form = PopularityQueryForm(initial={"query": request.COOKIES.get("newsblur_popularity_query", "")}) + + response = render( + request, + "analyzer/popularity_query.xhtml", + { + "popularity_query_form": form, + }, + ) return response diff --git a/apps/api/tests.py b/apps/api/tests.py index c7c4668e12..f51d798ffd 100644 --- a/apps/api/tests.py +++ b/apps/api/tests.py @@ -7,6 +7,7 @@ from django.test import TestCase + class SimpleTest(TestCase): def test_basic_addition(self): """ @@ -14,10 +15,12 @@ def test_basic_addition(self): """ self.assertEqual(1 + 1, 2) -__test__ = {"doctest": """ + +__test__ = { + "doctest": """ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True -"""} - +""" +} diff --git a/apps/api/urls.py b/apps/api/urls.py index bd4b6d43f7..9a3603e166 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -1,19 +1,20 @@ from django.conf.urls import url + from apps.api import views urlpatterns = [ - url(r'^logout', views.logout, name='api-logout'), - url(r'^login', views.login, name='api-login'), - url(r'^signup', views.signup, name='api-signup'), - url(r'^add_site_load_script/(?P\w+)', views.add_site_load_script, name='api-add-site-load-script'), - url(r'^add_site/(?P\w+)', views.add_site, name='api-add-site'), - url(r'^add_url/(?P\w+)', views.add_site, name='api-add-site'), - url(r'^add_site/?$', views.add_site_authed, name='api-add-site-authed'), - url(r'^add_url/?$', views.add_site_authed, name='api-add-site-authed'), - url(r'^check_share_on_site/(?P\w+)', views.check_share_on_site, name='api-check-share-on-site'), - url(r'^share_story/(?P\w+)', views.share_story, name='api-share-story'), - url(r'^save_story/(?P\w+)', views.save_story, name='api-save-story'), - url(r'^share_story/?$', views.share_story), - url(r'^save_story/?$', views.save_story), - url(r'^ip_addresses/?$', views.ip_addresses), + url(r"^logout", views.logout, name="api-logout"), + url(r"^login", views.login, name="api-login"), + url(r"^signup", views.signup, name="api-signup"), + url(r"^add_site_load_script/(?P\w+)", views.add_site_load_script, name="api-add-site-load-script"), + url(r"^add_site/(?P\w+)", views.add_site, name="api-add-site"), + url(r"^add_url/(?P\w+)", views.add_site, name="api-add-site"), + url(r"^add_site/?$", views.add_site_authed, name="api-add-site-authed"), + url(r"^add_url/?$", views.add_site_authed, name="api-add-site-authed"), + url(r"^check_share_on_site/(?P\w+)", views.check_share_on_site, name="api-check-share-on-site"), + url(r"^share_story/(?P\w+)", views.share_story, name="api-share-story"), + url(r"^save_story/(?P\w+)", views.save_story, name="api-save-story"), + url(r"^share_story/?$", views.share_story), + url(r"^save_story/?$", views.save_story), + url(r"^ip_addresses/?$", views.ip_addresses), ] diff --git a/apps/api/views.py b/apps/api/views.py index 1c23fbe5e8..5db5dd9248 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -29,10 +29,10 @@ def login(request): code = -1 errors = None - user_agent = request.environ.get('HTTP_USER_AGENT', '') - ip = request.META.get('HTTP_X_FORWARDED_FOR', None) or request.META['REMOTE_ADDR'] + user_agent = request.environ.get("HTTP_USER_AGENT", "") + ip = request.META.get("HTTP_X_FORWARDED_FOR", None) or request.META["REMOTE_ADDR"] - if not user_agent or user_agent.lower() in ['nativehost']: + if not user_agent or user_agent.lower() in ["nativehost"]: errors = dict(user_agent="You must set a user agent to login.") logging.user(request, "~FG~BB~SK~FRBlocked ~FGAPI Login~SN~FW: %s / %s" % (user_agent, ip)) elif request.method == "POST": @@ -40,19 +40,20 @@ def login(request): if form.errors: errors = form.errors if form.is_valid(): - login_user(request, form.get_user(), backend='django.contrib.auth.backends.ModelBackend') + login_user(request, form.get_user(), backend="django.contrib.auth.backends.ModelBackend") logging.user(request, "~FG~BB~SKAPI Login~SN~FW: %s / %s" % (user_agent, ip)) code = 1 else: errors = dict(method="Invalid method. Use POST. You used %s" % request.method) - + return dict(code=code, errors=errors) - + + @json.json_view def signup(request): code = -1 errors = None - ip = request.META.get('HTTP_X_FORWARDED_FOR', None) or request.META['REMOTE_ADDR'] + ip = request.META.get("HTTP_X_FORWARDED_FOR", None) or request.META["REMOTE_ADDR"] if request.method == "POST": form = SignupForm(data=request.POST) @@ -61,48 +62,47 @@ def signup(request): if form.is_valid(): try: new_user = form.save() - login_user(request, new_user, backend='django.contrib.auth.backends.ModelBackend') + login_user(request, new_user, backend="django.contrib.auth.backends.ModelBackend") logging.user(request, "~FG~SB~BBAPI NEW SIGNUP: ~FW%s / %s" % (new_user.email, ip)) code = 1 except forms.ValidationError as e: errors = [e.args[0]] else: errors = dict(method="Invalid method. Use POST. You used %s" % request.method) - return dict(code=code, errors=errors) - + + @json.json_view def logout(request): code = 1 logging.user(request, "~FG~BBAPI Logout~FW") logout_user(request) - + return dict(code=code) + def add_site_load_script(request, token): code = 0 usf = None profile = None user_profile = None starred_counts = {} - - def image_base64(image_name, path='icons/circular/'): - image_file = open(os.path.join(settings.MEDIA_ROOT, 'img/%s%s' % (path, image_name)), 'rb') - return base64.b64encode(image_file.read()).decode('utf-8') - - accept_image = image_base64('newuser_icn_setup.png') - error_image = image_base64('newuser_icn_sharewith_active.png') - new_folder_image = image_base64('g_icn_arrow_right.png') - add_image = image_base64('g_icn_expand_hover.png') + + def image_base64(image_name, path="icons/circular/"): + image_file = open(os.path.join(settings.MEDIA_ROOT, "img/%s%s" % (path, image_name)), "rb") + return base64.b64encode(image_file.read()).decode("utf-8") + + accept_image = image_base64("newuser_icn_setup.png") + error_image = image_base64("newuser_icn_sharewith_active.png") + new_folder_image = image_base64("g_icn_arrow_right.png") + add_image = image_base64("g_icn_expand_hover.png") try: profiles = Profile.objects.filter(secret_token=token) if profiles: profile = profiles[0] - usf = UserSubscriptionFolders.objects.get( - user=profile.user - ) + usf = UserSubscriptionFolders.objects.get(user=profile.user) user_profile = MSocialProfile.get_user(user_id=profile.user.pk) starred_counts = MStarredStoryCounts.user_counts(profile.user.pk) else: @@ -111,29 +111,34 @@ def image_base64(image_name, path='icons/circular/'): code = -1 except UserSubscriptionFolders.DoesNotExist: code = -1 - - return render(request, 'api/share_bookmarklet.js', { - 'code': code, - 'token': token, - 'folders': (usf and usf.folders) or [], - 'user': profile and profile.user or {}, - 'user_profile': user_profile and json.encode(user_profile.canonical()) or {}, - 'starred_counts': json.encode(starred_counts), - 'accept_image': accept_image, - 'error_image': error_image, - 'add_image': add_image, - 'new_folder_image': new_folder_image, - }, - content_type='application/javascript') + + return render( + request, + "api/share_bookmarklet.js", + { + "code": code, + "token": token, + "folders": (usf and usf.folders) or [], + "user": profile and profile.user or {}, + "user_profile": user_profile and json.encode(user_profile.canonical()) or {}, + "starred_counts": json.encode(starred_counts), + "accept_image": accept_image, + "error_image": error_image, + "add_image": add_image, + "new_folder_image": new_folder_image, + }, + content_type="application/javascript", + ) + def add_site(request, token): - code = 0 - get_post = getattr(request, request.method) - url = get_post.get('url') - folder = get_post.get('folder') - new_folder = get_post.get('new_folder') - callback = get_post.get('callback', '') - + code = 0 + get_post = getattr(request, request.method) + url = get_post.get("url") + folder = get_post.get("folder") + new_folder = get_post.get("new_folder") + callback = get_post.get("callback", "") + if not url: code = -1 else: @@ -144,35 +149,40 @@ def add_site(request, token): usf.add_folder(folder, new_folder) folder = new_folder code, message, us = UserSubscription.add_subscription( - user=profile.user, - feed_address=url, - folder=folder, - bookmarklet=True + user=profile.user, feed_address=url, folder=folder, bookmarklet=True ) except Profile.DoesNotExist: code = -1 - + if code > 0: - message = 'OK' - - logging.user(profile.user, "~FRAdding URL from site: ~SB%s (in %s)" % (url, folder), - request=request) - - return HttpResponse(callback + '(' + json.encode({ - 'code': code, - 'message': message, - 'usersub': us and us.feed_id, - }) + ')', content_type='text/plain') + message = "OK" + + logging.user(profile.user, "~FRAdding URL from site: ~SB%s (in %s)" % (url, folder), request=request) + + return HttpResponse( + callback + + "(" + + json.encode( + { + "code": code, + "message": message, + "usersub": us and us.feed_id, + } + ) + + ")", + content_type="text/plain", + ) + @ajax_login_required def add_site_authed(request): - code = 0 - url = request.GET['url'] - folder = request.GET['folder'] - new_folder = request.GET.get('new_folder') - callback = request.GET['callback'] - user = get_user(request) - + code = 0 + url = request.GET["url"] + folder = request.GET["folder"] + new_folder = request.GET.get("new_folder") + callback = request.GET["callback"] + user = get_user(request) + if not url: code = -1 else: @@ -181,40 +191,45 @@ def add_site_authed(request): usf.add_folder(folder, new_folder) folder = new_folder code, message, us = UserSubscription.add_subscription( - user=user, - feed_address=url, - folder=folder, - bookmarklet=True + user=user, feed_address=url, folder=folder, bookmarklet=True ) - + if code > 0: - message = 'OK' - - logging.user(user, "~FRAdding authed URL from site: ~SB%s (in %s)" % (url, folder), - request=request) - - return HttpResponse(callback + '(' + json.encode({ - 'code': code, - 'message': message, - 'usersub': us and us.feed_id, - }) + ')', content_type='text/plain') + message = "OK" + + logging.user(user, "~FRAdding authed URL from site: ~SB%s (in %s)" % (url, folder), request=request) + + return HttpResponse( + callback + + "(" + + json.encode( + { + "code": code, + "message": message, + "usersub": us and us.feed_id, + } + ) + + ")", + content_type="text/plain", + ) + def check_share_on_site(request, token): - code = 0 - story_url = request.GET['story_url'] - rss_url = request.GET.get('rss_url') - callback = request.GET['callback'] + code = 0 + story_url = request.GET["story_url"] + rss_url = request.GET.get("rss_url") + callback = request.GET["callback"] other_stories = None same_stories = None - usersub = None - message = None - user = None + usersub = None + message = None + user = None users = {} your_story = None same_stories = None other_stories = None previous_stories = None - + if not story_url: code = -1 else: @@ -223,7 +238,7 @@ def check_share_on_site(request, token): user = user_profile.user except Profile.DoesNotExist: code = -1 - + logging.user(request.user, "~FBFinding feed (check_share_on_site): %s" % rss_url) feed = Feed.get_feed_from_url(rss_url, create=False, fetch=False) if not feed: @@ -239,9 +254,9 @@ def check_share_on_site(request, token): logging.user(request.user, "~FBFinding feed (check_share_on_site): %s" % base_url) feed = Feed.get_feed_from_url(base_url, create=False, fetch=False) if not feed: - logging.user(request.user, "~FBFinding feed (check_share_on_site): %s" % (base_url + '/')) - feed = Feed.get_feed_from_url(base_url+'/', create=False, fetch=False) - + logging.user(request.user, "~FBFinding feed (check_share_on_site): %s" % (base_url + "/")) + feed = Feed.get_feed_from_url(base_url + "/", create=False, fetch=False) + if feed and user: try: usersub = UserSubscription.objects.filter(user=user, feed=feed) @@ -249,23 +264,27 @@ def check_share_on_site(request, token): usersub = None if user: feed_id = feed and feed.pk - your_story, same_stories, other_stories = MSharedStory.get_shared_stories_from_site(feed_id, - user_id=user.pk, story_url=story_url) - previous_stories = MSharedStory.objects.filter(user_id=user.pk).order_by('-shared_date').limit(3) - previous_stories = [{ - "user_id": story.user_id, - "story_title": story.story_title, - "comments": story.comments, - "shared_date": story.shared_date, - "relative_date": relative_timesince(story.shared_date), - "blurblog_permalink": story.blurblog_permalink(), - } for story in previous_stories] - + your_story, same_stories, other_stories = MSharedStory.get_shared_stories_from_site( + feed_id, user_id=user.pk, story_url=story_url + ) + previous_stories = MSharedStory.objects.filter(user_id=user.pk).order_by("-shared_date").limit(3) + previous_stories = [ + { + "user_id": story.user_id, + "story_title": story.story_title, + "comments": story.comments, + "shared_date": story.shared_date, + "relative_date": relative_timesince(story.shared_date), + "blurblog_permalink": story.blurblog_permalink(), + } + for story in previous_stories + ] + user_ids = set([user_profile.user.pk]) for story in same_stories: - user_ids.add(story['user_id']) + user_ids.add(story["user_id"]) for story in other_stories: - user_ids.add(story['user_id']) + user_ids.add(story["user_id"]) profiles = MSocialProfile.profiles(user_ids) for profile in profiles: @@ -273,39 +292,47 @@ def check_share_on_site(request, token): "username": profile.username, "photo_url": profile.photo_url, } - - logging.user(user, "~BM~FCChecking share from site: ~SB%s" % (story_url), - request=request) - - response = HttpResponse(callback + '(' + json.encode({ - 'code' : code, - 'message' : message, - 'feed' : feed, - 'subscribed' : bool(usersub), - 'your_story' : your_story, - 'same_stories' : same_stories, - 'other_stories' : other_stories, - 'previous_stories' : previous_stories, - 'users' : users, - }) + ')', content_type='text/plain') - response['Access-Control-Allow-Origin'] = '*' - response['Access-Control-Allow-Methods'] = 'GET' - + + logging.user(user, "~BM~FCChecking share from site: ~SB%s" % (story_url), request=request) + + response = HttpResponse( + callback + + "(" + + json.encode( + { + "code": code, + "message": message, + "feed": feed, + "subscribed": bool(usersub), + "your_story": your_story, + "same_stories": same_stories, + "other_stories": other_stories, + "previous_stories": previous_stories, + "users": users, + } + ) + + ")", + content_type="text/plain", + ) + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Methods"] = "GET" + return response -@required_params('story_url') + +@required_params("story_url") def share_story(request, token=None): - code = 0 - story_url = request.POST['story_url'] - comments = request.POST.get('comments', "") - title = request.POST.get('title', None) - content = request.POST.get('content', None) - rss_url = request.POST.get('rss_url', None) - feed_id = request.POST.get('feed_id', None) or 0 - feed = None - message = None - profile = None - + code = 0 + story_url = request.POST["story_url"] + comments = request.POST.get("comments", "") + title = request.POST.get("title", None) + content = request.POST.get("content", None) + rss_url = request.POST.get("rss_url", None) + feed_id = request.POST.get("feed_id", None) or 0 + feed = None + message = None + profile = None + if request.user.is_authenticated: profile = request.user.profile else: @@ -317,14 +344,19 @@ def share_story(request, token=None): message = "Not authenticated, couldn't find user by token." else: message = "Not authenticated, no token supplied and not authenticated." - + if not profile: - return HttpResponse(json.encode({ - 'code': code, - 'message': message, - 'story': None, - }), content_type='text/plain') - + return HttpResponse( + json.encode( + { + "code": code, + "message": message, + "story": None, + } + ), + content_type="text/plain", + ) + if feed_id: feed = Feed.get_by_id(feed_id) else: @@ -336,7 +368,7 @@ def share_story(request, token=None): feed = Feed.get_feed_from_url(story_url, create=True, fetch=True) if feed: feed_id = feed.pk - + if content: content = lxml.html.fromstring(content) content.make_links_absolute(story_url) @@ -346,13 +378,15 @@ def share_story(request, token=None): importer = TextImporter(story=None, story_url=story_url, request=request, debug=settings.DEBUG) document = importer.fetch(skip_save=True, return_document=True) if not content: - content = document['content'] + content = document["content"] if not title: - title = document['title'] - - shared_story = MSharedStory.objects.filter(user_id=profile.user.pk, - story_feed_id=feed_id, - story_guid=story_url).limit(1).first() + title = document["title"] + + shared_story = ( + MSharedStory.objects.filter(user_id=profile.user.pk, story_feed_id=feed_id, story_guid=story_url) + .limit(1) + .first() + ) if not shared_story: story_db = { "story_guid": story_url, @@ -361,7 +395,6 @@ def share_story(request, token=None): "story_feed_id": feed_id, "story_content": content, "story_date": datetime.datetime.now(), - "user_id": profile.user.pk, "comments": comments, "has_comments": bool(comments), @@ -382,49 +415,57 @@ def share_story(request, token=None): shared_story.has_comments = bool(comments) shared_story.story_feed_id = feed_id shared_story.save() - logging.user(profile.user, "~BM~FY~SBUpdating~SN shared story from site: ~SB%s: %s" % (story_url, comments)) + logging.user( + profile.user, "~BM~FY~SBUpdating~SN shared story from site: ~SB%s: %s" % (story_url, comments) + ) message = "Updating shared story from site: %s: %s" % (story_url, comments) try: - socialsub = MSocialSubscription.objects.get(user_id=profile.user.pk, - subscription_user_id=profile.user.pk) + socialsub = MSocialSubscription.objects.get( + user_id=profile.user.pk, subscription_user_id=profile.user.pk + ) except MSocialSubscription.DoesNotExist: socialsub = None - + if socialsub: - socialsub.mark_story_ids_as_read([shared_story.story_hash], - shared_story.story_feed_id, - request=request) + socialsub.mark_story_ids_as_read( + [shared_story.story_hash], shared_story.story_feed_id, request=request + ) else: RUserStory.mark_read(profile.user.pk, shared_story.story_feed_id, shared_story.story_hash) - shared_story.publish_update_to_subscribers() - - response = HttpResponse(json.encode({ - 'code': code, - 'message': message, - 'story': shared_story, - }), content_type='text/plain') - response['Access-Control-Allow-Origin'] = '*' - response['Access-Control-Allow-Methods'] = 'POST' - + + response = HttpResponse( + json.encode( + { + "code": code, + "message": message, + "story": shared_story, + } + ), + content_type="text/plain", + ) + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Methods"] = "POST" + return response -@required_params('story_url', 'title') + +@required_params("story_url", "title") def save_story(request, token=None): - code = 0 - story_url = request.POST['story_url'] - user_tags = request.POST.getlist('user_tags') or request.POST.getlist('user_tags[]') or [] - add_user_tag = request.POST.get('add_user_tag', None) - title = request.POST['title'] - content = request.POST.get('content', None) - rss_url = request.POST.get('rss_url', None) - user_notes = request.POST.get('user_notes', None) - feed_id = request.POST.get('feed_id', None) or 0 - feed = None - message = None - profile = None - + code = 0 + story_url = request.POST["story_url"] + user_tags = request.POST.getlist("user_tags") or request.POST.getlist("user_tags[]") or [] + add_user_tag = request.POST.get("add_user_tag", None) + title = request.POST["title"] + content = request.POST.get("content", None) + rss_url = request.POST.get("rss_url", None) + user_notes = request.POST.get("user_notes", None) + feed_id = request.POST.get("feed_id", None) or 0 + feed = None + message = None + profile = None + if request.user.is_authenticated: profile = request.user.profile else: @@ -436,14 +477,19 @@ def save_story(request, token=None): message = "Not authenticated, couldn't find user by token." else: message = "Not authenticated, no token supplied and not authenticated." - + if not profile: - return HttpResponse(json.encode({ - 'code': code, - 'message': message, - 'story': None, - }), content_type='text/plain') - + return HttpResponse( + json.encode( + { + "code": code, + "message": message, + "story": None, + } + ), + content_type="text/plain", + ) + if feed_id: feed = Feed.get_by_id(feed_id) else: @@ -455,7 +501,7 @@ def save_story(request, token=None): feed = Feed.get_feed_from_url(story_url, create=True, fetch=True) if feed: feed_id = feed.pk - + if content: content = lxml.html.fromstring(content) content.make_links_absolute(story_url) @@ -463,16 +509,18 @@ def save_story(request, token=None): else: importer = TextImporter(story=None, story_url=story_url, request=request, debug=settings.DEBUG) document = importer.fetch(skip_save=True, return_document=True) - content = document['content'] + content = document["content"] if not title: - title = document['title'] - + title = document["title"] + if add_user_tag: - user_tags = user_tags + [tag for tag in add_user_tag.split(',')] - - starred_story = MStarredStory.objects.filter(user_id=profile.user.pk, - story_feed_id=feed_id, - story_guid=story_url).limit(1).first() + user_tags = user_tags + [tag for tag in add_user_tag.split(",")] + + starred_story = ( + MStarredStory.objects.filter(user_id=profile.user.pk, story_feed_id=feed_id, story_guid=story_url) + .limit(1) + .first() + ) if not starred_story: story_db = { "story_guid": story_url, @@ -498,27 +546,35 @@ def save_story(request, token=None): starred_story.story_feed_id = feed_id starred_story.user_notes = user_notes starred_story.save() - logging.user(profile.user, "~BM~FC~SBUpdating~SN starred story from site: ~SB%s: %s" % (story_url, user_tags)) + logging.user( + profile.user, "~BM~FC~SBUpdating~SN starred story from site: ~SB%s: %s" % (story_url, user_tags) + ) message = "Updating saved story from site: %s: %s" % (story_url, user_tags) MStarredStoryCounts.schedule_count_tags_for_user(request.user.pk) - - response = HttpResponse(json.encode({ - 'code': code, - 'message': message, - 'story': starred_story, - }), content_type='text/plain') - response['Access-Control-Allow-Origin'] = '*' - response['Access-Control-Allow-Methods'] = 'POST' - + + response = HttpResponse( + json.encode( + { + "code": code, + "message": message, + "story": starred_story, + } + ), + content_type="text/plain", + ) + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Methods"] = "POST" + return response + def ip_addresses(request): # Read local file /srv/newsblur/apps/api/ip_addresses.txt and return that - with open('/srv/newsblur/apps/api/ip_addresses.txt', 'r') as f: + with open("/srv/newsblur/apps/api/ip_addresses.txt", "r") as f: addresses = f.read() if request.user.is_authenticated: mail_admins(f"IP Addresses accessed from {request.META['REMOTE_ADDR']} by {request.user}", addresses) - return HttpResponse(addresses, content_type='text/plain') + return HttpResponse(addresses, content_type="text/plain") diff --git a/apps/categories/models.py b/apps/categories/models.py index 7afa63151a..02d3b1ad93 100644 --- a/apps/categories/models.py +++ b/apps/categories/models.py @@ -1,25 +1,28 @@ -import mongoengine as mongo from itertools import groupby -from apps.rss_feeds.models import Feed + +import mongoengine as mongo + from apps.reader.models import UserSubscription, UserSubscriptionFolders +from apps.rss_feeds.models import Feed from utils import json_functions as json -from utils.feed_functions import add_object_to_folder from utils import log as logging +from utils.feed_functions import add_object_to_folder + class MCategory(mongo.Document): title = mongo.StringField() description = mongo.StringField() feed_ids = mongo.ListField(mongo.IntField()) - + meta = { - 'collection': 'category', - 'indexes': ['title'], - 'allow_inheritance': False, + "collection": "category", + "indexes": ["title"], + "allow_inheritance": False, } - + def __str__(self): return "%s: %s sites" % (self.title, len(self.feed_ids)) - + @classmethod def audit(cls): categories = cls.objects.all() @@ -39,28 +42,28 @@ def audit(cls): @classmethod def add(cls, title, description): return cls.objects.create(title=title, description=description) - + @classmethod def serialize(cls, category=None): categories = cls.objects.all() if category: categories = categories.filter(title=category) - + data = dict(categories=[], feeds={}) feed_ids = set() for category in categories: category_output = { - 'title': category.title, - 'description': category.description, - 'feed_ids': category.feed_ids, + "title": category.title, + "description": category.description, + "feed_ids": category.feed_ids, } - data['categories'].append(category_output) + data["categories"].append(category_output) feed_ids.update(list(category.feed_ids)) - + feeds = Feed.objects.filter(pk__in=feed_ids) for feed in feeds: - data['feeds'][feed.pk] = feed.canonical() - + data["feeds"][feed.pk] = feed.canonical() + return data @classmethod @@ -68,8 +71,10 @@ def reload_sites(cls, category_title=None): category_sites = MCategorySite.objects.all() if category_title: category_sites = category_sites.filter(category_title=category_title) - - category_groups = groupby(sorted(category_sites, key=lambda c: c.category_title), key=lambda c: c.category_title) + + category_groups = groupby( + sorted(category_sites, key=lambda c: c.category_title), key=lambda c: c.category_title + ) for category_title, sites in category_groups: try: category = cls.objects.get(title=category_title) @@ -79,27 +84,26 @@ def reload_sites(cls, category_title=None): category.feed_ids = [site.feed_id for site in sites] category.save() print(" ---> Reloaded category: %s" % category) - + @classmethod def subscribe(cls, user_id, category_title): category = cls.objects.get(title=category_title) for feed_id in category.feed_ids: us, _ = UserSubscription.objects.get_or_create( - feed_id=feed_id, + feed_id=feed_id, user_id=user_id, defaults={ - 'needs_unread_recalc': True, - 'active': True, - } + "needs_unread_recalc": True, + "active": True, + }, ) - + usf, created = UserSubscriptionFolders.objects.get_or_create( - user_id=user_id, - defaults={'folders': '[]'} + user_id=user_id, defaults={"folders": "[]"} ) - - usf.add_folder('', category.title) + + usf.add_folder("", category.title) folders = json.decode(usf.folders) for feed_id in category.feed_ids: feed = Feed.get_by_id(feed_id) @@ -108,27 +112,26 @@ def subscribe(cls, user_id, category_title): folders = add_object_to_folder(feed.pk, category.title, folders) usf.folders = json.encode(folders) usf.save() - - + + class MCategorySite(mongo.Document): feed_id = mongo.IntField() category_title = mongo.StringField() - + meta = { - 'collection': 'category_site', - 'indexes': ['feed_id', 'category_title'], - 'allow_inheritance': False, + "collection": "category_site", + "indexes": ["feed_id", "category_title"], + "allow_inheritance": False, } - + def __str__(self): feed = Feed.get_by_id(self.feed_id) return "%s: %s" % (self.category_title, feed) - + @classmethod def add(cls, category_title, feed_id): - category_site, created = cls.objects.get_or_create(category_title=category_title, - feed_id=feed_id) - + category_site, created = cls.objects.get_or_create(category_title=category_title, feed_id=feed_id) + if not created: print(" ---> Site is already in category: %s" % category_site) else: diff --git a/apps/categories/urls.py b/apps/categories/urls.py index dda7b05ece..77950af300 100644 --- a/apps/categories/urls.py +++ b/apps/categories/urls.py @@ -1,7 +1,8 @@ from django.conf.urls import url + from apps.categories import views urlpatterns = [ - url(r'^$', views.all_categories, name='all-categories'), - url(r'^subscribe/?$', views.subscribe, name='categories-subscribe'), + url(r"^$", views.all_categories, name="all-categories"), + url(r"^subscribe/?$", views.subscribe, name="categories-subscribe"), ] diff --git a/apps/categories/views.py b/apps/categories/views.py index 3616c62142..b3d51b320d 100644 --- a/apps/categories/views.py +++ b/apps/categories/views.py @@ -3,35 +3,42 @@ from utils import json_functions as json from utils.user_functions import ajax_login_required + @json.json_view def all_categories(request): categories = MCategory.serialize() - + return categories - + + @ajax_login_required @json.json_view def subscribe(request): user = request.user categories = MCategory.serialize() - category_titles = [c['title'] for c in categories['categories']] - subscribe_category_titles = request.POST.getlist('category') or request.POST.getlist('category[]') - + category_titles = [c["title"] for c in categories["categories"]] + subscribe_category_titles = request.POST.getlist("category") or request.POST.getlist("category[]") + invalid_category_title = False for category_title in subscribe_category_titles: if category_title not in category_titles: invalid_category_title = True - + if not subscribe_category_titles or invalid_category_title: - message = "Choose one or more of these categories: %s" % ', '.join(category_titles) + message = "Choose one or more of these categories: %s" % ", ".join(category_titles) return dict(code=-1, message=message) - + for category_title in subscribe_category_titles: MCategory.subscribe(user.pk, category_title) - + usf = UserSubscriptionFolders.objects.get(user=user.pk) - - return dict(code=1, message="Subscribed to %s %s" % ( - len(subscribe_category_titles), - 'category' if len(subscribe_category_titles) == 1 else 'categories', - ), folders=json.decode(usf.folders)) \ No newline at end of file + + return dict( + code=1, + message="Subscribed to %s %s" + % ( + len(subscribe_category_titles), + "category" if len(subscribe_category_titles) == 1 else "categories", + ), + folders=json.decode(usf.folders), + ) diff --git a/apps/feed_import/migrations/0001_initial.py b/apps/feed_import/migrations/0001_initial.py index 900e91b21e..c1e9d94c76 100644 --- a/apps/feed_import/migrations/0001_initial.py +++ b/apps/feed_import/migrations/0001_initial.py @@ -1,13 +1,13 @@ # Generated by Django 2.0 on 2020-06-16 06:52 import datetime + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ @@ -16,19 +16,30 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='OAuthToken', + name="OAuthToken", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('session_id', models.CharField(blank=True, max_length=50, null=True)), - ('uuid', models.CharField(blank=True, max_length=50, null=True)), - ('remote_ip', models.CharField(blank=True, max_length=50, null=True)), - ('request_token', models.CharField(max_length=50)), - ('request_token_secret', models.CharField(max_length=50)), - ('access_token', models.CharField(max_length=50)), - ('access_token_secret', models.CharField(max_length=50)), - ('credential', models.TextField(blank=True, null=True)), - ('created_date', models.DateTimeField(default=datetime.datetime.now)), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("session_id", models.CharField(blank=True, max_length=50, null=True)), + ("uuid", models.CharField(blank=True, max_length=50, null=True)), + ("remote_ip", models.CharField(blank=True, max_length=50, null=True)), + ("request_token", models.CharField(max_length=50)), + ("request_token_secret", models.CharField(max_length=50)), + ("access_token", models.CharField(max_length=50)), + ("access_token_secret", models.CharField(max_length=50)), + ("credential", models.TextField(blank=True, null=True)), + ("created_date", models.DateTimeField(default=datetime.datetime.now)), + ( + "user", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/apps/feed_import/models.py b/apps/feed_import/models.py index 6371a1ab85..b34c7db11d 100644 --- a/apps/feed_import/models.py +++ b/apps/feed_import/models.py @@ -1,20 +1,22 @@ +import base64 import datetime -import mongoengine as mongo import pickle -import base64 -from oauth2client.client import Error as OAuthError -from xml.etree.ElementTree import Element, SubElement, Comment, tostring -from lxml import etree -from django.db import models +from xml.etree.ElementTree import Comment, Element, SubElement, tostring + +import mongoengine as mongo from django.contrib.auth.models import User +from django.db import models +from lxml import etree from mongoengine.queryset import OperationError +from oauth2client.client import Error as OAuthError + import vendor.opml as opml -from apps.rss_feeds.models import Feed, DuplicateFeed from apps.reader.models import UserSubscription, UserSubscriptionFolders -from utils import json_functions as json, urlnorm +from apps.rss_feeds.models import DuplicateFeed, Feed +from utils import json_functions as json from utils import log as logging -from utils.feed_functions import timelimit -from utils.feed_functions import add_object_to_folder +from utils import urlnorm +from utils.feed_functions import add_object_to_folder, timelimit class OAuthToken(models.Model): @@ -28,75 +30,73 @@ class OAuthToken(models.Model): access_token_secret = models.CharField(max_length=50) credential = models.TextField(null=True, blank=True) created_date = models.DateTimeField(default=datetime.datetime.now) - -class Importer: +class Importer: def clear_feeds(self): UserSubscription.objects.filter(user=self.user).delete() def clear_folders(self): UserSubscriptionFolders.objects.filter(user=self.user).delete() - + def get_folders(self): - self.usf, _ = UserSubscriptionFolders.objects.get_or_create(user=self.user, - defaults={'folders': '[]'}) + self.usf, _ = UserSubscriptionFolders.objects.get_or_create( + user=self.user, defaults={"folders": "[]"} + ) return json.decode(self.usf.folders) - + class OPMLExporter(Importer): - def __init__(self, user): self.user = user self.fetch_feeds() - + def process(self, verbose=False): now = str(datetime.datetime.now()) - root = Element('opml') - root.set('version', '1.1') - root.append(Comment('Generated by NewsBlur - newsblur.com')) - - head = SubElement(root, 'head') - title = SubElement(head, 'title') - title.text = 'NewsBlur Feeds' - dc = SubElement(head, 'dateCreated') - dc.text = now - dm = SubElement(head, 'dateModified') - dm.text = now - folders = self.get_folders() - body = SubElement(root, 'body') + root = Element("opml") + root.set("version", "1.1") + root.append(Comment("Generated by NewsBlur - newsblur.com")) + + head = SubElement(root, "head") + title = SubElement(head, "title") + title.text = "NewsBlur Feeds" + dc = SubElement(head, "dateCreated") + dc.text = now + dm = SubElement(head, "dateModified") + dm.text = now + folders = self.get_folders() + body = SubElement(root, "body") self.process_outline(body, folders, verbose=verbose) - return tostring(root, encoding='utf8', method='xml') - + return tostring(root, encoding="utf8", method="xml") + def process_outline(self, body, folders, verbose=False): for obj in folders: if isinstance(obj, int) and obj in self.feeds: feed = self.feeds[obj] if verbose: - print(" ---> Adding feed: %s - %s" % (feed['id'], - feed['feed_title'][:30])) + print(" ---> Adding feed: %s - %s" % (feed["id"], feed["feed_title"][:30])) feed_attrs = self.make_feed_row(feed) - body.append(Element('outline', feed_attrs)) + body.append(Element("outline", feed_attrs)) elif isinstance(obj, dict): for folder_title, folder_objs in list(obj.items()): if verbose: print(" ---> Adding folder: %s" % folder_title) - folder_element = Element('outline', {'text': folder_title, 'title': folder_title}) + folder_element = Element("outline", {"text": folder_title, "title": folder_title}) body.append(self.process_outline(folder_element, folder_objs, verbose=verbose)) return body - + def make_feed_row(self, feed): feed_attrs = { - 'text': feed['feed_title'], - 'title': feed['feed_title'], - 'type': 'rss', - 'version': 'RSS', - 'htmlUrl': feed['feed_link'] or "", - 'xmlUrl': feed['feed_address'] or "", + "text": feed["feed_title"], + "title": feed["feed_title"], + "type": "rss", + "version": "RSS", + "htmlUrl": feed["feed_link"] or "", + "xmlUrl": feed["feed_address"] or "", } return feed_attrs - + def fetch_feeds(self): subs = UserSubscription.objects.filter(user=self.user) self.feeds = [] @@ -113,16 +113,15 @@ def feed_count(self): class OPMLImporter(Importer): - def __init__(self, opml_xml, user): self.user = user self.opml_xml = opml_xml - + @timelimit(10) def try_processing(self): folders = self.process() return folders - + def process(self): # self.clear_feeds() @@ -136,38 +135,37 @@ def process(self): # self.clear_folders() self.usf.folders = json.encode(folders) self.usf.save() - + return folders - - def process_outline(self, outline, folders, in_folder=''): + + def process_outline(self, outline, folders, in_folder=""): for item in outline: - if (not hasattr(item, 'xmlUrl') and - (hasattr(item, 'text') or hasattr(item, 'title'))): + if not hasattr(item, "xmlUrl") and (hasattr(item, "text") or hasattr(item, "title")): folder = item - title = getattr(item, 'text', None) or getattr(item, 'title', None) + title = getattr(item, "text", None) or getattr(item, "title", None) # if hasattr(folder, 'text'): # logging.info(' ---> [%s] ~FRNew Folder: %s' % (self.user, folder.text)) obj = {title: []} folders = add_object_to_folder(obj, in_folder, folders) folders = self.process_outline(folder, folders, title) - elif hasattr(item, 'xmlUrl'): + elif hasattr(item, "xmlUrl"): feed = item - if not hasattr(feed, 'htmlUrl'): - setattr(feed, 'htmlUrl', None) + if not hasattr(feed, "htmlUrl"): + setattr(feed, "htmlUrl", None) # If feed title matches what's in the DB, don't override it on subscription. - feed_title = getattr(feed, 'title', None) or getattr(feed, 'text', None) + feed_title = getattr(feed, "title", None) or getattr(feed, "text", None) if not feed_title: - setattr(feed, 'title', feed.htmlUrl or feed.xmlUrl) + setattr(feed, "title", feed.htmlUrl or feed.xmlUrl) user_feed_title = None else: - setattr(feed, 'title', feed_title) + setattr(feed, "title", feed_title) user_feed_title = feed.title feed_address = urlnorm.normalize(feed.xmlUrl) feed_link = urlnorm.normalize(feed.htmlUrl) - if len(feed_address) > Feed._meta.get_field('feed_address').max_length: + if len(feed_address) > Feed._meta.get_field("feed_address").max_length: continue - if feed_link and len(feed_link) > Feed._meta.get_field('feed_link').max_length: + if feed_link and len(feed_link) > Feed._meta.get_field("feed_link").max_length: continue # logging.info(' ---> \t~FR%s - %s - %s' % (feed.title, feed_link, feed_address,)) feed_data = dict(feed_address=feed_address, feed_link=feed_link, feed_title=feed.title) @@ -178,32 +176,31 @@ def process_outline(self, outline, folders, in_folder=''): if duplicate_feed: feed_db = duplicate_feed[0].feed else: - feed_data['active_subscribers'] = 1 - feed_data['num_subscribers'] = 1 - feed_db, _ = Feed.find_or_create(feed_address=feed_address, - feed_link=feed_link, - defaults=dict(**feed_data)) + feed_data["active_subscribers"] = 1 + feed_data["num_subscribers"] = 1 + feed_db, _ = Feed.find_or_create( + feed_address=feed_address, feed_link=feed_link, defaults=dict(**feed_data) + ) if user_feed_title == feed_db.feed_title: user_feed_title = None - + try: - us = UserSubscription.objects.get( - feed=feed_db, - user=self.user) + us = UserSubscription.objects.get(feed=feed_db, user=self.user) except UserSubscription.DoesNotExist: us = None - + if not us: us = UserSubscription( - feed=feed_db, + feed=feed_db, user=self.user, needs_unread_recalc=True, mark_read_date=datetime.datetime.utcnow() - datetime.timedelta(days=1), active=self.user.profile.is_premium, - user_title=user_feed_title) + user_title=user_feed_title, + ) us.save() - + if self.user.profile.is_premium and not us.active: us.active = True us.save() @@ -214,25 +211,25 @@ def process_outline(self, outline, folders, in_folder=''): folders = add_object_to_folder(feed_db.pk, in_folder, folders) return folders - + def count_feeds_in_opml(self): opml_count = len(opml.from_string(self.opml_xml)) sub_count = UserSubscription.objects.filter(user=self.user).count() return max(sub_count, opml_count) - + class UploadedOPML(mongo.Document): user_id = mongo.IntField() opml_file = mongo.StringField() upload_date = mongo.DateTimeField(default=datetime.datetime.now) - + def __str__(self): user = User.objects.get(pk=self.user_id) return "%s: %s characters" % (user.username, len(self.opml_file)) - + meta = { - 'collection': 'uploaded_opml', - 'allow_inheritance': False, - 'order': '-upload_date', - 'indexes': ['user_id', '-upload_date'], + "collection": "uploaded_opml", + "allow_inheritance": False, + "order": "-upload_date", + "indexes": ["user_id", "-upload_date"], } diff --git a/apps/feed_import/tasks.py b/apps/feed_import/tasks.py index 8f9d7e67fd..0742f9918f 100644 --- a/apps/feed_import/tasks.py +++ b/apps/feed_import/tasks.py @@ -1,8 +1,9 @@ -from newsblur_web.celeryapp import app from django.contrib.auth.models import User -from apps.feed_import.models import UploadedOPML, OPMLImporter + +from apps.feed_import.models import OPMLImporter, UploadedOPML from apps.reader.models import UserSubscription from apps.social.models import MActivity +from newsblur_web.celeryapp import app from utils import log as logging @@ -12,14 +13,14 @@ def ProcessOPML(user_id): logging.user(user, "~FR~SBOPML upload (task) starting...") opml = UploadedOPML.objects.filter(user_id=user_id).first() - opml_importer = OPMLImporter(opml.opml_file.encode('utf-8'), user) + opml_importer = OPMLImporter(opml.opml_file.encode("utf-8"), user) opml_importer.process() - + feed_count = UserSubscription.objects.filter(user=user).count() user.profile.send_upload_opml_finished_email(feed_count) logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count)) MActivity.new_opml_import(user_id=user.pk, count=feed_count) - + UserSubscription.queue_new_feeds(user) UserSubscription.refresh_stale_feeds(user, exclude_new=True) diff --git a/apps/feed_import/test_feed_import.py b/apps/feed_import/test_feed_import.py index b739eb1570..0f604182c3 100644 --- a/apps/feed_import/test_feed_import.py +++ b/apps/feed_import/test_feed_import.py @@ -1,69 +1,104 @@ +import json import os -from django.test.client import Client -from django.test import TestCase + from django.contrib.auth.models import User +from django.core.management import call_command +from django.test import TestCase +from django.test.client import Client from django.urls import reverse + from apps.reader.models import UserSubscription, UserSubscriptionFolders -from apps.rss_feeds.models import merge_feeds, DuplicateFeed, Feed +from apps.rss_feeds.models import DuplicateFeed, Feed, merge_feeds from utils import json_functions as json_functions -import json -from django.core.management import call_command + + class Test_Import(TestCase): - fixtures = [ - 'apps/rss_feeds/fixtures/initial_data.json', - 'opml_import.json' - ] - + fixtures = ["apps/rss_feeds/fixtures/initial_data.json", "opml_import.json"] + def setUp(self): self.client = Client() - + def test_opml_import(self): - self.client.login(username='conesus', password='test') - user = User.objects.get(username='conesus') - + self.client.login(username="conesus", password="test") + user = User.objects.get(username="conesus") + # Verify user has no feeds subs = UserSubscription.objects.filter(user=user) self.assertEqual(subs.count(), 0) - - f = open(os.path.join(os.path.dirname(__file__), 'fixtures/opml.xml')) - response = self.client.post(reverse('opml-upload'), {'file': f}) + + f = open(os.path.join(os.path.dirname(__file__), "fixtures/opml.xml")) + response = self.client.post(reverse("opml-upload"), {"file": f}) self.assertEqual(response.status_code, 200) - + # Verify user now has feeds subs = UserSubscription.objects.filter(user=user) self.assertEqual(subs.count(), 54) - + usf = UserSubscriptionFolders.objects.get(user=user) print(json_functions.decode(usf.folders)) - self.assertEqual(json_functions.decode(usf.folders), [{'Tech': [4, 5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]}, 1, 2, 3, 6, {'New York': [1, 2, 3, 4, 5, 6, 7, 8, 9]}, {'tech': []}, {'Blogs': [29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, {'The Bloglets': [45, 46, 47, 48, 49]}]}, {'Cooking': [50, 51, 52, 53]}, 54]) - + self.assertEqual( + json_functions.decode(usf.folders), + [ + {"Tech": [4, 5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]}, + 1, + 2, + 3, + 6, + {"New York": [1, 2, 3, 4, 5, 6, 7, 8, 9]}, + {"tech": []}, + { + "Blogs": [ + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + {"The Bloglets": [45, 46, 47, 48, 49]}, + ] + }, + {"Cooking": [50, 51, 52, 53]}, + 54, + ], + ) + def test_opml_import__empty(self): - self.client.login(username='conesus', password='test') - user = User.objects.get(username='conesus') - + self.client.login(username="conesus", password="test") + user = User.objects.get(username="conesus") + # Verify user has default feeds subs = UserSubscription.objects.filter(user=user) self.assertEqual(subs.count(), 0) - response = self.client.post(reverse('opml-upload')) + response = self.client.post(reverse("opml-upload")) self.assertEqual(response.status_code, 200) - + # Verify user now has feeds subs = UserSubscription.objects.filter(user=user) self.assertEquals(subs.count(), 0) + class Test_Duplicate_Feeds(TestCase): fixtures = [ - 'apps/rss_feeds/fixtures/initial_data.json', + "apps/rss_feeds/fixtures/initial_data.json", ] - def test_duplicate_feeds(self): # had to load the feed data this way to hit the save() override. # it wouldn't work with loaddata or fixures - with open('apps/feed_import/fixtures/duplicate_feeds.json') as json_file: + with open("apps/feed_import/fixtures/duplicate_feeds.json") as json_file: feed_data = json.loads(json_file.read()) feed_data_1 = feed_data[0] feed_data_2 = feed_data[1] @@ -72,15 +107,15 @@ def test_duplicate_feeds(self): feed_1.save() feed_2.save() - call_command('loaddata', 'apps/feed_import/fixtures/subscriptions.json') + call_command("loaddata", "apps/feed_import/fixtures/subscriptions.json") - user_1_feed_subscription = UserSubscription.objects.filter(user__id=1)[0].feed_id + user_1_feed_subscription = UserSubscription.objects.filter(user__id=1)[0].feed_id user_2_feed_subscription = UserSubscription.objects.filter(user__id=2)[0].feed_id self.assertNotEqual(user_1_feed_subscription, user_2_feed_subscription) original_feed_id = merge_feeds(user_1_feed_subscription, user_2_feed_subscription) - - user_1_feed_subscription = UserSubscription.objects.filter(user__id=1)[0].feed_id + + user_1_feed_subscription = UserSubscription.objects.filter(user__id=1)[0].feed_id user_2_feed_subscription = UserSubscription.objects.filter(user__id=2)[0].feed_id self.assertEqual(user_1_feed_subscription, user_2_feed_subscription) diff --git a/apps/feed_import/urls.py b/apps/feed_import/urls.py index 1fe34d44a3..3d10000167 100644 --- a/apps/feed_import/urls.py +++ b/apps/feed_import/urls.py @@ -1,7 +1,8 @@ from django.conf.urls import url + from apps.feed_import import views urlpatterns = [ - url(r'^opml_upload/?$', views.opml_upload, name='opml-upload'), - url(r'^opml_export/?$', views.opml_export, name='opml-export'), + url(r"^opml_upload/?$", views.opml_upload, name="opml-upload"), + url(r"^opml_export/?$", views.opml_export, name="opml-export"), ] diff --git a/apps/feed_import/views.py b/apps/feed_import/views.py index 9eaf98df58..cd6b8cb65f 100644 --- a/apps/feed_import/views.py +++ b/apps/feed_import/views.py @@ -1,26 +1,28 @@ +import base64 import datetime import pickle -import base64 -from utils import log as logging -from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError -from bson.errors import InvalidStringData import uuid -from django.contrib.sites.models import Site + +from bson.errors import InvalidStringData +from django.conf import settings +from django.contrib.auth import login as login_user from django.contrib.auth.models import User +from django.contrib.sites.models import Site + # from django.db import IntegrityError from django.http import HttpResponse, HttpResponseRedirect -from django.conf import settings from django.urls import reverse -from django.contrib.auth import login as login_user from mongoengine.errors import ValidationError +from oauth2client.client import FlowExchangeError, OAuth2WebServerFlow + +from apps.feed_import.models import OAuthToken, OPMLExporter, OPMLImporter, UploadedOPML +from apps.feed_import.tasks import ProcessOPML from apps.reader.forms import SignupForm from apps.reader.models import UserSubscription -from apps.feed_import.models import OAuthToken -from apps.feed_import.models import OPMLImporter, OPMLExporter, UploadedOPML -from apps.feed_import.tasks import ProcessOPML from utils import json_functions as json -from utils.user_functions import ajax_login_required, get_user +from utils import log as logging from utils.feed_functions import TimeoutError +from utils.user_functions import ajax_login_required, get_user @ajax_login_required @@ -29,11 +31,11 @@ def opml_upload(request): message = "OK" code = 1 payload = {} - - if request.method == 'POST': - if 'file' in request.FILES: + + if request.method == "POST": + if "file" in request.FILES: logging.user(request, "~FR~SBOPML upload starting...") - file = request.FILES['file'] + file = request.FILES["file"] xml_opml = file.read() try: UploadedOPML.objects.create(user_id=request.user.pk, opml_file=xml_opml) @@ -41,7 +43,7 @@ def opml_upload(request): folders = None code = -1 message = "There was a Unicode decode error when reading your OPML file. Ensure it's a text file with a .opml or .xml extension. Is it a zip file?" - + opml_importer = OPMLImporter(xml_opml, request.user) try: folders = opml_importer.try_processing() @@ -49,7 +51,9 @@ def opml_upload(request): folders = None ProcessOPML.delay(request.user.pk) feed_count = opml_importer.count_feeds_in_opml() - logging.user(request, "~FR~SBOPML upload took too long, found %s feeds. Tasking..." % feed_count) + logging.user( + request, "~FR~SBOPML upload took too long, found %s feeds. Tasking..." % feed_count + ) payload = dict(folders=folders, delayed=True, feed_count=feed_count) code = 2 message = "" @@ -64,32 +68,35 @@ def opml_upload(request): payload = dict(folders=folders, feeds=feeds) logging.user(request, "~FR~SBOPML Upload: ~SK%s~SN~SB~FR feeds" % (len(feeds))) from apps.social.models import MActivity + MActivity.new_opml_import(user_id=request.user.pk, count=len(feeds)) UserSubscription.queue_new_feeds(request.user) UserSubscription.refresh_stale_feeds(request.user, exclude_new=True) else: message = "Attach an .opml file." code = -1 - - return HttpResponse(json.encode(dict(message=message, code=code, payload=payload)), - content_type='text/html') + + return HttpResponse( + json.encode(dict(message=message, code=code, payload=payload)), content_type="text/html" + ) + def opml_export(request): - user = get_user(request) - now = datetime.datetime.now() - if request.GET.get('user_id') and user.is_staff: - user = User.objects.get(pk=request.GET['user_id']) + user = get_user(request) + now = datetime.datetime.now() + if request.GET.get("user_id") and user.is_staff: + user = User.objects.get(pk=request.GET["user_id"]) exporter = OPMLExporter(user) - opml = exporter.process() + opml = exporter.process() from apps.social.models import MActivity + MActivity.new_opml_export(user_id=user.pk, count=exporter.feed_count) - response = HttpResponse(opml, content_type='text/xml; charset=utf-8') - response['Content-Disposition'] = 'attachment; filename=NewsBlur-%s-%s.opml' % ( + response = HttpResponse(opml, content_type="text/xml; charset=utf-8") + response["Content-Disposition"] = "attachment; filename=NewsBlur-%s-%s.opml" % ( user.username, - now.strftime('%Y-%m-%d') + now.strftime("%Y-%m-%d"), ) - - return response + return response diff --git a/apps/mobile/tests.py b/apps/mobile/tests.py index 2247054b35..3748f41ba4 100644 --- a/apps/mobile/tests.py +++ b/apps/mobile/tests.py @@ -7,6 +7,7 @@ from django.test import TestCase + class SimpleTest(TestCase): def test_basic_addition(self): """ @@ -14,10 +15,12 @@ def test_basic_addition(self): """ self.failUnlessEqual(1 + 1, 2) -__test__ = {"doctest": """ + +__test__ = { + "doctest": """ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True -"""} - +""" +} diff --git a/apps/mobile/urls.py b/apps/mobile/urls.py index 01e7b15e6f..38f857101e 100644 --- a/apps/mobile/urls.py +++ b/apps/mobile/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import url + from apps.mobile import views urlpatterns = [ - url(r'^$', views.index, name='mobile-index'), + url(r"^$", views.index, name="mobile-index"), ] diff --git a/apps/mobile/views.py b/apps/mobile/views.py index f9e765f522..de4ec45fef 100644 --- a/apps/mobile/views.py +++ b/apps/mobile/views.py @@ -1,12 +1,15 @@ -import os import base64 +import os + from django.conf import settings from django.http import HttpResponse from django.shortcuts import render + from apps.profile.models import Profile from apps.reader.models import UserSubscription, UserSubscriptionFolders from utils import json_functions as json from utils import log as logging + def index(request): - return render(request, 'mobile/mobile_workspace.xhtml', {}) + return render(request, "mobile/mobile_workspace.xhtml", {}) diff --git a/apps/monitor/urls.py b/apps/monitor/urls.py index 2ae4c0fdc9..e6f7bc6732 100644 --- a/apps/monitor/urls.py +++ b/apps/monitor/urls.py @@ -1,24 +1,39 @@ from django.conf.urls import url -from apps.monitor.views import ( AppServers, AppTimes, -Classifiers, DbTimes, Errors, FeedCounts, Feeds, LoadTimes, - Stories, TasksCodes, TasksPipeline, TasksServers, TasksTimes, - Updates, Users, FeedSizes + +from apps.monitor.views import ( + AppServers, + AppTimes, + Classifiers, + DbTimes, + Errors, + FeedCounts, + Feeds, + FeedSizes, + LoadTimes, + Stories, + TasksCodes, + TasksPipeline, + TasksServers, + TasksTimes, + Updates, + Users, ) + urlpatterns = [ - url(r'^app-servers?$', AppServers.as_view(), name="app_servers"), - url(r'^app-times?$', AppTimes.as_view(), name="app_times"), - url(r'^classifiers?$', Classifiers.as_view(), name="classifiers"), - url(r'^db-times?$', DbTimes.as_view(), name="db_times"), - url(r'^errors?$', Errors.as_view(), name="errors"), - url(r'^feed-counts?$', FeedCounts.as_view(), name="feed_counts"), - url(r'^feed-sizes?$', FeedSizes.as_view(), name="feed_sizes"), - url(r'^feeds?$', Feeds.as_view(), name="feeds"), - url(r'^load-times?$', LoadTimes.as_view(), name="load_times"), - url(r'^stories?$', Stories.as_view(), name="stories"), - url(r'^task-codes?$', TasksCodes.as_view(), name="task_codes"), - url(r'^task-pipeline?$', TasksPipeline.as_view(), name="task_pipeline"), - url(r'^task-servers?$', TasksServers.as_view(), name="task_servers"), - url(r'^task-times?$', TasksTimes.as_view(), name="task_times"), - url(r'^updates?$', Updates.as_view(), name="updates"), - url(r'^users?$', Users.as_view(), name="users"), + url(r"^app-servers?$", AppServers.as_view(), name="app_servers"), + url(r"^app-times?$", AppTimes.as_view(), name="app_times"), + url(r"^classifiers?$", Classifiers.as_view(), name="classifiers"), + url(r"^db-times?$", DbTimes.as_view(), name="db_times"), + url(r"^errors?$", Errors.as_view(), name="errors"), + url(r"^feed-counts?$", FeedCounts.as_view(), name="feed_counts"), + url(r"^feed-sizes?$", FeedSizes.as_view(), name="feed_sizes"), + url(r"^feeds?$", Feeds.as_view(), name="feeds"), + url(r"^load-times?$", LoadTimes.as_view(), name="load_times"), + url(r"^stories?$", Stories.as_view(), name="stories"), + url(r"^task-codes?$", TasksCodes.as_view(), name="task_codes"), + url(r"^task-pipeline?$", TasksPipeline.as_view(), name="task_pipeline"), + url(r"^task-servers?$", TasksServers.as_view(), name="task_servers"), + url(r"^task-times?$", TasksTimes.as_view(), name="task_times"), + url(r"^updates?$", Updates.as_view(), name="updates"), + url(r"^users?$", Users.as_view(), name="users"), ] diff --git a/apps/monitor/views/newsblur_app_servers.py b/apps/monitor/views/newsblur_app_servers.py index aeae5286d3..11f9b76ae7 100755 --- a/apps/monitor/views/newsblur_app_servers.py +++ b/apps/monitor/views/newsblur_app_servers.py @@ -1,13 +1,14 @@ import datetime + from django.conf import settings -from django.views import View from django.shortcuts import render +from django.views import View -class AppServers(View): +class AppServers(View): def get(self, request): - data = dict((("%s" % s['_id'].replace('-', ''), s['feeds']) for s in self.stats)) - #total = self.total: + data = dict((("%s" % s["_id"].replace("-", ""), s["feeds"]) for s in self.stats)) + # total = self.total: # if total: # data['total'] = total[0]['feeds'] chart_name = "app_servers" @@ -21,38 +22,48 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") + @property def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate([{ - "$match": { - "date": { - "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate( + [ + { + "$match": { + "date": { + "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "feeds" : {"$sum": 1}, - }, - }]) - + { + "$group": { + "_id": "$server", + "feeds": {"$sum": 1}, + }, + }, + ] + ) + return list(stats) - + @property - def total(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + def total(self): + stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : 1, - "feeds" : {"$sum": 1}, - }, - }]) - + { + "$group": { + "_id": 1, + "feeds": {"$sum": 1}, + }, + }, + ] + ) + return list(stats) diff --git a/apps/monitor/views/newsblur_app_times.py b/apps/monitor/views/newsblur_app_times.py index d64b0bc158..8970434c2a 100755 --- a/apps/monitor/views/newsblur_app_times.py +++ b/apps/monitor/views/newsblur_app_times.py @@ -1,12 +1,13 @@ -from django.views import View -from django.shortcuts import render import datetime + from django.conf import settings +from django.shortcuts import render +from django.views import View -class AppTimes(View): +class AppTimes(View): def get(self, request): - servers = dict((("%s" % s['_id'], s['page_load']) for s in self.stats)) + servers = dict((("%s" % s["_id"], s["page_load"]) for s in self.stats)) data = servers chart_name = "app_times" chart_type = "counter" @@ -20,21 +21,26 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") + @property def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "page_load" : {"$avg": "$page_load"}, - }, - }]) - + { + "$group": { + "_id": "$server", + "page_load": {"$avg": "$page_load"}, + }, + }, + ] + ) + return list(stats) diff --git a/apps/monitor/views/newsblur_classifiers.py b/apps/monitor/views/newsblur_classifiers.py index bc7af32abe..43bb5c3768 100755 --- a/apps/monitor/views/newsblur_classifiers.py +++ b/apps/monitor/views/newsblur_classifiers.py @@ -1,16 +1,21 @@ -from django.views import View from django.shortcuts import render -from apps.analyzer.models import MClassifierFeed, MClassifierAuthor, MClassifierTag, MClassifierTitle +from django.views import View +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, +) -class Classifiers(View): +class Classifiers(View): def get(self, request): data = { - 'feeds': MClassifierFeed.objects._collection.count(), - 'authors': MClassifierAuthor.objects._collection.count(), - 'tags': MClassifierTag.objects._collection.count(), - 'titles': MClassifierTitle.objects._collection.count(), + "feeds": MClassifierFeed.objects._collection.count(), + "authors": MClassifierAuthor.objects._collection.count(), + "tags": MClassifierTag.objects._collection.count(), + "titles": MClassifierTitle.objects._collection.count(), } chart_name = "classifiers" @@ -24,5 +29,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_dbtimes.py b/apps/monitor/views/newsblur_dbtimes.py index d11daf595e..d23859edd1 100755 --- a/apps/monitor/views/newsblur_dbtimes.py +++ b/apps/monitor/views/newsblur_dbtimes.py @@ -3,24 +3,22 @@ from apps.statistics.models import MStatistics -class DbTimes(View): - +class DbTimes(View): def get(self, request): - data = { - 'sql_avg': MStatistics.get('latest_sql_avg'), - 'mongo_avg': MStatistics.get('latest_mongo_avg'), - 'redis_user_avg': MStatistics.get('latest_redis_user_avg'), - 'redis_story_avg': MStatistics.get('latest_redis_story_avg'), - 'redis_session_avg': MStatistics.get('latest_redis_session_avg'), - 'redis_pubsub_avg': MStatistics.get('latest_redis_pubsub_avg'), - 'task_sql_avg': MStatistics.get('latest_task_sql_avg'), - 'task_mongo_avg': MStatistics.get('latest_task_mongo_avg'), - 'task_redis_user_avg': MStatistics.get('latest_task_redis_user_avg'), - 'task_redis_story_avg': MStatistics.get('latest_task_redis_story_avg'), - 'task_redis_session_avg': MStatistics.get('latest_task_redis_session_avg'), - 'task_redis_pubsub_avg': MStatistics.get('latest_task_redis_pubsub_avg'), + "sql_avg": MStatistics.get("latest_sql_avg"), + "mongo_avg": MStatistics.get("latest_mongo_avg"), + "redis_user_avg": MStatistics.get("latest_redis_user_avg"), + "redis_story_avg": MStatistics.get("latest_redis_story_avg"), + "redis_session_avg": MStatistics.get("latest_redis_session_avg"), + "redis_pubsub_avg": MStatistics.get("latest_redis_pubsub_avg"), + "task_sql_avg": MStatistics.get("latest_task_sql_avg"), + "task_mongo_avg": MStatistics.get("latest_task_mongo_avg"), + "task_redis_user_avg": MStatistics.get("latest_task_redis_user_avg"), + "task_redis_story_avg": MStatistics.get("latest_task_redis_story_avg"), + "task_redis_session_avg": MStatistics.get("latest_task_redis_session_avg"), + "task_redis_pubsub_avg": MStatistics.get("latest_task_redis_pubsub_avg"), } chart_name = "db_times" chart_type = "counter" @@ -32,4 +30,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_errors.py b/apps/monitor/views/newsblur_errors.py index ea057efe85..7497efd4f7 100755 --- a/apps/monitor/views/newsblur_errors.py +++ b/apps/monitor/views/newsblur_errors.py @@ -3,23 +3,22 @@ from apps.statistics.models import MStatistics -class Errors(View): +class Errors(View): def get(self, request): statistics = MStatistics.all() data = { - 'feed_success': statistics['feeds_fetched'], + "feed_success": statistics["feeds_fetched"], } chart_name = "errors" chart_type = "counter" formatted_data = {} for k, v in data.items(): - formatted_data[k] = f'feed_success {v}' - + formatted_data[k] = f"feed_success {v}" + context = { "data": formatted_data, "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_feed_counts.py b/apps/monitor/views/newsblur_feed_counts.py index 7a08c954c8..3a4d318cd3 100755 --- a/apps/monitor/views/newsblur_feed_counts.py +++ b/apps/monitor/views/newsblur_feed_counts.py @@ -1,49 +1,49 @@ +import redis from django.conf import settings from django.shortcuts import render from django.views import View -import redis -from apps.rss_feeds.models import Feed, DuplicateFeed + from apps.push.models import PushSubscription +from apps.rss_feeds.models import DuplicateFeed, Feed from apps.statistics.models import MStatistics -class FeedCounts(View): +class FeedCounts(View): def get(self, request): - - exception_feeds = MStatistics.get('munin:exception_feeds') + exception_feeds = MStatistics.get("munin:exception_feeds") if not exception_feeds: exception_feeds = Feed.objects.filter(has_feed_exception=True).count() - MStatistics.set('munin:exception_feeds', exception_feeds, 60*60*12) + MStatistics.set("munin:exception_feeds", exception_feeds, 60 * 60 * 12) - exception_pages = MStatistics.get('munin:exception_pages') + exception_pages = MStatistics.get("munin:exception_pages") if not exception_pages: exception_pages = Feed.objects.filter(has_page_exception=True).count() - MStatistics.set('munin:exception_pages', exception_pages, 60*60*12) + MStatistics.set("munin:exception_pages", exception_pages, 60 * 60 * 12) - duplicate_feeds = MStatistics.get('munin:duplicate_feeds') + duplicate_feeds = MStatistics.get("munin:duplicate_feeds") if not duplicate_feeds: duplicate_feeds = DuplicateFeed.objects.count() - MStatistics.set('munin:duplicate_feeds', duplicate_feeds, 60*60*12) + MStatistics.set("munin:duplicate_feeds", duplicate_feeds, 60 * 60 * 12) - active_feeds = MStatistics.get('munin:active_feeds') + active_feeds = MStatistics.get("munin:active_feeds") if not active_feeds: active_feeds = Feed.objects.filter(active_subscribers__gt=0).count() - MStatistics.set('munin:active_feeds', active_feeds, 60*60*12) + MStatistics.set("munin:active_feeds", active_feeds, 60 * 60 * 12) - push_feeds = MStatistics.get('munin:push_feeds') + push_feeds = MStatistics.get("munin:push_feeds") if not push_feeds: push_feeds = PushSubscription.objects.filter(verified=True).count() - MStatistics.set('munin:push_feeds', push_feeds, 60*60*12) + MStatistics.set("munin:push_feeds", push_feeds, 60 * 60 * 12) r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - + data = { - 'scheduled_feeds': r.zcard('scheduled_updates'), - 'exception_feeds': exception_feeds, - 'exception_pages': exception_pages, - 'duplicate_feeds': duplicate_feeds, - 'active_feeds': active_feeds, - 'push_feeds': push_feeds, + "scheduled_feeds": r.zcard("scheduled_updates"), + "exception_feeds": exception_feeds, + "exception_pages": exception_pages, + "duplicate_feeds": duplicate_feeds, + "active_feeds": active_feeds, + "push_feeds": push_feeds, } chart_name = "feed_counts" chart_type = "counter" @@ -57,6 +57,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_feed_sizes.py b/apps/monitor/views/newsblur_feed_sizes.py index 553aee324b..f4fade4148 100644 --- a/apps/monitor/views/newsblur_feed_sizes.py +++ b/apps/monitor/views/newsblur_feed_sizes.py @@ -1,29 +1,31 @@ +import redis from django.conf import settings +from django.db.models import Sum from django.shortcuts import render from django.views import View -from django.db.models import Sum -import redis -from apps.rss_feeds.models import Feed, DuplicateFeed + from apps.push.models import PushSubscription +from apps.rss_feeds.models import DuplicateFeed, Feed from apps.statistics.models import MStatistics -class FeedSizes(View): +class FeedSizes(View): def get(self, request): - - fs_size_bytes = MStatistics.get('munin:fs_size_bytes') + fs_size_bytes = MStatistics.get("munin:fs_size_bytes") if not fs_size_bytes: - fs_size_bytes = Feed.objects.aggregate(Sum('fs_size_bytes'))['fs_size_bytes__sum'] - MStatistics.set('munin:fs_size_bytes', fs_size_bytes, 60*60*12) + fs_size_bytes = Feed.objects.aggregate(Sum("fs_size_bytes"))["fs_size_bytes__sum"] + MStatistics.set("munin:fs_size_bytes", fs_size_bytes, 60 * 60 * 12) - archive_users_size_bytes = MStatistics.get('munin:archive_users_size_bytes') + archive_users_size_bytes = MStatistics.get("munin:archive_users_size_bytes") if not archive_users_size_bytes: - archive_users_size_bytes = Feed.objects.filter(archive_subscribers__gte=1).aggregate(Sum('fs_size_bytes'))['fs_size_bytes__sum'] - MStatistics.set('munin:archive_users_size_bytes', archive_users_size_bytes, 60*60*12) + archive_users_size_bytes = Feed.objects.filter(archive_subscribers__gte=1).aggregate( + Sum("fs_size_bytes") + )["fs_size_bytes__sum"] + MStatistics.set("munin:archive_users_size_bytes", archive_users_size_bytes, 60 * 60 * 12) data = { - 'fs_size_bytes': fs_size_bytes, - 'archive_users_size_bytes': archive_users_size_bytes, + "fs_size_bytes": fs_size_bytes, + "archive_users_size_bytes": archive_users_size_bytes, } chart_name = "feed_sizes" chart_type = "counter" @@ -37,6 +39,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_feeds.py b/apps/monitor/views/newsblur_feeds.py index 9c527fa820..a6432228c5 100755 --- a/apps/monitor/views/newsblur_feeds.py +++ b/apps/monitor/views/newsblur_feeds.py @@ -1,42 +1,40 @@ -from django.views import View from django.shortcuts import render +from django.views import View -from apps.rss_feeds.models import Feed from apps.reader.models import UserSubscription +from apps.rss_feeds.models import Feed from apps.social.models import MSocialProfile, MSocialSubscription from apps.statistics.models import MStatistics -class Feeds(View): +class Feeds(View): def get(self, request): - - feeds_count = MStatistics.get('munin:feeds_count') + feeds_count = MStatistics.get("munin:feeds_count") if not feeds_count: feeds_count = Feed.objects.all().count() - MStatistics.set('munin:feeds_count', feeds_count, 60*60*12) + MStatistics.set("munin:feeds_count", feeds_count, 60 * 60 * 12) - subscriptions_count = MStatistics.get('munin:subscriptions_count') + subscriptions_count = MStatistics.get("munin:subscriptions_count") if not subscriptions_count: subscriptions_count = UserSubscription.objects.all().count() - MStatistics.set('munin:subscriptions_count', subscriptions_count, 60*60*12) + MStatistics.set("munin:subscriptions_count", subscriptions_count, 60 * 60 * 12) data = { - 'feeds': feeds_count, - 'subscriptions': subscriptions_count, - 'profiles': MSocialProfile.objects._collection.count(), - 'social_subscriptions': MSocialSubscription.objects._collection.count(), + "feeds": feeds_count, + "subscriptions": subscriptions_count, + "profiles": MSocialProfile.objects._collection.count(), + "social_subscriptions": MSocialSubscription.objects._collection.count(), } chart_name = "feeds" chart_type = "counter" formatted_data = {} for k, v in data.items(): formatted_data[k] = f'{chart_name}{{category="{k}"}} {v}' - + context = { "data": formatted_data, "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_loadtimes.py b/apps/monitor/views/newsblur_loadtimes.py index 64c93e1f60..40538f6df1 100755 --- a/apps/monitor/views/newsblur_loadtimes.py +++ b/apps/monitor/views/newsblur_loadtimes.py @@ -1,15 +1,15 @@ from django.shortcuts import render from django.views import View -class LoadTimes(View): +class LoadTimes(View): def get(self, request): from apps.statistics.models import MStatistics - + data = { - 'feed_loadtimes_1min': MStatistics.get('last_1_min_time_taken'), - 'feed_loadtimes_avg_hour': MStatistics.get('latest_avg_time_taken'), - 'feeds_loaded_hour': MStatistics.get('latest_sites_loaded'), + "feed_loadtimes_1min": MStatistics.get("last_1_min_time_taken"), + "feed_loadtimes_avg_hour": MStatistics.get("latest_avg_time_taken"), + "feeds_loaded_hour": MStatistics.get("latest_sites_loaded"), } chart_name = "load_times" chart_type = "counter" @@ -23,5 +23,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_stories.py b/apps/monitor/views/newsblur_stories.py index 3cbe3f0b97..ee73a196af 100755 --- a/apps/monitor/views/newsblur_stories.py +++ b/apps/monitor/views/newsblur_stories.py @@ -1,14 +1,14 @@ -from django.views import View from django.shortcuts import render -from apps.rss_feeds.models import MStory, MStarredStory -from apps.rss_feeds.models import MStory, MStarredStory - -class Stories(View): +from django.views import View + +from apps.rss_feeds.models import MStarredStory, MStory + +class Stories(View): def get(self, request): data = { - 'stories': MStory.objects._collection.count(), - 'starred_stories': MStarredStory.objects._collection.count(), + "stories": MStory.objects._collection.count(), + "starred_stories": MStarredStory.objects._collection.count(), } chart_name = "stories" chart_type = "counter" @@ -21,5 +21,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_tasks_codes.py b/apps/monitor/views/newsblur_tasks_codes.py index 652a136540..8bffd4c381 100755 --- a/apps/monitor/views/newsblur_tasks_codes.py +++ b/apps/monitor/views/newsblur_tasks_codes.py @@ -1,12 +1,13 @@ import datetime + from django.conf import settings from django.shortcuts import render from django.views import View -class TasksCodes(View): +class TasksCodes(View): def get(self, request): - data = dict((("_%s" % s['_id'], s['feeds']) for s in self.stats)) + data = dict((("_%s" % s["_id"], s["feeds"]) for s in self.stats)) chart_name = "task_codes" chart_type = "counter" formatted_data = {} @@ -18,22 +19,26 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") + @property - def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + def stats(self): + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, + }, + { + "$group": { + "_id": "$feed_code", + "feeds": {"$sum": 1}, + }, }, - }, - }, { - "$group": { - "_id" : "$feed_code", - "feeds" : {"$sum": 1}, - }, - }]) - + ] + ) + return list(stats) - \ No newline at end of file diff --git a/apps/monitor/views/newsblur_tasks_pipeline.py b/apps/monitor/views/newsblur_tasks_pipeline.py index e962fb9ef4..33931f5ac9 100755 --- a/apps/monitor/views/newsblur_tasks_pipeline.py +++ b/apps/monitor/views/newsblur_tasks_pipeline.py @@ -4,10 +4,10 @@ from django.shortcuts import render from django.views import View -class TasksPipeline(View): +class TasksPipeline(View): def get(self, request): - data =self.stats + data = self.stats chart_name = "task_pipeline" chart_type = "counter" @@ -19,27 +19,31 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") + @property def stats(self): - - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, + }, + { + "$group": { + "_id": 1, + "feed_fetch": {"$avg": "$feed_fetch"}, + "feed_process": {"$avg": "$feed_process"}, + "page": {"$avg": "$page"}, + "icon": {"$avg": "$icon"}, + "total": {"$avg": "$total"}, + }, }, - }, - }, { - "$group": { - "_id": 1, - "feed_fetch": {"$avg": "$feed_fetch"}, - "feed_process": {"$avg": "$feed_process"}, - "page": {"$avg": "$page"}, - "icon": {"$avg": "$icon"}, - "total": {"$avg": "$total"}, - }, - }]) + ] + ) stats = list(stats) if stats: print(stats) diff --git a/apps/monitor/views/newsblur_tasks_servers.py b/apps/monitor/views/newsblur_tasks_servers.py index 90a26fcf37..c8bcb394f9 100755 --- a/apps/monitor/views/newsblur_tasks_servers.py +++ b/apps/monitor/views/newsblur_tasks_servers.py @@ -4,10 +4,10 @@ from django.shortcuts import render from django.views import View -class TasksServers(View): +class TasksServers(View): def get(self, request): - data = dict((("%s" % s['_id'].replace('-', ''), s['feeds']) for s in self.stats)) + data = dict((("%s" % s["_id"].replace("-", ""), s["feeds"]) for s in self.stats)) chart_name = "task_servers" chart_type = "counter" @@ -19,39 +19,48 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") - @property def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "feeds" : {"$sum": 1}, - }, - }]) - + { + "$group": { + "_id": "$server", + "feeds": {"$sum": 1}, + }, + }, + ] + ) + return list(stats) - + @property def total(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, + }, + { + "$group": { + "_id": 1, + "feeds": {"$sum": 1}, + }, }, - }, - }, { - "$group": { - "_id" : 1, - "feeds" : {"$sum": 1}, - }, - }]) - + ] + ) + return list(stats) diff --git a/apps/monitor/views/newsblur_tasks_times.py b/apps/monitor/views/newsblur_tasks_times.py index 0d6a14f9ca..2ba7aa21b8 100755 --- a/apps/monitor/views/newsblur_tasks_times.py +++ b/apps/monitor/views/newsblur_tasks_times.py @@ -4,10 +4,10 @@ from django.shortcuts import render from django.views import View -class TasksTimes(View): +class TasksTimes(View): def get(self, request): - data = dict((("%s" % s['_id'], s['total']) for s in self.stats)) + data = dict((("%s" % s["_id"], s["total"]) for s in self.stats)) chart_name = "task_times" chart_type = "counter" @@ -19,22 +19,26 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") - @property def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "total" : {"$avg": "$total"}, - }, - }]) - + { + "$group": { + "_id": "$server", + "total": {"$avg": "$total"}, + }, + }, + ] + ) + return list(stats) diff --git a/apps/monitor/views/newsblur_updates.py b/apps/monitor/views/newsblur_updates.py index 38640407ff..21fada1120 100755 --- a/apps/monitor/views/newsblur_updates.py +++ b/apps/monitor/views/newsblur_updates.py @@ -1,29 +1,28 @@ import redis - from django.conf import settings from django.shortcuts import render from django.views import View -class Updates(View): - def get(self, request): +class Updates(View): + def get(self, request): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) data = { - 'update_queue': r.scard("queued_feeds"), - 'feeds_fetched': r.zcard("fetched_feeds_last_hour"), - 'tasked_feeds': r.zcard("tasked_feeds"), - 'error_feeds': r.zcard("error_feeds"), - 'celery_update_feeds': r.llen("update_feeds"), - 'celery_new_feeds': r.llen("new_feeds"), - 'celery_push_feeds': r.llen("push_feeds"), - 'celery_work_queue': r.llen("work_queue"), - 'celery_search_queue': r.llen("search_indexer"), + "update_queue": r.scard("queued_feeds"), + "feeds_fetched": r.zcard("fetched_feeds_last_hour"), + "tasked_feeds": r.zcard("tasked_feeds"), + "error_feeds": r.zcard("error_feeds"), + "celery_update_feeds": r.llen("update_feeds"), + "celery_new_feeds": r.llen("new_feeds"), + "celery_push_feeds": r.llen("push_feeds"), + "celery_work_queue": r.llen("work_queue"), + "celery_search_queue": r.llen("search_indexer"), } chart_name = "updates" chart_type = "counter" formatted_data = {} - + for k, v in data.items(): formatted_data[k] = f'{chart_name}{{category="{k}"}} {v}' context = { @@ -31,5 +30,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/newsblur_users.py b/apps/monitor/views/newsblur_users.py index 00218c8cde..76eed4d0f0 100755 --- a/apps/monitor/views/newsblur_users.py +++ b/apps/monitor/views/newsblur_users.py @@ -7,39 +7,63 @@ from apps.profile.models import Profile, RNewUserQueue from apps.statistics.models import MStatistics -class Users(View): +class Users(View): def get(self, request): last_year = datetime.datetime.utcnow() - datetime.timedelta(days=365) last_month = datetime.datetime.utcnow() - datetime.timedelta(days=30) - last_day = datetime.datetime.utcnow() - datetime.timedelta(minutes=60*24) - expiration_sec = 60*60 # 1 hour - + last_day = datetime.datetime.utcnow() - datetime.timedelta(minutes=60 * 24) + expiration_sec = 60 * 60 # 1 hour + data = { - 'all': MStatistics.get('munin:users_count', - lambda: User.objects.count(), - set_default=True, expiration_sec=expiration_sec), - 'yearly': MStatistics.get('munin:users_yearly', - lambda: Profile.objects.filter(last_seen_on__gte=last_year).count(), - set_default=True, expiration_sec=expiration_sec), - 'monthly': MStatistics.get('munin:users_monthly', - lambda: Profile.objects.filter(last_seen_on__gte=last_month).count(), - set_default=True, expiration_sec=expiration_sec), - 'daily': MStatistics.get('munin:users_daily', - lambda: Profile.objects.filter(last_seen_on__gte=last_day).count(), - set_default=True, expiration_sec=expiration_sec), - 'premium': MStatistics.get('munin:users_premium', - lambda: Profile.objects.filter(is_premium=True).count(), - set_default=True, expiration_sec=expiration_sec), - 'archive': MStatistics.get('munin:users_archive', - lambda: Profile.objects.filter(is_archive=True).count(), - set_default=True, expiration_sec=expiration_sec), - 'pro': MStatistics.get('munin:users_pro', - lambda: Profile.objects.filter(is_pro=True).count(), - set_default=True, expiration_sec=expiration_sec), - 'queued': MStatistics.get('munin:users_queued', - lambda: RNewUserQueue.user_count(), - set_default=True, expiration_sec=expiration_sec), + "all": MStatistics.get( + "munin:users_count", + lambda: User.objects.count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "yearly": MStatistics.get( + "munin:users_yearly", + lambda: Profile.objects.filter(last_seen_on__gte=last_year).count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "monthly": MStatistics.get( + "munin:users_monthly", + lambda: Profile.objects.filter(last_seen_on__gte=last_month).count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "daily": MStatistics.get( + "munin:users_daily", + lambda: Profile.objects.filter(last_seen_on__gte=last_day).count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "premium": MStatistics.get( + "munin:users_premium", + lambda: Profile.objects.filter(is_premium=True).count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "archive": MStatistics.get( + "munin:users_archive", + lambda: Profile.objects.filter(is_archive=True).count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "pro": MStatistics.get( + "munin:users_pro", + lambda: Profile.objects.filter(is_pro=True).count(), + set_default=True, + expiration_sec=expiration_sec, + ), + "queued": MStatistics.get( + "munin:users_queued", + lambda: RNewUserQueue.user_count(), + set_default=True, + expiration_sec=expiration_sec, + ), } chart_name = "users" chart_type = "counter" @@ -52,5 +76,4 @@ def get(self, request): "chart_name": chart_name, "chart_type": chart_type, } - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") - + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") diff --git a/apps/monitor/views/prometheus_redis.py b/apps/monitor/views/prometheus_redis.py index 8176287979..50ffde3f6b 100644 --- a/apps/monitor/views/prometheus_redis.py +++ b/apps/monitor/views/prometheus_redis.py @@ -1,8 +1,8 @@ import os import socket -from django.views import View from django.shortcuts import render +from django.views import View """ RedisActiveConnections @@ -12,6 +12,7 @@ RedisSize """ + class RedisGrafanaMetric(View): category = "Redis" @@ -23,9 +24,9 @@ def autoconf(self): return True def get_info(self): - host = os.environ.get('REDIS_HOST') or '127.0.0.1' - port = int(os.environ.get('REDIS_PORT') or '6379') - if host.startswith('/'): + host = os.environ.get("REDIS_HOST") or "127.0.0.1" + port = int(os.environ.get("REDIS_PORT") or "6379") + if host.startswith("/"): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(host) else: @@ -33,9 +34,9 @@ def get_info(self): s.connect((host, port)) s.send("*1\r\n$4\r\ninfo\r\n") buf = "" - while '\r\n' not in buf: + while "\r\n" not in buf: buf += s.recv(1024) - l, buf = buf.split('\r\n', 1) + l, buf = buf.split("\r\n", 1) if l[0] != "$": s.close() raise Exception("Protocol error") @@ -43,7 +44,7 @@ def get_info(self): if remaining > 0: buf += s.recv(remaining) s.close() - return dict(x.split(':', 1) for x in buf.split('\r\n') if ':' in x) + return dict(x.split(":", 1) for x in buf.split("\r\n") if ":" in x) def execute(self): stats = self.get_info() @@ -57,25 +58,28 @@ def execute(self): return values def get_fields(self): - raise NotImplementedError('You must implement the get_fields function') + raise NotImplementedError("You must implement the get_fields function") def get_context(self): - raise NotImplementedError('You must implement the get_context function') - + raise NotImplementedError("You must implement the get_context function") + def get(self, request): context = self.get_context() - return render(request, 'monitor/prometheus_data.html', context, content_type="text/plain") + return render(request, "monitor/prometheus_data.html", context, content_type="text/plain") + class RedisActiveConnection(RedisGrafanaMetric): - def get_fields(self): return ( - ('connected_clients', dict( - label = "connections", - info = "connections", - type = "GAUGE", - )), + ( + "connected_clients", + dict( + label="connections", + info="connections", + type="GAUGE", + ), + ), ) def get_context(self): - raise NotImplementedError('You must implement the get_context function') + raise NotImplementedError("You must implement the get_context function") diff --git a/apps/newsletters/models.py b/apps/newsletters/models.py index b7bd880cd5..d37c6f9017 100644 --- a/apps/newsletters/models.py +++ b/apps/newsletters/models.py @@ -1,39 +1,40 @@ import datetime import re + import redis +from django.conf import settings from django.contrib.sites.models import Site from django.core.mail import EmailMultiAlternatives -from django.urls import reverse -from django.conf import settings from django.template.loader import render_to_string +from django.urls import reverse from django.utils.html import linebreaks -from apps.rss_feeds.models import Feed, MStory, MFetchHistory -from apps.reader.models import UserSubscription, UserSubscriptionFolders -from apps.profile.models import Profile, MSentEmail -from apps.notifications.tasks import QueueNotifications -from apps.notifications.models import MUserFeedNotification +from apps.notifications.models import MUserFeedNotification +from apps.notifications.tasks import QueueNotifications +from apps.profile.models import MSentEmail, Profile +from apps.reader.models import UserSubscription, UserSubscriptionFolders +from apps.rss_feeds.models import Feed, MFetchHistory, MStory from utils import log as logging -from utils.story_functions import linkify from utils.scrubber import Scrubber +from utils.story_functions import linkify + class EmailNewsletter: - def receive_newsletter(self, params): - user = self._user_from_email(params['recipient']) + user = self._user_from_email(params["recipient"]) if not user: return - - sender_name, sender_username, sender_domain = self._split_sender(params['from']) + + sender_name, sender_username, sender_domain = self._split_sender(params["from"]) feed_address = self._feed_address(user, "%s@%s" % (sender_username, sender_domain)) - + try: usf = UserSubscriptionFolders.objects.get(user=user) except UserSubscriptionFolders.DoesNotExist: logging.user(user, "~FRUser does not have a USF, ignoring newsletter.") return - usf.add_folder('', 'Newsletters') - + usf.add_folder("", "Newsletters") + # First look for the email address try: feed = Feed.objects.get(feed_address=feed_address) @@ -46,45 +47,47 @@ def receive_newsletter(self, params): # If not found, check among titles user has subscribed to if not feed: - newsletter_subs = UserSubscription.objects.filter(user=user, feed__feed_address__contains="newsletter:").only('feed') + newsletter_subs = UserSubscription.objects.filter( + user=user, feed__feed_address__contains="newsletter:" + ).only("feed") newsletter_feed_ids = [us.feed.pk for us in newsletter_subs] feeds = Feed.objects.filter(feed_title__iexact=sender_name, pk__in=newsletter_feed_ids) if feeds.count(): feed = feeds[0] - + # Create a new feed if it doesn't exist by sender name or email if not feed: - feed = Feed.objects.create(feed_address=feed_address, - feed_link='http://' + sender_domain, - feed_title=sender_name, - fetched_once=True, - known_good=True) + feed = Feed.objects.create( + feed_address=feed_address, + feed_link="http://" + sender_domain, + feed_title=sender_name, + fetched_once=True, + known_good=True, + ) feed.update() logging.user(user, "~FCCreating newsletter feed: ~SB%s" % (feed)) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(user.username, 'reload:%s' % feed.pk) + r.publish(user.username, "reload:%s" % feed.pk) self._check_if_first_newsletter(user) - + feed.last_update = datetime.datetime.now() feed.last_story_date = datetime.datetime.now() feed.save() - + if feed.feed_title != sender_name: feed.feed_title = sender_name feed.save() - + try: usersub = UserSubscription.objects.get(user=user, feed=feed) except UserSubscription.DoesNotExist: _, _, usersub = UserSubscription.add_subscription( - user=user, - feed_address=feed_address, - folder='Newsletters' + user=user, feed_address=feed_address, folder="Newsletters" ) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(user.username, 'reload:feeds') - - story_hash = MStory.ensure_story_hash(params['signature'], feed.pk) + r.publish(user.username, "reload:feeds") + + story_hash = MStory.ensure_story_hash(params["signature"], feed.pk) story_content = self._get_content(params) plain_story_content = self._get_content(params, force_plain=True) if len(plain_story_content) > len(story_content): @@ -92,15 +95,16 @@ def receive_newsletter(self, params): story_content = self._clean_content(story_content) story_params = { "story_feed_id": feed.pk, - "story_date": datetime.datetime.fromtimestamp(int(params['timestamp'])), - "story_title": params['subject'], + "story_date": datetime.datetime.fromtimestamp(int(params["timestamp"])), + "story_title": params["subject"], "story_content": story_content, - "story_author_name": params['from'], - "story_permalink": "https://%s%s" % ( - Site.objects.get_current().domain, - reverse('newsletter-story', - kwargs={'story_hash': story_hash})), - "story_guid": params['signature'], + "story_author_name": params["from"], + "story_permalink": "https://%s%s" + % ( + Site.objects.get_current().domain, + reverse("newsletter-story", kwargs={"story_hash": story_hash}), + ), + "story_guid": params["signature"], } try: @@ -108,17 +112,17 @@ def receive_newsletter(self, params): except MStory.DoesNotExist: story = MStory(**story_params) story.save() - + usersub.needs_unread_recalc = True usersub.save() - + self._publish_to_subscribers(feed, story.story_hash) - - MFetchHistory.add(feed_id=feed.pk, fetch_type='push') + + MFetchHistory.add(feed_id=feed.pk, fetch_type="push") logging.user(user, "~FCNewsletter feed story: ~SB%s~SN / ~SB%s" % (story.story_title, feed)) - + return story - + def _check_if_first_newsletter(self, user, force=False): if not user.email: return @@ -129,10 +133,10 @@ def _check_if_first_newsletter(self, user, force=False): if sub.feed.is_newsletter: found_newsletter = True break - if not found_newsletter and not force: - return - - params = dict(receiver_user_id=user.pk, email_type='first_newsletter') + if not found_newsletter and not force: + return + + params = dict(receiver_user_id=user.pk, email_type="first_newsletter") try: MSentEmail.objects.get(**params) if not force: @@ -140,23 +144,26 @@ def _check_if_first_newsletter(self, user, force=False): return except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - - text = render_to_string('mail/email_first_newsletter.txt', {}) - html = render_to_string('mail/email_first_newsletter.xhtml', {}) + + text = render_to_string("mail/email_first_newsletter.txt", {}) + html = render_to_string("mail/email_first_newsletter.xhtml", {}) subject = "Your email newsletters are now being sent to NewsBlur" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(user, "~BB~FM~SBSending first newsletter email to: %s" % user.email) - + def _user_from_email(self, email): - tokens = re.search('(\w+)[\+\-\.](\w+)@newsletters.newsblur.com', email) + tokens = re.search("(\w+)[\+\-\.](\w+)@newsletters.newsblur.com", email) if not tokens: return - + username, secret_token = tokens.groups() try: profiles = Profile.objects.filter(secret_token=secret_token) @@ -165,55 +172,56 @@ def _user_from_email(self, email): profile = profiles[0] except Profile.DoesNotExist: return - + return profile.user - + def _feed_address(self, user, sender_email): - return 'newsletter:%s:%s' % (user.pk, sender_email) - + return "newsletter:%s:%s" % (user.pk, sender_email) + def _split_sender(self, sender): - tokens = re.search('(.*?) <(.*?)@(.*?)>', sender) + tokens = re.search("(.*?) <(.*?)@(.*?)>", sender) if not tokens: - name, domain = sender.split('@') + name, domain = sender.split("@") return name, sender, domain - + sender_name, sender_username, sender_domain = tokens.group(1), tokens.group(2), tokens.group(3) - sender_name = sender_name.replace('"', '') - + sender_name = sender_name.replace('"', "") + return sender_name, sender_username, sender_domain - + def _get_content(self, params, force_plain=False): - if 'body-enriched' in params and not force_plain: - return params['body-enriched'] - if 'body-html' in params and not force_plain: - return params['body-html'] - if 'stripped-html' in params and not force_plain: - return params['stripped-html'] - if 'body-plain' in params: - return linkify(linebreaks(params['body-plain'])) - + if "body-enriched" in params and not force_plain: + return params["body-enriched"] + if "body-html" in params and not force_plain: + return params["body-html"] + if "stripped-html" in params and not force_plain: + return params["stripped-html"] + if "body-plain" in params: + return linkify(linebreaks(params["body-plain"])) + if force_plain: return self._get_content(params, force_plain=False) - + def _clean_content(self, content): original = content scrubber = Scrubber() content = scrubber.scrub(content) - if len(content) < len(original)*0.01: + if len(content) < len(original) * 0.01: content = original - content = content.replace('!important', '') + content = content.replace("!important", "") return content - + def _publish_to_subscribers(self, feed, story_hash): try: r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - listeners_count = r.publish("%s:story" % feed.pk, 'story:new:%s' % story_hash) + listeners_count = r.publish("%s:story" % feed.pk, "story:new:%s" % story_hash) 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],)) - + if MUserFeedNotification.feed_has_users(feed.pk) > 0: QueueNotifications.delay(feed.pk, 1) - \ No newline at end of file diff --git a/apps/newsletters/urls.py b/apps/newsletters/urls.py index 224e2cc095..39439a1cad 100644 --- a/apps/newsletters/urls.py +++ b/apps/newsletters/urls.py @@ -1,7 +1,8 @@ from django.conf.urls import url + from apps.newsletters import views urlpatterns = [ - url(r'^receive/?$', views.newsletter_receive, name='newsletter-receive'), - url(r'^story/(?P[\w:]+)/?$', views.newsletter_story, name='newsletter-story'), + url(r"^receive/?$", views.newsletter_receive, name="newsletter-receive"), + url(r"^story/(?P[\w:]+)/?$", views.newsletter_story, name="newsletter-story"), ] diff --git a/apps/newsletters/views.py b/apps/newsletters/views.py index 4489127834..052c038ba1 100644 --- a/apps/newsletters/views.py +++ b/apps/newsletters/views.py @@ -1,13 +1,16 @@ from pprint import pprint -from django.http import HttpResponse, Http404 + from django.conf import settings -from utils import log as logging +from django.http import Http404, HttpResponse + from apps.newsletters.models import EmailNewsletter from apps.rss_feeds.models import Feed, MStory +from utils import log as logging + def newsletter_receive(request): """ - This function is called by mailgun's receive email feature. This is a + This function is called by mailgun's receive email feature. This is a private API used for the newsletter app. """ # params = { @@ -42,24 +45,25 @@ def newsletter_receive(request): # 'Subject':'Test Newsletter theskimm' # } params = request.POST - - response = HttpResponse('OK') - - if settings.DEBUG or 'samuel' in params.get('To', ''): + + response = HttpResponse("OK") + + if settings.DEBUG or "samuel" in params.get("To", ""): logging.debug(" ---> Email newsletter: %s" % params) - + if not params or not len(params.keys()): logging.debug(" ***> Email newsletter blank body: %s" % request.body) raise Http404 - + email_newsletter = EmailNewsletter() story = email_newsletter.receive_newsletter(params) - + if not story: raise Http404 - + return response + def newsletter_story(request, story_hash): try: story = MStory.objects.get(story_hash=story_hash) @@ -67,4 +71,4 @@ def newsletter_story(request, story_hash): raise Http404 story = Feed.format_story(story) - return HttpResponse(story['story_content']) + return HttpResponse(story["story_content"]) diff --git a/apps/notifications/models.py b/apps/notifications/models.py index 253dab95e5..9d158d69ea 100644 --- a/apps/notifications/models.py +++ b/apps/notifications/models.py @@ -1,34 +1,36 @@ import datetime import enum import html -import redis import re +import urllib.parse + import mongoengine as mongo +import redis +from apns2.client import APNsClient +from apns2.errors import BadDeviceToken, DeviceTokenNotForTopic, Unregistered +from apns2.payload import Payload +from bs4 import BeautifulSoup from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site -from django.template.loader import render_to_string from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string -# from django.utils.html import strip_tags -from apps.rss_feeds.models import MStory, Feed -from apps.reader.models import UserSubscription from apps.analyzer.models import ( - MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag, + MClassifierTitle, + compute_story_score, ) -from apps.analyzer.models import compute_story_score -from utils.view_functions import is_true -from utils.story_functions import truncate_chars +from apps.reader.models import UserSubscription + +# from django.utils.html import strip_tags +from apps.rss_feeds.models import Feed, MStory from utils import log as logging from utils import mongoengine_fields -from apns2.errors import BadDeviceToken, Unregistered, DeviceTokenNotForTopic -from apns2.client import APNsClient -from apns2.payload import Payload -from bs4 import BeautifulSoup -import urllib.parse +from utils.story_functions import truncate_chars +from utils.view_functions import is_true class NotificationFrequency(enum.Enum): @@ -40,21 +42,21 @@ class NotificationFrequency(enum.Enum): class MUserNotificationTokens(mongo.Document): - '''A user's push notification tokens''' + """A user's push notification tokens""" user_id = mongo.IntField() ios_tokens = mongo.ListField(mongo.StringField(max_length=1024)) use_sandbox = mongo.BooleanField(default=False) meta = { - 'collection': 'notification_tokens', - 'indexes': [ + "collection": "notification_tokens", + "indexes": [ { - 'fields': ['user_id'], - 'unique': True, + "fields": ["user_id"], + "unique": True, } ], - 'allow_inheritance': False, + "allow_inheritance": False, } @classmethod @@ -68,7 +70,7 @@ def get_tokens_for_user(cls, user_id): class MUserFeedNotification(mongo.Document): - '''A user's notifications of a single feed.''' + """A user's notifications of a single feed.""" user_id = mongo.IntField() feed_id = mongo.IntField() @@ -82,32 +84,32 @@ class MUserFeedNotification(mongo.Document): ios_tokens = mongo.ListField(mongo.StringField(max_length=1024)) meta = { - 'collection': 'notifications', - 'indexes': [ - 'feed_id', + "collection": "notifications", + "indexes": [ + "feed_id", { - 'fields': ['user_id', 'feed_id'], - 'unique': True, + "fields": ["user_id", "feed_id"], + "unique": True, }, ], - 'allow_inheritance': False, + "allow_inheritance": False, } def __str__(self): notification_types = [] if self.is_email: - notification_types.append('email') + notification_types.append("email") if self.is_web: - notification_types.append('web') + notification_types.append("web") if self.is_ios: - notification_types.append('ios') + notification_types.append("ios") if self.is_android: - notification_types.append('android') + notification_types.append("android") return "%s/%s: %s -> %s" % ( User.objects.get(pk=self.user_id).username, Feed.get_by_id(self.feed_id), - ','.join(notification_types), + ",".join(notification_types), self.last_notification_date, ) @@ -128,17 +130,17 @@ def feeds_for_user(cls, user_id): for feed in notifications: notifications_by_feed[feed.feed_id] = { - 'notification_types': [], - 'notification_filter': "focus" if feed.is_focus else "unread", + "notification_types": [], + "notification_filter": "focus" if feed.is_focus else "unread", } if feed.is_email: - notifications_by_feed[feed.feed_id]['notification_types'].append('email') + notifications_by_feed[feed.feed_id]["notification_types"].append("email") if feed.is_web: - notifications_by_feed[feed.feed_id]['notification_types'].append('web') + notifications_by_feed[feed.feed_id]["notification_types"].append("web") if feed.is_ios: - notifications_by_feed[feed.feed_id]['notification_types'].append('ios') + notifications_by_feed[feed.feed_id]["notification_types"].append("ios") if feed.is_android: - notifications_by_feed[feed.feed_id]['notification_types'].append('android') + notifications_by_feed[feed.feed_id]["notification_types"].append("android") return notifications_by_feed @@ -153,7 +155,7 @@ def push_feed_notifications(cls, feed_id, new_stories, force=False): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) latest_story_hashes = r.zrange("zF:%s" % feed.pk, -1 * new_stories, -1) - mstories = MStory.objects.filter(story_hash__in=latest_story_hashes).order_by('-story_date') + mstories = MStory.objects.filter(story_hash__in=latest_story_hashes).order_by("-story_date") stories = Feed.format_stories(mstories) total_sent_count = 0 @@ -186,19 +188,19 @@ def push_feed_notifications(cls, feed_id, new_stories, force=False): if settings.DEBUG: logging.debug("Sent too many, ignoring...") continue - if story['story_date'] <= last_notification_date and not force: + if story["story_date"] <= last_notification_date and not force: if settings.DEBUG: logging.debug( "Story date older than last notification date: %s <= %s" - % (story['story_date'], last_notification_date) + % (story["story_date"], last_notification_date) ) continue - if story['story_date'] > user_feed_notification.last_notification_date: - user_feed_notification.last_notification_date = story['story_date'] + if story["story_date"] > user_feed_notification.last_notification_date: + user_feed_notification.last_notification_date = story["story_date"] user_feed_notification.save() - story['story_content'] = html.unescape(story['story_content']) + story["story_content"] = html.unescape(story["story_content"]) sent = user_feed_notification.push_story_notification(story, classifiers, usersub) if sent: @@ -209,49 +211,40 @@ def push_feed_notifications(cls, feed_id, new_stories, force=False): def classifiers(self, usersub): classifiers = {} if usersub.is_trained: - classifiers['feeds'] = list( - MClassifierFeed.objects( - user_id=self.user_id, feed_id=self.feed_id, social_user_id=0 - ) + classifiers["feeds"] = list( + MClassifierFeed.objects(user_id=self.user_id, feed_id=self.feed_id, social_user_id=0) ) - classifiers['authors'] = list( + classifiers["authors"] = list( MClassifierAuthor.objects(user_id=self.user_id, feed_id=self.feed_id) ) - classifiers['titles'] = list( - MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id) - ) - classifiers['tags'] = list( - MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id) - ) + classifiers["titles"] = list(MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id)) + classifiers["tags"] = list(MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id)) return classifiers def title_and_body(self, story, usersub, notification_title_only=False): def replace_with_newlines(element): - text = '' + text = "" for elem in element.recursiveChildGenerator(): if isinstance(elem, (str,)): text += elem - elif elem.name == 'br': - text += '\n' - elif elem.name == 'p': - text += '\n\n' - text = re.sub(r' +', ' ', text).strip() + elif elem.name == "br": + text += "\n" + elif elem.name == "p": + text += "\n\n" + text = re.sub(r" +", " ", text).strip() return text feed_title = usersub.user_title or usersub.feed.feed_title # title = "%s: %s" % (feed_title, story['story_title']) title = feed_title - soup = BeautifulSoup(story['story_content'].strip(), features="lxml") + soup = BeautifulSoup(story["story_content"].strip(), features="lxml") # if notification_title_only: subtitle = None - body_title = html.unescape(story['story_title']).strip() + body_title = html.unescape(story["story_title"]).strip() body_content = replace_with_newlines(soup) if body_content: - if ( - body_title == body_content[: len(body_title)] - or body_content[:100] == body_title[:100] - ): + if body_title == body_content[: len(body_title)] or body_content[:100] == body_title[:100]: body_content = "" else: body_content = f"\n※ {body_content}" @@ -283,7 +276,7 @@ def push_story_notification(self, story, classifiers, usersub): logging.user( user, "~FCSending push notification: %s/%s (score: %s)" - % (story['story_title'][:40], story['story_hash'], story_score), + % (story["story_title"][:40], story["story_hash"], story_score), ) self.send_web(story, user) @@ -298,7 +291,7 @@ def send_web(self, story, user): return r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(user.username, 'notification:%s,%s' % (story['story_hash'], story['story_title'])) + r.publish(user.username, "notification:%s,%s" % (story["story_hash"], story["story_title"])) def send_ios(self, story, user, usersub): if not self.is_ios: @@ -319,45 +312,42 @@ def send_ios(self, story, user, usersub): # 7. cat aps.pem aps_key.noenc.pem > aps.p12.pem # 8. Verify: openssl s_client -connect gateway.push.apple.com:2195 -cert aps.p12.pem # 9. Deploy: aps -l work -t apns,repo,celery - apns = APNsClient( - '/srv/newsblur/config/certificates/aps.p12.pem', use_sandbox=tokens.use_sandbox - ) + apns = APNsClient("/srv/newsblur/config/certificates/aps.p12.pem", use_sandbox=tokens.use_sandbox) - notification_title_only = is_true(user.profile.preference_value('notification_title_only')) + notification_title_only = is_true(user.profile.preference_value("notification_title_only")) title, subtitle, body = self.title_and_body(story, usersub, notification_title_only) image_url = None - if len(story['image_urls']): - image_url = story['image_urls'][0] + if len(story["image_urls"]): + image_url = story["image_urls"][0] # print image_url confirmed_ios_tokens = [] for token in tokens.ios_tokens: logging.user( user, - '~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s' - % (story['story_title'][:50], usersub.feed.feed_title[:50]), + "~BMStory notification by iOS: ~FY~SB%s~SN~BM~FY/~SB%s" + % (story["story_title"][:50], usersub.feed.feed_title[:50]), ) payload = Payload( - alert={'title': title, 'subtitle': subtitle, 'body': body}, + alert={"title": title, "subtitle": subtitle, "body": body}, category="STORY_CATEGORY", mutable_content=True, custom={ - 'story_hash': story['story_hash'], - 'story_feed_id': story['story_feed_id'], - 'image_url': image_url, + "story_hash": story["story_hash"], + "story_feed_id": story["story_feed_id"], + "image_url": image_url, }, ) try: apns.send_notification(token, payload, topic="com.newsblur.NewsBlur") except (BadDeviceToken, Unregistered, DeviceTokenNotForTopic): - logging.user(user, '~BMiOS token expired: ~FR~SB%s' % (token[:50])) + logging.user(user, "~BMiOS token expired: ~FR~SB%s" % (token[:50])) else: confirmed_ios_tokens.append(token) if settings.DEBUG: logging.user( user, - '~BMiOS token good: ~FB~SB%s / %s' - % (token[:50], len(confirmed_ios_tokens)), + "~BMiOS token good: ~FB~SB%s / %s" % (token[:50], len(confirmed_ios_tokens)), ) if len(confirmed_ios_tokens) < len(tokens.ios_tokens): @@ -379,11 +369,14 @@ def send_email(self, story, usersub): r.expire(emails_sent_date_key, 60 * 60 * 24) # Keep for a day count = int(r.hget(emails_sent_date_key, usersub.user_id) or 0) if count > settings.MAX_EMAILS_SENT_PER_DAY_PER_USER: - logging.user(usersub.user, "~BMSent too many email Story notifications by email: ~FR~SB%s~SN~FR emails" % (count)) + logging.user( + usersub.user, + "~BMSent too many email Story notifications by email: ~FR~SB%s~SN~FR emails" % (count), + ) return feed = usersub.feed - story_content = self.sanitize_story(story['story_content']) + story_content = self.sanitize_story(story["story_content"]) params = { "story": story, @@ -392,14 +385,14 @@ def send_email(self, story, usersub): "feed_title": usersub.user_title or feed.feed_title, "favicon_border": feed.favicon_color, } - from_address = 'notifications@newsblur.com' - to_address = '%s <%s>' % (usersub.user.username, usersub.user.email) - text = render_to_string('mail/email_story_notification.txt', params) - html = render_to_string('mail/email_story_notification.xhtml', params) - subject = '%s: %s' % (usersub.user_title or usersub.feed.feed_title, story['story_title']) - subject = subject.replace('\n', ' ') + from_address = "notifications@newsblur.com" + to_address = "%s <%s>" % (usersub.user.username, usersub.user.email) + text = render_to_string("mail/email_story_notification.txt", params) + html = render_to_string("mail/email_story_notification.xhtml", params) + subject = "%s: %s" % (usersub.user_title or usersub.feed.feed_title, story["story_title"]) + subject = subject.replace("\n", " ") msg = EmailMultiAlternatives( - subject, text, from_email='NewsBlur <%s>' % from_address, to=[to_address] + subject, text, from_email="NewsBlur <%s>" % from_address, to=[to_address] ) msg.attach_alternative(html, "text/html") # try: @@ -409,8 +402,8 @@ def send_email(self, story, usersub): # return logging.user( usersub.user, - '~BMStory notification by email: ~FY~SB%s~SN~BM~FY/~SB%s' - % (story['story_title'][:50], usersub.feed.feed_title[:50]), + "~BMStory notification by email: ~FY~SB%s~SN~BM~FY/~SB%s" + % (story["story_title"][:50], usersub.feed.feed_title[:50]), ) def sanitize_story(self, story_content): @@ -419,15 +412,15 @@ def sanitize_story(self, story_content): # Convert videos in newsletters to images for iframe in soup("iframe"): - url = dict(iframe.attrs).get('src', "") + url = dict(iframe.attrs).get("src", "") youtube_id = self.extract_youtube_id(url) if youtube_id: - a = soup.new_tag('a', href=url) + a = soup.new_tag("a", href=url) img = soup.new_tag( - 'img', + "img", style="display: block; 'background-image': \"url(https://%s/img/reader/youtube_play.png), url(http://img.youtube.com/vi/%s/0.jpg)\"" % (fqdn, youtube_id), - src='http://img.youtube.com/vi/%s/0.jpg' % youtube_id, + src="http://img.youtube.com/vi/%s/0.jpg" % youtube_id, ) a.insert(0, img) iframe.replaceWith(a) @@ -439,20 +432,20 @@ def sanitize_story(self, story_content): def extract_youtube_id(self, url): youtube_id = None - if 'youtube.com' in url: + if "youtube.com" in url: youtube_parts = urllib.parse.urlparse(url) - if '/embed/' in youtube_parts.path: - youtube_id = youtube_parts.path.replace('/embed/', '') + if "/embed/" in youtube_parts.path: + youtube_id = youtube_parts.path.replace("/embed/", "") return youtube_id def story_score(self, story, classifiers): score = compute_story_score( story, - classifier_titles=classifiers.get('titles', []), - classifier_authors=classifiers.get('authors', []), - classifier_tags=classifiers.get('tags', []), - classifier_feeds=classifiers.get('feeds', []), + classifier_titles=classifiers.get("titles", []), + classifier_authors=classifiers.get("authors", []), + classifier_tags=classifiers.get("tags", []), + classifier_feeds=classifiers.get("feeds", []), ) return score diff --git a/apps/notifications/tasks.py b/apps/notifications/tasks.py index cb67208a3c..9a4fe4290c 100644 --- a/apps/notifications/tasks.py +++ b/apps/notifications/tasks.py @@ -1,6 +1,7 @@ -from newsblur_web.celeryapp import app from django.contrib.auth.models import User + from apps.notifications.models import MUserFeedNotification +from newsblur_web.celeryapp import app from utils import log as logging diff --git a/apps/notifications/urls.py b/apps/notifications/urls.py index a304edb6f9..90a81f313a 100644 --- a/apps/notifications/urls.py +++ b/apps/notifications/urls.py @@ -1,11 +1,12 @@ from django.conf.urls import url -from apps.notifications import views from oauth2_provider import views as op_views +from apps.notifications import views + urlpatterns = [ - url(r'^$', views.notifications_by_feed, name='notifications-by-feed'), - url(r'^feed/?$', views.set_notifications_for_feed, name='set-notifications-for-feed'), - url(r'^apns_token/?$', views.set_apns_token, name='set-apns-token'), - url(r'^android_token/?$', views.set_android_token, name='set-android-token'), - url(r'^force_push/?$', views.force_push, name='force-push-notification'), -] \ No newline at end of file + url(r"^$", views.notifications_by_feed, name="notifications-by-feed"), + url(r"^feed/?$", views.set_notifications_for_feed, name="set-notifications-for-feed"), + url(r"^apns_token/?$", views.set_apns_token, name="set-apns-token"), + url(r"^android_token/?$", views.set_android_token, name="set-android-token"), + url(r"^force_push/?$", views.force_push, name="force-push-notification"), +] diff --git a/apps/notifications/views.py b/apps/notifications/views.py index b31315f579..06f32101c0 100644 --- a/apps/notifications/views.py +++ b/apps/notifications/views.py @@ -1,12 +1,13 @@ import redis from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required -from utils import json_functions as json -from utils.user_functions import get_user, ajax_login_required + from apps.notifications.models import MUserFeedNotification, MUserNotificationTokens from apps.rss_feeds.models import Feed -from utils.view_functions import required_params +from utils import json_functions as json from utils import log as logging +from utils.user_functions import ajax_login_required, get_user +from utils.view_functions import required_params @ajax_login_required @@ -17,82 +18,90 @@ def notifications_by_feed(request): return notifications_by_feed + @ajax_login_required @json.json_view def set_notifications_for_feed(request): user = get_user(request) - feed_id = request.POST['feed_id'] - notification_types = request.POST.getlist('notification_types') or request.POST.getlist('notification_types[]') - notification_filter = request.POST.get('notification_filter') - + feed_id = request.POST["feed_id"] + notification_types = request.POST.getlist("notification_types") or request.POST.getlist( + "notification_types[]" + ) + notification_filter = request.POST.get("notification_filter") + try: notification = MUserFeedNotification.objects.get(user_id=user.pk, feed_id=feed_id) except MUserFeedNotification.DoesNotExist: params = { - "user_id": user.pk, + "user_id": user.pk, "feed_id": feed_id, } notification = MUserFeedNotification.objects.create(**params) - + web_was_off = not notification.is_web notification.is_focus = bool(notification_filter == "focus") - notification.is_email = bool('email' in notification_types) - notification.is_ios = bool('ios' in notification_types) - notification.is_android = bool('android' in notification_types) - notification.is_web = bool('web' in notification_types) + notification.is_email = bool("email" in notification_types) + notification.is_ios = bool("ios" in notification_types) + notification.is_android = bool("android" in notification_types) + notification.is_web = bool("web" in notification_types) notification.save() - - if (not notification.is_email and - not notification.is_ios and - not notification.is_android and - not notification.is_web): + + if ( + not notification.is_email + and not notification.is_ios + and not notification.is_android + and not notification.is_web + ): notification.delete() - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) if web_was_off and notification.is_web: - r.publish(user.username, 'notification:setup:%s' % feed_id) - + r.publish(user.username, "notification:setup:%s" % feed_id) + notifications_by_feed = MUserFeedNotification.feeds_for_user(user.pk) return {"notifications_by_feed": notifications_by_feed} + @ajax_login_required @json.json_view def set_apns_token(request): """ - Apple Push Notification Service, token is sent by the iOS app. Used to send + Apple Push Notification Service, token is sent by the iOS app. Used to send push notifications to iOS. """ user = get_user(request) tokens = MUserNotificationTokens.get_tokens_for_user(user.pk) - apns_token = request.POST['apns_token'] - + apns_token = request.POST["apns_token"] + logging.user(user, "~FCUpdating APNS push token") if apns_token not in tokens.ios_tokens: tokens.ios_tokens.append(apns_token) tokens.save() - return {'message': 'Token saved.'} - - return {'message': 'Token already saved.'} + return {"message": "Token saved."} + + return {"message": "Token already saved."} + @ajax_login_required @json.json_view def set_android_token(request): """ - Android's push notification tokens. Not sure why I can't find this function in + Android's push notification tokens. Not sure why I can't find this function in the Android code. """ user = get_user(request) tokens = MUserNotificationTokens.get_tokens_for_user(user.pk) - token = request.POST['token'] - + token = request.POST["token"] + logging.user(user, "~FCUpdating Android push token") if token not in tokens.android_tokens: tokens.android_tokens.append(token) tokens.save() - return {'message': 'Token saved.'} - - return {'message': 'Token already saved.'} + return {"message": "Token saved."} + + return {"message": "Token already saved."} + @required_params(feed_id=int) @staff_member_required @@ -102,10 +111,12 @@ def force_push(request): Intended to force a push notification for a feed for testing. Handier than the console. """ user = get_user(request) - feed_id = request.GET['feed_id'] - count = int(request.GET.get('count', 1)) - + feed_id = request.GET["feed_id"] + count = int(request.GET.get("count", 1)) + logging.user(user, "~BM~FWForce pushing %s stories: ~SB%s" % (count, Feed.get_by_id(feed_id))) - sent_count, user_count = MUserFeedNotification.push_feed_notifications(feed_id, new_stories=count, force=True) - - return {"message": "Pushed %s notifications to %s users" % (sent_count, user_count)} \ No newline at end of file + sent_count, user_count = MUserFeedNotification.push_feed_notifications( + feed_id, new_stories=count, force=True + ) + + return {"message": "Pushed %s notifications to %s users" % (sent_count, user_count)} diff --git a/apps/oauth/models.py b/apps/oauth/models.py index c6ff4f1c43..c1522cf114 100644 --- a/apps/oauth/models.py +++ b/apps/oauth/models.py @@ -1 +1 @@ -# No models for OAuth. Use MSocialServices model in social. \ No newline at end of file +# No models for OAuth. Use MSocialServices model in social. diff --git a/apps/oauth/urls.py b/apps/oauth/urls.py index 66742e5cf0..4875c790ce 100644 --- a/apps/oauth/urls.py +++ b/apps/oauth/urls.py @@ -1,35 +1,46 @@ from django.conf.urls import url -from apps.oauth import views from oauth2_provider import views as op_views -urlpatterns = [ - url(r'^twitter_connect/?$', views.twitter_connect, name='twitter-connect'), - url(r'^facebook_connect/?$', views.facebook_connect, name='facebook-connect'), - url(r'^twitter_disconnect/?$', views.twitter_disconnect, name='twitter-disconnect'), - url(r'^facebook_disconnect/?$', views.facebook_disconnect, name='facebook-disconnect'), - url(r'^follow_twitter_account/?$', views.follow_twitter_account, name='social-follow-twitter'), - url(r'^unfollow_twitter_account/?$', views.unfollow_twitter_account, name='social-unfollow-twitter'), +from apps.oauth import views +urlpatterns = [ + url(r"^twitter_connect/?$", views.twitter_connect, name="twitter-connect"), + url(r"^facebook_connect/?$", views.facebook_connect, name="facebook-connect"), + url(r"^twitter_disconnect/?$", views.twitter_disconnect, name="twitter-disconnect"), + url(r"^facebook_disconnect/?$", views.facebook_disconnect, name="facebook-disconnect"), + url(r"^follow_twitter_account/?$", views.follow_twitter_account, name="social-follow-twitter"), + url(r"^unfollow_twitter_account/?$", views.unfollow_twitter_account, name="social-unfollow-twitter"), # Django OAuth Toolkit - url(r'^status/?$', views.ifttt_status, name="ifttt-status"), - url(r'^authorize/?$', op_views.AuthorizationView.as_view(), name="oauth-authorize"), - url(r'^token/?$', op_views.TokenView.as_view(), name="oauth-token"), - url(r'^oauth2/authorize/?$', op_views.AuthorizationView.as_view(), name="ifttt-authorize"), - url(r'^oauth2/token/?$', op_views.TokenView.as_view(), name="ifttt-token"), - url(r'^user/info/?$', views.api_user_info, name="ifttt-user-info"), - url(r'^triggers/(?Pnew-unread-(focus-)?story)/fields/feed_or_folder/options/?$', - views.api_feed_list, name="ifttt-trigger-feedlist"), - url(r'^triggers/(?Pnew-unread-(focus-)?story)/?$', - views.api_unread_story, name="ifttt-trigger-unreadstory"), - url(r'^triggers/new-saved-story/fields/story_tag/options/?$', - views.api_saved_tag_list, name="ifttt-trigger-taglist"), - url(r'^triggers/new-saved-story/?$', views.api_saved_story, name="ifttt-trigger-saved"), - url(r'^triggers/new-shared-story/fields/blurblog_user/options/?$', - views.api_shared_usernames, name="ifttt-trigger-blurbloglist"), - url(r'^triggers/new-shared-story/?$', views.api_shared_story, name="ifttt-trigger-shared"), - url(r'^actions/share-story/?$', views.api_share_new_story, name="ifttt-action-share"), - url(r'^actions/save-story/?$', views.api_save_new_story, name="ifttt-action-saved"), - url(r'^actions/add-site/?$', views.api_save_new_subscription, name="ifttt-action-subscription"), - url(r'^actions/add-site/fields/folder/options/?$', - views.api_folder_list, name="ifttt-action-folderlist"), + url(r"^status/?$", views.ifttt_status, name="ifttt-status"), + url(r"^authorize/?$", op_views.AuthorizationView.as_view(), name="oauth-authorize"), + url(r"^token/?$", op_views.TokenView.as_view(), name="oauth-token"), + url(r"^oauth2/authorize/?$", op_views.AuthorizationView.as_view(), name="ifttt-authorize"), + url(r"^oauth2/token/?$", op_views.TokenView.as_view(), name="ifttt-token"), + url(r"^user/info/?$", views.api_user_info, name="ifttt-user-info"), + url( + r"^triggers/(?Pnew-unread-(focus-)?story)/fields/feed_or_folder/options/?$", + views.api_feed_list, + name="ifttt-trigger-feedlist", + ), + url( + r"^triggers/(?Pnew-unread-(focus-)?story)/?$", + views.api_unread_story, + name="ifttt-trigger-unreadstory", + ), + url( + r"^triggers/new-saved-story/fields/story_tag/options/?$", + views.api_saved_tag_list, + name="ifttt-trigger-taglist", + ), + url(r"^triggers/new-saved-story/?$", views.api_saved_story, name="ifttt-trigger-saved"), + url( + r"^triggers/new-shared-story/fields/blurblog_user/options/?$", + views.api_shared_usernames, + name="ifttt-trigger-blurbloglist", + ), + url(r"^triggers/new-shared-story/?$", views.api_shared_story, name="ifttt-trigger-shared"), + url(r"^actions/share-story/?$", views.api_share_new_story, name="ifttt-action-share"), + url(r"^actions/save-story/?$", views.api_save_new_story, name="ifttt-action-saved"), + url(r"^actions/add-site/?$", views.api_save_new_subscription, name="ifttt-action-subscription"), + url(r"^actions/add-site/fields/folder/options/?$", views.api_folder_list, name="ifttt-action-folderlist"), ] diff --git a/apps/oauth/views.py b/apps/oauth/views.py index 3c436f2579..44eb305a97 100644 --- a/apps/oauth/views.py +++ b/apps/oauth/views.py @@ -1,45 +1,54 @@ -import urllib.request, urllib.parse, urllib.error import datetime +import urllib.error +import urllib.parse +import urllib.request + import lxml.html import tweepy +from django.conf import settings from django.contrib.auth.decorators import login_required -from django.urls import reverse from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.http import HttpResponseForbidden, HttpResponseRedirect -from django.conf import settings -from mongoengine.queryset import NotUniqueError -from mongoengine.queryset import OperationError -from apps.social.models import MSocialServices, MSocialSubscription, MSharedStory -from apps.social.tasks import SyncTwitterFriends, SyncFacebookFriends -from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory -from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag -from apps.analyzer.models import compute_story_score -from apps.rss_feeds.models import Feed, MStory, MStarredStoryCounts, MStarredStory +from django.urls import reverse +from mongoengine.queryset import NotUniqueError, OperationError + +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + compute_story_score, +) +from apps.reader.models import RUserStory, UserSubscription, UserSubscriptionFolders +from apps.rss_feeds.models import Feed, MStarredStory, MStarredStoryCounts, MStory from apps.rss_feeds.text_importer import TextImporter +from apps.social.models import MSharedStory, MSocialServices, MSocialSubscription +from apps.social.tasks import SyncFacebookFriends, SyncTwitterFriends +from utils import json_functions as json from utils import log as logging +from utils import urlnorm from utils.user_functions import ajax_login_required, oauth_login_required from utils.view_functions import render_to -from utils import urlnorm -from utils import json_functions as json from vendor import facebook + @login_required -@render_to('social/social_connect.xhtml') +@render_to("social/social_connect.xhtml") def twitter_connect(request): twitter_consumer_key = settings.TWITTER_CONSUMER_KEY twitter_consumer_secret = settings.TWITTER_CONSUMER_SECRET - - oauth_token = request.GET.get('oauth_token') - oauth_verifier = request.GET.get('oauth_verifier') - denied = request.GET.get('denied') + + oauth_token = request.GET.get("oauth_token") + oauth_verifier = request.GET.get("oauth_verifier") + denied = request.GET.get("denied") if denied: logging.user(request, "~BB~FRDenied Twitter connect") - return {'error': 'Denied! Try connecting again.'} + return {"error": "Denied! Try connecting again."} elif oauth_token and oauth_verifier: try: auth = tweepy.OAuthHandler(twitter_consumer_key, twitter_consumer_secret) - auth.request_token = request.session['twitter_request_token'] + auth.request_token = request.session["twitter_request_token"] # auth.set_request_token(oauth_token, oauth_verifier) auth.get_access_token(oauth_verifier) api = tweepy.API(auth) @@ -54,9 +63,13 @@ def twitter_connect(request): try: user = User.objects.get(pk=existing_user[0].user_id) logging.user(request, "~BB~FRFailed Twitter connect, another user: %s" % user.username) - return dict(error=("Another user (%s, %s) has " - "already connected with those Twitter credentials." - % (user.username, user.email or "no email"))) + return dict( + error=( + "Another user (%s, %s) has " + "already connected with those Twitter credentials." + % (user.username, user.email or "no email") + ) + ) except User.DoesNotExist: existing_user.delete() @@ -68,42 +81,43 @@ def twitter_connect(request): social_services.save() SyncTwitterFriends.delay(user_id=request.user.pk) - + logging.user(request, "~BB~FRFinishing Twitter connect") return {} else: # Start the OAuth process auth = tweepy.OAuthHandler(twitter_consumer_key, twitter_consumer_secret) auth_url = auth.get_authorization_url() - request.session['twitter_request_token'] = auth.request_token + request.session["twitter_request_token"] = auth.request_token logging.user(request, "~BB~FRStarting Twitter connect: %s" % auth.request_token) - return {'next': auth_url} + return {"next": auth_url} @login_required -@render_to('social/social_connect.xhtml') +@render_to("social/social_connect.xhtml") def facebook_connect(request): facebook_app_id = settings.FACEBOOK_APP_ID facebook_secret = settings.FACEBOOK_SECRET - + args = { "client_id": facebook_app_id, - "redirect_uri": "https://" + Site.objects.get_current().domain + '/oauth/facebook_connect', + "redirect_uri": "https://" + Site.objects.get_current().domain + "/oauth/facebook_connect", "scope": "user_friends", "display": "popup", } - verification_code = request.GET.get('code') + verification_code = request.GET.get("code") if verification_code: args["client_secret"] = facebook_secret args["code"] = verification_code - uri = "https://graph.facebook.com/oauth/access_token?" + \ - urllib.parse.urlencode(args) + uri = "https://graph.facebook.com/oauth/access_token?" + urllib.parse.urlencode(args) response_text = urllib.request.urlopen(uri).read() response = json.decode(response_text) - + if "access_token" not in response: - logging.user(request, "~BB~FRFailed Facebook connect, no access_token. (%s): %s" % (args, response)) + logging.user( + request, "~BB~FRFailed Facebook connect, no access_token. (%s): %s" % (args, response) + ) return dict(error="Facebook has returned an error. Try connecting again.") access_token = response["access_token"] @@ -119,9 +133,13 @@ def facebook_connect(request): try: user = User.objects.get(pk=existing_user[0].user_id) logging.user(request, "~BB~FRFailed FB connect, another user: %s" % user.username) - return dict(error=("Another user (%s, %s) has " - "already connected with those Facebook credentials." - % (user.username, user.email or "no email"))) + return dict( + error=( + "Another user (%s, %s) has " + "already connected with those Facebook credentials." + % (user.username, user.email or "no email") + ) + ) except User.DoesNotExist: existing_user.delete() @@ -130,48 +148,51 @@ def facebook_connect(request): social_services.facebook_access_token = access_token social_services.syncing_facebook = True social_services.save() - + SyncFacebookFriends.delay(user_id=request.user.pk) - + logging.user(request, "~BB~FRFinishing Facebook connect") return {} - elif request.GET.get('error'): - logging.user(request, "~BB~FRFailed Facebook connect, error: %s" % request.GET.get('error')) - return {'error': '%s... Try connecting again.' % request.GET.get('error')} + elif request.GET.get("error"): + logging.user(request, "~BB~FRFailed Facebook connect, error: %s" % request.GET.get("error")) + return {"error": "%s... Try connecting again." % request.GET.get("error")} else: # Start the OAuth process logging.user(request, "~BB~FRStarting Facebook connect") url = "https://www.facebook.com/dialog/oauth?" + urllib.parse.urlencode(args) - return {'next': url} + return {"next": url} + @ajax_login_required def twitter_disconnect(request): logging.user(request, "~BB~FRDisconnecting Twitter") social_services = MSocialServices.objects.get(user_id=request.user.pk) social_services.disconnect_twitter() - - return HttpResponseRedirect(reverse('load-user-friends')) + + return HttpResponseRedirect(reverse("load-user-friends")) + @ajax_login_required def facebook_disconnect(request): logging.user(request, "~BB~FRDisconnecting Facebook") social_services = MSocialServices.objects.get(user_id=request.user.pk) social_services.disconnect_facebook() - - return HttpResponseRedirect(reverse('load-user-friends')) - + + return HttpResponseRedirect(reverse("load-user-friends")) + + @ajax_login_required @json.json_view def follow_twitter_account(request): - username = request.POST['username'] + username = request.POST["username"] code = 1 message = "OK" - + logging.user(request, "~BB~FR~SKFollowing Twitter: %s" % username) - - if username not in ['samuelclay', 'newsblur']: + + if username not in ["samuelclay", "newsblur"]: return HttpResponseForbidden() - + social_services = MSocialServices.objects.get(user_id=request.user.pk) try: api = social_services.twitter_api() @@ -179,21 +200,22 @@ def follow_twitter_account(request): except tweepy.TweepError as e: code = -1 message = e - - return {'code': code, 'message': message} - + + return {"code": code, "message": message} + + @ajax_login_required @json.json_view def unfollow_twitter_account(request): - username = request.POST['username'] + username = request.POST["username"] code = 1 message = "OK" - + logging.user(request, "~BB~FRUnfollowing Twitter: %s" % username) - - if username not in ['samuelclay', 'newsblur']: + + if username not in ["samuelclay", "newsblur"]: return HttpResponseForbidden() - + social_services = MSocialServices.objects.get(user_id=request.user.pk) try: api = social_services.twitter_api() @@ -201,18 +223,25 @@ def unfollow_twitter_account(request): except tweepy.TweepError as e: code = -1 message = e - - return {'code': code, 'message': message} + + return {"code": code, "message": message} + @oauth_login_required def api_user_info(request): user = request.user - - return json.json_response(request, {"data": { - "name": user.username, - "id": user.pk, - }}) - + + return json.json_response( + request, + { + "data": { + "name": user.username, + "id": user.pk, + } + }, + ) + + @oauth_login_required @json.json_view def api_feed_list(request, trigger_slug=None): @@ -220,18 +249,16 @@ def api_feed_list(request, trigger_slug=None): try: usf = UserSubscriptionFolders.objects.get(user=user) except UserSubscriptionFolders.DoesNotExist: - return {"errors": [{ - 'message': 'Could not find feeds for user.' - }]} + return {"errors": [{"message": "Could not find feeds for user."}]} flat_folders = usf.flatten_folders() titles = [dict(label=" - Folder: All Site Stories", value="all")] feeds = {} - - user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True) - + + user_subs = UserSubscription.objects.select_related("feed").filter(user=user, active=True) + for sub in user_subs: feeds[sub.feed_id] = sub.canonical() - + for folder_title in sorted(flat_folders.keys()): if folder_title and folder_title != " ": titles.append(dict(label=" - Folder: %s" % folder_title, value=folder_title, optgroup=True)) @@ -239,53 +266,62 @@ def api_feed_list(request, trigger_slug=None): titles.append(dict(label=" - Folder: Top Level", value="Top Level", optgroup=True)) folder_contents = [] for feed_id in flat_folders[folder_title]: - if feed_id not in feeds: continue + if feed_id not in feeds: + continue feed = feeds[feed_id] - folder_contents.append(dict(label=feed['feed_title'], value=str(feed['id']))) - folder_contents = sorted(folder_contents, key=lambda f: f['label'].lower()) + folder_contents.append(dict(label=feed["feed_title"], value=str(feed["id"]))) + folder_contents = sorted(folder_contents, key=lambda f: f["label"].lower()) titles.extend(folder_contents) - + return {"data": titles} - + + @oauth_login_required @json.json_view def api_folder_list(request, trigger_slug=None): user = request.user usf = UserSubscriptionFolders.objects.get(user=user) flat_folders = usf.flatten_folders() - if 'add-new-subscription' in request.path: + if "add-new-subscription" in request.path: titles = [] else: titles = [dict(label="All Site Stories", value="all")] - + for folder_title in sorted(flat_folders.keys()): if folder_title and folder_title != " ": titles.append(dict(label=folder_title, value=folder_title)) else: titles.append(dict(label="Top Level", value="Top Level")) - + return {"data": titles} + @oauth_login_required @json.json_view def api_saved_tag_list(request): user = request.user starred_counts, starred_count = MStarredStoryCounts.user_counts(user.pk, include_total=True) tags = [] - + for tag in starred_counts: - if not tag['tag'] or tag['tag'] == "": continue - tags.append(dict(label="%s (%s %s)" % (tag['tag'], tag['count'], - 'story' if tag['count'] == 1 else 'stories'), - value=tag['tag'])) - tags = sorted(tags, key=lambda t: t['value'].lower()) - catchall = dict(label="All Saved Stories (%s %s)" % (starred_count, - 'story' if starred_count == 1 else 'stories'), - value="all") + if not tag["tag"] or tag["tag"] == "": + continue + tags.append( + dict( + label="%s (%s %s)" % (tag["tag"], tag["count"], "story" if tag["count"] == 1 else "stories"), + value=tag["tag"], + ) + ) + tags = sorted(tags, key=lambda t: t["value"].lower()) + catchall = dict( + label="All Saved Stories (%s %s)" % (starred_count, "story" if starred_count == 1 else "stories"), + value="all", + ) tags.insert(0, catchall) - + return {"data": tags} + @oauth_login_required @json.json_view def api_shared_usernames(request): @@ -294,28 +330,36 @@ def api_shared_usernames(request): blurblogs = [] for social_feed in social_feeds: - if not social_feed['shared_stories_count']: continue - blurblogs.append(dict(label="%s (%s %s)" % (social_feed['username'], - social_feed['shared_stories_count'], - 'story' if social_feed['shared_stories_count'] == 1 else 'stories'), - value="%s" % social_feed['user_id'])) - blurblogs = sorted(blurblogs, key=lambda b: b['label'].lower()) - catchall = dict(label="All Shared Stories", - value="all") + if not social_feed["shared_stories_count"]: + continue + blurblogs.append( + dict( + label="%s (%s %s)" + % ( + social_feed["username"], + social_feed["shared_stories_count"], + "story" if social_feed["shared_stories_count"] == 1 else "stories", + ), + value="%s" % social_feed["user_id"], + ) + ) + blurblogs = sorted(blurblogs, key=lambda b: b["label"].lower()) + catchall = dict(label="All Shared Stories", value="all") blurblogs.insert(0, catchall) - + return {"data": blurblogs} + @oauth_login_required @json.json_view def api_unread_story(request, trigger_slug=None): user = request.user body = request.body_json - after = body.get('after', None) - before = body.get('before', None) - limit = body.get('limit', 50) - fields = body.get('triggerFields') - feed_or_folder = fields['feed_or_folder'] + after = body.get("after", None) + before = body.get("before", None) + limit = body.get("limit", 50) + fields = body.get("triggerFields") + feed_or_folder = fields["feed_or_folder"] entries = [] if isinstance(feed_or_folder, int) or feed_or_folder.isdigit(): @@ -326,8 +370,7 @@ def api_unread_story(request, trigger_slug=None): return dict(data=[]) found_feed_ids = [feed_id] found_trained_feed_ids = [feed_id] if usersub.is_trained else [] - stories = usersub.get_stories(order="newest", read_filter="unread", - offset=0, limit=limit) + stories = usersub.get_stories(order="newest", read_filter="unread", offset=0, limit=limit) else: folder_title = feed_or_folder if folder_title == "Top Level": @@ -337,11 +380,10 @@ def api_unread_story(request, trigger_slug=None): feed_ids = None if folder_title != "all": feed_ids = flat_folders.get(folder_title) - usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, - read_filter="unread") + usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, read_filter="unread") feed_ids = [sub.feed_id for sub in usersubs] params = { - "user_id": user.pk, + "user_id": user.pk, "feed_ids": feed_ids, "offset": 0, "limit": limit, @@ -351,261 +393,321 @@ def api_unread_story(request, trigger_slug=None): "cutoff_date": user.profile.unread_cutoff, } story_hashes, unread_feed_story_hashes = UserSubscription.feed_stories(**params) - mstories = MStory.objects(story_hash__in=story_hashes).order_by('-story_date') + mstories = MStory.objects(story_hash__in=story_hashes).order_by("-story_date") stories = Feed.format_stories(mstories) - found_feed_ids = list(set([story['story_feed_id'] for story in stories])) + found_feed_ids = list(set([story["story_feed_id"] for story in stories])) trained_feed_ids = [sub.feed_id for sub in usersubs if sub.is_trained] found_trained_feed_ids = list(set(trained_feed_ids) & set(found_feed_ids)) - + if found_trained_feed_ids: - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - feeds = dict([(f.pk, { - "title": f.feed_title, - "website": f.feed_link, - "address": f.feed_address, - }) for f in Feed.objects.filter(pk__in=found_feed_ids)]) + classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids)) + classifier_authors = list( + MClassifierAuthor.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids) + ) + classifier_titles = list( + MClassifierTitle.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids) + ) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids)) + feeds = dict( + [ + ( + f.pk, + { + "title": f.feed_title, + "website": f.feed_link, + "address": f.feed_address, + }, + ) + for f in Feed.objects.filter(pk__in=found_feed_ids) + ] + ) for story in stories: - if before and int(story['story_date'].strftime("%s")) > before: continue - if after and int(story['story_date'].strftime("%s")) < after: continue + if before and int(story["story_date"].strftime("%s")) > before: + continue + if after and int(story["story_date"].strftime("%s")) < after: + continue score = 0 - if found_trained_feed_ids and story['story_feed_id'] in found_trained_feed_ids: - score = compute_story_score(story, classifier_titles=classifier_titles, - classifier_authors=classifier_authors, - classifier_tags=classifier_tags, - classifier_feeds=classifier_feeds) - if score < 0: continue - if trigger_slug == "new-unread-focus-story" and score < 1: continue - feed = feeds.get(story['story_feed_id'], None) - entries.append({ - "StoryTitle": story['story_title'], - "StoryContent": story['story_content'], - "StoryURL": story['story_permalink'], - "StoryAuthor": story['story_authors'], - "PublishedAt": story['story_date'].strftime("%Y-%m-%dT%H:%M:%SZ"), - "StoryScore": score, - "Site": feed and feed['title'], - "SiteURL": feed and feed['website'], - "SiteRSS": feed and feed['address'], - "meta": { - "id": story['story_hash'], - "timestamp": int(story['story_date'].strftime("%s")) - }, - }) - + if found_trained_feed_ids and story["story_feed_id"] in found_trained_feed_ids: + score = compute_story_score( + story, + classifier_titles=classifier_titles, + classifier_authors=classifier_authors, + classifier_tags=classifier_tags, + classifier_feeds=classifier_feeds, + ) + if score < 0: + continue + if trigger_slug == "new-unread-focus-story" and score < 1: + continue + feed = feeds.get(story["story_feed_id"], None) + entries.append( + { + "StoryTitle": story["story_title"], + "StoryContent": story["story_content"], + "StoryURL": story["story_permalink"], + "StoryAuthor": story["story_authors"], + "PublishedAt": story["story_date"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "StoryScore": score, + "Site": feed and feed["title"], + "SiteURL": feed and feed["website"], + "SiteRSS": feed and feed["address"], + "meta": {"id": story["story_hash"], "timestamp": int(story["story_date"].strftime("%s"))}, + } + ) + if after: - entries = sorted(entries, key=lambda s: s['meta']['timestamp']) - - logging.user(request, "~FYChecking unread%s stories with ~SB~FCIFTTT~SN~FY: ~SB%s~SN - ~SB%s~SN stories" % (" ~SBfocus~SN" if trigger_slug == "new-unread-focus-story" else "", feed_or_folder, len(entries))) - + entries = sorted(entries, key=lambda s: s["meta"]["timestamp"]) + + logging.user( + request, + "~FYChecking unread%s stories with ~SB~FCIFTTT~SN~FY: ~SB%s~SN - ~SB%s~SN stories" + % (" ~SBfocus~SN" if trigger_slug == "new-unread-focus-story" else "", feed_or_folder, len(entries)), + ) + return {"data": entries[:limit]} + @oauth_login_required @json.json_view def api_saved_story(request): user = request.user body = request.body_json - after = body.get('after', None) - before = body.get('before', None) - limit = body.get('limit', 50) - fields = body.get('triggerFields') - story_tag = fields['story_tag'] + after = body.get("after", None) + before = body.get("before", None) + limit = body.get("limit", 50) + fields = body.get("triggerFields") + story_tag = fields["story_tag"] entries = [] - + if story_tag == "all": story_tag = "" - + params = dict(user_id=user.pk) if story_tag: params.update(dict(user_tags__contains=story_tag)) - mstories = MStarredStory.objects(**params).order_by('-starred_date')[:limit] - stories = Feed.format_stories(mstories) - - found_feed_ids = list(set([story['story_feed_id'] for story in stories])) - feeds = dict([(f.pk, { - "title": f.feed_title, - "website": f.feed_link, - "address": f.feed_address, - }) for f in Feed.objects.filter(pk__in=found_feed_ids)]) + mstories = MStarredStory.objects(**params).order_by("-starred_date")[:limit] + stories = Feed.format_stories(mstories) + + found_feed_ids = list(set([story["story_feed_id"] for story in stories])) + feeds = dict( + [ + ( + f.pk, + { + "title": f.feed_title, + "website": f.feed_link, + "address": f.feed_address, + }, + ) + for f in Feed.objects.filter(pk__in=found_feed_ids) + ] + ) for story in stories: - if before and int(story['story_date'].strftime("%s")) > before: continue - if after and int(story['story_date'].strftime("%s")) < after: continue - feed = feeds.get(story['story_feed_id'], None) - entries.append({ - "StoryTitle": story['story_title'], - "StoryContent": story['story_content'], - "StoryURL": story['story_permalink'], - "StoryAuthor": story['story_authors'], - "PublishedAt": story['story_date'].strftime("%Y-%m-%dT%H:%M:%SZ"), - "SavedAt": story['starred_date'].strftime("%Y-%m-%dT%H:%M:%SZ"), - "Tags": ', '.join(story['user_tags']), - "Site": feed and feed['title'], - "SiteURL": feed and feed['website'], - "SiteRSS": feed and feed['address'], - "meta": { - "id": story['story_hash'], - "timestamp": int(story['starred_date'].strftime("%s")) - }, - }) + if before and int(story["story_date"].strftime("%s")) > before: + continue + if after and int(story["story_date"].strftime("%s")) < after: + continue + feed = feeds.get(story["story_feed_id"], None) + entries.append( + { + "StoryTitle": story["story_title"], + "StoryContent": story["story_content"], + "StoryURL": story["story_permalink"], + "StoryAuthor": story["story_authors"], + "PublishedAt": story["story_date"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "SavedAt": story["starred_date"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "Tags": ", ".join(story["user_tags"]), + "Site": feed and feed["title"], + "SiteURL": feed and feed["website"], + "SiteRSS": feed and feed["address"], + "meta": {"id": story["story_hash"], "timestamp": int(story["starred_date"].strftime("%s"))}, + } + ) if after: - entries = sorted(entries, key=lambda s: s['meta']['timestamp']) - - logging.user(request, "~FCChecking saved stories from ~SBIFTTT~SB: ~SB%s~SN - ~SB%s~SN stories" % (story_tag if story_tag else "[All stories]", len(entries))) - + entries = sorted(entries, key=lambda s: s["meta"]["timestamp"]) + + logging.user( + request, + "~FCChecking saved stories from ~SBIFTTT~SB: ~SB%s~SN - ~SB%s~SN stories" + % (story_tag if story_tag else "[All stories]", len(entries)), + ) + return {"data": entries} - + + @oauth_login_required @json.json_view def api_shared_story(request): user = request.user body = request.body_json - after = body.get('after', None) - before = body.get('before', None) - limit = body.get('limit', 50) - fields = body.get('triggerFields') - blurblog_user = fields['blurblog_user'] + after = body.get("after", None) + before = body.get("before", None) + limit = body.get("limit", 50) + fields = body.get("triggerFields") + blurblog_user = fields["blurblog_user"] entries = [] - + if isinstance(blurblog_user, int) or blurblog_user.isdigit(): social_user_ids = [int(blurblog_user)] elif blurblog_user == "all": socialsubs = MSocialSubscription.objects.filter(user_id=user.pk) social_user_ids = [ss.subscription_user_id for ss in socialsubs] - mstories = MSharedStory.objects( - user_id__in=social_user_ids - ).order_by('-shared_date')[:limit] + mstories = MSharedStory.objects(user_id__in=social_user_ids).order_by("-shared_date")[:limit] stories = Feed.format_stories(mstories) - - found_feed_ids = list(set([story['story_feed_id'] for story in stories])) - share_user_ids = list(set([story['user_id'] for story in stories])) - users = dict([(u.pk, u.username) - for u in User.objects.filter(pk__in=share_user_ids).only('pk', 'username')]) - feeds = dict([(f.pk, { - "title": f.feed_title, - "website": f.feed_link, - "address": f.feed_address, - }) for f in Feed.objects.filter(pk__in=found_feed_ids)]) - - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, - social_user_id__in=social_user_ids)) - classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, - social_user_id__in=social_user_ids)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, - social_user_id__in=social_user_ids)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, - social_user_id__in=social_user_ids)) + + found_feed_ids = list(set([story["story_feed_id"] for story in stories])) + share_user_ids = list(set([story["user_id"] for story in stories])) + users = dict( + [(u.pk, u.username) for u in User.objects.filter(pk__in=share_user_ids).only("pk", "username")] + ) + feeds = dict( + [ + ( + f.pk, + { + "title": f.feed_title, + "website": f.feed_link, + "address": f.feed_address, + }, + ) + for f in Feed.objects.filter(pk__in=found_feed_ids) + ] + ) + + classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, social_user_id__in=social_user_ids)) + classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, social_user_id__in=social_user_ids)) + classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, social_user_id__in=social_user_ids)) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, social_user_id__in=social_user_ids)) # Merge with feed specific classifiers - classifier_feeds = classifier_feeds + list(MClassifierFeed.objects(user_id=user.pk, - feed_id__in=found_feed_ids)) - classifier_authors = classifier_authors + list(MClassifierAuthor.objects(user_id=user.pk, - feed_id__in=found_feed_ids)) - classifier_titles = classifier_titles + list(MClassifierTitle.objects(user_id=user.pk, - feed_id__in=found_feed_ids)) - classifier_tags = classifier_tags + list(MClassifierTag.objects(user_id=user.pk, - feed_id__in=found_feed_ids)) - + classifier_feeds = classifier_feeds + list( + MClassifierFeed.objects(user_id=user.pk, feed_id__in=found_feed_ids) + ) + classifier_authors = classifier_authors + list( + MClassifierAuthor.objects(user_id=user.pk, feed_id__in=found_feed_ids) + ) + classifier_titles = classifier_titles + list( + MClassifierTitle.objects(user_id=user.pk, feed_id__in=found_feed_ids) + ) + classifier_tags = classifier_tags + list( + MClassifierTag.objects(user_id=user.pk, feed_id__in=found_feed_ids) + ) + for story in stories: - if before and int(story['shared_date'].strftime("%s")) > before: continue - if after and int(story['shared_date'].strftime("%s")) < after: continue - score = compute_story_score(story, classifier_titles=classifier_titles, - classifier_authors=classifier_authors, - classifier_tags=classifier_tags, - classifier_feeds=classifier_feeds) - if score < 0: continue - feed = feeds.get(story['story_feed_id'], None) - entries.append({ - "StoryTitle": story['story_title'], - "StoryContent": story['story_content'], - "StoryURL": story['story_permalink'], - "StoryAuthor": story['story_authors'], - "PublishedAt": story['story_date'].strftime("%Y-%m-%dT%H:%M:%SZ"), - "StoryScore": score, - "Comments": story['comments'], - "Username": users.get(story['user_id']), - "SharedAt": story['shared_date'].strftime("%Y-%m-%dT%H:%M:%SZ"), - "Site": feed and feed['title'], - "SiteURL": feed and feed['website'], - "SiteRSS": feed and feed['address'], - "meta": { - "id": story['story_hash'], - "timestamp": int(story['shared_date'].strftime("%s")) - }, - }) + if before and int(story["shared_date"].strftime("%s")) > before: + continue + if after and int(story["shared_date"].strftime("%s")) < after: + continue + score = compute_story_score( + story, + classifier_titles=classifier_titles, + classifier_authors=classifier_authors, + classifier_tags=classifier_tags, + classifier_feeds=classifier_feeds, + ) + if score < 0: + continue + feed = feeds.get(story["story_feed_id"], None) + entries.append( + { + "StoryTitle": story["story_title"], + "StoryContent": story["story_content"], + "StoryURL": story["story_permalink"], + "StoryAuthor": story["story_authors"], + "PublishedAt": story["story_date"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "StoryScore": score, + "Comments": story["comments"], + "Username": users.get(story["user_id"]), + "SharedAt": story["shared_date"].strftime("%Y-%m-%dT%H:%M:%SZ"), + "Site": feed and feed["title"], + "SiteURL": feed and feed["website"], + "SiteRSS": feed and feed["address"], + "meta": {"id": story["story_hash"], "timestamp": int(story["shared_date"].strftime("%s"))}, + } + ) if after: - entries = sorted(entries, key=lambda s: s['meta']['timestamp']) - - logging.user(request, "~FMChecking shared stories from ~SB~FCIFTTT~SN~FM: ~SB~FM%s~FM~SN - ~SB%s~SN stories" % (blurblog_user, len(entries))) + entries = sorted(entries, key=lambda s: s["meta"]["timestamp"]) + + logging.user( + request, + "~FMChecking shared stories from ~SB~FCIFTTT~SN~FM: ~SB~FM%s~FM~SN - ~SB%s~SN stories" + % (blurblog_user, len(entries)), + ) return {"data": entries} + @json.json_view def ifttt_status(request): logging.user(request, "~FCChecking ~SBIFTTT~SN status") - return {"data": { - "status": "OK", - "time": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), - }} + return { + "data": { + "status": "OK", + "time": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + } + } + @oauth_login_required @json.json_view def api_share_new_story(request): user = request.user body = request.body_json - fields = body.get('actionFields') - story_url = urlnorm.normalize(fields['story_url']) - story_content = fields.get('story_content', "") - story_title = fields.get('story_title', "") - story_author = fields.get('story_author', "") - comments = fields.get('comments', None) - + fields = body.get("actionFields") + story_url = urlnorm.normalize(fields["story_url"]) + story_content = fields.get("story_content", "") + story_title = fields.get("story_title", "") + story_author = fields.get("story_author", "") + comments = fields.get("comments", None) + logging.user(request.user, "~FBFinding feed (api_share_new_story): %s" % story_url) original_feed = Feed.get_feed_from_url(story_url, create=True, fetch=True) story_hash = MStory.guid_hash_unsaved(story_url) - feed_id = (original_feed and original_feed.pk or 0) + feed_id = original_feed and original_feed.pk or 0 if not user.profile.is_premium and MSharedStory.feed_quota(user.pk, story_hash, feed_id=feed_id): - return {"errors": [{ - 'message': 'Only premium users can share multiple stories per day from the same site.' - }]} - + return { + "errors": [ + {"message": "Only premium users can share multiple stories per day from the same site."} + ] + } + quota = 3 if MSharedStory.feed_quota(user.pk, story_hash, quota=quota): - logging.user(request, "~BM~FRNOT ~FYSharing story from ~SB~FCIFTTT~FY, over quota: ~SB%s: %s" % (story_url, comments)) - return {"errors": [{ - 'message': 'You can only share %s stories per day.' % quota - }]} - + logging.user( + request, + "~BM~FRNOT ~FYSharing story from ~SB~FCIFTTT~FY, over quota: ~SB%s: %s" % (story_url, comments), + ) + return {"errors": [{"message": "You can only share %s stories per day." % quota}]} + if not story_content or not story_title: ti = TextImporter(feed=original_feed, story_url=story_url, request=request) original_story = ti.fetch(return_document=True) if original_story: - story_url = original_story['url'] + story_url = original_story["url"] if not story_content: - story_content = original_story['content'] + story_content = original_story["content"] if not story_title: - story_title = original_story['title'] - + story_title = original_story["title"] + if story_content: story_content = lxml.html.fromstring(story_content) story_content.make_links_absolute(story_url) story_content = lxml.html.tostring(story_content) - - shared_story = MSharedStory.objects.filter(user_id=user.pk, - story_feed_id=original_feed and original_feed.pk or 0, - story_guid=story_url).limit(1).first() + + shared_story = ( + MSharedStory.objects.filter( + user_id=user.pk, story_feed_id=original_feed and original_feed.pk or 0, story_guid=story_url + ) + .limit(1) + .first() + ) if not shared_story: - title_max = MSharedStory._fields['story_title'].max_length + title_max = MSharedStory._fields["story_title"].max_length story_db = { "story_guid": story_url, "story_permalink": story_url, @@ -624,107 +726,121 @@ def api_share_new_story(request): for socialsub in socialsubs: socialsub.needs_unread_recalc = True socialsub.save() - logging.user(request, "~BM~FYSharing story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments)) + logging.user( + request, "~BM~FYSharing story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments) + ) except NotUniqueError: - logging.user(request, "~BM~FY~SBAlready~SN shared story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments)) + logging.user( + request, + "~BM~FY~SBAlready~SN shared story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments), + ) else: - logging.user(request, "~BM~FY~SBAlready~SN shared story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments)) - + logging.user( + request, "~BM~FY~SBAlready~SN shared story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments) + ) + try: - socialsub = MSocialSubscription.objects.get(user_id=user.pk, - subscription_user_id=user.pk) + socialsub = MSocialSubscription.objects.get(user_id=user.pk, subscription_user_id=user.pk) except MSocialSubscription.DoesNotExist: socialsub = None - + if socialsub and shared_story: - socialsub.mark_story_ids_as_read([shared_story.story_hash], - shared_story.story_feed_id, - request=request) + socialsub.mark_story_ids_as_read( + [shared_story.story_hash], shared_story.story_feed_id, request=request + ) elif shared_story: RUserStory.mark_read(user.pk, shared_story.story_feed_id, shared_story.story_hash) - + if shared_story: shared_story.publish_update_to_subscribers() - - return {"data": [{ - "id": shared_story and shared_story.story_guid, - "url": shared_story and shared_story.blurblog_permalink() - }]} + + return { + "data": [ + { + "id": shared_story and shared_story.story_guid, + "url": shared_story and shared_story.blurblog_permalink(), + } + ] + } + @oauth_login_required @json.json_view def api_save_new_story(request): user = request.user body = request.body_json - fields = body.get('actionFields') - story_url = urlnorm.normalize(fields['story_url']) - story_content = fields.get('story_content', "") - story_title = fields.get('story_title', "") - story_author = fields.get('story_author', "") - user_tags = fields.get('user_tags', "") + fields = body.get("actionFields") + story_url = urlnorm.normalize(fields["story_url"]) + story_content = fields.get("story_content", "") + story_title = fields.get("story_title", "") + story_author = fields.get("story_author", "") + user_tags = fields.get("user_tags", "") story = None - + logging.user(request.user, "~FBFinding feed (api_save_new_story): %s" % story_url) original_feed = Feed.get_feed_from_url(story_url) if not story_content or not story_title: ti = TextImporter(feed=original_feed, story_url=story_url, request=request) original_story = ti.fetch(return_document=True) if original_story: - story_url = original_story['url'] + story_url = original_story["url"] if not story_content: - story_content = original_story['content'] + story_content = original_story["content"] if not story_title: - story_title = original_story['title'] + story_title = original_story["title"] try: story_db = { "user_id": user.pk, "starred_date": datetime.datetime.now(), "story_date": datetime.datetime.now(), - "story_title": story_title or '[Untitled]', + "story_title": story_title or "[Untitled]", "story_permalink": story_url, "story_guid": story_url, "story_content": story_content, "story_author_name": story_author, "story_feed_id": original_feed and original_feed.pk or 0, - "user_tags": [tag for tag in user_tags.split(',')] + "user_tags": [tag for tag in user_tags.split(",")], } story = MStarredStory.objects.create(**story_db) - logging.user(request, "~FCStarring by ~SBIFTTT~SN: ~SB%s~SN in ~SB%s" % (story_db['story_title'][:50], original_feed and original_feed)) + logging.user( + request, + "~FCStarring by ~SBIFTTT~SN: ~SB%s~SN in ~SB%s" + % (story_db["story_title"][:50], original_feed and original_feed), + ) MStarredStoryCounts.count_for_user(user.pk) except OperationError: - logging.user(request, "~FCAlready starred by ~SBIFTTT~SN: ~SB%s" % (story_db['story_title'][:50])) + logging.user(request, "~FCAlready starred by ~SBIFTTT~SN: ~SB%s" % (story_db["story_title"][:50])) pass - - return {"data": [{ - "id": story and story.id, - "url": story and story.story_permalink - }]} + + return {"data": [{"id": story and story.id, "url": story and story.story_permalink}]} + @oauth_login_required @json.json_view def api_save_new_subscription(request): user = request.user body = request.body_json - fields = body.get('actionFields') - url = urlnorm.normalize(fields['url']) - folder = fields['folder'] - + fields = body.get("actionFields") + url = urlnorm.normalize(fields["url"]) + folder = fields["folder"] + if folder == "Top Level": folder = " " - + code, message, us = UserSubscription.add_subscription( - user=user, - feed_address=url, - folder=folder, - bookmarklet=True + user=user, feed_address=url, folder=folder, bookmarklet=True ) - + logging.user(request, "~FRAdding URL from ~FC~SBIFTTT~SN~FR: ~SB%s (in %s)" % (url, folder)) if us and us.feed: url = us.feed.feed_address - return {"data": [{ - "id": us and us.feed_id, - "url": url, - }]} + return { + "data": [ + { + "id": us and us.feed_id, + "url": url, + } + ] + } diff --git a/apps/profile/factories.py b/apps/profile/factories.py index b5b57d51bf..fba0246b86 100644 --- a/apps/profile/factories.py +++ b/apps/profile/factories.py @@ -1,20 +1,22 @@ import factory -from factory.django import DjangoModelFactory from django.contrib.auth.models import User +from factory.django import DjangoModelFactory + from apps.profile.models import Profile + class UserFactory(DjangoModelFactory): - first_name = factory.Faker('first_name') - last_name = factory.Faker('last_name') - username = factory.Faker('email') - date_joined = factory.Faker('date_time') + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + username = factory.Faker("email") + date_joined = factory.Faker("date_time") class Meta: model = User - class ProfileFactory(DjangoModelFactory): user = factory.SubFactory(UserFactory) + class Meta: model = Profile diff --git a/apps/profile/forms.py b/apps/profile/forms.py index b47a06a3da..6e48680260 100644 --- a/apps/profile/forms.py +++ b/apps/profile/forms.py @@ -1,12 +1,19 @@ import re + import requests from django import forms -from vendor.zebra.forms import StripePaymentForm -from django.utils.safestring import mark_safe from django.contrib.auth import authenticate from django.contrib.auth.models import User -from apps.profile.models import change_password, blank_authenticate, MGiftCode, MCustomStyling +from django.utils.safestring import mark_safe + +from apps.profile.models import ( + MCustomStyling, + MGiftCode, + blank_authenticate, + change_password, +) from apps.social.models import MSocialProfile +from vendor.zebra.forms import StripePaymentForm PLANS = [ ("newsblur-premium-36", mark_safe("$36 / year ($3/month)")), @@ -14,135 +21,133 @@ ("newsblur-premium-pro", mark_safe("$299 / year (~$25/month)")), ] + class HorizRadioRenderer(forms.RadioSelect): - """ this overrides widget method to put radio buttons horizontally - instead of vertically. + """this overrides widget method to put radio buttons horizontally + instead of vertically. """ + def render(self, name, value, attrs=None, renderer=None): - """Outputs radios""" - choices = '\n'.join(['%s\n' % w for w in self]) - return mark_safe('
%s
' % choices) + """Outputs radios""" + choices = "\n".join(["%s\n" % w for w in self]) + return mark_safe('
%s
' % choices) + class StripePlusPaymentForm(StripePaymentForm): def __init__(self, *args, **kwargs): - email = kwargs.pop('email') - plan = kwargs.pop('plan', '') + email = kwargs.pop("email") + plan = kwargs.pop("plan", "") super(StripePlusPaymentForm, self).__init__(*args, **kwargs) - self.fields['email'].initial = email + self.fields["email"].initial = email if plan: - self.fields['plan'].initial = plan + self.fields["plan"].initial = plan - email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)), - label='Email address', - required=False) - plan = forms.ChoiceField(required=False, widget=forms.RadioSelect, - choices=PLANS, label='Plan') + email = forms.EmailField( + widget=forms.TextInput(attrs=dict(maxlength=75)), label="Email address", required=False + ) + plan = forms.ChoiceField(required=False, widget=forms.RadioSelect, choices=PLANS, label="Plan") class DeleteAccountForm(forms.Form): - password = forms.CharField(widget=forms.PasswordInput(), - label="Confirm your password", - required=False) - confirm = forms.CharField(label="Type \"Delete\" to confirm", - widget=forms.TextInput(), - required=False) + password = forms.CharField(widget=forms.PasswordInput(), label="Confirm your password", required=False) + confirm = forms.CharField(label='Type "Delete" to confirm', widget=forms.TextInput(), required=False) def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') + self.user = kwargs.pop("user") super(DeleteAccountForm, self).__init__(*args, **kwargs) - + def clean_password(self): - user_auth = authenticate(username=self.user.username, - password=self.cleaned_data['password']) + user_auth = authenticate(username=self.user.username, password=self.cleaned_data["password"]) if not user_auth: user_auth = blank_authenticate(username=self.user.username) - + if not user_auth: - raise forms.ValidationError('Your password doesn\'t match.') + raise forms.ValidationError("Your password doesn't match.") - return self.cleaned_data['password'] + return self.cleaned_data["password"] def clean_confirm(self): - if self.cleaned_data.get('confirm', "").lower() != "delete": + if self.cleaned_data.get("confirm", "").lower() != "delete": raise forms.ValidationError('Please type "DELETE" to confirm deletion.') - return self.cleaned_data['confirm'] + return self.cleaned_data["confirm"] + class ForgotPasswordForm(forms.Form): - email = forms.CharField(widget=forms.TextInput(), - label="Your email address", - required=False) + email = forms.CharField(widget=forms.TextInput(), label="Your email address", required=False) def __init__(self, *args, **kwargs): super(ForgotPasswordForm, self).__init__(*args, **kwargs) - + def clean_email(self): - if not self.cleaned_data['email']: - raise forms.ValidationError('Please enter in an email address.') + if not self.cleaned_data["email"]: + raise forms.ValidationError("Please enter in an email address.") try: - User.objects.get(email__iexact=self.cleaned_data['email']) + User.objects.get(email__iexact=self.cleaned_data["email"]) except User.MultipleObjectsReturned: pass except User.DoesNotExist: - raise forms.ValidationError('No user has that email address.') + raise forms.ValidationError("No user has that email address.") + + return self.cleaned_data["email"] - return self.cleaned_data['email'] class ForgotPasswordReturnForm(forms.Form): - password = forms.CharField(widget=forms.PasswordInput(), - label="Your new password", - required=False) + password = forms.CharField(widget=forms.PasswordInput(), label="Your new password", required=False) + class AccountSettingsForm(forms.Form): use_required_attribute = False - username = forms.RegexField(regex=r'^\w+$', - max_length=30, - widget=forms.TextInput(attrs={'class': 'NB-input'}), - label='username', - required=False, - error_messages={ - 'invalid': "Your username may only contain letters and numbers." - }) - email = forms.EmailField(widget=forms.TextInput(attrs={'maxlength': 75, 'class': 'NB-input'}), - label='email address', - required=True, - error_messages={'required': 'Please enter an email.'}) - new_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'NB-input'}), - label='password', - required=False) - # error_messages={'required': 'Please enter a password.'}) - old_password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'NB-input'}), - label='password', - required=False) - custom_js = forms.CharField(widget=forms.TextInput(attrs={'class': 'NB-input'}), - label='custom_js', - required=False) - custom_css = forms.CharField(widget=forms.TextInput(attrs={'class': 'NB-input'}), - label='custom_css', - required=False) - + username = forms.RegexField( + regex=r"^\w+$", + max_length=30, + widget=forms.TextInput(attrs={"class": "NB-input"}), + label="username", + required=False, + error_messages={"invalid": "Your username may only contain letters and numbers."}, + ) + email = forms.EmailField( + widget=forms.TextInput(attrs={"maxlength": 75, "class": "NB-input"}), + label="email address", + required=True, + error_messages={"required": "Please enter an email."}, + ) + new_password = forms.CharField( + widget=forms.PasswordInput(attrs={"class": "NB-input"}), label="password", required=False + ) + # error_messages={'required': 'Please enter a password.'}) + old_password = forms.CharField( + widget=forms.PasswordInput(attrs={"class": "NB-input"}), label="password", required=False + ) + custom_js = forms.CharField( + widget=forms.TextInput(attrs={"class": "NB-input"}), label="custom_js", required=False + ) + custom_css = forms.CharField( + widget=forms.TextInput(attrs={"class": "NB-input"}), label="custom_css", required=False + ) + def __init__(self, user, *args, **kwargs): self.user = user super(AccountSettingsForm, self).__init__(*args, **kwargs) - + def clean_username(self): - username = self.cleaned_data['username'] + username = self.cleaned_data["username"] return username def clean_password(self): - if not self.cleaned_data['password']: + if not self.cleaned_data["password"]: return "" - return self.cleaned_data['password'] - + return self.cleaned_data["password"] + def clean_email(self): - return self.cleaned_data['email'] - + return self.cleaned_data["email"] + def clean(self): - username = self.cleaned_data.get('username', '') - new_password = self.cleaned_data.get('new_password', '') - old_password = self.cleaned_data.get('old_password', '') - email = self.cleaned_data.get('email', None) - + username = self.cleaned_data.get("username", "") + new_password = self.cleaned_data.get("new_password", "") + old_password = self.cleaned_data.get("old_password", "") + email = self.cleaned_data.get("email", None) + if username and self.user.username != username: try: User.objects.get(username__iexact=username) @@ -150,26 +155,28 @@ def clean(self): pass else: raise forms.ValidationError("This username is already taken. Try something different.") - + if self.user.email != email: if email and User.objects.filter(email__iexact=email).count(): - raise forms.ValidationError("This email is already being used by another account. Try something different.") - + raise forms.ValidationError( + "This email is already being used by another account. Try something different." + ) + if old_password or new_password: code = change_password(self.user, old_password, new_password, only_check=True) if code <= 0: - raise forms.ValidationError("Your old password is incorrect.") + raise forms.ValidationError("Your old password is incorrect.") return self.cleaned_data - + def save(self, profile_callback=None): - username = self.cleaned_data['username'] - new_password = self.cleaned_data.get('new_password', None) - old_password = self.cleaned_data.get('old_password', None) - email = self.cleaned_data.get('email', None) - custom_css = self.cleaned_data.get('custom_css', None) - custom_js = self.cleaned_data.get('custom_js', None) - + username = self.cleaned_data["username"] + new_password = self.cleaned_data.get("new_password", None) + old_password = self.cleaned_data.get("old_password", None) + email = self.cleaned_data.get("email", None) + custom_css = self.cleaned_data.get("custom_css", None) + custom_js = self.cleaned_data.get("custom_js", None) + if username and self.user.username != username: change_password(self.user, self.user.username, username) self.user.username = username @@ -178,28 +185,26 @@ def save(self, profile_callback=None): social_profile.username = username social_profile.save() - self.user.profile.update_email(email) - + if old_password or new_password: change_password(self.user, old_password, new_password) - + MCustomStyling.save_user(self.user.pk, custom_css, custom_js) - + + class RedeemCodeForm(forms.Form): use_required_attribute = False - gift_code = forms.CharField(widget=forms.TextInput(), - label="Gift code", - required=True) - + gift_code = forms.CharField(widget=forms.TextInput(), label="Gift code", required=True) + def clean_gift_code(self): - gift_code = self.cleaned_data['gift_code'] - - gift_code = re.sub(r'[^a-zA-Z0-9]', '', gift_code).lower() + gift_code = self.cleaned_data["gift_code"] + + gift_code = re.sub(r"[^a-zA-Z0-9]", "", gift_code).lower() if len(gift_code) != 12: - raise forms.ValidationError('Your gift code should be 12 characters long.') - + raise forms.ValidationError("Your gift code should be 12 characters long.") + newsblur_gift_code = MGiftCode.objects.filter(gift_code__iexact=gift_code) if newsblur_gift_code: @@ -208,15 +213,17 @@ def clean_gift_code(self): return newsblur_gift_code.gift_code else: # Thinkup / Good Web Bundle - req = requests.get('https://www.thinkup.com/join/api/bundle/', params={'code': gift_code}) + req = requests.get("https://www.thinkup.com/join/api/bundle/", params={"code": gift_code}) response = req.json() - - is_valid = response.get('is_valid', None) + + is_valid = response.get("is_valid", None) if is_valid: return gift_code elif is_valid == False: - raise forms.ValidationError('Your gift code is invalid. Check it for errors.') - elif response.get('error', None): - raise forms.ValidationError('Your gift code is invalid, says the server: %s' % response['error']) - + raise forms.ValidationError("Your gift code is invalid. Check it for errors.") + elif response.get("error", None): + raise forms.ValidationError( + "Your gift code is invalid, says the server: %s" % response["error"] + ) + return gift_code diff --git a/apps/profile/management/commands/check_db.py b/apps/profile/management/commands/check_db.py index 941f5b57e5..7d2496a1d2 100644 --- a/apps/profile/management/commands/check_db.py +++ b/apps/profile/management/commands/check_db.py @@ -1,12 +1,13 @@ import time + from django.core.management.base import BaseCommand from django.db import connections from django.db.utils import OperationalError -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): - db_conn = connections['default'] + db_conn = connections["default"] connected = False while not connected: try: diff --git a/apps/profile/management/commands/fp.py b/apps/profile/management/commands/fp.py index 21b055f564..7dc22f990a 100644 --- a/apps/profile/management/commands/fp.py +++ b/apps/profile/management/commands/fp.py @@ -1,15 +1,15 @@ -from django.core.management.base import BaseCommand from django.contrib.auth.models import User +from django.core.management.base import BaseCommand -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-u", "--username", dest="username", nargs=1, help="Specify user id or username") parser.add_argument("-e", "--email", dest="email", nargs=1, help="Specify email if it doesn't exist") def handle(self, *args, **options): - username = options.get('username') - email = options.get('email') + username = options.get("username") + email = options.get("email") user = None if username: try: @@ -30,11 +30,9 @@ def handle(self, *args, **options): user = users[0] except User.DoesNotExist: print(" ---> No email found at: %s" % email) - + if user: email = options.get("email") or user.email user.profile.send_forgot_password_email(email) else: print(" ---> No user/email found at: %s/%s" % (username, email)) - - \ No newline at end of file diff --git a/apps/profile/management/commands/reimport_paypal_history.py b/apps/profile/management/commands/reimport_paypal_history.py index 10c4afd6bd..eb853c4f72 100644 --- a/apps/profile/management/commands/reimport_paypal_history.py +++ b/apps/profile/management/commands/reimport_paypal_history.py @@ -1,31 +1,54 @@ -import stripe, datetime, time -from django.conf import settings +import datetime +import time -from django.core.management.base import BaseCommand +import stripe +from django.conf import settings from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +from apps.profile.models import PaymentHistory, Profile from utils import log as logging -from apps.profile.models import Profile, PaymentHistory -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument("-d", "--days", dest="days", nargs=1, type=int, default=365, help="Number of days to go back") - parser.add_argument("-o", "--offset", dest="offset", nargs=1, type=int, default=0, help="Offset customer (in date DESC)") - parser.add_argument("-f", "--force", dest="force", nargs=1, type=bool, default=False, help="Force reimport for every user") - + parser.add_argument( + "-d", "--days", dest="days", nargs=1, type=int, default=365, help="Number of days to go back" + ) + parser.add_argument( + "-o", + "--offset", + dest="offset", + nargs=1, + type=int, + default=0, + help="Offset customer (in date DESC)", + ) + parser.add_argument( + "-f", + "--force", + dest="force", + nargs=1, + type=bool, + default=False, + help="Force reimport for every user", + ) + def handle(self, *args, **options): stripe.api_key = settings.STRIPE_SECRET - week = datetime.datetime.now() - datetime.timedelta(days=int(options.get('days'))) + week = datetime.datetime.now() - datetime.timedelta(days=int(options.get("days"))) failed = [] limit = 100 - offset = options.get('offset') - + offset = options.get("offset") + while True: logging.debug(" ---> At %s" % offset) - user_ids = PaymentHistory.objects.filter(payment_provider='paypal', - payment_date__gte=week).values('user_id').distinct()[offset:offset+limit] - user_ids = [u['user_id'] for u in user_ids] + user_ids = ( + PaymentHistory.objects.filter(payment_provider="paypal", payment_date__gte=week) + .values("user_id") + .distinct()[offset : offset + limit] + ) + user_ids = [u["user_id"] for u in user_ids] if not len(user_ids): logging.debug("At %s, finished" % offset) break @@ -36,7 +59,7 @@ def handle(self, *args, **options): except User.DoesNotExist: logging.debug(" ***> Couldn't find paypal user_id=%s" % user_id) failed.append(user_id) - + if not user.profile.is_premium: user.profile.activate_premium() elif user.payments.all().count() != 1: @@ -45,10 +68,9 @@ def handle(self, *args, **options): user.profile.setup_premium_history() elif user.profile.premium_expire > datetime.datetime.now() + datetime.timedelta(days=365): user.profile.setup_premium_history() - elif options.get('force'): + elif options.get("force"): user.profile.setup_premium_history() else: logging.debug(" ---> %s is fine" % user.username) return failed - diff --git a/apps/profile/management/commands/reimport_stripe_history.py b/apps/profile/management/commands/reimport_stripe_history.py index fe00e10bdb..5bb728c300 100644 --- a/apps/profile/management/commands/reimport_stripe_history.py +++ b/apps/profile/management/commands/reimport_stripe_history.py @@ -1,21 +1,36 @@ -import stripe, datetime, time -from django.conf import settings +import datetime +import time +import stripe +from django.conf import settings from django.core.management.base import BaseCommand -from utils import log as logging from apps.profile.models import Profile +from utils import log as logging + class Command(BaseCommand): - def add_arguments(self, parser) - parser.add_argument("-d", "--days", dest="days", nargs=1, type='int', default=365, help="Number of days to go back") - parser.add_argument("-l", "--limit", dest="limit", nargs=1, type='int', default=100, help="Charges per batch") - parser.add_argument("-s", "--start", dest="start", nargs=1, type='string', default=None, help="Offset customer_id (starting_after)") + def add_arguments(self, parser): + parser.add_argument( + "-d", "--days", dest="days", nargs=1, type="int", default=365, help="Number of days to go back" + ) + parser.add_argument( + "-l", "--limit", dest="limit", nargs=1, type="int", default=100, help="Charges per batch" + ) + parser.add_argument( + "-s", + "--start", + dest="start", + nargs=1, + type="string", + default=None, + help="Offset customer_id (starting_after)", + ) def handle(self, *args, **options): - limit = options.get('limit') - days = int(options.get('days')) - starting_after = options.get('start') - - Profile.reimport_stripe_history(limit, days, starting_after) \ No newline at end of file + limit = options.get("limit") + days = int(options.get("days")) + starting_after = options.get("start") + + Profile.reimport_stripe_history(limit, days, starting_after) diff --git a/apps/profile/management/commands/remove_last_user.py b/apps/profile/management/commands/remove_last_user.py index 3e6a07883f..f61b9b188e 100644 --- a/apps/profile/management/commands/remove_last_user.py +++ b/apps/profile/management/commands/remove_last_user.py @@ -5,11 +5,12 @@ from django.core.management.base import BaseCommand from apps.profile.models import Profile -class Command(BaseCommand): + +class Command(BaseCommand): def handle(self, *args, **options): user = User.objects.last() - profile = Profile.objects.get(user=user) + profile = Profile.objects.get(user=user) profile.delete() user.delete() - print("User and profile for user {0} deleted".format(user)) \ No newline at end of file + print("User and profile for user {0} deleted".format(user)) diff --git a/apps/profile/middleware.py b/apps/profile/middleware.py index 4a0d23e16a..6fc33de1b1 100644 --- a/apps/profile/middleware.py +++ b/apps/profile/middleware.py @@ -1,15 +1,17 @@ import datetime -import re import random +import re import time + import redis -from utils import log as logging -from django.http import HttpResponse from django.conf import settings from django.db import connection -from django.template import Template, Context +from django.http import HttpResponse +from django.template import Context, Template + from apps.statistics.rstats import round_time from utils import json_functions as json +from utils import log as logging class LastSeenMiddleware(object): @@ -19,16 +21,16 @@ def __init__(self, get_response=None): 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') + 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 hasattr(request, "user") 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'] + 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) @@ -50,11 +52,11 @@ def process_response(self, request, response): def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response @@ -65,31 +67,31 @@ def __init__(self, get_response=None): self.get_response = get_response def process_request(self, request): - setattr(request, 'activated_segments', []) + setattr(request, "activated_segments", []) if ( - # request.path.startswith('/reader/feed') or - request.path.startswith('/reader/feed/') + # request.path.startswith('/reader/feed') or + request.path.startswith("/reader/feed/") ) and random.random() < 0.05: - request.activated_segments.append('db_profiler') + request.activated_segments.append("db_profiler") connection.use_debug_cursor = True - setattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG) + setattr(settings, "ORIGINAL_DEBUG", settings.DEBUG) settings.DEBUG = True def process_celery(self): - setattr(self, 'activated_segments', []) + setattr(self, "activated_segments", []) if random.random() < 0.01 or settings.DEBUG_QUERIES: - self.activated_segments.append('db_profiler') + self.activated_segments.append("db_profiler") connection.use_debug_cursor = True - setattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG) + setattr(settings, "ORIGINAL_DEBUG", settings.DEBUG) settings.DEBUG = True return self def process_exception(self, request, exception): - if hasattr(request, 'sql_times_elapsed'): + if hasattr(request, "sql_times_elapsed"): self._save_times(request.sql_times_elapsed) def process_response(self, request, response): - if hasattr(request, 'sql_times_elapsed'): + if hasattr(request, "sql_times_elapsed"): # middleware = SQLLogToConsoleMiddleware() # middleware.process_celery(self) # logging.debug(" ---> ~FGProfiling~FB app: %s" % request.sql_times_elapsed) @@ -99,16 +101,16 @@ def process_response(self, request, response): def process_celery_finished(self): middleware = SQLLogToConsoleMiddleware() middleware.process_celery(self) - if hasattr(self, 'sql_times_elapsed'): + if hasattr(self, "sql_times_elapsed"): logging.debug(" ---> ~FGProfiling~FB task: %s" % self.sql_times_elapsed) - self._save_times(self.sql_times_elapsed, 'task_') + 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'): + if hasattr(self, "sql_times_elapsed"): logging.debug(" ---> ~FGProfiling~FB app: %s" % self.sql_times_elapsed) - self._save_times(self.sql_times_elapsed, 'app_') + self._save_times(self.sql_times_elapsed, "app_") def _save_times(self, db_times, prefix=""): if not db_times: @@ -118,7 +120,7 @@ def _save_times(self, db_times, prefix=""): pipe = r.pipeline() minute = round_time(round_to=60) for db, duration in list(db_times.items()): - key = "DB:%s%s:%s" % (prefix, db, minute.strftime('%s')) + key = "DB:%s%s:%s" % (prefix, db, minute.strftime("%s")) pipe.incr("%s:c" % key) pipe.expireat("%s:c" % key, (minute + datetime.timedelta(days=2)).strftime("%s")) if duration: @@ -128,11 +130,11 @@ def _save_times(self, db_times, prefix=""): def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response @@ -144,7 +146,7 @@ def __init__(self, get_response=None): def activated(self, request): return settings.DEBUG_QUERIES or ( - hasattr(request, 'activated_segments') and 'db_profiler' in request.activated_segments + hasattr(request, "activated_segments") and "db_profiler" in request.activated_segments ) def process_response(self, request, response): @@ -152,38 +154,39 @@ def process_response(self, request, response): return response if connection.queries: queries = connection.queries - if getattr(connection, 'queriesx', False): + if getattr(connection, "queriesx", False): queries.extend(connection.queriesx) connection.queriesx = [] - time_elapsed = sum([float(q['time']) for q in connection.queries]) + time_elapsed = sum([float(q["time"]) for q in connection.queries]) for query in queries: - sql_time = float(query['time']) - query['color'] = '~FC' if sql_time < 0.015 else '~FK~SB' if sql_time < 0.05 else '~FR~SB' - if query.get('mongo'): - query['sql'] = "~FM%s %s: %s" % (query['mongo']['op'], query['mongo']['collection'], query['mongo']['query']) - elif query.get('redis_user'): - query['sql'] = "~FC%s" % (query['redis_user']['query']) - elif query.get('redis_story'): - query['sql'] = "~FC%s" % (query['redis_story']['query']) - elif query.get('redis_session'): - query['sql'] = "~FC%s" % (query['redis_session']['query']) - elif query.get('redis_pubsub'): - query['sql'] = "~FC%s" % (query['redis_pubsub']['query']) - elif query.get('db_redis'): - query['sql'] = "~FC%s" % (query['db_redis']['query']) - elif 'sql' not in query: + sql_time = float(query["time"]) + query["color"] = "~FC" if sql_time < 0.015 else "~FK~SB" if sql_time < 0.05 else "~FR~SB" + if query.get("mongo"): + query["sql"] = "~FM%s %s: %s" % ( + query["mongo"]["op"], + query["mongo"]["collection"], + query["mongo"]["query"], + ) + elif query.get("redis_user"): + query["sql"] = "~FC%s" % (query["redis_user"]["query"]) + elif query.get("redis_story"): + query["sql"] = "~FC%s" % (query["redis_story"]["query"]) + elif query.get("redis_session"): + query["sql"] = "~FC%s" % (query["redis_session"]["query"]) + elif query.get("redis_pubsub"): + query["sql"] = "~FC%s" % (query["redis_pubsub"]["query"]) + elif query.get("db_redis"): + query["sql"] = "~FC%s" % (query["db_redis"]["query"]) + elif "sql" not in query: logging.debug(" ***> Query log missing: %s" % query) else: - query['sql'] = re.sub(r'SELECT (.*?) FROM', 'SELECT * FROM', query['sql']) - query['sql'] = re.sub(r'SELECT', '~FYSELECT', query['sql']) - 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_QUERIES - and not getattr(settings, 'DEBUG_QUERIES_SUMMARY_ONLY', False) - ): + query["sql"] = re.sub(r"SELECT (.*?) FROM", "SELECT * FROM", query["sql"]) + query["sql"] = re.sub(r"SELECT", "~FYSELECT", query["sql"]) + 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_QUERIES and not getattr(settings, "DEBUG_QUERIES_SUMMARY_ONLY", False): t = Template( "{% for sql in sqllog %}{% if not forloop.first %} {% endif %}[{{forloop.counter}}] {{sql.color}}{{sql.time}}~SN~FW: {{sql.sql|safe}}{% if not forloop.last %}\n{% endif %}{% endfor %}" ) @@ -191,51 +194,51 @@ def process_response(self, request, response): t.render( Context( { - 'sqllog': queries, - 'count': len(queries), - 'time': time_elapsed, + "sqllog": queries, + "count": len(queries), + "time": time_elapsed, } ) ) ) times_elapsed = { - 'sql': sum( + "sql": sum( [ - float(q['time']) + 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') + 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')]), - 'redis_session': sum([float(q['time']) for q in queries if q.get('redis_session')]), - 'redis_pubsub': sum([float(q['time']) for q in queries if 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")]), + "redis_session": sum([float(q["time"]) for q in queries if q.get("redis_session")]), + "redis_pubsub": sum([float(q["time"]) for q in queries if q.get("redis_pubsub")]), } - setattr(request, 'sql_times_elapsed', times_elapsed) + setattr(request, "sql_times_elapsed", times_elapsed) else: print(" ***> No queries") - if not getattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG): + if not getattr(settings, "ORIGINAL_DEBUG", settings.DEBUG): settings.DEBUG = False return response def process_celery(self, profiler): self.process_response(profiler, None) - if not getattr(settings, 'ORIGINAL_DEBUG', settings.DEBUG): + if not getattr(settings, "ORIGINAL_DEBUG", settings.DEBUG): settings.DEBUG = False def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response @@ -246,7 +249,7 @@ def __call__(self, request): ("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.\"", + '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."), ( @@ -261,7 +264,7 @@ def __call__(self, request): ), ( "Comic Book Guy", - "Your questions have become more redundant and annoying than the last three \"Highlander\" movies.", + '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."), ( @@ -282,11 +285,11 @@ def __call__(self, request): ), ( "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.\"", + '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.", + '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."), @@ -307,7 +310,7 @@ def __call__(self, request): ("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?", + '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", @@ -325,8 +328,8 @@ def __call__(self, request): "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.\""), + ("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."), ( @@ -350,7 +353,7 @@ def __call__(self, request): ), ( "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!\"", + '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."), ( @@ -425,7 +428,7 @@ def __call__(self, request): ("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.", + '"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!"), @@ -487,18 +490,18 @@ def __init__(self, get_response=None): def process_response(self, request, response): quote = random.choice(SIMPSONS_QUOTES) - source = quote[0].replace(' ', '-') + source = quote[0].replace(" ", "-") response["X-%s" % source] = quote[1] return response def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response @@ -515,11 +518,11 @@ def process_response(self, request, response): def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response @@ -530,7 +533,7 @@ def __init__(self, get_response=None): self.get_response = get_response def process_request(self, request): - setattr(request, 'start_time', time.time()) + setattr(request, "start_time", time.time()) def __call__(self, request): response = self.process_request(request) @@ -541,8 +544,8 @@ def __call__(self, request): BANNED_USER_AGENTS = ( - 'feed reader-background', - 'missing', + "feed reader-background", + "missing", ) BANNED_USERNAMES = () @@ -553,46 +556,46 @@ def __init__(self, get_response=None): self.get_response = get_response def process_request(self, request): - user_agent = request.environ.get('HTTP_USER_AGENT', 'missing').lower() + user_agent = request.environ.get("HTTP_USER_AGENT", "missing").lower() - if 'profile' in request.path: + if "profile" in request.path: return - if 'haproxy' in request.path: + if "haproxy" in request.path: return - if 'dbcheck' in request.path: + if "dbcheck" in request.path: return - if 'account' in request.path: + if "account" in request.path: return - if 'push' in request.path: + if "push" in request.path: return - if getattr(settings, 'TEST_DEBUG'): + 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} + 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') + 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} + 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') + return HttpResponse(json.encode(data), status=403, content_type="text/json") def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response diff --git a/apps/profile/migrations/0001_initial.py b/apps/profile/migrations/0001_initial.py index 82d4a4fe7b..6e50059d8b 100644 --- a/apps/profile/migrations/0001_initial.py +++ b/apps/profile/migrations/0001_initial.py @@ -1,14 +1,15 @@ # Generated by Django 2.0 on 2020-06-16 06:52 import datetime + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + import vendor.timezones.fields class Migration(migrations.Migration): - initial = True dependencies = [ @@ -17,51 +18,528 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='PaymentHistory', + name="PaymentHistory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('payment_date', models.DateTimeField()), - ('payment_amount', models.IntegerField()), - ('payment_provider', models.CharField(max_length=20)), - ('payment_identifier', models.CharField(max_length=100, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("payment_date", models.DateTimeField()), + ("payment_amount", models.IntegerField()), + ("payment_provider", models.CharField(max_length=20)), + ("payment_identifier", models.CharField(max_length=100, null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="payments", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'ordering': ['-payment_date'], + "ordering": ["-payment_date"], }, ), migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_premium', models.BooleanField(default=False)), - ('premium_expire', models.DateTimeField(blank=True, null=True)), - ('send_emails', models.BooleanField(default=True)), - ('preferences', models.TextField(default='{}')), - ('view_settings', models.TextField(default='{}')), - ('collapsed_folders', models.TextField(default='[]')), - ('feed_pane_size', models.IntegerField(default=242)), - ('tutorial_finished', models.BooleanField(default=False)), - ('hide_getting_started', models.NullBooleanField(default=False)), - ('has_setup_feeds', models.NullBooleanField(default=False)), - ('has_found_friends', models.NullBooleanField(default=False)), - ('has_trained_intelligence', models.NullBooleanField(default=False)), - ('last_seen_on', models.DateTimeField(default=datetime.datetime.now)), - ('last_seen_ip', models.CharField(blank=True, max_length=50, null=True)), - ('dashboard_date', models.DateTimeField(default=datetime.datetime.now)), - ('timezone', vendor.timezones.fields.TimeZoneField(choices=[('Africa/Abidjan', '(GMT+0000) Africa/Abidjan'), ('Africa/Accra', '(GMT+0000) Africa/Accra'), ('Africa/Addis_Ababa', '(GMT+0300) Africa/Addis_Ababa'), ('Africa/Algiers', '(GMT+0100) Africa/Algiers'), ('Africa/Asmara', '(GMT+0300) Africa/Asmara'), ('Africa/Bamako', '(GMT+0000) Africa/Bamako'), ('Africa/Bangui', '(GMT+0100) Africa/Bangui'), ('Africa/Banjul', '(GMT+0000) Africa/Banjul'), ('Africa/Bissau', '(GMT+0000) Africa/Bissau'), ('Africa/Blantyre', '(GMT+0200) Africa/Blantyre'), ('Africa/Brazzaville', '(GMT+0100) Africa/Brazzaville'), ('Africa/Bujumbura', '(GMT+0200) Africa/Bujumbura'), ('Africa/Cairo', '(GMT+0200) Africa/Cairo'), ('Africa/Casablanca', '(GMT+0100) Africa/Casablanca'), ('Africa/Ceuta', '(GMT+0200) Africa/Ceuta'), ('Africa/Conakry', '(GMT+0000) Africa/Conakry'), ('Africa/Dakar', '(GMT+0000) Africa/Dakar'), ('Africa/Dar_es_Salaam', '(GMT+0300) Africa/Dar_es_Salaam'), ('Africa/Djibouti', '(GMT+0300) Africa/Djibouti'), ('Africa/Douala', '(GMT+0100) Africa/Douala'), ('Africa/El_Aaiun', '(GMT+0100) Africa/El_Aaiun'), ('Africa/Freetown', '(GMT+0000) Africa/Freetown'), ('Africa/Gaborone', '(GMT+0200) Africa/Gaborone'), ('Africa/Harare', '(GMT+0200) Africa/Harare'), ('Africa/Johannesburg', '(GMT+0200) Africa/Johannesburg'), ('Africa/Juba', '(GMT+0300) Africa/Juba'), ('Africa/Kampala', '(GMT+0300) Africa/Kampala'), ('Africa/Khartoum', '(GMT+0200) Africa/Khartoum'), ('Africa/Kigali', '(GMT+0200) Africa/Kigali'), ('Africa/Kinshasa', '(GMT+0100) Africa/Kinshasa'), ('Africa/Lagos', '(GMT+0100) Africa/Lagos'), ('Africa/Libreville', '(GMT+0100) Africa/Libreville'), ('Africa/Lome', '(GMT+0000) Africa/Lome'), ('Africa/Luanda', '(GMT+0100) Africa/Luanda'), ('Africa/Lubumbashi', '(GMT+0200) Africa/Lubumbashi'), ('Africa/Lusaka', '(GMT+0200) Africa/Lusaka'), ('Africa/Malabo', '(GMT+0100) Africa/Malabo'), ('Africa/Maputo', '(GMT+0200) Africa/Maputo'), ('Africa/Maseru', '(GMT+0200) Africa/Maseru'), ('Africa/Mbabane', '(GMT+0200) Africa/Mbabane'), ('Africa/Mogadishu', '(GMT+0300) Africa/Mogadishu'), ('Africa/Monrovia', '(GMT+0000) Africa/Monrovia'), ('Africa/Nairobi', '(GMT+0300) Africa/Nairobi'), ('Africa/Ndjamena', '(GMT+0100) Africa/Ndjamena'), ('Africa/Niamey', '(GMT+0100) Africa/Niamey'), ('Africa/Nouakchott', '(GMT+0000) Africa/Nouakchott'), ('Africa/Ouagadougou', '(GMT+0000) Africa/Ouagadougou'), ('Africa/Porto-Novo', '(GMT+0100) Africa/Porto-Novo'), ('Africa/Sao_Tome', '(GMT+0100) Africa/Sao_Tome'), ('Africa/Tripoli', '(GMT+0200) Africa/Tripoli'), ('Africa/Tunis', '(GMT+0100) Africa/Tunis'), ('Africa/Windhoek', '(GMT+0200) Africa/Windhoek'), ('America/Adak', '(GMT-0900) America/Adak'), ('America/Anchorage', '(GMT-0800) America/Anchorage'), ('America/Anguilla', '(GMT-0400) America/Anguilla'), ('America/Antigua', '(GMT-0400) America/Antigua'), ('America/Araguaina', '(GMT-0300) America/Araguaina'), ('America/Argentina/Buenos_Aires', '(GMT-0300) America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', '(GMT-0300) America/Argentina/Catamarca'), ('America/Argentina/Cordoba', '(GMT-0300) America/Argentina/Cordoba'), ('America/Argentina/Jujuy', '(GMT-0300) America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', '(GMT-0300) America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', '(GMT-0300) America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', '(GMT-0300) America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', '(GMT-0300) America/Argentina/Salta'), ('America/Argentina/San_Juan', '(GMT-0300) America/Argentina/San_Juan'), ('America/Argentina/San_Luis', '(GMT-0300) America/Argentina/San_Luis'), ('America/Argentina/Tucuman', '(GMT-0300) America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', '(GMT-0300) America/Argentina/Ushuaia'), ('America/Aruba', '(GMT-0400) America/Aruba'), ('America/Asuncion', '(GMT-0400) America/Asuncion'), ('America/Atikokan', '(GMT-0500) America/Atikokan'), ('America/Bahia', '(GMT-0300) America/Bahia'), ('America/Bahia_Banderas', '(GMT-0500) America/Bahia_Banderas'), ('America/Barbados', '(GMT-0400) America/Barbados'), ('America/Belem', '(GMT-0300) America/Belem'), ('America/Belize', '(GMT-0600) America/Belize'), ('America/Blanc-Sablon', '(GMT-0400) America/Blanc-Sablon'), ('America/Boa_Vista', '(GMT-0400) America/Boa_Vista'), ('America/Bogota', '(GMT-0500) America/Bogota'), ('America/Boise', '(GMT-0600) America/Boise'), ('America/Cambridge_Bay', '(GMT-0600) America/Cambridge_Bay'), ('America/Campo_Grande', '(GMT-0400) America/Campo_Grande'), ('America/Cancun', '(GMT-0500) America/Cancun'), ('America/Caracas', '(GMT-0400) America/Caracas'), ('America/Cayenne', '(GMT-0300) America/Cayenne'), ('America/Cayman', '(GMT-0500) America/Cayman'), ('America/Chicago', '(GMT-0500) America/Chicago'), ('America/Chihuahua', '(GMT-0600) America/Chihuahua'), ('America/Costa_Rica', '(GMT-0600) America/Costa_Rica'), ('America/Creston', '(GMT-0700) America/Creston'), ('America/Cuiaba', '(GMT-0400) America/Cuiaba'), ('America/Curacao', '(GMT-0400) America/Curacao'), ('America/Danmarkshavn', '(GMT+0000) America/Danmarkshavn'), ('America/Dawson', '(GMT-0700) America/Dawson'), ('America/Dawson_Creek', '(GMT-0700) America/Dawson_Creek'), ('America/Denver', '(GMT-0600) America/Denver'), ('America/Detroit', '(GMT-0400) America/Detroit'), ('America/Dominica', '(GMT-0400) America/Dominica'), ('America/Edmonton', '(GMT-0600) America/Edmonton'), ('America/Eirunepe', '(GMT-0500) America/Eirunepe'), ('America/El_Salvador', '(GMT-0600) America/El_Salvador'), ('America/Fort_Nelson', '(GMT-0700) America/Fort_Nelson'), ('America/Fortaleza', '(GMT-0300) America/Fortaleza'), ('America/Glace_Bay', '(GMT-0300) America/Glace_Bay'), ('America/Godthab', '(GMT-0200) America/Godthab'), ('America/Goose_Bay', '(GMT-0300) America/Goose_Bay'), ('America/Grand_Turk', '(GMT-0400) America/Grand_Turk'), ('America/Grenada', '(GMT-0400) America/Grenada'), ('America/Guadeloupe', '(GMT-0400) America/Guadeloupe'), ('America/Guatemala', '(GMT-0600) America/Guatemala'), ('America/Guayaquil', '(GMT-0500) America/Guayaquil'), ('America/Guyana', '(GMT-0400) America/Guyana'), ('America/Halifax', '(GMT-0300) America/Halifax'), ('America/Havana', '(GMT-0400) America/Havana'), ('America/Hermosillo', '(GMT-0700) America/Hermosillo'), ('America/Indiana/Indianapolis', '(GMT-0400) America/Indiana/Indianapolis'), ('America/Indiana/Knox', '(GMT-0500) America/Indiana/Knox'), ('America/Indiana/Marengo', '(GMT-0400) America/Indiana/Marengo'), ('America/Indiana/Petersburg', '(GMT-0400) America/Indiana/Petersburg'), ('America/Indiana/Tell_City', '(GMT-0500) America/Indiana/Tell_City'), ('America/Indiana/Vevay', '(GMT-0400) America/Indiana/Vevay'), ('America/Indiana/Vincennes', '(GMT-0400) America/Indiana/Vincennes'), ('America/Indiana/Winamac', '(GMT-0400) America/Indiana/Winamac'), ('America/Inuvik', '(GMT-0600) America/Inuvik'), ('America/Iqaluit', '(GMT-0400) America/Iqaluit'), ('America/Jamaica', '(GMT-0500) America/Jamaica'), ('America/Juneau', '(GMT-0800) America/Juneau'), ('America/Kentucky/Louisville', '(GMT-0400) America/Kentucky/Louisville'), ('America/Kentucky/Monticello', '(GMT-0400) America/Kentucky/Monticello'), ('America/Kralendijk', '(GMT-0400) America/Kralendijk'), ('America/La_Paz', '(GMT-0400) America/La_Paz'), ('America/Lima', '(GMT-0500) America/Lima'), ('America/Los_Angeles', '(GMT-0700) America/Los_Angeles'), ('America/Lower_Princes', '(GMT-0400) America/Lower_Princes'), ('America/Maceio', '(GMT-0300) America/Maceio'), ('America/Managua', '(GMT-0600) America/Managua'), ('America/Manaus', '(GMT-0400) America/Manaus'), ('America/Marigot', '(GMT-0400) America/Marigot'), ('America/Martinique', '(GMT-0400) America/Martinique'), ('America/Matamoros', '(GMT-0500) America/Matamoros'), ('America/Mazatlan', '(GMT-0600) America/Mazatlan'), ('America/Menominee', '(GMT-0500) America/Menominee'), ('America/Merida', '(GMT-0500) America/Merida'), ('America/Metlakatla', '(GMT-0800) America/Metlakatla'), ('America/Mexico_City', '(GMT-0500) America/Mexico_City'), ('America/Miquelon', '(GMT-0200) America/Miquelon'), ('America/Moncton', '(GMT-0300) America/Moncton'), ('America/Monterrey', '(GMT-0500) America/Monterrey'), ('America/Montevideo', '(GMT-0300) America/Montevideo'), ('America/Montserrat', '(GMT-0400) America/Montserrat'), ('America/Nassau', '(GMT-0400) America/Nassau'), ('America/New_York', '(GMT-0400) America/New_York'), ('America/Nipigon', '(GMT-0400) America/Nipigon'), ('America/Nome', '(GMT-0800) America/Nome'), ('America/Noronha', '(GMT-0200) America/Noronha'), ('America/North_Dakota/Beulah', '(GMT-0500) America/North_Dakota/Beulah'), ('America/North_Dakota/Center', '(GMT-0500) America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', '(GMT-0500) America/North_Dakota/New_Salem'), ('America/Ojinaga', '(GMT-0600) America/Ojinaga'), ('America/Panama', '(GMT-0500) America/Panama'), ('America/Pangnirtung', '(GMT-0400) America/Pangnirtung'), ('America/Paramaribo', '(GMT-0300) America/Paramaribo'), ('America/Phoenix', '(GMT-0700) America/Phoenix'), ('America/Port-au-Prince', '(GMT-0400) America/Port-au-Prince'), ('America/Port_of_Spain', '(GMT-0400) America/Port_of_Spain'), ('America/Porto_Velho', '(GMT-0400) America/Porto_Velho'), ('America/Puerto_Rico', '(GMT-0400) America/Puerto_Rico'), ('America/Punta_Arenas', '(GMT-0300) America/Punta_Arenas'), ('America/Rainy_River', '(GMT-0500) America/Rainy_River'), ('America/Rankin_Inlet', '(GMT-0500) America/Rankin_Inlet'), ('America/Recife', '(GMT-0300) America/Recife'), ('America/Regina', '(GMT-0600) America/Regina'), ('America/Resolute', '(GMT-0500) America/Resolute'), ('America/Rio_Branco', '(GMT-0500) America/Rio_Branco'), ('America/Santarem', '(GMT-0300) America/Santarem'), ('America/Santiago', '(GMT-0400) America/Santiago'), ('America/Santo_Domingo', '(GMT-0400) America/Santo_Domingo'), ('America/Sao_Paulo', '(GMT-0300) America/Sao_Paulo'), ('America/Scoresbysund', '(GMT+0000) America/Scoresbysund'), ('America/Sitka', '(GMT-0800) America/Sitka'), ('America/St_Barthelemy', '(GMT-0400) America/St_Barthelemy'), ('America/St_Johns', '(GMT-0230) America/St_Johns'), ('America/St_Kitts', '(GMT-0400) America/St_Kitts'), ('America/St_Lucia', '(GMT-0400) America/St_Lucia'), ('America/St_Thomas', '(GMT-0400) America/St_Thomas'), ('America/St_Vincent', '(GMT-0400) America/St_Vincent'), ('America/Swift_Current', '(GMT-0600) America/Swift_Current'), ('America/Tegucigalpa', '(GMT-0600) America/Tegucigalpa'), ('America/Thule', '(GMT-0300) America/Thule'), ('America/Thunder_Bay', '(GMT-0400) America/Thunder_Bay'), ('America/Tijuana', '(GMT-0700) America/Tijuana'), ('America/Toronto', '(GMT-0400) America/Toronto'), ('America/Tortola', '(GMT-0400) America/Tortola'), ('America/Vancouver', '(GMT-0700) America/Vancouver'), ('America/Whitehorse', '(GMT-0700) America/Whitehorse'), ('America/Winnipeg', '(GMT-0500) America/Winnipeg'), ('America/Yakutat', '(GMT-0800) America/Yakutat'), ('America/Yellowknife', '(GMT-0600) America/Yellowknife'), ('Antarctica/Casey', '(GMT+1100) Antarctica/Casey'), ('Antarctica/Davis', '(GMT+0700) Antarctica/Davis'), ('Antarctica/DumontDUrville', '(GMT+1000) Antarctica/DumontDUrville'), ('Antarctica/Macquarie', '(GMT+1100) Antarctica/Macquarie'), ('Antarctica/Mawson', '(GMT+0500) Antarctica/Mawson'), ('Antarctica/McMurdo', '(GMT+1200) Antarctica/McMurdo'), ('Antarctica/Palmer', '(GMT-0300) Antarctica/Palmer'), ('Antarctica/Rothera', '(GMT-0300) Antarctica/Rothera'), ('Antarctica/Syowa', '(GMT+0300) Antarctica/Syowa'), ('Antarctica/Troll', '(GMT+0200) Antarctica/Troll'), ('Antarctica/Vostok', '(GMT+0600) Antarctica/Vostok'), ('Arctic/Longyearbyen', '(GMT+0200) Arctic/Longyearbyen'), ('Asia/Aden', '(GMT+0300) Asia/Aden'), ('Asia/Almaty', '(GMT+0600) Asia/Almaty'), ('Asia/Amman', '(GMT+0300) Asia/Amman'), ('Asia/Anadyr', '(GMT+1200) Asia/Anadyr'), ('Asia/Aqtau', '(GMT+0500) Asia/Aqtau'), ('Asia/Aqtobe', '(GMT+0500) Asia/Aqtobe'), ('Asia/Ashgabat', '(GMT+0500) Asia/Ashgabat'), ('Asia/Atyrau', '(GMT+0500) Asia/Atyrau'), ('Asia/Baghdad', '(GMT+0300) Asia/Baghdad'), ('Asia/Bahrain', '(GMT+0300) Asia/Bahrain'), ('Asia/Baku', '(GMT+0400) Asia/Baku'), ('Asia/Bangkok', '(GMT+0700) Asia/Bangkok'), ('Asia/Barnaul', '(GMT+0700) Asia/Barnaul'), ('Asia/Beirut', '(GMT+0300) Asia/Beirut'), ('Asia/Bishkek', '(GMT+0600) Asia/Bishkek'), ('Asia/Brunei', '(GMT+0800) Asia/Brunei'), ('Asia/Chita', '(GMT+0900) Asia/Chita'), ('Asia/Choibalsan', '(GMT+0800) Asia/Choibalsan'), ('Asia/Colombo', '(GMT+0530) Asia/Colombo'), ('Asia/Damascus', '(GMT+0300) Asia/Damascus'), ('Asia/Dhaka', '(GMT+0600) Asia/Dhaka'), ('Asia/Dili', '(GMT+0900) Asia/Dili'), ('Asia/Dubai', '(GMT+0400) Asia/Dubai'), ('Asia/Dushanbe', '(GMT+0500) Asia/Dushanbe'), ('Asia/Famagusta', '(GMT+0300) Asia/Famagusta'), ('Asia/Gaza', '(GMT+0300) Asia/Gaza'), ('Asia/Hebron', '(GMT+0300) Asia/Hebron'), ('Asia/Ho_Chi_Minh', '(GMT+0700) Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', '(GMT+0800) Asia/Hong_Kong'), ('Asia/Hovd', '(GMT+0700) Asia/Hovd'), ('Asia/Irkutsk', '(GMT+0800) Asia/Irkutsk'), ('Asia/Jakarta', '(GMT+0700) Asia/Jakarta'), ('Asia/Jayapura', '(GMT+0900) Asia/Jayapura'), ('Asia/Jerusalem', '(GMT+0300) Asia/Jerusalem'), ('Asia/Kabul', '(GMT+0430) Asia/Kabul'), ('Asia/Kamchatka', '(GMT+1200) Asia/Kamchatka'), ('Asia/Karachi', '(GMT+0500) Asia/Karachi'), ('Asia/Kathmandu', '(GMT+0545) Asia/Kathmandu'), ('Asia/Khandyga', '(GMT+0900) Asia/Khandyga'), ('Asia/Kolkata', '(GMT+0530) Asia/Kolkata'), ('Asia/Krasnoyarsk', '(GMT+0700) Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', '(GMT+0800) Asia/Kuala_Lumpur'), ('Asia/Kuching', '(GMT+0800) Asia/Kuching'), ('Asia/Kuwait', '(GMT+0300) Asia/Kuwait'), ('Asia/Macau', '(GMT+0800) Asia/Macau'), ('Asia/Magadan', '(GMT+1100) Asia/Magadan'), ('Asia/Makassar', '(GMT+0800) Asia/Makassar'), ('Asia/Manila', '(GMT+0800) Asia/Manila'), ('Asia/Muscat', '(GMT+0400) Asia/Muscat'), ('Asia/Nicosia', '(GMT+0300) Asia/Nicosia'), ('Asia/Novokuznetsk', '(GMT+0700) Asia/Novokuznetsk'), ('Asia/Novosibirsk', '(GMT+0700) Asia/Novosibirsk'), ('Asia/Omsk', '(GMT+0600) Asia/Omsk'), ('Asia/Oral', '(GMT+0500) Asia/Oral'), ('Asia/Phnom_Penh', '(GMT+0700) Asia/Phnom_Penh'), ('Asia/Pontianak', '(GMT+0700) Asia/Pontianak'), ('Asia/Pyongyang', '(GMT+0830) Asia/Pyongyang'), ('Asia/Qatar', '(GMT+0300) Asia/Qatar'), ('Asia/Qyzylorda', '(GMT+0600) Asia/Qyzylorda'), ('Asia/Riyadh', '(GMT+0300) Asia/Riyadh'), ('Asia/Sakhalin', '(GMT+1100) Asia/Sakhalin'), ('Asia/Samarkand', '(GMT+0500) Asia/Samarkand'), ('Asia/Seoul', '(GMT+0900) Asia/Seoul'), ('Asia/Shanghai', '(GMT+0800) Asia/Shanghai'), ('Asia/Singapore', '(GMT+0800) Asia/Singapore'), ('Asia/Srednekolymsk', '(GMT+1100) Asia/Srednekolymsk'), ('Asia/Taipei', '(GMT+0800) Asia/Taipei'), ('Asia/Tashkent', '(GMT+0500) Asia/Tashkent'), ('Asia/Tbilisi', '(GMT+0400) Asia/Tbilisi'), ('Asia/Tehran', '(GMT+0430) Asia/Tehran'), ('Asia/Thimphu', '(GMT+0600) Asia/Thimphu'), ('Asia/Tokyo', '(GMT+0900) Asia/Tokyo'), ('Asia/Tomsk', '(GMT+0700) Asia/Tomsk'), ('Asia/Ulaanbaatar', '(GMT+0800) Asia/Ulaanbaatar'), ('Asia/Urumqi', '(GMT+0600) Asia/Urumqi'), ('Asia/Ust-Nera', '(GMT+1000) Asia/Ust-Nera'), ('Asia/Vientiane', '(GMT+0700) Asia/Vientiane'), ('Asia/Vladivostok', '(GMT+1000) Asia/Vladivostok'), ('Asia/Yakutsk', '(GMT+0900) Asia/Yakutsk'), ('Asia/Yangon', '(GMT+0630) Asia/Yangon'), ('Asia/Yekaterinburg', '(GMT+0500) Asia/Yekaterinburg'), ('Asia/Yerevan', '(GMT+0400) Asia/Yerevan'), ('Atlantic/Azores', '(GMT+0000) Atlantic/Azores'), ('Atlantic/Bermuda', '(GMT-0300) Atlantic/Bermuda'), ('Atlantic/Canary', '(GMT+0100) Atlantic/Canary'), ('Atlantic/Cape_Verde', '(GMT-0100) Atlantic/Cape_Verde'), ('Atlantic/Faroe', '(GMT+0100) Atlantic/Faroe'), ('Atlantic/Madeira', '(GMT+0100) Atlantic/Madeira'), ('Atlantic/Reykjavik', '(GMT+0000) Atlantic/Reykjavik'), ('Atlantic/South_Georgia', '(GMT-0200) Atlantic/South_Georgia'), ('Atlantic/St_Helena', '(GMT+0000) Atlantic/St_Helena'), ('Atlantic/Stanley', '(GMT-0300) Atlantic/Stanley'), ('Australia/Adelaide', '(GMT+0930) Australia/Adelaide'), ('Australia/Brisbane', '(GMT+1000) Australia/Brisbane'), ('Australia/Broken_Hill', '(GMT+0930) Australia/Broken_Hill'), ('Australia/Currie', '(GMT+1000) Australia/Currie'), ('Australia/Darwin', '(GMT+0930) Australia/Darwin'), ('Australia/Eucla', '(GMT+0845) Australia/Eucla'), ('Australia/Hobart', '(GMT+1000) Australia/Hobart'), ('Australia/Lindeman', '(GMT+1000) Australia/Lindeman'), ('Australia/Lord_Howe', '(GMT+1030) Australia/Lord_Howe'), ('Australia/Melbourne', '(GMT+1000) Australia/Melbourne'), ('Australia/Perth', '(GMT+0800) Australia/Perth'), ('Australia/Sydney', '(GMT+1000) Australia/Sydney'), ('Canada/Atlantic', '(GMT-0300) Canada/Atlantic'), ('Canada/Central', '(GMT-0500) Canada/Central'), ('Canada/Eastern', '(GMT-0400) Canada/Eastern'), ('Canada/Mountain', '(GMT-0600) Canada/Mountain'), ('Canada/Newfoundland', '(GMT-0230) Canada/Newfoundland'), ('Canada/Pacific', '(GMT-0700) Canada/Pacific'), ('Europe/Amsterdam', '(GMT+0200) Europe/Amsterdam'), ('Europe/Andorra', '(GMT+0200) Europe/Andorra'), ('Europe/Astrakhan', '(GMT+0400) Europe/Astrakhan'), ('Europe/Athens', '(GMT+0300) Europe/Athens'), ('Europe/Belgrade', '(GMT+0200) Europe/Belgrade'), ('Europe/Berlin', '(GMT+0200) Europe/Berlin'), ('Europe/Bratislava', '(GMT+0200) Europe/Bratislava'), ('Europe/Brussels', '(GMT+0200) Europe/Brussels'), ('Europe/Bucharest', '(GMT+0300) Europe/Bucharest'), ('Europe/Budapest', '(GMT+0200) Europe/Budapest'), ('Europe/Busingen', '(GMT+0200) Europe/Busingen'), ('Europe/Chisinau', '(GMT+0300) Europe/Chisinau'), ('Europe/Copenhagen', '(GMT+0200) Europe/Copenhagen'), ('Europe/Dublin', '(GMT+0100) Europe/Dublin'), ('Europe/Gibraltar', '(GMT+0200) Europe/Gibraltar'), ('Europe/Guernsey', '(GMT+0100) Europe/Guernsey'), ('Europe/Helsinki', '(GMT+0300) Europe/Helsinki'), ('Europe/Isle_of_Man', '(GMT+0100) Europe/Isle_of_Man'), ('Europe/Istanbul', '(GMT+0300) Europe/Istanbul'), ('Europe/Jersey', '(GMT+0100) Europe/Jersey'), ('Europe/Kaliningrad', '(GMT+0200) Europe/Kaliningrad'), ('Europe/Kiev', '(GMT+0300) Europe/Kiev'), ('Europe/Kirov', '(GMT+0300) Europe/Kirov'), ('Europe/Lisbon', '(GMT+0100) Europe/Lisbon'), ('Europe/Ljubljana', '(GMT+0200) Europe/Ljubljana'), ('Europe/London', '(GMT+0100) Europe/London'), ('Europe/Luxembourg', '(GMT+0200) Europe/Luxembourg'), ('Europe/Madrid', '(GMT+0200) Europe/Madrid'), ('Europe/Malta', '(GMT+0200) Europe/Malta'), ('Europe/Mariehamn', '(GMT+0300) Europe/Mariehamn'), ('Europe/Minsk', '(GMT+0300) Europe/Minsk'), ('Europe/Monaco', '(GMT+0200) Europe/Monaco'), ('Europe/Moscow', '(GMT+0300) Europe/Moscow'), ('Europe/Oslo', '(GMT+0200) Europe/Oslo'), ('Europe/Paris', '(GMT+0200) Europe/Paris'), ('Europe/Podgorica', '(GMT+0200) Europe/Podgorica'), ('Europe/Prague', '(GMT+0200) Europe/Prague'), ('Europe/Riga', '(GMT+0300) Europe/Riga'), ('Europe/Rome', '(GMT+0200) Europe/Rome'), ('Europe/Samara', '(GMT+0400) Europe/Samara'), ('Europe/San_Marino', '(GMT+0200) Europe/San_Marino'), ('Europe/Sarajevo', '(GMT+0200) Europe/Sarajevo'), ('Europe/Saratov', '(GMT+0400) Europe/Saratov'), ('Europe/Simferopol', '(GMT+0300) Europe/Simferopol'), ('Europe/Skopje', '(GMT+0200) Europe/Skopje'), ('Europe/Sofia', '(GMT+0300) Europe/Sofia'), ('Europe/Stockholm', '(GMT+0200) Europe/Stockholm'), ('Europe/Tallinn', '(GMT+0300) Europe/Tallinn'), ('Europe/Tirane', '(GMT+0200) Europe/Tirane'), ('Europe/Ulyanovsk', '(GMT+0400) Europe/Ulyanovsk'), ('Europe/Uzhgorod', '(GMT+0300) Europe/Uzhgorod'), ('Europe/Vaduz', '(GMT+0200) Europe/Vaduz'), ('Europe/Vatican', '(GMT+0200) Europe/Vatican'), ('Europe/Vienna', '(GMT+0200) Europe/Vienna'), ('Europe/Vilnius', '(GMT+0300) Europe/Vilnius'), ('Europe/Volgograd', '(GMT+0300) Europe/Volgograd'), ('Europe/Warsaw', '(GMT+0200) Europe/Warsaw'), ('Europe/Zagreb', '(GMT+0200) Europe/Zagreb'), ('Europe/Zaporozhye', '(GMT+0300) Europe/Zaporozhye'), ('Europe/Zurich', '(GMT+0200) Europe/Zurich'), ('GMT', '(GMT+0000) GMT'), ('Indian/Antananarivo', '(GMT+0300) Indian/Antananarivo'), ('Indian/Chagos', '(GMT+0600) Indian/Chagos'), ('Indian/Christmas', '(GMT+0700) Indian/Christmas'), ('Indian/Cocos', '(GMT+0630) Indian/Cocos'), ('Indian/Comoro', '(GMT+0300) Indian/Comoro'), ('Indian/Kerguelen', '(GMT+0500) Indian/Kerguelen'), ('Indian/Mahe', '(GMT+0400) Indian/Mahe'), ('Indian/Maldives', '(GMT+0500) Indian/Maldives'), ('Indian/Mauritius', '(GMT+0400) Indian/Mauritius'), ('Indian/Mayotte', '(GMT+0300) Indian/Mayotte'), ('Indian/Reunion', '(GMT+0400) Indian/Reunion'), ('Pacific/Apia', '(GMT+1300) Pacific/Apia'), ('Pacific/Auckland', '(GMT+1200) Pacific/Auckland'), ('Pacific/Bougainville', '(GMT+1100) Pacific/Bougainville'), ('Pacific/Chatham', '(GMT+1245) Pacific/Chatham'), ('Pacific/Chuuk', '(GMT+1000) Pacific/Chuuk'), ('Pacific/Easter', '(GMT-0600) Pacific/Easter'), ('Pacific/Efate', '(GMT+1100) Pacific/Efate'), ('Pacific/Enderbury', '(GMT+1300) Pacific/Enderbury'), ('Pacific/Fakaofo', '(GMT+1300) Pacific/Fakaofo'), ('Pacific/Fiji', '(GMT+1200) Pacific/Fiji'), ('Pacific/Funafuti', '(GMT+1200) Pacific/Funafuti'), ('Pacific/Galapagos', '(GMT-0600) Pacific/Galapagos'), ('Pacific/Gambier', '(GMT-0900) Pacific/Gambier'), ('Pacific/Guadalcanal', '(GMT+1100) Pacific/Guadalcanal'), ('Pacific/Guam', '(GMT+1000) Pacific/Guam'), ('Pacific/Honolulu', '(GMT-1000) Pacific/Honolulu'), ('Pacific/Kiritimati', '(GMT+1400) Pacific/Kiritimati'), ('Pacific/Kosrae', '(GMT+1100) Pacific/Kosrae'), ('Pacific/Kwajalein', '(GMT+1200) Pacific/Kwajalein'), ('Pacific/Majuro', '(GMT+1200) Pacific/Majuro'), ('Pacific/Marquesas', '(GMT-0930) Pacific/Marquesas'), ('Pacific/Midway', '(GMT-1100) Pacific/Midway'), ('Pacific/Nauru', '(GMT+1200) Pacific/Nauru'), ('Pacific/Niue', '(GMT-1100) Pacific/Niue'), ('Pacific/Norfolk', '(GMT+1100) Pacific/Norfolk'), ('Pacific/Noumea', '(GMT+1100) Pacific/Noumea'), ('Pacific/Pago_Pago', '(GMT-1100) Pacific/Pago_Pago'), ('Pacific/Palau', '(GMT+0900) Pacific/Palau'), ('Pacific/Pitcairn', '(GMT-0800) Pacific/Pitcairn'), ('Pacific/Pohnpei', '(GMT+1100) Pacific/Pohnpei'), ('Pacific/Port_Moresby', '(GMT+1000) Pacific/Port_Moresby'), ('Pacific/Rarotonga', '(GMT-1000) Pacific/Rarotonga'), ('Pacific/Saipan', '(GMT+1000) Pacific/Saipan'), ('Pacific/Tahiti', '(GMT-1000) Pacific/Tahiti'), ('Pacific/Tarawa', '(GMT+1200) Pacific/Tarawa'), ('Pacific/Tongatapu', '(GMT+1300) Pacific/Tongatapu'), ('Pacific/Wake', '(GMT+1200) Pacific/Wake'), ('Pacific/Wallis', '(GMT+1200) Pacific/Wallis'), ('US/Alaska', '(GMT-0800) US/Alaska'), ('US/Arizona', '(GMT-0700) US/Arizona'), ('US/Central', '(GMT-0500) US/Central'), ('US/Eastern', '(GMT-0400) US/Eastern'), ('US/Hawaii', '(GMT-1000) US/Hawaii'), ('US/Mountain', '(GMT-0600) US/Mountain'), ('US/Pacific', '(GMT-0700) US/Pacific'), ('UTC', '(GMT+0000) UTC')], default='America/New_York', max_length=100)), - ('secret_token', models.CharField(blank=True, max_length=12, null=True)), - ('stripe_4_digits', models.CharField(blank=True, max_length=4, null=True)), - ('stripe_id', models.CharField(blank=True, max_length=24, null=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("is_premium", models.BooleanField(default=False)), + ("premium_expire", models.DateTimeField(blank=True, null=True)), + ("send_emails", models.BooleanField(default=True)), + ("preferences", models.TextField(default="{}")), + ("view_settings", models.TextField(default="{}")), + ("collapsed_folders", models.TextField(default="[]")), + ("feed_pane_size", models.IntegerField(default=242)), + ("tutorial_finished", models.BooleanField(default=False)), + ("hide_getting_started", models.NullBooleanField(default=False)), + ("has_setup_feeds", models.NullBooleanField(default=False)), + ("has_found_friends", models.NullBooleanField(default=False)), + ("has_trained_intelligence", models.NullBooleanField(default=False)), + ("last_seen_on", models.DateTimeField(default=datetime.datetime.now)), + ("last_seen_ip", models.CharField(blank=True, max_length=50, null=True)), + ("dashboard_date", models.DateTimeField(default=datetime.datetime.now)), + ( + "timezone", + vendor.timezones.fields.TimeZoneField( + choices=[ + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"), + ("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Dar_es_Salaam", "(GMT+0300) Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0300) Africa/Juba"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Sao_Tome", "(GMT+0100) Africa/Sao_Tome"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("America/Adak", "(GMT-0900) America/Adak"), + ("America/Anchorage", "(GMT-0800) America/Anchorage"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ("America/Argentina/Buenos_Aires", "(GMT-0300) America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "(GMT-0300) America/Argentina/Catamarca"), + ("America/Argentina/Cordoba", "(GMT-0300) America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "(GMT-0300) America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "(GMT-0300) America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "(GMT-0300) America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "(GMT-0300) America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "(GMT-0300) America/Argentina/Salta"), + ("America/Argentina/San_Juan", "(GMT-0300) America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "(GMT-0300) America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "(GMT-0300) America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "(GMT-0300) America/Argentina/Ushuaia"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0400) America/Asuncion"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Bahia_Banderas", "(GMT-0500) America/Bahia_Banderas"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Blanc-Sablon", "(GMT-0400) America/Blanc-Sablon"), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Boise", "(GMT-0600) America/Boise"), + ("America/Cambridge_Bay", "(GMT-0600) America/Cambridge_Bay"), + ("America/Campo_Grande", "(GMT-0400) America/Campo_Grande"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0500) America/Chicago"), + ("America/Chihuahua", "(GMT-0600) America/Chihuahua"), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Danmarkshavn", "(GMT+0000) America/Danmarkshavn"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ("America/Dawson_Creek", "(GMT-0700) America/Dawson_Creek"), + ("America/Denver", "(GMT-0600) America/Denver"), + ("America/Detroit", "(GMT-0400) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Edmonton", "(GMT-0600) America/Edmonton"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"), + ("America/Godthab", "(GMT-0200) America/Godthab"), + ("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"), + ("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Halifax", "(GMT-0300) America/Halifax"), + ("America/Havana", "(GMT-0400) America/Havana"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Indiana/Indianapolis", "(GMT-0400) America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "(GMT-0500) America/Indiana/Knox"), + ("America/Indiana/Marengo", "(GMT-0400) America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "(GMT-0400) America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "(GMT-0500) America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "(GMT-0400) America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "(GMT-0400) America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "(GMT-0400) America/Indiana/Winamac"), + ("America/Inuvik", "(GMT-0600) America/Inuvik"), + ("America/Iqaluit", "(GMT-0400) America/Iqaluit"), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Juneau", "(GMT-0800) America/Juneau"), + ("America/Kentucky/Louisville", "(GMT-0400) America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "(GMT-0400) America/Kentucky/Monticello"), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"), + ("America/Lower_Princes", "(GMT-0400) America/Lower_Princes"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Matamoros", "(GMT-0500) America/Matamoros"), + ("America/Mazatlan", "(GMT-0600) America/Mazatlan"), + ("America/Menominee", "(GMT-0500) America/Menominee"), + ("America/Merida", "(GMT-0500) America/Merida"), + ("America/Metlakatla", "(GMT-0800) America/Metlakatla"), + ("America/Mexico_City", "(GMT-0500) America/Mexico_City"), + ("America/Miquelon", "(GMT-0200) America/Miquelon"), + ("America/Moncton", "(GMT-0300) America/Moncton"), + ("America/Monterrey", "(GMT-0500) America/Monterrey"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0400) America/Nassau"), + ("America/New_York", "(GMT-0400) America/New_York"), + ("America/Nipigon", "(GMT-0400) America/Nipigon"), + ("America/Nome", "(GMT-0800) America/Nome"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/North_Dakota/Beulah", "(GMT-0500) America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "(GMT-0500) America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "(GMT-0500) America/North_Dakota/New_Salem"), + ("America/Ojinaga", "(GMT-0600) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ("America/Pangnirtung", "(GMT-0400) America/Pangnirtung"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Port-au-Prince", "(GMT-0400) America/Port-au-Prince"), + ("America/Port_of_Spain", "(GMT-0400) America/Port_of_Spain"), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Punta_Arenas", "(GMT-0300) America/Punta_Arenas"), + ("America/Rainy_River", "(GMT-0500) America/Rainy_River"), + ("America/Rankin_Inlet", "(GMT-0500) America/Rankin_Inlet"), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Regina", "(GMT-0600) America/Regina"), + ("America/Resolute", "(GMT-0500) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Santiago", "(GMT-0400) America/Santiago"), + ("America/Santo_Domingo", "(GMT-0400) America/Santo_Domingo"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Scoresbysund", "(GMT+0000) America/Scoresbysund"), + ("America/Sitka", "(GMT-0800) America/Sitka"), + ("America/St_Barthelemy", "(GMT-0400) America/St_Barthelemy"), + ("America/St_Johns", "(GMT-0230) America/St_Johns"), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Swift_Current", "(GMT-0600) America/Swift_Current"), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Thule", "(GMT-0300) America/Thule"), + ("America/Thunder_Bay", "(GMT-0400) America/Thunder_Bay"), + ("America/Tijuana", "(GMT-0700) America/Tijuana"), + ("America/Toronto", "(GMT-0400) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("America/Vancouver", "(GMT-0700) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("America/Winnipeg", "(GMT-0500) America/Winnipeg"), + ("America/Yakutat", "(GMT-0800) America/Yakutat"), + ("America/Yellowknife", "(GMT-0600) America/Yellowknife"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Antarctica/DumontDUrville", "(GMT+1000) Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "(GMT+1100) Antarctica/Macquarie"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Antarctica/McMurdo", "(GMT+1200) Antarctica/McMurdo"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Amman", "(GMT+0300) Asia/Amman"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Beirut", "(GMT+0300) Asia/Beirut"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Damascus", "(GMT+0300) Asia/Damascus"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0300) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0300) Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Pyongyang", "(GMT+0830) Asia/Pyongyang"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Qyzylorda", "(GMT+0600) Asia/Qyzylorda"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Tehran", "(GMT+0430) Asia/Tehran"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"), + ("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"), + ("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "(GMT-0200) Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Australia/Adelaide", "(GMT+0930) Australia/Adelaide"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Broken_Hill", "(GMT+0930) Australia/Broken_Hill"), + ("Australia/Currie", "(GMT+1000) Australia/Currie"), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Australia/Hobart", "(GMT+1000) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Lord_Howe", "(GMT+1030) Australia/Lord_Howe"), + ("Australia/Melbourne", "(GMT+1000) Australia/Melbourne"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Sydney", "(GMT+1000) Australia/Sydney"), + ("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"), + ("Canada/Central", "(GMT-0500) Canada/Central"), + ("Canada/Eastern", "(GMT-0400) Canada/Eastern"), + ("Canada/Mountain", "(GMT-0600) Canada/Mountain"), + ("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"), + ("Canada/Pacific", "(GMT-0700) Canada/Pacific"), + ("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0200) Europe/Andorra"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Athens", "(GMT+0300) Europe/Athens"), + ("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0200) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0200) Europe/Brussels"), + ("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"), + ("Europe/Budapest", "(GMT+0200) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0200) Europe/Busingen"), + ("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"), + ("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"), + ("Europe/Dublin", "(GMT+0100) Europe/Dublin"), + ("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"), + ("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"), + ("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"), + ("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Jersey", "(GMT+0100) Europe/Jersey"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Kiev", "(GMT+0300) Europe/Kiev"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"), + ("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"), + ("Europe/London", "(GMT+0100) Europe/London"), + ("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0200) Europe/Madrid"), + ("Europe/Malta", "(GMT+0200) Europe/Malta"), + ("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Monaco", "(GMT+0200) Europe/Monaco"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Oslo", "(GMT+0200) Europe/Oslo"), + ("Europe/Paris", "(GMT+0200) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0200) Europe/Prague"), + ("Europe/Riga", "(GMT+0300) Europe/Riga"), + ("Europe/Rome", "(GMT+0200) Europe/Rome"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Skopje", "(GMT+0200) Europe/Skopje"), + ("Europe/Sofia", "(GMT+0300) Europe/Sofia"), + ("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"), + ("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"), + ("Europe/Tirane", "(GMT+0200) Europe/Tirane"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "(GMT+0300) Europe/Uzhgorod"), + ("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0200) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0200) Europe/Vienna"), + ("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"), + ("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"), + ("Europe/Zaporozhye", "(GMT+0300) Europe/Zaporozhye"), + ("Europe/Zurich", "(GMT+0200) Europe/Zurich"), + ("GMT", "(GMT+0000) GMT"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Pacific/Apia", "(GMT+1300) Pacific/Apia"), + ("Pacific/Auckland", "(GMT+1200) Pacific/Auckland"), + ("Pacific/Bougainville", "(GMT+1100) Pacific/Bougainville"), + ("Pacific/Chatham", "(GMT+1245) Pacific/Chatham"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Easter", "(GMT-0600) Pacific/Easter"), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Enderbury", "(GMT+1300) Pacific/Enderbury"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Pacific/Port_Moresby", "(GMT+1000) Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("US/Alaska", "(GMT-0800) US/Alaska"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Central", "(GMT-0500) US/Central"), + ("US/Eastern", "(GMT-0400) US/Eastern"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("US/Mountain", "(GMT-0600) US/Mountain"), + ("US/Pacific", "(GMT-0700) US/Pacific"), + ("UTC", "(GMT+0000) UTC"), + ], + default="America/New_York", + max_length=100, + ), + ), + ("secret_token", models.CharField(blank=True, max_length=12, null=True)), + ("stripe_4_digits", models.CharField(blank=True, max_length=4, null=True)), + ("stripe_id", models.CharField(blank=True, max_length=24, null=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='StripeIds', + name="StripeIds", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('stripe_id', models.CharField(blank=True, max_length=24, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stripe_ids', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("stripe_id", models.CharField(blank=True, max_length=24, null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="stripe_ids", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/apps/profile/migrations/0002_auto_20200620_0803.py b/apps/profile/migrations/0002_auto_20200620_0803.py index 8214809149..7c5321a0ee 100644 --- a/apps/profile/migrations/0002_auto_20200620_0803.py +++ b/apps/profile/migrations/0002_auto_20200620_0803.py @@ -1,20 +1,24 @@ # Generated by Django 2.0 on 2020-06-20 08:03 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('profile', '0001_initial'), + ("profile", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='stripeids', - name='user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stripe_ids', to=settings.AUTH_USER_MODEL), + model_name="stripeids", + name="user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="stripe_ids", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/apps/profile/migrations/0003_auto_20201005_0932.py b/apps/profile/migrations/0003_auto_20201005_0932.py index 28c8dd8a22..c5539d865e 100644 --- a/apps/profile/migrations/0003_auto_20201005_0932.py +++ b/apps/profile/migrations/0003_auto_20201005_0932.py @@ -1,19 +1,463 @@ # Generated by Django 3.0.10 on 2020-10-05 09:32 from django.db import migrations + import vendor.timezones.fields class Migration(migrations.Migration): - dependencies = [ - ('profile', '0002_auto_20200620_0803'), + ("profile", "0002_auto_20200620_0803"), ] operations = [ migrations.AlterField( - model_name='profile', - name='timezone', - field=vendor.timezones.fields.TimeZoneField(choices=[('Africa/Abidjan', '(GMT+0000) Africa/Abidjan'), ('Africa/Accra', '(GMT+0000) Africa/Accra'), ('Africa/Addis_Ababa', '(GMT+0300) Africa/Addis_Ababa'), ('Africa/Algiers', '(GMT+0100) Africa/Algiers'), ('Africa/Asmara', '(GMT+0300) Africa/Asmara'), ('Africa/Bamako', '(GMT+0000) Africa/Bamako'), ('Africa/Bangui', '(GMT+0100) Africa/Bangui'), ('Africa/Banjul', '(GMT+0000) Africa/Banjul'), ('Africa/Bissau', '(GMT+0000) Africa/Bissau'), ('Africa/Blantyre', '(GMT+0200) Africa/Blantyre'), ('Africa/Brazzaville', '(GMT+0100) Africa/Brazzaville'), ('Africa/Bujumbura', '(GMT+0200) Africa/Bujumbura'), ('Africa/Cairo', '(GMT+0200) Africa/Cairo'), ('Africa/Casablanca', '(GMT+0100) Africa/Casablanca'), ('Africa/Ceuta', '(GMT+0200) Africa/Ceuta'), ('Africa/Conakry', '(GMT+0000) Africa/Conakry'), ('Africa/Dakar', '(GMT+0000) Africa/Dakar'), ('Africa/Dar_es_Salaam', '(GMT+0300) Africa/Dar_es_Salaam'), ('Africa/Djibouti', '(GMT+0300) Africa/Djibouti'), ('Africa/Douala', '(GMT+0100) Africa/Douala'), ('Africa/El_Aaiun', '(GMT+0100) Africa/El_Aaiun'), ('Africa/Freetown', '(GMT+0000) Africa/Freetown'), ('Africa/Gaborone', '(GMT+0200) Africa/Gaborone'), ('Africa/Harare', '(GMT+0200) Africa/Harare'), ('Africa/Johannesburg', '(GMT+0200) Africa/Johannesburg'), ('Africa/Juba', '(GMT+0300) Africa/Juba'), ('Africa/Kampala', '(GMT+0300) Africa/Kampala'), ('Africa/Khartoum', '(GMT+0200) Africa/Khartoum'), ('Africa/Kigali', '(GMT+0200) Africa/Kigali'), ('Africa/Kinshasa', '(GMT+0100) Africa/Kinshasa'), ('Africa/Lagos', '(GMT+0100) Africa/Lagos'), ('Africa/Libreville', '(GMT+0100) Africa/Libreville'), ('Africa/Lome', '(GMT+0000) Africa/Lome'), ('Africa/Luanda', '(GMT+0100) Africa/Luanda'), ('Africa/Lubumbashi', '(GMT+0200) Africa/Lubumbashi'), ('Africa/Lusaka', '(GMT+0200) Africa/Lusaka'), ('Africa/Malabo', '(GMT+0100) Africa/Malabo'), ('Africa/Maputo', '(GMT+0200) Africa/Maputo'), ('Africa/Maseru', '(GMT+0200) Africa/Maseru'), ('Africa/Mbabane', '(GMT+0200) Africa/Mbabane'), ('Africa/Mogadishu', '(GMT+0300) Africa/Mogadishu'), ('Africa/Monrovia', '(GMT+0000) Africa/Monrovia'), ('Africa/Nairobi', '(GMT+0300) Africa/Nairobi'), ('Africa/Ndjamena', '(GMT+0100) Africa/Ndjamena'), ('Africa/Niamey', '(GMT+0100) Africa/Niamey'), ('Africa/Nouakchott', '(GMT+0000) Africa/Nouakchott'), ('Africa/Ouagadougou', '(GMT+0000) Africa/Ouagadougou'), ('Africa/Porto-Novo', '(GMT+0100) Africa/Porto-Novo'), ('Africa/Sao_Tome', '(GMT+0100) Africa/Sao_Tome'), ('Africa/Tripoli', '(GMT+0200) Africa/Tripoli'), ('Africa/Tunis', '(GMT+0100) Africa/Tunis'), ('Africa/Windhoek', '(GMT+0200) Africa/Windhoek'), ('America/Adak', '(GMT-0900) America/Adak'), ('America/Anchorage', '(GMT-0800) America/Anchorage'), ('America/Anguilla', '(GMT-0400) America/Anguilla'), ('America/Antigua', '(GMT-0400) America/Antigua'), ('America/Araguaina', '(GMT-0300) America/Araguaina'), ('America/Argentina/Buenos_Aires', '(GMT-0300) America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', '(GMT-0300) America/Argentina/Catamarca'), ('America/Argentina/Cordoba', '(GMT-0300) America/Argentina/Cordoba'), ('America/Argentina/Jujuy', '(GMT-0300) America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', '(GMT-0300) America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', '(GMT-0300) America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', '(GMT-0300) America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', '(GMT-0300) America/Argentina/Salta'), ('America/Argentina/San_Juan', '(GMT-0300) America/Argentina/San_Juan'), ('America/Argentina/San_Luis', '(GMT-0300) America/Argentina/San_Luis'), ('America/Argentina/Tucuman', '(GMT-0300) America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', '(GMT-0300) America/Argentina/Ushuaia'), ('America/Aruba', '(GMT-0400) America/Aruba'), ('America/Asuncion', '(GMT-0300) America/Asuncion'), ('America/Atikokan', '(GMT-0500) America/Atikokan'), ('America/Bahia', '(GMT-0300) America/Bahia'), ('America/Bahia_Banderas', '(GMT-0500) America/Bahia_Banderas'), ('America/Barbados', '(GMT-0400) America/Barbados'), ('America/Belem', '(GMT-0300) America/Belem'), ('America/Belize', '(GMT-0600) America/Belize'), ('America/Blanc-Sablon', '(GMT-0400) America/Blanc-Sablon'), ('America/Boa_Vista', '(GMT-0400) America/Boa_Vista'), ('America/Bogota', '(GMT-0500) America/Bogota'), ('America/Boise', '(GMT-0600) America/Boise'), ('America/Cambridge_Bay', '(GMT-0600) America/Cambridge_Bay'), ('America/Campo_Grande', '(GMT-0400) America/Campo_Grande'), ('America/Cancun', '(GMT-0500) America/Cancun'), ('America/Caracas', '(GMT-0400) America/Caracas'), ('America/Cayenne', '(GMT-0300) America/Cayenne'), ('America/Cayman', '(GMT-0500) America/Cayman'), ('America/Chicago', '(GMT-0500) America/Chicago'), ('America/Chihuahua', '(GMT-0600) America/Chihuahua'), ('America/Costa_Rica', '(GMT-0600) America/Costa_Rica'), ('America/Creston', '(GMT-0700) America/Creston'), ('America/Cuiaba', '(GMT-0400) America/Cuiaba'), ('America/Curacao', '(GMT-0400) America/Curacao'), ('America/Danmarkshavn', '(GMT+0000) America/Danmarkshavn'), ('America/Dawson', '(GMT-0700) America/Dawson'), ('America/Dawson_Creek', '(GMT-0700) America/Dawson_Creek'), ('America/Denver', '(GMT-0600) America/Denver'), ('America/Detroit', '(GMT-0400) America/Detroit'), ('America/Dominica', '(GMT-0400) America/Dominica'), ('America/Edmonton', '(GMT-0600) America/Edmonton'), ('America/Eirunepe', '(GMT-0500) America/Eirunepe'), ('America/El_Salvador', '(GMT-0600) America/El_Salvador'), ('America/Fort_Nelson', '(GMT-0700) America/Fort_Nelson'), ('America/Fortaleza', '(GMT-0300) America/Fortaleza'), ('America/Glace_Bay', '(GMT-0300) America/Glace_Bay'), ('America/Godthab', '(GMT-0200) America/Godthab'), ('America/Goose_Bay', '(GMT-0300) America/Goose_Bay'), ('America/Grand_Turk', '(GMT-0400) America/Grand_Turk'), ('America/Grenada', '(GMT-0400) America/Grenada'), ('America/Guadeloupe', '(GMT-0400) America/Guadeloupe'), ('America/Guatemala', '(GMT-0600) America/Guatemala'), ('America/Guayaquil', '(GMT-0500) America/Guayaquil'), ('America/Guyana', '(GMT-0400) America/Guyana'), ('America/Halifax', '(GMT-0300) America/Halifax'), ('America/Havana', '(GMT-0400) America/Havana'), ('America/Hermosillo', '(GMT-0700) America/Hermosillo'), ('America/Indiana/Indianapolis', '(GMT-0400) America/Indiana/Indianapolis'), ('America/Indiana/Knox', '(GMT-0500) America/Indiana/Knox'), ('America/Indiana/Marengo', '(GMT-0400) America/Indiana/Marengo'), ('America/Indiana/Petersburg', '(GMT-0400) America/Indiana/Petersburg'), ('America/Indiana/Tell_City', '(GMT-0500) America/Indiana/Tell_City'), ('America/Indiana/Vevay', '(GMT-0400) America/Indiana/Vevay'), ('America/Indiana/Vincennes', '(GMT-0400) America/Indiana/Vincennes'), ('America/Indiana/Winamac', '(GMT-0400) America/Indiana/Winamac'), ('America/Inuvik', '(GMT-0600) America/Inuvik'), ('America/Iqaluit', '(GMT-0400) America/Iqaluit'), ('America/Jamaica', '(GMT-0500) America/Jamaica'), ('America/Juneau', '(GMT-0800) America/Juneau'), ('America/Kentucky/Louisville', '(GMT-0400) America/Kentucky/Louisville'), ('America/Kentucky/Monticello', '(GMT-0400) America/Kentucky/Monticello'), ('America/Kralendijk', '(GMT-0400) America/Kralendijk'), ('America/La_Paz', '(GMT-0400) America/La_Paz'), ('America/Lima', '(GMT-0500) America/Lima'), ('America/Los_Angeles', '(GMT-0700) America/Los_Angeles'), ('America/Lower_Princes', '(GMT-0400) America/Lower_Princes'), ('America/Maceio', '(GMT-0300) America/Maceio'), ('America/Managua', '(GMT-0600) America/Managua'), ('America/Manaus', '(GMT-0400) America/Manaus'), ('America/Marigot', '(GMT-0400) America/Marigot'), ('America/Martinique', '(GMT-0400) America/Martinique'), ('America/Matamoros', '(GMT-0500) America/Matamoros'), ('America/Mazatlan', '(GMT-0600) America/Mazatlan'), ('America/Menominee', '(GMT-0500) America/Menominee'), ('America/Merida', '(GMT-0500) America/Merida'), ('America/Metlakatla', '(GMT-0800) America/Metlakatla'), ('America/Mexico_City', '(GMT-0500) America/Mexico_City'), ('America/Miquelon', '(GMT-0200) America/Miquelon'), ('America/Moncton', '(GMT-0300) America/Moncton'), ('America/Monterrey', '(GMT-0500) America/Monterrey'), ('America/Montevideo', '(GMT-0300) America/Montevideo'), ('America/Montserrat', '(GMT-0400) America/Montserrat'), ('America/Nassau', '(GMT-0400) America/Nassau'), ('America/New_York', '(GMT-0400) America/New_York'), ('America/Nipigon', '(GMT-0400) America/Nipigon'), ('America/Nome', '(GMT-0800) America/Nome'), ('America/Noronha', '(GMT-0200) America/Noronha'), ('America/North_Dakota/Beulah', '(GMT-0500) America/North_Dakota/Beulah'), ('America/North_Dakota/Center', '(GMT-0500) America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', '(GMT-0500) America/North_Dakota/New_Salem'), ('America/Ojinaga', '(GMT-0600) America/Ojinaga'), ('America/Panama', '(GMT-0500) America/Panama'), ('America/Pangnirtung', '(GMT-0400) America/Pangnirtung'), ('America/Paramaribo', '(GMT-0300) America/Paramaribo'), ('America/Phoenix', '(GMT-0700) America/Phoenix'), ('America/Port-au-Prince', '(GMT-0400) America/Port-au-Prince'), ('America/Port_of_Spain', '(GMT-0400) America/Port_of_Spain'), ('America/Porto_Velho', '(GMT-0400) America/Porto_Velho'), ('America/Puerto_Rico', '(GMT-0400) America/Puerto_Rico'), ('America/Punta_Arenas', '(GMT-0300) America/Punta_Arenas'), ('America/Rainy_River', '(GMT-0500) America/Rainy_River'), ('America/Rankin_Inlet', '(GMT-0500) America/Rankin_Inlet'), ('America/Recife', '(GMT-0300) America/Recife'), ('America/Regina', '(GMT-0600) America/Regina'), ('America/Resolute', '(GMT-0500) America/Resolute'), ('America/Rio_Branco', '(GMT-0500) America/Rio_Branco'), ('America/Santarem', '(GMT-0300) America/Santarem'), ('America/Santiago', '(GMT-0300) America/Santiago'), ('America/Santo_Domingo', '(GMT-0400) America/Santo_Domingo'), ('America/Sao_Paulo', '(GMT-0300) America/Sao_Paulo'), ('America/Scoresbysund', '(GMT+0000) America/Scoresbysund'), ('America/Sitka', '(GMT-0800) America/Sitka'), ('America/St_Barthelemy', '(GMT-0400) America/St_Barthelemy'), ('America/St_Johns', '(GMT-0230) America/St_Johns'), ('America/St_Kitts', '(GMT-0400) America/St_Kitts'), ('America/St_Lucia', '(GMT-0400) America/St_Lucia'), ('America/St_Thomas', '(GMT-0400) America/St_Thomas'), ('America/St_Vincent', '(GMT-0400) America/St_Vincent'), ('America/Swift_Current', '(GMT-0600) America/Swift_Current'), ('America/Tegucigalpa', '(GMT-0600) America/Tegucigalpa'), ('America/Thule', '(GMT-0300) America/Thule'), ('America/Thunder_Bay', '(GMT-0400) America/Thunder_Bay'), ('America/Tijuana', '(GMT-0700) America/Tijuana'), ('America/Toronto', '(GMT-0400) America/Toronto'), ('America/Tortola', '(GMT-0400) America/Tortola'), ('America/Vancouver', '(GMT-0700) America/Vancouver'), ('America/Whitehorse', '(GMT-0700) America/Whitehorse'), ('America/Winnipeg', '(GMT-0500) America/Winnipeg'), ('America/Yakutat', '(GMT-0800) America/Yakutat'), ('America/Yellowknife', '(GMT-0600) America/Yellowknife'), ('Antarctica/Casey', '(GMT+1100) Antarctica/Casey'), ('Antarctica/Davis', '(GMT+0700) Antarctica/Davis'), ('Antarctica/DumontDUrville', '(GMT+1000) Antarctica/DumontDUrville'), ('Antarctica/Macquarie', '(GMT+1100) Antarctica/Macquarie'), ('Antarctica/Mawson', '(GMT+0500) Antarctica/Mawson'), ('Antarctica/McMurdo', '(GMT+1300) Antarctica/McMurdo'), ('Antarctica/Palmer', '(GMT-0300) Antarctica/Palmer'), ('Antarctica/Rothera', '(GMT-0300) Antarctica/Rothera'), ('Antarctica/Syowa', '(GMT+0300) Antarctica/Syowa'), ('Antarctica/Troll', '(GMT+0200) Antarctica/Troll'), ('Antarctica/Vostok', '(GMT+0600) Antarctica/Vostok'), ('Arctic/Longyearbyen', '(GMT+0200) Arctic/Longyearbyen'), ('Asia/Aden', '(GMT+0300) Asia/Aden'), ('Asia/Almaty', '(GMT+0600) Asia/Almaty'), ('Asia/Amman', '(GMT+0300) Asia/Amman'), ('Asia/Anadyr', '(GMT+1200) Asia/Anadyr'), ('Asia/Aqtau', '(GMT+0500) Asia/Aqtau'), ('Asia/Aqtobe', '(GMT+0500) Asia/Aqtobe'), ('Asia/Ashgabat', '(GMT+0500) Asia/Ashgabat'), ('Asia/Atyrau', '(GMT+0500) Asia/Atyrau'), ('Asia/Baghdad', '(GMT+0300) Asia/Baghdad'), ('Asia/Bahrain', '(GMT+0300) Asia/Bahrain'), ('Asia/Baku', '(GMT+0400) Asia/Baku'), ('Asia/Bangkok', '(GMT+0700) Asia/Bangkok'), ('Asia/Barnaul', '(GMT+0700) Asia/Barnaul'), ('Asia/Beirut', '(GMT+0300) Asia/Beirut'), ('Asia/Bishkek', '(GMT+0600) Asia/Bishkek'), ('Asia/Brunei', '(GMT+0800) Asia/Brunei'), ('Asia/Chita', '(GMT+0900) Asia/Chita'), ('Asia/Choibalsan', '(GMT+0800) Asia/Choibalsan'), ('Asia/Colombo', '(GMT+0530) Asia/Colombo'), ('Asia/Damascus', '(GMT+0300) Asia/Damascus'), ('Asia/Dhaka', '(GMT+0600) Asia/Dhaka'), ('Asia/Dili', '(GMT+0900) Asia/Dili'), ('Asia/Dubai', '(GMT+0400) Asia/Dubai'), ('Asia/Dushanbe', '(GMT+0500) Asia/Dushanbe'), ('Asia/Famagusta', '(GMT+0300) Asia/Famagusta'), ('Asia/Gaza', '(GMT+0300) Asia/Gaza'), ('Asia/Hebron', '(GMT+0300) Asia/Hebron'), ('Asia/Ho_Chi_Minh', '(GMT+0700) Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', '(GMT+0800) Asia/Hong_Kong'), ('Asia/Hovd', '(GMT+0700) Asia/Hovd'), ('Asia/Irkutsk', '(GMT+0800) Asia/Irkutsk'), ('Asia/Jakarta', '(GMT+0700) Asia/Jakarta'), ('Asia/Jayapura', '(GMT+0900) Asia/Jayapura'), ('Asia/Jerusalem', '(GMT+0300) Asia/Jerusalem'), ('Asia/Kabul', '(GMT+0430) Asia/Kabul'), ('Asia/Kamchatka', '(GMT+1200) Asia/Kamchatka'), ('Asia/Karachi', '(GMT+0500) Asia/Karachi'), ('Asia/Kathmandu', '(GMT+0545) Asia/Kathmandu'), ('Asia/Khandyga', '(GMT+0900) Asia/Khandyga'), ('Asia/Kolkata', '(GMT+0530) Asia/Kolkata'), ('Asia/Krasnoyarsk', '(GMT+0700) Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', '(GMT+0800) Asia/Kuala_Lumpur'), ('Asia/Kuching', '(GMT+0800) Asia/Kuching'), ('Asia/Kuwait', '(GMT+0300) Asia/Kuwait'), ('Asia/Macau', '(GMT+0800) Asia/Macau'), ('Asia/Magadan', '(GMT+1100) Asia/Magadan'), ('Asia/Makassar', '(GMT+0800) Asia/Makassar'), ('Asia/Manila', '(GMT+0800) Asia/Manila'), ('Asia/Muscat', '(GMT+0400) Asia/Muscat'), ('Asia/Nicosia', '(GMT+0300) Asia/Nicosia'), ('Asia/Novokuznetsk', '(GMT+0700) Asia/Novokuznetsk'), ('Asia/Novosibirsk', '(GMT+0700) Asia/Novosibirsk'), ('Asia/Omsk', '(GMT+0600) Asia/Omsk'), ('Asia/Oral', '(GMT+0500) Asia/Oral'), ('Asia/Phnom_Penh', '(GMT+0700) Asia/Phnom_Penh'), ('Asia/Pontianak', '(GMT+0700) Asia/Pontianak'), ('Asia/Pyongyang', '(GMT+0830) Asia/Pyongyang'), ('Asia/Qatar', '(GMT+0300) Asia/Qatar'), ('Asia/Qyzylorda', '(GMT+0600) Asia/Qyzylorda'), ('Asia/Riyadh', '(GMT+0300) Asia/Riyadh'), ('Asia/Sakhalin', '(GMT+1100) Asia/Sakhalin'), ('Asia/Samarkand', '(GMT+0500) Asia/Samarkand'), ('Asia/Seoul', '(GMT+0900) Asia/Seoul'), ('Asia/Shanghai', '(GMT+0800) Asia/Shanghai'), ('Asia/Singapore', '(GMT+0800) Asia/Singapore'), ('Asia/Srednekolymsk', '(GMT+1100) Asia/Srednekolymsk'), ('Asia/Taipei', '(GMT+0800) Asia/Taipei'), ('Asia/Tashkent', '(GMT+0500) Asia/Tashkent'), ('Asia/Tbilisi', '(GMT+0400) Asia/Tbilisi'), ('Asia/Tehran', '(GMT+0330) Asia/Tehran'), ('Asia/Thimphu', '(GMT+0600) Asia/Thimphu'), ('Asia/Tokyo', '(GMT+0900) Asia/Tokyo'), ('Asia/Tomsk', '(GMT+0700) Asia/Tomsk'), ('Asia/Ulaanbaatar', '(GMT+0800) Asia/Ulaanbaatar'), ('Asia/Urumqi', '(GMT+0600) Asia/Urumqi'), ('Asia/Ust-Nera', '(GMT+1000) Asia/Ust-Nera'), ('Asia/Vientiane', '(GMT+0700) Asia/Vientiane'), ('Asia/Vladivostok', '(GMT+1000) Asia/Vladivostok'), ('Asia/Yakutsk', '(GMT+0900) Asia/Yakutsk'), ('Asia/Yangon', '(GMT+0630) Asia/Yangon'), ('Asia/Yekaterinburg', '(GMT+0500) Asia/Yekaterinburg'), ('Asia/Yerevan', '(GMT+0400) Asia/Yerevan'), ('Atlantic/Azores', '(GMT+0000) Atlantic/Azores'), ('Atlantic/Bermuda', '(GMT-0300) Atlantic/Bermuda'), ('Atlantic/Canary', '(GMT+0100) Atlantic/Canary'), ('Atlantic/Cape_Verde', '(GMT-0100) Atlantic/Cape_Verde'), ('Atlantic/Faroe', '(GMT+0100) Atlantic/Faroe'), ('Atlantic/Madeira', '(GMT+0100) Atlantic/Madeira'), ('Atlantic/Reykjavik', '(GMT+0000) Atlantic/Reykjavik'), ('Atlantic/South_Georgia', '(GMT-0200) Atlantic/South_Georgia'), ('Atlantic/St_Helena', '(GMT+0000) Atlantic/St_Helena'), ('Atlantic/Stanley', '(GMT-0300) Atlantic/Stanley'), ('Australia/Adelaide', '(GMT+1030) Australia/Adelaide'), ('Australia/Brisbane', '(GMT+1000) Australia/Brisbane'), ('Australia/Broken_Hill', '(GMT+1030) Australia/Broken_Hill'), ('Australia/Currie', '(GMT+1100) Australia/Currie'), ('Australia/Darwin', '(GMT+0930) Australia/Darwin'), ('Australia/Eucla', '(GMT+0845) Australia/Eucla'), ('Australia/Hobart', '(GMT+1100) Australia/Hobart'), ('Australia/Lindeman', '(GMT+1000) Australia/Lindeman'), ('Australia/Lord_Howe', '(GMT+1100) Australia/Lord_Howe'), ('Australia/Melbourne', '(GMT+1100) Australia/Melbourne'), ('Australia/Perth', '(GMT+0800) Australia/Perth'), ('Australia/Sydney', '(GMT+1100) Australia/Sydney'), ('Canada/Atlantic', '(GMT-0300) Canada/Atlantic'), ('Canada/Central', '(GMT-0500) Canada/Central'), ('Canada/Eastern', '(GMT-0400) Canada/Eastern'), ('Canada/Mountain', '(GMT-0600) Canada/Mountain'), ('Canada/Newfoundland', '(GMT-0230) Canada/Newfoundland'), ('Canada/Pacific', '(GMT-0700) Canada/Pacific'), ('Europe/Amsterdam', '(GMT+0200) Europe/Amsterdam'), ('Europe/Andorra', '(GMT+0200) Europe/Andorra'), ('Europe/Astrakhan', '(GMT+0400) Europe/Astrakhan'), ('Europe/Athens', '(GMT+0300) Europe/Athens'), ('Europe/Belgrade', '(GMT+0200) Europe/Belgrade'), ('Europe/Berlin', '(GMT+0200) Europe/Berlin'), ('Europe/Bratislava', '(GMT+0200) Europe/Bratislava'), ('Europe/Brussels', '(GMT+0200) Europe/Brussels'), ('Europe/Bucharest', '(GMT+0300) Europe/Bucharest'), ('Europe/Budapest', '(GMT+0200) Europe/Budapest'), ('Europe/Busingen', '(GMT+0200) Europe/Busingen'), ('Europe/Chisinau', '(GMT+0300) Europe/Chisinau'), ('Europe/Copenhagen', '(GMT+0200) Europe/Copenhagen'), ('Europe/Dublin', '(GMT+0100) Europe/Dublin'), ('Europe/Gibraltar', '(GMT+0200) Europe/Gibraltar'), ('Europe/Guernsey', '(GMT+0100) Europe/Guernsey'), ('Europe/Helsinki', '(GMT+0300) Europe/Helsinki'), ('Europe/Isle_of_Man', '(GMT+0100) Europe/Isle_of_Man'), ('Europe/Istanbul', '(GMT+0300) Europe/Istanbul'), ('Europe/Jersey', '(GMT+0100) Europe/Jersey'), ('Europe/Kaliningrad', '(GMT+0200) Europe/Kaliningrad'), ('Europe/Kiev', '(GMT+0300) Europe/Kiev'), ('Europe/Kirov', '(GMT+0300) Europe/Kirov'), ('Europe/Lisbon', '(GMT+0100) Europe/Lisbon'), ('Europe/Ljubljana', '(GMT+0200) Europe/Ljubljana'), ('Europe/London', '(GMT+0100) Europe/London'), ('Europe/Luxembourg', '(GMT+0200) Europe/Luxembourg'), ('Europe/Madrid', '(GMT+0200) Europe/Madrid'), ('Europe/Malta', '(GMT+0200) Europe/Malta'), ('Europe/Mariehamn', '(GMT+0300) Europe/Mariehamn'), ('Europe/Minsk', '(GMT+0300) Europe/Minsk'), ('Europe/Monaco', '(GMT+0200) Europe/Monaco'), ('Europe/Moscow', '(GMT+0300) Europe/Moscow'), ('Europe/Oslo', '(GMT+0200) Europe/Oslo'), ('Europe/Paris', '(GMT+0200) Europe/Paris'), ('Europe/Podgorica', '(GMT+0200) Europe/Podgorica'), ('Europe/Prague', '(GMT+0200) Europe/Prague'), ('Europe/Riga', '(GMT+0300) Europe/Riga'), ('Europe/Rome', '(GMT+0200) Europe/Rome'), ('Europe/Samara', '(GMT+0400) Europe/Samara'), ('Europe/San_Marino', '(GMT+0200) Europe/San_Marino'), ('Europe/Sarajevo', '(GMT+0200) Europe/Sarajevo'), ('Europe/Saratov', '(GMT+0400) Europe/Saratov'), ('Europe/Simferopol', '(GMT+0300) Europe/Simferopol'), ('Europe/Skopje', '(GMT+0200) Europe/Skopje'), ('Europe/Sofia', '(GMT+0300) Europe/Sofia'), ('Europe/Stockholm', '(GMT+0200) Europe/Stockholm'), ('Europe/Tallinn', '(GMT+0300) Europe/Tallinn'), ('Europe/Tirane', '(GMT+0200) Europe/Tirane'), ('Europe/Ulyanovsk', '(GMT+0400) Europe/Ulyanovsk'), ('Europe/Uzhgorod', '(GMT+0300) Europe/Uzhgorod'), ('Europe/Vaduz', '(GMT+0200) Europe/Vaduz'), ('Europe/Vatican', '(GMT+0200) Europe/Vatican'), ('Europe/Vienna', '(GMT+0200) Europe/Vienna'), ('Europe/Vilnius', '(GMT+0300) Europe/Vilnius'), ('Europe/Volgograd', '(GMT+0300) Europe/Volgograd'), ('Europe/Warsaw', '(GMT+0200) Europe/Warsaw'), ('Europe/Zagreb', '(GMT+0200) Europe/Zagreb'), ('Europe/Zaporozhye', '(GMT+0300) Europe/Zaporozhye'), ('Europe/Zurich', '(GMT+0200) Europe/Zurich'), ('GMT', '(GMT+0000) GMT'), ('Indian/Antananarivo', '(GMT+0300) Indian/Antananarivo'), ('Indian/Chagos', '(GMT+0600) Indian/Chagos'), ('Indian/Christmas', '(GMT+0700) Indian/Christmas'), ('Indian/Cocos', '(GMT+0630) Indian/Cocos'), ('Indian/Comoro', '(GMT+0300) Indian/Comoro'), ('Indian/Kerguelen', '(GMT+0500) Indian/Kerguelen'), ('Indian/Mahe', '(GMT+0400) Indian/Mahe'), ('Indian/Maldives', '(GMT+0500) Indian/Maldives'), ('Indian/Mauritius', '(GMT+0400) Indian/Mauritius'), ('Indian/Mayotte', '(GMT+0300) Indian/Mayotte'), ('Indian/Reunion', '(GMT+0400) Indian/Reunion'), ('Pacific/Apia', '(GMT+1400) Pacific/Apia'), ('Pacific/Auckland', '(GMT+1300) Pacific/Auckland'), ('Pacific/Bougainville', '(GMT+1100) Pacific/Bougainville'), ('Pacific/Chatham', '(GMT+1345) Pacific/Chatham'), ('Pacific/Chuuk', '(GMT+1000) Pacific/Chuuk'), ('Pacific/Easter', '(GMT-0500) Pacific/Easter'), ('Pacific/Efate', '(GMT+1100) Pacific/Efate'), ('Pacific/Enderbury', '(GMT+1300) Pacific/Enderbury'), ('Pacific/Fakaofo', '(GMT+1300) Pacific/Fakaofo'), ('Pacific/Fiji', '(GMT+1200) Pacific/Fiji'), ('Pacific/Funafuti', '(GMT+1200) Pacific/Funafuti'), ('Pacific/Galapagos', '(GMT-0600) Pacific/Galapagos'), ('Pacific/Gambier', '(GMT-0900) Pacific/Gambier'), ('Pacific/Guadalcanal', '(GMT+1100) Pacific/Guadalcanal'), ('Pacific/Guam', '(GMT+1000) Pacific/Guam'), ('Pacific/Honolulu', '(GMT-1000) Pacific/Honolulu'), ('Pacific/Kiritimati', '(GMT+1400) Pacific/Kiritimati'), ('Pacific/Kosrae', '(GMT+1100) Pacific/Kosrae'), ('Pacific/Kwajalein', '(GMT+1200) Pacific/Kwajalein'), ('Pacific/Majuro', '(GMT+1200) Pacific/Majuro'), ('Pacific/Marquesas', '(GMT-0930) Pacific/Marquesas'), ('Pacific/Midway', '(GMT-1100) Pacific/Midway'), ('Pacific/Nauru', '(GMT+1200) Pacific/Nauru'), ('Pacific/Niue', '(GMT-1100) Pacific/Niue'), ('Pacific/Norfolk', '(GMT+1100) Pacific/Norfolk'), ('Pacific/Noumea', '(GMT+1100) Pacific/Noumea'), ('Pacific/Pago_Pago', '(GMT-1100) Pacific/Pago_Pago'), ('Pacific/Palau', '(GMT+0900) Pacific/Palau'), ('Pacific/Pitcairn', '(GMT-0800) Pacific/Pitcairn'), ('Pacific/Pohnpei', '(GMT+1100) Pacific/Pohnpei'), ('Pacific/Port_Moresby', '(GMT+1000) Pacific/Port_Moresby'), ('Pacific/Rarotonga', '(GMT-1000) Pacific/Rarotonga'), ('Pacific/Saipan', '(GMT+1000) Pacific/Saipan'), ('Pacific/Tahiti', '(GMT-1000) Pacific/Tahiti'), ('Pacific/Tarawa', '(GMT+1200) Pacific/Tarawa'), ('Pacific/Tongatapu', '(GMT+1300) Pacific/Tongatapu'), ('Pacific/Wake', '(GMT+1200) Pacific/Wake'), ('Pacific/Wallis', '(GMT+1200) Pacific/Wallis'), ('US/Alaska', '(GMT-0800) US/Alaska'), ('US/Arizona', '(GMT-0700) US/Arizona'), ('US/Central', '(GMT-0500) US/Central'), ('US/Eastern', '(GMT-0400) US/Eastern'), ('US/Hawaii', '(GMT-1000) US/Hawaii'), ('US/Mountain', '(GMT-0600) US/Mountain'), ('US/Pacific', '(GMT-0700) US/Pacific'), ('UTC', '(GMT+0000) UTC')], default='America/New_York', max_length=100), + model_name="profile", + name="timezone", + field=vendor.timezones.fields.TimeZoneField( + choices=[ + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"), + ("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Dar_es_Salaam", "(GMT+0300) Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0300) Africa/Juba"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Sao_Tome", "(GMT+0100) Africa/Sao_Tome"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("America/Adak", "(GMT-0900) America/Adak"), + ("America/Anchorage", "(GMT-0800) America/Anchorage"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ("America/Argentina/Buenos_Aires", "(GMT-0300) America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "(GMT-0300) America/Argentina/Catamarca"), + ("America/Argentina/Cordoba", "(GMT-0300) America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "(GMT-0300) America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "(GMT-0300) America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "(GMT-0300) America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "(GMT-0300) America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "(GMT-0300) America/Argentina/Salta"), + ("America/Argentina/San_Juan", "(GMT-0300) America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "(GMT-0300) America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "(GMT-0300) America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "(GMT-0300) America/Argentina/Ushuaia"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0300) America/Asuncion"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Bahia_Banderas", "(GMT-0500) America/Bahia_Banderas"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Blanc-Sablon", "(GMT-0400) America/Blanc-Sablon"), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Boise", "(GMT-0600) America/Boise"), + ("America/Cambridge_Bay", "(GMT-0600) America/Cambridge_Bay"), + ("America/Campo_Grande", "(GMT-0400) America/Campo_Grande"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0500) America/Chicago"), + ("America/Chihuahua", "(GMT-0600) America/Chihuahua"), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Danmarkshavn", "(GMT+0000) America/Danmarkshavn"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ("America/Dawson_Creek", "(GMT-0700) America/Dawson_Creek"), + ("America/Denver", "(GMT-0600) America/Denver"), + ("America/Detroit", "(GMT-0400) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Edmonton", "(GMT-0600) America/Edmonton"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"), + ("America/Godthab", "(GMT-0200) America/Godthab"), + ("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"), + ("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Halifax", "(GMT-0300) America/Halifax"), + ("America/Havana", "(GMT-0400) America/Havana"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Indiana/Indianapolis", "(GMT-0400) America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "(GMT-0500) America/Indiana/Knox"), + ("America/Indiana/Marengo", "(GMT-0400) America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "(GMT-0400) America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "(GMT-0500) America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "(GMT-0400) America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "(GMT-0400) America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "(GMT-0400) America/Indiana/Winamac"), + ("America/Inuvik", "(GMT-0600) America/Inuvik"), + ("America/Iqaluit", "(GMT-0400) America/Iqaluit"), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Juneau", "(GMT-0800) America/Juneau"), + ("America/Kentucky/Louisville", "(GMT-0400) America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "(GMT-0400) America/Kentucky/Monticello"), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"), + ("America/Lower_Princes", "(GMT-0400) America/Lower_Princes"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Matamoros", "(GMT-0500) America/Matamoros"), + ("America/Mazatlan", "(GMT-0600) America/Mazatlan"), + ("America/Menominee", "(GMT-0500) America/Menominee"), + ("America/Merida", "(GMT-0500) America/Merida"), + ("America/Metlakatla", "(GMT-0800) America/Metlakatla"), + ("America/Mexico_City", "(GMT-0500) America/Mexico_City"), + ("America/Miquelon", "(GMT-0200) America/Miquelon"), + ("America/Moncton", "(GMT-0300) America/Moncton"), + ("America/Monterrey", "(GMT-0500) America/Monterrey"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0400) America/Nassau"), + ("America/New_York", "(GMT-0400) America/New_York"), + ("America/Nipigon", "(GMT-0400) America/Nipigon"), + ("America/Nome", "(GMT-0800) America/Nome"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/North_Dakota/Beulah", "(GMT-0500) America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "(GMT-0500) America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "(GMT-0500) America/North_Dakota/New_Salem"), + ("America/Ojinaga", "(GMT-0600) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ("America/Pangnirtung", "(GMT-0400) America/Pangnirtung"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Port-au-Prince", "(GMT-0400) America/Port-au-Prince"), + ("America/Port_of_Spain", "(GMT-0400) America/Port_of_Spain"), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Punta_Arenas", "(GMT-0300) America/Punta_Arenas"), + ("America/Rainy_River", "(GMT-0500) America/Rainy_River"), + ("America/Rankin_Inlet", "(GMT-0500) America/Rankin_Inlet"), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Regina", "(GMT-0600) America/Regina"), + ("America/Resolute", "(GMT-0500) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Santiago", "(GMT-0300) America/Santiago"), + ("America/Santo_Domingo", "(GMT-0400) America/Santo_Domingo"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Scoresbysund", "(GMT+0000) America/Scoresbysund"), + ("America/Sitka", "(GMT-0800) America/Sitka"), + ("America/St_Barthelemy", "(GMT-0400) America/St_Barthelemy"), + ("America/St_Johns", "(GMT-0230) America/St_Johns"), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Swift_Current", "(GMT-0600) America/Swift_Current"), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Thule", "(GMT-0300) America/Thule"), + ("America/Thunder_Bay", "(GMT-0400) America/Thunder_Bay"), + ("America/Tijuana", "(GMT-0700) America/Tijuana"), + ("America/Toronto", "(GMT-0400) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("America/Vancouver", "(GMT-0700) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("America/Winnipeg", "(GMT-0500) America/Winnipeg"), + ("America/Yakutat", "(GMT-0800) America/Yakutat"), + ("America/Yellowknife", "(GMT-0600) America/Yellowknife"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Antarctica/DumontDUrville", "(GMT+1000) Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "(GMT+1100) Antarctica/Macquarie"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Amman", "(GMT+0300) Asia/Amman"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Beirut", "(GMT+0300) Asia/Beirut"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Damascus", "(GMT+0300) Asia/Damascus"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0300) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0300) Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Pyongyang", "(GMT+0830) Asia/Pyongyang"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Qyzylorda", "(GMT+0600) Asia/Qyzylorda"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Tehran", "(GMT+0330) Asia/Tehran"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"), + ("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"), + ("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "(GMT-0200) Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Australia/Adelaide", "(GMT+1030) Australia/Adelaide"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Broken_Hill", "(GMT+1030) Australia/Broken_Hill"), + ("Australia/Currie", "(GMT+1100) Australia/Currie"), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Australia/Hobart", "(GMT+1100) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"), + ("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Sydney", "(GMT+1100) Australia/Sydney"), + ("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"), + ("Canada/Central", "(GMT-0500) Canada/Central"), + ("Canada/Eastern", "(GMT-0400) Canada/Eastern"), + ("Canada/Mountain", "(GMT-0600) Canada/Mountain"), + ("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"), + ("Canada/Pacific", "(GMT-0700) Canada/Pacific"), + ("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0200) Europe/Andorra"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Athens", "(GMT+0300) Europe/Athens"), + ("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0200) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0200) Europe/Brussels"), + ("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"), + ("Europe/Budapest", "(GMT+0200) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0200) Europe/Busingen"), + ("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"), + ("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"), + ("Europe/Dublin", "(GMT+0100) Europe/Dublin"), + ("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"), + ("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"), + ("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"), + ("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Jersey", "(GMT+0100) Europe/Jersey"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Kiev", "(GMT+0300) Europe/Kiev"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"), + ("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"), + ("Europe/London", "(GMT+0100) Europe/London"), + ("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0200) Europe/Madrid"), + ("Europe/Malta", "(GMT+0200) Europe/Malta"), + ("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Monaco", "(GMT+0200) Europe/Monaco"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Oslo", "(GMT+0200) Europe/Oslo"), + ("Europe/Paris", "(GMT+0200) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0200) Europe/Prague"), + ("Europe/Riga", "(GMT+0300) Europe/Riga"), + ("Europe/Rome", "(GMT+0200) Europe/Rome"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Skopje", "(GMT+0200) Europe/Skopje"), + ("Europe/Sofia", "(GMT+0300) Europe/Sofia"), + ("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"), + ("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"), + ("Europe/Tirane", "(GMT+0200) Europe/Tirane"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "(GMT+0300) Europe/Uzhgorod"), + ("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0200) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0200) Europe/Vienna"), + ("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"), + ("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"), + ("Europe/Zaporozhye", "(GMT+0300) Europe/Zaporozhye"), + ("Europe/Zurich", "(GMT+0200) Europe/Zurich"), + ("GMT", "(GMT+0000) GMT"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Pacific/Apia", "(GMT+1400) Pacific/Apia"), + ("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"), + ("Pacific/Bougainville", "(GMT+1100) Pacific/Bougainville"), + ("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Easter", "(GMT-0500) Pacific/Easter"), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Enderbury", "(GMT+1300) Pacific/Enderbury"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Pacific/Port_Moresby", "(GMT+1000) Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("US/Alaska", "(GMT-0800) US/Alaska"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Central", "(GMT-0500) US/Central"), + ("US/Eastern", "(GMT-0400) US/Eastern"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("US/Mountain", "(GMT-0600) US/Mountain"), + ("US/Pacific", "(GMT-0700) US/Pacific"), + ("UTC", "(GMT+0000) UTC"), + ], + default="America/New_York", + max_length=100, + ), ), ] diff --git a/apps/profile/migrations/0004_auto_20220110_2106.py b/apps/profile/migrations/0004_auto_20220110_2106.py index 5aaed426cb..5d53fb6f48 100644 --- a/apps/profile/migrations/0004_auto_20220110_2106.py +++ b/apps/profile/migrations/0004_auto_20220110_2106.py @@ -1,44 +1,489 @@ # Generated by Django 3.1.10 on 2022-01-10 21:06 from django.db import migrations, models + import vendor.timezones.fields class Migration(migrations.Migration): - dependencies = [ - ('profile', '0003_auto_20201005_0932'), + ("profile", "0003_auto_20201005_0932"), ] operations = [ migrations.AddField( - model_name='profile', - name='is_pro', + model_name="profile", + name="is_pro", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='profile', - name='has_found_friends', + model_name="profile", + name="has_found_friends", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='profile', - name='has_setup_feeds', + model_name="profile", + name="has_setup_feeds", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='profile', - name='has_trained_intelligence', + model_name="profile", + name="has_trained_intelligence", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='profile', - name='hide_getting_started', + model_name="profile", + name="hide_getting_started", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=vendor.timezones.fields.TimeZoneField(choices=[('Africa/Abidjan', '(GMT+0000) Africa/Abidjan'), ('Africa/Accra', '(GMT+0000) Africa/Accra'), ('Africa/Addis_Ababa', '(GMT+0300) Africa/Addis_Ababa'), ('Africa/Algiers', '(GMT+0100) Africa/Algiers'), ('Africa/Asmara', '(GMT+0300) Africa/Asmara'), ('Africa/Bamako', '(GMT+0000) Africa/Bamako'), ('Africa/Bangui', '(GMT+0100) Africa/Bangui'), ('Africa/Banjul', '(GMT+0000) Africa/Banjul'), ('Africa/Bissau', '(GMT+0000) Africa/Bissau'), ('Africa/Blantyre', '(GMT+0200) Africa/Blantyre'), ('Africa/Brazzaville', '(GMT+0100) Africa/Brazzaville'), ('Africa/Bujumbura', '(GMT+0200) Africa/Bujumbura'), ('Africa/Cairo', '(GMT+0200) Africa/Cairo'), ('Africa/Casablanca', '(GMT+0100) Africa/Casablanca'), ('Africa/Ceuta', '(GMT+0100) Africa/Ceuta'), ('Africa/Conakry', '(GMT+0000) Africa/Conakry'), ('Africa/Dakar', '(GMT+0000) Africa/Dakar'), ('Africa/Dar_es_Salaam', '(GMT+0300) Africa/Dar_es_Salaam'), ('Africa/Djibouti', '(GMT+0300) Africa/Djibouti'), ('Africa/Douala', '(GMT+0100) Africa/Douala'), ('Africa/El_Aaiun', '(GMT+0100) Africa/El_Aaiun'), ('Africa/Freetown', '(GMT+0000) Africa/Freetown'), ('Africa/Gaborone', '(GMT+0200) Africa/Gaborone'), ('Africa/Harare', '(GMT+0200) Africa/Harare'), ('Africa/Johannesburg', '(GMT+0200) Africa/Johannesburg'), ('Africa/Juba', '(GMT+0300) Africa/Juba'), ('Africa/Kampala', '(GMT+0300) Africa/Kampala'), ('Africa/Khartoum', '(GMT+0200) Africa/Khartoum'), ('Africa/Kigali', '(GMT+0200) Africa/Kigali'), ('Africa/Kinshasa', '(GMT+0100) Africa/Kinshasa'), ('Africa/Lagos', '(GMT+0100) Africa/Lagos'), ('Africa/Libreville', '(GMT+0100) Africa/Libreville'), ('Africa/Lome', '(GMT+0000) Africa/Lome'), ('Africa/Luanda', '(GMT+0100) Africa/Luanda'), ('Africa/Lubumbashi', '(GMT+0200) Africa/Lubumbashi'), ('Africa/Lusaka', '(GMT+0200) Africa/Lusaka'), ('Africa/Malabo', '(GMT+0100) Africa/Malabo'), ('Africa/Maputo', '(GMT+0200) Africa/Maputo'), ('Africa/Maseru', '(GMT+0200) Africa/Maseru'), ('Africa/Mbabane', '(GMT+0200) Africa/Mbabane'), ('Africa/Mogadishu', '(GMT+0300) Africa/Mogadishu'), ('Africa/Monrovia', '(GMT+0000) Africa/Monrovia'), ('Africa/Nairobi', '(GMT+0300) Africa/Nairobi'), ('Africa/Ndjamena', '(GMT+0100) Africa/Ndjamena'), ('Africa/Niamey', '(GMT+0100) Africa/Niamey'), ('Africa/Nouakchott', '(GMT+0000) Africa/Nouakchott'), ('Africa/Ouagadougou', '(GMT+0000) Africa/Ouagadougou'), ('Africa/Porto-Novo', '(GMT+0100) Africa/Porto-Novo'), ('Africa/Sao_Tome', '(GMT+0000) Africa/Sao_Tome'), ('Africa/Tripoli', '(GMT+0200) Africa/Tripoli'), ('Africa/Tunis', '(GMT+0100) Africa/Tunis'), ('Africa/Windhoek', '(GMT+0200) Africa/Windhoek'), ('America/Adak', '(GMT-1000) America/Adak'), ('America/Anchorage', '(GMT-0900) America/Anchorage'), ('America/Anguilla', '(GMT-0400) America/Anguilla'), ('America/Antigua', '(GMT-0400) America/Antigua'), ('America/Araguaina', '(GMT-0300) America/Araguaina'), ('America/Argentina/Buenos_Aires', '(GMT-0300) America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', '(GMT-0300) America/Argentina/Catamarca'), ('America/Argentina/Cordoba', '(GMT-0300) America/Argentina/Cordoba'), ('America/Argentina/Jujuy', '(GMT-0300) America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', '(GMT-0300) America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', '(GMT-0300) America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', '(GMT-0300) America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', '(GMT-0300) America/Argentina/Salta'), ('America/Argentina/San_Juan', '(GMT-0300) America/Argentina/San_Juan'), ('America/Argentina/San_Luis', '(GMT-0300) America/Argentina/San_Luis'), ('America/Argentina/Tucuman', '(GMT-0300) America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', '(GMT-0300) America/Argentina/Ushuaia'), ('America/Aruba', '(GMT-0400) America/Aruba'), ('America/Asuncion', '(GMT-0300) America/Asuncion'), ('America/Atikokan', '(GMT-0500) America/Atikokan'), ('America/Bahia', '(GMT-0300) America/Bahia'), ('America/Bahia_Banderas', '(GMT-0600) America/Bahia_Banderas'), ('America/Barbados', '(GMT-0400) America/Barbados'), ('America/Belem', '(GMT-0300) America/Belem'), ('America/Belize', '(GMT-0600) America/Belize'), ('America/Blanc-Sablon', '(GMT-0400) America/Blanc-Sablon'), ('America/Boa_Vista', '(GMT-0400) America/Boa_Vista'), ('America/Bogota', '(GMT-0500) America/Bogota'), ('America/Boise', '(GMT-0700) America/Boise'), ('America/Cambridge_Bay', '(GMT-0700) America/Cambridge_Bay'), ('America/Campo_Grande', '(GMT-0400) America/Campo_Grande'), ('America/Cancun', '(GMT-0500) America/Cancun'), ('America/Caracas', '(GMT-0400) America/Caracas'), ('America/Cayenne', '(GMT-0300) America/Cayenne'), ('America/Cayman', '(GMT-0500) America/Cayman'), ('America/Chicago', '(GMT-0600) America/Chicago'), ('America/Chihuahua', '(GMT-0700) America/Chihuahua'), ('America/Costa_Rica', '(GMT-0600) America/Costa_Rica'), ('America/Creston', '(GMT-0700) America/Creston'), ('America/Cuiaba', '(GMT-0400) America/Cuiaba'), ('America/Curacao', '(GMT-0400) America/Curacao'), ('America/Danmarkshavn', '(GMT+0000) America/Danmarkshavn'), ('America/Dawson', '(GMT-0700) America/Dawson'), ('America/Dawson_Creek', '(GMT-0700) America/Dawson_Creek'), ('America/Denver', '(GMT-0700) America/Denver'), ('America/Detroit', '(GMT-0500) America/Detroit'), ('America/Dominica', '(GMT-0400) America/Dominica'), ('America/Edmonton', '(GMT-0700) America/Edmonton'), ('America/Eirunepe', '(GMT-0500) America/Eirunepe'), ('America/El_Salvador', '(GMT-0600) America/El_Salvador'), ('America/Fort_Nelson', '(GMT-0700) America/Fort_Nelson'), ('America/Fortaleza', '(GMT-0300) America/Fortaleza'), ('America/Glace_Bay', '(GMT-0400) America/Glace_Bay'), ('America/Goose_Bay', '(GMT-0400) America/Goose_Bay'), ('America/Grand_Turk', '(GMT-0500) America/Grand_Turk'), ('America/Grenada', '(GMT-0400) America/Grenada'), ('America/Guadeloupe', '(GMT-0400) America/Guadeloupe'), ('America/Guatemala', '(GMT-0600) America/Guatemala'), ('America/Guayaquil', '(GMT-0500) America/Guayaquil'), ('America/Guyana', '(GMT-0400) America/Guyana'), ('America/Halifax', '(GMT-0400) America/Halifax'), ('America/Havana', '(GMT-0500) America/Havana'), ('America/Hermosillo', '(GMT-0700) America/Hermosillo'), ('America/Indiana/Indianapolis', '(GMT-0500) America/Indiana/Indianapolis'), ('America/Indiana/Knox', '(GMT-0600) America/Indiana/Knox'), ('America/Indiana/Marengo', '(GMT-0500) America/Indiana/Marengo'), ('America/Indiana/Petersburg', '(GMT-0500) America/Indiana/Petersburg'), ('America/Indiana/Tell_City', '(GMT-0600) America/Indiana/Tell_City'), ('America/Indiana/Vevay', '(GMT-0500) America/Indiana/Vevay'), ('America/Indiana/Vincennes', '(GMT-0500) America/Indiana/Vincennes'), ('America/Indiana/Winamac', '(GMT-0500) America/Indiana/Winamac'), ('America/Inuvik', '(GMT-0700) America/Inuvik'), ('America/Iqaluit', '(GMT-0500) America/Iqaluit'), ('America/Jamaica', '(GMT-0500) America/Jamaica'), ('America/Juneau', '(GMT-0900) America/Juneau'), ('America/Kentucky/Louisville', '(GMT-0500) America/Kentucky/Louisville'), ('America/Kentucky/Monticello', '(GMT-0500) America/Kentucky/Monticello'), ('America/Kralendijk', '(GMT-0400) America/Kralendijk'), ('America/La_Paz', '(GMT-0400) America/La_Paz'), ('America/Lima', '(GMT-0500) America/Lima'), ('America/Los_Angeles', '(GMT-0800) America/Los_Angeles'), ('America/Lower_Princes', '(GMT-0400) America/Lower_Princes'), ('America/Maceio', '(GMT-0300) America/Maceio'), ('America/Managua', '(GMT-0600) America/Managua'), ('America/Manaus', '(GMT-0400) America/Manaus'), ('America/Marigot', '(GMT-0400) America/Marigot'), ('America/Martinique', '(GMT-0400) America/Martinique'), ('America/Matamoros', '(GMT-0600) America/Matamoros'), ('America/Mazatlan', '(GMT-0700) America/Mazatlan'), ('America/Menominee', '(GMT-0600) America/Menominee'), ('America/Merida', '(GMT-0600) America/Merida'), ('America/Metlakatla', '(GMT-0900) America/Metlakatla'), ('America/Mexico_City', '(GMT-0600) America/Mexico_City'), ('America/Miquelon', '(GMT-0300) America/Miquelon'), ('America/Moncton', '(GMT-0400) America/Moncton'), ('America/Monterrey', '(GMT-0600) America/Monterrey'), ('America/Montevideo', '(GMT-0300) America/Montevideo'), ('America/Montserrat', '(GMT-0400) America/Montserrat'), ('America/Nassau', '(GMT-0500) America/Nassau'), ('America/New_York', '(GMT-0500) America/New_York'), ('America/Nipigon', '(GMT-0500) America/Nipigon'), ('America/Nome', '(GMT-0900) America/Nome'), ('America/Noronha', '(GMT-0200) America/Noronha'), ('America/North_Dakota/Beulah', '(GMT-0600) America/North_Dakota/Beulah'), ('America/North_Dakota/Center', '(GMT-0600) America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', '(GMT-0600) America/North_Dakota/New_Salem'), ('America/Nuuk', '(GMT-0300) America/Nuuk'), ('America/Ojinaga', '(GMT-0700) America/Ojinaga'), ('America/Panama', '(GMT-0500) America/Panama'), ('America/Pangnirtung', '(GMT-0500) America/Pangnirtung'), ('America/Paramaribo', '(GMT-0300) America/Paramaribo'), ('America/Phoenix', '(GMT-0700) America/Phoenix'), ('America/Port-au-Prince', '(GMT-0500) America/Port-au-Prince'), ('America/Port_of_Spain', '(GMT-0400) America/Port_of_Spain'), ('America/Porto_Velho', '(GMT-0400) America/Porto_Velho'), ('America/Puerto_Rico', '(GMT-0400) America/Puerto_Rico'), ('America/Punta_Arenas', '(GMT-0300) America/Punta_Arenas'), ('America/Rainy_River', '(GMT-0600) America/Rainy_River'), ('America/Rankin_Inlet', '(GMT-0600) America/Rankin_Inlet'), ('America/Recife', '(GMT-0300) America/Recife'), ('America/Regina', '(GMT-0600) America/Regina'), ('America/Resolute', '(GMT-0600) America/Resolute'), ('America/Rio_Branco', '(GMT-0500) America/Rio_Branco'), ('America/Santarem', '(GMT-0300) America/Santarem'), ('America/Santiago', '(GMT-0300) America/Santiago'), ('America/Santo_Domingo', '(GMT-0400) America/Santo_Domingo'), ('America/Sao_Paulo', '(GMT-0300) America/Sao_Paulo'), ('America/Scoresbysund', '(GMT-0100) America/Scoresbysund'), ('America/Sitka', '(GMT-0900) America/Sitka'), ('America/St_Barthelemy', '(GMT-0400) America/St_Barthelemy'), ('America/St_Johns', '(GMT-0330) America/St_Johns'), ('America/St_Kitts', '(GMT-0400) America/St_Kitts'), ('America/St_Lucia', '(GMT-0400) America/St_Lucia'), ('America/St_Thomas', '(GMT-0400) America/St_Thomas'), ('America/St_Vincent', '(GMT-0400) America/St_Vincent'), ('America/Swift_Current', '(GMT-0600) America/Swift_Current'), ('America/Tegucigalpa', '(GMT-0600) America/Tegucigalpa'), ('America/Thule', '(GMT-0400) America/Thule'), ('America/Thunder_Bay', '(GMT-0500) America/Thunder_Bay'), ('America/Tijuana', '(GMT-0800) America/Tijuana'), ('America/Toronto', '(GMT-0500) America/Toronto'), ('America/Tortola', '(GMT-0400) America/Tortola'), ('America/Vancouver', '(GMT-0800) America/Vancouver'), ('America/Whitehorse', '(GMT-0700) America/Whitehorse'), ('America/Winnipeg', '(GMT-0600) America/Winnipeg'), ('America/Yakutat', '(GMT-0900) America/Yakutat'), ('America/Yellowknife', '(GMT-0700) America/Yellowknife'), ('Antarctica/Casey', '(GMT+1100) Antarctica/Casey'), ('Antarctica/Davis', '(GMT+0700) Antarctica/Davis'), ('Antarctica/DumontDUrville', '(GMT+1000) Antarctica/DumontDUrville'), ('Antarctica/Macquarie', '(GMT+1100) Antarctica/Macquarie'), ('Antarctica/Mawson', '(GMT+0500) Antarctica/Mawson'), ('Antarctica/McMurdo', '(GMT+1300) Antarctica/McMurdo'), ('Antarctica/Palmer', '(GMT-0300) Antarctica/Palmer'), ('Antarctica/Rothera', '(GMT-0300) Antarctica/Rothera'), ('Antarctica/Syowa', '(GMT+0300) Antarctica/Syowa'), ('Antarctica/Troll', '(GMT+0000) Antarctica/Troll'), ('Antarctica/Vostok', '(GMT+0600) Antarctica/Vostok'), ('Arctic/Longyearbyen', '(GMT+0100) Arctic/Longyearbyen'), ('Asia/Aden', '(GMT+0300) Asia/Aden'), ('Asia/Almaty', '(GMT+0600) Asia/Almaty'), ('Asia/Amman', '(GMT+0200) Asia/Amman'), ('Asia/Anadyr', '(GMT+1200) Asia/Anadyr'), ('Asia/Aqtau', '(GMT+0500) Asia/Aqtau'), ('Asia/Aqtobe', '(GMT+0500) Asia/Aqtobe'), ('Asia/Ashgabat', '(GMT+0500) Asia/Ashgabat'), ('Asia/Atyrau', '(GMT+0500) Asia/Atyrau'), ('Asia/Baghdad', '(GMT+0300) Asia/Baghdad'), ('Asia/Bahrain', '(GMT+0300) Asia/Bahrain'), ('Asia/Baku', '(GMT+0400) Asia/Baku'), ('Asia/Bangkok', '(GMT+0700) Asia/Bangkok'), ('Asia/Barnaul', '(GMT+0700) Asia/Barnaul'), ('Asia/Beirut', '(GMT+0200) Asia/Beirut'), ('Asia/Bishkek', '(GMT+0600) Asia/Bishkek'), ('Asia/Brunei', '(GMT+0800) Asia/Brunei'), ('Asia/Chita', '(GMT+0900) Asia/Chita'), ('Asia/Choibalsan', '(GMT+0800) Asia/Choibalsan'), ('Asia/Colombo', '(GMT+0530) Asia/Colombo'), ('Asia/Damascus', '(GMT+0200) Asia/Damascus'), ('Asia/Dhaka', '(GMT+0600) Asia/Dhaka'), ('Asia/Dili', '(GMT+0900) Asia/Dili'), ('Asia/Dubai', '(GMT+0400) Asia/Dubai'), ('Asia/Dushanbe', '(GMT+0500) Asia/Dushanbe'), ('Asia/Famagusta', '(GMT+0200) Asia/Famagusta'), ('Asia/Gaza', '(GMT+0200) Asia/Gaza'), ('Asia/Hebron', '(GMT+0200) Asia/Hebron'), ('Asia/Ho_Chi_Minh', '(GMT+0700) Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', '(GMT+0800) Asia/Hong_Kong'), ('Asia/Hovd', '(GMT+0700) Asia/Hovd'), ('Asia/Irkutsk', '(GMT+0800) Asia/Irkutsk'), ('Asia/Jakarta', '(GMT+0700) Asia/Jakarta'), ('Asia/Jayapura', '(GMT+0900) Asia/Jayapura'), ('Asia/Jerusalem', '(GMT+0200) Asia/Jerusalem'), ('Asia/Kabul', '(GMT+0430) Asia/Kabul'), ('Asia/Kamchatka', '(GMT+1200) Asia/Kamchatka'), ('Asia/Karachi', '(GMT+0500) Asia/Karachi'), ('Asia/Kathmandu', '(GMT+0545) Asia/Kathmandu'), ('Asia/Khandyga', '(GMT+0900) Asia/Khandyga'), ('Asia/Kolkata', '(GMT+0530) Asia/Kolkata'), ('Asia/Krasnoyarsk', '(GMT+0700) Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', '(GMT+0800) Asia/Kuala_Lumpur'), ('Asia/Kuching', '(GMT+0800) Asia/Kuching'), ('Asia/Kuwait', '(GMT+0300) Asia/Kuwait'), ('Asia/Macau', '(GMT+0800) Asia/Macau'), ('Asia/Magadan', '(GMT+1100) Asia/Magadan'), ('Asia/Makassar', '(GMT+0800) Asia/Makassar'), ('Asia/Manila', '(GMT+0800) Asia/Manila'), ('Asia/Muscat', '(GMT+0400) Asia/Muscat'), ('Asia/Nicosia', '(GMT+0200) Asia/Nicosia'), ('Asia/Novokuznetsk', '(GMT+0700) Asia/Novokuznetsk'), ('Asia/Novosibirsk', '(GMT+0700) Asia/Novosibirsk'), ('Asia/Omsk', '(GMT+0600) Asia/Omsk'), ('Asia/Oral', '(GMT+0500) Asia/Oral'), ('Asia/Phnom_Penh', '(GMT+0700) Asia/Phnom_Penh'), ('Asia/Pontianak', '(GMT+0700) Asia/Pontianak'), ('Asia/Pyongyang', '(GMT+0900) Asia/Pyongyang'), ('Asia/Qatar', '(GMT+0300) Asia/Qatar'), ('Asia/Qostanay', '(GMT+0600) Asia/Qostanay'), ('Asia/Qyzylorda', '(GMT+0500) Asia/Qyzylorda'), ('Asia/Riyadh', '(GMT+0300) Asia/Riyadh'), ('Asia/Sakhalin', '(GMT+1100) Asia/Sakhalin'), ('Asia/Samarkand', '(GMT+0500) Asia/Samarkand'), ('Asia/Seoul', '(GMT+0900) Asia/Seoul'), ('Asia/Shanghai', '(GMT+0800) Asia/Shanghai'), ('Asia/Singapore', '(GMT+0800) Asia/Singapore'), ('Asia/Srednekolymsk', '(GMT+1100) Asia/Srednekolymsk'), ('Asia/Taipei', '(GMT+0800) Asia/Taipei'), ('Asia/Tashkent', '(GMT+0500) Asia/Tashkent'), ('Asia/Tbilisi', '(GMT+0400) Asia/Tbilisi'), ('Asia/Tehran', '(GMT+0330) Asia/Tehran'), ('Asia/Thimphu', '(GMT+0600) Asia/Thimphu'), ('Asia/Tokyo', '(GMT+0900) Asia/Tokyo'), ('Asia/Tomsk', '(GMT+0700) Asia/Tomsk'), ('Asia/Ulaanbaatar', '(GMT+0800) Asia/Ulaanbaatar'), ('Asia/Urumqi', '(GMT+0600) Asia/Urumqi'), ('Asia/Ust-Nera', '(GMT+1000) Asia/Ust-Nera'), ('Asia/Vientiane', '(GMT+0700) Asia/Vientiane'), ('Asia/Vladivostok', '(GMT+1000) Asia/Vladivostok'), ('Asia/Yakutsk', '(GMT+0900) Asia/Yakutsk'), ('Asia/Yangon', '(GMT+0630) Asia/Yangon'), ('Asia/Yekaterinburg', '(GMT+0500) Asia/Yekaterinburg'), ('Asia/Yerevan', '(GMT+0400) Asia/Yerevan'), ('Atlantic/Azores', '(GMT-0100) Atlantic/Azores'), ('Atlantic/Bermuda', '(GMT-0400) Atlantic/Bermuda'), ('Atlantic/Canary', '(GMT+0000) Atlantic/Canary'), ('Atlantic/Cape_Verde', '(GMT-0100) Atlantic/Cape_Verde'), ('Atlantic/Faroe', '(GMT+0000) Atlantic/Faroe'), ('Atlantic/Madeira', '(GMT+0000) Atlantic/Madeira'), ('Atlantic/Reykjavik', '(GMT+0000) Atlantic/Reykjavik'), ('Atlantic/South_Georgia', '(GMT-0200) Atlantic/South_Georgia'), ('Atlantic/St_Helena', '(GMT+0000) Atlantic/St_Helena'), ('Atlantic/Stanley', '(GMT-0300) Atlantic/Stanley'), ('Australia/Adelaide', '(GMT+1030) Australia/Adelaide'), ('Australia/Brisbane', '(GMT+1000) Australia/Brisbane'), ('Australia/Broken_Hill', '(GMT+1030) Australia/Broken_Hill'), ('Australia/Currie', '(GMT+1100) Australia/Currie'), ('Australia/Darwin', '(GMT+0930) Australia/Darwin'), ('Australia/Eucla', '(GMT+0845) Australia/Eucla'), ('Australia/Hobart', '(GMT+1100) Australia/Hobart'), ('Australia/Lindeman', '(GMT+1000) Australia/Lindeman'), ('Australia/Lord_Howe', '(GMT+1100) Australia/Lord_Howe'), ('Australia/Melbourne', '(GMT+1100) Australia/Melbourne'), ('Australia/Perth', '(GMT+0800) Australia/Perth'), ('Australia/Sydney', '(GMT+1100) Australia/Sydney'), ('Canada/Atlantic', '(GMT-0400) Canada/Atlantic'), ('Canada/Central', '(GMT-0600) Canada/Central'), ('Canada/Eastern', '(GMT-0500) Canada/Eastern'), ('Canada/Mountain', '(GMT-0700) Canada/Mountain'), ('Canada/Newfoundland', '(GMT-0330) Canada/Newfoundland'), ('Canada/Pacific', '(GMT-0800) Canada/Pacific'), ('Europe/Amsterdam', '(GMT+0100) Europe/Amsterdam'), ('Europe/Andorra', '(GMT+0100) Europe/Andorra'), ('Europe/Astrakhan', '(GMT+0400) Europe/Astrakhan'), ('Europe/Athens', '(GMT+0200) Europe/Athens'), ('Europe/Belgrade', '(GMT+0100) Europe/Belgrade'), ('Europe/Berlin', '(GMT+0100) Europe/Berlin'), ('Europe/Bratislava', '(GMT+0100) Europe/Bratislava'), ('Europe/Brussels', '(GMT+0100) Europe/Brussels'), ('Europe/Bucharest', '(GMT+0200) Europe/Bucharest'), ('Europe/Budapest', '(GMT+0100) Europe/Budapest'), ('Europe/Busingen', '(GMT+0100) Europe/Busingen'), ('Europe/Chisinau', '(GMT+0200) Europe/Chisinau'), ('Europe/Copenhagen', '(GMT+0100) Europe/Copenhagen'), ('Europe/Dublin', '(GMT+0000) Europe/Dublin'), ('Europe/Gibraltar', '(GMT+0100) Europe/Gibraltar'), ('Europe/Guernsey', '(GMT+0000) Europe/Guernsey'), ('Europe/Helsinki', '(GMT+0200) Europe/Helsinki'), ('Europe/Isle_of_Man', '(GMT+0000) Europe/Isle_of_Man'), ('Europe/Istanbul', '(GMT+0300) Europe/Istanbul'), ('Europe/Jersey', '(GMT+0000) Europe/Jersey'), ('Europe/Kaliningrad', '(GMT+0200) Europe/Kaliningrad'), ('Europe/Kiev', '(GMT+0200) Europe/Kiev'), ('Europe/Kirov', '(GMT+0300) Europe/Kirov'), ('Europe/Lisbon', '(GMT+0000) Europe/Lisbon'), ('Europe/Ljubljana', '(GMT+0100) Europe/Ljubljana'), ('Europe/London', '(GMT+0000) Europe/London'), ('Europe/Luxembourg', '(GMT+0100) Europe/Luxembourg'), ('Europe/Madrid', '(GMT+0100) Europe/Madrid'), ('Europe/Malta', '(GMT+0100) Europe/Malta'), ('Europe/Mariehamn', '(GMT+0200) Europe/Mariehamn'), ('Europe/Minsk', '(GMT+0300) Europe/Minsk'), ('Europe/Monaco', '(GMT+0100) Europe/Monaco'), ('Europe/Moscow', '(GMT+0300) Europe/Moscow'), ('Europe/Oslo', '(GMT+0100) Europe/Oslo'), ('Europe/Paris', '(GMT+0100) Europe/Paris'), ('Europe/Podgorica', '(GMT+0100) Europe/Podgorica'), ('Europe/Prague', '(GMT+0100) Europe/Prague'), ('Europe/Riga', '(GMT+0200) Europe/Riga'), ('Europe/Rome', '(GMT+0100) Europe/Rome'), ('Europe/Samara', '(GMT+0400) Europe/Samara'), ('Europe/San_Marino', '(GMT+0100) Europe/San_Marino'), ('Europe/Sarajevo', '(GMT+0100) Europe/Sarajevo'), ('Europe/Saratov', '(GMT+0400) Europe/Saratov'), ('Europe/Simferopol', '(GMT+0300) Europe/Simferopol'), ('Europe/Skopje', '(GMT+0100) Europe/Skopje'), ('Europe/Sofia', '(GMT+0200) Europe/Sofia'), ('Europe/Stockholm', '(GMT+0100) Europe/Stockholm'), ('Europe/Tallinn', '(GMT+0200) Europe/Tallinn'), ('Europe/Tirane', '(GMT+0100) Europe/Tirane'), ('Europe/Ulyanovsk', '(GMT+0400) Europe/Ulyanovsk'), ('Europe/Uzhgorod', '(GMT+0200) Europe/Uzhgorod'), ('Europe/Vaduz', '(GMT+0100) Europe/Vaduz'), ('Europe/Vatican', '(GMT+0100) Europe/Vatican'), ('Europe/Vienna', '(GMT+0100) Europe/Vienna'), ('Europe/Vilnius', '(GMT+0200) Europe/Vilnius'), ('Europe/Volgograd', '(GMT+0400) Europe/Volgograd'), ('Europe/Warsaw', '(GMT+0100) Europe/Warsaw'), ('Europe/Zagreb', '(GMT+0100) Europe/Zagreb'), ('Europe/Zaporozhye', '(GMT+0200) Europe/Zaporozhye'), ('Europe/Zurich', '(GMT+0100) Europe/Zurich'), ('GMT', '(GMT+0000) GMT'), ('Indian/Antananarivo', '(GMT+0300) Indian/Antananarivo'), ('Indian/Chagos', '(GMT+0600) Indian/Chagos'), ('Indian/Christmas', '(GMT+0700) Indian/Christmas'), ('Indian/Cocos', '(GMT+0630) Indian/Cocos'), ('Indian/Comoro', '(GMT+0300) Indian/Comoro'), ('Indian/Kerguelen', '(GMT+0500) Indian/Kerguelen'), ('Indian/Mahe', '(GMT+0400) Indian/Mahe'), ('Indian/Maldives', '(GMT+0500) Indian/Maldives'), ('Indian/Mauritius', '(GMT+0400) Indian/Mauritius'), ('Indian/Mayotte', '(GMT+0300) Indian/Mayotte'), ('Indian/Reunion', '(GMT+0400) Indian/Reunion'), ('Pacific/Apia', '(GMT+1400) Pacific/Apia'), ('Pacific/Auckland', '(GMT+1300) Pacific/Auckland'), ('Pacific/Bougainville', '(GMT+1100) Pacific/Bougainville'), ('Pacific/Chatham', '(GMT+1345) Pacific/Chatham'), ('Pacific/Chuuk', '(GMT+1000) Pacific/Chuuk'), ('Pacific/Easter', '(GMT-0500) Pacific/Easter'), ('Pacific/Efate', '(GMT+1100) Pacific/Efate'), ('Pacific/Enderbury', '(GMT+1300) Pacific/Enderbury'), ('Pacific/Fakaofo', '(GMT+1300) Pacific/Fakaofo'), ('Pacific/Fiji', '(GMT+1300) Pacific/Fiji'), ('Pacific/Funafuti', '(GMT+1200) Pacific/Funafuti'), ('Pacific/Galapagos', '(GMT-0600) Pacific/Galapagos'), ('Pacific/Gambier', '(GMT-0900) Pacific/Gambier'), ('Pacific/Guadalcanal', '(GMT+1100) Pacific/Guadalcanal'), ('Pacific/Guam', '(GMT+1000) Pacific/Guam'), ('Pacific/Honolulu', '(GMT-1000) Pacific/Honolulu'), ('Pacific/Kiritimati', '(GMT+1400) Pacific/Kiritimati'), ('Pacific/Kosrae', '(GMT+1100) Pacific/Kosrae'), ('Pacific/Kwajalein', '(GMT+1200) Pacific/Kwajalein'), ('Pacific/Majuro', '(GMT+1200) Pacific/Majuro'), ('Pacific/Marquesas', '(GMT-0930) Pacific/Marquesas'), ('Pacific/Midway', '(GMT-1100) Pacific/Midway'), ('Pacific/Nauru', '(GMT+1200) Pacific/Nauru'), ('Pacific/Niue', '(GMT-1100) Pacific/Niue'), ('Pacific/Norfolk', '(GMT+1200) Pacific/Norfolk'), ('Pacific/Noumea', '(GMT+1100) Pacific/Noumea'), ('Pacific/Pago_Pago', '(GMT-1100) Pacific/Pago_Pago'), ('Pacific/Palau', '(GMT+0900) Pacific/Palau'), ('Pacific/Pitcairn', '(GMT-0800) Pacific/Pitcairn'), ('Pacific/Pohnpei', '(GMT+1100) Pacific/Pohnpei'), ('Pacific/Port_Moresby', '(GMT+1000) Pacific/Port_Moresby'), ('Pacific/Rarotonga', '(GMT-1000) Pacific/Rarotonga'), ('Pacific/Saipan', '(GMT+1000) Pacific/Saipan'), ('Pacific/Tahiti', '(GMT-1000) Pacific/Tahiti'), ('Pacific/Tarawa', '(GMT+1200) Pacific/Tarawa'), ('Pacific/Tongatapu', '(GMT+1300) Pacific/Tongatapu'), ('Pacific/Wake', '(GMT+1200) Pacific/Wake'), ('Pacific/Wallis', '(GMT+1200) Pacific/Wallis'), ('US/Alaska', '(GMT-0900) US/Alaska'), ('US/Arizona', '(GMT-0700) US/Arizona'), ('US/Central', '(GMT-0600) US/Central'), ('US/Eastern', '(GMT-0500) US/Eastern'), ('US/Hawaii', '(GMT-1000) US/Hawaii'), ('US/Mountain', '(GMT-0700) US/Mountain'), ('US/Pacific', '(GMT-0800) US/Pacific'), ('UTC', '(GMT+0000) UTC')], default='America/New_York', max_length=100), + model_name="profile", + name="timezone", + field=vendor.timezones.fields.TimeZoneField( + choices=[ + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"), + ("Africa/Ceuta", "(GMT+0100) Africa/Ceuta"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Dar_es_Salaam", "(GMT+0300) Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0300) Africa/Juba"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("America/Adak", "(GMT-1000) America/Adak"), + ("America/Anchorage", "(GMT-0900) America/Anchorage"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ("America/Argentina/Buenos_Aires", "(GMT-0300) America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "(GMT-0300) America/Argentina/Catamarca"), + ("America/Argentina/Cordoba", "(GMT-0300) America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "(GMT-0300) America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "(GMT-0300) America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "(GMT-0300) America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "(GMT-0300) America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "(GMT-0300) America/Argentina/Salta"), + ("America/Argentina/San_Juan", "(GMT-0300) America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "(GMT-0300) America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "(GMT-0300) America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "(GMT-0300) America/Argentina/Ushuaia"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0300) America/Asuncion"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Bahia_Banderas", "(GMT-0600) America/Bahia_Banderas"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Blanc-Sablon", "(GMT-0400) America/Blanc-Sablon"), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Boise", "(GMT-0700) America/Boise"), + ("America/Cambridge_Bay", "(GMT-0700) America/Cambridge_Bay"), + ("America/Campo_Grande", "(GMT-0400) America/Campo_Grande"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0600) America/Chicago"), + ("America/Chihuahua", "(GMT-0700) America/Chihuahua"), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Danmarkshavn", "(GMT+0000) America/Danmarkshavn"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ("America/Dawson_Creek", "(GMT-0700) America/Dawson_Creek"), + ("America/Denver", "(GMT-0700) America/Denver"), + ("America/Detroit", "(GMT-0500) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Edmonton", "(GMT-0700) America/Edmonton"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0400) America/Glace_Bay"), + ("America/Goose_Bay", "(GMT-0400) America/Goose_Bay"), + ("America/Grand_Turk", "(GMT-0500) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Halifax", "(GMT-0400) America/Halifax"), + ("America/Havana", "(GMT-0500) America/Havana"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Indiana/Indianapolis", "(GMT-0500) America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "(GMT-0600) America/Indiana/Knox"), + ("America/Indiana/Marengo", "(GMT-0500) America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "(GMT-0500) America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "(GMT-0600) America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "(GMT-0500) America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "(GMT-0500) America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "(GMT-0500) America/Indiana/Winamac"), + ("America/Inuvik", "(GMT-0700) America/Inuvik"), + ("America/Iqaluit", "(GMT-0500) America/Iqaluit"), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Juneau", "(GMT-0900) America/Juneau"), + ("America/Kentucky/Louisville", "(GMT-0500) America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "(GMT-0500) America/Kentucky/Monticello"), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Los_Angeles", "(GMT-0800) America/Los_Angeles"), + ("America/Lower_Princes", "(GMT-0400) America/Lower_Princes"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Matamoros", "(GMT-0600) America/Matamoros"), + ("America/Mazatlan", "(GMT-0700) America/Mazatlan"), + ("America/Menominee", "(GMT-0600) America/Menominee"), + ("America/Merida", "(GMT-0600) America/Merida"), + ("America/Metlakatla", "(GMT-0900) America/Metlakatla"), + ("America/Mexico_City", "(GMT-0600) America/Mexico_City"), + ("America/Miquelon", "(GMT-0300) America/Miquelon"), + ("America/Moncton", "(GMT-0400) America/Moncton"), + ("America/Monterrey", "(GMT-0600) America/Monterrey"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0500) America/Nassau"), + ("America/New_York", "(GMT-0500) America/New_York"), + ("America/Nipigon", "(GMT-0500) America/Nipigon"), + ("America/Nome", "(GMT-0900) America/Nome"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/North_Dakota/Beulah", "(GMT-0600) America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "(GMT-0600) America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "(GMT-0600) America/North_Dakota/New_Salem"), + ("America/Nuuk", "(GMT-0300) America/Nuuk"), + ("America/Ojinaga", "(GMT-0700) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ("America/Pangnirtung", "(GMT-0500) America/Pangnirtung"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Port-au-Prince", "(GMT-0500) America/Port-au-Prince"), + ("America/Port_of_Spain", "(GMT-0400) America/Port_of_Spain"), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Punta_Arenas", "(GMT-0300) America/Punta_Arenas"), + ("America/Rainy_River", "(GMT-0600) America/Rainy_River"), + ("America/Rankin_Inlet", "(GMT-0600) America/Rankin_Inlet"), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Regina", "(GMT-0600) America/Regina"), + ("America/Resolute", "(GMT-0600) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Santiago", "(GMT-0300) America/Santiago"), + ("America/Santo_Domingo", "(GMT-0400) America/Santo_Domingo"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Scoresbysund", "(GMT-0100) America/Scoresbysund"), + ("America/Sitka", "(GMT-0900) America/Sitka"), + ("America/St_Barthelemy", "(GMT-0400) America/St_Barthelemy"), + ("America/St_Johns", "(GMT-0330) America/St_Johns"), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Swift_Current", "(GMT-0600) America/Swift_Current"), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Thule", "(GMT-0400) America/Thule"), + ("America/Thunder_Bay", "(GMT-0500) America/Thunder_Bay"), + ("America/Tijuana", "(GMT-0800) America/Tijuana"), + ("America/Toronto", "(GMT-0500) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("America/Vancouver", "(GMT-0800) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("America/Winnipeg", "(GMT-0600) America/Winnipeg"), + ("America/Yakutat", "(GMT-0900) America/Yakutat"), + ("America/Yellowknife", "(GMT-0700) America/Yellowknife"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Antarctica/DumontDUrville", "(GMT+1000) Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "(GMT+1100) Antarctica/Macquarie"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Antarctica/Troll", "(GMT+0000) Antarctica/Troll"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Arctic/Longyearbyen", "(GMT+0100) Arctic/Longyearbyen"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Amman", "(GMT+0200) Asia/Amman"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Beirut", "(GMT+0200) Asia/Beirut"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Damascus", "(GMT+0200) Asia/Damascus"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Famagusta", "(GMT+0200) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0200) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0200) Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Jerusalem", "(GMT+0200) Asia/Jerusalem"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Nicosia", "(GMT+0200) Asia/Nicosia"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"), + ("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Tehran", "(GMT+0330) Asia/Tehran"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Atlantic/Azores", "(GMT-0100) Atlantic/Azores"), + ("Atlantic/Bermuda", "(GMT-0400) Atlantic/Bermuda"), + ("Atlantic/Canary", "(GMT+0000) Atlantic/Canary"), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Atlantic/Faroe", "(GMT+0000) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0000) Atlantic/Madeira"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "(GMT-0200) Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Australia/Adelaide", "(GMT+1030) Australia/Adelaide"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Broken_Hill", "(GMT+1030) Australia/Broken_Hill"), + ("Australia/Currie", "(GMT+1100) Australia/Currie"), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Australia/Hobart", "(GMT+1100) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"), + ("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Sydney", "(GMT+1100) Australia/Sydney"), + ("Canada/Atlantic", "(GMT-0400) Canada/Atlantic"), + ("Canada/Central", "(GMT-0600) Canada/Central"), + ("Canada/Eastern", "(GMT-0500) Canada/Eastern"), + ("Canada/Mountain", "(GMT-0700) Canada/Mountain"), + ("Canada/Newfoundland", "(GMT-0330) Canada/Newfoundland"), + ("Canada/Pacific", "(GMT-0800) Canada/Pacific"), + ("Europe/Amsterdam", "(GMT+0100) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0100) Europe/Andorra"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Athens", "(GMT+0200) Europe/Athens"), + ("Europe/Belgrade", "(GMT+0100) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0100) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0100) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0100) Europe/Brussels"), + ("Europe/Bucharest", "(GMT+0200) Europe/Bucharest"), + ("Europe/Budapest", "(GMT+0100) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0100) Europe/Busingen"), + ("Europe/Chisinau", "(GMT+0200) Europe/Chisinau"), + ("Europe/Copenhagen", "(GMT+0100) Europe/Copenhagen"), + ("Europe/Dublin", "(GMT+0000) Europe/Dublin"), + ("Europe/Gibraltar", "(GMT+0100) Europe/Gibraltar"), + ("Europe/Guernsey", "(GMT+0000) Europe/Guernsey"), + ("Europe/Helsinki", "(GMT+0200) Europe/Helsinki"), + ("Europe/Isle_of_Man", "(GMT+0000) Europe/Isle_of_Man"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Jersey", "(GMT+0000) Europe/Jersey"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Kiev", "(GMT+0200) Europe/Kiev"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Lisbon", "(GMT+0000) Europe/Lisbon"), + ("Europe/Ljubljana", "(GMT+0100) Europe/Ljubljana"), + ("Europe/London", "(GMT+0000) Europe/London"), + ("Europe/Luxembourg", "(GMT+0100) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0100) Europe/Madrid"), + ("Europe/Malta", "(GMT+0100) Europe/Malta"), + ("Europe/Mariehamn", "(GMT+0200) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Monaco", "(GMT+0100) Europe/Monaco"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Oslo", "(GMT+0100) Europe/Oslo"), + ("Europe/Paris", "(GMT+0100) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0100) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0100) Europe/Prague"), + ("Europe/Riga", "(GMT+0200) Europe/Riga"), + ("Europe/Rome", "(GMT+0100) Europe/Rome"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/San_Marino", "(GMT+0100) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0100) Europe/Sarajevo"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Skopje", "(GMT+0100) Europe/Skopje"), + ("Europe/Sofia", "(GMT+0200) Europe/Sofia"), + ("Europe/Stockholm", "(GMT+0100) Europe/Stockholm"), + ("Europe/Tallinn", "(GMT+0200) Europe/Tallinn"), + ("Europe/Tirane", "(GMT+0100) Europe/Tirane"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "(GMT+0200) Europe/Uzhgorod"), + ("Europe/Vaduz", "(GMT+0100) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0100) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0100) Europe/Vienna"), + ("Europe/Vilnius", "(GMT+0200) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0400) Europe/Volgograd"), + ("Europe/Warsaw", "(GMT+0100) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0100) Europe/Zagreb"), + ("Europe/Zaporozhye", "(GMT+0200) Europe/Zaporozhye"), + ("Europe/Zurich", "(GMT+0100) Europe/Zurich"), + ("GMT", "(GMT+0000) GMT"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Pacific/Apia", "(GMT+1400) Pacific/Apia"), + ("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"), + ("Pacific/Bougainville", "(GMT+1100) Pacific/Bougainville"), + ("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Easter", "(GMT-0500) Pacific/Easter"), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Enderbury", "(GMT+1300) Pacific/Enderbury"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Fiji", "(GMT+1300) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Norfolk", "(GMT+1200) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Pacific/Port_Moresby", "(GMT+1000) Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("US/Alaska", "(GMT-0900) US/Alaska"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Central", "(GMT-0600) US/Central"), + ("US/Eastern", "(GMT-0500) US/Eastern"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("US/Mountain", "(GMT-0700) US/Mountain"), + ("US/Pacific", "(GMT-0800) US/Pacific"), + ("UTC", "(GMT+0000) UTC"), + ], + default="America/New_York", + max_length=100, + ), ), ] diff --git a/apps/profile/migrations/0005_profile_is_archive.py b/apps/profile/migrations/0005_profile_is_archive.py index 0b87acc915..66f2e1ceab 100644 --- a/apps/profile/migrations/0005_profile_is_archive.py +++ b/apps/profile/migrations/0005_profile_is_archive.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('profile', '0004_auto_20220110_2106'), + ("profile", "0004_auto_20220110_2106"), ] operations = [ migrations.AddField( - model_name='profile', - name='is_archive', + model_name="profile", + name="is_archive", field=models.BooleanField(blank=True, default=False, null=True), ), ] diff --git a/apps/profile/migrations/0006_profile_days_of_unread.py b/apps/profile/migrations/0006_profile_days_of_unread.py index d7740bbfde..240bcce63a 100644 --- a/apps/profile/migrations/0006_profile_days_of_unread.py +++ b/apps/profile/migrations/0006_profile_days_of_unread.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('profile', '0005_profile_is_archive'), + ("profile", "0005_profile_is_archive"), ] operations = [ migrations.AddField( - model_name='profile', - name='days_of_unread', + model_name="profile", + name="days_of_unread", field=models.IntegerField(default=30, blank=True, null=True), ), ] diff --git a/apps/profile/migrations/0007_auto_20220125_2108.py b/apps/profile/migrations/0007_auto_20220125_2108.py index 624d89d85f..c5472fc8b7 100644 --- a/apps/profile/migrations/0007_auto_20220125_2108.py +++ b/apps/profile/migrations/0007_auto_20220125_2108.py @@ -1,24 +1,469 @@ # Generated by Django 3.1.10 on 2022-01-25 21:08 from django.db import migrations, models + import vendor.timezones.fields class Migration(migrations.Migration): - dependencies = [ - ('profile', '0006_profile_days_of_unread'), + ("profile", "0006_profile_days_of_unread"), ] operations = [ migrations.AddField( - model_name='profile', - name='premium_renewal', + model_name="profile", + name="premium_renewal", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=vendor.timezones.fields.TimeZoneField(choices=[('Africa/Abidjan', '(GMT+0000) Africa/Abidjan'), ('Africa/Accra', '(GMT+0000) Africa/Accra'), ('Africa/Addis_Ababa', '(GMT+0300) Africa/Addis_Ababa'), ('Africa/Algiers', '(GMT+0100) Africa/Algiers'), ('Africa/Asmara', '(GMT+0300) Africa/Asmara'), ('Africa/Bamako', '(GMT+0000) Africa/Bamako'), ('Africa/Bangui', '(GMT+0100) Africa/Bangui'), ('Africa/Banjul', '(GMT+0000) Africa/Banjul'), ('Africa/Bissau', '(GMT+0000) Africa/Bissau'), ('Africa/Blantyre', '(GMT+0200) Africa/Blantyre'), ('Africa/Brazzaville', '(GMT+0100) Africa/Brazzaville'), ('Africa/Bujumbura', '(GMT+0200) Africa/Bujumbura'), ('Africa/Cairo', '(GMT+0200) Africa/Cairo'), ('Africa/Casablanca', '(GMT+0100) Africa/Casablanca'), ('Africa/Ceuta', '(GMT+0100) Africa/Ceuta'), ('Africa/Conakry', '(GMT+0000) Africa/Conakry'), ('Africa/Dakar', '(GMT+0000) Africa/Dakar'), ('Africa/Dar_es_Salaam', '(GMT+0300) Africa/Dar_es_Salaam'), ('Africa/Djibouti', '(GMT+0300) Africa/Djibouti'), ('Africa/Douala', '(GMT+0100) Africa/Douala'), ('Africa/El_Aaiun', '(GMT+0100) Africa/El_Aaiun'), ('Africa/Freetown', '(GMT+0000) Africa/Freetown'), ('Africa/Gaborone', '(GMT+0200) Africa/Gaborone'), ('Africa/Harare', '(GMT+0200) Africa/Harare'), ('Africa/Johannesburg', '(GMT+0200) Africa/Johannesburg'), ('Africa/Juba', '(GMT+0300) Africa/Juba'), ('Africa/Kampala', '(GMT+0300) Africa/Kampala'), ('Africa/Khartoum', '(GMT+0200) Africa/Khartoum'), ('Africa/Kigali', '(GMT+0200) Africa/Kigali'), ('Africa/Kinshasa', '(GMT+0100) Africa/Kinshasa'), ('Africa/Lagos', '(GMT+0100) Africa/Lagos'), ('Africa/Libreville', '(GMT+0100) Africa/Libreville'), ('Africa/Lome', '(GMT+0000) Africa/Lome'), ('Africa/Luanda', '(GMT+0100) Africa/Luanda'), ('Africa/Lubumbashi', '(GMT+0200) Africa/Lubumbashi'), ('Africa/Lusaka', '(GMT+0200) Africa/Lusaka'), ('Africa/Malabo', '(GMT+0100) Africa/Malabo'), ('Africa/Maputo', '(GMT+0200) Africa/Maputo'), ('Africa/Maseru', '(GMT+0200) Africa/Maseru'), ('Africa/Mbabane', '(GMT+0200) Africa/Mbabane'), ('Africa/Mogadishu', '(GMT+0300) Africa/Mogadishu'), ('Africa/Monrovia', '(GMT+0000) Africa/Monrovia'), ('Africa/Nairobi', '(GMT+0300) Africa/Nairobi'), ('Africa/Ndjamena', '(GMT+0100) Africa/Ndjamena'), ('Africa/Niamey', '(GMT+0100) Africa/Niamey'), ('Africa/Nouakchott', '(GMT+0000) Africa/Nouakchott'), ('Africa/Ouagadougou', '(GMT+0000) Africa/Ouagadougou'), ('Africa/Porto-Novo', '(GMT+0100) Africa/Porto-Novo'), ('Africa/Sao_Tome', '(GMT+0000) Africa/Sao_Tome'), ('Africa/Tripoli', '(GMT+0200) Africa/Tripoli'), ('Africa/Tunis', '(GMT+0100) Africa/Tunis'), ('Africa/Windhoek', '(GMT+0200) Africa/Windhoek'), ('America/Adak', '(GMT-1000) America/Adak'), ('America/Anchorage', '(GMT-0900) America/Anchorage'), ('America/Anguilla', '(GMT-0400) America/Anguilla'), ('America/Antigua', '(GMT-0400) America/Antigua'), ('America/Araguaina', '(GMT-0300) America/Araguaina'), ('America/Argentina/Buenos_Aires', '(GMT-0300) America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', '(GMT-0300) America/Argentina/Catamarca'), ('America/Argentina/Cordoba', '(GMT-0300) America/Argentina/Cordoba'), ('America/Argentina/Jujuy', '(GMT-0300) America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', '(GMT-0300) America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', '(GMT-0300) America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', '(GMT-0300) America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', '(GMT-0300) America/Argentina/Salta'), ('America/Argentina/San_Juan', '(GMT-0300) America/Argentina/San_Juan'), ('America/Argentina/San_Luis', '(GMT-0300) America/Argentina/San_Luis'), ('America/Argentina/Tucuman', '(GMT-0300) America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', '(GMT-0300) America/Argentina/Ushuaia'), ('America/Aruba', '(GMT-0400) America/Aruba'), ('America/Asuncion', '(GMT-0300) America/Asuncion'), ('America/Atikokan', '(GMT-0500) America/Atikokan'), ('America/Bahia', '(GMT-0300) America/Bahia'), ('America/Bahia_Banderas', '(GMT-0600) America/Bahia_Banderas'), ('America/Barbados', '(GMT-0400) America/Barbados'), ('America/Belem', '(GMT-0300) America/Belem'), ('America/Belize', '(GMT-0600) America/Belize'), ('America/Blanc-Sablon', '(GMT-0400) America/Blanc-Sablon'), ('America/Boa_Vista', '(GMT-0400) America/Boa_Vista'), ('America/Bogota', '(GMT-0500) America/Bogota'), ('America/Boise', '(GMT-0700) America/Boise'), ('America/Cambridge_Bay', '(GMT-0700) America/Cambridge_Bay'), ('America/Campo_Grande', '(GMT-0400) America/Campo_Grande'), ('America/Cancun', '(GMT-0500) America/Cancun'), ('America/Caracas', '(GMT-0400) America/Caracas'), ('America/Cayenne', '(GMT-0300) America/Cayenne'), ('America/Cayman', '(GMT-0500) America/Cayman'), ('America/Chicago', '(GMT-0600) America/Chicago'), ('America/Chihuahua', '(GMT-0700) America/Chihuahua'), ('America/Costa_Rica', '(GMT-0600) America/Costa_Rica'), ('America/Creston', '(GMT-0700) America/Creston'), ('America/Cuiaba', '(GMT-0400) America/Cuiaba'), ('America/Curacao', '(GMT-0400) America/Curacao'), ('America/Danmarkshavn', '(GMT+0000) America/Danmarkshavn'), ('America/Dawson', '(GMT-0700) America/Dawson'), ('America/Dawson_Creek', '(GMT-0700) America/Dawson_Creek'), ('America/Denver', '(GMT-0700) America/Denver'), ('America/Detroit', '(GMT-0500) America/Detroit'), ('America/Dominica', '(GMT-0400) America/Dominica'), ('America/Edmonton', '(GMT-0700) America/Edmonton'), ('America/Eirunepe', '(GMT-0500) America/Eirunepe'), ('America/El_Salvador', '(GMT-0600) America/El_Salvador'), ('America/Fort_Nelson', '(GMT-0700) America/Fort_Nelson'), ('America/Fortaleza', '(GMT-0300) America/Fortaleza'), ('America/Glace_Bay', '(GMT-0400) America/Glace_Bay'), ('America/Goose_Bay', '(GMT-0400) America/Goose_Bay'), ('America/Grand_Turk', '(GMT-0500) America/Grand_Turk'), ('America/Grenada', '(GMT-0400) America/Grenada'), ('America/Guadeloupe', '(GMT-0400) America/Guadeloupe'), ('America/Guatemala', '(GMT-0600) America/Guatemala'), ('America/Guayaquil', '(GMT-0500) America/Guayaquil'), ('America/Guyana', '(GMT-0400) America/Guyana'), ('America/Halifax', '(GMT-0400) America/Halifax'), ('America/Havana', '(GMT-0500) America/Havana'), ('America/Hermosillo', '(GMT-0700) America/Hermosillo'), ('America/Indiana/Indianapolis', '(GMT-0500) America/Indiana/Indianapolis'), ('America/Indiana/Knox', '(GMT-0600) America/Indiana/Knox'), ('America/Indiana/Marengo', '(GMT-0500) America/Indiana/Marengo'), ('America/Indiana/Petersburg', '(GMT-0500) America/Indiana/Petersburg'), ('America/Indiana/Tell_City', '(GMT-0600) America/Indiana/Tell_City'), ('America/Indiana/Vevay', '(GMT-0500) America/Indiana/Vevay'), ('America/Indiana/Vincennes', '(GMT-0500) America/Indiana/Vincennes'), ('America/Indiana/Winamac', '(GMT-0500) America/Indiana/Winamac'), ('America/Inuvik', '(GMT-0700) America/Inuvik'), ('America/Iqaluit', '(GMT-0500) America/Iqaluit'), ('America/Jamaica', '(GMT-0500) America/Jamaica'), ('America/Juneau', '(GMT-0900) America/Juneau'), ('America/Kentucky/Louisville', '(GMT-0500) America/Kentucky/Louisville'), ('America/Kentucky/Monticello', '(GMT-0500) America/Kentucky/Monticello'), ('America/Kralendijk', '(GMT-0400) America/Kralendijk'), ('America/La_Paz', '(GMT-0400) America/La_Paz'), ('America/Lima', '(GMT-0500) America/Lima'), ('America/Los_Angeles', '(GMT-0800) America/Los_Angeles'), ('America/Lower_Princes', '(GMT-0400) America/Lower_Princes'), ('America/Maceio', '(GMT-0300) America/Maceio'), ('America/Managua', '(GMT-0600) America/Managua'), ('America/Manaus', '(GMT-0400) America/Manaus'), ('America/Marigot', '(GMT-0400) America/Marigot'), ('America/Martinique', '(GMT-0400) America/Martinique'), ('America/Matamoros', '(GMT-0600) America/Matamoros'), ('America/Mazatlan', '(GMT-0700) America/Mazatlan'), ('America/Menominee', '(GMT-0600) America/Menominee'), ('America/Merida', '(GMT-0600) America/Merida'), ('America/Metlakatla', '(GMT-0900) America/Metlakatla'), ('America/Mexico_City', '(GMT-0600) America/Mexico_City'), ('America/Miquelon', '(GMT-0300) America/Miquelon'), ('America/Moncton', '(GMT-0400) America/Moncton'), ('America/Monterrey', '(GMT-0600) America/Monterrey'), ('America/Montevideo', '(GMT-0300) America/Montevideo'), ('America/Montserrat', '(GMT-0400) America/Montserrat'), ('America/Nassau', '(GMT-0500) America/Nassau'), ('America/New_York', '(GMT-0500) America/New_York'), ('America/Nipigon', '(GMT-0500) America/Nipigon'), ('America/Nome', '(GMT-0900) America/Nome'), ('America/Noronha', '(GMT-0200) America/Noronha'), ('America/North_Dakota/Beulah', '(GMT-0600) America/North_Dakota/Beulah'), ('America/North_Dakota/Center', '(GMT-0600) America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', '(GMT-0600) America/North_Dakota/New_Salem'), ('America/Nuuk', '(GMT-0300) America/Nuuk'), ('America/Ojinaga', '(GMT-0700) America/Ojinaga'), ('America/Panama', '(GMT-0500) America/Panama'), ('America/Pangnirtung', '(GMT-0500) America/Pangnirtung'), ('America/Paramaribo', '(GMT-0300) America/Paramaribo'), ('America/Phoenix', '(GMT-0700) America/Phoenix'), ('America/Port-au-Prince', '(GMT-0500) America/Port-au-Prince'), ('America/Port_of_Spain', '(GMT-0400) America/Port_of_Spain'), ('America/Porto_Velho', '(GMT-0400) America/Porto_Velho'), ('America/Puerto_Rico', '(GMT-0400) America/Puerto_Rico'), ('America/Punta_Arenas', '(GMT-0300) America/Punta_Arenas'), ('America/Rainy_River', '(GMT-0600) America/Rainy_River'), ('America/Rankin_Inlet', '(GMT-0600) America/Rankin_Inlet'), ('America/Recife', '(GMT-0300) America/Recife'), ('America/Regina', '(GMT-0600) America/Regina'), ('America/Resolute', '(GMT-0600) America/Resolute'), ('America/Rio_Branco', '(GMT-0500) America/Rio_Branco'), ('America/Santarem', '(GMT-0300) America/Santarem'), ('America/Santiago', '(GMT-0300) America/Santiago'), ('America/Santo_Domingo', '(GMT-0400) America/Santo_Domingo'), ('America/Sao_Paulo', '(GMT-0300) America/Sao_Paulo'), ('America/Scoresbysund', '(GMT-0100) America/Scoresbysund'), ('America/Sitka', '(GMT-0900) America/Sitka'), ('America/St_Barthelemy', '(GMT-0400) America/St_Barthelemy'), ('America/St_Johns', '(GMT-0330) America/St_Johns'), ('America/St_Kitts', '(GMT-0400) America/St_Kitts'), ('America/St_Lucia', '(GMT-0400) America/St_Lucia'), ('America/St_Thomas', '(GMT-0400) America/St_Thomas'), ('America/St_Vincent', '(GMT-0400) America/St_Vincent'), ('America/Swift_Current', '(GMT-0600) America/Swift_Current'), ('America/Tegucigalpa', '(GMT-0600) America/Tegucigalpa'), ('America/Thule', '(GMT-0400) America/Thule'), ('America/Thunder_Bay', '(GMT-0500) America/Thunder_Bay'), ('America/Tijuana', '(GMT-0800) America/Tijuana'), ('America/Toronto', '(GMT-0500) America/Toronto'), ('America/Tortola', '(GMT-0400) America/Tortola'), ('America/Vancouver', '(GMT-0800) America/Vancouver'), ('America/Whitehorse', '(GMT-0700) America/Whitehorse'), ('America/Winnipeg', '(GMT-0600) America/Winnipeg'), ('America/Yakutat', '(GMT-0900) America/Yakutat'), ('America/Yellowknife', '(GMT-0700) America/Yellowknife'), ('Antarctica/Casey', '(GMT+1100) Antarctica/Casey'), ('Antarctica/Davis', '(GMT+0700) Antarctica/Davis'), ('Antarctica/DumontDUrville', '(GMT+1000) Antarctica/DumontDUrville'), ('Antarctica/Macquarie', '(GMT+1100) Antarctica/Macquarie'), ('Antarctica/Mawson', '(GMT+0500) Antarctica/Mawson'), ('Antarctica/McMurdo', '(GMT+1300) Antarctica/McMurdo'), ('Antarctica/Palmer', '(GMT-0300) Antarctica/Palmer'), ('Antarctica/Rothera', '(GMT-0300) Antarctica/Rothera'), ('Antarctica/Syowa', '(GMT+0300) Antarctica/Syowa'), ('Antarctica/Troll', '(GMT+0000) Antarctica/Troll'), ('Antarctica/Vostok', '(GMT+0600) Antarctica/Vostok'), ('Arctic/Longyearbyen', '(GMT+0100) Arctic/Longyearbyen'), ('Asia/Aden', '(GMT+0300) Asia/Aden'), ('Asia/Almaty', '(GMT+0600) Asia/Almaty'), ('Asia/Amman', '(GMT+0200) Asia/Amman'), ('Asia/Anadyr', '(GMT+1200) Asia/Anadyr'), ('Asia/Aqtau', '(GMT+0500) Asia/Aqtau'), ('Asia/Aqtobe', '(GMT+0500) Asia/Aqtobe'), ('Asia/Ashgabat', '(GMT+0500) Asia/Ashgabat'), ('Asia/Atyrau', '(GMT+0500) Asia/Atyrau'), ('Asia/Baghdad', '(GMT+0300) Asia/Baghdad'), ('Asia/Bahrain', '(GMT+0300) Asia/Bahrain'), ('Asia/Baku', '(GMT+0400) Asia/Baku'), ('Asia/Bangkok', '(GMT+0700) Asia/Bangkok'), ('Asia/Barnaul', '(GMT+0700) Asia/Barnaul'), ('Asia/Beirut', '(GMT+0200) Asia/Beirut'), ('Asia/Bishkek', '(GMT+0600) Asia/Bishkek'), ('Asia/Brunei', '(GMT+0800) Asia/Brunei'), ('Asia/Chita', '(GMT+0900) Asia/Chita'), ('Asia/Choibalsan', '(GMT+0800) Asia/Choibalsan'), ('Asia/Colombo', '(GMT+0530) Asia/Colombo'), ('Asia/Damascus', '(GMT+0200) Asia/Damascus'), ('Asia/Dhaka', '(GMT+0600) Asia/Dhaka'), ('Asia/Dili', '(GMT+0900) Asia/Dili'), ('Asia/Dubai', '(GMT+0400) Asia/Dubai'), ('Asia/Dushanbe', '(GMT+0500) Asia/Dushanbe'), ('Asia/Famagusta', '(GMT+0200) Asia/Famagusta'), ('Asia/Gaza', '(GMT+0200) Asia/Gaza'), ('Asia/Hebron', '(GMT+0200) Asia/Hebron'), ('Asia/Ho_Chi_Minh', '(GMT+0700) Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', '(GMT+0800) Asia/Hong_Kong'), ('Asia/Hovd', '(GMT+0700) Asia/Hovd'), ('Asia/Irkutsk', '(GMT+0800) Asia/Irkutsk'), ('Asia/Jakarta', '(GMT+0700) Asia/Jakarta'), ('Asia/Jayapura', '(GMT+0900) Asia/Jayapura'), ('Asia/Jerusalem', '(GMT+0200) Asia/Jerusalem'), ('Asia/Kabul', '(GMT+0430) Asia/Kabul'), ('Asia/Kamchatka', '(GMT+1200) Asia/Kamchatka'), ('Asia/Karachi', '(GMT+0500) Asia/Karachi'), ('Asia/Kathmandu', '(GMT+0545) Asia/Kathmandu'), ('Asia/Khandyga', '(GMT+0900) Asia/Khandyga'), ('Asia/Kolkata', '(GMT+0530) Asia/Kolkata'), ('Asia/Krasnoyarsk', '(GMT+0700) Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', '(GMT+0800) Asia/Kuala_Lumpur'), ('Asia/Kuching', '(GMT+0800) Asia/Kuching'), ('Asia/Kuwait', '(GMT+0300) Asia/Kuwait'), ('Asia/Macau', '(GMT+0800) Asia/Macau'), ('Asia/Magadan', '(GMT+1100) Asia/Magadan'), ('Asia/Makassar', '(GMT+0800) Asia/Makassar'), ('Asia/Manila', '(GMT+0800) Asia/Manila'), ('Asia/Muscat', '(GMT+0400) Asia/Muscat'), ('Asia/Nicosia', '(GMT+0200) Asia/Nicosia'), ('Asia/Novokuznetsk', '(GMT+0700) Asia/Novokuznetsk'), ('Asia/Novosibirsk', '(GMT+0700) Asia/Novosibirsk'), ('Asia/Omsk', '(GMT+0600) Asia/Omsk'), ('Asia/Oral', '(GMT+0500) Asia/Oral'), ('Asia/Phnom_Penh', '(GMT+0700) Asia/Phnom_Penh'), ('Asia/Pontianak', '(GMT+0700) Asia/Pontianak'), ('Asia/Pyongyang', '(GMT+0900) Asia/Pyongyang'), ('Asia/Qatar', '(GMT+0300) Asia/Qatar'), ('Asia/Qostanay', '(GMT+0600) Asia/Qostanay'), ('Asia/Qyzylorda', '(GMT+0500) Asia/Qyzylorda'), ('Asia/Riyadh', '(GMT+0300) Asia/Riyadh'), ('Asia/Sakhalin', '(GMT+1100) Asia/Sakhalin'), ('Asia/Samarkand', '(GMT+0500) Asia/Samarkand'), ('Asia/Seoul', '(GMT+0900) Asia/Seoul'), ('Asia/Shanghai', '(GMT+0800) Asia/Shanghai'), ('Asia/Singapore', '(GMT+0800) Asia/Singapore'), ('Asia/Srednekolymsk', '(GMT+1100) Asia/Srednekolymsk'), ('Asia/Taipei', '(GMT+0800) Asia/Taipei'), ('Asia/Tashkent', '(GMT+0500) Asia/Tashkent'), ('Asia/Tbilisi', '(GMT+0400) Asia/Tbilisi'), ('Asia/Tehran', '(GMT+0330) Asia/Tehran'), ('Asia/Thimphu', '(GMT+0600) Asia/Thimphu'), ('Asia/Tokyo', '(GMT+0900) Asia/Tokyo'), ('Asia/Tomsk', '(GMT+0700) Asia/Tomsk'), ('Asia/Ulaanbaatar', '(GMT+0800) Asia/Ulaanbaatar'), ('Asia/Urumqi', '(GMT+0600) Asia/Urumqi'), ('Asia/Ust-Nera', '(GMT+1000) Asia/Ust-Nera'), ('Asia/Vientiane', '(GMT+0700) Asia/Vientiane'), ('Asia/Vladivostok', '(GMT+1000) Asia/Vladivostok'), ('Asia/Yakutsk', '(GMT+0900) Asia/Yakutsk'), ('Asia/Yangon', '(GMT+0630) Asia/Yangon'), ('Asia/Yekaterinburg', '(GMT+0500) Asia/Yekaterinburg'), ('Asia/Yerevan', '(GMT+0400) Asia/Yerevan'), ('Atlantic/Azores', '(GMT-0100) Atlantic/Azores'), ('Atlantic/Bermuda', '(GMT-0400) Atlantic/Bermuda'), ('Atlantic/Canary', '(GMT+0000) Atlantic/Canary'), ('Atlantic/Cape_Verde', '(GMT-0100) Atlantic/Cape_Verde'), ('Atlantic/Faroe', '(GMT+0000) Atlantic/Faroe'), ('Atlantic/Madeira', '(GMT+0000) Atlantic/Madeira'), ('Atlantic/Reykjavik', '(GMT+0000) Atlantic/Reykjavik'), ('Atlantic/South_Georgia', '(GMT-0200) Atlantic/South_Georgia'), ('Atlantic/St_Helena', '(GMT+0000) Atlantic/St_Helena'), ('Atlantic/Stanley', '(GMT-0300) Atlantic/Stanley'), ('Australia/Adelaide', '(GMT+1030) Australia/Adelaide'), ('Australia/Brisbane', '(GMT+1000) Australia/Brisbane'), ('Australia/Broken_Hill', '(GMT+1030) Australia/Broken_Hill'), ('Australia/Currie', '(GMT+1100) Australia/Currie'), ('Australia/Darwin', '(GMT+0930) Australia/Darwin'), ('Australia/Eucla', '(GMT+0845) Australia/Eucla'), ('Australia/Hobart', '(GMT+1100) Australia/Hobart'), ('Australia/Lindeman', '(GMT+1000) Australia/Lindeman'), ('Australia/Lord_Howe', '(GMT+1100) Australia/Lord_Howe'), ('Australia/Melbourne', '(GMT+1100) Australia/Melbourne'), ('Australia/Perth', '(GMT+0800) Australia/Perth'), ('Australia/Sydney', '(GMT+1100) Australia/Sydney'), ('Canada/Atlantic', '(GMT-0400) Canada/Atlantic'), ('Canada/Central', '(GMT-0600) Canada/Central'), ('Canada/Eastern', '(GMT-0500) Canada/Eastern'), ('Canada/Mountain', '(GMT-0700) Canada/Mountain'), ('Canada/Newfoundland', '(GMT-0330) Canada/Newfoundland'), ('Canada/Pacific', '(GMT-0800) Canada/Pacific'), ('Europe/Amsterdam', '(GMT+0100) Europe/Amsterdam'), ('Europe/Andorra', '(GMT+0100) Europe/Andorra'), ('Europe/Astrakhan', '(GMT+0400) Europe/Astrakhan'), ('Europe/Athens', '(GMT+0200) Europe/Athens'), ('Europe/Belgrade', '(GMT+0100) Europe/Belgrade'), ('Europe/Berlin', '(GMT+0100) Europe/Berlin'), ('Europe/Bratislava', '(GMT+0100) Europe/Bratislava'), ('Europe/Brussels', '(GMT+0100) Europe/Brussels'), ('Europe/Bucharest', '(GMT+0200) Europe/Bucharest'), ('Europe/Budapest', '(GMT+0100) Europe/Budapest'), ('Europe/Busingen', '(GMT+0100) Europe/Busingen'), ('Europe/Chisinau', '(GMT+0200) Europe/Chisinau'), ('Europe/Copenhagen', '(GMT+0100) Europe/Copenhagen'), ('Europe/Dublin', '(GMT+0000) Europe/Dublin'), ('Europe/Gibraltar', '(GMT+0100) Europe/Gibraltar'), ('Europe/Guernsey', '(GMT+0000) Europe/Guernsey'), ('Europe/Helsinki', '(GMT+0200) Europe/Helsinki'), ('Europe/Isle_of_Man', '(GMT+0000) Europe/Isle_of_Man'), ('Europe/Istanbul', '(GMT+0300) Europe/Istanbul'), ('Europe/Jersey', '(GMT+0000) Europe/Jersey'), ('Europe/Kaliningrad', '(GMT+0200) Europe/Kaliningrad'), ('Europe/Kiev', '(GMT+0200) Europe/Kiev'), ('Europe/Kirov', '(GMT+0300) Europe/Kirov'), ('Europe/Lisbon', '(GMT+0000) Europe/Lisbon'), ('Europe/Ljubljana', '(GMT+0100) Europe/Ljubljana'), ('Europe/London', '(GMT+0000) Europe/London'), ('Europe/Luxembourg', '(GMT+0100) Europe/Luxembourg'), ('Europe/Madrid', '(GMT+0100) Europe/Madrid'), ('Europe/Malta', '(GMT+0100) Europe/Malta'), ('Europe/Mariehamn', '(GMT+0200) Europe/Mariehamn'), ('Europe/Minsk', '(GMT+0300) Europe/Minsk'), ('Europe/Monaco', '(GMT+0100) Europe/Monaco'), ('Europe/Moscow', '(GMT+0300) Europe/Moscow'), ('Europe/Oslo', '(GMT+0100) Europe/Oslo'), ('Europe/Paris', '(GMT+0100) Europe/Paris'), ('Europe/Podgorica', '(GMT+0100) Europe/Podgorica'), ('Europe/Prague', '(GMT+0100) Europe/Prague'), ('Europe/Riga', '(GMT+0200) Europe/Riga'), ('Europe/Rome', '(GMT+0100) Europe/Rome'), ('Europe/Samara', '(GMT+0400) Europe/Samara'), ('Europe/San_Marino', '(GMT+0100) Europe/San_Marino'), ('Europe/Sarajevo', '(GMT+0100) Europe/Sarajevo'), ('Europe/Saratov', '(GMT+0400) Europe/Saratov'), ('Europe/Simferopol', '(GMT+0300) Europe/Simferopol'), ('Europe/Skopje', '(GMT+0100) Europe/Skopje'), ('Europe/Sofia', '(GMT+0200) Europe/Sofia'), ('Europe/Stockholm', '(GMT+0100) Europe/Stockholm'), ('Europe/Tallinn', '(GMT+0200) Europe/Tallinn'), ('Europe/Tirane', '(GMT+0100) Europe/Tirane'), ('Europe/Ulyanovsk', '(GMT+0400) Europe/Ulyanovsk'), ('Europe/Uzhgorod', '(GMT+0200) Europe/Uzhgorod'), ('Europe/Vaduz', '(GMT+0100) Europe/Vaduz'), ('Europe/Vatican', '(GMT+0100) Europe/Vatican'), ('Europe/Vienna', '(GMT+0100) Europe/Vienna'), ('Europe/Vilnius', '(GMT+0200) Europe/Vilnius'), ('Europe/Volgograd', '(GMT+0400) Europe/Volgograd'), ('Europe/Warsaw', '(GMT+0100) Europe/Warsaw'), ('Europe/Zagreb', '(GMT+0100) Europe/Zagreb'), ('Europe/Zaporozhye', '(GMT+0200) Europe/Zaporozhye'), ('Europe/Zurich', '(GMT+0100) Europe/Zurich'), ('GMT', '(GMT+0000) GMT'), ('Indian/Antananarivo', '(GMT+0300) Indian/Antananarivo'), ('Indian/Chagos', '(GMT+0600) Indian/Chagos'), ('Indian/Christmas', '(GMT+0700) Indian/Christmas'), ('Indian/Cocos', '(GMT+0630) Indian/Cocos'), ('Indian/Comoro', '(GMT+0300) Indian/Comoro'), ('Indian/Kerguelen', '(GMT+0500) Indian/Kerguelen'), ('Indian/Mahe', '(GMT+0400) Indian/Mahe'), ('Indian/Maldives', '(GMT+0500) Indian/Maldives'), ('Indian/Mauritius', '(GMT+0400) Indian/Mauritius'), ('Indian/Mayotte', '(GMT+0300) Indian/Mayotte'), ('Indian/Reunion', '(GMT+0400) Indian/Reunion'), ('Pacific/Apia', '(GMT+1400) Pacific/Apia'), ('Pacific/Auckland', '(GMT+1300) Pacific/Auckland'), ('Pacific/Bougainville', '(GMT+1100) Pacific/Bougainville'), ('Pacific/Chatham', '(GMT+1345) Pacific/Chatham'), ('Pacific/Chuuk', '(GMT+1000) Pacific/Chuuk'), ('Pacific/Easter', '(GMT-0500) Pacific/Easter'), ('Pacific/Efate', '(GMT+1100) Pacific/Efate'), ('Pacific/Enderbury', '(GMT+1300) Pacific/Enderbury'), ('Pacific/Fakaofo', '(GMT+1300) Pacific/Fakaofo'), ('Pacific/Fiji', '(GMT+1200) Pacific/Fiji'), ('Pacific/Funafuti', '(GMT+1200) Pacific/Funafuti'), ('Pacific/Galapagos', '(GMT-0600) Pacific/Galapagos'), ('Pacific/Gambier', '(GMT-0900) Pacific/Gambier'), ('Pacific/Guadalcanal', '(GMT+1100) Pacific/Guadalcanal'), ('Pacific/Guam', '(GMT+1000) Pacific/Guam'), ('Pacific/Honolulu', '(GMT-1000) Pacific/Honolulu'), ('Pacific/Kiritimati', '(GMT+1400) Pacific/Kiritimati'), ('Pacific/Kosrae', '(GMT+1100) Pacific/Kosrae'), ('Pacific/Kwajalein', '(GMT+1200) Pacific/Kwajalein'), ('Pacific/Majuro', '(GMT+1200) Pacific/Majuro'), ('Pacific/Marquesas', '(GMT-0930) Pacific/Marquesas'), ('Pacific/Midway', '(GMT-1100) Pacific/Midway'), ('Pacific/Nauru', '(GMT+1200) Pacific/Nauru'), ('Pacific/Niue', '(GMT-1100) Pacific/Niue'), ('Pacific/Norfolk', '(GMT+1200) Pacific/Norfolk'), ('Pacific/Noumea', '(GMT+1100) Pacific/Noumea'), ('Pacific/Pago_Pago', '(GMT-1100) Pacific/Pago_Pago'), ('Pacific/Palau', '(GMT+0900) Pacific/Palau'), ('Pacific/Pitcairn', '(GMT-0800) Pacific/Pitcairn'), ('Pacific/Pohnpei', '(GMT+1100) Pacific/Pohnpei'), ('Pacific/Port_Moresby', '(GMT+1000) Pacific/Port_Moresby'), ('Pacific/Rarotonga', '(GMT-1000) Pacific/Rarotonga'), ('Pacific/Saipan', '(GMT+1000) Pacific/Saipan'), ('Pacific/Tahiti', '(GMT-1000) Pacific/Tahiti'), ('Pacific/Tarawa', '(GMT+1200) Pacific/Tarawa'), ('Pacific/Tongatapu', '(GMT+1300) Pacific/Tongatapu'), ('Pacific/Wake', '(GMT+1200) Pacific/Wake'), ('Pacific/Wallis', '(GMT+1200) Pacific/Wallis'), ('US/Alaska', '(GMT-0900) US/Alaska'), ('US/Arizona', '(GMT-0700) US/Arizona'), ('US/Central', '(GMT-0600) US/Central'), ('US/Eastern', '(GMT-0500) US/Eastern'), ('US/Hawaii', '(GMT-1000) US/Hawaii'), ('US/Mountain', '(GMT-0700) US/Mountain'), ('US/Pacific', '(GMT-0800) US/Pacific'), ('UTC', '(GMT+0000) UTC')], default='America/New_York', max_length=100), + model_name="profile", + name="timezone", + field=vendor.timezones.fields.TimeZoneField( + choices=[ + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"), + ("Africa/Ceuta", "(GMT+0100) Africa/Ceuta"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Dar_es_Salaam", "(GMT+0300) Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0300) Africa/Juba"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("America/Adak", "(GMT-1000) America/Adak"), + ("America/Anchorage", "(GMT-0900) America/Anchorage"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ("America/Argentina/Buenos_Aires", "(GMT-0300) America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "(GMT-0300) America/Argentina/Catamarca"), + ("America/Argentina/Cordoba", "(GMT-0300) America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "(GMT-0300) America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "(GMT-0300) America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "(GMT-0300) America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "(GMT-0300) America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "(GMT-0300) America/Argentina/Salta"), + ("America/Argentina/San_Juan", "(GMT-0300) America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "(GMT-0300) America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "(GMT-0300) America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "(GMT-0300) America/Argentina/Ushuaia"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0300) America/Asuncion"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Bahia_Banderas", "(GMT-0600) America/Bahia_Banderas"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Blanc-Sablon", "(GMT-0400) America/Blanc-Sablon"), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Boise", "(GMT-0700) America/Boise"), + ("America/Cambridge_Bay", "(GMT-0700) America/Cambridge_Bay"), + ("America/Campo_Grande", "(GMT-0400) America/Campo_Grande"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0600) America/Chicago"), + ("America/Chihuahua", "(GMT-0700) America/Chihuahua"), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Danmarkshavn", "(GMT+0000) America/Danmarkshavn"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ("America/Dawson_Creek", "(GMT-0700) America/Dawson_Creek"), + ("America/Denver", "(GMT-0700) America/Denver"), + ("America/Detroit", "(GMT-0500) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Edmonton", "(GMT-0700) America/Edmonton"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0400) America/Glace_Bay"), + ("America/Goose_Bay", "(GMT-0400) America/Goose_Bay"), + ("America/Grand_Turk", "(GMT-0500) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Halifax", "(GMT-0400) America/Halifax"), + ("America/Havana", "(GMT-0500) America/Havana"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Indiana/Indianapolis", "(GMT-0500) America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "(GMT-0600) America/Indiana/Knox"), + ("America/Indiana/Marengo", "(GMT-0500) America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "(GMT-0500) America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "(GMT-0600) America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "(GMT-0500) America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "(GMT-0500) America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "(GMT-0500) America/Indiana/Winamac"), + ("America/Inuvik", "(GMT-0700) America/Inuvik"), + ("America/Iqaluit", "(GMT-0500) America/Iqaluit"), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Juneau", "(GMT-0900) America/Juneau"), + ("America/Kentucky/Louisville", "(GMT-0500) America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "(GMT-0500) America/Kentucky/Monticello"), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Los_Angeles", "(GMT-0800) America/Los_Angeles"), + ("America/Lower_Princes", "(GMT-0400) America/Lower_Princes"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Matamoros", "(GMT-0600) America/Matamoros"), + ("America/Mazatlan", "(GMT-0700) America/Mazatlan"), + ("America/Menominee", "(GMT-0600) America/Menominee"), + ("America/Merida", "(GMT-0600) America/Merida"), + ("America/Metlakatla", "(GMT-0900) America/Metlakatla"), + ("America/Mexico_City", "(GMT-0600) America/Mexico_City"), + ("America/Miquelon", "(GMT-0300) America/Miquelon"), + ("America/Moncton", "(GMT-0400) America/Moncton"), + ("America/Monterrey", "(GMT-0600) America/Monterrey"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0500) America/Nassau"), + ("America/New_York", "(GMT-0500) America/New_York"), + ("America/Nipigon", "(GMT-0500) America/Nipigon"), + ("America/Nome", "(GMT-0900) America/Nome"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/North_Dakota/Beulah", "(GMT-0600) America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "(GMT-0600) America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "(GMT-0600) America/North_Dakota/New_Salem"), + ("America/Nuuk", "(GMT-0300) America/Nuuk"), + ("America/Ojinaga", "(GMT-0700) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ("America/Pangnirtung", "(GMT-0500) America/Pangnirtung"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Port-au-Prince", "(GMT-0500) America/Port-au-Prince"), + ("America/Port_of_Spain", "(GMT-0400) America/Port_of_Spain"), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Punta_Arenas", "(GMT-0300) America/Punta_Arenas"), + ("America/Rainy_River", "(GMT-0600) America/Rainy_River"), + ("America/Rankin_Inlet", "(GMT-0600) America/Rankin_Inlet"), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Regina", "(GMT-0600) America/Regina"), + ("America/Resolute", "(GMT-0600) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Santiago", "(GMT-0300) America/Santiago"), + ("America/Santo_Domingo", "(GMT-0400) America/Santo_Domingo"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Scoresbysund", "(GMT-0100) America/Scoresbysund"), + ("America/Sitka", "(GMT-0900) America/Sitka"), + ("America/St_Barthelemy", "(GMT-0400) America/St_Barthelemy"), + ("America/St_Johns", "(GMT-0330) America/St_Johns"), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Swift_Current", "(GMT-0600) America/Swift_Current"), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Thule", "(GMT-0400) America/Thule"), + ("America/Thunder_Bay", "(GMT-0500) America/Thunder_Bay"), + ("America/Tijuana", "(GMT-0800) America/Tijuana"), + ("America/Toronto", "(GMT-0500) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("America/Vancouver", "(GMT-0800) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("America/Winnipeg", "(GMT-0600) America/Winnipeg"), + ("America/Yakutat", "(GMT-0900) America/Yakutat"), + ("America/Yellowknife", "(GMT-0700) America/Yellowknife"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Antarctica/DumontDUrville", "(GMT+1000) Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "(GMT+1100) Antarctica/Macquarie"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Antarctica/Troll", "(GMT+0000) Antarctica/Troll"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Arctic/Longyearbyen", "(GMT+0100) Arctic/Longyearbyen"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Amman", "(GMT+0200) Asia/Amman"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Beirut", "(GMT+0200) Asia/Beirut"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Damascus", "(GMT+0200) Asia/Damascus"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Famagusta", "(GMT+0200) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0200) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0200) Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Jerusalem", "(GMT+0200) Asia/Jerusalem"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Nicosia", "(GMT+0200) Asia/Nicosia"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"), + ("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Tehran", "(GMT+0330) Asia/Tehran"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Atlantic/Azores", "(GMT-0100) Atlantic/Azores"), + ("Atlantic/Bermuda", "(GMT-0400) Atlantic/Bermuda"), + ("Atlantic/Canary", "(GMT+0000) Atlantic/Canary"), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Atlantic/Faroe", "(GMT+0000) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0000) Atlantic/Madeira"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "(GMT-0200) Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Australia/Adelaide", "(GMT+1030) Australia/Adelaide"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Broken_Hill", "(GMT+1030) Australia/Broken_Hill"), + ("Australia/Currie", "(GMT+1100) Australia/Currie"), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Australia/Hobart", "(GMT+1100) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"), + ("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Sydney", "(GMT+1100) Australia/Sydney"), + ("Canada/Atlantic", "(GMT-0400) Canada/Atlantic"), + ("Canada/Central", "(GMT-0600) Canada/Central"), + ("Canada/Eastern", "(GMT-0500) Canada/Eastern"), + ("Canada/Mountain", "(GMT-0700) Canada/Mountain"), + ("Canada/Newfoundland", "(GMT-0330) Canada/Newfoundland"), + ("Canada/Pacific", "(GMT-0800) Canada/Pacific"), + ("Europe/Amsterdam", "(GMT+0100) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0100) Europe/Andorra"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Athens", "(GMT+0200) Europe/Athens"), + ("Europe/Belgrade", "(GMT+0100) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0100) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0100) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0100) Europe/Brussels"), + ("Europe/Bucharest", "(GMT+0200) Europe/Bucharest"), + ("Europe/Budapest", "(GMT+0100) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0100) Europe/Busingen"), + ("Europe/Chisinau", "(GMT+0200) Europe/Chisinau"), + ("Europe/Copenhagen", "(GMT+0100) Europe/Copenhagen"), + ("Europe/Dublin", "(GMT+0000) Europe/Dublin"), + ("Europe/Gibraltar", "(GMT+0100) Europe/Gibraltar"), + ("Europe/Guernsey", "(GMT+0000) Europe/Guernsey"), + ("Europe/Helsinki", "(GMT+0200) Europe/Helsinki"), + ("Europe/Isle_of_Man", "(GMT+0000) Europe/Isle_of_Man"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Jersey", "(GMT+0000) Europe/Jersey"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Kiev", "(GMT+0200) Europe/Kiev"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Lisbon", "(GMT+0000) Europe/Lisbon"), + ("Europe/Ljubljana", "(GMT+0100) Europe/Ljubljana"), + ("Europe/London", "(GMT+0000) Europe/London"), + ("Europe/Luxembourg", "(GMT+0100) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0100) Europe/Madrid"), + ("Europe/Malta", "(GMT+0100) Europe/Malta"), + ("Europe/Mariehamn", "(GMT+0200) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Monaco", "(GMT+0100) Europe/Monaco"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Oslo", "(GMT+0100) Europe/Oslo"), + ("Europe/Paris", "(GMT+0100) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0100) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0100) Europe/Prague"), + ("Europe/Riga", "(GMT+0200) Europe/Riga"), + ("Europe/Rome", "(GMT+0100) Europe/Rome"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/San_Marino", "(GMT+0100) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0100) Europe/Sarajevo"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Skopje", "(GMT+0100) Europe/Skopje"), + ("Europe/Sofia", "(GMT+0200) Europe/Sofia"), + ("Europe/Stockholm", "(GMT+0100) Europe/Stockholm"), + ("Europe/Tallinn", "(GMT+0200) Europe/Tallinn"), + ("Europe/Tirane", "(GMT+0100) Europe/Tirane"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "(GMT+0200) Europe/Uzhgorod"), + ("Europe/Vaduz", "(GMT+0100) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0100) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0100) Europe/Vienna"), + ("Europe/Vilnius", "(GMT+0200) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0400) Europe/Volgograd"), + ("Europe/Warsaw", "(GMT+0100) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0100) Europe/Zagreb"), + ("Europe/Zaporozhye", "(GMT+0200) Europe/Zaporozhye"), + ("Europe/Zurich", "(GMT+0100) Europe/Zurich"), + ("GMT", "(GMT+0000) GMT"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Pacific/Apia", "(GMT+1400) Pacific/Apia"), + ("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"), + ("Pacific/Bougainville", "(GMT+1100) Pacific/Bougainville"), + ("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Easter", "(GMT-0500) Pacific/Easter"), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Enderbury", "(GMT+1300) Pacific/Enderbury"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Norfolk", "(GMT+1200) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Pacific/Port_Moresby", "(GMT+1000) Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("US/Alaska", "(GMT-0900) US/Alaska"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Central", "(GMT-0600) US/Central"), + ("US/Eastern", "(GMT-0500) US/Eastern"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("US/Mountain", "(GMT-0700) US/Mountain"), + ("US/Pacific", "(GMT-0800) US/Pacific"), + ("UTC", "(GMT+0000) UTC"), + ], + default="America/New_York", + max_length=100, + ), ), ] diff --git a/apps/profile/migrations/0008_profile_paypal_sub_id.py b/apps/profile/migrations/0008_profile_paypal_sub_id.py index 1b700d17c4..a974b2cc92 100644 --- a/apps/profile/migrations/0008_profile_paypal_sub_id.py +++ b/apps/profile/migrations/0008_profile_paypal_sub_id.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('profile', '0007_auto_20220125_2108'), + ("profile", "0007_auto_20220125_2108"), ] operations = [ migrations.AddField( - model_name='profile', - name='paypal_sub_id', + model_name="profile", + name="paypal_sub_id", field=models.CharField(blank=True, max_length=24, null=True), ), ] diff --git a/apps/profile/migrations/0009_paypalids.py b/apps/profile/migrations/0009_paypalids.py index c181c77b33..a27cb1b3b9 100644 --- a/apps/profile/migrations/0009_paypalids.py +++ b/apps/profile/migrations/0009_paypalids.py @@ -1,24 +1,34 @@ # Generated by Django 3.1.10 on 2022-02-08 23:15 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('profile', '0008_profile_paypal_sub_id'), + ("profile", "0008_profile_paypal_sub_id"), ] operations = [ migrations.CreateModel( - name='PaypalIds', + name="PaypalIds", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('paypal_sub_id', models.CharField(blank=True, max_length=24, null=True)), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='paypal_ids', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("paypal_sub_id", models.CharField(blank=True, max_length=24, null=True)), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="paypal_ids", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/apps/profile/migrations/0010_profile_active_provider.py b/apps/profile/migrations/0010_profile_active_provider.py index 53c49773b4..c880e37a90 100644 --- a/apps/profile/migrations/0010_profile_active_provider.py +++ b/apps/profile/migrations/0010_profile_active_provider.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('profile', '0009_paypalids'), + ("profile", "0009_paypalids"), ] operations = [ migrations.AddField( - model_name='profile', - name='active_provider', + model_name="profile", + name="active_provider", field=models.CharField(blank=True, max_length=24, null=True), ), ] diff --git a/apps/profile/migrations/0011_auto_20220408_1908.py b/apps/profile/migrations/0011_auto_20220408_1908.py index 76f8bc6b12..91bcf90186 100644 --- a/apps/profile/migrations/0011_auto_20220408_1908.py +++ b/apps/profile/migrations/0011_auto_20220408_1908.py @@ -1,29 +1,474 @@ # Generated by Django 3.1.10 on 2022-04-08 19:08 from django.db import migrations, models + import vendor.timezones.fields class Migration(migrations.Migration): - dependencies = [ - ('profile', '0010_profile_active_provider'), + ("profile", "0010_profile_active_provider"), ] operations = [ migrations.AddField( - model_name='paymenthistory', - name='refunded', + model_name="paymenthistory", + name="refunded", field=models.BooleanField(blank=True, null=True), ), migrations.AlterField( - model_name='profile', - name='feed_pane_size', + model_name="profile", + name="feed_pane_size", field=models.IntegerField(default=282), ), migrations.AlterField( - model_name='profile', - name='timezone', - field=vendor.timezones.fields.TimeZoneField(choices=[('Africa/Abidjan', '(GMT+0000) Africa/Abidjan'), ('Africa/Accra', '(GMT+0000) Africa/Accra'), ('Africa/Addis_Ababa', '(GMT+0300) Africa/Addis_Ababa'), ('Africa/Algiers', '(GMT+0100) Africa/Algiers'), ('Africa/Asmara', '(GMT+0300) Africa/Asmara'), ('Africa/Bamako', '(GMT+0000) Africa/Bamako'), ('Africa/Bangui', '(GMT+0100) Africa/Bangui'), ('Africa/Banjul', '(GMT+0000) Africa/Banjul'), ('Africa/Bissau', '(GMT+0000) Africa/Bissau'), ('Africa/Blantyre', '(GMT+0200) Africa/Blantyre'), ('Africa/Brazzaville', '(GMT+0100) Africa/Brazzaville'), ('Africa/Bujumbura', '(GMT+0200) Africa/Bujumbura'), ('Africa/Cairo', '(GMT+0200) Africa/Cairo'), ('Africa/Casablanca', '(GMT+0000) Africa/Casablanca'), ('Africa/Ceuta', '(GMT+0200) Africa/Ceuta'), ('Africa/Conakry', '(GMT+0000) Africa/Conakry'), ('Africa/Dakar', '(GMT+0000) Africa/Dakar'), ('Africa/Dar_es_Salaam', '(GMT+0300) Africa/Dar_es_Salaam'), ('Africa/Djibouti', '(GMT+0300) Africa/Djibouti'), ('Africa/Douala', '(GMT+0100) Africa/Douala'), ('Africa/El_Aaiun', '(GMT+0000) Africa/El_Aaiun'), ('Africa/Freetown', '(GMT+0000) Africa/Freetown'), ('Africa/Gaborone', '(GMT+0200) Africa/Gaborone'), ('Africa/Harare', '(GMT+0200) Africa/Harare'), ('Africa/Johannesburg', '(GMT+0200) Africa/Johannesburg'), ('Africa/Juba', '(GMT+0300) Africa/Juba'), ('Africa/Kampala', '(GMT+0300) Africa/Kampala'), ('Africa/Khartoum', '(GMT+0200) Africa/Khartoum'), ('Africa/Kigali', '(GMT+0200) Africa/Kigali'), ('Africa/Kinshasa', '(GMT+0100) Africa/Kinshasa'), ('Africa/Lagos', '(GMT+0100) Africa/Lagos'), ('Africa/Libreville', '(GMT+0100) Africa/Libreville'), ('Africa/Lome', '(GMT+0000) Africa/Lome'), ('Africa/Luanda', '(GMT+0100) Africa/Luanda'), ('Africa/Lubumbashi', '(GMT+0200) Africa/Lubumbashi'), ('Africa/Lusaka', '(GMT+0200) Africa/Lusaka'), ('Africa/Malabo', '(GMT+0100) Africa/Malabo'), ('Africa/Maputo', '(GMT+0200) Africa/Maputo'), ('Africa/Maseru', '(GMT+0200) Africa/Maseru'), ('Africa/Mbabane', '(GMT+0200) Africa/Mbabane'), ('Africa/Mogadishu', '(GMT+0300) Africa/Mogadishu'), ('Africa/Monrovia', '(GMT+0000) Africa/Monrovia'), ('Africa/Nairobi', '(GMT+0300) Africa/Nairobi'), ('Africa/Ndjamena', '(GMT+0100) Africa/Ndjamena'), ('Africa/Niamey', '(GMT+0100) Africa/Niamey'), ('Africa/Nouakchott', '(GMT+0000) Africa/Nouakchott'), ('Africa/Ouagadougou', '(GMT+0000) Africa/Ouagadougou'), ('Africa/Porto-Novo', '(GMT+0100) Africa/Porto-Novo'), ('Africa/Sao_Tome', '(GMT+0000) Africa/Sao_Tome'), ('Africa/Tripoli', '(GMT+0200) Africa/Tripoli'), ('Africa/Tunis', '(GMT+0100) Africa/Tunis'), ('Africa/Windhoek', '(GMT+0200) Africa/Windhoek'), ('America/Adak', '(GMT-0900) America/Adak'), ('America/Anchorage', '(GMT-0800) America/Anchorage'), ('America/Anguilla', '(GMT-0400) America/Anguilla'), ('America/Antigua', '(GMT-0400) America/Antigua'), ('America/Araguaina', '(GMT-0300) America/Araguaina'), ('America/Argentina/Buenos_Aires', '(GMT-0300) America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', '(GMT-0300) America/Argentina/Catamarca'), ('America/Argentina/Cordoba', '(GMT-0300) America/Argentina/Cordoba'), ('America/Argentina/Jujuy', '(GMT-0300) America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', '(GMT-0300) America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', '(GMT-0300) America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', '(GMT-0300) America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', '(GMT-0300) America/Argentina/Salta'), ('America/Argentina/San_Juan', '(GMT-0300) America/Argentina/San_Juan'), ('America/Argentina/San_Luis', '(GMT-0300) America/Argentina/San_Luis'), ('America/Argentina/Tucuman', '(GMT-0300) America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', '(GMT-0300) America/Argentina/Ushuaia'), ('America/Aruba', '(GMT-0400) America/Aruba'), ('America/Asuncion', '(GMT-0400) America/Asuncion'), ('America/Atikokan', '(GMT-0500) America/Atikokan'), ('America/Bahia', '(GMT-0300) America/Bahia'), ('America/Bahia_Banderas', '(GMT-0500) America/Bahia_Banderas'), ('America/Barbados', '(GMT-0400) America/Barbados'), ('America/Belem', '(GMT-0300) America/Belem'), ('America/Belize', '(GMT-0600) America/Belize'), ('America/Blanc-Sablon', '(GMT-0400) America/Blanc-Sablon'), ('America/Boa_Vista', '(GMT-0400) America/Boa_Vista'), ('America/Bogota', '(GMT-0500) America/Bogota'), ('America/Boise', '(GMT-0600) America/Boise'), ('America/Cambridge_Bay', '(GMT-0600) America/Cambridge_Bay'), ('America/Campo_Grande', '(GMT-0400) America/Campo_Grande'), ('America/Cancun', '(GMT-0500) America/Cancun'), ('America/Caracas', '(GMT-0400) America/Caracas'), ('America/Cayenne', '(GMT-0300) America/Cayenne'), ('America/Cayman', '(GMT-0500) America/Cayman'), ('America/Chicago', '(GMT-0500) America/Chicago'), ('America/Chihuahua', '(GMT-0600) America/Chihuahua'), ('America/Costa_Rica', '(GMT-0600) America/Costa_Rica'), ('America/Creston', '(GMT-0700) America/Creston'), ('America/Cuiaba', '(GMT-0400) America/Cuiaba'), ('America/Curacao', '(GMT-0400) America/Curacao'), ('America/Danmarkshavn', '(GMT+0000) America/Danmarkshavn'), ('America/Dawson', '(GMT-0700) America/Dawson'), ('America/Dawson_Creek', '(GMT-0700) America/Dawson_Creek'), ('America/Denver', '(GMT-0600) America/Denver'), ('America/Detroit', '(GMT-0400) America/Detroit'), ('America/Dominica', '(GMT-0400) America/Dominica'), ('America/Edmonton', '(GMT-0600) America/Edmonton'), ('America/Eirunepe', '(GMT-0500) America/Eirunepe'), ('America/El_Salvador', '(GMT-0600) America/El_Salvador'), ('America/Fort_Nelson', '(GMT-0700) America/Fort_Nelson'), ('America/Fortaleza', '(GMT-0300) America/Fortaleza'), ('America/Glace_Bay', '(GMT-0300) America/Glace_Bay'), ('America/Goose_Bay', '(GMT-0300) America/Goose_Bay'), ('America/Grand_Turk', '(GMT-0400) America/Grand_Turk'), ('America/Grenada', '(GMT-0400) America/Grenada'), ('America/Guadeloupe', '(GMT-0400) America/Guadeloupe'), ('America/Guatemala', '(GMT-0600) America/Guatemala'), ('America/Guayaquil', '(GMT-0500) America/Guayaquil'), ('America/Guyana', '(GMT-0400) America/Guyana'), ('America/Halifax', '(GMT-0300) America/Halifax'), ('America/Havana', '(GMT-0400) America/Havana'), ('America/Hermosillo', '(GMT-0700) America/Hermosillo'), ('America/Indiana/Indianapolis', '(GMT-0400) America/Indiana/Indianapolis'), ('America/Indiana/Knox', '(GMT-0500) America/Indiana/Knox'), ('America/Indiana/Marengo', '(GMT-0400) America/Indiana/Marengo'), ('America/Indiana/Petersburg', '(GMT-0400) America/Indiana/Petersburg'), ('America/Indiana/Tell_City', '(GMT-0500) America/Indiana/Tell_City'), ('America/Indiana/Vevay', '(GMT-0400) America/Indiana/Vevay'), ('America/Indiana/Vincennes', '(GMT-0400) America/Indiana/Vincennes'), ('America/Indiana/Winamac', '(GMT-0400) America/Indiana/Winamac'), ('America/Inuvik', '(GMT-0600) America/Inuvik'), ('America/Iqaluit', '(GMT-0400) America/Iqaluit'), ('America/Jamaica', '(GMT-0500) America/Jamaica'), ('America/Juneau', '(GMT-0800) America/Juneau'), ('America/Kentucky/Louisville', '(GMT-0400) America/Kentucky/Louisville'), ('America/Kentucky/Monticello', '(GMT-0400) America/Kentucky/Monticello'), ('America/Kralendijk', '(GMT-0400) America/Kralendijk'), ('America/La_Paz', '(GMT-0400) America/La_Paz'), ('America/Lima', '(GMT-0500) America/Lima'), ('America/Los_Angeles', '(GMT-0700) America/Los_Angeles'), ('America/Lower_Princes', '(GMT-0400) America/Lower_Princes'), ('America/Maceio', '(GMT-0300) America/Maceio'), ('America/Managua', '(GMT-0600) America/Managua'), ('America/Manaus', '(GMT-0400) America/Manaus'), ('America/Marigot', '(GMT-0400) America/Marigot'), ('America/Martinique', '(GMT-0400) America/Martinique'), ('America/Matamoros', '(GMT-0500) America/Matamoros'), ('America/Mazatlan', '(GMT-0600) America/Mazatlan'), ('America/Menominee', '(GMT-0500) America/Menominee'), ('America/Merida', '(GMT-0500) America/Merida'), ('America/Metlakatla', '(GMT-0800) America/Metlakatla'), ('America/Mexico_City', '(GMT-0500) America/Mexico_City'), ('America/Miquelon', '(GMT-0200) America/Miquelon'), ('America/Moncton', '(GMT-0300) America/Moncton'), ('America/Monterrey', '(GMT-0500) America/Monterrey'), ('America/Montevideo', '(GMT-0300) America/Montevideo'), ('America/Montserrat', '(GMT-0400) America/Montserrat'), ('America/Nassau', '(GMT-0400) America/Nassau'), ('America/New_York', '(GMT-0400) America/New_York'), ('America/Nipigon', '(GMT-0400) America/Nipigon'), ('America/Nome', '(GMT-0800) America/Nome'), ('America/Noronha', '(GMT-0200) America/Noronha'), ('America/North_Dakota/Beulah', '(GMT-0500) America/North_Dakota/Beulah'), ('America/North_Dakota/Center', '(GMT-0500) America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', '(GMT-0500) America/North_Dakota/New_Salem'), ('America/Nuuk', '(GMT-0200) America/Nuuk'), ('America/Ojinaga', '(GMT-0600) America/Ojinaga'), ('America/Panama', '(GMT-0500) America/Panama'), ('America/Pangnirtung', '(GMT-0400) America/Pangnirtung'), ('America/Paramaribo', '(GMT-0300) America/Paramaribo'), ('America/Phoenix', '(GMT-0700) America/Phoenix'), ('America/Port-au-Prince', '(GMT-0400) America/Port-au-Prince'), ('America/Port_of_Spain', '(GMT-0400) America/Port_of_Spain'), ('America/Porto_Velho', '(GMT-0400) America/Porto_Velho'), ('America/Puerto_Rico', '(GMT-0400) America/Puerto_Rico'), ('America/Punta_Arenas', '(GMT-0300) America/Punta_Arenas'), ('America/Rainy_River', '(GMT-0500) America/Rainy_River'), ('America/Rankin_Inlet', '(GMT-0500) America/Rankin_Inlet'), ('America/Recife', '(GMT-0300) America/Recife'), ('America/Regina', '(GMT-0600) America/Regina'), ('America/Resolute', '(GMT-0500) America/Resolute'), ('America/Rio_Branco', '(GMT-0500) America/Rio_Branco'), ('America/Santarem', '(GMT-0300) America/Santarem'), ('America/Santiago', '(GMT-0400) America/Santiago'), ('America/Santo_Domingo', '(GMT-0400) America/Santo_Domingo'), ('America/Sao_Paulo', '(GMT-0300) America/Sao_Paulo'), ('America/Scoresbysund', '(GMT+0000) America/Scoresbysund'), ('America/Sitka', '(GMT-0800) America/Sitka'), ('America/St_Barthelemy', '(GMT-0400) America/St_Barthelemy'), ('America/St_Johns', '(GMT-0230) America/St_Johns'), ('America/St_Kitts', '(GMT-0400) America/St_Kitts'), ('America/St_Lucia', '(GMT-0400) America/St_Lucia'), ('America/St_Thomas', '(GMT-0400) America/St_Thomas'), ('America/St_Vincent', '(GMT-0400) America/St_Vincent'), ('America/Swift_Current', '(GMT-0600) America/Swift_Current'), ('America/Tegucigalpa', '(GMT-0600) America/Tegucigalpa'), ('America/Thule', '(GMT-0300) America/Thule'), ('America/Thunder_Bay', '(GMT-0400) America/Thunder_Bay'), ('America/Tijuana', '(GMT-0700) America/Tijuana'), ('America/Toronto', '(GMT-0400) America/Toronto'), ('America/Tortola', '(GMT-0400) America/Tortola'), ('America/Vancouver', '(GMT-0700) America/Vancouver'), ('America/Whitehorse', '(GMT-0700) America/Whitehorse'), ('America/Winnipeg', '(GMT-0500) America/Winnipeg'), ('America/Yakutat', '(GMT-0800) America/Yakutat'), ('America/Yellowknife', '(GMT-0600) America/Yellowknife'), ('Antarctica/Casey', '(GMT+1100) Antarctica/Casey'), ('Antarctica/Davis', '(GMT+0700) Antarctica/Davis'), ('Antarctica/DumontDUrville', '(GMT+1000) Antarctica/DumontDUrville'), ('Antarctica/Macquarie', '(GMT+1000) Antarctica/Macquarie'), ('Antarctica/Mawson', '(GMT+0500) Antarctica/Mawson'), ('Antarctica/McMurdo', '(GMT+1200) Antarctica/McMurdo'), ('Antarctica/Palmer', '(GMT-0300) Antarctica/Palmer'), ('Antarctica/Rothera', '(GMT-0300) Antarctica/Rothera'), ('Antarctica/Syowa', '(GMT+0300) Antarctica/Syowa'), ('Antarctica/Troll', '(GMT+0200) Antarctica/Troll'), ('Antarctica/Vostok', '(GMT+0600) Antarctica/Vostok'), ('Arctic/Longyearbyen', '(GMT+0200) Arctic/Longyearbyen'), ('Asia/Aden', '(GMT+0300) Asia/Aden'), ('Asia/Almaty', '(GMT+0600) Asia/Almaty'), ('Asia/Amman', '(GMT+0300) Asia/Amman'), ('Asia/Anadyr', '(GMT+1200) Asia/Anadyr'), ('Asia/Aqtau', '(GMT+0500) Asia/Aqtau'), ('Asia/Aqtobe', '(GMT+0500) Asia/Aqtobe'), ('Asia/Ashgabat', '(GMT+0500) Asia/Ashgabat'), ('Asia/Atyrau', '(GMT+0500) Asia/Atyrau'), ('Asia/Baghdad', '(GMT+0300) Asia/Baghdad'), ('Asia/Bahrain', '(GMT+0300) Asia/Bahrain'), ('Asia/Baku', '(GMT+0400) Asia/Baku'), ('Asia/Bangkok', '(GMT+0700) Asia/Bangkok'), ('Asia/Barnaul', '(GMT+0700) Asia/Barnaul'), ('Asia/Beirut', '(GMT+0300) Asia/Beirut'), ('Asia/Bishkek', '(GMT+0600) Asia/Bishkek'), ('Asia/Brunei', '(GMT+0800) Asia/Brunei'), ('Asia/Chita', '(GMT+0900) Asia/Chita'), ('Asia/Choibalsan', '(GMT+0800) Asia/Choibalsan'), ('Asia/Colombo', '(GMT+0530) Asia/Colombo'), ('Asia/Damascus', '(GMT+0300) Asia/Damascus'), ('Asia/Dhaka', '(GMT+0600) Asia/Dhaka'), ('Asia/Dili', '(GMT+0900) Asia/Dili'), ('Asia/Dubai', '(GMT+0400) Asia/Dubai'), ('Asia/Dushanbe', '(GMT+0500) Asia/Dushanbe'), ('Asia/Famagusta', '(GMT+0300) Asia/Famagusta'), ('Asia/Gaza', '(GMT+0300) Asia/Gaza'), ('Asia/Hebron', '(GMT+0300) Asia/Hebron'), ('Asia/Ho_Chi_Minh', '(GMT+0700) Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', '(GMT+0800) Asia/Hong_Kong'), ('Asia/Hovd', '(GMT+0700) Asia/Hovd'), ('Asia/Irkutsk', '(GMT+0800) Asia/Irkutsk'), ('Asia/Jakarta', '(GMT+0700) Asia/Jakarta'), ('Asia/Jayapura', '(GMT+0900) Asia/Jayapura'), ('Asia/Jerusalem', '(GMT+0300) Asia/Jerusalem'), ('Asia/Kabul', '(GMT+0430) Asia/Kabul'), ('Asia/Kamchatka', '(GMT+1200) Asia/Kamchatka'), ('Asia/Karachi', '(GMT+0500) Asia/Karachi'), ('Asia/Kathmandu', '(GMT+0545) Asia/Kathmandu'), ('Asia/Khandyga', '(GMT+0900) Asia/Khandyga'), ('Asia/Kolkata', '(GMT+0530) Asia/Kolkata'), ('Asia/Krasnoyarsk', '(GMT+0700) Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', '(GMT+0800) Asia/Kuala_Lumpur'), ('Asia/Kuching', '(GMT+0800) Asia/Kuching'), ('Asia/Kuwait', '(GMT+0300) Asia/Kuwait'), ('Asia/Macau', '(GMT+0800) Asia/Macau'), ('Asia/Magadan', '(GMT+1100) Asia/Magadan'), ('Asia/Makassar', '(GMT+0800) Asia/Makassar'), ('Asia/Manila', '(GMT+0800) Asia/Manila'), ('Asia/Muscat', '(GMT+0400) Asia/Muscat'), ('Asia/Nicosia', '(GMT+0300) Asia/Nicosia'), ('Asia/Novokuznetsk', '(GMT+0700) Asia/Novokuznetsk'), ('Asia/Novosibirsk', '(GMT+0700) Asia/Novosibirsk'), ('Asia/Omsk', '(GMT+0600) Asia/Omsk'), ('Asia/Oral', '(GMT+0500) Asia/Oral'), ('Asia/Phnom_Penh', '(GMT+0700) Asia/Phnom_Penh'), ('Asia/Pontianak', '(GMT+0700) Asia/Pontianak'), ('Asia/Pyongyang', '(GMT+0900) Asia/Pyongyang'), ('Asia/Qatar', '(GMT+0300) Asia/Qatar'), ('Asia/Qostanay', '(GMT+0600) Asia/Qostanay'), ('Asia/Qyzylorda', '(GMT+0500) Asia/Qyzylorda'), ('Asia/Riyadh', '(GMT+0300) Asia/Riyadh'), ('Asia/Sakhalin', '(GMT+1100) Asia/Sakhalin'), ('Asia/Samarkand', '(GMT+0500) Asia/Samarkand'), ('Asia/Seoul', '(GMT+0900) Asia/Seoul'), ('Asia/Shanghai', '(GMT+0800) Asia/Shanghai'), ('Asia/Singapore', '(GMT+0800) Asia/Singapore'), ('Asia/Srednekolymsk', '(GMT+1100) Asia/Srednekolymsk'), ('Asia/Taipei', '(GMT+0800) Asia/Taipei'), ('Asia/Tashkent', '(GMT+0500) Asia/Tashkent'), ('Asia/Tbilisi', '(GMT+0400) Asia/Tbilisi'), ('Asia/Tehran', '(GMT+0430) Asia/Tehran'), ('Asia/Thimphu', '(GMT+0600) Asia/Thimphu'), ('Asia/Tokyo', '(GMT+0900) Asia/Tokyo'), ('Asia/Tomsk', '(GMT+0700) Asia/Tomsk'), ('Asia/Ulaanbaatar', '(GMT+0800) Asia/Ulaanbaatar'), ('Asia/Urumqi', '(GMT+0600) Asia/Urumqi'), ('Asia/Ust-Nera', '(GMT+1000) Asia/Ust-Nera'), ('Asia/Vientiane', '(GMT+0700) Asia/Vientiane'), ('Asia/Vladivostok', '(GMT+1000) Asia/Vladivostok'), ('Asia/Yakutsk', '(GMT+0900) Asia/Yakutsk'), ('Asia/Yangon', '(GMT+0630) Asia/Yangon'), ('Asia/Yekaterinburg', '(GMT+0500) Asia/Yekaterinburg'), ('Asia/Yerevan', '(GMT+0400) Asia/Yerevan'), ('Atlantic/Azores', '(GMT+0000) Atlantic/Azores'), ('Atlantic/Bermuda', '(GMT-0300) Atlantic/Bermuda'), ('Atlantic/Canary', '(GMT+0100) Atlantic/Canary'), ('Atlantic/Cape_Verde', '(GMT-0100) Atlantic/Cape_Verde'), ('Atlantic/Faroe', '(GMT+0100) Atlantic/Faroe'), ('Atlantic/Madeira', '(GMT+0100) Atlantic/Madeira'), ('Atlantic/Reykjavik', '(GMT+0000) Atlantic/Reykjavik'), ('Atlantic/South_Georgia', '(GMT-0200) Atlantic/South_Georgia'), ('Atlantic/St_Helena', '(GMT+0000) Atlantic/St_Helena'), ('Atlantic/Stanley', '(GMT-0300) Atlantic/Stanley'), ('Australia/Adelaide', '(GMT+0930) Australia/Adelaide'), ('Australia/Brisbane', '(GMT+1000) Australia/Brisbane'), ('Australia/Broken_Hill', '(GMT+0930) Australia/Broken_Hill'), ('Australia/Currie', '(GMT+1000) Australia/Currie'), ('Australia/Darwin', '(GMT+0930) Australia/Darwin'), ('Australia/Eucla', '(GMT+0845) Australia/Eucla'), ('Australia/Hobart', '(GMT+1000) Australia/Hobart'), ('Australia/Lindeman', '(GMT+1000) Australia/Lindeman'), ('Australia/Lord_Howe', '(GMT+1030) Australia/Lord_Howe'), ('Australia/Melbourne', '(GMT+1000) Australia/Melbourne'), ('Australia/Perth', '(GMT+0800) Australia/Perth'), ('Australia/Sydney', '(GMT+1000) Australia/Sydney'), ('Canada/Atlantic', '(GMT-0300) Canada/Atlantic'), ('Canada/Central', '(GMT-0500) Canada/Central'), ('Canada/Eastern', '(GMT-0400) Canada/Eastern'), ('Canada/Mountain', '(GMT-0600) Canada/Mountain'), ('Canada/Newfoundland', '(GMT-0230) Canada/Newfoundland'), ('Canada/Pacific', '(GMT-0700) Canada/Pacific'), ('Europe/Amsterdam', '(GMT+0200) Europe/Amsterdam'), ('Europe/Andorra', '(GMT+0200) Europe/Andorra'), ('Europe/Astrakhan', '(GMT+0400) Europe/Astrakhan'), ('Europe/Athens', '(GMT+0300) Europe/Athens'), ('Europe/Belgrade', '(GMT+0200) Europe/Belgrade'), ('Europe/Berlin', '(GMT+0200) Europe/Berlin'), ('Europe/Bratislava', '(GMT+0200) Europe/Bratislava'), ('Europe/Brussels', '(GMT+0200) Europe/Brussels'), ('Europe/Bucharest', '(GMT+0300) Europe/Bucharest'), ('Europe/Budapest', '(GMT+0200) Europe/Budapest'), ('Europe/Busingen', '(GMT+0200) Europe/Busingen'), ('Europe/Chisinau', '(GMT+0300) Europe/Chisinau'), ('Europe/Copenhagen', '(GMT+0200) Europe/Copenhagen'), ('Europe/Dublin', '(GMT+0100) Europe/Dublin'), ('Europe/Gibraltar', '(GMT+0200) Europe/Gibraltar'), ('Europe/Guernsey', '(GMT+0100) Europe/Guernsey'), ('Europe/Helsinki', '(GMT+0300) Europe/Helsinki'), ('Europe/Isle_of_Man', '(GMT+0100) Europe/Isle_of_Man'), ('Europe/Istanbul', '(GMT+0300) Europe/Istanbul'), ('Europe/Jersey', '(GMT+0100) Europe/Jersey'), ('Europe/Kaliningrad', '(GMT+0200) Europe/Kaliningrad'), ('Europe/Kiev', '(GMT+0300) Europe/Kiev'), ('Europe/Kirov', '(GMT+0300) Europe/Kirov'), ('Europe/Lisbon', '(GMT+0100) Europe/Lisbon'), ('Europe/Ljubljana', '(GMT+0200) Europe/Ljubljana'), ('Europe/London', '(GMT+0100) Europe/London'), ('Europe/Luxembourg', '(GMT+0200) Europe/Luxembourg'), ('Europe/Madrid', '(GMT+0200) Europe/Madrid'), ('Europe/Malta', '(GMT+0200) Europe/Malta'), ('Europe/Mariehamn', '(GMT+0300) Europe/Mariehamn'), ('Europe/Minsk', '(GMT+0300) Europe/Minsk'), ('Europe/Monaco', '(GMT+0200) Europe/Monaco'), ('Europe/Moscow', '(GMT+0300) Europe/Moscow'), ('Europe/Oslo', '(GMT+0200) Europe/Oslo'), ('Europe/Paris', '(GMT+0200) Europe/Paris'), ('Europe/Podgorica', '(GMT+0200) Europe/Podgorica'), ('Europe/Prague', '(GMT+0200) Europe/Prague'), ('Europe/Riga', '(GMT+0300) Europe/Riga'), ('Europe/Rome', '(GMT+0200) Europe/Rome'), ('Europe/Samara', '(GMT+0400) Europe/Samara'), ('Europe/San_Marino', '(GMT+0200) Europe/San_Marino'), ('Europe/Sarajevo', '(GMT+0200) Europe/Sarajevo'), ('Europe/Saratov', '(GMT+0400) Europe/Saratov'), ('Europe/Simferopol', '(GMT+0300) Europe/Simferopol'), ('Europe/Skopje', '(GMT+0200) Europe/Skopje'), ('Europe/Sofia', '(GMT+0300) Europe/Sofia'), ('Europe/Stockholm', '(GMT+0200) Europe/Stockholm'), ('Europe/Tallinn', '(GMT+0300) Europe/Tallinn'), ('Europe/Tirane', '(GMT+0200) Europe/Tirane'), ('Europe/Ulyanovsk', '(GMT+0400) Europe/Ulyanovsk'), ('Europe/Uzhgorod', '(GMT+0300) Europe/Uzhgorod'), ('Europe/Vaduz', '(GMT+0200) Europe/Vaduz'), ('Europe/Vatican', '(GMT+0200) Europe/Vatican'), ('Europe/Vienna', '(GMT+0200) Europe/Vienna'), ('Europe/Vilnius', '(GMT+0300) Europe/Vilnius'), ('Europe/Volgograd', '(GMT+0400) Europe/Volgograd'), ('Europe/Warsaw', '(GMT+0200) Europe/Warsaw'), ('Europe/Zagreb', '(GMT+0200) Europe/Zagreb'), ('Europe/Zaporozhye', '(GMT+0300) Europe/Zaporozhye'), ('Europe/Zurich', '(GMT+0200) Europe/Zurich'), ('GMT', '(GMT+0000) GMT'), ('Indian/Antananarivo', '(GMT+0300) Indian/Antananarivo'), ('Indian/Chagos', '(GMT+0600) Indian/Chagos'), ('Indian/Christmas', '(GMT+0700) Indian/Christmas'), ('Indian/Cocos', '(GMT+0630) Indian/Cocos'), ('Indian/Comoro', '(GMT+0300) Indian/Comoro'), ('Indian/Kerguelen', '(GMT+0500) Indian/Kerguelen'), ('Indian/Mahe', '(GMT+0400) Indian/Mahe'), ('Indian/Maldives', '(GMT+0500) Indian/Maldives'), ('Indian/Mauritius', '(GMT+0400) Indian/Mauritius'), ('Indian/Mayotte', '(GMT+0300) Indian/Mayotte'), ('Indian/Reunion', '(GMT+0400) Indian/Reunion'), ('Pacific/Apia', '(GMT+1300) Pacific/Apia'), ('Pacific/Auckland', '(GMT+1200) Pacific/Auckland'), ('Pacific/Bougainville', '(GMT+1100) Pacific/Bougainville'), ('Pacific/Chatham', '(GMT+1245) Pacific/Chatham'), ('Pacific/Chuuk', '(GMT+1000) Pacific/Chuuk'), ('Pacific/Easter', '(GMT-0600) Pacific/Easter'), ('Pacific/Efate', '(GMT+1100) Pacific/Efate'), ('Pacific/Enderbury', '(GMT+1300) Pacific/Enderbury'), ('Pacific/Fakaofo', '(GMT+1300) Pacific/Fakaofo'), ('Pacific/Fiji', '(GMT+1200) Pacific/Fiji'), ('Pacific/Funafuti', '(GMT+1200) Pacific/Funafuti'), ('Pacific/Galapagos', '(GMT-0600) Pacific/Galapagos'), ('Pacific/Gambier', '(GMT-0900) Pacific/Gambier'), ('Pacific/Guadalcanal', '(GMT+1100) Pacific/Guadalcanal'), ('Pacific/Guam', '(GMT+1000) Pacific/Guam'), ('Pacific/Honolulu', '(GMT-1000) Pacific/Honolulu'), ('Pacific/Kiritimati', '(GMT+1400) Pacific/Kiritimati'), ('Pacific/Kosrae', '(GMT+1100) Pacific/Kosrae'), ('Pacific/Kwajalein', '(GMT+1200) Pacific/Kwajalein'), ('Pacific/Majuro', '(GMT+1200) Pacific/Majuro'), ('Pacific/Marquesas', '(GMT-0930) Pacific/Marquesas'), ('Pacific/Midway', '(GMT-1100) Pacific/Midway'), ('Pacific/Nauru', '(GMT+1200) Pacific/Nauru'), ('Pacific/Niue', '(GMT-1100) Pacific/Niue'), ('Pacific/Norfolk', '(GMT+1100) Pacific/Norfolk'), ('Pacific/Noumea', '(GMT+1100) Pacific/Noumea'), ('Pacific/Pago_Pago', '(GMT-1100) Pacific/Pago_Pago'), ('Pacific/Palau', '(GMT+0900) Pacific/Palau'), ('Pacific/Pitcairn', '(GMT-0800) Pacific/Pitcairn'), ('Pacific/Pohnpei', '(GMT+1100) Pacific/Pohnpei'), ('Pacific/Port_Moresby', '(GMT+1000) Pacific/Port_Moresby'), ('Pacific/Rarotonga', '(GMT-1000) Pacific/Rarotonga'), ('Pacific/Saipan', '(GMT+1000) Pacific/Saipan'), ('Pacific/Tahiti', '(GMT-1000) Pacific/Tahiti'), ('Pacific/Tarawa', '(GMT+1200) Pacific/Tarawa'), ('Pacific/Tongatapu', '(GMT+1300) Pacific/Tongatapu'), ('Pacific/Wake', '(GMT+1200) Pacific/Wake'), ('Pacific/Wallis', '(GMT+1200) Pacific/Wallis'), ('US/Alaska', '(GMT-0800) US/Alaska'), ('US/Arizona', '(GMT-0700) US/Arizona'), ('US/Central', '(GMT-0500) US/Central'), ('US/Eastern', '(GMT-0400) US/Eastern'), ('US/Hawaii', '(GMT-1000) US/Hawaii'), ('US/Mountain', '(GMT-0600) US/Mountain'), ('US/Pacific', '(GMT-0700) US/Pacific'), ('UTC', '(GMT+0000) UTC')], default='America/New_York', max_length=100), + model_name="profile", + name="timezone", + field=vendor.timezones.fields.TimeZoneField( + choices=[ + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Casablanca", "(GMT+0000) Africa/Casablanca"), + ("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Dar_es_Salaam", "(GMT+0300) Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0000) Africa/El_Aaiun"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0300) Africa/Juba"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("America/Adak", "(GMT-0900) America/Adak"), + ("America/Anchorage", "(GMT-0800) America/Anchorage"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ("America/Argentina/Buenos_Aires", "(GMT-0300) America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "(GMT-0300) America/Argentina/Catamarca"), + ("America/Argentina/Cordoba", "(GMT-0300) America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "(GMT-0300) America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "(GMT-0300) America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "(GMT-0300) America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "(GMT-0300) America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "(GMT-0300) America/Argentina/Salta"), + ("America/Argentina/San_Juan", "(GMT-0300) America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "(GMT-0300) America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "(GMT-0300) America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "(GMT-0300) America/Argentina/Ushuaia"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0400) America/Asuncion"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Bahia_Banderas", "(GMT-0500) America/Bahia_Banderas"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Blanc-Sablon", "(GMT-0400) America/Blanc-Sablon"), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Boise", "(GMT-0600) America/Boise"), + ("America/Cambridge_Bay", "(GMT-0600) America/Cambridge_Bay"), + ("America/Campo_Grande", "(GMT-0400) America/Campo_Grande"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0500) America/Chicago"), + ("America/Chihuahua", "(GMT-0600) America/Chihuahua"), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Danmarkshavn", "(GMT+0000) America/Danmarkshavn"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ("America/Dawson_Creek", "(GMT-0700) America/Dawson_Creek"), + ("America/Denver", "(GMT-0600) America/Denver"), + ("America/Detroit", "(GMT-0400) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Edmonton", "(GMT-0600) America/Edmonton"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"), + ("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"), + ("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Halifax", "(GMT-0300) America/Halifax"), + ("America/Havana", "(GMT-0400) America/Havana"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Indiana/Indianapolis", "(GMT-0400) America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "(GMT-0500) America/Indiana/Knox"), + ("America/Indiana/Marengo", "(GMT-0400) America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "(GMT-0400) America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "(GMT-0500) America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "(GMT-0400) America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "(GMT-0400) America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "(GMT-0400) America/Indiana/Winamac"), + ("America/Inuvik", "(GMT-0600) America/Inuvik"), + ("America/Iqaluit", "(GMT-0400) America/Iqaluit"), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Juneau", "(GMT-0800) America/Juneau"), + ("America/Kentucky/Louisville", "(GMT-0400) America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "(GMT-0400) America/Kentucky/Monticello"), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"), + ("America/Lower_Princes", "(GMT-0400) America/Lower_Princes"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Matamoros", "(GMT-0500) America/Matamoros"), + ("America/Mazatlan", "(GMT-0600) America/Mazatlan"), + ("America/Menominee", "(GMT-0500) America/Menominee"), + ("America/Merida", "(GMT-0500) America/Merida"), + ("America/Metlakatla", "(GMT-0800) America/Metlakatla"), + ("America/Mexico_City", "(GMT-0500) America/Mexico_City"), + ("America/Miquelon", "(GMT-0200) America/Miquelon"), + ("America/Moncton", "(GMT-0300) America/Moncton"), + ("America/Monterrey", "(GMT-0500) America/Monterrey"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0400) America/Nassau"), + ("America/New_York", "(GMT-0400) America/New_York"), + ("America/Nipigon", "(GMT-0400) America/Nipigon"), + ("America/Nome", "(GMT-0800) America/Nome"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/North_Dakota/Beulah", "(GMT-0500) America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "(GMT-0500) America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "(GMT-0500) America/North_Dakota/New_Salem"), + ("America/Nuuk", "(GMT-0200) America/Nuuk"), + ("America/Ojinaga", "(GMT-0600) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ("America/Pangnirtung", "(GMT-0400) America/Pangnirtung"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Port-au-Prince", "(GMT-0400) America/Port-au-Prince"), + ("America/Port_of_Spain", "(GMT-0400) America/Port_of_Spain"), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Punta_Arenas", "(GMT-0300) America/Punta_Arenas"), + ("America/Rainy_River", "(GMT-0500) America/Rainy_River"), + ("America/Rankin_Inlet", "(GMT-0500) America/Rankin_Inlet"), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Regina", "(GMT-0600) America/Regina"), + ("America/Resolute", "(GMT-0500) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Santiago", "(GMT-0400) America/Santiago"), + ("America/Santo_Domingo", "(GMT-0400) America/Santo_Domingo"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Scoresbysund", "(GMT+0000) America/Scoresbysund"), + ("America/Sitka", "(GMT-0800) America/Sitka"), + ("America/St_Barthelemy", "(GMT-0400) America/St_Barthelemy"), + ("America/St_Johns", "(GMT-0230) America/St_Johns"), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Swift_Current", "(GMT-0600) America/Swift_Current"), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Thule", "(GMT-0300) America/Thule"), + ("America/Thunder_Bay", "(GMT-0400) America/Thunder_Bay"), + ("America/Tijuana", "(GMT-0700) America/Tijuana"), + ("America/Toronto", "(GMT-0400) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("America/Vancouver", "(GMT-0700) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("America/Winnipeg", "(GMT-0500) America/Winnipeg"), + ("America/Yakutat", "(GMT-0800) America/Yakutat"), + ("America/Yellowknife", "(GMT-0600) America/Yellowknife"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Antarctica/DumontDUrville", "(GMT+1000) Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "(GMT+1000) Antarctica/Macquarie"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Antarctica/McMurdo", "(GMT+1200) Antarctica/McMurdo"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Amman", "(GMT+0300) Asia/Amman"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Beirut", "(GMT+0300) Asia/Beirut"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Damascus", "(GMT+0300) Asia/Damascus"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0300) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0300) Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"), + ("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Tehran", "(GMT+0430) Asia/Tehran"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"), + ("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"), + ("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "(GMT-0200) Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Australia/Adelaide", "(GMT+0930) Australia/Adelaide"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Broken_Hill", "(GMT+0930) Australia/Broken_Hill"), + ("Australia/Currie", "(GMT+1000) Australia/Currie"), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Australia/Hobart", "(GMT+1000) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Lord_Howe", "(GMT+1030) Australia/Lord_Howe"), + ("Australia/Melbourne", "(GMT+1000) Australia/Melbourne"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Sydney", "(GMT+1000) Australia/Sydney"), + ("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"), + ("Canada/Central", "(GMT-0500) Canada/Central"), + ("Canada/Eastern", "(GMT-0400) Canada/Eastern"), + ("Canada/Mountain", "(GMT-0600) Canada/Mountain"), + ("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"), + ("Canada/Pacific", "(GMT-0700) Canada/Pacific"), + ("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0200) Europe/Andorra"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Athens", "(GMT+0300) Europe/Athens"), + ("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0200) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0200) Europe/Brussels"), + ("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"), + ("Europe/Budapest", "(GMT+0200) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0200) Europe/Busingen"), + ("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"), + ("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"), + ("Europe/Dublin", "(GMT+0100) Europe/Dublin"), + ("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"), + ("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"), + ("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"), + ("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Jersey", "(GMT+0100) Europe/Jersey"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Kiev", "(GMT+0300) Europe/Kiev"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"), + ("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"), + ("Europe/London", "(GMT+0100) Europe/London"), + ("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0200) Europe/Madrid"), + ("Europe/Malta", "(GMT+0200) Europe/Malta"), + ("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Monaco", "(GMT+0200) Europe/Monaco"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Oslo", "(GMT+0200) Europe/Oslo"), + ("Europe/Paris", "(GMT+0200) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0200) Europe/Prague"), + ("Europe/Riga", "(GMT+0300) Europe/Riga"), + ("Europe/Rome", "(GMT+0200) Europe/Rome"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Skopje", "(GMT+0200) Europe/Skopje"), + ("Europe/Sofia", "(GMT+0300) Europe/Sofia"), + ("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"), + ("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"), + ("Europe/Tirane", "(GMT+0200) Europe/Tirane"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "(GMT+0300) Europe/Uzhgorod"), + ("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0200) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0200) Europe/Vienna"), + ("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0400) Europe/Volgograd"), + ("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"), + ("Europe/Zaporozhye", "(GMT+0300) Europe/Zaporozhye"), + ("Europe/Zurich", "(GMT+0200) Europe/Zurich"), + ("GMT", "(GMT+0000) GMT"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Pacific/Apia", "(GMT+1300) Pacific/Apia"), + ("Pacific/Auckland", "(GMT+1200) Pacific/Auckland"), + ("Pacific/Bougainville", "(GMT+1100) Pacific/Bougainville"), + ("Pacific/Chatham", "(GMT+1245) Pacific/Chatham"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Easter", "(GMT-0600) Pacific/Easter"), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Enderbury", "(GMT+1300) Pacific/Enderbury"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Pacific/Port_Moresby", "(GMT+1000) Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("US/Alaska", "(GMT-0800) US/Alaska"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Central", "(GMT-0500) US/Central"), + ("US/Eastern", "(GMT-0400) US/Eastern"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("US/Mountain", "(GMT-0600) US/Mountain"), + ("US/Pacific", "(GMT-0700) US/Pacific"), + ("UTC", "(GMT+0000) UTC"), + ], + default="America/New_York", + max_length=100, + ), ), ] diff --git a/apps/profile/migrations/0012_auto_20220511_1710.py b/apps/profile/migrations/0012_auto_20220511_1710.py index a915e7c80b..0257cafbcd 100644 --- a/apps/profile/migrations/0012_auto_20220511_1710.py +++ b/apps/profile/migrations/0012_auto_20220511_1710.py @@ -1,19 +1,464 @@ # Generated by Django 3.1.10 on 2022-05-11 17:10 from django.db import migrations + import vendor.timezones.fields class Migration(migrations.Migration): - dependencies = [ - ('profile', '0011_auto_20220408_1908'), + ("profile", "0011_auto_20220408_1908"), ] operations = [ migrations.AlterField( - model_name='profile', - name='timezone', - field=vendor.timezones.fields.TimeZoneField(choices=[('Africa/Abidjan', '(GMT+0000) Africa/Abidjan'), ('Africa/Accra', '(GMT+0000) Africa/Accra'), ('Africa/Addis_Ababa', '(GMT+0300) Africa/Addis_Ababa'), ('Africa/Algiers', '(GMT+0100) Africa/Algiers'), ('Africa/Asmara', '(GMT+0300) Africa/Asmara'), ('Africa/Bamako', '(GMT+0000) Africa/Bamako'), ('Africa/Bangui', '(GMT+0100) Africa/Bangui'), ('Africa/Banjul', '(GMT+0000) Africa/Banjul'), ('Africa/Bissau', '(GMT+0000) Africa/Bissau'), ('Africa/Blantyre', '(GMT+0200) Africa/Blantyre'), ('Africa/Brazzaville', '(GMT+0100) Africa/Brazzaville'), ('Africa/Bujumbura', '(GMT+0200) Africa/Bujumbura'), ('Africa/Cairo', '(GMT+0200) Africa/Cairo'), ('Africa/Casablanca', '(GMT+0100) Africa/Casablanca'), ('Africa/Ceuta', '(GMT+0200) Africa/Ceuta'), ('Africa/Conakry', '(GMT+0000) Africa/Conakry'), ('Africa/Dakar', '(GMT+0000) Africa/Dakar'), ('Africa/Dar_es_Salaam', '(GMT+0300) Africa/Dar_es_Salaam'), ('Africa/Djibouti', '(GMT+0300) Africa/Djibouti'), ('Africa/Douala', '(GMT+0100) Africa/Douala'), ('Africa/El_Aaiun', '(GMT+0100) Africa/El_Aaiun'), ('Africa/Freetown', '(GMT+0000) Africa/Freetown'), ('Africa/Gaborone', '(GMT+0200) Africa/Gaborone'), ('Africa/Harare', '(GMT+0200) Africa/Harare'), ('Africa/Johannesburg', '(GMT+0200) Africa/Johannesburg'), ('Africa/Juba', '(GMT+0300) Africa/Juba'), ('Africa/Kampala', '(GMT+0300) Africa/Kampala'), ('Africa/Khartoum', '(GMT+0200) Africa/Khartoum'), ('Africa/Kigali', '(GMT+0200) Africa/Kigali'), ('Africa/Kinshasa', '(GMT+0100) Africa/Kinshasa'), ('Africa/Lagos', '(GMT+0100) Africa/Lagos'), ('Africa/Libreville', '(GMT+0100) Africa/Libreville'), ('Africa/Lome', '(GMT+0000) Africa/Lome'), ('Africa/Luanda', '(GMT+0100) Africa/Luanda'), ('Africa/Lubumbashi', '(GMT+0200) Africa/Lubumbashi'), ('Africa/Lusaka', '(GMT+0200) Africa/Lusaka'), ('Africa/Malabo', '(GMT+0100) Africa/Malabo'), ('Africa/Maputo', '(GMT+0200) Africa/Maputo'), ('Africa/Maseru', '(GMT+0200) Africa/Maseru'), ('Africa/Mbabane', '(GMT+0200) Africa/Mbabane'), ('Africa/Mogadishu', '(GMT+0300) Africa/Mogadishu'), ('Africa/Monrovia', '(GMT+0000) Africa/Monrovia'), ('Africa/Nairobi', '(GMT+0300) Africa/Nairobi'), ('Africa/Ndjamena', '(GMT+0100) Africa/Ndjamena'), ('Africa/Niamey', '(GMT+0100) Africa/Niamey'), ('Africa/Nouakchott', '(GMT+0000) Africa/Nouakchott'), ('Africa/Ouagadougou', '(GMT+0000) Africa/Ouagadougou'), ('Africa/Porto-Novo', '(GMT+0100) Africa/Porto-Novo'), ('Africa/Sao_Tome', '(GMT+0000) Africa/Sao_Tome'), ('Africa/Tripoli', '(GMT+0200) Africa/Tripoli'), ('Africa/Tunis', '(GMT+0100) Africa/Tunis'), ('Africa/Windhoek', '(GMT+0200) Africa/Windhoek'), ('America/Adak', '(GMT-0900) America/Adak'), ('America/Anchorage', '(GMT-0800) America/Anchorage'), ('America/Anguilla', '(GMT-0400) America/Anguilla'), ('America/Antigua', '(GMT-0400) America/Antigua'), ('America/Araguaina', '(GMT-0300) America/Araguaina'), ('America/Argentina/Buenos_Aires', '(GMT-0300) America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', '(GMT-0300) America/Argentina/Catamarca'), ('America/Argentina/Cordoba', '(GMT-0300) America/Argentina/Cordoba'), ('America/Argentina/Jujuy', '(GMT-0300) America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', '(GMT-0300) America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', '(GMT-0300) America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', '(GMT-0300) America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', '(GMT-0300) America/Argentina/Salta'), ('America/Argentina/San_Juan', '(GMT-0300) America/Argentina/San_Juan'), ('America/Argentina/San_Luis', '(GMT-0300) America/Argentina/San_Luis'), ('America/Argentina/Tucuman', '(GMT-0300) America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', '(GMT-0300) America/Argentina/Ushuaia'), ('America/Aruba', '(GMT-0400) America/Aruba'), ('America/Asuncion', '(GMT-0400) America/Asuncion'), ('America/Atikokan', '(GMT-0500) America/Atikokan'), ('America/Bahia', '(GMT-0300) America/Bahia'), ('America/Bahia_Banderas', '(GMT-0500) America/Bahia_Banderas'), ('America/Barbados', '(GMT-0400) America/Barbados'), ('America/Belem', '(GMT-0300) America/Belem'), ('America/Belize', '(GMT-0600) America/Belize'), ('America/Blanc-Sablon', '(GMT-0400) America/Blanc-Sablon'), ('America/Boa_Vista', '(GMT-0400) America/Boa_Vista'), ('America/Bogota', '(GMT-0500) America/Bogota'), ('America/Boise', '(GMT-0600) America/Boise'), ('America/Cambridge_Bay', '(GMT-0600) America/Cambridge_Bay'), ('America/Campo_Grande', '(GMT-0400) America/Campo_Grande'), ('America/Cancun', '(GMT-0500) America/Cancun'), ('America/Caracas', '(GMT-0400) America/Caracas'), ('America/Cayenne', '(GMT-0300) America/Cayenne'), ('America/Cayman', '(GMT-0500) America/Cayman'), ('America/Chicago', '(GMT-0500) America/Chicago'), ('America/Chihuahua', '(GMT-0600) America/Chihuahua'), ('America/Costa_Rica', '(GMT-0600) America/Costa_Rica'), ('America/Creston', '(GMT-0700) America/Creston'), ('America/Cuiaba', '(GMT-0400) America/Cuiaba'), ('America/Curacao', '(GMT-0400) America/Curacao'), ('America/Danmarkshavn', '(GMT+0000) America/Danmarkshavn'), ('America/Dawson', '(GMT-0700) America/Dawson'), ('America/Dawson_Creek', '(GMT-0700) America/Dawson_Creek'), ('America/Denver', '(GMT-0600) America/Denver'), ('America/Detroit', '(GMT-0400) America/Detroit'), ('America/Dominica', '(GMT-0400) America/Dominica'), ('America/Edmonton', '(GMT-0600) America/Edmonton'), ('America/Eirunepe', '(GMT-0500) America/Eirunepe'), ('America/El_Salvador', '(GMT-0600) America/El_Salvador'), ('America/Fort_Nelson', '(GMT-0700) America/Fort_Nelson'), ('America/Fortaleza', '(GMT-0300) America/Fortaleza'), ('America/Glace_Bay', '(GMT-0300) America/Glace_Bay'), ('America/Goose_Bay', '(GMT-0300) America/Goose_Bay'), ('America/Grand_Turk', '(GMT-0400) America/Grand_Turk'), ('America/Grenada', '(GMT-0400) America/Grenada'), ('America/Guadeloupe', '(GMT-0400) America/Guadeloupe'), ('America/Guatemala', '(GMT-0600) America/Guatemala'), ('America/Guayaquil', '(GMT-0500) America/Guayaquil'), ('America/Guyana', '(GMT-0400) America/Guyana'), ('America/Halifax', '(GMT-0300) America/Halifax'), ('America/Havana', '(GMT-0400) America/Havana'), ('America/Hermosillo', '(GMT-0700) America/Hermosillo'), ('America/Indiana/Indianapolis', '(GMT-0400) America/Indiana/Indianapolis'), ('America/Indiana/Knox', '(GMT-0500) America/Indiana/Knox'), ('America/Indiana/Marengo', '(GMT-0400) America/Indiana/Marengo'), ('America/Indiana/Petersburg', '(GMT-0400) America/Indiana/Petersburg'), ('America/Indiana/Tell_City', '(GMT-0500) America/Indiana/Tell_City'), ('America/Indiana/Vevay', '(GMT-0400) America/Indiana/Vevay'), ('America/Indiana/Vincennes', '(GMT-0400) America/Indiana/Vincennes'), ('America/Indiana/Winamac', '(GMT-0400) America/Indiana/Winamac'), ('America/Inuvik', '(GMT-0600) America/Inuvik'), ('America/Iqaluit', '(GMT-0400) America/Iqaluit'), ('America/Jamaica', '(GMT-0500) America/Jamaica'), ('America/Juneau', '(GMT-0800) America/Juneau'), ('America/Kentucky/Louisville', '(GMT-0400) America/Kentucky/Louisville'), ('America/Kentucky/Monticello', '(GMT-0400) America/Kentucky/Monticello'), ('America/Kralendijk', '(GMT-0400) America/Kralendijk'), ('America/La_Paz', '(GMT-0400) America/La_Paz'), ('America/Lima', '(GMT-0500) America/Lima'), ('America/Los_Angeles', '(GMT-0700) America/Los_Angeles'), ('America/Lower_Princes', '(GMT-0400) America/Lower_Princes'), ('America/Maceio', '(GMT-0300) America/Maceio'), ('America/Managua', '(GMT-0600) America/Managua'), ('America/Manaus', '(GMT-0400) America/Manaus'), ('America/Marigot', '(GMT-0400) America/Marigot'), ('America/Martinique', '(GMT-0400) America/Martinique'), ('America/Matamoros', '(GMT-0500) America/Matamoros'), ('America/Mazatlan', '(GMT-0600) America/Mazatlan'), ('America/Menominee', '(GMT-0500) America/Menominee'), ('America/Merida', '(GMT-0500) America/Merida'), ('America/Metlakatla', '(GMT-0800) America/Metlakatla'), ('America/Mexico_City', '(GMT-0500) America/Mexico_City'), ('America/Miquelon', '(GMT-0200) America/Miquelon'), ('America/Moncton', '(GMT-0300) America/Moncton'), ('America/Monterrey', '(GMT-0500) America/Monterrey'), ('America/Montevideo', '(GMT-0300) America/Montevideo'), ('America/Montserrat', '(GMT-0400) America/Montserrat'), ('America/Nassau', '(GMT-0400) America/Nassau'), ('America/New_York', '(GMT-0400) America/New_York'), ('America/Nipigon', '(GMT-0400) America/Nipigon'), ('America/Nome', '(GMT-0800) America/Nome'), ('America/Noronha', '(GMT-0200) America/Noronha'), ('America/North_Dakota/Beulah', '(GMT-0500) America/North_Dakota/Beulah'), ('America/North_Dakota/Center', '(GMT-0500) America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', '(GMT-0500) America/North_Dakota/New_Salem'), ('America/Nuuk', '(GMT-0200) America/Nuuk'), ('America/Ojinaga', '(GMT-0600) America/Ojinaga'), ('America/Panama', '(GMT-0500) America/Panama'), ('America/Pangnirtung', '(GMT-0400) America/Pangnirtung'), ('America/Paramaribo', '(GMT-0300) America/Paramaribo'), ('America/Phoenix', '(GMT-0700) America/Phoenix'), ('America/Port-au-Prince', '(GMT-0400) America/Port-au-Prince'), ('America/Port_of_Spain', '(GMT-0400) America/Port_of_Spain'), ('America/Porto_Velho', '(GMT-0400) America/Porto_Velho'), ('America/Puerto_Rico', '(GMT-0400) America/Puerto_Rico'), ('America/Punta_Arenas', '(GMT-0300) America/Punta_Arenas'), ('America/Rainy_River', '(GMT-0500) America/Rainy_River'), ('America/Rankin_Inlet', '(GMT-0500) America/Rankin_Inlet'), ('America/Recife', '(GMT-0300) America/Recife'), ('America/Regina', '(GMT-0600) America/Regina'), ('America/Resolute', '(GMT-0500) America/Resolute'), ('America/Rio_Branco', '(GMT-0500) America/Rio_Branco'), ('America/Santarem', '(GMT-0300) America/Santarem'), ('America/Santiago', '(GMT-0400) America/Santiago'), ('America/Santo_Domingo', '(GMT-0400) America/Santo_Domingo'), ('America/Sao_Paulo', '(GMT-0300) America/Sao_Paulo'), ('America/Scoresbysund', '(GMT+0000) America/Scoresbysund'), ('America/Sitka', '(GMT-0800) America/Sitka'), ('America/St_Barthelemy', '(GMT-0400) America/St_Barthelemy'), ('America/St_Johns', '(GMT-0230) America/St_Johns'), ('America/St_Kitts', '(GMT-0400) America/St_Kitts'), ('America/St_Lucia', '(GMT-0400) America/St_Lucia'), ('America/St_Thomas', '(GMT-0400) America/St_Thomas'), ('America/St_Vincent', '(GMT-0400) America/St_Vincent'), ('America/Swift_Current', '(GMT-0600) America/Swift_Current'), ('America/Tegucigalpa', '(GMT-0600) America/Tegucigalpa'), ('America/Thule', '(GMT-0300) America/Thule'), ('America/Thunder_Bay', '(GMT-0400) America/Thunder_Bay'), ('America/Tijuana', '(GMT-0700) America/Tijuana'), ('America/Toronto', '(GMT-0400) America/Toronto'), ('America/Tortola', '(GMT-0400) America/Tortola'), ('America/Vancouver', '(GMT-0700) America/Vancouver'), ('America/Whitehorse', '(GMT-0700) America/Whitehorse'), ('America/Winnipeg', '(GMT-0500) America/Winnipeg'), ('America/Yakutat', '(GMT-0800) America/Yakutat'), ('America/Yellowknife', '(GMT-0600) America/Yellowknife'), ('Antarctica/Casey', '(GMT+1100) Antarctica/Casey'), ('Antarctica/Davis', '(GMT+0700) Antarctica/Davis'), ('Antarctica/DumontDUrville', '(GMT+1000) Antarctica/DumontDUrville'), ('Antarctica/Macquarie', '(GMT+1000) Antarctica/Macquarie'), ('Antarctica/Mawson', '(GMT+0500) Antarctica/Mawson'), ('Antarctica/McMurdo', '(GMT+1200) Antarctica/McMurdo'), ('Antarctica/Palmer', '(GMT-0300) Antarctica/Palmer'), ('Antarctica/Rothera', '(GMT-0300) Antarctica/Rothera'), ('Antarctica/Syowa', '(GMT+0300) Antarctica/Syowa'), ('Antarctica/Troll', '(GMT+0200) Antarctica/Troll'), ('Antarctica/Vostok', '(GMT+0600) Antarctica/Vostok'), ('Arctic/Longyearbyen', '(GMT+0200) Arctic/Longyearbyen'), ('Asia/Aden', '(GMT+0300) Asia/Aden'), ('Asia/Almaty', '(GMT+0600) Asia/Almaty'), ('Asia/Amman', '(GMT+0300) Asia/Amman'), ('Asia/Anadyr', '(GMT+1200) Asia/Anadyr'), ('Asia/Aqtau', '(GMT+0500) Asia/Aqtau'), ('Asia/Aqtobe', '(GMT+0500) Asia/Aqtobe'), ('Asia/Ashgabat', '(GMT+0500) Asia/Ashgabat'), ('Asia/Atyrau', '(GMT+0500) Asia/Atyrau'), ('Asia/Baghdad', '(GMT+0300) Asia/Baghdad'), ('Asia/Bahrain', '(GMT+0300) Asia/Bahrain'), ('Asia/Baku', '(GMT+0400) Asia/Baku'), ('Asia/Bangkok', '(GMT+0700) Asia/Bangkok'), ('Asia/Barnaul', '(GMT+0700) Asia/Barnaul'), ('Asia/Beirut', '(GMT+0300) Asia/Beirut'), ('Asia/Bishkek', '(GMT+0600) Asia/Bishkek'), ('Asia/Brunei', '(GMT+0800) Asia/Brunei'), ('Asia/Chita', '(GMT+0900) Asia/Chita'), ('Asia/Choibalsan', '(GMT+0800) Asia/Choibalsan'), ('Asia/Colombo', '(GMT+0530) Asia/Colombo'), ('Asia/Damascus', '(GMT+0300) Asia/Damascus'), ('Asia/Dhaka', '(GMT+0600) Asia/Dhaka'), ('Asia/Dili', '(GMT+0900) Asia/Dili'), ('Asia/Dubai', '(GMT+0400) Asia/Dubai'), ('Asia/Dushanbe', '(GMT+0500) Asia/Dushanbe'), ('Asia/Famagusta', '(GMT+0300) Asia/Famagusta'), ('Asia/Gaza', '(GMT+0300) Asia/Gaza'), ('Asia/Hebron', '(GMT+0300) Asia/Hebron'), ('Asia/Ho_Chi_Minh', '(GMT+0700) Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', '(GMT+0800) Asia/Hong_Kong'), ('Asia/Hovd', '(GMT+0700) Asia/Hovd'), ('Asia/Irkutsk', '(GMT+0800) Asia/Irkutsk'), ('Asia/Jakarta', '(GMT+0700) Asia/Jakarta'), ('Asia/Jayapura', '(GMT+0900) Asia/Jayapura'), ('Asia/Jerusalem', '(GMT+0300) Asia/Jerusalem'), ('Asia/Kabul', '(GMT+0430) Asia/Kabul'), ('Asia/Kamchatka', '(GMT+1200) Asia/Kamchatka'), ('Asia/Karachi', '(GMT+0500) Asia/Karachi'), ('Asia/Kathmandu', '(GMT+0545) Asia/Kathmandu'), ('Asia/Khandyga', '(GMT+0900) Asia/Khandyga'), ('Asia/Kolkata', '(GMT+0530) Asia/Kolkata'), ('Asia/Krasnoyarsk', '(GMT+0700) Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', '(GMT+0800) Asia/Kuala_Lumpur'), ('Asia/Kuching', '(GMT+0800) Asia/Kuching'), ('Asia/Kuwait', '(GMT+0300) Asia/Kuwait'), ('Asia/Macau', '(GMT+0800) Asia/Macau'), ('Asia/Magadan', '(GMT+1100) Asia/Magadan'), ('Asia/Makassar', '(GMT+0800) Asia/Makassar'), ('Asia/Manila', '(GMT+0800) Asia/Manila'), ('Asia/Muscat', '(GMT+0400) Asia/Muscat'), ('Asia/Nicosia', '(GMT+0300) Asia/Nicosia'), ('Asia/Novokuznetsk', '(GMT+0700) Asia/Novokuznetsk'), ('Asia/Novosibirsk', '(GMT+0700) Asia/Novosibirsk'), ('Asia/Omsk', '(GMT+0600) Asia/Omsk'), ('Asia/Oral', '(GMT+0500) Asia/Oral'), ('Asia/Phnom_Penh', '(GMT+0700) Asia/Phnom_Penh'), ('Asia/Pontianak', '(GMT+0700) Asia/Pontianak'), ('Asia/Pyongyang', '(GMT+0900) Asia/Pyongyang'), ('Asia/Qatar', '(GMT+0300) Asia/Qatar'), ('Asia/Qostanay', '(GMT+0600) Asia/Qostanay'), ('Asia/Qyzylorda', '(GMT+0500) Asia/Qyzylorda'), ('Asia/Riyadh', '(GMT+0300) Asia/Riyadh'), ('Asia/Sakhalin', '(GMT+1100) Asia/Sakhalin'), ('Asia/Samarkand', '(GMT+0500) Asia/Samarkand'), ('Asia/Seoul', '(GMT+0900) Asia/Seoul'), ('Asia/Shanghai', '(GMT+0800) Asia/Shanghai'), ('Asia/Singapore', '(GMT+0800) Asia/Singapore'), ('Asia/Srednekolymsk', '(GMT+1100) Asia/Srednekolymsk'), ('Asia/Taipei', '(GMT+0800) Asia/Taipei'), ('Asia/Tashkent', '(GMT+0500) Asia/Tashkent'), ('Asia/Tbilisi', '(GMT+0400) Asia/Tbilisi'), ('Asia/Tehran', '(GMT+0430) Asia/Tehran'), ('Asia/Thimphu', '(GMT+0600) Asia/Thimphu'), ('Asia/Tokyo', '(GMT+0900) Asia/Tokyo'), ('Asia/Tomsk', '(GMT+0700) Asia/Tomsk'), ('Asia/Ulaanbaatar', '(GMT+0800) Asia/Ulaanbaatar'), ('Asia/Urumqi', '(GMT+0600) Asia/Urumqi'), ('Asia/Ust-Nera', '(GMT+1000) Asia/Ust-Nera'), ('Asia/Vientiane', '(GMT+0700) Asia/Vientiane'), ('Asia/Vladivostok', '(GMT+1000) Asia/Vladivostok'), ('Asia/Yakutsk', '(GMT+0900) Asia/Yakutsk'), ('Asia/Yangon', '(GMT+0630) Asia/Yangon'), ('Asia/Yekaterinburg', '(GMT+0500) Asia/Yekaterinburg'), ('Asia/Yerevan', '(GMT+0400) Asia/Yerevan'), ('Atlantic/Azores', '(GMT+0000) Atlantic/Azores'), ('Atlantic/Bermuda', '(GMT-0300) Atlantic/Bermuda'), ('Atlantic/Canary', '(GMT+0100) Atlantic/Canary'), ('Atlantic/Cape_Verde', '(GMT-0100) Atlantic/Cape_Verde'), ('Atlantic/Faroe', '(GMT+0100) Atlantic/Faroe'), ('Atlantic/Madeira', '(GMT+0100) Atlantic/Madeira'), ('Atlantic/Reykjavik', '(GMT+0000) Atlantic/Reykjavik'), ('Atlantic/South_Georgia', '(GMT-0200) Atlantic/South_Georgia'), ('Atlantic/St_Helena', '(GMT+0000) Atlantic/St_Helena'), ('Atlantic/Stanley', '(GMT-0300) Atlantic/Stanley'), ('Australia/Adelaide', '(GMT+0930) Australia/Adelaide'), ('Australia/Brisbane', '(GMT+1000) Australia/Brisbane'), ('Australia/Broken_Hill', '(GMT+0930) Australia/Broken_Hill'), ('Australia/Currie', '(GMT+1000) Australia/Currie'), ('Australia/Darwin', '(GMT+0930) Australia/Darwin'), ('Australia/Eucla', '(GMT+0845) Australia/Eucla'), ('Australia/Hobart', '(GMT+1000) Australia/Hobart'), ('Australia/Lindeman', '(GMT+1000) Australia/Lindeman'), ('Australia/Lord_Howe', '(GMT+1030) Australia/Lord_Howe'), ('Australia/Melbourne', '(GMT+1000) Australia/Melbourne'), ('Australia/Perth', '(GMT+0800) Australia/Perth'), ('Australia/Sydney', '(GMT+1000) Australia/Sydney'), ('Canada/Atlantic', '(GMT-0300) Canada/Atlantic'), ('Canada/Central', '(GMT-0500) Canada/Central'), ('Canada/Eastern', '(GMT-0400) Canada/Eastern'), ('Canada/Mountain', '(GMT-0600) Canada/Mountain'), ('Canada/Newfoundland', '(GMT-0230) Canada/Newfoundland'), ('Canada/Pacific', '(GMT-0700) Canada/Pacific'), ('Europe/Amsterdam', '(GMT+0200) Europe/Amsterdam'), ('Europe/Andorra', '(GMT+0200) Europe/Andorra'), ('Europe/Astrakhan', '(GMT+0400) Europe/Astrakhan'), ('Europe/Athens', '(GMT+0300) Europe/Athens'), ('Europe/Belgrade', '(GMT+0200) Europe/Belgrade'), ('Europe/Berlin', '(GMT+0200) Europe/Berlin'), ('Europe/Bratislava', '(GMT+0200) Europe/Bratislava'), ('Europe/Brussels', '(GMT+0200) Europe/Brussels'), ('Europe/Bucharest', '(GMT+0300) Europe/Bucharest'), ('Europe/Budapest', '(GMT+0200) Europe/Budapest'), ('Europe/Busingen', '(GMT+0200) Europe/Busingen'), ('Europe/Chisinau', '(GMT+0300) Europe/Chisinau'), ('Europe/Copenhagen', '(GMT+0200) Europe/Copenhagen'), ('Europe/Dublin', '(GMT+0100) Europe/Dublin'), ('Europe/Gibraltar', '(GMT+0200) Europe/Gibraltar'), ('Europe/Guernsey', '(GMT+0100) Europe/Guernsey'), ('Europe/Helsinki', '(GMT+0300) Europe/Helsinki'), ('Europe/Isle_of_Man', '(GMT+0100) Europe/Isle_of_Man'), ('Europe/Istanbul', '(GMT+0300) Europe/Istanbul'), ('Europe/Jersey', '(GMT+0100) Europe/Jersey'), ('Europe/Kaliningrad', '(GMT+0200) Europe/Kaliningrad'), ('Europe/Kiev', '(GMT+0300) Europe/Kiev'), ('Europe/Kirov', '(GMT+0300) Europe/Kirov'), ('Europe/Lisbon', '(GMT+0100) Europe/Lisbon'), ('Europe/Ljubljana', '(GMT+0200) Europe/Ljubljana'), ('Europe/London', '(GMT+0100) Europe/London'), ('Europe/Luxembourg', '(GMT+0200) Europe/Luxembourg'), ('Europe/Madrid', '(GMT+0200) Europe/Madrid'), ('Europe/Malta', '(GMT+0200) Europe/Malta'), ('Europe/Mariehamn', '(GMT+0300) Europe/Mariehamn'), ('Europe/Minsk', '(GMT+0300) Europe/Minsk'), ('Europe/Monaco', '(GMT+0200) Europe/Monaco'), ('Europe/Moscow', '(GMT+0300) Europe/Moscow'), ('Europe/Oslo', '(GMT+0200) Europe/Oslo'), ('Europe/Paris', '(GMT+0200) Europe/Paris'), ('Europe/Podgorica', '(GMT+0200) Europe/Podgorica'), ('Europe/Prague', '(GMT+0200) Europe/Prague'), ('Europe/Riga', '(GMT+0300) Europe/Riga'), ('Europe/Rome', '(GMT+0200) Europe/Rome'), ('Europe/Samara', '(GMT+0400) Europe/Samara'), ('Europe/San_Marino', '(GMT+0200) Europe/San_Marino'), ('Europe/Sarajevo', '(GMT+0200) Europe/Sarajevo'), ('Europe/Saratov', '(GMT+0400) Europe/Saratov'), ('Europe/Simferopol', '(GMT+0300) Europe/Simferopol'), ('Europe/Skopje', '(GMT+0200) Europe/Skopje'), ('Europe/Sofia', '(GMT+0300) Europe/Sofia'), ('Europe/Stockholm', '(GMT+0200) Europe/Stockholm'), ('Europe/Tallinn', '(GMT+0300) Europe/Tallinn'), ('Europe/Tirane', '(GMT+0200) Europe/Tirane'), ('Europe/Ulyanovsk', '(GMT+0400) Europe/Ulyanovsk'), ('Europe/Uzhgorod', '(GMT+0300) Europe/Uzhgorod'), ('Europe/Vaduz', '(GMT+0200) Europe/Vaduz'), ('Europe/Vatican', '(GMT+0200) Europe/Vatican'), ('Europe/Vienna', '(GMT+0200) Europe/Vienna'), ('Europe/Vilnius', '(GMT+0300) Europe/Vilnius'), ('Europe/Volgograd', '(GMT+0400) Europe/Volgograd'), ('Europe/Warsaw', '(GMT+0200) Europe/Warsaw'), ('Europe/Zagreb', '(GMT+0200) Europe/Zagreb'), ('Europe/Zaporozhye', '(GMT+0300) Europe/Zaporozhye'), ('Europe/Zurich', '(GMT+0200) Europe/Zurich'), ('GMT', '(GMT+0000) GMT'), ('Indian/Antananarivo', '(GMT+0300) Indian/Antananarivo'), ('Indian/Chagos', '(GMT+0600) Indian/Chagos'), ('Indian/Christmas', '(GMT+0700) Indian/Christmas'), ('Indian/Cocos', '(GMT+0630) Indian/Cocos'), ('Indian/Comoro', '(GMT+0300) Indian/Comoro'), ('Indian/Kerguelen', '(GMT+0500) Indian/Kerguelen'), ('Indian/Mahe', '(GMT+0400) Indian/Mahe'), ('Indian/Maldives', '(GMT+0500) Indian/Maldives'), ('Indian/Mauritius', '(GMT+0400) Indian/Mauritius'), ('Indian/Mayotte', '(GMT+0300) Indian/Mayotte'), ('Indian/Reunion', '(GMT+0400) Indian/Reunion'), ('Pacific/Apia', '(GMT+1300) Pacific/Apia'), ('Pacific/Auckland', '(GMT+1200) Pacific/Auckland'), ('Pacific/Bougainville', '(GMT+1100) Pacific/Bougainville'), ('Pacific/Chatham', '(GMT+1245) Pacific/Chatham'), ('Pacific/Chuuk', '(GMT+1000) Pacific/Chuuk'), ('Pacific/Easter', '(GMT-0600) Pacific/Easter'), ('Pacific/Efate', '(GMT+1100) Pacific/Efate'), ('Pacific/Enderbury', '(GMT+1300) Pacific/Enderbury'), ('Pacific/Fakaofo', '(GMT+1300) Pacific/Fakaofo'), ('Pacific/Fiji', '(GMT+1200) Pacific/Fiji'), ('Pacific/Funafuti', '(GMT+1200) Pacific/Funafuti'), ('Pacific/Galapagos', '(GMT-0600) Pacific/Galapagos'), ('Pacific/Gambier', '(GMT-0900) Pacific/Gambier'), ('Pacific/Guadalcanal', '(GMT+1100) Pacific/Guadalcanal'), ('Pacific/Guam', '(GMT+1000) Pacific/Guam'), ('Pacific/Honolulu', '(GMT-1000) Pacific/Honolulu'), ('Pacific/Kiritimati', '(GMT+1400) Pacific/Kiritimati'), ('Pacific/Kosrae', '(GMT+1100) Pacific/Kosrae'), ('Pacific/Kwajalein', '(GMT+1200) Pacific/Kwajalein'), ('Pacific/Majuro', '(GMT+1200) Pacific/Majuro'), ('Pacific/Marquesas', '(GMT-0930) Pacific/Marquesas'), ('Pacific/Midway', '(GMT-1100) Pacific/Midway'), ('Pacific/Nauru', '(GMT+1200) Pacific/Nauru'), ('Pacific/Niue', '(GMT-1100) Pacific/Niue'), ('Pacific/Norfolk', '(GMT+1100) Pacific/Norfolk'), ('Pacific/Noumea', '(GMT+1100) Pacific/Noumea'), ('Pacific/Pago_Pago', '(GMT-1100) Pacific/Pago_Pago'), ('Pacific/Palau', '(GMT+0900) Pacific/Palau'), ('Pacific/Pitcairn', '(GMT-0800) Pacific/Pitcairn'), ('Pacific/Pohnpei', '(GMT+1100) Pacific/Pohnpei'), ('Pacific/Port_Moresby', '(GMT+1000) Pacific/Port_Moresby'), ('Pacific/Rarotonga', '(GMT-1000) Pacific/Rarotonga'), ('Pacific/Saipan', '(GMT+1000) Pacific/Saipan'), ('Pacific/Tahiti', '(GMT-1000) Pacific/Tahiti'), ('Pacific/Tarawa', '(GMT+1200) Pacific/Tarawa'), ('Pacific/Tongatapu', '(GMT+1300) Pacific/Tongatapu'), ('Pacific/Wake', '(GMT+1200) Pacific/Wake'), ('Pacific/Wallis', '(GMT+1200) Pacific/Wallis'), ('US/Alaska', '(GMT-0800) US/Alaska'), ('US/Arizona', '(GMT-0700) US/Arizona'), ('US/Central', '(GMT-0500) US/Central'), ('US/Eastern', '(GMT-0400) US/Eastern'), ('US/Hawaii', '(GMT-1000) US/Hawaii'), ('US/Mountain', '(GMT-0600) US/Mountain'), ('US/Pacific', '(GMT-0700) US/Pacific'), ('UTC', '(GMT+0000) UTC')], default='America/New_York', max_length=100), + model_name="profile", + name="timezone", + field=vendor.timezones.fields.TimeZoneField( + choices=[ + ("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"), + ("Africa/Accra", "(GMT+0000) Africa/Accra"), + ("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"), + ("Africa/Algiers", "(GMT+0100) Africa/Algiers"), + ("Africa/Asmara", "(GMT+0300) Africa/Asmara"), + ("Africa/Bamako", "(GMT+0000) Africa/Bamako"), + ("Africa/Bangui", "(GMT+0100) Africa/Bangui"), + ("Africa/Banjul", "(GMT+0000) Africa/Banjul"), + ("Africa/Bissau", "(GMT+0000) Africa/Bissau"), + ("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"), + ("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"), + ("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"), + ("Africa/Cairo", "(GMT+0200) Africa/Cairo"), + ("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"), + ("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"), + ("Africa/Conakry", "(GMT+0000) Africa/Conakry"), + ("Africa/Dakar", "(GMT+0000) Africa/Dakar"), + ("Africa/Dar_es_Salaam", "(GMT+0300) Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"), + ("Africa/Douala", "(GMT+0100) Africa/Douala"), + ("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"), + ("Africa/Freetown", "(GMT+0000) Africa/Freetown"), + ("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"), + ("Africa/Harare", "(GMT+0200) Africa/Harare"), + ("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"), + ("Africa/Juba", "(GMT+0300) Africa/Juba"), + ("Africa/Kampala", "(GMT+0300) Africa/Kampala"), + ("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"), + ("Africa/Kigali", "(GMT+0200) Africa/Kigali"), + ("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"), + ("Africa/Lagos", "(GMT+0100) Africa/Lagos"), + ("Africa/Libreville", "(GMT+0100) Africa/Libreville"), + ("Africa/Lome", "(GMT+0000) Africa/Lome"), + ("Africa/Luanda", "(GMT+0100) Africa/Luanda"), + ("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"), + ("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"), + ("Africa/Malabo", "(GMT+0100) Africa/Malabo"), + ("Africa/Maputo", "(GMT+0200) Africa/Maputo"), + ("Africa/Maseru", "(GMT+0200) Africa/Maseru"), + ("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"), + ("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"), + ("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"), + ("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"), + ("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"), + ("Africa/Niamey", "(GMT+0100) Africa/Niamey"), + ("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"), + ("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"), + ("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"), + ("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"), + ("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"), + ("Africa/Tunis", "(GMT+0100) Africa/Tunis"), + ("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"), + ("America/Adak", "(GMT-0900) America/Adak"), + ("America/Anchorage", "(GMT-0800) America/Anchorage"), + ("America/Anguilla", "(GMT-0400) America/Anguilla"), + ("America/Antigua", "(GMT-0400) America/Antigua"), + ("America/Araguaina", "(GMT-0300) America/Araguaina"), + ("America/Argentina/Buenos_Aires", "(GMT-0300) America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "(GMT-0300) America/Argentina/Catamarca"), + ("America/Argentina/Cordoba", "(GMT-0300) America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "(GMT-0300) America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "(GMT-0300) America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "(GMT-0300) America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "(GMT-0300) America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "(GMT-0300) America/Argentina/Salta"), + ("America/Argentina/San_Juan", "(GMT-0300) America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "(GMT-0300) America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "(GMT-0300) America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "(GMT-0300) America/Argentina/Ushuaia"), + ("America/Aruba", "(GMT-0400) America/Aruba"), + ("America/Asuncion", "(GMT-0400) America/Asuncion"), + ("America/Atikokan", "(GMT-0500) America/Atikokan"), + ("America/Bahia", "(GMT-0300) America/Bahia"), + ("America/Bahia_Banderas", "(GMT-0500) America/Bahia_Banderas"), + ("America/Barbados", "(GMT-0400) America/Barbados"), + ("America/Belem", "(GMT-0300) America/Belem"), + ("America/Belize", "(GMT-0600) America/Belize"), + ("America/Blanc-Sablon", "(GMT-0400) America/Blanc-Sablon"), + ("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"), + ("America/Bogota", "(GMT-0500) America/Bogota"), + ("America/Boise", "(GMT-0600) America/Boise"), + ("America/Cambridge_Bay", "(GMT-0600) America/Cambridge_Bay"), + ("America/Campo_Grande", "(GMT-0400) America/Campo_Grande"), + ("America/Cancun", "(GMT-0500) America/Cancun"), + ("America/Caracas", "(GMT-0400) America/Caracas"), + ("America/Cayenne", "(GMT-0300) America/Cayenne"), + ("America/Cayman", "(GMT-0500) America/Cayman"), + ("America/Chicago", "(GMT-0500) America/Chicago"), + ("America/Chihuahua", "(GMT-0600) America/Chihuahua"), + ("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"), + ("America/Creston", "(GMT-0700) America/Creston"), + ("America/Cuiaba", "(GMT-0400) America/Cuiaba"), + ("America/Curacao", "(GMT-0400) America/Curacao"), + ("America/Danmarkshavn", "(GMT+0000) America/Danmarkshavn"), + ("America/Dawson", "(GMT-0700) America/Dawson"), + ("America/Dawson_Creek", "(GMT-0700) America/Dawson_Creek"), + ("America/Denver", "(GMT-0600) America/Denver"), + ("America/Detroit", "(GMT-0400) America/Detroit"), + ("America/Dominica", "(GMT-0400) America/Dominica"), + ("America/Edmonton", "(GMT-0600) America/Edmonton"), + ("America/Eirunepe", "(GMT-0500) America/Eirunepe"), + ("America/El_Salvador", "(GMT-0600) America/El_Salvador"), + ("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"), + ("America/Fortaleza", "(GMT-0300) America/Fortaleza"), + ("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"), + ("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"), + ("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"), + ("America/Grenada", "(GMT-0400) America/Grenada"), + ("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"), + ("America/Guatemala", "(GMT-0600) America/Guatemala"), + ("America/Guayaquil", "(GMT-0500) America/Guayaquil"), + ("America/Guyana", "(GMT-0400) America/Guyana"), + ("America/Halifax", "(GMT-0300) America/Halifax"), + ("America/Havana", "(GMT-0400) America/Havana"), + ("America/Hermosillo", "(GMT-0700) America/Hermosillo"), + ("America/Indiana/Indianapolis", "(GMT-0400) America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "(GMT-0500) America/Indiana/Knox"), + ("America/Indiana/Marengo", "(GMT-0400) America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "(GMT-0400) America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "(GMT-0500) America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "(GMT-0400) America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "(GMT-0400) America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "(GMT-0400) America/Indiana/Winamac"), + ("America/Inuvik", "(GMT-0600) America/Inuvik"), + ("America/Iqaluit", "(GMT-0400) America/Iqaluit"), + ("America/Jamaica", "(GMT-0500) America/Jamaica"), + ("America/Juneau", "(GMT-0800) America/Juneau"), + ("America/Kentucky/Louisville", "(GMT-0400) America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "(GMT-0400) America/Kentucky/Monticello"), + ("America/Kralendijk", "(GMT-0400) America/Kralendijk"), + ("America/La_Paz", "(GMT-0400) America/La_Paz"), + ("America/Lima", "(GMT-0500) America/Lima"), + ("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"), + ("America/Lower_Princes", "(GMT-0400) America/Lower_Princes"), + ("America/Maceio", "(GMT-0300) America/Maceio"), + ("America/Managua", "(GMT-0600) America/Managua"), + ("America/Manaus", "(GMT-0400) America/Manaus"), + ("America/Marigot", "(GMT-0400) America/Marigot"), + ("America/Martinique", "(GMT-0400) America/Martinique"), + ("America/Matamoros", "(GMT-0500) America/Matamoros"), + ("America/Mazatlan", "(GMT-0600) America/Mazatlan"), + ("America/Menominee", "(GMT-0500) America/Menominee"), + ("America/Merida", "(GMT-0500) America/Merida"), + ("America/Metlakatla", "(GMT-0800) America/Metlakatla"), + ("America/Mexico_City", "(GMT-0500) America/Mexico_City"), + ("America/Miquelon", "(GMT-0200) America/Miquelon"), + ("America/Moncton", "(GMT-0300) America/Moncton"), + ("America/Monterrey", "(GMT-0500) America/Monterrey"), + ("America/Montevideo", "(GMT-0300) America/Montevideo"), + ("America/Montserrat", "(GMT-0400) America/Montserrat"), + ("America/Nassau", "(GMT-0400) America/Nassau"), + ("America/New_York", "(GMT-0400) America/New_York"), + ("America/Nipigon", "(GMT-0400) America/Nipigon"), + ("America/Nome", "(GMT-0800) America/Nome"), + ("America/Noronha", "(GMT-0200) America/Noronha"), + ("America/North_Dakota/Beulah", "(GMT-0500) America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "(GMT-0500) America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "(GMT-0500) America/North_Dakota/New_Salem"), + ("America/Nuuk", "(GMT-0200) America/Nuuk"), + ("America/Ojinaga", "(GMT-0600) America/Ojinaga"), + ("America/Panama", "(GMT-0500) America/Panama"), + ("America/Pangnirtung", "(GMT-0400) America/Pangnirtung"), + ("America/Paramaribo", "(GMT-0300) America/Paramaribo"), + ("America/Phoenix", "(GMT-0700) America/Phoenix"), + ("America/Port-au-Prince", "(GMT-0400) America/Port-au-Prince"), + ("America/Port_of_Spain", "(GMT-0400) America/Port_of_Spain"), + ("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"), + ("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"), + ("America/Punta_Arenas", "(GMT-0300) America/Punta_Arenas"), + ("America/Rainy_River", "(GMT-0500) America/Rainy_River"), + ("America/Rankin_Inlet", "(GMT-0500) America/Rankin_Inlet"), + ("America/Recife", "(GMT-0300) America/Recife"), + ("America/Regina", "(GMT-0600) America/Regina"), + ("America/Resolute", "(GMT-0500) America/Resolute"), + ("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"), + ("America/Santarem", "(GMT-0300) America/Santarem"), + ("America/Santiago", "(GMT-0400) America/Santiago"), + ("America/Santo_Domingo", "(GMT-0400) America/Santo_Domingo"), + ("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"), + ("America/Scoresbysund", "(GMT+0000) America/Scoresbysund"), + ("America/Sitka", "(GMT-0800) America/Sitka"), + ("America/St_Barthelemy", "(GMT-0400) America/St_Barthelemy"), + ("America/St_Johns", "(GMT-0230) America/St_Johns"), + ("America/St_Kitts", "(GMT-0400) America/St_Kitts"), + ("America/St_Lucia", "(GMT-0400) America/St_Lucia"), + ("America/St_Thomas", "(GMT-0400) America/St_Thomas"), + ("America/St_Vincent", "(GMT-0400) America/St_Vincent"), + ("America/Swift_Current", "(GMT-0600) America/Swift_Current"), + ("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"), + ("America/Thule", "(GMT-0300) America/Thule"), + ("America/Thunder_Bay", "(GMT-0400) America/Thunder_Bay"), + ("America/Tijuana", "(GMT-0700) America/Tijuana"), + ("America/Toronto", "(GMT-0400) America/Toronto"), + ("America/Tortola", "(GMT-0400) America/Tortola"), + ("America/Vancouver", "(GMT-0700) America/Vancouver"), + ("America/Whitehorse", "(GMT-0700) America/Whitehorse"), + ("America/Winnipeg", "(GMT-0500) America/Winnipeg"), + ("America/Yakutat", "(GMT-0800) America/Yakutat"), + ("America/Yellowknife", "(GMT-0600) America/Yellowknife"), + ("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"), + ("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"), + ("Antarctica/DumontDUrville", "(GMT+1000) Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "(GMT+1000) Antarctica/Macquarie"), + ("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"), + ("Antarctica/McMurdo", "(GMT+1200) Antarctica/McMurdo"), + ("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"), + ("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"), + ("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"), + ("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"), + ("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"), + ("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"), + ("Asia/Aden", "(GMT+0300) Asia/Aden"), + ("Asia/Almaty", "(GMT+0600) Asia/Almaty"), + ("Asia/Amman", "(GMT+0300) Asia/Amman"), + ("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"), + ("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"), + ("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"), + ("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"), + ("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"), + ("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"), + ("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"), + ("Asia/Baku", "(GMT+0400) Asia/Baku"), + ("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"), + ("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"), + ("Asia/Beirut", "(GMT+0300) Asia/Beirut"), + ("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"), + ("Asia/Brunei", "(GMT+0800) Asia/Brunei"), + ("Asia/Chita", "(GMT+0900) Asia/Chita"), + ("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"), + ("Asia/Colombo", "(GMT+0530) Asia/Colombo"), + ("Asia/Damascus", "(GMT+0300) Asia/Damascus"), + ("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"), + ("Asia/Dili", "(GMT+0900) Asia/Dili"), + ("Asia/Dubai", "(GMT+0400) Asia/Dubai"), + ("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"), + ("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"), + ("Asia/Gaza", "(GMT+0300) Asia/Gaza"), + ("Asia/Hebron", "(GMT+0300) Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"), + ("Asia/Hovd", "(GMT+0700) Asia/Hovd"), + ("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"), + ("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"), + ("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"), + ("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"), + ("Asia/Kabul", "(GMT+0430) Asia/Kabul"), + ("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"), + ("Asia/Karachi", "(GMT+0500) Asia/Karachi"), + ("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"), + ("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"), + ("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"), + ("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"), + ("Asia/Kuching", "(GMT+0800) Asia/Kuching"), + ("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"), + ("Asia/Macau", "(GMT+0800) Asia/Macau"), + ("Asia/Magadan", "(GMT+1100) Asia/Magadan"), + ("Asia/Makassar", "(GMT+0800) Asia/Makassar"), + ("Asia/Manila", "(GMT+0800) Asia/Manila"), + ("Asia/Muscat", "(GMT+0400) Asia/Muscat"), + ("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"), + ("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"), + ("Asia/Omsk", "(GMT+0600) Asia/Omsk"), + ("Asia/Oral", "(GMT+0500) Asia/Oral"), + ("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"), + ("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"), + ("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"), + ("Asia/Qatar", "(GMT+0300) Asia/Qatar"), + ("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"), + ("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"), + ("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"), + ("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"), + ("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"), + ("Asia/Seoul", "(GMT+0900) Asia/Seoul"), + ("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"), + ("Asia/Singapore", "(GMT+0800) Asia/Singapore"), + ("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"), + ("Asia/Taipei", "(GMT+0800) Asia/Taipei"), + ("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"), + ("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"), + ("Asia/Tehran", "(GMT+0430) Asia/Tehran"), + ("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"), + ("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"), + ("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"), + ("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"), + ("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"), + ("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"), + ("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"), + ("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"), + ("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"), + ("Asia/Yangon", "(GMT+0630) Asia/Yangon"), + ("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"), + ("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"), + ("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"), + ("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"), + ("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"), + ("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"), + ("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"), + ("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"), + ("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "(GMT-0200) Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"), + ("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"), + ("Australia/Adelaide", "(GMT+0930) Australia/Adelaide"), + ("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"), + ("Australia/Broken_Hill", "(GMT+0930) Australia/Broken_Hill"), + ("Australia/Currie", "(GMT+1000) Australia/Currie"), + ("Australia/Darwin", "(GMT+0930) Australia/Darwin"), + ("Australia/Eucla", "(GMT+0845) Australia/Eucla"), + ("Australia/Hobart", "(GMT+1000) Australia/Hobart"), + ("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"), + ("Australia/Lord_Howe", "(GMT+1030) Australia/Lord_Howe"), + ("Australia/Melbourne", "(GMT+1000) Australia/Melbourne"), + ("Australia/Perth", "(GMT+0800) Australia/Perth"), + ("Australia/Sydney", "(GMT+1000) Australia/Sydney"), + ("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"), + ("Canada/Central", "(GMT-0500) Canada/Central"), + ("Canada/Eastern", "(GMT-0400) Canada/Eastern"), + ("Canada/Mountain", "(GMT-0600) Canada/Mountain"), + ("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"), + ("Canada/Pacific", "(GMT-0700) Canada/Pacific"), + ("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"), + ("Europe/Andorra", "(GMT+0200) Europe/Andorra"), + ("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"), + ("Europe/Athens", "(GMT+0300) Europe/Athens"), + ("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"), + ("Europe/Berlin", "(GMT+0200) Europe/Berlin"), + ("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"), + ("Europe/Brussels", "(GMT+0200) Europe/Brussels"), + ("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"), + ("Europe/Budapest", "(GMT+0200) Europe/Budapest"), + ("Europe/Busingen", "(GMT+0200) Europe/Busingen"), + ("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"), + ("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"), + ("Europe/Dublin", "(GMT+0100) Europe/Dublin"), + ("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"), + ("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"), + ("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"), + ("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"), + ("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"), + ("Europe/Jersey", "(GMT+0100) Europe/Jersey"), + ("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"), + ("Europe/Kiev", "(GMT+0300) Europe/Kiev"), + ("Europe/Kirov", "(GMT+0300) Europe/Kirov"), + ("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"), + ("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"), + ("Europe/London", "(GMT+0100) Europe/London"), + ("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"), + ("Europe/Madrid", "(GMT+0200) Europe/Madrid"), + ("Europe/Malta", "(GMT+0200) Europe/Malta"), + ("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"), + ("Europe/Minsk", "(GMT+0300) Europe/Minsk"), + ("Europe/Monaco", "(GMT+0200) Europe/Monaco"), + ("Europe/Moscow", "(GMT+0300) Europe/Moscow"), + ("Europe/Oslo", "(GMT+0200) Europe/Oslo"), + ("Europe/Paris", "(GMT+0200) Europe/Paris"), + ("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"), + ("Europe/Prague", "(GMT+0200) Europe/Prague"), + ("Europe/Riga", "(GMT+0300) Europe/Riga"), + ("Europe/Rome", "(GMT+0200) Europe/Rome"), + ("Europe/Samara", "(GMT+0400) Europe/Samara"), + ("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"), + ("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"), + ("Europe/Saratov", "(GMT+0400) Europe/Saratov"), + ("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"), + ("Europe/Skopje", "(GMT+0200) Europe/Skopje"), + ("Europe/Sofia", "(GMT+0300) Europe/Sofia"), + ("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"), + ("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"), + ("Europe/Tirane", "(GMT+0200) Europe/Tirane"), + ("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "(GMT+0300) Europe/Uzhgorod"), + ("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"), + ("Europe/Vatican", "(GMT+0200) Europe/Vatican"), + ("Europe/Vienna", "(GMT+0200) Europe/Vienna"), + ("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"), + ("Europe/Volgograd", "(GMT+0400) Europe/Volgograd"), + ("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"), + ("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"), + ("Europe/Zaporozhye", "(GMT+0300) Europe/Zaporozhye"), + ("Europe/Zurich", "(GMT+0200) Europe/Zurich"), + ("GMT", "(GMT+0000) GMT"), + ("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"), + ("Indian/Chagos", "(GMT+0600) Indian/Chagos"), + ("Indian/Christmas", "(GMT+0700) Indian/Christmas"), + ("Indian/Cocos", "(GMT+0630) Indian/Cocos"), + ("Indian/Comoro", "(GMT+0300) Indian/Comoro"), + ("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"), + ("Indian/Mahe", "(GMT+0400) Indian/Mahe"), + ("Indian/Maldives", "(GMT+0500) Indian/Maldives"), + ("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"), + ("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"), + ("Indian/Reunion", "(GMT+0400) Indian/Reunion"), + ("Pacific/Apia", "(GMT+1300) Pacific/Apia"), + ("Pacific/Auckland", "(GMT+1200) Pacific/Auckland"), + ("Pacific/Bougainville", "(GMT+1100) Pacific/Bougainville"), + ("Pacific/Chatham", "(GMT+1245) Pacific/Chatham"), + ("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"), + ("Pacific/Easter", "(GMT-0600) Pacific/Easter"), + ("Pacific/Efate", "(GMT+1100) Pacific/Efate"), + ("Pacific/Enderbury", "(GMT+1300) Pacific/Enderbury"), + ("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"), + ("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"), + ("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"), + ("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"), + ("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"), + ("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"), + ("Pacific/Guam", "(GMT+1000) Pacific/Guam"), + ("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"), + ("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"), + ("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"), + ("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"), + ("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"), + ("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"), + ("Pacific/Midway", "(GMT-1100) Pacific/Midway"), + ("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"), + ("Pacific/Niue", "(GMT-1100) Pacific/Niue"), + ("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"), + ("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"), + ("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"), + ("Pacific/Palau", "(GMT+0900) Pacific/Palau"), + ("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"), + ("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"), + ("Pacific/Port_Moresby", "(GMT+1000) Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"), + ("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"), + ("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"), + ("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"), + ("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"), + ("Pacific/Wake", "(GMT+1200) Pacific/Wake"), + ("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"), + ("US/Alaska", "(GMT-0800) US/Alaska"), + ("US/Arizona", "(GMT-0700) US/Arizona"), + ("US/Central", "(GMT-0500) US/Central"), + ("US/Eastern", "(GMT-0400) US/Eastern"), + ("US/Hawaii", "(GMT-1000) US/Hawaii"), + ("US/Mountain", "(GMT-0600) US/Mountain"), + ("US/Pacific", "(GMT-0700) US/Pacific"), + ("UTC", "(GMT+0000) UTC"), + ], + default="America/New_York", + max_length=100, + ), ), ] diff --git a/apps/profile/models.py b/apps/profile/models.py index 2482c01660..1226cce810 100644 --- a/apps/profile/models.py +++ b/apps/profile/models.py @@ -1,84 +1,85 @@ -import time import datetime -from wsgiref.util import application_uri -import dateutil -import stripe import hashlib import re -import redis +import time import uuid -import paypalrestsdk +from wsgiref.util import application_uri + +import dateutil import mongoengine as mongo -from django.db import models -from django.db import IntegrityError -from django.db.utils import DatabaseError -from django.db.models.signals import post_save -from django.db.models import Sum, Avg, Count -from django.db.models import Q +import paypalrestsdk +import redis +import stripe from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.contrib.sites.models import Site -from django.core.mail import EmailMultiAlternatives -from django.core.mail import mail_admins -from django.urls import reverse +from django.core.mail import EmailMultiAlternatives, mail_admins +from django.db import IntegrityError, models +from django.db.models import Avg, Count, Q, Sum +from django.db.models.signals import post_save +from django.db.utils import DatabaseError from django.template.loader import render_to_string -from apps.rss_feeds.models import Feed, MStory, MStarredStory -from apps.rss_feeds.tasks import SchedulePremiumSetup +from django.urls import reverse +from paypal.standard.ipn.models import PayPalIPN +from paypal.standard.ipn.signals import invalid_ipn_received, valid_ipn_received +from zebra.signals import ( + zebra_webhook_charge_refunded, + zebra_webhook_charge_succeeded, + zebra_webhook_checkout_session_completed, + zebra_webhook_customer_subscription_created, + zebra_webhook_customer_subscription_updated, +) + from apps.feed_import.models import OPMLExporter -from apps.reader.models import UserSubscription -from apps.reader.models import RUserStory -from utils import log as logging +from apps.reader.models import RUserStory, UserSubscription +from apps.rss_feeds.models import Feed, MStarredStory, MStory +from apps.rss_feeds.tasks import SchedulePremiumSetup from utils import json_functions as json -from utils.user_functions import generate_secret_token +from utils import log as logging from utils.feed_functions import chunks +from utils.user_functions import generate_secret_token from vendor.timezones.fields import TimeZoneField -from paypal.standard.ipn.signals import valid_ipn_received, invalid_ipn_received -from paypal.standard.ipn.models import PayPalIPN -from zebra.signals import zebra_webhook_customer_subscription_created -from zebra.signals import zebra_webhook_customer_subscription_updated -from zebra.signals import zebra_webhook_charge_succeeded -from zebra.signals import zebra_webhook_charge_refunded -from zebra.signals import zebra_webhook_checkout_session_completed + class Profile(models.Model): - user = models.OneToOneField(User, unique=True, related_name="profile", on_delete=models.CASCADE) - is_premium = models.BooleanField(default=False) - is_archive = models.BooleanField(default=False, blank=True, null=True) - is_pro = models.BooleanField(default=False, blank=True, null=True) - premium_expire = models.DateTimeField(blank=True, null=True) - send_emails = models.BooleanField(default=True) - preferences = models.TextField(default="{}") - view_settings = models.TextField(default="{}") + user = models.OneToOneField(User, unique=True, related_name="profile", on_delete=models.CASCADE) + is_premium = models.BooleanField(default=False) + is_archive = models.BooleanField(default=False, blank=True, null=True) + is_pro = models.BooleanField(default=False, blank=True, null=True) + premium_expire = models.DateTimeField(blank=True, null=True) + send_emails = models.BooleanField(default=True) + preferences = models.TextField(default="{}") + view_settings = models.TextField(default="{}") collapsed_folders = models.TextField(default="[]") - feed_pane_size = models.IntegerField(default=282) - days_of_unread = models.IntegerField(default=settings.DAYS_OF_UNREAD, blank=True, null=True) + feed_pane_size = models.IntegerField(default=282) + days_of_unread = models.IntegerField(default=settings.DAYS_OF_UNREAD, blank=True, null=True) tutorial_finished = models.BooleanField(default=False) hide_getting_started = models.BooleanField(default=False, null=True, blank=True) - has_setup_feeds = models.BooleanField(default=False, null=True, blank=True) + has_setup_feeds = models.BooleanField(default=False, null=True, blank=True) has_found_friends = models.BooleanField(default=False, null=True, blank=True) has_trained_intelligence = models.BooleanField(default=False, null=True, blank=True) - last_seen_on = models.DateTimeField(default=datetime.datetime.now) - last_seen_ip = models.CharField(max_length=50, blank=True, null=True) - dashboard_date = models.DateTimeField(default=datetime.datetime.now) - timezone = TimeZoneField(default="America/New_York") - secret_token = models.CharField(max_length=12, blank=True, null=True) - stripe_4_digits = models.CharField(max_length=4, blank=True, null=True) - stripe_id = models.CharField(max_length=24, blank=True, null=True) - paypal_sub_id = models.CharField(max_length=24, blank=True, null=True) + last_seen_on = models.DateTimeField(default=datetime.datetime.now) + last_seen_ip = models.CharField(max_length=50, blank=True, null=True) + dashboard_date = models.DateTimeField(default=datetime.datetime.now) + timezone = TimeZoneField(default="America/New_York") + secret_token = models.CharField(max_length=12, blank=True, null=True) + stripe_4_digits = models.CharField(max_length=4, blank=True, null=True) + stripe_id = models.CharField(max_length=24, blank=True, null=True) + paypal_sub_id = models.CharField(max_length=24, blank=True, null=True) # paypal_payer_id = models.CharField(max_length=24, blank=True, null=True) - premium_renewal = models.BooleanField(default=False, blank=True, null=True) - active_provider = models.CharField(max_length=24, blank=True, null=True) - + premium_renewal = models.BooleanField(default=False, blank=True, null=True) + active_provider = models.CharField(max_length=24, blank=True, null=True) + def __str__(self): return "%s <%s>%s%s%s" % ( - self.user, - self.user.email, - " (Premium)" if self.is_premium and not self.is_archive and not self.is_pro else "", + self.user, + self.user.email, + " (Premium)" if self.is_premium and not self.is_archive and not self.is_pro else "", " (Premium ARCHIVE)" if self.is_archive and not self.is_pro else "", " (Premium PRO)" if self.is_pro else "", ) - + @classmethod def plan_to_stripe_price(cls, plan): price = None @@ -93,7 +94,7 @@ def plan_to_stripe_price(cls, plan): if settings.DEBUG: price = "price_0KK5twwdsmP8XBlasifbX56Z" return price - + @classmethod def plan_to_paypal_plan_id(cls, plan): price = None @@ -118,13 +119,13 @@ def unread_cutoff(self, force_premium=False, force_archive=False): return datetime.datetime.utcnow() - datetime.timedelta(days=days_of_unread) if self.is_premium or force_premium: return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD) - + return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD_FREE) @property def unread_cutoff_premium(self): return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD) - + @property def days_of_story_hashes(self): if self.is_archive: @@ -133,19 +134,19 @@ def days_of_story_hashes(self): def canonical(self): return { - 'is_premium': self.is_premium, - 'is_archive': self.is_archive, - 'is_pro': self.is_pro, - 'premium_expire': int(self.premium_expire.strftime('%s')) if self.premium_expire else 0, - 'preferences': json.decode(self.preferences), - 'tutorial_finished': self.tutorial_finished, - 'hide_getting_started': self.hide_getting_started, - 'has_setup_feeds': self.has_setup_feeds, - 'has_found_friends': self.has_found_friends, - 'has_trained_intelligence': self.has_trained_intelligence, - 'dashboard_date': self.dashboard_date + "is_premium": self.is_premium, + "is_archive": self.is_archive, + "is_pro": self.is_pro, + "premium_expire": int(self.premium_expire.strftime("%s")) if self.premium_expire else 0, + "preferences": json.decode(self.preferences), + "tutorial_finished": self.tutorial_finished, + "hide_getting_started": self.hide_getting_started, + "has_setup_feeds": self.has_setup_feeds, + "has_found_friends": self.has_found_friends, + "has_trained_intelligence": self.has_trained_intelligence, + "dashboard_date": self.dashboard_date, } - + def save(self, *args, **kwargs): if not self.secret_token: self.secret_token = generate_secret_token(self.user.username, 12) @@ -153,26 +154,34 @@ def save(self, *args, **kwargs): super(Profile, self).save(*args, **kwargs) except DatabaseError as e: print(f" ---> Profile not saved: {e}") - + def delete_user(self, confirm=False, fast=False): if not confirm: print(" ---> You must pass confirm=True to delete this user.") return - + logging.user(self.user, "Deleting user: %s / %s" % (self.user.email, self.user.profile.last_seen_ip)) try: if not fast: self.cancel_premium() except: logging.user(self.user, "~BR~SK~FWError cancelling premium renewal for: %s" % self.user.username) - - from apps.social.models import MSocialProfile, MSharedStory, MSocialSubscription - from apps.social.models import MActivity, MInteraction + + from apps.social.models import ( + MActivity, + MInteraction, + MSharedStory, + MSocialProfile, + MSocialSubscription, + ) + try: social_profile = MSocialProfile.objects.get(user_id=self.user.pk) - logging.user(self.user, "Unfollowing %s followings and %s followers" % - (social_profile.following_count, - social_profile.follower_count)) + logging.user( + self.user, + "Unfollowing %s followings and %s followers" + % (social_profile.following_count, social_profile.follower_count), + ) for follow in social_profile.following_user_ids: social_profile.unfollow_user(follow) for follower in social_profile.follower_user_ids: @@ -182,7 +191,7 @@ def delete_user(self, confirm=False, fast=False): except (MSocialProfile.DoesNotExist, IndexError): logging.user(self.user, " ***> No social profile found. S'ok, moving on.") pass - + shared_stories = MSharedStory.objects.filter(user_id=self.user.pk) logging.user(self.user, "Deleting %s shared stories" % shared_stories.count()) for story in shared_stories: @@ -193,54 +202,56 @@ def delete_user(self, confirm=False, fast=False): except MStory.DoesNotExist: pass story.delete() - + subscriptions = MSocialSubscription.objects.filter(subscription_user_id=self.user.pk) logging.user(self.user, "Deleting %s social subscriptions" % subscriptions.count()) subscriptions.delete() - + interactions = MInteraction.objects.filter(user_id=self.user.pk) logging.user(self.user, "Deleting %s interactions for user." % interactions.count()) interactions.delete() - + interactions = MInteraction.objects.filter(with_user_id=self.user.pk) logging.user(self.user, "Deleting %s interactions with user." % interactions.count()) interactions.delete() - + activities = MActivity.objects.filter(user_id=self.user.pk) logging.user(self.user, "Deleting %s activities for user." % activities.count()) activities.delete() - + activities = MActivity.objects.filter(with_user_id=self.user.pk) logging.user(self.user, "Deleting %s activities with user." % activities.count()) activities.delete() - + starred_stories = MStarredStory.objects.filter(user_id=self.user.pk) logging.user(self.user, "Deleting %s starred stories." % starred_stories.count()) starred_stories.delete() - + paypal_ids = PaypalIds.objects.filter(user=self.user) logging.user(self.user, "Deleting %s PayPal IDs." % paypal_ids.count()) paypal_ids.delete() - + stripe_ids = StripeIds.objects.filter(user=self.user) logging.user(self.user, "Deleting %s Stripe IDs." % stripe_ids.count()) stripe_ids.delete() - + logging.user(self.user, "Deleting user: %s" % self.user) self.user.delete() - + def activate_premium(self, never_expire=False): from apps.profile.tasks import EmailNewPremium - + EmailNewPremium.delay(user_id=self.user.pk) subs = UserSubscription.objects.filter(user=self.user) if subs.count() > 5000: logging.user(self.user, "~FR~SK~FW~SBWARNING! ~FR%s subscriptions~SN!" % (subs.count())) - mail_admins(f"WARNING! {self.user.username} has {subs.count()} subscriptions", - f"{self.user.username} has {subs.count()} subscriptions and just upgraded to premium. They'll need a refund: {self.user.profile.paypal_sub_id} {self.user.profile.stripe_id} {self.user.email}") + mail_admins( + f"WARNING! {self.user.username} has {subs.count()} subscriptions", + f"{self.user.username} has {subs.count()} subscriptions and just upgraded to premium. They'll need a refund: {self.user.profile.paypal_sub_id} {self.user.profile.stripe_id} {self.user.email}", + ) return False - + was_premium = self.is_premium self.is_premium = True self.is_archive = False @@ -248,48 +259,57 @@ def activate_premium(self, never_expire=False): self.save() self.user.is_active = True self.user.save() - + # Only auto-enable every feed if a free user is moving to premium if not was_premium: for sub in subs: - if sub.active: continue + if sub.active: + continue sub.active = True try: sub.save() except (IntegrityError, Feed.DoesNotExist): pass - + try: scheduled_feeds = [sub.feed.pk for sub in subs] except Feed.DoesNotExist: scheduled_feeds = [] - logging.user(self.user, "~SN~FMTasking the scheduling immediate premium setup of ~SB%s~SN feeds..." % - len(scheduled_feeds)) + logging.user( + self.user, + "~SN~FMTasking the scheduling immediate premium setup of ~SB%s~SN feeds..." + % len(scheduled_feeds), + ) SchedulePremiumSetup.apply_async(kwargs=dict(feed_ids=scheduled_feeds)) - + UserSubscription.queue_new_feeds(self.user) - + # self.setup_premium_history() # Let's not call this unnecessarily - + if never_expire: self.premium_expire = None self.save() if not was_premium: - logging.user(self.user, "~BY~SK~FW~SBNEW PREMIUM ACCOUNT! WOOHOO!!! ~FR%s subscriptions~SN!" % (subs.count())) - + logging.user( + self.user, + "~BY~SK~FW~SBNEW PREMIUM ACCOUNT! WOOHOO!!! ~FR%s subscriptions~SN!" % (subs.count()), + ) + return True - + def activate_archive(self, never_expire=False): UserSubscription.schedule_fetch_archive_feeds_for_user(self.user.pk) - + subs = UserSubscription.objects.filter(user=self.user) if subs.count() > 2000: logging.user(self.user, "~FR~SK~FW~SBWARNING! ~FR%s subscriptions~SN!" % (subs.count())) - mail_admins(f"WARNING! {self.user.username} has {subs.count()} subscriptions", - f"{self.user.username} has {subs.count()} subscriptions and just upgraded to archive. They'll need a refund: {self.user.profile.paypal_sub_id} {self.user.profile.stripe_id} {self.user.email}") + mail_admins( + f"WARNING! {self.user.username} has {subs.count()} subscriptions", + f"{self.user.username} has {subs.count()} subscriptions and just upgraded to archive. They'll need a refund: {self.user.profile.paypal_sub_id} {self.user.profile.stripe_id} {self.user.email}", + ) return False - + was_premium = self.is_premium was_archive = self.is_archive was_pro = self.is_pro @@ -298,52 +318,62 @@ def activate_archive(self, never_expire=False): self.save() self.user.is_active = True self.user.save() - + # Only auto-enable every feed if a free user is moving to premium if not was_premium: for sub in subs: - if sub.active: continue + if sub.active: + continue sub.active = True try: sub.save() except (IntegrityError, Feed.DoesNotExist): pass - + # Count subscribers to turn on archive_subscribers counts, then show that count to users # on the paypal_archive_return page. try: scheduled_feeds = [sub.feed.pk for sub in subs] except Feed.DoesNotExist: scheduled_feeds = [] - logging.user(self.user, "~SN~FMTasking the scheduling immediate premium setup of ~SB%s~SN feeds..." % - len(scheduled_feeds)) + logging.user( + self.user, + "~SN~FMTasking the scheduling immediate premium setup of ~SB%s~SN feeds..." + % len(scheduled_feeds), + ) SchedulePremiumSetup.apply_async(kwargs=dict(feed_ids=scheduled_feeds)) UserSubscription.queue_new_feeds(self.user) - + self.setup_premium_history() - + if never_expire: self.premium_expire = None self.save() if not was_archive: - logging.user(self.user, "~BY~SK~FW~SBNEW PREMIUM ~BBARCHIVE~BY ACCOUNT! WOOHOO!!! ~FR%s subscriptions~SN!" % (subs.count())) - + logging.user( + self.user, + "~BY~SK~FW~SBNEW PREMIUM ~BBARCHIVE~BY ACCOUNT! WOOHOO!!! ~FR%s subscriptions~SN!" + % (subs.count()), + ) + return True - + def activate_pro(self, never_expire=False): from apps.profile.tasks import EmailNewPremiumPro - + EmailNewPremiumPro.delay(user_id=self.user.pk) - + subs = UserSubscription.objects.filter(user=self.user) if subs.count() > 1000: logging.user(self.user, "~FR~SK~FW~SBWARNING! ~FR%s subscriptions~SN!" % (subs.count())) - mail_admins(f"WARNING! {self.user.username} has {subs.count()} subscriptions", - f"{self.user.username} has {subs.count()} subscriptions and just upgraded to pro. They'll need a refund: {self.user.profile.paypal_sub_id} {self.user.profile.stripe_id} {self.user.email}") + mail_admins( + f"WARNING! {self.user.username} has {subs.count()} subscriptions", + f"{self.user.username} has {subs.count()} subscriptions and just upgraded to pro. They'll need a refund: {self.user.profile.paypal_sub_id} {self.user.profile.stripe_id} {self.user.email}", + ) return False - + was_premium = self.is_premium was_archive = self.is_archive was_pro = self.is_pro @@ -353,44 +383,52 @@ def activate_pro(self, never_expire=False): self.save() self.user.is_active = True self.user.save() - + # Only auto-enable every feed if a free user is moving to premium if not was_premium: for sub in subs: - if sub.active: continue + if sub.active: + continue sub.active = True try: sub.save() except (IntegrityError, Feed.DoesNotExist): pass - + try: scheduled_feeds = [sub.feed.pk for sub in subs] except Feed.DoesNotExist: scheduled_feeds = [] - logging.user(self.user, "~SN~FMTasking the scheduling immediate premium setup of ~SB%s~SN feeds..." % - len(scheduled_feeds)) + logging.user( + self.user, + "~SN~FMTasking the scheduling immediate premium setup of ~SB%s~SN feeds..." + % len(scheduled_feeds), + ) SchedulePremiumSetup.apply_async(kwargs=dict(feed_ids=scheduled_feeds)) - + UserSubscription.queue_new_feeds(self.user) - + self.setup_premium_history() - + if never_expire: self.premium_expire = None self.save() if not was_pro: - logging.user(self.user, "~BY~SK~FW~SBNEW PREMIUM ~BGPRO~BY ACCOUNT! WOOHOO!!! ~FR%s subscriptions~SN!" % (subs.count())) - + logging.user( + self.user, + "~BY~SK~FW~SBNEW PREMIUM ~BGPRO~BY ACCOUNT! WOOHOO!!! ~FR%s subscriptions~SN!" + % (subs.count()), + ) + return True - + def deactivate_premium(self): self.is_premium = False self.is_pro = False self.is_archive = False self.save() - + subs = UserSubscription.objects.filter(user=self.user) for sub in subs: sub.active = False @@ -400,57 +438,61 @@ def deactivate_premium(self): # sub.feed.setup_feed_for_premium_subscribers() except (IntegrityError, Feed.DoesNotExist): pass - - logging.user(self.user, "~BY~FW~SBBOO! Deactivating premium account: ~FR%s subscriptions~SN!" % (subs.count())) - + + logging.user( + self.user, "~BY~FW~SBBOO! Deactivating premium account: ~FR%s subscriptions~SN!" % (subs.count()) + ) + def activate_free(self): if self.user.is_active: return - + self.user.is_active = True self.user.save() self.send_new_user_queue_email() - + def paypal_change_billing_details_url(self): return "https://paypal.com" - + def switch_stripe_subscription(self, plan): stripe_customer = self.stripe_customer() if not stripe_customer: return - + stripe_subscriptions = stripe.Subscription.list(customer=stripe_customer.id).data existing_subscription = None for subscription in stripe_subscriptions: if subscription.plan.active: existing_subscription = subscription break - if not existing_subscription: + if not existing_subscription: return try: stripe.Subscription.modify( existing_subscription.id, cancel_at_period_end=False, - proration_behavior='always_invoice', - items=[{ - 'id': existing_subscription['items']['data'][0].id, - 'price': Profile.plan_to_stripe_price(plan) - }] + proration_behavior="always_invoice", + items=[ + { + "id": existing_subscription["items"]["data"][0].id, + "price": Profile.plan_to_stripe_price(plan), + } + ], ) except stripe.error.CardError as e: logging.user(self.user, f"~FRStripe switch subscription failed: ~SB{e}") return - + self.setup_premium_history() - + return True def cancel_and_prorate_existing_paypal_subscriptions(self, data): paypal_api = self.paypal_api() if not paypal_api: return - + canceled_paypal_sub_id = self.cancel_premium_paypal(cancel_older_subscriptions_only=True) if not canceled_paypal_sub_id: logging.user(self.user, f"~FRCould not cancel and prorate older paypal premium: {data}") @@ -463,36 +505,43 @@ def switch_paypal_subscription_approval_url(self, plan): paypal_api = self.paypal_api() if not paypal_api: return - paypal_return = reverse('paypal-return') + paypal_return = reverse("paypal-return") if plan == "archive": - paypal_return = reverse('paypal-archive-return') + paypal_return = reverse("paypal-archive-return") try: application_context = { - 'shipping_preference': 'NO_SHIPPING', - 'user_action': 'SUBSCRIBE_NOW', + "shipping_preference": "NO_SHIPPING", + "user_action": "SUBSCRIBE_NOW", } if settings.DEBUG: - application_context['return_url'] = f"https://a6d3-161-77-224-226.ngrok.io{paypal_return}" + application_context["return_url"] = f"https://a6d3-161-77-224-226.ngrok.io{paypal_return}" else: - application_context['return_url'] = f"https://{Site.objects.get_current().domain}{paypal_return}" - paypal_subscription = paypal_api.post(f'/v1/billing/subscriptions', { - 'plan_id': Profile.plan_to_paypal_plan_id(plan), - 'custom_id': self.user.pk, - 'application_context': application_context, - }) + application_context[ + "return_url" + ] = f"https://{Site.objects.get_current().domain}{paypal_return}" + paypal_subscription = paypal_api.post( + f"/v1/billing/subscriptions", + { + "plan_id": Profile.plan_to_paypal_plan_id(plan), + "custom_id": self.user.pk, + "application_context": application_context, + }, + ) except paypalrestsdk.ResourceNotFound as e: - logging.user(self.user, f"~FRCouldn't create paypal subscription: {self.paypal_sub_id} {plan}: {e}") + logging.user( + self.user, f"~FRCouldn't create paypal subscription: {self.paypal_sub_id} {plan}: {e}" + ) paypal_subscription = None if not paypal_subscription: return logging.user(self.user, paypal_subscription) - - for link in paypal_subscription.get('links', []): - if link['rel'] == 'approve': - return link['href'] - + + for link in paypal_subscription.get("links", []): + if link["rel"] == "approve": + return link["href"] + logging.user(self.user, f"~FRFailed to switch paypal subscription: ~FC{paypal_subscription}") def store_paypal_sub_id(self, paypal_sub_id, skip_save_primary=False): @@ -503,12 +552,12 @@ def store_paypal_sub_id(self, paypal_sub_id, skip_save_primary=False): if not skip_save_primary or not self.paypal_sub_id: self.paypal_sub_id = paypal_sub_id self.save() - + seen_paypal_ids = set(p.paypal_sub_id for p in self.user.paypal_ids.all()) if paypal_sub_id in seen_paypal_ids: logging.user(self.user, f"~FBPaypal sub seen before, ignoring: {paypal_sub_id}") return - + self.user.paypal_ids.create(paypal_sub_id=paypal_sub_id) logging.user(self.user, f"~FBPaypal sub ~SBadded~SN: ~SB{paypal_sub_id}") @@ -519,7 +568,7 @@ def setup_premium_history(self, alt_email=None, set_premium_expire=True, force_e active_plan = None premium_renewal = False active_provider = None - + # Find modern Paypal payments self.retrieve_paypal_ids() if self.paypal_sub_id: @@ -534,76 +583,92 @@ def setup_premium_history(self, alt_email=None, set_premium_expire=True, force_e seen_payments.add(payment.payment_date.date()) total_paypal_payments += 1 if deleted_paypal_payments > 0: - logging.user(self.user, f"~BY~SN~FRDeleting~FW duplicate paypal history: ~SB{deleted_paypal_payments} payments") + logging.user( + self.user, + f"~BY~SN~FRDeleting~FW duplicate paypal history: ~SB{deleted_paypal_payments} payments", + ) paypal_api = self.paypal_api() for paypal_id_model in self.user.paypal_ids.all(): paypal_id = paypal_id_model.paypal_sub_id try: - paypal_subscription = paypal_api.get(f'/v1/billing/subscriptions/{paypal_id}?fields=plan') + paypal_subscription = paypal_api.get(f"/v1/billing/subscriptions/{paypal_id}?fields=plan") except paypalrestsdk.ResourceNotFound: logging.user(self.user, f"~FRCouldn't find paypal payments: {paypal_id}") paypal_subscription = None if paypal_subscription: - if paypal_subscription['status'] in ["APPROVAL_PENDING", "APPROVED", "ACTIVE"]: - active_plan = paypal_subscription.get('plan_id', None) + if paypal_subscription["status"] in ["APPROVAL_PENDING", "APPROVED", "ACTIVE"]: + active_plan = paypal_subscription.get("plan_id", None) if not active_plan: - active_plan = paypal_subscription['plan']['name'] + active_plan = paypal_subscription["plan"]["name"] active_provider = "paypal" premium_renewal = True start_date = datetime.datetime(2009, 1, 1).strftime("%Y-%m-%dT%H:%M:%S.000Z") end_date = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z") try: - transactions = paypal_api.get(f"/v1/billing/subscriptions/{paypal_id}/transactions?start_time={start_date}&end_time={end_date}") + transactions = paypal_api.get( + f"/v1/billing/subscriptions/{paypal_id}/transactions?start_time={start_date}&end_time={end_date}" + ) except paypalrestsdk.exceptions.ResourceNotFound: transactions = None - if not transactions or 'transactions' not in transactions: + if not transactions or "transactions" not in transactions: logging.user(self.user, f"~FRCouldn't find paypal transactions: ~SB{paypal_id}") continue - for transaction in transactions['transactions']: - created = dateutil.parser.parse(transaction['time']).date() - if transaction['status'] not in ['COMPLETED', 'PARTIALLY_REFUNDED', 'REFUNDED']: continue - if created in seen_payments: continue + for transaction in transactions["transactions"]: + created = dateutil.parser.parse(transaction["time"]).date() + if transaction["status"] not in ["COMPLETED", "PARTIALLY_REFUNDED", "REFUNDED"]: + continue + if created in seen_payments: + continue seen_payments.add(created) total_paypal_payments += 1 refunded = None - if transaction['status'] in ['PARTIALLY_REFUNDED', 'REFUNDED']: + if transaction["status"] in ["PARTIALLY_REFUNDED", "REFUNDED"]: refunded = True - PaymentHistory.objects.get_or_create(user=self.user, - payment_date=created, - payment_amount=int(float(transaction['amount_with_breakdown']['gross_amount']['value'])), - payment_provider='paypal', - refunded=refunded) - - ipns = PayPalIPN.objects.filter(Q(custom=self.user.username) | - Q(payer_email=self.user.email) | - Q(custom=self.user.pk)).order_by('-payment_date') + PaymentHistory.objects.get_or_create( + user=self.user, + payment_date=created, + payment_amount=int( + float(transaction["amount_with_breakdown"]["gross_amount"]["value"]) + ), + payment_provider="paypal", + refunded=refunded, + ) + + ipns = PayPalIPN.objects.filter( + Q(custom=self.user.username) | Q(payer_email=self.user.email) | Q(custom=self.user.pk) + ).order_by("-payment_date") for transaction in ipns: if transaction.txn_type != "subscr_payment": continue created = transaction.payment_date.date() - if created in seen_payments: + if created in seen_payments: continue seen_payments.add(created) total_paypal_payments += 1 - PaymentHistory.objects.get_or_create(user=self.user, - payment_date=created, - payment_amount=int(transaction.payment_gross), - payment_provider='paypal') + PaymentHistory.objects.get_or_create( + user=self.user, + payment_date=created, + payment_amount=int(transaction.payment_gross), + payment_provider="paypal", + ) else: logging.user(self.user, "~FBNo Paypal payments") - + # Record Stripe payments - existing_stripe_history = PaymentHistory.objects.filter(user=self.user, - payment_provider="stripe") + existing_stripe_history = PaymentHistory.objects.filter(user=self.user, payment_provider="stripe") if existing_stripe_history.count(): - logging.user(self.user, "~BY~SN~FRDeleting~FW existing stripe history: ~SB%s payments" % existing_stripe_history.count()) + logging.user( + self.user, + "~BY~SN~FRDeleting~FW existing stripe history: ~SB%s payments" + % existing_stripe_history.count(), + ) existing_stripe_history.delete() - + if self.stripe_id: self.retrieve_stripe_ids() - + stripe.api_key = settings.STRIPE_SECRET seen_payments = set() for stripe_id_model in self.user.stripe_ids.all(): @@ -611,7 +676,7 @@ def setup_premium_history(self, alt_email=None, set_premium_expire=True, force_e stripe_customer = stripe.Customer.retrieve(stripe_id) stripe_payments = stripe.Charge.list(customer=stripe_customer.id).data stripe_subscriptions = stripe.Subscription.list(customer=stripe_customer.id).data - + for subscription in stripe_subscriptions: if subscription.plan.active: active_plan = subscription.plan.id @@ -619,21 +684,25 @@ def setup_premium_history(self, alt_email=None, set_premium_expire=True, force_e if not subscription.cancel_at: premium_renewal = True break - + for payment in stripe_payments: created = datetime.datetime.fromtimestamp(payment.created) - if payment.status == 'failed': continue - if created in seen_payments: continue + if payment.status == "failed": + continue + if created in seen_payments: + continue seen_payments.add(created) total_stripe_payments += 1 refunded = None if payment.refunded: refunded = True - PaymentHistory.objects.get_or_create(user=self.user, - payment_date=created, - payment_amount=payment.amount / 100.0, - payment_provider='stripe', - refunded=refunded) + PaymentHistory.objects.get_or_create( + user=self.user, + payment_date=created, + payment_amount=payment.amount / 100.0, + payment_provider="stripe", + refunded=refunded, + ) else: logging.user(self.user, "~FBNo Stripe payments") @@ -655,14 +724,17 @@ def setup_premium_history(self, alt_email=None, set_premium_expire=True, force_e recent_payments_count += 1 if not oldest_recent_payment_date or payment.payment_date < oldest_recent_payment_date: oldest_recent_payment_date = payment.payment_date - + if oldest_recent_payment_date: - new_premium_expire = (oldest_recent_payment_date + - datetime.timedelta(days=365*recent_payments_count)) + new_premium_expire = oldest_recent_payment_date + datetime.timedelta( + days=365 * recent_payments_count + ) # Only move premium expire forward, never earlier. Also set expiration if not premium. - if (force_expiration or - (set_premium_expire and not self.premium_expire and not free_lifetime_premium) or - (self.premium_expire and new_premium_expire > self.premium_expire)): + if ( + force_expiration + or (set_premium_expire and not self.premium_expire and not free_lifetime_premium) + or (self.premium_expire and new_premium_expire > self.premium_expire) + ): self.premium_expire = new_premium_expire self.save() @@ -670,28 +742,43 @@ def setup_premium_history(self, alt_email=None, set_premium_expire=True, force_e active_sub_id = self.stripe_id if active_provider == "paypal": active_sub_id = self.paypal_sub_id - logging.user(self.user, "~FCTurning ~SB~%s~SN~FC premium renewal (%s: %s)" % ("FRoff" if not premium_renewal else "FBon", active_provider, active_sub_id)) + logging.user( + self.user, + "~FCTurning ~SB~%s~SN~FC premium renewal (%s: %s)" + % ("FRoff" if not premium_renewal else "FBon", active_provider, active_sub_id), + ) self.premium_renewal = premium_renewal self.active_provider = active_provider self.save() - - logging.user(self.user, "~BY~SN~FWFound ~SB~FB%s paypal~FW~SN and ~SB~FC%s stripe~FW~SN payments (~SB%s payments expire: ~SN~FB%s~FW)" % ( - total_paypal_payments, total_stripe_payments, len(payment_history), self.premium_expire)) - if (set_premium_expire and not self.is_premium and - self.premium_expire > datetime.datetime.now()): + logging.user( + self.user, + "~BY~SN~FWFound ~SB~FB%s paypal~FW~SN and ~SB~FC%s stripe~FW~SN payments (~SB%s payments expire: ~SN~FB%s~FW)" + % (total_paypal_payments, total_stripe_payments, len(payment_history), self.premium_expire), + ) + + if set_premium_expire and not self.is_premium and self.premium_expire > datetime.datetime.now(): self.activate_premium() - - logging.user(self.user, "~FCActive plan: %s, stripe/paypal: %s/%s, is_archive? %s" % (active_plan, Profile.plan_to_stripe_price('archive'), Profile.plan_to_paypal_plan_id('archive'), self.is_archive)) - if (active_plan == Profile.plan_to_stripe_price('pro') and not self.is_pro): + + logging.user( + self.user, + "~FCActive plan: %s, stripe/paypal: %s/%s, is_archive? %s" + % ( + active_plan, + Profile.plan_to_stripe_price("archive"), + Profile.plan_to_paypal_plan_id("archive"), + self.is_archive, + ), + ) + if active_plan == Profile.plan_to_stripe_price("pro") and not self.is_pro: self.activate_pro() - elif (active_plan == Profile.plan_to_stripe_price('archive') and not self.is_archive): + elif active_plan == Profile.plan_to_stripe_price("archive") and not self.is_archive: self.activate_archive() - elif (active_plan == Profile.plan_to_paypal_plan_id('pro') and not self.is_pro): + elif active_plan == Profile.plan_to_paypal_plan_id("pro") and not self.is_pro: self.activate_pro() - elif (active_plan == Profile.plan_to_paypal_plan_id('archive') and not self.is_archive): + elif active_plan == Profile.plan_to_paypal_plan_id("archive") and not self.is_archive: self.activate_archive() - + def preference_value(self, key, default=None): preferences = json.decode(self.preferences) return preferences.get(key, default) @@ -700,8 +787,7 @@ def preference_value(self, key, default=None): def resync_stripe_and_paypal_history(cls, start_days=365, end_days=0, skip=0): start_date = datetime.datetime.now() - datetime.timedelta(days=start_days) end_date = datetime.datetime.now() - datetime.timedelta(days=end_days) - payments = PaymentHistory.objects.filter(payment_date__gte=start_date, - payment_date__lte=end_date) + payments = PaymentHistory.objects.filter(payment_date__gte=start_date, payment_date__lte=end_date) last_seen_date = None for p, payment in enumerate(payments): if p < skip: @@ -711,30 +797,30 @@ def resync_stripe_and_paypal_history(cls, start_days=365, end_days=0, skip=0): if payment.payment_date.date() != last_seen_date: last_seen_date = payment.payment_date.date() print(f" ---> Payment date: {last_seen_date} (#{p})") - + payment.user.profile.setup_premium_history() @classmethod def reimport_stripe_history(cls, limit=10, days=7, starting_after=None): stripe.api_key = settings.STRIPE_SECRET - week = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime('%s') + week = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime("%s") failed = [] i = 0 - + while True: logging.debug(" ---> At %s / %s" % (i, starting_after)) i += 1 try: - data = stripe.Charge.list(created={'gt': week}, count=limit, starting_after=starting_after) + data = stripe.Charge.list(created={"gt": week}, count=limit, starting_after=starting_after) except stripe.error.APIConnectionError: time.sleep(10) continue - charges = data['data'] + charges = data["data"] if not len(charges): logging.debug("At %s (%s), finished" % (i, starting_after)) break starting_after = charges[-1]["id"] - customers = [c['customer'] for c in charges if 'customer' in c] + customers = [c["customer"] for c in charges if "customer" in c] for customer in customers: if not customer: print(" ***> No customer!") @@ -758,8 +844,8 @@ def reimport_stripe_history(cls, limit=10, days=7, starting_after=None): time.sleep(2) continue - return ','.join(failed) - + return ",".join(failed) + def refund_premium(self, partial=False, provider=None): refunded = False if provider == "paypal": @@ -770,24 +856,27 @@ def refund_premium(self, partial=False, provider=None): # self.cancel_premium_stripe() else: # Find last payment, refund that - payment_history = PaymentHistory.objects.filter(user=self.user, - payment_provider__in=['paypal', 'stripe']) + payment_history = PaymentHistory.objects.filter( + user=self.user, payment_provider__in=["paypal", "stripe"] + ) if payment_history.count(): provider = payment_history[0].payment_provider if provider == "stripe": refunded = self.refund_latest_stripe_payment(partial=partial) # self.cancel_premium_stripe() elif provider == "paypal": - refunded = self.refund_paypal_payment_from_subscription(self.paypal_sub_id, prorate=partial) + refunded = self.refund_paypal_payment_from_subscription( + self.paypal_sub_id, prorate=partial + ) self.cancel_premium_paypal() return refunded - + def refund_latest_stripe_payment(self, partial=False): refunded = False if not self.stripe_id: return - + stripe.api_key = settings.STRIPE_SECRET stripe_customer = stripe.Customer.retrieve(self.stripe_id) stripe_payments = stripe.Charge.list(customer=stripe_customer.id).data @@ -797,116 +886,128 @@ def refund_latest_stripe_payment(self, partial=False): else: stripe_payments[0].refund() self.cancel_premium_stripe() - refunded = stripe_payments[0].amount/100 - + refunded = stripe_payments[0].amount / 100 + logging.user(self.user, "~FRRefunding stripe payment: $%s" % refunded) return refunded - + def refund_paypal_payment_from_subscription(self, paypal_sub_id, prorate=False): - if not paypal_sub_id: + if not paypal_sub_id: return - + paypal_api = self.paypal_api() refunded = False # Find transaction from subscription now = datetime.datetime.now() + datetime.timedelta(days=1) # 200 days captures Paypal's 180 day limit on refunds - start_date = (now-datetime.timedelta(days=200)).strftime("%Y-%m-%dT%H:%M:%SZ") + start_date = (now - datetime.timedelta(days=200)).strftime("%Y-%m-%dT%H:%M:%SZ") end_date = now.strftime("%Y-%m-%dT%H:%M:%SZ") try: - transactions = paypal_api.get(f"/v1/billing/subscriptions/{paypal_sub_id}/transactions?start_time={start_date}&end_time={end_date}") + transactions = paypal_api.get( + f"/v1/billing/subscriptions/{paypal_sub_id}/transactions?start_time={start_date}&end_time={end_date}" + ) except paypalrestsdk.ResourceNotFound: transactions = {} - if 'transactions' not in transactions or not len(transactions['transactions']): - logging.user(self.user, f"~FRCouldn't find paypal transactions for refund: {paypal_sub_id} {transactions}") + if "transactions" not in transactions or not len(transactions["transactions"]): + logging.user( + self.user, f"~FRCouldn't find paypal transactions for refund: {paypal_sub_id} {transactions}" + ) return - + # Refund the latest transaction - transaction = transactions['transactions'][0] - today = datetime.datetime.now().strftime('%B %d, %Y') + transaction = transactions["transactions"][0] + today = datetime.datetime.now().strftime("%B %d, %Y") url = f"/v2/payments/captures/{transaction['id']}/refund" - refund_amount = float(transaction['amount_with_breakdown']['gross_amount']['value']) + refund_amount = float(transaction["amount_with_breakdown"]["gross_amount"]["value"]) if prorate: - transaction_date = dateutil.parser.parse(transaction['time']) + transaction_date = dateutil.parser.parse(transaction["time"]) days_since = (datetime.datetime.now() - transaction_date.replace(tzinfo=None)).days if days_since < 365: - days_left = (365 - days_since) - pct_left = days_left/365 + days_left = 365 - days_since + pct_left = days_left / 365 refund_amount = pct_left * refund_amount else: logging.user(self.user, f"~FRCouldn't prorate paypal payment, too old: ~SB{transaction}") try: - response = paypal_api.post(url, { - 'reason': f"Refunded on {today}", - 'amount': { - 'currency_code': 'USD', - 'value': f"{refund_amount:.2f}", - } - }) + response = paypal_api.post( + url, + { + "reason": f"Refunded on {today}", + "amount": { + "currency_code": "USD", + "value": f"{refund_amount:.2f}", + }, + }, + ) except paypalrestsdk.exceptions.ResourceInvalid as e: response = e.response.json() - if len(response.get('details', [])): - response = response['details'][0]['description'] + if len(response.get("details", [])): + response = response["details"][0]["description"] if settings.DEBUG: logging.user(self.user, f"Paypal refund response: {response}") - if 'status' in response and response['status'] == "COMPLETED": - refunded = int(float(transaction['amount_with_breakdown']['gross_amount']['value'])) + if "status" in response and response["status"] == "COMPLETED": + refunded = int(float(transaction["amount_with_breakdown"]["gross_amount"]["value"])) logging.user(self.user, "~FRRefunding paypal payment: $%s/%s" % (refund_amount, refunded)) else: logging.user(self.user, "~FRCouldn't refund paypal payment: %s" % response) refunded = response - + return refunded - + def cancel_premium(self): paypal_cancel = self.cancel_premium_paypal() stripe_cancel = self.cancel_premium_stripe() - self.setup_premium_history() # Sure, webhooks will force new history, but they take forever + self.setup_premium_history() # Sure, webhooks will force new history, but they take forever return stripe_cancel or paypal_cancel - + def cancel_premium_paypal(self, cancel_older_subscriptions_only=False): self.retrieve_paypal_ids() if not self.paypal_sub_id: logging.user(self.user, "~FRUser doesn't have a Paypal subscription, how did we get here?") return if not self.premium_renewal and not cancel_older_subscriptions_only: - logging.user(self.user, "~FRUser ~SBalready~SN canceled Paypal subscription: %s" % self.paypal_sub_id) + logging.user( + self.user, "~FRUser ~SBalready~SN canceled Paypal subscription: %s" % self.paypal_sub_id + ) return paypal_api = self.paypal_api() - today = datetime.datetime.now().strftime('%B %d, %Y') + today = datetime.datetime.now().strftime("%B %d, %Y") for paypal_id_model in self.user.paypal_ids.all(): paypal_id = paypal_id_model.paypal_sub_id if cancel_older_subscriptions_only and paypal_id == self.paypal_sub_id: - logging.user(self.user, "~FBNot canceling active Paypal subscription: %s" % self.paypal_sub_id) + logging.user( + self.user, "~FBNot canceling active Paypal subscription: %s" % self.paypal_sub_id + ) continue try: - paypal_subscription = paypal_api.get(f'/v1/billing/subscriptions/{paypal_id}') + paypal_subscription = paypal_api.get(f"/v1/billing/subscriptions/{paypal_id}") except paypalrestsdk.ResourceNotFound: logging.user(self.user, f"~FRCouldn't find paypal payments: {paypal_id}") continue - if paypal_subscription['status'] not in ['ACTIVE', 'APPROVED', 'APPROVAL_PENDING']: + if paypal_subscription["status"] not in ["ACTIVE", "APPROVED", "APPROVAL_PENDING"]: logging.user(self.user, "~FRUser ~SBalready~SN canceled Paypal subscription: %s" % paypal_id) continue url = f"/v1/billing/subscriptions/{paypal_id}/suspend" try: - response = paypal_api.post(url, { - 'reason': f"Cancelled on {today}" - }) + response = paypal_api.post(url, {"reason": f"Cancelled on {today}"}) except paypalrestsdk.ResourceNotFound as e: - logging.user(self.user, f"~FRCouldn't find paypal response during ~FB~SB{paypal_id}~SN~FR profile suspend: ~SB~FB{e}") - + logging.user( + self.user, + f"~FRCouldn't find paypal response during ~FB~SB{paypal_id}~SN~FR profile suspend: ~SB~FB{e}", + ) + logging.user(self.user, "~FRCanceling Paypal subscription: %s" % paypal_id) return paypal_id return True - + def cancel_premium_stripe(self): if not self.stripe_id: return - + stripe.api_key = settings.STRIPE_SECRET for stripe_id_model in self.user.stripe_ids.all(): stripe_id = stripe_id_model.stripe_id @@ -914,56 +1015,57 @@ def cancel_premium_stripe(self): try: subscriptions = stripe.Subscription.list(customer=stripe_customer) for subscription in subscriptions.data: - stripe.Subscription.modify(subscription['id'], cancel_at_period_end=True) - logging.user(self.user, "~FRCanceling Stripe subscription: %s" % subscription['id']) + stripe.Subscription.modify(subscription["id"], cancel_at_period_end=True) + logging.user(self.user, "~FRCanceling Stripe subscription: %s" % subscription["id"]) except stripe.error.InvalidRequestError: logging.user(self.user, "~FRFailed to cancel Stripe subscription: %s" % stripe_id) continue - + return True - + def retrieve_stripe_ids(self): if not self.stripe_id: return - + stripe.api_key = settings.STRIPE_SECRET stripe_customer = stripe.Customer.retrieve(self.stripe_id) stripe_email = stripe_customer.email - + stripe_ids = set() for email in set([stripe_email, self.user.email]): customers = stripe.Customer.list(email=email) for customer in customers: stripe_ids.add(customer.stripe_id) - + self.user.stripe_ids.all().delete() for stripe_id in stripe_ids: self.user.stripe_ids.create(stripe_id=stripe_id) - + def retrieve_paypal_ids(self, force=False): if self.paypal_sub_id and not force: return - - ipns = PayPalIPN.objects.filter(Q(custom=self.user.username) | - Q(payer_email=self.user.email) | - Q(custom=self.user.pk)).order_by('-payment_date') + + ipns = PayPalIPN.objects.filter( + Q(custom=self.user.username) | Q(payer_email=self.user.email) | Q(custom=self.user.pk) + ).order_by("-payment_date") if not len(ipns): return - + self.paypal_sub_id = ipns[0].subscr_id self.save() paypal_ids = set() for ipn in ipns: - if not ipn.subscr_id: continue + if not ipn.subscr_id: + continue paypal_ids.add(ipn.subscr_id) - + seen_paypal_ids = set(p.paypal_sub_id for p in self.user.paypal_ids.all()) for paypal_id in paypal_ids: if paypal_id in seen_paypal_ids: continue self.user.paypal_ids.create(paypal_sub_id=paypal_id) - + @property def latest_paypal_email(self): ipn = PayPalIPN.objects.filter(custom=self.user.username) @@ -971,9 +1073,9 @@ def latest_paypal_email(self): ipn = PayPalIPN.objects.filter(payer_email=self.user.email) if not len(ipn): return - + return ipn[0].payer_email - + def update_email(self, new_email): from apps.social.models import MSocialProfile @@ -982,14 +1084,14 @@ def update_email(self, new_email): self.user.email = new_email self.user.save() - + sp = MSocialProfile.get_user(self.user.pk) sp.email = new_email sp.save() if self.stripe_id: stripe_customer = self.stripe_customer() - stripe_customer.update({'email': new_email}) + stripe_customer.update({"email": new_email}) stripe_customer.save() def stripe_customer(self): @@ -997,71 +1099,85 @@ def stripe_customer(self): stripe.api_key = settings.STRIPE_SECRET stripe_customer = stripe.Customer.retrieve(self.stripe_id) return stripe_customer - + def paypal_api(self): if self.paypal_sub_id: - api = paypalrestsdk.Api({ - "mode": "sandbox" if settings.DEBUG else "live", - "client_id": settings.PAYPAL_API_CLIENTID, - "client_secret": settings.PAYPAL_API_SECRET - }) + api = paypalrestsdk.Api( + { + "mode": "sandbox" if settings.DEBUG else "live", + "client_id": settings.PAYPAL_API_CLIENTID, + "client_secret": settings.PAYPAL_API_SECRET, + } + ) return api - + def activate_ios_premium(self, transaction_identifier=None, amount=36): - payments = PaymentHistory.objects.filter(user=self.user, - payment_identifier=transaction_identifier, - payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3)) + payments = PaymentHistory.objects.filter( + user=self.user, + payment_identifier=transaction_identifier, + payment_date__gte=datetime.datetime.now() - datetime.timedelta(days=3), + ) if len(payments): # Already paid - logging.user(self.user, "~FG~BBAlready paid iOS premium subscription: $%s~FW" % transaction_identifier) + logging.user( + self.user, "~FG~BBAlready paid iOS premium subscription: $%s~FW" % transaction_identifier + ) return False - PaymentHistory.objects.create(user=self.user, - payment_date=datetime.datetime.now(), - payment_amount=amount, - payment_provider='ios-subscription', - payment_identifier=transaction_identifier) - + PaymentHistory.objects.create( + user=self.user, + payment_date=datetime.datetime.now(), + payment_amount=amount, + payment_provider="ios-subscription", + payment_identifier=transaction_identifier, + ) + self.setup_premium_history() - + if not self.is_premium: self.activate_premium() - + logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % amount) return True - + def activate_android_premium(self, order_id=None, amount=36): - payments = PaymentHistory.objects.filter(user=self.user, - payment_identifier=order_id, - payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3)) + payments = PaymentHistory.objects.filter( + user=self.user, + payment_identifier=order_id, + payment_date__gte=datetime.datetime.now() - datetime.timedelta(days=3), + ) if len(payments): # Already paid logging.user(self.user, "~FG~BBAlready paid Android premium subscription: $%s~FW" % amount) return False - PaymentHistory.objects.create(user=self.user, - payment_date=datetime.datetime.now(), - payment_amount=amount, - payment_provider='android-subscription', - payment_identifier=order_id) - + PaymentHistory.objects.create( + user=self.user, + payment_date=datetime.datetime.now(), + payment_amount=amount, + payment_provider="android-subscription", + payment_identifier=order_id, + ) + self.setup_premium_history() - + if order_id == "nb.premium.archive.99": self.activate_archive() elif not self.is_premium: self.activate_premium() - + logging.user(self.user, "~FG~BBNew Android premium subscription: $%s~FW" % amount) return True - + @classmethod def clear_dead_spammers(self, days=30, confirm=False): - users = User.objects.filter(date_joined__gte=datetime.datetime.now()-datetime.timedelta(days=days)).order_by('-date_joined') + users = User.objects.filter( + date_joined__gte=datetime.datetime.now() - datetime.timedelta(days=days) + ).order_by("-date_joined") usernames = set() - numerics = re.compile(r'[0-9]+') + numerics = re.compile(r"[0-9]+") for user in users: - opens = UserSubscription.objects.filter(user=user).aggregate(sum=Sum('feed_opens'))['sum'] + opens = UserSubscription.objects.filter(user=user).aggregate(sum=Sum("feed_opens"))["sum"] reads = RUserStory.read_story_count(user.pk) has_numbers = numerics.search(user.username) @@ -1069,7 +1185,9 @@ def clear_dead_spammers(self, days=30, confirm=False): has_profile = user.profile.last_seen_ip except Profile.DoesNotExist: usernames.add(user.username) - print(" ---> Missing profile: %-20s %-30s %-6s %-6s" % (user.username, user.email, opens, reads)) + print( + " ---> Missing profile: %-20s %-30s %-6s %-6s" % (user.username, user.email, opens, reads) + ) continue if opens is None and not reads and has_numbers: @@ -1078,9 +1196,10 @@ def clear_dead_spammers(self, days=30, confirm=False): elif not has_profile: usernames.add(user.username) print(" ---> No IP: %-20s %-30s %-6s %-6s" % (user.username, user.email, opens, reads)) - - if not confirm: return usernames - + + if not confirm: + return usernames + for username in usernames: try: u = User.objects.get(username=username) @@ -1090,27 +1209,33 @@ def clear_dead_spammers(self, days=30, confirm=False): RNewUserQueue.user_count() RNewUserQueue.activate_all() - + @classmethod def count_feed_subscribers(self, feed_id=None, user_id=None, verbose=True): SUBSCRIBER_EXPIRE = datetime.datetime.now() - datetime.timedelta(days=settings.SUBSCRIBER_EXPIRE) r = redis.Redis(connection_pool=settings.REDIS_FEED_SUB_POOL) entire_feed_counted = False - + if verbose: feed = Feed.get_by_id(feed_id) - logging.debug(" ---> [%-30s] ~SN~FBCounting subscribers for feed:~SB~FM%s~SN~FB user:~SB~FM%s" % (feed.log_title[:30], feed_id, user_id)) - + logging.debug( + " ---> [%-30s] ~SN~FBCounting subscribers for feed:~SB~FM%s~SN~FB user:~SB~FM%s" + % (feed.log_title[:30], feed_id, user_id) + ) + if feed_id: feed_ids = [feed_id] elif user_id: - feed_ids = [us['feed_id'] for us in UserSubscription.objects.filter(user=user_id, active=True).values('feed_id')] + feed_ids = [ + us["feed_id"] + for us in UserSubscription.objects.filter(user=user_id, active=True).values("feed_id") + ] else: assert False, "feed_id or user_id required" if feed_id and not user_id: entire_feed_counted = True - + for feed_id in feed_ids: total = 0 premium = 0 @@ -1118,20 +1243,26 @@ def count_feed_subscribers(self, feed_id=None, user_id=None, verbose=True): active_premium = 0 archive = 0 pro = 0 - key = 's:%s' % feed_id - premium_key = 'sp:%s' % feed_id - archive_key = 'sarchive:%s' % feed_id - pro_key = 'spro:%s' % feed_id - + key = "s:%s" % feed_id + premium_key = "sp:%s" % feed_id + archive_key = "sarchive:%s" % feed_id + pro_key = "spro:%s" % feed_id + if user_id: - active = UserSubscription.objects.get(feed_id=feed_id, user_id=user_id).only('active').active + active = UserSubscription.objects.get(feed_id=feed_id, user_id=user_id).only("active").active user_active_feeds = dict([(user_id, active)]) else: - user_active_feeds = dict([(us.user_id, us.active) - for us in UserSubscription.objects.filter(feed_id=feed_id).only('user', 'active')]) - profiles = Profile.objects.filter(user_id__in=list(user_active_feeds.keys())).values('user_id', 'last_seen_on', 'is_premium', 'is_archive', 'is_pro') + user_active_feeds = dict( + [ + (us.user_id, us.active) + for us in UserSubscription.objects.filter(feed_id=feed_id).only("user", "active") + ] + ) + profiles = Profile.objects.filter(user_id__in=list(user_active_feeds.keys())).values( + "user_id", "last_seen_on", "is_premium", "is_archive", "is_pro" + ) feed = Feed.get_by_id(feed_id) - + if entire_feed_counted: pipeline = r.pipeline() pipeline.delete(key) @@ -1139,150 +1270,167 @@ def count_feed_subscribers(self, feed_id=None, user_id=None, verbose=True): pipeline.delete(archive_key) pipeline.delete(pro_key) pipeline.execute() - + for profiles_group in chunks(profiles, 20): pipeline = r.pipeline() for profile in profiles_group: - last_seen_on = int(profile['last_seen_on'].strftime('%s')) - muted_feed = not bool(user_active_feeds[profile['user_id']]) + last_seen_on = int(profile["last_seen_on"].strftime("%s")) + muted_feed = not bool(user_active_feeds[profile["user_id"]]) if muted_feed: last_seen_on = 0 - pipeline.zadd(key, { profile['user_id']: last_seen_on }) + pipeline.zadd(key, {profile["user_id"]: last_seen_on}) total += 1 - if profile['is_premium']: - pipeline.zadd(premium_key, { profile['user_id']: last_seen_on }) + if profile["is_premium"]: + pipeline.zadd(premium_key, {profile["user_id"]: last_seen_on}) premium += 1 else: - pipeline.zrem(premium_key, profile['user_id']) - if profile['is_archive']: - pipeline.zadd(archive_key, { profile['user_id']: last_seen_on }) + pipeline.zrem(premium_key, profile["user_id"]) + if profile["is_archive"]: + pipeline.zadd(archive_key, {profile["user_id"]: last_seen_on}) archive += 1 else: - pipeline.zrem(archive_key, profile['user_id']) - if profile['is_pro']: - pipeline.zadd(pro_key, { profile['user_id']: last_seen_on }) + pipeline.zrem(archive_key, profile["user_id"]) + if profile["is_pro"]: + pipeline.zadd(pro_key, {profile["user_id"]: last_seen_on}) pro += 1 else: - pipeline.zrem(pro_key, profile['user_id']) - if profile['last_seen_on'] > SUBSCRIBER_EXPIRE and not muted_feed: + pipeline.zrem(pro_key, profile["user_id"]) + if profile["last_seen_on"] > SUBSCRIBER_EXPIRE and not muted_feed: active += 1 - if profile['is_premium']: + if profile["is_premium"]: active_premium += 1 - + pipeline.execute() - + if entire_feed_counted: - now = int(datetime.datetime.now().strftime('%s')) - r.zadd(key, { -1: now }) - r.expire(key, settings.SUBSCRIBER_EXPIRE*24*60*60) + now = int(datetime.datetime.now().strftime("%s")) + r.zadd(key, {-1: now}) + r.expire(key, settings.SUBSCRIBER_EXPIRE * 24 * 60 * 60) r.zadd(premium_key, {-1: now}) - r.expire(premium_key, settings.SUBSCRIBER_EXPIRE*24*60*60) + r.expire(premium_key, settings.SUBSCRIBER_EXPIRE * 24 * 60 * 60) r.zadd(archive_key, {-1: now}) - r.expire(archive_key, settings.SUBSCRIBER_EXPIRE*24*60*60) + r.expire(archive_key, settings.SUBSCRIBER_EXPIRE * 24 * 60 * 60) r.zadd(pro_key, {-1: now}) - r.expire(pro_key, settings.SUBSCRIBER_EXPIRE*24*60*60) - - logging.info(" ---> [%-30s] ~SN~FBCounting subscribers, storing in ~SBredis~SN: ~FMt:~SB~FM%s~SN a:~SB%s~SN p:~SB%s~SN ap:~SB%s~SN archive:~SB%s~SN pro:~SB%s" % - (feed.log_title[:30], total, active, premium, active_premium, archive, pro)) + r.expire(pro_key, settings.SUBSCRIBER_EXPIRE * 24 * 60 * 60) + + logging.info( + " ---> [%-30s] ~SN~FBCounting subscribers, storing in ~SBredis~SN: ~FMt:~SB~FM%s~SN a:~SB%s~SN p:~SB%s~SN ap:~SB%s~SN archive:~SB%s~SN pro:~SB%s" + % (feed.log_title[:30], total, active, premium, active_premium, archive, pro) + ) @classmethod def count_all_feed_subscribers_for_user(self, user): r = redis.Redis(connection_pool=settings.REDIS_FEED_SUB_POOL) if not isinstance(user, User): user = User.objects.get(pk=user) - - active_feed_ids = [us['feed_id'] for us in UserSubscription.objects.filter(user=user.pk, active=True).values('feed_id')] - muted_feed_ids = [us['feed_id'] for us in UserSubscription.objects.filter(user=user.pk, active=False).values('feed_id')] - logging.user(user, "~SN~FBRefreshing user last_login_on for ~SB%s~SN/~SB%s subscriptions~SN" % - (len(active_feed_ids), len(muted_feed_ids))) + + active_feed_ids = [ + us["feed_id"] + for us in UserSubscription.objects.filter(user=user.pk, active=True).values("feed_id") + ] + muted_feed_ids = [ + us["feed_id"] + for us in UserSubscription.objects.filter(user=user.pk, active=False).values("feed_id") + ] + logging.user( + user, + "~SN~FBRefreshing user last_login_on for ~SB%s~SN/~SB%s subscriptions~SN" + % (len(active_feed_ids), len(muted_feed_ids)), + ) for feed_ids in [active_feed_ids, muted_feed_ids]: for feeds_group in chunks(feed_ids, 20): pipeline = r.pipeline() for feed_id in feeds_group: - key = 's:%s' % feed_id - premium_key = 'sp:%s' % feed_id - archive_key = 'sarchive:%s' % feed_id - pro_key = 'spro:%s' % feed_id + key = "s:%s" % feed_id + premium_key = "sp:%s" % feed_id + archive_key = "sarchive:%s" % feed_id + pro_key = "spro:%s" % feed_id - last_seen_on = int(user.profile.last_seen_on.strftime('%s')) + last_seen_on = int(user.profile.last_seen_on.strftime("%s")) if feed_ids is muted_feed_ids: last_seen_on = 0 - pipeline.zadd(key, { user.pk: last_seen_on }) + pipeline.zadd(key, {user.pk: last_seen_on}) if user.profile.is_premium: - pipeline.zadd(premium_key, { user.pk: last_seen_on }) + pipeline.zadd(premium_key, {user.pk: last_seen_on}) else: pipeline.zrem(premium_key, user.pk) if user.profile.is_archive: - pipeline.zadd(archive_key, { user.pk: last_seen_on }) + pipeline.zadd(archive_key, {user.pk: last_seen_on}) else: pipeline.zrem(archive_key, user.pk) if user.profile.is_pro: - pipeline.zadd(pro_key, { user.pk: last_seen_on }) + pipeline.zadd(pro_key, {user.pk: last_seen_on}) else: pipeline.zrem(pro_key, user.pk) pipeline.execute() - + def send_new_user_email(self): if not self.user.email or not self.send_emails: return - - user = self.user - text = render_to_string('mail/email_new_account.txt', locals()) - html = render_to_string('mail/email_new_account.xhtml', locals()) + + user = self.user + text = render_to_string("mail/email_new_account.txt", locals()) + html = render_to_string("mail/email_new_account.xhtml", locals()) subject = "Welcome to NewsBlur, %s" % (self.user.username) - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for new user: %s" % self.user.email) - + def send_opml_export_email(self, reason=None, force=False): if not self.user.email: return - - emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk, - email_type='opml_export') + + emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk, email_type="opml_export") day_ago = datetime.datetime.now() - datetime.timedelta(days=1) for email in emails_sent: if email.date_sent > day_ago and not force: logging.user(self.user, "~SN~FMNot sending opml export email, already sent today.") return - MSentEmail.record(receiver_user_id=self.user.pk, email_type='opml_export') - + MSentEmail.record(receiver_user_id=self.user.pk, email_type="opml_export") + exporter = OPMLExporter(self.user) - opml = exporter.process() + opml = exporter.process() params = { - 'feed_count': UserSubscription.objects.filter(user=self.user).count(), - 'reason': reason, + "feed_count": UserSubscription.objects.filter(user=self.user).count(), + "reason": reason, } - user = self.user - text = render_to_string('mail/email_opml_export.txt', params) - html = render_to_string('mail/email_opml_export.xhtml', params) + user = self.user + text = render_to_string("mail/email_opml_export.txt", params) + html = render_to_string("mail/email_opml_export.xhtml", params) subject = "Backup OPML file of your NewsBlur sites" - filename= 'NewsBlur Subscriptions - %s.xml' % datetime.datetime.now().strftime('%Y-%m-%d') - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + filename = "NewsBlur Subscriptions - %s.xml" % datetime.datetime.now().strftime("%Y-%m-%d") + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") - msg.attach(filename, opml, 'text/xml') + msg.attach(filename, opml, "text/xml") msg.send() - + from apps.social.models import MActivity + MActivity.new_opml_export(user_id=self.user.pk, count=exporter.feed_count, automated=True) - + logging.user(self.user, "~BB~FM~SBSending OPML backup email to: %s" % self.user.email) - + def send_first_share_to_blurblog_email(self, force=False): - from apps.social.models import MSocialProfile, MSharedStory - + from apps.social.models import MSharedStory, MSocialProfile + if not self.user.email: return - - params = dict(receiver_user_id=self.user.pk, email_type='first_share') + + params = dict(receiver_user_id=self.user.pk, email_type="first_share") try: MSentEmail.objects.get(**params) if not force: @@ -1290,30 +1438,33 @@ def send_first_share_to_blurblog_email(self, force=False): return except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - + social_profile = MSocialProfile.objects.get(user_id=self.user.pk) params = { - 'shared_stories': MSharedStory.objects.filter(user_id=self.user.pk).count(), - 'blurblog_url': social_profile.blurblog_url, - 'blurblog_rss': social_profile.blurblog_rss + "shared_stories": MSharedStory.objects.filter(user_id=self.user.pk).count(), + "blurblog_url": social_profile.blurblog_url, + "blurblog_rss": social_profile.blurblog_rss, } - user = self.user - text = render_to_string('mail/email_first_share_to_blurblog.txt', params) - html = render_to_string('mail/email_first_share_to_blurblog.xhtml', params) + user = self.user + text = render_to_string("mail/email_first_share_to_blurblog.txt", params) + html = render_to_string("mail/email_first_share_to_blurblog.xhtml", params) subject = "Your shared stories on NewsBlur are available on your Blurblog" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending first share to blurblog email to: %s" % self.user.email) - - def send_new_premium_email(self, force=False): + + def send_new_premium_email(self, force=False): if not self.user.email or not self.send_emails: return - - params = dict(receiver_user_id=self.user.pk, email_type='new_premium') + + params = dict(receiver_user_id=self.user.pk, email_type="new_premium") try: MSentEmail.objects.get(**params) if not force: @@ -1322,52 +1473,66 @@ def send_new_premium_email(self, force=False): except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - user = self.user - text = render_to_string('mail/email_new_premium.txt', locals()) - html = render_to_string('mail/email_new_premium.xhtml', locals()) + user = self.user + text = render_to_string("mail/email_new_premium.txt", locals()) + html = render_to_string("mail/email_new_premium.xhtml", locals()) subject = "Thank you for subscribing to NewsBlur Premium!" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for new premium: %s" % self.user.email) - + def send_new_premium_archive_email(self, total_story_count, pre_archive_count, force=False): if not self.user.email: return - - params = dict(receiver_user_id=self.user.pk, email_type='new_premium_archive') + + params = dict(receiver_user_id=self.user.pk, email_type="new_premium_archive") try: MSentEmail.objects.get(**params) if not force: # Return if email already sent - logging.user(self.user, "~BB~FMNot ~SBSending email for new premium archive: %s (%s to %s stories)" % (self.user.email, pre_archive_count, total_story_count)) + logging.user( + self.user, + "~BB~FMNot ~SBSending email for new premium archive: %s (%s to %s stories)" + % (self.user.email, pre_archive_count, total_story_count), + ) return except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) feed_count = UserSubscription.objects.filter(user=self.user).count() - user = self.user - text = render_to_string('mail/email_new_premium_archive.txt', locals()) - html = render_to_string('mail/email_new_premium_archive.xhtml', locals()) + user = self.user + text = render_to_string("mail/email_new_premium_archive.txt", locals()) + html = render_to_string("mail/email_new_premium_archive.xhtml", locals()) if total_story_count > pre_archive_count: subject = f"NewsBlur archive backfill is complete: from {pre_archive_count:,} to {total_story_count:,} stories" else: subject = f"NewsBlur archive backfill is complete: {total_story_count:,} stories" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - logging.user(self.user, "~BB~FM~SBSending email for new premium archive: %s (%s to %s stories)" % (self.user.email, pre_archive_count, total_story_count)) - + + logging.user( + self.user, + "~BB~FM~SBSending email for new premium archive: %s (%s to %s stories)" + % (self.user.email, pre_archive_count, total_story_count), + ) + def send_new_premium_pro_email(self, force=False): if not self.user.email or not self.send_emails: return - - params = dict(receiver_user_id=self.user.pk, email_type='new_premium_pro') + + params = dict(receiver_user_id=self.user.pk, email_type="new_premium_pro") try: MSentEmail.objects.get(**params) if not force: @@ -1376,45 +1541,51 @@ def send_new_premium_pro_email(self, force=False): except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - user = self.user - text = render_to_string('mail/email_new_premium_pro.txt', locals()) - html = render_to_string('mail/email_new_premium_pro.xhtml', locals()) + user = self.user + text = render_to_string("mail/email_new_premium_pro.txt", locals()) + html = render_to_string("mail/email_new_premium_pro.xhtml", locals()) subject = "Thanks for subscribing to NewsBlur Premium Pro!" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for new premium pro: %s" % self.user.email) - + def send_forgot_password_email(self, email=None): if not self.user.email and not email: print("Please provide an email address.") return - + if not self.user.email and email: self.user.email = email self.user.save() - - user = self.user - text = render_to_string('mail/email_forgot_password.txt', locals()) - html = render_to_string('mail/email_forgot_password.xhtml', locals()) + + user = self.user + text = render_to_string("mail/email_forgot_password.txt", locals()) + html = render_to_string("mail/email_forgot_password.xhtml", locals()) subject = "Forgot your password on NewsBlur?" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for forgotten password: %s" % self.user.email) - + def send_new_user_queue_email(self, force=False): if not self.user.email: print("Please provide an email address.") return - - params = dict(receiver_user_id=self.user.pk, email_type='new_user_queue') + + params = dict(receiver_user_id=self.user.pk, email_type="new_user_queue") try: MSentEmail.objects.get(**params) if not force: @@ -1423,238 +1594,306 @@ def send_new_user_queue_email(self, force=False): except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - user = self.user - text = render_to_string('mail/email_new_user_queue.txt', locals()) - html = render_to_string('mail/email_new_user_queue.xhtml', locals()) + user = self.user + text = render_to_string("mail/email_new_user_queue.txt", locals()) + html = render_to_string("mail/email_new_user_queue.xhtml", locals()) subject = "Your free account is now ready to go on NewsBlur" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for new user queue: %s" % self.user.email) - + def send_upload_opml_finished_email(self, feed_count): if not self.user.email: print("Please provide an email address.") return - - user = self.user - text = render_to_string('mail/email_upload_opml_finished.txt', locals()) - html = render_to_string('mail/email_upload_opml_finished.xhtml', locals()) + + user = self.user + text = render_to_string("mail/email_upload_opml_finished.txt", locals()) + html = render_to_string("mail/email_upload_opml_finished.xhtml", locals()) subject = "Your OPML upload is complete. Get going with NewsBlur!" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for OPML upload: %s" % self.user.email) - + def send_import_reader_finished_email(self, feed_count): if not self.user.email: print("Please provide an email address.") return - - user = self.user - text = render_to_string('mail/email_import_reader_finished.txt', locals()) - html = render_to_string('mail/email_import_reader_finished.xhtml', locals()) + + user = self.user + text = render_to_string("mail/email_import_reader_finished.txt", locals()) + html = render_to_string("mail/email_import_reader_finished.xhtml", locals()) subject = "Your Google Reader import is complete. Get going with NewsBlur!" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + logging.user(self.user, "~BB~FM~SBSending email for Google Reader import: %s" % self.user.email) - + def send_import_reader_starred_finished_email(self, feed_count, starred_count): if not self.user.email: print("Please provide an email address.") return - - user = self.user - text = render_to_string('mail/email_import_reader_starred_finished.txt', locals()) - html = render_to_string('mail/email_import_reader_starred_finished.xhtml', locals()) + + user = self.user + text = render_to_string("mail/email_import_reader_starred_finished.txt", locals()) + html = render_to_string("mail/email_import_reader_starred_finished.xhtml", locals()) subject = "Your Google Reader starred stories import is complete. Get going with NewsBlur!" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - logging.user(self.user, "~BB~FM~SBSending email for Google Reader starred stories import: %s" % self.user.email) - + + logging.user( + self.user, "~BB~FM~SBSending email for Google Reader starred stories import: %s" % self.user.email + ) + def send_launch_social_email(self, force=False): if not self.user.email or not self.send_emails: - logging.user(self.user, "~FM~SB~FRNot~FM sending launch social email for user, %s: %s" % (self.user.email and 'opt-out: ' or 'blank', self.user.email)) + logging.user( + self.user, + "~FM~SB~FRNot~FM sending launch social email for user, %s: %s" + % (self.user.email and "opt-out: " or "blank", self.user.email), + ) return - - params = dict(receiver_user_id=self.user.pk, email_type='launch_social') + + params = dict(receiver_user_id=self.user.pk, email_type="launch_social") try: MSentEmail.objects.get(**params) if not force: # Return if email already sent - logging.user(self.user, "~FM~SB~FRNot~FM sending launch social email for user, sent already: %s" % self.user.email) + logging.user( + self.user, + "~FM~SB~FRNot~FM sending launch social email for user, sent already: %s" + % self.user.email, + ) return except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - - delta = datetime.datetime.now() - self.last_seen_on + + delta = datetime.datetime.now() - self.last_seen_on months_ago = delta.days / 30 - user = self.user - data = dict(user=user, months_ago=months_ago) - text = render_to_string('mail/email_launch_social.txt', data) - html = render_to_string('mail/email_launch_social.xhtml', data) + user = self.user + data = dict(user=user, months_ago=months_ago) + text = render_to_string("mail/email_launch_social.txt", data) + html = render_to_string("mail/email_launch_social.xhtml", data) subject = "NewsBlur is now a social news reader" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - logging.user(self.user, "~BB~FM~SBSending launch social email for user: %s months, %s" % (months_ago, self.user.email)) - + + logging.user( + self.user, + "~BB~FM~SBSending launch social email for user: %s months, %s" % (months_ago, self.user.email), + ) + def send_launch_turntouch_email(self, force=False): if not self.user.email or not self.send_emails: - logging.user(self.user, "~FM~SB~FRNot~FM sending launch TT email for user, %s: %s" % (self.user.email and 'opt-out: ' or 'blank', self.user.email)) + logging.user( + self.user, + "~FM~SB~FRNot~FM sending launch TT email for user, %s: %s" + % (self.user.email and "opt-out: " or "blank", self.user.email), + ) return - - params = dict(receiver_user_id=self.user.pk, email_type='launch_turntouch') + + params = dict(receiver_user_id=self.user.pk, email_type="launch_turntouch") try: MSentEmail.objects.get(**params) if not force: # Return if email already sent - logging.user(self.user, "~FM~SB~FRNot~FM sending launch social email for user, sent already: %s" % self.user.email) + logging.user( + self.user, + "~FM~SB~FRNot~FM sending launch social email for user, sent already: %s" + % self.user.email, + ) return except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - - delta = datetime.datetime.now() - self.last_seen_on + + delta = datetime.datetime.now() - self.last_seen_on months_ago = delta.days / 30 - user = self.user - data = dict(user=user, months_ago=months_ago) - text = render_to_string('mail/email_launch_turntouch.txt', data) - html = render_to_string('mail/email_launch_turntouch.xhtml', data) + user = self.user + data = dict(user=user, months_ago=months_ago) + text = render_to_string("mail/email_launch_turntouch.txt", data) + html = render_to_string("mail/email_launch_turntouch.xhtml", data) subject = "Introducing Turn Touch for NewsBlur" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - logging.user(self.user, "~BB~FM~SBSending launch TT email for user: %s months, %s" % (months_ago, self.user.email)) + + logging.user( + self.user, + "~BB~FM~SBSending launch TT email for user: %s months, %s" % (months_ago, self.user.email), + ) def send_launch_turntouch_end_email(self, force=False): if not self.user.email or not self.send_emails: - logging.user(self.user, "~FM~SB~FRNot~FM sending launch TT end email for user, %s: %s" % (self.user.email and 'opt-out: ' or 'blank', self.user.email)) + logging.user( + self.user, + "~FM~SB~FRNot~FM sending launch TT end email for user, %s: %s" + % (self.user.email and "opt-out: " or "blank", self.user.email), + ) return - - params = dict(receiver_user_id=self.user.pk, email_type='launch_turntouch_end') + + params = dict(receiver_user_id=self.user.pk, email_type="launch_turntouch_end") try: MSentEmail.objects.get(**params) if not force: # Return if email already sent - logging.user(self.user, "~FM~SB~FRNot~FM sending launch TT end email for user, sent already: %s" % self.user.email) + logging.user( + self.user, + "~FM~SB~FRNot~FM sending launch TT end email for user, sent already: %s" + % self.user.email, + ) return except MSentEmail.DoesNotExist: MSentEmail.objects.create(**params) - - delta = datetime.datetime.now() - self.last_seen_on + + delta = datetime.datetime.now() - self.last_seen_on months_ago = delta.days / 30 - user = self.user - data = dict(user=user, months_ago=months_ago) - text = render_to_string('mail/email_launch_turntouch_end.txt', data) - html = render_to_string('mail/email_launch_turntouch_end.xhtml', data) + user = self.user + data = dict(user=user, months_ago=months_ago) + text = render_to_string("mail/email_launch_turntouch_end.txt", data) + html = render_to_string("mail/email_launch_turntouch_end.xhtml", data) subject = "Last day to back Turn Touch: NewsBlur's beautiful remote" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - logging.user(self.user, "~BB~FM~SBSending launch TT end email for user: %s months, %s" % (months_ago, self.user.email)) - + + logging.user( + self.user, + "~BB~FM~SBSending launch TT end email for user: %s months, %s" % (months_ago, self.user.email), + ) + def grace_period_email_sent(self, force=False): - emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk, - email_type='premium_expire_grace') + emails_sent = MSentEmail.objects.filter( + receiver_user_id=self.user.pk, email_type="premium_expire_grace" + ) day_ago = datetime.datetime.now() - datetime.timedelta(days=360) for email in emails_sent: if email.date_sent > day_ago and not force: logging.user(self.user, "~SN~FMNot sending premium expire grace email, already sent before.") return True - + def send_premium_expire_grace_period_email(self, force=False): if not self.user.email: - logging.user(self.user, "~FM~SB~FRNot~FM~SN sending premium expire grace for user: %s" % (self.user)) + logging.user( + self.user, "~FM~SB~FRNot~FM~SN sending premium expire grace for user: %s" % (self.user) + ) return if self.grace_period_email_sent(force=force): return - + if self.premium_expire and self.premium_expire < datetime.datetime.now(): self.premium_expire = datetime.datetime.now() self.save() - - delta = datetime.datetime.now() - self.last_seen_on + + delta = datetime.datetime.now() - self.last_seen_on months_ago = round(delta.days / 30) - user = self.user - data = dict(user=user, months_ago=months_ago) - text = render_to_string('mail/email_premium_expire_grace.txt', data) - html = render_to_string('mail/email_premium_expire_grace.xhtml', data) + user = self.user + data = dict(user=user, months_ago=months_ago) + text = render_to_string("mail/email_premium_expire_grace.txt", data) + html = render_to_string("mail/email_premium_expire_grace.xhtml", data) subject = "Your premium account on NewsBlur has one more month!" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - MSentEmail.record(receiver_user_id=self.user.pk, email_type='premium_expire_grace') - logging.user(self.user, "~BB~FM~SBSending premium expire grace email for user: %s months, %s" % (months_ago, self.user.email)) - + + MSentEmail.record(receiver_user_id=self.user.pk, email_type="premium_expire_grace") + logging.user( + self.user, + "~BB~FM~SBSending premium expire grace email for user: %s months, %s" + % (months_ago, self.user.email), + ) + def send_premium_expire_email(self, force=False): if not self.user.email: logging.user(self.user, "~FM~SB~FRNot~FM sending premium expire for user: %s" % (self.user)) return - emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk, - email_type='premium_expire') + emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk, email_type="premium_expire") day_ago = datetime.datetime.now() - datetime.timedelta(days=360) for email in emails_sent: if email.date_sent > day_ago and not force: logging.user(self.user, "~FM~SBNot sending premium expire email, already sent before.") return - - delta = datetime.datetime.now() - self.last_seen_on + + delta = datetime.datetime.now() - self.last_seen_on months_ago = round(delta.days / 30) - user = self.user - data = dict(user=user, months_ago=months_ago) - text = render_to_string('mail/email_premium_expire.txt', data) - html = render_to_string('mail/email_premium_expire.xhtml', data) + user = self.user + data = dict(user=user, months_ago=months_ago) + text = render_to_string("mail/email_premium_expire.txt", data) + html = render_to_string("mail/email_premium_expire.xhtml", data) subject = "Your premium account on NewsBlur has expired" - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - MSentEmail.record(receiver_user_id=self.user.pk, email_type='premium_expire') - logging.user(self.user, "~BB~FM~SBSending premium expire email for user: %s months, %s" % (months_ago, self.user.email)) - + + MSentEmail.record(receiver_user_id=self.user.pk, email_type="premium_expire") + logging.user( + self.user, + "~BB~FM~SBSending premium expire email for user: %s months, %s" % (months_ago, self.user.email), + ) + def autologin_url(self, next=None): - return reverse('autologin', kwargs={ - 'username': self.user.username, - 'secret': self.secret_token - }) + ('?' + next + '=1' if next else '') - - + return reverse("autologin", kwargs={"username": self.user.username, "secret": self.secret_token}) + ( + "?" + next + "=1" if next else "" + ) + @classmethod def doublecheck_paypal_payments(cls, days=14): - payments = PayPalIPN.objects.filter(txn_type='subscr_payment', - updated_at__gte=datetime.datetime.now() - - datetime.timedelta(days) - ).order_by('-created_at') + payments = PayPalIPN.objects.filter( + txn_type="subscr_payment", updated_at__gte=datetime.datetime.now() - datetime.timedelta(days) + ).order_by("-created_at") for payment in payments: try: profile = Profile.objects.get(user__username=payment.custom) @@ -1662,10 +1901,10 @@ def doublecheck_paypal_payments(cls, days=14): logging.debug(" ---> ~FRCouldn't find user: ~SB~FC%s" % payment.custom) continue profile.setup_premium_history() - + class StripeIds(models.Model): - user = models.ForeignKey(User, related_name='stripe_ids', on_delete=models.CASCADE, null=True) + user = models.ForeignKey(User, related_name="stripe_ids", on_delete=models.CASCADE, null=True) stripe_id = models.CharField(max_length=24, blank=True, null=True) def __str__(self): @@ -1673,18 +1912,20 @@ def __str__(self): class PaypalIds(models.Model): - user = models.ForeignKey(User, related_name='paypal_ids', on_delete=models.CASCADE, null=True) + user = models.ForeignKey(User, related_name="paypal_ids", on_delete=models.CASCADE, null=True) paypal_sub_id = models.CharField(max_length=24, blank=True, null=True) def __str__(self): return "%s: %s" % (self.user.username, self.paypal_sub_id) - + def create_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) else: Profile.objects.get_or_create(user=instance) + + post_save.connect(create_profile, sender=User) @@ -1702,7 +1943,7 @@ def paypal_signup(sender, **kwargs): user = User.objects.get(email__iexact=ipn_obj.payer_email) except User.DoesNotExist: pass - + if not user and ipn_obj.custom: try: user = User.objects.get(pk=ipn_obj.custom) @@ -1716,9 +1957,10 @@ def paypal_signup(sender, **kwargs): pass if not user: - logging.debug(" ---> Paypal subscription not found during paypal_signup: %s/%s" % ( - ipn_obj.payer_email, - ipn_obj.custom)) + logging.debug( + " ---> Paypal subscription not found during paypal_signup: %s/%s" + % (ipn_obj.payer_email, ipn_obj.custom) + ) return {"code": -1, "message": "User doesn't exist."} logging.user(user, "~BC~SB~FBPaypal subscription signup") @@ -1733,8 +1975,11 @@ def paypal_signup(sender, **kwargs): # user.profile.cancel_premium_paypal(second_most_recent_only=True) # assert False, "Shouldn't be here anymore as the new Paypal REST API uses webhooks" + + valid_ipn_received.connect(paypal_signup) + def paypal_payment_history_sync(sender, **kwargs): ipn_obj = sender try: @@ -1743,9 +1988,10 @@ def paypal_payment_history_sync(sender, **kwargs): try: user = User.objects.get(email__iexact=ipn_obj.payer_email) except User.DoesNotExist: - logging.debug(" ---> Paypal subscription not found during flagging: %s/%s" % ( - ipn_obj.payer_email, - ipn_obj.custom)) + logging.debug( + " ---> Paypal subscription not found during flagging: %s/%s" + % (ipn_obj.payer_email, ipn_obj.custom) + ) return {"code": -1, "message": "User doesn't exist."} logging.user(user, "~BC~SB~FBPaypal subscription payment") @@ -1753,8 +1999,11 @@ def paypal_payment_history_sync(sender, **kwargs): user.profile.setup_premium_history() except: return {"code": -1, "message": "User doesn't exist."} + + valid_ipn_received.connect(paypal_payment_history_sync) + def paypal_payment_was_flagged(sender, **kwargs): ipn_obj = sender try: @@ -1763,27 +2012,31 @@ def paypal_payment_was_flagged(sender, **kwargs): try: user = User.objects.get(email__iexact=ipn_obj.payer_email) except User.DoesNotExist: - logging.debug(" ---> Paypal subscription not found during flagging: %s/%s" % ( - ipn_obj.payer_email, - ipn_obj.custom)) + logging.debug( + " ---> Paypal subscription not found during flagging: %s/%s" + % (ipn_obj.payer_email, ipn_obj.custom) + ) return {"code": -1, "message": "User doesn't exist."} - + try: user.profile.setup_premium_history() logging.user(user, "~BC~SB~FBPaypal subscription payment flagged") except: return {"code": -1, "message": "User doesn't exist."} + + invalid_ipn_received.connect(paypal_payment_was_flagged) + def stripe_checkout_session_completed(sender, full_json, **kwargs): - newsblur_user_id = full_json['data']['object']['metadata']['newsblur_user_id'] - stripe_id = full_json['data']['object']['customer'] + newsblur_user_id = full_json["data"]["object"]["metadata"]["newsblur_user_id"] + stripe_id = full_json["data"]["object"]["customer"] profile = None try: profile = Profile.objects.get(stripe_id=stripe_id) except Profile.DoesNotExist: pass - + if not profile: try: profile = User.objects.get(pk=int(newsblur_user_id)).profile @@ -1791,46 +2044,56 @@ def stripe_checkout_session_completed(sender, full_json, **kwargs): profile.save() except User.DoesNotExist: pass - + if profile: logging.user(profile.user, "~BC~SB~FBStripe checkout subscription signup") profile.retrieve_stripe_ids() else: logging.user(profile.user, "~BR~SB~FRCouldn't find Stripe user: ~FW%s" % full_json) return {"code": -1, "message": "User doesn't exist."} + + zebra_webhook_checkout_session_completed.connect(stripe_checkout_session_completed) + def stripe_signup(sender, full_json, **kwargs): - stripe_id = full_json['data']['object']['customer'] - plan_id = full_json['data']['object']['plan']['id'] + stripe_id = full_json["data"]["object"]["customer"] + plan_id = full_json["data"]["object"]["plan"]["id"] try: profile = Profile.objects.get(stripe_id=stripe_id) logging.user(profile.user, "~BC~SB~FBStripe subscription signup") - if plan_id == Profile.plan_to_stripe_price('premium'): + if plan_id == Profile.plan_to_stripe_price("premium"): profile.activate_premium() - elif plan_id == Profile.plan_to_stripe_price('archive'): + elif plan_id == Profile.plan_to_stripe_price("archive"): profile.activate_archive() - elif plan_id == Profile.plan_to_stripe_price('pro'): + elif plan_id == Profile.plan_to_stripe_price("pro"): profile.activate_pro() profile.cancel_premium_paypal() profile.retrieve_stripe_ids() except Profile.DoesNotExist: return {"code": -1, "message": "User doesn't exist."} + + zebra_webhook_customer_subscription_created.connect(stripe_signup) + def stripe_subscription_updated(sender, full_json, **kwargs): - stripe_id = full_json['data']['object']['customer'] - plan_id = full_json['data']['object']['plan']['id'] + stripe_id = full_json["data"]["object"]["customer"] + plan_id = full_json["data"]["object"]["plan"]["id"] try: profile = Profile.objects.get(stripe_id=stripe_id) - active = not full_json['data']['object']['cancel_at'] and full_json['data']['object']['plan']['active'] - logging.user(profile.user, "~BC~SB~FBStripe subscription updated: %s" % "active" if active else "cancelled") + active = ( + not full_json["data"]["object"]["cancel_at"] and full_json["data"]["object"]["plan"]["active"] + ) + logging.user( + profile.user, "~BC~SB~FBStripe subscription updated: %s" % "active" if active else "cancelled" + ) if active: - if plan_id == Profile.plan_to_stripe_price('premium'): + if plan_id == Profile.plan_to_stripe_price("premium"): profile.activate_premium() - elif plan_id == Profile.plan_to_stripe_price('archive'): + elif plan_id == Profile.plan_to_stripe_price("archive"): profile.activate_archive() - elif plan_id == Profile.plan_to_stripe_price('pro'): + elif plan_id == Profile.plan_to_stripe_price("pro"): profile.activate_pro() profile.cancel_premium_paypal() profile.retrieve_stripe_ids() @@ -1838,19 +2101,25 @@ def stripe_subscription_updated(sender, full_json, **kwargs): profile.setup_premium_history() except Profile.DoesNotExist: return {"code": -1, "message": "User doesn't exist."} + + zebra_webhook_customer_subscription_updated.connect(stripe_subscription_updated) + def stripe_payment_history_sync(sender, full_json, **kwargs): - stripe_id = full_json['data']['object']['customer'] + stripe_id = full_json["data"]["object"]["customer"] try: profile = Profile.objects.get(stripe_id=stripe_id) logging.user(profile.user, "~BC~SB~FBStripe subscription payment") profile.setup_premium_history() except Profile.DoesNotExist: - return {"code": -1, "message": "User doesn't exist."} + return {"code": -1, "message": "User doesn't exist."} + + zebra_webhook_charge_succeeded.connect(stripe_payment_history_sync) zebra_webhook_charge_refunded.connect(stripe_payment_history_sync) + def change_password(user, old_password, new_password, only_check=False): user_db = authenticate(username=user.username, password=old_password) if user_db is None: @@ -1860,7 +2129,7 @@ def change_password(user, old_password, new_password, only_check=False): user.save() if user_db is None: user_db = authenticate(username=user.username, password=user.username) - + if not user_db: return -1 else: @@ -1869,48 +2138,53 @@ def change_password(user, old_password, new_password, only_check=False): user_db.save() return 1 + def blank_authenticate(username, password=""): try: user = User.objects.get(username__iexact=username) except User.DoesNotExist: return - + if user.password == "!": return user - - algorithm, salt, hash = user.password.split('$', 2) - encoded_blank = hashlib.sha1((salt + password).encode(encoding='utf-8')).hexdigest() + + algorithm, salt, hash = user.password.split("$", 2) + encoded_blank = hashlib.sha1((salt + password).encode(encoding="utf-8")).hexdigest() encoded_username = authenticate(username=username, password=username) if encoded_blank == hash or encoded_username == user: return user + # Unfinished class MEmailUnsubscribe(mongo.Document): user_id = mongo.IntField() email_type = mongo.StringField() date = mongo.DateTimeField(default=datetime.datetime.now) - - EMAIL_TYPE_FOLLOWS = 'follows' - EMAIL_TYPE_REPLIES = 'replies' - EMAIL_TYOE_PRODUCT = 'product' - + + EMAIL_TYPE_FOLLOWS = "follows" + EMAIL_TYPE_REPLIES = "replies" + EMAIL_TYOE_PRODUCT = "product" + meta = { - 'collection': 'email_unsubscribes', - 'allow_inheritance': False, - 'indexes': ['user_id', - {'fields': ['user_id', 'email_type'], - 'unique': True, - }], + "collection": "email_unsubscribes", + "allow_inheritance": False, + "indexes": [ + "user_id", + { + "fields": ["user_id", "email_type"], + "unique": True, + }, + ], } - + def __str__(self): return "%s unsubscribed from %s on %s" % (self.user_id, self.email_type, self.date) - + @classmethod def user(cls, user_id): unsubs = cls.objects(user_id=user_id) return unsubs - + @classmethod def unsubscribe(cls, user_id, email_type): cls.objects.create() @@ -1921,13 +2195,13 @@ class MSentEmail(mongo.Document): receiver_user_id = mongo.IntField() email_type = mongo.StringField() date_sent = mongo.DateTimeField(default=datetime.datetime.now) - + meta = { - 'collection': 'sent_emails', - 'allow_inheritance': False, - 'indexes': ['sending_user_id', 'receiver_user_id', 'email_type'], + "collection": "sent_emails", + "allow_inheritance": False, + "indexes": ["sending_user_id", "receiver_user_id", "email_type"], } - + def __str__(self): sender_user = self.sending_user_id if sender_user: @@ -1935,61 +2209,83 @@ def __str__(self): receiver_user = self.receiver_user_id if receiver_user: receiver_user = User.objects.get(pk=self.receiver_user_id) - return "%s sent %s email to %s %s" % (sender_user, self.email_type, receiver_user, receiver_user.profile if receiver_user else receiver_user) - + return "%s sent %s email to %s %s" % ( + sender_user, + self.email_type, + receiver_user, + receiver_user.profile if receiver_user else receiver_user, + ) + @classmethod def record(cls, email_type, receiver_user_id, sending_user_id=None): - cls.objects.create(email_type=email_type, - receiver_user_id=receiver_user_id, - sending_user_id=sending_user_id) + cls.objects.create( + email_type=email_type, receiver_user_id=receiver_user_id, sending_user_id=sending_user_id + ) + class PaymentHistory(models.Model): - user = models.ForeignKey(User, related_name='payments', on_delete=models.CASCADE) + user = models.ForeignKey(User, related_name="payments", on_delete=models.CASCADE) payment_date = models.DateTimeField() payment_amount = models.IntegerField() payment_provider = models.CharField(max_length=20) payment_identifier = models.CharField(max_length=100, null=True) refunded = models.BooleanField(blank=True, null=True) - + def __str__(self): - return "[%s] $%s/%s %s" % (self.payment_date.strftime("%Y-%m-%d"), self.payment_amount, - self.payment_provider, "" if self.refunded else "") + return "[%s] $%s/%s %s" % ( + self.payment_date.strftime("%Y-%m-%d"), + self.payment_amount, + self.payment_provider, + "" if self.refunded else "", + ) + class Meta: - ordering = ['-payment_date'] - + ordering = ["-payment_date"] + def canonical(self): return { - 'payment_date': self.payment_date.strftime('%Y-%m-%d'), - 'payment_amount': self.payment_amount, - 'payment_provider': self.payment_provider, - 'refunded': self.refunded, + "payment_date": self.payment_date.strftime("%Y-%m-%d"), + "payment_amount": self.payment_amount, + "payment_provider": self.payment_provider, + "refunded": self.refunded, } - + @classmethod def report(cls, months=26): output = "" - + def _counter(start_date, end_date, output, payments=None): if not payments: - payments = PaymentHistory.objects.filter(payment_date__gte=start_date, payment_date__lte=end_date) - payments = payments.aggregate(avg=Avg('payment_amount'), - sum=Sum('payment_amount'), - count=Count('user')) + payments = PaymentHistory.objects.filter( + payment_date__gte=start_date, payment_date__lte=end_date + ) + payments = payments.aggregate( + avg=Avg("payment_amount"), sum=Sum("payment_amount"), count=Count("user") + ) output += "%s-%02d-%02d - %s-%02d-%02d:\t$%.2f\t$%-6s\t%-4s\n" % ( - start_date.year, start_date.month, start_date.day, - end_date.year, end_date.month, end_date.day, - round(payments['avg'] if payments['avg'] else 0, 2), payments['sum'] if payments['sum'] else 0, payments['count']) - + start_date.year, + start_date.month, + start_date.day, + end_date.year, + end_date.month, + end_date.day, + round(payments["avg"] if payments["avg"] else 0, 2), + payments["sum"] if payments["sum"] else 0, + payments["count"], + ) + return payments, output output += "\nMonthly Totals:\n" for m in reversed(list(range(months))): now = datetime.datetime.now() - start_date = datetime.datetime(now.year, now.month, 1) - dateutil.relativedelta.relativedelta(months=m) + start_date = datetime.datetime(now.year, now.month, 1) - dateutil.relativedelta.relativedelta( + months=m + ) end_time = start_date + datetime.timedelta(days=31) end_date = datetime.datetime(end_time.year, end_time.month, 1) - datetime.timedelta(seconds=1) total, output = _counter(start_date, end_date, output) - total = total['sum'] + total = total["sum"] output += "\nMTD Totals:\n" years = datetime.datetime.now().year - 2009 @@ -2001,18 +2297,21 @@ def _counter(start_date, end_date, output, payments=None): this_mtd_count = 0 for y in reversed(list(range(years))): now = datetime.datetime.now() - start_date = datetime.datetime(now.year, now.month, 1) - dateutil.relativedelta.relativedelta(years=y) + start_date = datetime.datetime(now.year, now.month, 1) - dateutil.relativedelta.relativedelta( + years=y + ) end_date = now - dateutil.relativedelta.relativedelta(years=y) - if end_date > now: end_date = now + if end_date > now: + end_date = now count, output = _counter(start_date, end_date, output) if end_date.year != now.year: - last_mtd_avg = count['avg'] or 0 - last_mtd_sum = count['sum'] or 0 - last_mtd_count = count['count'] + last_mtd_avg = count["avg"] or 0 + last_mtd_sum = count["sum"] or 0 + last_mtd_count = count["count"] else: - this_mtd_avg = count['avg'] or 0 - this_mtd_sum = count['sum'] or 0 - this_mtd_count = count['count'] + this_mtd_avg = count["avg"] or 0 + this_mtd_sum = count["sum"] or 0 + this_mtd_count = count["count"] output += "\nCurrent Month Totals:\n" years = datetime.datetime.now().year - 2009 @@ -2021,19 +2320,25 @@ def _counter(start_date, end_date, output, payments=None): last_month_count = 0 for y in reversed(list(range(years))): now = datetime.datetime.now() - start_date = datetime.datetime(now.year, now.month, 1) - dateutil.relativedelta.relativedelta(years=y) + start_date = datetime.datetime(now.year, now.month, 1) - dateutil.relativedelta.relativedelta( + years=y + ) end_time = start_date + datetime.timedelta(days=31) end_date = datetime.datetime(end_time.year, end_time.month, 1) - datetime.timedelta(seconds=1) if end_date > now: - payments = {'avg': this_mtd_avg / (max(1, last_mtd_avg) / float(max(1, last_month_avg))), - 'sum': int(round(this_mtd_sum / (max(1, last_mtd_sum) / float(max(1, last_month_sum))))), - 'count': int(round(this_mtd_count / (max(1, last_mtd_count) / float(max(1, last_month_count)))))} + payments = { + "avg": this_mtd_avg / (max(1, last_mtd_avg) / float(max(1, last_month_avg))), + "sum": int(round(this_mtd_sum / (max(1, last_mtd_sum) / float(max(1, last_month_sum))))), + "count": int( + round(this_mtd_count / (max(1, last_mtd_count) / float(max(1, last_month_count)))) + ), + } _, output = _counter(start_date, end_date, output, payments=payments) else: count, output = _counter(start_date, end_date, output) - last_month_avg = count['avg'] - last_month_sum = count['sum'] - last_month_count = count['count'] + last_month_avg = count["avg"] + last_month_sum = count["sum"] + last_month_count = count["count"] output += "\nYTD Totals:\n" years = datetime.datetime.now().year - 2009 @@ -2049,13 +2354,13 @@ def _counter(start_date, end_date, output, payments=None): end_date = now - dateutil.relativedelta.relativedelta(years=y) count, output = _counter(start_date, end_date, output) if end_date.year != now.year: - last_ytd_avg = count['avg'] or 0 - last_ytd_sum = count['sum'] or 0 - last_ytd_count = count['count'] + last_ytd_avg = count["avg"] or 0 + last_ytd_sum = count["sum"] or 0 + last_ytd_count = count["count"] else: - this_ytd_avg = count['avg'] or 0 - this_ytd_sum = count['sum'] or 0 - this_ytd_count = count['count'] + this_ytd_avg = count["avg"] or 0 + this_ytd_sum = count["sum"] or 0 + this_ytd_count = count["count"] output += "\nYearly Totals:\n" years = datetime.datetime.now().year - 2009 @@ -2066,26 +2371,33 @@ def _counter(start_date, end_date, output, payments=None): for y in reversed(list(range(years))): now = datetime.datetime.now() start_date = datetime.datetime(now.year, 1, 1) - dateutil.relativedelta.relativedelta(years=y) - end_date = datetime.datetime(now.year, 1, 1) - dateutil.relativedelta.relativedelta(years=y-1) - datetime.timedelta(seconds=1) + end_date = ( + datetime.datetime(now.year, 1, 1) + - dateutil.relativedelta.relativedelta(years=y - 1) + - datetime.timedelta(seconds=1) + ) if end_date > now: - payments = {'avg': this_ytd_avg / (max(1, last_ytd_avg) / float(max(1, last_year_avg))), - 'sum': int(round(this_ytd_sum / (max(1, last_ytd_sum) / float(max(1, last_year_sum))))), - 'count': int(round(this_ytd_count / (max(1, last_ytd_count) / float(max(1, last_year_count)))))} + payments = { + "avg": this_ytd_avg / (max(1, last_ytd_avg) / float(max(1, last_year_avg))), + "sum": int(round(this_ytd_sum / (max(1, last_ytd_sum) / float(max(1, last_year_sum))))), + "count": int( + round(this_ytd_count / (max(1, last_ytd_count) / float(max(1, last_year_count)))) + ), + } count, output = _counter(start_date, end_date, output, payments=payments) - annual = count['sum'] + annual = count["sum"] else: count, output = _counter(start_date, end_date, output) - last_year_avg = count['avg'] or 0 - last_year_sum = count['sum'] or 0 - last_year_count = count['count'] - - - total = cls.objects.all().aggregate(sum=Sum('payment_amount')) - output += "\nTotal: $%s\n" % total['sum'] - + last_year_avg = count["avg"] or 0 + last_year_sum = count["sum"] or 0 + last_year_count = count["count"] + + total = cls.objects.all().aggregate(sum=Sum("payment_amount")) + output += "\nTotal: $%s\n" % total["sum"] + print(output) - - return {'annual': annual, 'output': output} + + return {"annual": annual, "output": output} class MGiftCode(mongo.Document): @@ -2095,108 +2407,124 @@ class MGiftCode(mongo.Document): duration_days = mongo.IntField() payment_amount = mongo.IntField() created_date = mongo.DateTimeField(default=datetime.datetime.now) - + meta = { - 'collection': 'gift_codes', - 'allow_inheritance': False, - 'indexes': ['gifting_user_id', 'receiving_user_id', 'created_date'], + "collection": "gift_codes", + "allow_inheritance": False, + "indexes": ["gifting_user_id", "receiving_user_id", "created_date"], } - + def __str__(self): - return "%s gifted %s on %s: %s (redeemed %s times)" % (self.gifting_user_id, self.receiving_user_id, self.created_date, self.gift_code, self.redeemed) - + return "%s gifted %s on %s: %s (redeemed %s times)" % ( + self.gifting_user_id, + self.receiving_user_id, + self.created_date, + self.gift_code, + self.redeemed, + ) + @property def redeemed(self): redeemed_code = MRedeemedCode.objects.filter(gift_code=self.gift_code) return len(redeemed_code) - + @staticmethod def create_code(gift_code=None): u = str(uuid.uuid4()) code = u[:8] + u[9:13] if gift_code: - code = gift_code + code[len(gift_code):] + code = gift_code + code[len(gift_code) :] return code - + @classmethod def add(cls, gift_code=None, duration=0, gifting_user_id=None, receiving_user_id=None, payment=0): - return cls.objects.create(gift_code=cls.create_code(gift_code), - gifting_user_id=gifting_user_id, - receiving_user_id=receiving_user_id, - duration_days=duration, - payment_amount=payment) + return cls.objects.create( + gift_code=cls.create_code(gift_code), + gifting_user_id=gifting_user_id, + receiving_user_id=receiving_user_id, + duration_days=duration, + payment_amount=payment, + ) class MRedeemedCode(mongo.Document): user_id = mongo.IntField() gift_code = mongo.StringField() redeemed_date = mongo.DateTimeField(default=datetime.datetime.now) - + meta = { - 'collection': 'redeemed_codes', - 'allow_inheritance': False, - 'indexes': ['user_id', 'gift_code', 'redeemed_date'], + "collection": "redeemed_codes", + "allow_inheritance": False, + "indexes": ["user_id", "gift_code", "redeemed_date"], } - + def __str__(self): return "%s redeemed %s on %s" % (self.user_id, self.gift_code, self.redeemed_date) - + @classmethod def record(cls, user_id, gift_code): - cls.objects.create(user_id=user_id, - gift_code=gift_code) + cls.objects.create(user_id=user_id, gift_code=gift_code) + @classmethod def redeem(cls, user, gift_code): newsblur_gift_code = MGiftCode.objects.filter(gift_code__iexact=gift_code) if newsblur_gift_code: newsblur_gift_code = newsblur_gift_code[0] - PaymentHistory.objects.create(user=user, - payment_date=datetime.datetime.now(), - payment_amount=newsblur_gift_code.payment_amount, - payment_provider='newsblur-gift') - + PaymentHistory.objects.create( + user=user, + payment_date=datetime.datetime.now(), + payment_amount=newsblur_gift_code.payment_amount, + payment_provider="newsblur-gift", + ) + else: # Thinkup / Good Web Bundle - PaymentHistory.objects.create(user=user, - payment_date=datetime.datetime.now(), - payment_amount=12, - payment_provider='good-web-bundle') + PaymentHistory.objects.create( + user=user, + payment_date=datetime.datetime.now(), + payment_amount=12, + payment_provider="good-web-bundle", + ) cls.record(user.pk, gift_code) user.profile.activate_premium() logging.user(user, "~FG~BBRedeeming gift code: %s~FW" % gift_code) - + class MCustomStyling(mongo.Document): user_id = mongo.IntField(unique=True) custom_css = mongo.StringField() custom_js = mongo.StringField() updated_date = mongo.DateTimeField(default=datetime.datetime.now) - + meta = { - 'collection': 'custom_styling', - 'allow_inheritance': False, - 'indexes': ['user_id'], + "collection": "custom_styling", + "allow_inheritance": False, + "indexes": ["user_id"], } - + def __str__(self): - return "%s custom style %s/%s %s" % (self.user_id, len(self.custom_css) if self.custom_css else "-", - len(self.custom_js) if self.custom_js else "-", self.updated_date) - + return "%s custom style %s/%s %s" % ( + self.user_id, + len(self.custom_css) if self.custom_css else "-", + len(self.custom_js) if self.custom_js else "-", + self.updated_date, + ) + def canonical(self): return { - 'css': self.custom_css, - 'js': self.custom_js, + "css": self.custom_css, + "js": self.custom_js, } - + @classmethod def get_user(cls, user_id): try: styling = cls.objects.get(user_id=user_id) except cls.DoesNotExist: return None - + return styling - + @classmethod def save_user(cls, user_id, css, js): styling = cls.get_user(user_id) @@ -2220,13 +2548,16 @@ class MDashboardRiver(mongo.Document): river_order = mongo.IntField() meta = { - 'collection': 'dashboard_river', - 'allow_inheritance': False, - 'indexes': ['user_id', - {'fields': ['user_id', 'river_id', 'river_side', 'river_order'], - 'unique': True, - }], - 'ordering': ['river_order'] + "collection": "dashboard_river", + "allow_inheritance": False, + "indexes": [ + "user_id", + { + "fields": ["user_id", "river_id", "river_side", "river_order"], + "unique": True, + }, + ], + "ordering": ["river_order"], } def __str__(self): @@ -2235,14 +2566,14 @@ def __str__(self): except User.DoesNotExist: u = "" return f"{u} ({self.river_side}/{self.river_order}): {self.river_id}" - + def canonical(self): return { - 'river_id': self.river_id, - 'river_side': self.river_side, - 'river_order': self.river_order, + "river_id": self.river_id, + "river_side": self.river_side, + "river_order": self.river_order, } - + @classmethod def get_user_rivers(cls, user_id): return cls.objects(user_id=user_id) @@ -2270,59 +2601,67 @@ def save_user(cls, user_id, river_id, river_side, river_order): river = None if not river: - river = cls.objects.create(user_id=user_id, river_id=river_id, - river_side=river_side, river_order=river_order) + river = cls.objects.create( + user_id=user_id, river_id=river_id, river_side=river_side, river_order=river_order + ) river.river_id = river_id river.river_side = river_side river.river_order = river_order river.save() + class RNewUserQueue: - KEY = "new_user_queue" - + @classmethod def activate_next(cls): count = cls.user_count() if not count: return - + user_id = cls.pop_user() try: user = User.objects.get(pk=user_id) except User.DoesNotExist: - logging.debug("~FRCan't activate free account, can't find user ~SB%s~SN. ~FB%s still in queue." % (user_id, count-1)) + logging.debug( + "~FRCan't activate free account, can't find user ~SB%s~SN. ~FB%s still in queue." + % (user_id, count - 1) + ) return - - logging.user(user, "~FBActivating free account (%s / %s). %s still in queue." % (user.email, user.profile.last_seen_ip, (count-1))) + + logging.user( + user, + "~FBActivating free account (%s / %s). %s still in queue." + % (user.email, user.profile.last_seen_ip, (count - 1)), + ) user.profile.activate_free() - + @classmethod def activate_all(cls): count = cls.user_count() if not count: logging.debug("~FBNo users to activate, sleeping...") return - + for i in range(count): cls.activate_next() - + @classmethod def add_user(cls, user_id): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) now = time.time() - - r.zadd(cls.KEY, { user_id: now }) - + + r.zadd(cls.KEY, {user_id: now}) + @classmethod def user_count(cls): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) count = r.zcard(cls.KEY) return count - + @classmethod def user_position(cls, user_id): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) @@ -2331,7 +2670,7 @@ def user_position(cls, user_id): return -1 if position >= 0: return position + 1 - + @classmethod def pop_user(cls): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) @@ -2339,4 +2678,3 @@ def pop_user(cls): r.zrem(cls.KEY, user) return user - diff --git a/apps/profile/tasks.py b/apps/profile/tasks.py index 9b8f7fd4b8..70cdb043a6 100644 --- a/apps/profile/tasks.py +++ b/apps/profile/tasks.py @@ -1,20 +1,24 @@ import datetime -from newsblur_web.celeryapp import app + from apps.profile.models import Profile, RNewUserQueue -from utils import log as logging from apps.reader.models import UserSubscription, UserSubscriptionFolders -from apps.social.models import MSocialServices, MActivity, MInteraction +from apps.social.models import MActivity, MInteraction, MSocialServices +from newsblur_web.celeryapp import app +from utils import log as logging + @app.task(name="email-new-user") def EmailNewUser(user_id): user_profile = Profile.objects.get(user__pk=user_id) user_profile.send_new_user_email() + @app.task(name="email-new-premium") def EmailNewPremium(user_id): user_profile = Profile.objects.get(user__pk=user_id) user_profile.send_new_premium_email() + @app.task() def FetchArchiveFeedsForUser(user_id): # subs = UserSubscription.objects.filter(user=user_id) @@ -23,33 +27,39 @@ def FetchArchiveFeedsForUser(user_id): UserSubscription.fetch_archive_feeds_for_user(user_id) + @app.task() def FetchArchiveFeedsChunk(user_id, feed_ids): # logging.debug(" ---> Fetching archive stories: %s for %s" % (feed_ids, user_id)) UserSubscription.fetch_archive_feeds_chunk(user_id, feed_ids) + @app.task() def FinishFetchArchiveFeeds(results, user_id, start_time, starting_story_count): # logging.debug(" ---> Fetching archive stories finished for %s" % (user_id)) - ending_story_count, pre_archive_count = UserSubscription.finish_fetch_archive_feeds(user_id, start_time, starting_story_count) + ending_story_count, pre_archive_count = UserSubscription.finish_fetch_archive_feeds( + user_id, start_time, starting_story_count + ) user_profile = Profile.objects.get(user__pk=user_id) user_profile.send_new_premium_archive_email(ending_story_count, pre_archive_count) + @app.task(name="email-new-premium-pro") def EmailNewPremiumPro(user_id): user_profile = Profile.objects.get(user__pk=user_id) user_profile.send_new_premium_pro_email() + @app.task(name="premium-expire") def PremiumExpire(**kwargs): # Get expired but grace period users two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2) thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30) - expired_profiles = Profile.objects.filter(is_premium=True, - premium_expire__lte=two_days_ago, - premium_expire__gt=thirty_days_ago) + expired_profiles = Profile.objects.filter( + is_premium=True, premium_expire__lte=two_days_ago, premium_expire__gt=thirty_days_ago + ) logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count()) for profile in expired_profiles: if profile.grace_period_email_sent(): @@ -57,21 +67,24 @@ def PremiumExpire(**kwargs): profile.setup_premium_history() if profile.premium_expire < two_days_ago: profile.send_premium_expire_grace_period_email() - + # Get fully expired users - expired_profiles = Profile.objects.filter(is_premium=True, - premium_expire__lte=thirty_days_ago) - logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count()) + expired_profiles = Profile.objects.filter(is_premium=True, premium_expire__lte=thirty_days_ago) + logging.debug( + " ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count() + ) for profile in expired_profiles: profile.setup_premium_history() if profile.premium_expire < thirty_days_ago: profile.send_premium_expire_email() profile.deactivate_premium() + @app.task(name="activate-next-new-user") def ActivateNextNewUser(): RNewUserQueue.activate_next() + @app.task(name="cleanup-user") def CleanupUser(user_id): UserSubscription.trim_user_read_stories(user_id) @@ -82,7 +95,7 @@ def CleanupUser(user_id): UserSubscriptionFolders.add_missing_feeds_for_user(user_id) UserSubscriptionFolders.compact_for_user(user_id) UserSubscription.refresh_stale_feeds(user_id) - + try: ss = MSocialServices.objects.get(user_id=user_id) except MSocialServices.DoesNotExist: @@ -90,14 +103,14 @@ def CleanupUser(user_id): return ss.sync_twitter_photo() + @app.task(name="clean-spam") def CleanSpam(): logging.debug(" ---> Finding spammers...") Profile.clear_dead_spammers(confirm=True) + @app.task(name="reimport-stripe-history") def ReimportStripeHistory(): logging.debug(" ---> Reimporting Stripe history...") Profile.reimport_stripe_history(limit=10, days=1) - - diff --git a/apps/profile/test_profile.py b/apps/profile/test_profile.py index d35afcf3b4..ac66ac37f7 100644 --- a/apps/profile/test_profile.py +++ b/apps/profile/test_profile.py @@ -1,37 +1,41 @@ -from utils import json_functions as json -from django.test.client import Client +from django.conf import settings from django.test import TestCase +from django.test.client import Client from django.urls import reverse -from django.conf import settings from mongoengine.connection import connect, disconnect +from utils import json_functions as json + + class Test_Profile(TestCase): fixtures = [ - 'subscriptions.json', - 'rss_feeds.json', + "subscriptions.json", + "rss_feeds.json", ] - + def setUp(self): disconnect() - settings.MONGODB = connect('test_newsblur') - self.client = Client(HTTP_USER_AGENT='Mozilla/5.0') + settings.MONGODB = connect("test_newsblur") + self.client = Client(HTTP_USER_AGENT="Mozilla/5.0") def tearDown(self): - settings.MONGODB.drop_database('test_newsblur') - + settings.MONGODB.drop_database("test_newsblur") + def test_create_account(self): - resp = self.client.get(reverse('load-feeds')) + resp = self.client.get(reverse("load-feeds")) response = json.decode(resp.content) - self.assertEquals(response['authenticated'], False) + self.assertEquals(response["authenticated"], False) - response = self.client.post(reverse('welcome-signup'), { - 'signup-username': 'test', - 'signup-password': 'password', - 'signup-email': 'test@newsblur.com', - }) + response = self.client.post( + reverse("welcome-signup"), + { + "signup-username": "test", + "signup-password": "password", + "signup-email": "test@newsblur.com", + }, + ) self.assertEquals(response.status_code, 302) - resp = self.client.get(reverse('load-feeds')) + resp = self.client.get(reverse("load-feeds")) response = json.decode(resp.content) - self.assertEquals(response['authenticated'], True) - \ No newline at end of file + self.assertEquals(response["authenticated"], True) diff --git a/apps/profile/urls.py b/apps/profile/urls.py index cc264e4e41..ac5a0b9498 100644 --- a/apps/profile/urls.py +++ b/apps/profile/urls.py @@ -1,42 +1,47 @@ from django.conf.urls import * + from apps.profile import views urlpatterns = [ - url(r'^get_preferences?/?', views.get_preference), - url(r'^set_preference/?', views.set_preference), - url(r'^set_account_settings/?', views.set_account_settings), - url(r'^get_view_setting/?', views.get_view_setting), - url(r'^set_view_setting/?', views.set_view_setting), - url(r'^clear_view_setting/?', views.clear_view_setting), - url(r'^set_collapsed_folders/?', views.set_collapsed_folders), - url(r'^paypal_form/?', views.paypal_form), - url(r'^paypal_return/?', views.paypal_return, name='paypal-return'), - url(r'^paypal_archive_return/?', views.paypal_archive_return, name='paypal-archive-return'), - url(r'^stripe_return/?', views.paypal_return, name='stripe-return'), - url(r'^switch_stripe_subscription/?', views.switch_stripe_subscription, name='switch-stripe-subscription'), - url(r'^switch_paypal_subscription/?', views.switch_paypal_subscription, name='switch-paypal-subscription'), - url(r'^is_premium/?', views.profile_is_premium, name='profile-is-premium'), - url(r'^is_premium_archive/?', views.profile_is_premium_archive, name='profile-is-premium-archive'), + url(r"^get_preferences?/?", views.get_preference), + url(r"^set_preference/?", views.set_preference), + url(r"^set_account_settings/?", views.set_account_settings), + url(r"^get_view_setting/?", views.get_view_setting), + url(r"^set_view_setting/?", views.set_view_setting), + url(r"^clear_view_setting/?", views.clear_view_setting), + url(r"^set_collapsed_folders/?", views.set_collapsed_folders), + url(r"^paypal_form/?", views.paypal_form), + url(r"^paypal_return/?", views.paypal_return, name="paypal-return"), + url(r"^paypal_archive_return/?", views.paypal_archive_return, name="paypal-archive-return"), + url(r"^stripe_return/?", views.paypal_return, name="stripe-return"), + url( + r"^switch_stripe_subscription/?", views.switch_stripe_subscription, name="switch-stripe-subscription" + ), + url( + r"^switch_paypal_subscription/?", views.switch_paypal_subscription, name="switch-paypal-subscription" + ), + url(r"^is_premium/?", views.profile_is_premium, name="profile-is-premium"), + url(r"^is_premium_archive/?", views.profile_is_premium_archive, name="profile-is-premium-archive"), # url(r'^paypal_ipn/?', include('paypal.standard.ipn.urls'), name='paypal-ipn'), - url(r'^paypal_ipn/?', views.paypal_ipn, name='paypal-ipn'), - url(r'^paypal_webhooks/?', views.paypal_webhooks, name='paypal-webhooks'), - url(r'^stripe_form/?', views.stripe_form, name='stripe-form'), - url(r'^stripe_checkout/?', views.stripe_checkout, name='stripe-checkout'), - url(r'^activities/?', views.load_activities, name='profile-activities'), - url(r'^payment_history/?', views.payment_history, name='profile-payment-history'), - url(r'^cancel_premium/?', views.cancel_premium, name='profile-cancel-premium'), - url(r'^refund_premium/?', views.refund_premium, name='profile-refund-premium'), - url(r'^never_expire_premium/?', views.never_expire_premium, name='profile-never-expire-premium'), - url(r'^upgrade_premium/?', views.upgrade_premium, name='profile-upgrade-premium'), - url(r'^save_ios_receipt/?', views.save_ios_receipt, name='save-ios-receipt'), - url(r'^save_android_receipt/?', views.save_android_receipt, name='save-android-receipt'), - url(r'^update_payment_history/?', views.update_payment_history, name='profile-update-payment-history'), - url(r'^delete_account/?', views.delete_account, name='profile-delete-account'), - url(r'^forgot_password_return/?', views.forgot_password_return, name='profile-forgot-password-return'), - url(r'^forgot_password/?', views.forgot_password, name='profile-forgot-password'), - url(r'^delete_starred_stories/?', views.delete_starred_stories, name='profile-delete-starred-stories'), - url(r'^delete_all_sites/?', views.delete_all_sites, name='profile-delete-all-sites'), - url(r'^email_optout/?', views.email_optout, name='profile-email-optout'), - url(r'^ios_subscription_status/?', views.ios_subscription_status, name='profile-ios-subscription-status'), - url(r'debug/?', views.trigger_error, name='trigger-error'), + url(r"^paypal_ipn/?", views.paypal_ipn, name="paypal-ipn"), + url(r"^paypal_webhooks/?", views.paypal_webhooks, name="paypal-webhooks"), + url(r"^stripe_form/?", views.stripe_form, name="stripe-form"), + url(r"^stripe_checkout/?", views.stripe_checkout, name="stripe-checkout"), + url(r"^activities/?", views.load_activities, name="profile-activities"), + url(r"^payment_history/?", views.payment_history, name="profile-payment-history"), + url(r"^cancel_premium/?", views.cancel_premium, name="profile-cancel-premium"), + url(r"^refund_premium/?", views.refund_premium, name="profile-refund-premium"), + url(r"^never_expire_premium/?", views.never_expire_premium, name="profile-never-expire-premium"), + url(r"^upgrade_premium/?", views.upgrade_premium, name="profile-upgrade-premium"), + url(r"^save_ios_receipt/?", views.save_ios_receipt, name="save-ios-receipt"), + url(r"^save_android_receipt/?", views.save_android_receipt, name="save-android-receipt"), + url(r"^update_payment_history/?", views.update_payment_history, name="profile-update-payment-history"), + url(r"^delete_account/?", views.delete_account, name="profile-delete-account"), + url(r"^forgot_password_return/?", views.forgot_password_return, name="profile-forgot-password-return"), + url(r"^forgot_password/?", views.forgot_password, name="profile-forgot-password"), + url(r"^delete_starred_stories/?", views.delete_starred_stories, name="profile-delete-starred-stories"), + url(r"^delete_all_sites/?", views.delete_all_sites, name="profile-delete-all-sites"), + url(r"^email_optout/?", views.email_optout, name="profile-email-optout"), + url(r"^ios_subscription_status/?", views.ios_subscription_status, name="profile-ios-subscription-status"), + url(r"debug/?", views.trigger_error, name="trigger-error"), ] diff --git a/apps/profile/views.py b/apps/profile/views.py index 4700aa6594..0065c41383 100644 --- a/apps/profile/views.py +++ b/apps/profile/views.py @@ -1,101 +1,138 @@ -import re -import stripe -import requests import datetime +import json as python_json +import re + import dateutil -from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_POST -from django.views.decorators.csrf import csrf_protect, csrf_exempt -from django.contrib.auth import logout as logout_user +import requests +import stripe +from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth import login as login_user +from django.contrib.auth import logout as logout_user +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.core.mail import mail_admins from django.db.models.aggregates import Sum from django.http import HttpResponse, HttpResponseRedirect -from django.contrib.sites.models import Site -from django.contrib.auth.models import User -from django.contrib.admin.views.decorators import staff_member_required -from django.urls import reverse from django.shortcuts import render -from django.core.mail import mail_admins -from django.conf import settings -from apps.profile.models import Profile, PaymentHistory, RNewUserQueue, MRedeemedCode, MGiftCode, PaypalIds -from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory -from apps.profile.forms import StripePlusPaymentForm, PLANS, DeleteAccountForm -from apps.profile.forms import ForgotPasswordForm, ForgotPasswordReturnForm, AccountSettingsForm -from apps.profile.forms import RedeemCodeForm -from apps.reader.forms import SignupForm, LoginForm +from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt, csrf_protect +from django.views.decorators.http import require_POST +from paypal.standard.forms import PayPalPaymentsForm +from paypal.standard.ipn.views import ipn as paypal_standard_ipn + +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, +) +from apps.profile.forms import ( + PLANS, + AccountSettingsForm, + DeleteAccountForm, + ForgotPasswordForm, + ForgotPasswordReturnForm, + RedeemCodeForm, + StripePlusPaymentForm, +) +from apps.profile.models import ( + MGiftCode, + MRedeemedCode, + PaymentHistory, + PaypalIds, + Profile, + RNewUserQueue, +) +from apps.reader.forms import LoginForm, SignupForm +from apps.reader.models import RUserStory, UserSubscription, UserSubscriptionFolders from apps.rss_feeds.models import MStarredStory, MStarredStoryCounts -from apps.social.models import MSocialServices, MActivity, MSocialProfile -from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag +from apps.social.models import MActivity, MSocialProfile, MSocialServices from utils import json_functions as json -import json as python_json -from utils.user_functions import ajax_login_required -from utils.view_functions import render_to, is_true -from utils.user_functions import get_user from utils import log as logging +from utils.user_functions import ajax_login_required, get_user +from utils.view_functions import is_true, render_to from vendor.paypalapi.exceptions import PayPalAPIResponseError -from paypal.standard.forms import PayPalPaymentsForm -from paypal.standard.ipn.views import ipn as paypal_standard_ipn -INTEGER_FIELD_PREFS = ('feed_pane_size', 'days_of_unread') -SINGLE_FIELD_PREFS = ('timezone','hide_mobile','send_emails', - 'hide_getting_started', 'has_setup_feeds', 'has_found_friends', - 'has_trained_intelligence') -SPECIAL_PREFERENCES = ('old_password', 'new_password', 'autofollow_friends', 'dashboard_date',) +INTEGER_FIELD_PREFS = ("feed_pane_size", "days_of_unread") +SINGLE_FIELD_PREFS = ( + "timezone", + "hide_mobile", + "send_emails", + "hide_getting_started", + "has_setup_feeds", + "has_found_friends", + "has_trained_intelligence", +) +SPECIAL_PREFERENCES = ( + "old_password", + "new_password", + "autofollow_friends", + "dashboard_date", +) + @ajax_login_required @require_POST @json.json_view def set_preference(request): code = 1 - message = '' + message = "" new_preferences = request.POST - + preferences = json.decode(request.user.profile.preferences) for preference_name, preference_value in list(new_preferences.items()): - if preference_value in ['true','false']: preference_value = True if preference_value == 'true' else False + if preference_value in ["true", "false"]: + preference_value = True if preference_value == "true" else False if preference_name in SINGLE_FIELD_PREFS: setattr(request.user.profile, preference_name, preference_value) elif preference_name in INTEGER_FIELD_PREFS: - if preference_name == "days_of_unread" and int(preference_value) != request.user.profile.days_of_unread: + if ( + preference_name == "days_of_unread" + and int(preference_value) != request.user.profile.days_of_unread + ): UserSubscription.all_subs_needs_unread_recalc(request.user.pk) setattr(request.user.profile, preference_name, int(preference_value)) if preference_name in preferences: del preferences[preference_name] elif preference_name in SPECIAL_PREFERENCES: - if preference_name == 'autofollow_friends': + if preference_name == "autofollow_friends": social_services = MSocialServices.get_user(request.user.pk) social_services.autofollow = preference_value social_services.save() - elif preference_name == 'dashboard_date': + elif preference_name == "dashboard_date": request.user.profile.dashboard_date = datetime.datetime.utcnow() else: if preference_value in ["true", "false"]: preference_value = True if preference_value == "true" else False preferences[preference_name] = preference_value - if preference_name == 'intro_page': + if preference_name == "intro_page": logging.user(request, "~FBAdvancing intro to page ~FM~SB%s" % preference_value) - + request.user.profile.preferences = json.encode(preferences) request.user.profile.save() - + logging.user(request, "~FMSaving preference: %s" % new_preferences) response = dict(code=code, message=message, new_preferences=new_preferences) return response + @ajax_login_required @json.json_view def get_preference(request): code = 1 - preference_name = request.POST.get('preference') + preference_name = request.POST.get("preference") preferences = json.decode(request.user.profile.preferences) - + payload = preferences if preference_name: payload = preferences.get(preference_name) - + response = dict(code=code, payload=payload) return response + @csrf_protect def login(request): form = LoginForm() @@ -103,74 +140,80 @@ def login(request): if request.method == "POST": form = LoginForm(data=request.POST) if form.is_valid(): - login_user(request, form.get_user(), backend='django.contrib.auth.backends.ModelBackend') + login_user(request, form.get_user(), backend="django.contrib.auth.backends.ModelBackend") logging.user(form.get_user(), "~FG~BBOAuth Login~FW") - return HttpResponseRedirect(request.POST['next'] or reverse('index')) + return HttpResponseRedirect(request.POST["next"] or reverse("index")) + + return render( + request, + "accounts/login.html", + {"form": form, "next": request.POST.get("next", "") or request.GET.get("next", "")}, + ) + - return render(request, 'accounts/login.html', { - 'form': form, - 'next': request.POST.get('next', "") or request.GET.get('next', "") - }) - @csrf_exempt def signup(request): form = SignupForm(prefix="signup") - recaptcha = request.POST.get('g-recaptcha-response', None) + recaptcha = request.POST.get("g-recaptcha-response", None) recaptcha_error = None - + if settings.ENFORCE_SIGNUP_CAPTCHA: if not recaptcha: - recaptcha_error = "Please hit the \"I'm not a robot\" button." + recaptcha_error = 'Please hit the "I\'m not a robot" button.' else: - response = requests.post('https://www.google.com/recaptcha/api/siteverify', { - 'secret': settings.RECAPTCHA_SECRET_KEY, - 'response': recaptcha, - }) + response = requests.post( + "https://www.google.com/recaptcha/api/siteverify", + { + "secret": settings.RECAPTCHA_SECRET_KEY, + "response": recaptcha, + }, + ) result = response.json() - if not result['success']: - recaptcha_error = "Really, please hit the \"I'm not a robot\" button." + if not result["success"]: + recaptcha_error = 'Really, please hit the "I\'m not a robot" button.' if request.method == "POST": form = SignupForm(data=request.POST, prefix="signup") if form.is_valid() and not recaptcha_error: new_user = form.save() - login_user(request, new_user, backend='django.contrib.auth.backends.ModelBackend') + login_user(request, new_user, backend="django.contrib.auth.backends.ModelBackend") logging.user(new_user, "~FG~SB~BBNEW SIGNUP: ~FW%s" % new_user.email) new_user.profile.activate_free() - return HttpResponseRedirect(request.POST['next'] or reverse('index')) + return HttpResponseRedirect(request.POST["next"] or reverse("index")) + + return render( + request, + "accounts/signup.html", + {"form": form, "recaptcha_error": recaptcha_error, "next": request.POST.get("next", "")}, + ) - return render(request, 'accounts/signup.html', { - 'form': form, - 'recaptcha_error': recaptcha_error, - 'next': request.POST.get('next', "") - }) @login_required @csrf_protect def redeem_code(request): - code = request.GET.get('code', None) - form = RedeemCodeForm(initial={'gift_code': code}) + code = request.GET.get("code", None) + form = RedeemCodeForm(initial={"gift_code": code}) if request.method == "POST": form = RedeemCodeForm(data=request.POST) if form.is_valid(): - gift_code = request.POST['gift_code'] + gift_code = request.POST["gift_code"] MRedeemedCode.redeem(user=request.user, gift_code=gift_code) - return render(request, 'reader/paypal_return.xhtml') + return render(request, "reader/paypal_return.xhtml") + + return render( + request, + "accounts/redeem_code.html", + {"form": form, "code": request.POST.get("code", ""), "next": request.POST.get("next", "")}, + ) - return render(request, 'accounts/redeem_code.html', { - 'form': form, - 'code': request.POST.get('code', ""), - 'next': request.POST.get('next', "") - }) - @ajax_login_required @require_POST @json.json_view def set_account_settings(request): code = -1 - message = 'OK' + message = "OK" form = AccountSettingsForm(user=request.user, data=request.POST) if form.is_valid(): @@ -178,100 +221,113 @@ def set_account_settings(request): code = 1 else: message = form.errors[list(form.errors.keys())[0]][0] - + payload = { "username": request.user.username, "email": request.user.email, - "social_profile": MSocialProfile.profile(request.user.pk) + "social_profile": MSocialProfile.profile(request.user.pk), } return dict(code=code, message=message, payload=payload) - + + @ajax_login_required @require_POST @json.json_view def set_view_setting(request): code = 1 - feed_id = request.POST['feed_id'] - feed_view_setting = request.POST.get('feed_view_setting') - feed_order_setting = request.POST.get('feed_order_setting') - feed_read_filter_setting = request.POST.get('feed_read_filter_setting') - feed_layout_setting = request.POST.get('feed_layout_setting') - feed_dashboard_count_setting = request.POST.get('feed_dashboard_count_setting') + feed_id = request.POST["feed_id"] + feed_view_setting = request.POST.get("feed_view_setting") + feed_order_setting = request.POST.get("feed_order_setting") + feed_read_filter_setting = request.POST.get("feed_read_filter_setting") + feed_layout_setting = request.POST.get("feed_layout_setting") + feed_dashboard_count_setting = request.POST.get("feed_dashboard_count_setting") view_settings = json.decode(request.user.profile.view_settings) - + setting = view_settings.get(feed_id, {}) - if isinstance(setting, str): setting = {'v': setting} - if feed_view_setting: setting['v'] = feed_view_setting - if feed_order_setting: setting['o'] = feed_order_setting - if feed_read_filter_setting: setting['r'] = feed_read_filter_setting - if feed_dashboard_count_setting: setting['d'] = feed_dashboard_count_setting - if feed_layout_setting: setting['l'] = feed_layout_setting - + if isinstance(setting, str): + setting = {"v": setting} + if feed_view_setting: + setting["v"] = feed_view_setting + if feed_order_setting: + setting["o"] = feed_order_setting + if feed_read_filter_setting: + setting["r"] = feed_read_filter_setting + if feed_dashboard_count_setting: + setting["d"] = feed_dashboard_count_setting + if feed_layout_setting: + setting["l"] = feed_layout_setting + view_settings[feed_id] = setting request.user.profile.view_settings = json.encode(view_settings) request.user.profile.save() - - logging.user(request, "~FMView settings: %s/%s/%s/%s" % (feed_view_setting, - feed_order_setting, feed_read_filter_setting, feed_layout_setting)) + + logging.user( + request, + "~FMView settings: %s/%s/%s/%s" + % (feed_view_setting, feed_order_setting, feed_read_filter_setting, feed_layout_setting), + ) response = dict(code=code) return response + @ajax_login_required @require_POST @json.json_view def clear_view_setting(request): code = 1 - view_setting_type = request.POST.get('view_setting_type') + view_setting_type = request.POST.get("view_setting_type") view_settings = json.decode(request.user.profile.view_settings) new_view_settings = {} removed = 0 for feed_id, view_setting in list(view_settings.items()): - if view_setting_type == 'layout' and 'l' in view_setting: - del view_setting['l'] + if view_setting_type == "layout" and "l" in view_setting: + del view_setting["l"] removed += 1 - if view_setting_type == 'view' and 'v' in view_setting: - del view_setting['v'] + if view_setting_type == "view" and "v" in view_setting: + del view_setting["v"] removed += 1 - if view_setting_type == 'order' and 'o' in view_setting: - del view_setting['o'] + if view_setting_type == "order" and "o" in view_setting: + del view_setting["o"] removed += 1 - if view_setting_type == 'order' and 'r' in view_setting: - del view_setting['r'] + if view_setting_type == "order" and "r" in view_setting: + del view_setting["r"] removed += 1 new_view_settings[feed_id] = view_setting request.user.profile.view_settings = json.encode(new_view_settings) request.user.profile.save() - + logging.user(request, "~FMClearing view settings: %s (found %s)" % (view_setting_type, removed)) response = dict(code=code, view_settings=view_settings, removed=removed) return response - + + @ajax_login_required @json.json_view def get_view_setting(request): code = 1 - feed_id = request.POST['feed_id'] + feed_id = request.POST["feed_id"] view_settings = json.decode(request.user.profile.view_settings) - + response = dict(code=code, payload=view_settings.get(feed_id)) return response - + @ajax_login_required @require_POST @json.json_view def set_collapsed_folders(request): code = 1 - collapsed_folders = request.POST['collapsed_folders'] - + collapsed_folders = request.POST["collapsed_folders"] + request.user.profile.collapsed_folders = collapsed_folders request.user.profile.save() - + logging.user(request, "~FMCollapsing folder: %s" % collapsed_folders) response = dict(code=code) return response + def paypal_ipn(request): try: return paypal_standard_ipn(request) @@ -279,23 +335,24 @@ def paypal_ipn(request): # Paypal may have sent webhooks to ipn, so redirect logging.user(request, f" ---> Paypal IPN to webhooks redirect: {request.body}") return paypal_webhooks(request) - + + def paypal_webhooks(request): try: data = json.decode(request.body) except python_json.decoder.JSONDecodeError: # Kick it over to paypal ipn return paypal_standard_ipn(request) - + logging.user(request, f" ---> Paypal webhooks {data.get('event_type', '')} data: {data}") - - if data['event_type'] == "BILLING.SUBSCRIPTION.CREATED": + + if data["event_type"] == "BILLING.SUBSCRIPTION.CREATED": # Don't start a subscription but save it in case the payment comes before the subscription activation - user = User.objects.get(pk=int(data['resource']['custom_id'])) - user.profile.store_paypal_sub_id(data['resource']['id'], skip_save_primary=True) - elif data['event_type'] in ["BILLING.SUBSCRIPTION.ACTIVATED", "BILLING.SUBSCRIPTION.UPDATED"]: - user = User.objects.get(pk=int(data['resource']['custom_id'])) - user.profile.store_paypal_sub_id(data['resource']['id']) + user = User.objects.get(pk=int(data["resource"]["custom_id"])) + user.profile.store_paypal_sub_id(data["resource"]["id"], skip_save_primary=True) + elif data["event_type"] in ["BILLING.SUBSCRIPTION.ACTIVATED", "BILLING.SUBSCRIPTION.UPDATED"]: + user = User.objects.get(pk=int(data["resource"]["custom_id"])) + user.profile.store_paypal_sub_id(data["resource"]["id"]) # plan_id = data['resource']['plan_id'] # if plan_id == Profile.plan_to_paypal_plan_id('premium'): # user.profile.activate_premium() @@ -305,43 +362,44 @@ def paypal_webhooks(request): # user.profile.activate_pro() user.profile.cancel_premium_stripe() user.profile.setup_premium_history() - if data['event_type'] == "BILLING.SUBSCRIPTION.ACTIVATED": + if data["event_type"] == "BILLING.SUBSCRIPTION.ACTIVATED": user.profile.cancel_and_prorate_existing_paypal_subscriptions(data) - elif data['event_type'] == "PAYMENT.SALE.COMPLETED": - user = User.objects.get(pk=int(data['resource']['custom'])) + elif data["event_type"] == "PAYMENT.SALE.COMPLETED": + user = User.objects.get(pk=int(data["resource"]["custom"])) user.profile.setup_premium_history() - elif data['event_type'] == "PAYMENT.CAPTURE.REFUNDED": - user = User.objects.get(pk=int(data['resource']['custom_id'])) + elif data["event_type"] == "PAYMENT.CAPTURE.REFUNDED": + user = User.objects.get(pk=int(data["resource"]["custom_id"])) user.profile.setup_premium_history() - elif data['event_type'] in ["BILLING.SUBSCRIPTION.CANCELLED", "BILLING.SUBSCRIPTION.SUSPENDED"]: - custom_id = data['resource'].get('custom_id', None) + elif data["event_type"] in ["BILLING.SUBSCRIPTION.CANCELLED", "BILLING.SUBSCRIPTION.SUSPENDED"]: + custom_id = data["resource"].get("custom_id", None) if custom_id: user = User.objects.get(pk=int(custom_id)) else: - paypal_id = PaypalIds.objects.get(paypal_sub_id=data['resource']['id']) + paypal_id = PaypalIds.objects.get(paypal_sub_id=data["resource"]["id"]) user = paypal_id.user user.profile.setup_premium_history() return HttpResponse("OK") + def paypal_form(request): domain = Site.objects.get_current().domain if settings.DEBUG: domain = "73ee-71-233-245-159.ngrok.io" - + paypal_dict = { "cmd": "_xclick-subscriptions", "business": "samuel@ofbrooklyn.com", - "a3": "12.00", # price - "p3": 1, # duration of each unit (depends on unit) - "t3": "Y", # duration unit ("M for Month") - "src": "1", # make payments recur - "sra": "1", # reattempt payment on payment error - "no_note": "1", # remove extra notes (optional) + "a3": "12.00", # price + "p3": 1, # duration of each unit (depends on unit) + "t3": "Y", # duration unit ("M for Month") + "src": "1", # make payments recur + "sra": "1", # reattempt payment on payment error + "no_note": "1", # remove extra notes (optional) "item_name": "NewsBlur Premium Account", - "notify_url": "https://%s%s" % (domain, reverse('paypal-ipn')), - "return_url": "https://%s%s" % (domain, reverse('paypal-return')), - "cancel_return": "https://%s%s" % (domain, reverse('index')), + "notify_url": "https://%s%s" % (domain, reverse("paypal-ipn")), + "return_url": "https://%s%s" % (domain, reverse("paypal-return")), + "cancel_return": "https://%s%s" % (domain, reverse("index")), "custom": request.user.username, } @@ -351,303 +409,360 @@ def paypal_form(request): logging.user(request, "~FBLoading paypal/feedchooser") # Output the button. - return HttpResponse(form.render(), content_type='text/html') + return HttpResponse(form.render(), content_type="text/html") + @login_required def paypal_return(request): + return render( + request, + "reader/paypal_return.xhtml", + { + "user_profile": request.user.profile, + }, + ) - return render(request, 'reader/paypal_return.xhtml', { - 'user_profile': request.user.profile, - }) @login_required def paypal_archive_return(request): + return render( + request, + "reader/paypal_archive_return.xhtml", + { + "user_profile": request.user.profile, + }, + ) - return render(request, 'reader/paypal_archive_return.xhtml', { - 'user_profile': request.user.profile, - }) @login_required def activate_premium(request): - return HttpResponseRedirect(reverse('index')) - + return HttpResponseRedirect(reverse("index")) + + @ajax_login_required @json.json_view def profile_is_premium(request): # Check tries code = 0 - retries = int(request.GET['retries']) - + retries = int(request.GET["retries"]) + subs = UserSubscription.objects.filter(user=request.user) total_subs = subs.count() activated_subs = subs.filter(active=True).count() - + if retries >= 30: code = -1 if not request.user.profile.is_premium: subject = "Premium activation failed: %s (%s/%s)" % (request.user, activated_subs, total_subs) - message = """User: %s (%s) -- Email: %s""" % (request.user.username, request.user.pk, request.user.email) + message = """User: %s (%s) -- Email: %s""" % ( + request.user.username, + request.user.pk, + request.user.email, + ) mail_admins(subject, message) request.user.profile.activate_premium() - + profile = Profile.objects.get(user=request.user) return { - 'is_premium': profile.is_premium, - 'is_premium_archive': profile.is_archive, - 'code': code, - 'activated_subs': activated_subs, - 'total_subs': total_subs, + "is_premium": profile.is_premium, + "is_premium_archive": profile.is_archive, + "code": code, + "activated_subs": activated_subs, + "total_subs": total_subs, } + @ajax_login_required @json.json_view def profile_is_premium_archive(request): # Check tries code = 0 - retries = int(request.GET['retries']) + retries = int(request.GET["retries"]) subs = UserSubscription.objects.filter(user=request.user) total_subs = subs.count() activated_subs = subs.filter(feed__archive_subscribers__gte=1).count() - + if retries >= 30: code = -1 if not request.user.profile.is_premium_archive: - subject = "Premium archive activation failed: %s (%s/%s)" % (request.user, activated_subs, total_subs) - message = """User: %s (%s) -- Email: %s""" % (request.user.username, request.user.pk, request.user.email) + subject = "Premium archive activation failed: %s (%s/%s)" % ( + request.user, + activated_subs, + total_subs, + ) + message = """User: %s (%s) -- Email: %s""" % ( + request.user.username, + request.user.pk, + request.user.email, + ) mail_admins(subject, message) request.user.profile.activate_archive() profile = Profile.objects.get(user=request.user) return { - 'is_premium': profile.is_premium, - 'is_premium_archive': profile.is_archive, - 'code': code, - 'activated_subs': activated_subs, - 'total_subs': total_subs, + "is_premium": profile.is_premium, + "is_premium_archive": profile.is_archive, + "code": code, + "activated_subs": activated_subs, + "total_subs": total_subs, } + @ajax_login_required @json.json_view def save_ios_receipt(request): - receipt = request.POST.get('receipt') - product_identifier = request.POST.get('product_identifier') - transaction_identifier = request.POST.get('transaction_identifier') - + receipt = request.POST.get("receipt") + product_identifier = request.POST.get("product_identifier") + transaction_identifier = request.POST.get("transaction_identifier") + logging.user(request, "~BM~FBSaving iOS Receipt: %s %s" % (product_identifier, transaction_identifier)) - + paid = request.user.profile.activate_ios_premium(transaction_identifier) if paid: - logging.user(request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier)) + logging.user( + request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier) + ) subject = "iOS Premium: %s (%s)" % (request.user.profile, product_identifier) - message = """User: %s (%s) -- Email: %s, product: %s, txn: %s, receipt: %s""" % (request.user.username, request.user.pk, request.user.email, product_identifier, transaction_identifier, receipt) + message = """User: %s (%s) -- Email: %s, product: %s, txn: %s, receipt: %s""" % ( + request.user.username, + request.user.pk, + request.user.email, + product_identifier, + transaction_identifier, + receipt, + ) mail_admins(subject, message) else: - logging.user(request, "~BM~FBNot sending iOS Receipt email, already paid: %s %s" % (product_identifier, transaction_identifier)) - - + logging.user( + request, + "~BM~FBNot sending iOS Receipt email, already paid: %s %s" + % (product_identifier, transaction_identifier), + ) + return request.user.profile - + + @ajax_login_required @json.json_view def save_android_receipt(request): - order_id = request.POST.get('order_id') - product_id = request.POST.get('product_id') - + order_id = request.POST.get("order_id") + product_id = request.POST.get("product_id") + logging.user(request, "~BM~FBSaving Android Receipt: %s %s" % (product_id, order_id)) - + paid = request.user.profile.activate_android_premium(order_id) if paid: logging.user(request, "~BM~FBSending Android Receipt email: %s %s" % (product_id, order_id)) subject = "Android Premium: %s (%s)" % (request.user.profile, product_id) - message = """User: %s (%s) -- Email: %s, product: %s, order: %s""" % (request.user.username, request.user.pk, request.user.email, product_id, order_id) + message = """User: %s (%s) -- Email: %s, product: %s, order: %s""" % ( + request.user.username, + request.user.pk, + request.user.email, + product_id, + order_id, + ) mail_admins(subject, message) else: - logging.user(request, "~BM~FBNot sending Android Receipt email, already paid: %s %s" % (product_id, order_id)) - - + logging.user( + request, "~BM~FBNot sending Android Receipt email, already paid: %s %s" % (product_id, order_id) + ) + return request.user.profile - + + @login_required def stripe_form(request): user = request.user success_updating = False stripe.api_key = settings.STRIPE_SECRET plan = PLANS[0][0] - renew = is_true(request.GET.get('renew', False)) + renew = is_true(request.GET.get("renew", False)) error = None - - if request.method == 'POST': + + if request.method == "POST": zebra_form = StripePlusPaymentForm(request.POST, email=user.email) if zebra_form.is_valid(): - user.email = zebra_form.cleaned_data['email'] + user.email = zebra_form.cleaned_data["email"] user.save() customer = None - current_premium = (user.profile.is_premium and - user.profile.premium_expire and - user.profile.premium_expire > datetime.datetime.now()) - + current_premium = ( + user.profile.is_premium + and user.profile.premium_expire + and user.profile.premium_expire > datetime.datetime.now() + ) + # Are they changing their existing card? if user.profile.stripe_id: customer = stripe.Customer.retrieve(user.profile.stripe_id) try: - card = customer.sources.create(source=zebra_form.cleaned_data['stripe_token']) + card = customer.sources.create(source=zebra_form.cleaned_data["stripe_token"]) except stripe.error.CardError: error = "This card was declined." else: customer.default_card = card.id customer.save() - user.profile.strip_4_digits = zebra_form.cleaned_data['last_4_digits'] + user.profile.strip_4_digits = zebra_form.cleaned_data["last_4_digits"] user.profile.save() - user.profile.activate_premium() # TODO: Remove, because webhooks are slow + user.profile.activate_premium() # TODO: Remove, because webhooks are slow success_updating = True else: try: - customer = stripe.Customer.create(**{ - 'source': zebra_form.cleaned_data['stripe_token'], - 'plan': zebra_form.cleaned_data['plan'], - 'email': user.email, - 'description': user.username, - }) + customer = stripe.Customer.create( + **{ + "source": zebra_form.cleaned_data["stripe_token"], + "plan": zebra_form.cleaned_data["plan"], + "email": user.email, + "description": user.username, + } + ) except stripe.error.CardError: error = "This card was declined." else: - user.profile.strip_4_digits = zebra_form.cleaned_data['last_4_digits'] + user.profile.strip_4_digits = zebra_form.cleaned_data["last_4_digits"] user.profile.stripe_id = customer.id user.profile.save() - user.profile.activate_premium() # TODO: Remove, because webhooks are slow + user.profile.activate_premium() # TODO: Remove, because webhooks are slow success_updating = True - + # Check subscription to ensure latest plan, otherwise cancel it and subscribe if success_updating and customer and customer.subscriptions.total_count == 1: subscription = customer.subscriptions.data[0] - if subscription['plan']['id'] != "newsblur-premium-36": + if subscription["plan"]["id"] != "newsblur-premium-36": for sub in customer.subscriptions: sub.delete() customer = stripe.Customer.retrieve(user.profile.stripe_id) - + if success_updating and customer and customer.subscriptions.total_count == 0: params = dict( - customer=customer.id, - items=[ - { - "plan": "newsblur-premium-36", - }, - ]) + customer=customer.id, + items=[ + { + "plan": "newsblur-premium-36", + }, + ], + ) premium_expire = user.profile.premium_expire if current_premium and premium_expire: if premium_expire < (datetime.datetime.now() + datetime.timedelta(days=365)): - params['billing_cycle_anchor'] = premium_expire.strftime('%s') - params['trial_end'] = premium_expire.strftime('%s') + params["billing_cycle_anchor"] = premium_expire.strftime("%s") + params["trial_end"] = premium_expire.strftime("%s") stripe.Subscription.create(**params) else: zebra_form = StripePlusPaymentForm(email=user.email, plan=plan) - + if success_updating: - return render(request, 'reader/paypal_return.xhtml') - + return render(request, "reader/paypal_return.xhtml") + new_user_queue_count = RNewUserQueue.user_count() new_user_queue_position = RNewUserQueue.user_position(request.user.pk) new_user_queue_behind = 0 if new_user_queue_position >= 0: - new_user_queue_behind = new_user_queue_count - new_user_queue_position + new_user_queue_behind = new_user_queue_count - new_user_queue_position new_user_queue_position -= 1 - + immediate_charge = True if user.profile.premium_expire and user.profile.premium_expire > datetime.datetime.now(): immediate_charge = False - + logging.user(request, "~BM~FBLoading Stripe form") - return render(request, 'profile/stripe_form.xhtml', + return render( + request, + "profile/stripe_form.xhtml", { - 'zebra_form': zebra_form, - 'publishable': settings.STRIPE_PUBLISHABLE, - 'success_updating': success_updating, - 'new_user_queue_count': new_user_queue_count - 1, - 'new_user_queue_position': new_user_queue_position, - 'new_user_queue_behind': new_user_queue_behind, - 'renew': renew, - 'immediate_charge': immediate_charge, - 'error': error, - } + "zebra_form": zebra_form, + "publishable": settings.STRIPE_PUBLISHABLE, + "success_updating": success_updating, + "new_user_queue_count": new_user_queue_count - 1, + "new_user_queue_position": new_user_queue_position, + "new_user_queue_behind": new_user_queue_behind, + "renew": renew, + "immediate_charge": immediate_charge, + "error": error, + }, ) + @login_required def switch_stripe_subscription(request): - plan = request.POST['plan'] + plan = request.POST["plan"] if plan == "change_stripe": return stripe_checkout(request) elif plan == "change_paypal": paypal_url = request.user.profile.paypal_change_billing_details_url() return HttpResponseRedirect(paypal_url) - + switch_successful = request.user.profile.switch_stripe_subscription(plan) - - logging.user(request, "~FCSwitching subscription to ~SB%s~SN~FC (%s)" %( - plan, - '~FGsucceeded~FC' if switch_successful else '~FRfailed~FC' - )) - + + logging.user( + request, + "~FCSwitching subscription to ~SB%s~SN~FC (%s)" + % (plan, "~FGsucceeded~FC" if switch_successful else "~FRfailed~FC"), + ) + if switch_successful: - return HttpResponseRedirect(reverse('stripe-return')) - + return HttpResponseRedirect(reverse("stripe-return")) + return stripe_checkout(request) + def switch_paypal_subscription(request): - plan = request.POST['plan'] + plan = request.POST["plan"] if plan == "change_stripe": return stripe_checkout(request) elif plan == "change_paypal": paypal_url = request.user.profile.paypal_change_billing_details_url() return HttpResponseRedirect(paypal_url) - + approve_url = request.user.profile.switch_paypal_subscription_approval_url(plan) - - logging.user(request, "~FCSwitching subscription to ~SB%s~SN~FC (%s)" %( - plan, - '~FGsucceeded~FC' if approve_url else '~FRfailed~FC' - )) - + + logging.user( + request, + "~FCSwitching subscription to ~SB%s~SN~FC (%s)" + % (plan, "~FGsucceeded~FC" if approve_url else "~FRfailed~FC"), + ) + if approve_url: return HttpResponseRedirect(approve_url) - paypal_return = reverse('paypal-return') + paypal_return = reverse("paypal-return") if plan == "archive": - paypal_return = reverse('paypal-archive-return') + paypal_return = reverse("paypal-archive-return") return HttpResponseRedirect(paypal_return) + @login_required def stripe_checkout(request): stripe.api_key = settings.STRIPE_SECRET domain = Site.objects.get_current().domain - plan = request.POST['plan'] - + plan = request.POST["plan"] + if plan == "change_stripe": checkout_session = stripe.billing_portal.Session.create( customer=request.user.profile.stripe_id, - return_url="http://%s%s?next=payments" % (domain, reverse('index')), + return_url="https://%s%s?next=payments" % (domain, reverse('index')), ) return HttpResponseRedirect(checkout_session.url, status=303) - + price = Profile.plan_to_stripe_price(plan) - + session_dict = { "line_items": [ { - 'price': price, - 'quantity': 1, + "price": price, + "quantity": 1, }, ], - "mode": 'subscription', + "mode": "subscription", "metadata": {"newsblur_user_id": request.user.pk}, - "success_url": "http://%s%s" % (domain, reverse('stripe-return')), - "cancel_url": "http://%s%s" % (domain, reverse('index')), + "success_url": "https://%s%s" % (domain, reverse('stripe-return')), + "cancel_url": "https://%s%s" % (domain, reverse('index')), } if request.user.profile.stripe_id: - session_dict['customer'] = request.user.profile.stripe_id + session_dict["customer"] = request.user.profile.stripe_id else: session_dict["customer_email"] = request.user.email @@ -657,25 +772,27 @@ def stripe_checkout(request): return HttpResponseRedirect(checkout_session.url, status=303) -@render_to('reader/activities_module.xhtml') + +@render_to("reader/activities_module.xhtml") def load_activities(request): user = get_user(request) - page = max(1, int(request.GET.get('page', 1))) + page = max(1, int(request.GET.get("page", 1))) activities, has_next_page = MActivity.user(user.pk, page=page) return { - 'activities': activities, - 'page': page, - 'has_next_page': has_next_page, - 'username': 'You', + "activities": activities, + "page": page, + "has_next_page": has_next_page, + "username": "You", } + @ajax_login_required @json.json_view def payment_history(request): user = request.user if request.user.is_staff: - user_id = request.GET.get('user_id', request.user.pk) + user_id = request.GET.get("user_id", request.user.pk) user = User.objects.get(pk=user_id) history = PaymentHistory.objects.filter(user=user) @@ -690,19 +807,19 @@ def payment_history(request): "feeds": UserSubscription.objects.filter(user=user).count(), "email": user.email, "read_story_count": RUserStory.read_story_count(user.pk), - "feed_opens": UserSubscription.objects.filter(user=user).aggregate(sum=Sum('feed_opens'))['sum'], + "feed_opens": UserSubscription.objects.filter(user=user).aggregate(sum=Sum("feed_opens"))["sum"], "training": { - 'title_ps': MClassifierTitle.objects.filter(user_id=user.pk, score__gt=0).count(), - 'title_ng': MClassifierTitle.objects.filter(user_id=user.pk, score__lt=0).count(), - 'tag_ps': MClassifierTag.objects.filter(user_id=user.pk, score__gt=0).count(), - 'tag_ng': MClassifierTag.objects.filter(user_id=user.pk, score__lt=0).count(), - 'author_ps': MClassifierAuthor.objects.filter(user_id=user.pk, score__gt=0).count(), - 'author_ng': MClassifierAuthor.objects.filter(user_id=user.pk, score__lt=0).count(), - 'feed_ps': MClassifierFeed.objects.filter(user_id=user.pk, score__gt=0).count(), - 'feed_ng': MClassifierFeed.objects.filter(user_id=user.pk, score__lt=0).count(), - } + "title_ps": MClassifierTitle.objects.filter(user_id=user.pk, score__gt=0).count(), + "title_ng": MClassifierTitle.objects.filter(user_id=user.pk, score__lt=0).count(), + "tag_ps": MClassifierTag.objects.filter(user_id=user.pk, score__gt=0).count(), + "tag_ng": MClassifierTag.objects.filter(user_id=user.pk, score__lt=0).count(), + "author_ps": MClassifierAuthor.objects.filter(user_id=user.pk, score__gt=0).count(), + "author_ng": MClassifierAuthor.objects.filter(user_id=user.pk, score__lt=0).count(), + "feed_ps": MClassifierFeed.objects.filter(user_id=user.pk, score__gt=0).count(), + "feed_ng": MClassifierFeed.objects.filter(user_id=user.pk, score__lt=0).count(), + }, } - + next_invoice = None stripe_customer = user.profile.stripe_customer() paypal_api = user.profile.paypal_api() @@ -710,48 +827,54 @@ def payment_history(request): try: invoice = stripe.Invoice.upcoming(customer=stripe_customer.id) for lines in invoice.lines.data: - next_invoice = dict(payment_date=datetime.datetime.fromtimestamp(lines.period.start), - payment_amount=invoice.amount_due/100.0, - payment_provider="(scheduled)", - scheduled=True) + next_invoice = dict( + payment_date=datetime.datetime.fromtimestamp(lines.period.start), + payment_amount=invoice.amount_due / 100.0, + payment_provider="(scheduled)", + scheduled=True, + ) break except stripe.error.InvalidRequestError: pass - + if paypal_api and not next_invoice and user.profile.premium_renewal and len(history): - next_invoice = dict(payment_date=history[0].payment_date+dateutil.relativedelta.relativedelta(years=1), - payment_amount=history[0].payment_amount, - payment_provider="(scheduled)", - scheduled=True) - + next_invoice = dict( + payment_date=history[0].payment_date + dateutil.relativedelta.relativedelta(years=1), + payment_amount=history[0].payment_amount, + payment_provider="(scheduled)", + scheduled=True, + ) + return { - 'is_premium': user.profile.is_premium, - 'is_archive': user.profile.is_archive, - 'is_pro': user.profile.is_pro, - 'premium_expire': user.profile.premium_expire, - 'premium_renewal': user.profile.premium_renewal, - 'active_provider': user.profile.active_provider, - 'payments': history, - 'statistics': statistics, - 'next_invoice': next_invoice, + "is_premium": user.profile.is_premium, + "is_archive": user.profile.is_archive, + "is_pro": user.profile.is_pro, + "premium_expire": user.profile.premium_expire, + "premium_renewal": user.profile.premium_renewal, + "active_provider": user.profile.active_provider, + "payments": history, + "statistics": statistics, + "next_invoice": next_invoice, } + @ajax_login_required @json.json_view def cancel_premium(request): canceled = request.user.profile.cancel_premium() - + return { - 'code': 1 if canceled else -1, + "code": 1 if canceled else -1, } + @staff_member_required @ajax_login_required @json.json_view def refund_premium(request): - user_id = request.POST.get('user_id') - partial = request.POST.get('partial', False) - provider = request.POST.get('provider', None) + user_id = request.POST.get("user_id") + partial = request.POST.get("partial", False) + provider = request.POST.get("provider", None) user = User.objects.get(pk=user_id) try: refunded = user.profile.refund_premium(partial=partial, provider=provider) @@ -760,179 +883,185 @@ def refund_premium(request): except PayPalAPIResponseError as e: refunded = e - return {'code': 1 if type(refunded) == int else -1, 'refunded': refunded} + return {"code": 1 if type(refunded) == int else -1, "refunded": refunded} + @staff_member_required @ajax_login_required @json.json_view def upgrade_premium(request): - user_id = request.POST.get('user_id') + user_id = request.POST.get("user_id") user = User.objects.get(pk=user_id) - - gift = MGiftCode.add(gifting_user_id=User.objects.get(username='samuel').pk, - receiving_user_id=user.pk) + + gift = MGiftCode.add(gifting_user_id=User.objects.get(username="samuel").pk, receiving_user_id=user.pk) MRedeemedCode.redeem(user, gift.gift_code) - - return {'code': user.profile.is_premium} + + return {"code": user.profile.is_premium} + @staff_member_required @ajax_login_required @json.json_view def never_expire_premium(request): - user_id = request.POST.get('user_id') - years = int(request.POST.get('years', 0)) + user_id = request.POST.get("user_id") + years = int(request.POST.get("years", 0)) user = User.objects.get(pk=user_id) if user.profile.is_premium: if years: - user.profile.premium_expire = datetime.datetime.now() + datetime.timedelta(days=365*years) + user.profile.premium_expire = datetime.datetime.now() + datetime.timedelta(days=365 * years) else: user.profile.premium_expire = None user.profile.save() - return {'code': 1} - - return {'code': -1} + return {"code": 1} + + return {"code": -1} + @staff_member_required @ajax_login_required @json.json_view def update_payment_history(request): - user_id = request.POST.get('user_id') + user_id = request.POST.get("user_id") user = User.objects.get(pk=user_id) user.profile.setup_premium_history(set_premium_expire=False) - - return {'code': 1} - + + return {"code": 1} + + @login_required -@render_to('profile/delete_account.xhtml') +@render_to("profile/delete_account.xhtml") def delete_account(request): - if request.method == 'POST': + if request.method == "POST": form = DeleteAccountForm(request.POST, user=request.user) if form.is_valid(): - logging.user(request.user, "~SK~BC~FRDeleting ~SB%s~SN's account." % - request.user.username) + logging.user(request.user, "~SK~BC~FRDeleting ~SB%s~SN's account." % request.user.username) request.user.profile.delete_user(confirm=True) logout_user(request) - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse("index")) else: - logging.user(request.user, "~BC~FRFailed attempt to delete ~SB%s~SN's account." % - request.user.username) + logging.user( + request.user, "~BC~FRFailed attempt to delete ~SB%s~SN's account." % request.user.username + ) else: - logging.user(request.user, "~BC~FRAttempting to delete ~SB%s~SN's account." % - request.user.username) + logging.user(request.user, "~BC~FRAttempting to delete ~SB%s~SN's account." % request.user.username) form = DeleteAccountForm(user=request.user) return { - 'delete_form': form, + "delete_form": form, } - -@render_to('profile/forgot_password.xhtml') + +@render_to("profile/forgot_password.xhtml") def forgot_password(request): - if request.method == 'POST': + if request.method == "POST": form = ForgotPasswordForm(request.POST) if form.is_valid(): - logging.user(request.user, "~BC~FRForgot password: ~SB%s" % request.POST['email']) + logging.user(request.user, "~BC~FRForgot password: ~SB%s" % request.POST["email"]) try: - user = User.objects.get(email__iexact=request.POST['email']) + user = User.objects.get(email__iexact=request.POST["email"]) except User.MultipleObjectsReturned: - user = User.objects.filter(email__iexact=request.POST['email'])[0] + user = User.objects.filter(email__iexact=request.POST["email"])[0] user.profile.send_forgot_password_email() - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse("index")) else: - logging.user(request.user, "~BC~FRFailed forgot password: ~SB%s~SN" % - request.POST['email']) + logging.user(request.user, "~BC~FRFailed forgot password: ~SB%s~SN" % request.POST.get("email")) else: logging.user(request.user, "~BC~FRAttempting to retrieve forgotton password.") form = ForgotPasswordForm() return { - 'forgot_password_form': form, + "forgot_password_form": form, } - + + @login_required -@render_to('profile/forgot_password_return.xhtml') +@render_to("profile/forgot_password_return.xhtml") def forgot_password_return(request): - if request.method == 'POST': - logging.user(request.user, "~BC~FRReseting ~SB%s~SN's password." % - request.user.username) - new_password = request.POST.get('password', '') + if request.method == "POST": + logging.user(request.user, "~BC~FRReseting ~SB%s~SN's password." % request.user.username) + new_password = request.POST.get("password", "") request.user.set_password(new_password) request.user.save() - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse("index")) else: - logging.user(request.user, "~BC~FRAttempting to reset ~SB%s~SN's password." % - request.user.username) + logging.user(request.user, "~BC~FRAttempting to reset ~SB%s~SN's password." % request.user.username) form = ForgotPasswordReturnForm() return { - 'forgot_password_return_form': form, + "forgot_password_return_form": form, } + @ajax_login_required @json.json_view def delete_starred_stories(request): - timestamp = request.POST.get('timestamp', None) + timestamp = request.POST.get("timestamp", None) if timestamp: delete_date = datetime.datetime.fromtimestamp(int(timestamp)) else: delete_date = datetime.datetime.now() - starred_stories = MStarredStory.objects.filter(user_id=request.user.pk, - starred_date__lte=delete_date) + starred_stories = MStarredStory.objects.filter(user_id=request.user.pk, starred_date__lte=delete_date) stories_deleted = starred_stories.count() starred_stories.delete() MStarredStoryCounts.count_for_user(request.user.pk, total_only=True) starred_counts, starred_count = MStarredStoryCounts.user_counts(request.user.pk, include_total=True) - - logging.user(request.user, "~BC~FRDeleting %s/%s starred stories (%s)" % (stories_deleted, - stories_deleted+starred_count, delete_date)) - return dict(code=1, stories_deleted=stories_deleted, starred_counts=starred_counts, - starred_count=starred_count) + logging.user( + request.user, + "~BC~FRDeleting %s/%s starred stories (%s)" + % (stories_deleted, stories_deleted + starred_count, delete_date), + ) + + return dict( + code=1, stories_deleted=stories_deleted, starred_counts=starred_counts, starred_count=starred_count + ) @ajax_login_required @json.json_view def delete_all_sites(request): - request.user.profile.send_opml_export_email(reason="You have deleted all of your sites, so here's a backup of all of your subscriptions just in case.") - + request.user.profile.send_opml_export_email( + reason="You have deleted all of your sites, so here's a backup of all of your subscriptions just in case." + ) + subs = UserSubscription.objects.filter(user=request.user) sub_count = subs.count() subs.delete() - + usf = UserSubscriptionFolders.objects.get(user=request.user) - usf.folders = '[]' + usf.folders = "[]" usf.save() - + logging.user(request.user, "~BC~FRDeleting %s sites" % sub_count) return dict(code=1) @login_required -@render_to('profile/email_optout.xhtml') +@render_to("profile/email_optout.xhtml") def email_optout(request): user = request.user user.profile.send_emails = False user.profile.save() - + return { "user": user, } + @json.json_view def ios_subscription_status(request): logging.debug(" ---> iOS Subscription Status: %s" % request.body) data = json.decode(request.body) - subject = "iOS Subscription Status: %s" % data.get('notification_type', "[missing]") + subject = "iOS Subscription Status: %s" % data.get("notification_type", "[missing]") message = """%s""" % (request.body) mail_admins(subject, message) - - return { - "code": 1 - } + + return {"code": 1} + def trigger_error(request): logging.user(request.user, "~BR~FW~SBTriggering divison by zero") division_by_zero = 1 / 0 - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse("index")) diff --git a/apps/push/migrations/0001_initial.py b/apps/push/migrations/0001_initial.py index 4792b60bcd..3a7e2d495d 100644 --- a/apps/push/migrations/0001_initial.py +++ b/apps/push/migrations/0001_initial.py @@ -1,29 +1,37 @@ # Generated by Django 2.0 on 2020-06-16 06:52 import datetime -from django.db import migrations, models + import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ - ('rss_feeds', '0001_initial'), + ("rss_feeds", "0001_initial"), ] operations = [ migrations.CreateModel( - name='PushSubscription', + name="PushSubscription", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('hub', models.URLField(db_index=True)), - ('topic', models.URLField(db_index=True)), - ('verified', models.BooleanField(default=False)), - ('verify_token', models.CharField(max_length=60)), - ('lease_expires', models.DateTimeField(default=datetime.datetime.now)), - ('feed', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='push', to='rss_feeds.Feed')), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("hub", models.URLField(db_index=True)), + ("topic", models.URLField(db_index=True)), + ("verified", models.BooleanField(default=False)), + ("verify_token", models.CharField(max_length=60)), + ("lease_expires", models.DateTimeField(default=datetime.datetime.now)), + ( + "feed", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, related_name="push", to="rss_feeds.Feed" + ), + ), ], ), ] diff --git a/apps/push/models.py b/apps/push/models.py index d7b4c1c314..8840a4c1c4 100644 --- a/apps/push/models.py +++ b/apps/push/models.py @@ -1,36 +1,34 @@ # Adapted from djpubsubhubbub. See License: http://git.participatoryculture.org/djpubsubhubbub/tree/LICENSE +import hashlib +import re from datetime import datetime, timedelta + import feedparser import requests -import re - from django.conf import settings from django.db import models from django.urls import reverse -import hashlib from apps.push import signals from apps.rss_feeds.models import Feed from utils import log as logging -from utils.feed_functions import timelimit, TimeoutError +from utils.feed_functions import TimeoutError, timelimit + +DEFAULT_LEASE_SECONDS = 10 * 24 * 60 * 60 # 10 days -DEFAULT_LEASE_SECONDS = (10 * 24 * 60 * 60) # 10 days class PushSubscriptionManager(models.Manager): - @timelimit(5) - def subscribe(self, topic, feed, hub=None, callback=None, - lease_seconds=None, force_retry=False): + def subscribe(self, topic, feed, hub=None, callback=None, lease_seconds=None, force_retry=False): if hub is None: hub = self._get_hub(topic) if hub is None: - raise TypeError('hub cannot be None if the feed does not provide it') + raise TypeError("hub cannot be None if the feed does not provide it") if lease_seconds is None: - lease_seconds = getattr(settings, 'PUBSUBHUBBUB_LEASE_SECONDS', - DEFAULT_LEASE_SECONDS) + lease_seconds = getattr(settings, "PUBSUBHUBBUB_LEASE_SECONDS", DEFAULT_LEASE_SECONDS) feed = Feed.get_by_id(feed.id) subscription, created = self.get_or_create(feed=feed) signals.pre_subscribe.send(sender=subscription, created=created) @@ -41,38 +39,44 @@ def subscribe(self, topic, feed, hub=None, callback=None, subscription.topic = feed.feed_link[:200] subscription.hub = hub subscription.save() - + if callback is None: - callback_path = reverse('push-callback', args=(subscription.pk,)) - callback = 'https://' + settings.PUSH_DOMAIN + callback_path + callback_path = reverse("push-callback", args=(subscription.pk,)) + callback = "https://" + settings.PUSH_DOMAIN + callback_path # callback = "https://push.newsblur.com/push/%s" % subscription.pk # + callback_path try: - response = self._send_request(hub, { - 'hub.mode' : 'subscribe', - 'hub.callback' : callback, - 'hub.topic' : topic, - 'hub.verify' : ['async', 'sync'], - 'hub.verify_token' : subscription.generate_token('subscribe'), - 'hub.lease_seconds' : lease_seconds, - }) + response = self._send_request( + hub, + { + "hub.mode": "subscribe", + "hub.callback": callback, + "hub.topic": topic, + "hub.verify": ["async", "sync"], + "hub.verify_token": subscription.generate_token("subscribe"), + "hub.lease_seconds": lease_seconds, + }, + ) except (requests.ConnectionError, requests.exceptions.MissingSchema): response = None if response and response.status_code == 204: subscription.verified = True - elif response and response.status_code == 202: # async verification + elif response and response.status_code == 202: # async verification subscription.verified = False else: error = response and response.text or "" - if not force_retry and 'You may only subscribe to' in error: + if not force_retry and "You may only subscribe to" in error: extracted_topic = re.search("You may only subscribe to (.*?) ", error) if extracted_topic: - subscription = self.subscribe(extracted_topic.group(1), - feed=feed, hub=hub, force_retry=True) + subscription = self.subscribe( + extracted_topic.group(1), feed=feed, hub=hub, force_retry=True + ) else: - logging.debug(u' ---> [%-30s] ~FR~BKFeed failed to subscribe to push: %s (code: %s)' % ( - subscription.feed.log_title[:30], error[:100], response and response.status_code)) + logging.debug( + " ---> [%-30s] ~FR~BKFeed failed to subscribe to push: %s (code: %s)" + % (subscription.feed.log_title[:30], error[:100], response and response.status_code) + ) subscription.save() feed.setup_push() @@ -80,18 +84,18 @@ def subscribe(self, topic, feed, hub=None, callback=None, signals.verified.send(sender=subscription) return subscription - def _get_hub(self, topic): parsed = feedparser.parse(topic) for link in parsed.feed.links: - if link['rel'] == 'hub': - return link['href'] + if link["rel"] == "hub": + return link["href"] def _send_request(self, url, data): return requests.post(url, data=data) + class PushSubscription(models.Model): - feed = models.OneToOneField(Feed, db_index=True, related_name='push', on_delete=models.CASCADE) + feed = models.OneToOneField(Feed, db_index=True, related_name="push", on_delete=models.CASCADE) hub = models.URLField(db_index=True) topic = models.URLField(db_index=True) verified = models.BooleanField(default=False) @@ -104,43 +108,45 @@ class PushSubscription(models.Model): # unique_together = [ # ('hub', 'topic') # ] - + def unsubscribe(self): feed = self.feed self.delete() feed.setup_push() - + def set_expiration(self, lease_seconds): - self.lease_expires = datetime.now() + timedelta( - seconds=lease_seconds) + self.lease_expires = datetime.now() + timedelta(seconds=lease_seconds) self.save() def generate_token(self, mode): - assert self.pk is not None, \ - 'Subscription must be saved before generating token' - token = mode[:20] + hashlib.sha1(('%s%i%s' % ( - settings.SECRET_KEY, self.pk, mode)).encode(encoding='utf-8')).hexdigest() + assert self.pk is not None, "Subscription must be saved before generating token" + token = ( + mode[:20] + + hashlib.sha1( + ("%s%i%s" % (settings.SECRET_KEY, self.pk, mode)).encode(encoding="utf-8") + ).hexdigest() + ) self.verify_token = token self.save() return token - + def check_urls_against_pushed_data(self, parsed): - if hasattr(parsed.feed, 'links'): # single notification + if hasattr(parsed.feed, "links"): # single notification hub_url = self.hub self_url = self.topic for link in parsed.feed.links: - href = link.get('href', '') - if any(w in href for w in ['wp-admin', 'wp-cron']): + href = link.get("href", "") + if any(w in href for w in ["wp-admin", "wp-cron"]): continue - - if link['rel'] == 'hub': - hub_url = link['href'] - elif link['rel'] == 'self': - self_url = link['href'] - - if hub_url and hub_url.startswith('//'): + + if link["rel"] == "hub": + hub_url = link["href"] + elif link["rel"] == "self": + self_url = link["href"] + + if hub_url and hub_url.startswith("//"): hub_url = "http:%s" % hub_url - + needs_update = False if hub_url and self.hub != hub_url: # hub URL has changed; let's update our subscription @@ -150,23 +156,24 @@ def check_urls_against_pushed_data(self, parsed): needs_update = True if needs_update: - logging.debug(u' ---> [%-30s] ~FR~BKUpdating PuSH hub/topic: %s / %s' % ( - self.feed, hub_url, self_url)) + logging.debug( + " ---> [%-30s] ~FR~BKUpdating PuSH hub/topic: %s / %s" % (self.feed, hub_url, self_url) + ) expiration_time = self.lease_expires - datetime.now() - seconds = expiration_time.days*86400 + expiration_time.seconds + seconds = expiration_time.days * 86400 + expiration_time.seconds try: PushSubscription.objects.subscribe( - self_url, feed=self.feed, hub=hub_url, - lease_seconds=seconds) + self_url, feed=self.feed, hub=hub_url, lease_seconds=seconds + ) except TimeoutError: - logging.debug(u' ---> [%-30s] ~FR~BKTimed out updating PuSH hub/topic: %s / %s' % ( - self.feed, hub_url, self_url)) - - + logging.debug( + " ---> [%-30s] ~FR~BKTimed out updating PuSH hub/topic: %s / %s" + % (self.feed, hub_url, self_url) + ) + def __str__(self): if self.verified: - verified = u'verified' + verified = "verified" else: - verified = u'unverified' - return u'to %s on %s: %s' % ( - self.topic, self.hub, verified) + verified = "unverified" + return "to %s on %s: %s" % (self.topic, self.hub, verified) diff --git a/apps/push/signals.py b/apps/push/signals.py index 2f2aa7d3d0..8c915dc808 100644 --- a/apps/push/signals.py +++ b/apps/push/signals.py @@ -2,6 +2,6 @@ from django.dispatch import Signal -pre_subscribe = Signal(providing_args=['created']) +pre_subscribe = Signal(providing_args=["created"]) verified = Signal() -updated = Signal(providing_args=['update']) +updated = Signal(providing_args=["update"]) diff --git a/apps/push/test_push.py b/apps/push/test_push.py index 8aac0d8828..c1cb032864 100644 --- a/apps/push/test_push.py +++ b/apps/push/test_push.py @@ -1,17 +1,17 @@ # Copyright 2009 - Participatory Culture Foundation -# +# # This file is part of djpubsubhubbub. -# +# # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: -# +# # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. -# +# # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. @@ -23,14 +23,15 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from datetime import datetime, timedelta import urllib +from datetime import datetime, timedelta -from django.urls import reverse from django.test import TestCase +from django.urls import reverse from apps.push.models import PushSubscription, PushSubscriptionManager -from apps.push.signals import pre_subscribe, verified, updated +from apps.push.signals import pre_subscribe, updated, verified + class MockResponse(object): def __init__(self, status, data=None): @@ -42,13 +43,13 @@ def info(self): def read(self): if self.data is None: - return '' + return "" data, self.data = self.data, None return data -class PSHBTestBase: - urls = 'apps.push.urls' +class PSHBTestBase: + urls = "apps.push.urls" def setUp(self): self._old_send_request = PushSubscriptionManager._send_request @@ -57,8 +58,10 @@ def setUp(self): self.requests = [] self.signals = [] for connecter in pre_subscribe, verified, updated: + def callback(signal=None, **kwargs): self.signals.append((signal, kwargs)) + connecter.connect(callback, dispatch_uid=connecter, weak=False) def tearDown(self): @@ -71,34 +74,32 @@ def _send_request(self, url, data): self.requests.append((url, data)) return self.responses.pop() -class Test_PSHBSubscriptionManagerTest(PSHBTestBase, TestCase): +class Test_PSHBSubscriptionManagerTest(PSHBTestBase, TestCase): def test_sync_verify(self): """ If the hub returns a 204 response, the subscription is verified and active. """ self.responses.append(MockResponse(204)) - sub = PushSubscription.objects.subscribe('topic', 'hub', 'callback', 2000) + sub = PushSubscription.objects.subscribe("topic", "hub", "callback", 2000) self.assertEquals(len(self.signals), 2) - self.assertEquals(self.signals[0], (pre_subscribe, {'sender': sub, - 'created': True})) - self.assertEquals(self.signals[1], (verified, {'sender': sub})) - self.assertEquals(sub.hub, 'hub') - self.assertEquals(sub.topic, 'topic') + self.assertEquals(self.signals[0], (pre_subscribe, {"sender": sub, "created": True})) + self.assertEquals(self.signals[1], (verified, {"sender": sub})) + self.assertEquals(sub.hub, "hub") + self.assertEquals(sub.topic, "topic") self.assertEquals(sub.verified, True) rough_expires = datetime.now() + timedelta(seconds=2000) - self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, - 'lease more than 5 seconds off') + self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, "lease more than 5 seconds off") self.assertEquals(len(self.requests), 1) request = self.requests[0] - self.assertEquals(request[0], 'hub') - self.assertEquals(request[1]['mode'], 'subscribe') - self.assertEquals(request[1]['topic'], 'topic') - self.assertEquals(request[1]['callback'], 'callback') - self.assertEquals(request[1]['verify'], ('async', 'sync')) - self.assertEquals(request[1]['verify_token'], sub.verify_token) - self.assertEquals(request[1]['lease_seconds'], 2000) + self.assertEquals(request[0], "hub") + self.assertEquals(request[1]["mode"], "subscribe") + self.assertEquals(request[1]["topic"], "topic") + self.assertEquals(request[1]["callback"], "callback") + self.assertEquals(request[1]["verify"], ("async", "sync")) + self.assertEquals(request[1]["verify_token"], sub.verify_token) + self.assertEquals(request[1]["lease_seconds"], 2000) def test_async_verify(self): """ @@ -106,25 +107,23 @@ def test_async_verify(self): subscription is verified. """ self.responses.append(MockResponse(202)) - sub = PushSubscription.objects.subscribe('topic', 'hub', 'callback', 2000) + sub = PushSubscription.objects.subscribe("topic", "hub", "callback", 2000) self.assertEquals(len(self.signals), 1) - self.assertEquals(self.signals[0], (pre_subscribe, {'sender': sub, - 'created': True})) - self.assertEquals(sub.hub, 'hub') - self.assertEquals(sub.topic, 'topic') + self.assertEquals(self.signals[0], (pre_subscribe, {"sender": sub, "created": True})) + self.assertEquals(sub.hub, "hub") + self.assertEquals(sub.topic, "topic") self.assertEquals(sub.verified, False) rough_expires = datetime.now() + timedelta(seconds=2000) - self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, - 'lease more than 5 seconds off') + self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, "lease more than 5 seconds off") self.assertEquals(len(self.requests), 1) request = self.requests[0] - self.assertEquals(request[0], 'hub') - self.assertEquals(request[1]['mode'], 'subscribe') - self.assertEquals(request[1]['topic'], 'topic') - self.assertEquals(request[1]['callback'], 'callback') - self.assertEquals(request[1]['verify'], ('async', 'sync')) - self.assertEquals(request[1]['verify_token'], sub.verify_token) - self.assertEquals(request[1]['lease_seconds'], 2000) + self.assertEquals(request[0], "hub") + self.assertEquals(request[1]["mode"], "subscribe") + self.assertEquals(request[1]["topic"], "topic") + self.assertEquals(request[1]["callback"], "callback") + self.assertEquals(request[1]["verify"], ("async", "sync")) + self.assertEquals(request[1]["verify_token"], sub.verify_token) + self.assertEquals(request[1]["lease_seconds"], 2000) def test_least_seconds_default(self): """ @@ -132,53 +131,51 @@ def test_least_seconds_default(self): should default to 2592000 (30 days). """ self.responses.append(MockResponse(202)) - sub = PushSubscription.objects.subscribe('topic', 'hub', 'callback') + sub = PushSubscription.objects.subscribe("topic", "hub", "callback") rough_expires = datetime.now() + timedelta(seconds=2592000) - self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, - 'lease more than 5 seconds off') + self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, "lease more than 5 seconds off") self.assertEquals(len(self.requests), 1) request = self.requests[0] - self.assertEquals(request[1]['lease_seconds'], 2592000) + self.assertEquals(request[1]["lease_seconds"], 2592000) def test_error_on_subscribe_raises_URLError(self): """ If a non-202/204 status is returned, raise a URLError. """ - self.responses.append(MockResponse(500, 'error data')) + self.responses.append(MockResponse(500, "error data")) try: - PushSubscription.objects.subscribe('topic', 'hub', 'callback') + PushSubscription.objects.subscribe("topic", "hub", "callback") except urllib.error.URLError as e: - self.assertEquals(e.reason, - 'error subscribing to topic on hub:\nerror data') + self.assertEquals(e.reason, "error subscribing to topic on hub:\nerror data") else: - self.fail('subscription did not raise URLError exception') + self.fail("subscription did not raise URLError exception") -class Test_PSHBCallbackViewCase(PSHBTestBase, TestCase): +class Test_PSHBCallbackViewCase(PSHBTestBase, TestCase): def test_verify(self): """ Getting the callback from the server should verify the subscription. """ - sub = PushSubscription.objects.create( - topic='topic', - hub='hub', - verified=False) - verify_token = sub.generate_token('subscribe') - - response = self.client.get(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - {'hub.mode': 'subscribe', - 'hub.topic': sub.topic, - 'hub.challenge': 'challenge', - 'hub.lease_seconds': 2000, - 'hub.verify_token': verify_token}) + sub = PushSubscription.objects.create(topic="topic", hub="hub", verified=False) + verify_token = sub.generate_token("subscribe") + + response = self.client.get( + reverse("pubsubhubbub_callback", args=(sub.pk,)), + { + "hub.mode": "subscribe", + "hub.topic": sub.topic, + "hub.challenge": "challenge", + "hub.lease_seconds": 2000, + "hub.verify_token": verify_token, + }, + ) self.assertEquals(response.status_code, 200) - self.assertEquals(response.content, 'challenge') + self.assertEquals(response.content, "challenge") sub = PushSubscription.objects.get(pk=sub.pk) self.assertEquals(sub.verified, True) self.assertEquals(len(self.signals), 1) - self.assertEquals(self.signals[0], (verified, {'sender': sub})) + self.assertEquals(self.signals[0], (verified, {"sender": sub})) def test_404(self): """ @@ -189,54 +186,63 @@ def test_404(self): * subscription doesn't exist * token doesn't match the subscription """ - sub = PushSubscription.objects.create( - topic='topic', - hub='hub', - verified=False) - verify_token = sub.generate_token('subscribe') - - response = self.client.get(reverse('pubsubhubbub_callback', - args=(0,)), - {'hub.mode': 'subscribe', - 'hub.topic': sub.topic, - 'hub.challenge': 'challenge', - 'hub.lease_seconds': 2000, - 'hub.verify_token': verify_token[1:]}) + sub = PushSubscription.objects.create(topic="topic", hub="hub", verified=False) + verify_token = sub.generate_token("subscribe") + + response = self.client.get( + reverse("pubsubhubbub_callback", args=(0,)), + { + "hub.mode": "subscribe", + "hub.topic": sub.topic, + "hub.challenge": "challenge", + "hub.lease_seconds": 2000, + "hub.verify_token": verify_token[1:], + }, + ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) - response = self.client.get(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - {'hub.mode': 'subscribe', - 'hub.topic': sub.topic, - 'hub.challenge': 'challenge', - 'hub.lease_seconds': 2000, - 'hub.verify_token': verify_token[1:]}) + response = self.client.get( + reverse("pubsubhubbub_callback", args=(sub.pk,)), + { + "hub.mode": "subscribe", + "hub.topic": sub.topic, + "hub.challenge": "challenge", + "hub.lease_seconds": 2000, + "hub.verify_token": verify_token[1:], + }, + ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) - response = self.client.get(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - {'hub.mode': 'subscribe', - 'hub.topic': sub.topic + 'extra', - 'hub.challenge': 'challenge', - 'hub.lease_seconds': 2000, - 'hub.verify_token': verify_token}) + response = self.client.get( + reverse("pubsubhubbub_callback", args=(sub.pk,)), + { + "hub.mode": "subscribe", + "hub.topic": sub.topic + "extra", + "hub.challenge": "challenge", + "hub.lease_seconds": 2000, + "hub.verify_token": verify_token, + }, + ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) - response = self.client.get(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - {'hub.mode': 'subscribe', - 'hub.topic': sub.topic, - 'hub.challenge': 'challenge', - 'hub.lease_seconds': 2000, - 'hub.verify_token': verify_token[:-5]}) + response = self.client.get( + reverse("pubsubhubbub_callback", args=(sub.pk,)), + { + "hub.mode": "subscribe", + "hub.topic": sub.topic, + "hub.challenge": "challenge", + "hub.lease_seconds": 2000, + "hub.verify_token": verify_token[:-5], + }, + ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) -class Test_PSHBUpdateCase(PSHBTestBase, TestCase): +class Test_PSHBUpdateCase(PSHBTestBase, TestCase): def test_update(self): # this data comes from # http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.1.html#anchor3 @@ -293,32 +299,27 @@ def test_update(self): """ sub = PushSubscription.objects.create( - hub="http://myhub.example.com/endpoint", - topic="http://publisher.example.com/happycats.xml") + hub="http://myhub.example.com/endpoint", topic="http://publisher.example.com/happycats.xml" + ) callback_data = [] updated.connect( - lambda sender=None, update=None, **kwargs: callback_data.append( - (sender, update)), - weak=False) + lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False + ) - response = self.client.post(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - update_data, 'application/atom+xml') + response = self.client.post( + reverse("pubsubhubbub_callback", args=(sub.pk,)), update_data, "application/atom+xml" + ) self.assertEquals(response.status_code, 200) self.assertEquals(len(callback_data), 1) sender, update = callback_data[0] self.assertEquals(sender, sub) self.assertEquals(len(update.entries), 4) - self.assertEquals(update.entries[0].id, - 'http://publisher.example.com/happycat25.xml') - self.assertEquals(update.entries[1].id, - 'http://publisher.example.com/happycat25.xml') - self.assertEquals(update.entries[2].id, - 'http://publisher.example.com/happycat25.xml') - self.assertEquals(update.entries[3].id, - 'http://publisher.example.com/happycat25.xml') + self.assertEquals(update.entries[0].id, "http://publisher.example.com/happycat25.xml") + self.assertEquals(update.entries[1].id, "http://publisher.example.com/happycat25.xml") + self.assertEquals(update.entries[2].id, "http://publisher.example.com/happycat25.xml") + self.assertEquals(update.entries[3].id, "http://publisher.example.com/happycat25.xml") def test_update_with_changed_hub(self): update_data = """ @@ -343,31 +344,32 @@ def test_update_with_changed_hub(self): sub = PushSubscription.objects.create( hub="hub", topic="http://publisher.example.com/happycats.xml", - lease_expires=datetime.now() + timedelta(days=1)) + lease_expires=datetime.now() + timedelta(days=1), + ) callback_data = [] updated.connect( - lambda sender=None, update=None, **kwargs: callback_data.append( - (sender, update)), - weak=False) + lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False + ) self.responses.append(MockResponse(204)) - response = self.client.post(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - update_data, 'application/atom+xml') + response = self.client.post( + reverse("pubsubhubbub_callback", args=(sub.pk,)), update_data, "application/atom+xml" + ) self.assertEquals(response.status_code, 200) self.assertEquals( PushSubscription.objects.filter( - hub='http://myhub.example.com/endpoint', - topic='http://publisher.example.com/happycats.xml', - verified=True).count(), 1) + hub="http://myhub.example.com/endpoint", + topic="http://publisher.example.com/happycats.xml", + verified=True, + ).count(), + 1, + ) self.assertEquals(len(self.requests), 1) - self.assertEquals(self.requests[0][0], - 'http://myhub.example.com/endpoint') - self.assertEquals(self.requests[0][1]['callback'], - 'http://test.nb.local.com/1/') - self.assert_((self.requests[0][1]['lease_seconds'] - 86400) < 5) + self.assertEquals(self.requests[0][0], "http://myhub.example.com/endpoint") + self.assertEquals(self.requests[0][1]["callback"], "http://test.nb.local.com/1/") + self.assert_((self.requests[0][1]["lease_seconds"] - 86400) < 5) def test_update_with_changed_self(self): update_data = """ @@ -392,30 +394,32 @@ def test_update_with_changed_self(self): sub = PushSubscription.objects.create( hub="http://myhub.example.com/endpoint", topic="topic", - lease_expires=datetime.now() + timedelta(days=1)) + lease_expires=datetime.now() + timedelta(days=1), + ) callback_data = [] updated.connect( - lambda sender=None, update=None, **kwargs: callback_data.append( - (sender, update)), - weak=False) + lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False + ) self.responses.append(MockResponse(204)) - response = self.client.post(reverse('pubsubhubbub_callback', kwargs={'push_id': sub.pk}), - update_data, 'application/atom+xml') + response = self.client.post( + reverse("pubsubhubbub_callback", kwargs={"push_id": sub.pk}), update_data, "application/atom+xml" + ) self.assertEquals(response.status_code, 200) self.assertEquals( PushSubscription.objects.filter( - hub='http://myhub.example.com/endpoint', - topic='http://publisher.example.com/happycats.xml', - verified=True).count(), 1) + hub="http://myhub.example.com/endpoint", + topic="http://publisher.example.com/happycats.xml", + verified=True, + ).count(), + 1, + ) self.assertEquals(len(self.requests), 1) - self.assertEquals(self.requests[0][0], - 'http://myhub.example.com/endpoint') - self.assertEquals(self.requests[0][1]['callback'], - 'http://test.nb.local.com/1/') - self.assert_((self.requests[0][1]['lease_seconds'] - 86400) < 5) + self.assertEquals(self.requests[0][0], "http://myhub.example.com/endpoint") + self.assertEquals(self.requests[0][1]["callback"], "http://test.nb.local.com/1/") + self.assert_((self.requests[0][1]["lease_seconds"] - 86400) < 5) def test_update_with_changed_hub_and_self(self): update_data = """ @@ -438,30 +442,29 @@ def test_update_with_changed_hub_and_self(self): """ sub = PushSubscription.objects.create( - hub="hub", - topic="topic", - lease_expires=datetime.now() + timedelta(days=1)) + hub="hub", topic="topic", lease_expires=datetime.now() + timedelta(days=1) + ) callback_data = [] updated.connect( - lambda sender=None, update=None, **kwargs: callback_data.append( - (sender, update)), - weak=False) + lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False + ) self.responses.append(MockResponse(204)) - response = self.client.post(reverse('pubsubhubbub_callback', - args=(sub.pk,)), - update_data, 'application/atom+xml') + response = self.client.post( + reverse("pubsubhubbub_callback", args=(sub.pk,)), update_data, "application/atom+xml" + ) self.assertEquals(response.status_code, 200) self.assertEquals( PushSubscription.objects.filter( - hub='http://myhub.example.com/endpoint', - topic='http://publisher.example.com/happycats.xml', - verified=True).count(), 1) + hub="http://myhub.example.com/endpoint", + topic="http://publisher.example.com/happycats.xml", + verified=True, + ).count(), + 1, + ) self.assertEquals(len(self.requests), 1) - self.assertEquals(self.requests[0][0], - 'http://myhub.example.com/endpoint') - self.assertEquals(self.requests[0][1]['callback'], - 'http://test.nb.local.com/1/') - self.assert_((self.requests[0][1]['lease_seconds'] - 86400) < 5) + self.assertEquals(self.requests[0][0], "http://myhub.example.com/endpoint") + self.assertEquals(self.requests[0][1]["callback"], "http://test.nb.local.com/1/") + self.assert_((self.requests[0][1]["lease_seconds"] - 86400) < 5) diff --git a/apps/push/urls.py b/apps/push/urls.py index 223e40b290..592014001b 100644 --- a/apps/push/urls.py +++ b/apps/push/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import * + from apps.push import views urlpatterns = [ - url(r'^(?P\d+)/?$', views.push_callback, name='push-callback'), + url(r"^(?P\d+)/?$", views.push_callback, name="push-callback"), ] diff --git a/apps/push/views.py b/apps/push/views.py index 78460008d8..1d670ef665 100644 --- a/apps/push/views.py +++ b/apps/push/views.py @@ -1,10 +1,10 @@ # Adapted from djpubsubhubbub. See License: http://git.participatoryculture.org/djpubsubhubbub/tree/LICENSE -import feedparser -import random import datetime +import random -from django.http import HttpResponse, Http404 +import feedparser +from django.http import Http404, HttpResponse from django.http.request import UnreadablePostError from django.shortcuts import get_object_or_404 @@ -13,43 +13,49 @@ from apps.rss_feeds.models import MFetchHistory from utils import log as logging + def push_callback(request, push_id): - if request.method == 'GET': - mode = request.GET['hub.mode'] - topic = request.GET['hub.topic'] - challenge = request.GET.get('hub.challenge', '') - lease_seconds = request.GET.get('hub.lease_seconds') - verify_token = request.GET.get('hub.verify_token', '') + if request.method == "GET": + mode = request.GET["hub.mode"] + topic = request.GET["hub.topic"] + challenge = request.GET.get("hub.challenge", "") + lease_seconds = request.GET.get("hub.lease_seconds") + verify_token = request.GET.get("hub.verify_token", "") - if mode == 'subscribe': - if not verify_token.startswith('subscribe'): + if mode == "subscribe": + if not verify_token.startswith("subscribe"): raise Http404 - subscription = get_object_or_404(PushSubscription, - pk=push_id, - topic=topic, - verify_token=verify_token) + subscription = get_object_or_404( + PushSubscription, pk=push_id, topic=topic, verify_token=verify_token + ) subscription.verified = True subscription.set_expiration(int(lease_seconds)) subscription.save() subscription.feed.setup_push() - logging.debug(' ---> [%-30s] [%s] ~BBVerified PuSH' % (subscription.feed, subscription.feed_id)) + logging.debug(" ---> [%-30s] [%s] ~BBVerified PuSH" % (subscription.feed, subscription.feed_id)) verified.send(sender=subscription) - return HttpResponse(challenge, content_type='text/plain') - elif request.method == 'POST': + return HttpResponse(challenge, content_type="text/plain") + elif request.method == "POST": subscription = get_object_or_404(PushSubscription, pk=push_id) fetch_history = MFetchHistory.feed(subscription.feed_id) latest_push_date_delta = None - if fetch_history and fetch_history.get('push_history'): - latest_push = fetch_history['push_history'][0]['push_date'] - latest_push_date = datetime.datetime.strptime(latest_push, '%Y-%m-%d %H:%M:%S') + if fetch_history and fetch_history.get("push_history"): + latest_push = fetch_history["push_history"][0]["push_date"] + latest_push_date = datetime.datetime.strptime(latest_push, "%Y-%m-%d %H:%M:%S") latest_push_date_delta = datetime.datetime.now() - latest_push_date if latest_push_date > datetime.datetime.now() - datetime.timedelta(minutes=1): - logging.debug(' ---> [%-30s] ~SN~FBSkipping feed fetch, pushed %s seconds ago' % (subscription.feed, latest_push_date_delta.seconds)) - return HttpResponse('Slow down, you just pushed %s seconds ago...' % latest_push_date_delta.seconds, status=429) - + logging.debug( + " ---> [%-30s] ~SN~FBSkipping feed fetch, pushed %s seconds ago" + % (subscription.feed, latest_push_date_delta.seconds) + ) + return HttpResponse( + "Slow down, you just pushed %s seconds ago..." % latest_push_date_delta.seconds, + status=429, + ) + # XXX TODO: Optimize this by removing feedparser. It just needs to find out # the hub_url or topic has changed. ElementTree could do it. if random.random() < 0.1: @@ -63,10 +69,12 @@ def push_callback(request, push_id): # subscription.feed.queue_pushed_feed_xml(request.body) if subscription.feed.active_subscribers >= 1: subscription.feed.queue_pushed_feed_xml("Fetch me", latest_push_date_delta=latest_push_date_delta) - MFetchHistory.add(feed_id=subscription.feed_id, - fetch_type='push') + MFetchHistory.add(feed_id=subscription.feed_id, fetch_type="push") else: - logging.debug(' ---> [%-30s] ~FBSkipping feed fetch, no actives: %s' % (subscription.feed, subscription.feed)) - - return HttpResponse('OK') + logging.debug( + " ---> [%-30s] ~FBSkipping feed fetch, no actives: %s" + % (subscription.feed, subscription.feed) + ) + + return HttpResponse("OK") return Http404 diff --git a/apps/reader/admin.py b/apps/reader/admin.py index 03daf35fc6..2d1f2f2a5f 100644 --- a/apps/reader/admin.py +++ b/apps/reader/admin.py @@ -1,6 +1,7 @@ -from apps.reader.models import UserSubscription, UserSubscriptionFolders, Feature from django.contrib import admin +from apps.reader.models import Feature, UserSubscription, UserSubscriptionFolders + admin.site.register(UserSubscription) admin.site.register(UserSubscriptionFolders) -admin.site.register(Feature) \ No newline at end of file +admin.site.register(Feature) diff --git a/apps/reader/factories.py b/apps/reader/factories.py index ba7f22c221..c4f8327ada 100644 --- a/apps/reader/factories.py +++ b/apps/reader/factories.py @@ -1,13 +1,15 @@ import factory -from factory.fuzzy import FuzzyAttribute from factory.django import DjangoModelFactory +from factory.fuzzy import FuzzyAttribute from faker import Faker -from apps.rss_feeds.factories import FeedFactory -from apps.reader.models import Feature, UserSubscription, UserSubscriptionFolders + from apps.profile.factories import UserFactory +from apps.reader.models import Feature, UserSubscription, UserSubscriptionFolders +from apps.rss_feeds.factories import FeedFactory fake = Faker() + def generate_folder(): string = '{"' string += " ".join(fake.words(2)) @@ -18,12 +20,13 @@ def generate_folder(): string += "]}," return string + def generate_folders(): """ "folders": "[5299728, 644144, 1187026, {\"Brainiacs & Opinion\": [569, 38, 3581, 183139, 1186180, 15]}, {\"Science & Technology\": [731503, 140145, 1272495, 76, 161, 39, {\"Hacker\": [5985150, 3323431]}]}, {\"Humor\": [212379, 3530, 5994357]}, {\"Videos\": [3240, 5168]}]" """ string = '"folders":[' - + for _ in range(3): string += f"{fake.pyint()}, " for _ in range(3): @@ -32,6 +35,7 @@ def generate_folders(): string = string[:-1] + "]" return string + class UserSubscriptionFoldersFactory(DjangoModelFactory): user = factory.SubFactory(UserFactory) folders = FuzzyAttribute(generate_folders) @@ -39,18 +43,19 @@ class UserSubscriptionFoldersFactory(DjangoModelFactory): class Meta: model = UserSubscriptionFolders - + class UserSubscriptionFactory(DjangoModelFactory): user = factory.SubFactory(UserFactory) feed = FuzzyAttribute(FeedFactory) - last_read_date = factory.Faker('date_time') + last_read_date = factory.Faker("date_time") class Meta: model = UserSubscription class FeatureFactory(DjangoModelFactory): - description = factory.Faker('text') - date = factory.Faker('date_time') + description = factory.Faker("text") + date = factory.Faker("date_time") + class Meta: model = Feature diff --git a/apps/reader/forms.py b/apps/reader/forms.py index c0fe51d48a..e7f563c9d1 100644 --- a/apps/reader/forms.py +++ b/apps/reader/forms.py @@ -1,27 +1,39 @@ import datetime + from django import forms -from django.utils.translation import gettext_lazy as _ -from django.contrib.auth.models import User +from django.conf import settings from django.contrib.auth import authenticate +from django.contrib.auth.models import User from django.db.models import Q -from django.conf import settings -from apps.reader.models import Feature +from django.utils.translation import gettext_lazy as _ +from dns.resolver import ( + NXDOMAIN, + NoAnswer, + NoNameservers, + NoResolverConfiguration, + query, +) + +from apps.profile.models import RNewUserQueue, blank_authenticate from apps.profile.tasks import EmailNewUser +from apps.reader.models import Feature from apps.social.models import MActivity -from apps.profile.models import blank_authenticate, RNewUserQueue from utils import log as logging -from dns.resolver import query, NXDOMAIN, NoNameservers, NoAnswer -from dns.resolver import NoResolverConfiguration class LoginForm(forms.Form): - username = forms.CharField(label=_("Username or Email"), max_length=30, - widget=forms.TextInput(attrs={'tabindex': 1, 'class': 'NB-input'}), - error_messages={'required': 'Please enter a username.'}) - password = forms.CharField(label=_("Password"), - widget=forms.PasswordInput(attrs={'tabindex': 2, 'class': 'NB-input'}), - required=False) - # error_messages={'required': 'Please enter a password.'}) + username = forms.CharField( + label=_("Username or Email"), + max_length=30, + widget=forms.TextInput(attrs={"tabindex": 1, "class": "NB-input"}), + error_messages={"required": "Please enter a username."}, + ) + password = forms.CharField( + label=_("Password"), + widget=forms.PasswordInput(attrs={"tabindex": 2, "class": "NB-input"}), + required=False, + ) + # error_messages={'required': 'Please enter a password.'}) add = forms.CharField(required=False, widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): @@ -29,10 +41,10 @@ def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) def clean(self): - username = self.cleaned_data.get('username', '').lower() - password = self.cleaned_data.get('password', '') - - if '@' in username: + username = self.cleaned_data.get("username", "").lower() + password = self.cleaned_data.get("password", "") + + if "@" in username: user = User.objects.filter(email=username) if not user: user = User.objects.filter(email__iexact=username) @@ -60,13 +72,15 @@ def clean(self): if blank: email_user.set_password(email_user.username) email_user.save() - self.user_cache = authenticate(username=email_user.username, password=email_user.username) + self.user_cache = authenticate( + username=email_user.username, password=email_user.username + ) if self.user_cache is None: logging.info(" ***> [%s] Bad Login" % username) raise forms.ValidationError(_("Whoopsy-daisy, wrong password. Try again.")) elif username and not user: raise forms.ValidationError(_("That username is not registered. Please try again.")) - + return self.cleaned_data def get_user_id(self): @@ -81,113 +95,135 @@ def get_user(self): class SignupForm(forms.Form): use_required_attribute = False - username = forms.RegexField(regex=r'^\w+$', - max_length=30, - widget=forms.TextInput(attrs={'class': 'NB-input'}), - label=_('Username'), - error_messages={ - 'required': 'Please enter a username.', - 'invalid': "Your username may only contain letters and numbers." - }) - email = forms.EmailField(widget=forms.TextInput(attrs={'maxlength': 75, 'class': 'NB-input'}), - label=_('Email'), - required=True, - error_messages={'required': 'Please enter an email.'}) - password = forms.CharField(widget=forms.PasswordInput(attrs={'class': 'NB-input'}, - render_value=True,), - label=_('Password'), - required=False) - # error_messages={'required': 'Please enter a password.'}) - + username = forms.RegexField( + regex=r"^\w+$", + max_length=30, + widget=forms.TextInput(attrs={"class": "NB-input"}), + label=_("Username"), + error_messages={ + "required": "Please enter a username.", + "invalid": "Your username may only contain letters and numbers.", + }, + ) + email = forms.EmailField( + widget=forms.TextInput(attrs={"maxlength": 75, "class": "NB-input"}), + label=_("Email"), + required=True, + error_messages={"required": "Please enter an email."}, + ) + password = forms.CharField( + widget=forms.PasswordInput( + attrs={"class": "NB-input"}, + render_value=True, + ), + label=_("Password"), + required=False, + ) + # error_messages={'required': 'Please enter a password.'}) + def clean_username(self): - username = self.cleaned_data['username'] + username = self.cleaned_data["username"] return username def clean_password(self): - if not self.cleaned_data['password']: + if not self.cleaned_data["password"]: return "" - return self.cleaned_data['password'] - + return self.cleaned_data["password"] + def clean_email(self): - email = self.cleaned_data.get('email', None) + email = self.cleaned_data.get("email", None) if email: email_exists = User.objects.filter(email__iexact=email).count() if email_exists: - raise forms.ValidationError(_('Someone is already using that email address.')) - if any([banned in email for banned in ['mailwire24', 'mailbox9', 'scintillamail', 'bluemailboxes', 'devmailing']]): - logging.info(" ***> [%s] Spammer signup banned: %s/%s" % (self.cleaned_data.get('username', None), self.cleaned_data.get('password', None), email)) - raise forms.ValidationError('Seriously, fuck off spammer.') + raise forms.ValidationError(_("Someone is already using that email address.")) + if any( + [ + banned in email + for banned in ["mailwire24", "mailbox9", "scintillamail", "bluemailboxes", "devmailing"] + ] + ): + logging.info( + " ***> [%s] Spammer signup banned: %s/%s" + % ( + self.cleaned_data.get("username", None), + self.cleaned_data.get("password", None), + email, + ) + ) + raise forms.ValidationError("Seriously, fuck off spammer.") try: - domain = email.rsplit('@', 1)[-1] - if not query(domain, 'MX'): - raise forms.ValidationError('Sorry, that email is invalid.') + domain = email.rsplit("@", 1)[-1] + if not query(domain, "MX"): + raise forms.ValidationError("Sorry, that email is invalid.") except (NXDOMAIN, NoNameservers, NoAnswer): - raise forms.ValidationError('Sorry, that email is invalid.') + raise forms.ValidationError("Sorry, that email is invalid.") except NoResolverConfiguration as e: logging.info(f" ***> ~FRFailed to check spamminess of domain: ~FY{domain} ~FR{e}") pass - return self.cleaned_data['email'] - + return self.cleaned_data["email"] + def clean(self): - username = self.cleaned_data.get('username', '') - password = self.cleaned_data.get('password', '') - email = self.cleaned_data.get('email', None) - + username = self.cleaned_data.get("username", "") + password = self.cleaned_data.get("password", "") + email = self.cleaned_data.get("email", None) + exists = User.objects.filter(username__iexact=username).count() if exists: user_auth = authenticate(username=username, password=password) if not user_auth: - raise forms.ValidationError(_('Someone is already using that username.')) - + raise forms.ValidationError(_("Someone is already using that username.")) + return self.cleaned_data - + def save(self, profile_callback=None): - username = self.cleaned_data['username'] - password = self.cleaned_data['password'] - email = self.cleaned_data['email'] + username = self.cleaned_data["username"] + password = self.cleaned_data["password"] + email = self.cleaned_data["email"] exists = User.objects.filter(username__iexact=username).count() if exists: user_auth = authenticate(username=username, password=password) if not user_auth: - raise forms.ValidationError(_('Someone is already using that username.')) + raise forms.ValidationError(_("Someone is already using that username.")) else: return user_auth - + if not password: password = username - + new_user = User(username=username) new_user.set_password(password) - if not getattr(settings, 'AUTO_ENABLE_NEW_USERS', True): + if not getattr(settings, "AUTO_ENABLE_NEW_USERS", True): new_user.is_active = False new_user.email = email new_user.last_login = datetime.datetime.now() new_user.save() - new_user = authenticate(username=username, - password=password) + new_user = authenticate(username=username, password=password) new_user = User.objects.get(username=username) MActivity.new_signup(user_id=new_user.pk) - + RNewUserQueue.add_user(new_user.pk) - + if new_user.email: EmailNewUser.delay(user_id=new_user.pk) - - if getattr(settings, 'AUTO_PREMIUM_NEW_USERS', False): + + if getattr(settings, "AUTO_PREMIUM_NEW_USERS", False): new_user.profile.activate_premium() - elif getattr(settings, 'AUTO_ENABLE_NEW_USERS', False): + elif getattr(settings, "AUTO_ENABLE_NEW_USERS", False): new_user.profile.activate_free() - + return new_user + class FeatureForm(forms.Form): use_required_attribute = False description = forms.CharField(required=True) - + def save(self): - feature = Feature(description=self.cleaned_data['description'], - date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1)) + feature = Feature( + description=self.cleaned_data["description"], + date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1), + ) feature.save() return feature diff --git a/apps/reader/http.py b/apps/reader/http.py index 6fee2f04ba..af0edbff0f 100644 --- a/apps/reader/http.py +++ b/apps/reader/http.py @@ -1,8 +1,9 @@ from django.shortcuts import render + def respond(request, template_name, context_dict, **kwargs): """ Use this function rather than render_to_response directly. The idea is to ensure that we're always using RequestContext. It's too easy to forget. """ - return render(request, template_name, context_dict, **kwargs) \ No newline at end of file + return render(request, template_name, context_dict, **kwargs) diff --git a/apps/reader/managers.py b/apps/reader/managers.py index 697fda2876..a5a56083d0 100644 --- a/apps/reader/managers.py +++ b/apps/reader/managers.py @@ -1,35 +1,41 @@ import sys -from django.db import models + from django.contrib.auth.models import User +from django.db import models + from apps.rss_feeds.models import DuplicateFeed from utils import log as logging + class UserSubscriptionManager(models.Manager): def get(self, *args, **kwargs): try: return super(UserSubscriptionManager, self).get(*args, **kwargs) except self.model.DoesNotExist as exception: - if isinstance(kwargs.get('feed'), int): - feed_id = kwargs.get('feed') - elif 'feed' in kwargs: - feed_id = kwargs['feed'].pk - elif 'feed__pk' in kwargs: - feed_id = kwargs['feed__pk'] - elif 'feed_id' in kwargs: - feed_id = kwargs['feed_id'] + if isinstance(kwargs.get("feed"), int): + feed_id = kwargs.get("feed") + elif "feed" in kwargs: + feed_id = kwargs["feed"].pk + elif "feed__pk" in kwargs: + feed_id = kwargs["feed__pk"] + elif "feed_id" in kwargs: + feed_id = kwargs["feed_id"] dupe_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id) if dupe_feed: feed = dupe_feed[0].feed - if 'feed' in kwargs: - kwargs['feed'] = feed - elif 'feed__pk' in kwargs: - kwargs['feed__pk'] = feed.pk - elif 'feed_id' in kwargs: - kwargs['feed_id'] = feed.pk - user = kwargs.get('user') + if "feed" in kwargs: + kwargs["feed"] = feed + elif "feed__pk" in kwargs: + kwargs["feed__pk"] = feed.pk + elif "feed_id" in kwargs: + kwargs["feed_id"] = feed.pk + user = kwargs.get("user") if isinstance(user, int): user = User.objects.get(pk=user) - logging.debug(" ---> [%s] ~BRFound dupe UserSubscription: ~SB%s (%s)" % (user and user.username, feed, feed_id)) + logging.debug( + " ---> [%s] ~BRFound dupe UserSubscription: ~SB%s (%s)" + % (user and user.username, feed, feed_id) + ) return super(UserSubscriptionManager, self).get(*args, **kwargs) else: raise exception diff --git a/apps/reader/migrations/0001_initial.py b/apps/reader/migrations/0001_initial.py index da718c584b..386be088a1 100644 --- a/apps/reader/migrations/0001_initial.py +++ b/apps/reader/migrations/0001_initial.py @@ -1,67 +1,96 @@ # Generated by Django 2.0 on 2020-06-16 06:52 -import apps.reader.models import datetime + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion +import apps.reader.models -class Migration(migrations.Migration): +class Migration(migrations.Migration): initial = True dependencies = [ - ('rss_feeds', '0001_initial'), + ("rss_feeds", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Feature', + name="Feature", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField(default='')), - ('date', models.DateTimeField(default=datetime.datetime.now)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("description", models.TextField(default="")), + ("date", models.DateTimeField(default=datetime.datetime.now)), ], options={ - 'ordering': ['-date'], + "ordering": ["-date"], }, ), migrations.CreateModel( - name='UserSubscription', + name="UserSubscription", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('user_title', models.CharField(blank=True, max_length=255, null=True)), - ('active', models.BooleanField(default=False)), - ('last_read_date', models.DateTimeField(default=apps.reader.models.unread_cutoff_default)), - ('mark_read_date', models.DateTimeField(default=apps.reader.models.unread_cutoff_default)), - ('unread_count_neutral', models.IntegerField(default=0)), - ('unread_count_positive', models.IntegerField(default=0)), - ('unread_count_negative', models.IntegerField(default=0)), - ('unread_count_updated', models.DateTimeField(default=datetime.datetime.now)), - ('oldest_unread_story_date', models.DateTimeField(default=datetime.datetime.now)), - ('needs_unread_recalc', models.BooleanField(default=False)), - ('feed_opens', models.IntegerField(default=0)), - ('is_trained', models.BooleanField(default=False)), - ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscribers', to='rss_feeds.Feed')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("user_title", models.CharField(blank=True, max_length=255, null=True)), + ("active", models.BooleanField(default=False)), + ("last_read_date", models.DateTimeField(default=apps.reader.models.unread_cutoff_default)), + ("mark_read_date", models.DateTimeField(default=apps.reader.models.unread_cutoff_default)), + ("unread_count_neutral", models.IntegerField(default=0)), + ("unread_count_positive", models.IntegerField(default=0)), + ("unread_count_negative", models.IntegerField(default=0)), + ("unread_count_updated", models.DateTimeField(default=datetime.datetime.now)), + ("oldest_unread_story_date", models.DateTimeField(default=datetime.datetime.now)), + ("needs_unread_recalc", models.BooleanField(default=False)), + ("feed_opens", models.IntegerField(default=0)), + ("is_trained", models.BooleanField(default=False)), + ( + "feed", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="subscribers", + to="rss_feeds.Feed", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="subscriptions", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='UserSubscriptionFolders', + name="UserSubscriptionFolders", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('folders', models.TextField(default='[]')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("folders", models.TextField(default="[]")), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), ], options={ - 'verbose_name': 'folder', - 'verbose_name_plural': 'folders', + "verbose_name": "folder", + "verbose_name_plural": "folders", }, ), migrations.AlterUniqueTogether( - name='usersubscription', - unique_together={('user', 'feed')}, + name="usersubscription", + unique_together={("user", "feed")}, ), ] diff --git a/apps/reader/models.py b/apps/reader/models.py index 3ff6f0d381..6c89e888e3 100644 --- a/apps/reader/models.py +++ b/apps/reader/models.py @@ -1,45 +1,55 @@ import datetime -import time import re -import redis -import pymongo -import celery -import mongoengine as mongo +import time from operator import itemgetter from pprint import pprint -from utils import log as logging -from utils import json_functions as json -from django.db import models, IntegrityError -from django.db.models import Q -from django.db.models import Count + +import celery +import mongoengine as mongo +import pymongo +import redis from django.conf import settings from django.contrib.auth.models import User from django.core.cache import cache +from django.db import IntegrityError, models +from django.db.models import Count, Q from django.template.defaultfilters import slugify -from mongoengine.queryset import OperationError -from mongoengine.queryset import NotUniqueError +from mongoengine.queryset import NotUniqueError, OperationError + +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + apply_classifier_authors, + apply_classifier_feeds, + apply_classifier_tags, + apply_classifier_titles, +) +from apps.analyzer.tfidf import tfidf from apps.reader.managers import UserSubscriptionManager -from apps.rss_feeds.models import Feed, MStory, DuplicateFeed +from apps.rss_feeds.models import DuplicateFeed, Feed, MStory from apps.rss_feeds.tasks import NewFeeds -from apps.analyzer.models import MClassifierFeed, MClassifierAuthor, MClassifierTag, MClassifierTitle -from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds, apply_classifier_authors, apply_classifier_tags -from apps.analyzer.tfidf import tfidf +from utils import json_functions as json +from utils import log as logging from utils.feed_functions import add_object_to_folder, chunks + def unread_cutoff_default(): return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD) - + + class UserSubscription(models.Model): """ A feed which a user has subscribed to. Carries all of the cached information about the subscription, including unread counts of the three primary scores. - + Also has a dirty flag (needs_unread_recalc) which means that the unread counts are not accurate and need to be calculated with `self.calculate_feed_scores()`. """ - - user = models.ForeignKey(User, related_name='subscriptions', on_delete=models.CASCADE) - feed = models.ForeignKey(Feed, related_name='subscribers', on_delete=models.CASCADE) + + user = models.ForeignKey(User, related_name="subscriptions", on_delete=models.CASCADE) + feed = models.ForeignKey(Feed, related_name="subscribers", on_delete=models.CASCADE) user_title = models.CharField(max_length=255, null=True, blank=True) active = models.BooleanField(default=False) last_read_date = models.DateTimeField(default=unread_cutoff_default) @@ -52,32 +62,31 @@ class UserSubscription(models.Model): needs_unread_recalc = models.BooleanField(default=False) feed_opens = models.IntegerField(default=0) is_trained = models.BooleanField(default=False) - + objects = UserSubscriptionManager() def __str__(self): - return '[%s (%s): %s (%s)] ' % (self.user.username, self.user.pk, - self.feed.feed_title, self.feed.pk) - + return "[%s (%s): %s (%s)] " % (self.user.username, self.user.pk, self.feed.feed_title, self.feed.pk) + class Meta: unique_together = ("user", "feed") - + def canonical(self, full=False, include_favicon=True, classifiers=None): - feed = self.feed.canonical(full=full, include_favicon=include_favicon) - feed['feed_title'] = self.user_title or feed['feed_title'] - feed['ps'] = self.unread_count_positive - feed['nt'] = self.unread_count_neutral - feed['ng'] = self.unread_count_negative - feed['active'] = self.active - feed['feed_opens'] = self.feed_opens - feed['subscribed'] = True + feed = self.feed.canonical(full=full, include_favicon=include_favicon) + feed["feed_title"] = self.user_title or feed["feed_title"] + feed["ps"] = self.unread_count_positive + feed["nt"] = self.unread_count_neutral + feed["ng"] = self.unread_count_negative + feed["active"] = self.active + feed["feed_opens"] = self.feed_opens + feed["subscribed"] = True if classifiers: - feed['classifiers'] = classifiers + feed["classifiers"] = classifiers return feed - + def save(self, *args, **kwargs): - user_title_max = self._meta.get_field('user_title').max_length + user_title_max = self._meta.get_field("user_title").max_length if self.user_title and len(self.user_title) > user_title_max: self.user_title = self.user_title[:user_title_max] try: @@ -91,37 +100,50 @@ def save(self, *args, **kwargs): super(UserSubscription, self).save(*args, **kwargs) break else: - if self and self.id: self.delete() - + if self and self.id: + self.delete() + @classmethod def subs_for_feeds(cls, user_id, feed_ids=None, read_filter="unread"): usersubs = cls.objects if read_filter == "unread": - usersubs = usersubs.filter(Q(unread_count_neutral__gt=0) | - Q(unread_count_positive__gt=0)) + usersubs = usersubs.filter(Q(unread_count_neutral__gt=0) | Q(unread_count_positive__gt=0)) if not feed_ids: - usersubs = usersubs.filter(user=user_id, - active=True).only('feed', 'mark_read_date', 'is_trained', 'needs_unread_recalc') + usersubs = usersubs.filter(user=user_id, active=True).only( + "feed", "mark_read_date", "is_trained", "needs_unread_recalc" + ) else: - usersubs = usersubs.filter(user=user_id, - active=True, - feed__in=feed_ids).only('feed', 'mark_read_date', 'is_trained', 'needs_unread_recalc') - + usersubs = usersubs.filter(user=user_id, active=True, feed__in=feed_ids).only( + "feed", "mark_read_date", "is_trained", "needs_unread_recalc" + ) + return usersubs - + @classmethod - def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread", order="newest", - include_timestamps=False, group_by_feed=False, cutoff_date=None, - across_all_feeds=True, store_stories_key=None, offset=0, limit=500): + def story_hashes( + cls, + user_id, + feed_ids=None, + usersubs=None, + read_filter="unread", + order="newest", + include_timestamps=False, + group_by_feed=False, + cutoff_date=None, + across_all_feeds=True, + store_stories_key=None, + offset=0, + limit=500, + ): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) pipeline = r.pipeline() user = User.objects.get(pk=user_id) story_hashes = {} if group_by_feed else [] is_archive = user.profile.is_archive - + if not feed_ids and not across_all_feeds: return story_hashes - + if not usersubs: usersubs = cls.subs_for_feeds(user_id, feed_ids=feed_ids, read_filter=read_filter) if not usersubs: @@ -130,12 +152,12 @@ def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread if not feed_ids: return story_hashes - current_time = int(time.time() + 60*60*24) + current_time = int(time.time() + 60 * 60 * 24) if not cutoff_date: cutoff_date = user.profile.unread_cutoff feed_counter = 0 unread_ranked_stories_keys = [] - + read_dates = dict() needs_unread_recalc = dict() manual_unread_pipeline = r.pipeline() @@ -143,9 +165,9 @@ def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread oldest_manual_unread = None # usersub_count = len(usersubs) for us in usersubs: - read_dates[us.feed_id] = int(max(us.mark_read_date, cutoff_date).strftime('%s')) + read_dates[us.feed_id] = int(max(us.mark_read_date, cutoff_date).strftime("%s")) if read_filter == "unread": - needs_unread_recalc[us.feed_id] = us.needs_unread_recalc # or usersub_count == 1 + needs_unread_recalc[us.feed_id] = us.needs_unread_recalc # or usersub_count == 1 user_manual_unread_stories_feed_key = f"uU:{user_id}:{us.feed_id}" manual_unread_pipeline.exists(user_manual_unread_stories_feed_key) user_unread_ranked_stories_key = f"zU:{user_id}:{us.feed_id}" @@ -153,25 +175,27 @@ def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread if read_filter == "unread": results = manual_unread_pipeline.execute() for i, us in enumerate(usersubs): - if results[i*2]: # user_manual_unread_stories_feed_key + if results[i * 2]: # user_manual_unread_stories_feed_key user_manual_unread_stories_feed_key = f"uU:{user_id}:{us.feed_id}" - oldest_manual_unread = r.zrevrange(user_manual_unread_stories_feed_key, -1, -1, withscores=True) + oldest_manual_unread = r.zrevrange( + user_manual_unread_stories_feed_key, -1, -1, withscores=True + ) manual_unread_feed_oldest_date[us.feed_id] = int(oldest_manual_unread[0][1]) - if read_filter == "unread" and not results[i*2+1]: # user_unread_ranked_stories_key + if read_filter == "unread" and not results[i * 2 + 1]: # user_unread_ranked_stories_key needs_unread_recalc[us.feed_id] = True - + for feed_id_group in chunks(feed_ids, 500): pipeline = r.pipeline() for feed_id in feed_id_group: - stories_key = 'F:%s' % feed_id - sorted_stories_key = 'zF:%s' % feed_id - read_stories_key = 'RS:%s:%s' % (user_id, feed_id) - unread_stories_key = 'U:%s:%s' % (user_id, feed_id) - unread_ranked_stories_key = 'zU:%s:%s' % (user_id, feed_id) + stories_key = "F:%s" % feed_id + sorted_stories_key = "zF:%s" % feed_id + read_stories_key = "RS:%s:%s" % (user_id, feed_id) + unread_stories_key = "U:%s:%s" % (user_id, feed_id) + unread_ranked_stories_key = "zU:%s:%s" % (user_id, feed_id) user_manual_unread_stories_feed_key = f"uU:{user_id}:{feed_id}" - + max_score = current_time - if read_filter == 'unread': + if read_filter == "unread": min_score = read_dates[feed_id] # if needs_unread_recalc[feed_id]: # pipeline.sdiffstore(unread_stories_key, stories_key, read_stories_key) @@ -180,53 +204,73 @@ def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread else: min_score = 0 - if order == 'oldest': + if order == "oldest": byscorefunc = pipeline.zrangebyscore else: byscorefunc = pipeline.zrevrangebyscore min_score, max_score = max_score, min_score ranked_stories_key = unread_ranked_stories_key - if read_filter == 'unread': + if read_filter == "unread": if needs_unread_recalc[feed_id]: pipeline.zdiffstore(unread_ranked_stories_key, [sorted_stories_key, read_stories_key]) # pipeline.expire(unread_ranked_stories_key, unread_cutoff_diff.days*24*60*60) - pipeline.expire(unread_ranked_stories_key, 1*60*60) # 1 hours - if order == 'oldest': - pipeline.zremrangebyscore(ranked_stories_key, 0, min_score-1) - pipeline.zremrangebyscore(ranked_stories_key, max_score+1, 2*max_score) + pipeline.expire(unread_ranked_stories_key, 1 * 60 * 60) # 1 hours + if order == "oldest": + pipeline.zremrangebyscore(ranked_stories_key, 0, min_score - 1) + pipeline.zremrangebyscore(ranked_stories_key, max_score + 1, 2 * max_score) else: - pipeline.zremrangebyscore(ranked_stories_key, 0, max_score-1) - pipeline.zremrangebyscore(ranked_stories_key, min_score+1, 2*min_score) + pipeline.zremrangebyscore(ranked_stories_key, 0, max_score - 1) + pipeline.zremrangebyscore(ranked_stories_key, min_score + 1, 2 * min_score) else: ranked_stories_key = sorted_stories_key - + # If archive premium user has manually marked an older story as unread if is_archive and feed_id in manual_unread_feed_oldest_date and read_filter == "unread": - if order == 'oldest': + if order == "oldest": min_score = manual_unread_feed_oldest_date[feed_id] else: max_score = manual_unread_feed_oldest_date[feed_id] - - pipeline.zunionstore(unread_ranked_stories_key, [unread_ranked_stories_key, user_manual_unread_stories_feed_key], aggregate="MAX") - + + pipeline.zunionstore( + unread_ranked_stories_key, + [unread_ranked_stories_key, user_manual_unread_stories_feed_key], + aggregate="MAX", + ) + if settings.DEBUG and False: debug_stories = r.zrevrange(unread_ranked_stories_key, 0, -1, withscores=True) - print((" ---> Story hashes (%s/%s - %s/%s) %s stories: %s" % ( - min_score, datetime.datetime.fromtimestamp(min_score).strftime('%Y-%m-%d %T'), - max_score, datetime.datetime.fromtimestamp(max_score).strftime('%Y-%m-%d %T'), - len(debug_stories), - debug_stories))) + print( + ( + " ---> Story hashes (%s/%s - %s/%s) %s stories: %s" + % ( + min_score, + datetime.datetime.fromtimestamp(min_score).strftime("%Y-%m-%d %T"), + max_score, + datetime.datetime.fromtimestamp(max_score).strftime("%Y-%m-%d %T"), + len(debug_stories), + debug_stories, + ) + ) + ) if not store_stories_key: - byscorefunc(ranked_stories_key, min_score, max_score, withscores=include_timestamps, start=offset, num=limit) + byscorefunc( + ranked_stories_key, + min_score, + max_score, + withscores=include_timestamps, + start=offset, + num=limit, + ) unread_ranked_stories_keys.append(ranked_stories_key) - + results = pipeline.execute() if not store_stories_key: for hashes in results: - if not isinstance(hashes, list): continue + if not isinstance(hashes, list): + continue if group_by_feed: story_hashes[feed_ids[feed_counter]] = hashes feed_counter += 1 @@ -241,10 +285,18 @@ def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread else: pipeline = r.pipeline() for unread_ranked_stories_keys_group in chunks(unread_ranked_stories_keys, chunk_size): - pipeline.zunionstore(f"{store_stories_key}-chunk{chunk_count}", unread_ranked_stories_keys_group, aggregate="MAX") + pipeline.zunionstore( + f"{store_stories_key}-chunk{chunk_count}", + unread_ranked_stories_keys_group, + aggregate="MAX", + ) chunk_count += 1 pipeline.execute() - r.zunionstore(store_stories_key, [f"{store_stories_key}-chunk{i}" for i in range(chunk_count)], aggregate="MAX") + r.zunionstore( + store_stories_key, + [f"{store_stories_key}-chunk{i}" for i in range(chunk_count)], + aggregate="MAX", + ) pipeline = r.pipeline() for i in range(chunk_count): pipeline.delete(f"{store_stories_key}-chunk{i}") @@ -252,39 +304,54 @@ def story_hashes(cls, user_id, feed_ids=None, usersubs=None, read_filter="unread if not store_stories_key: return story_hashes - - def get_stories(self, offset=0, limit=6, order='newest', read_filter='all', cutoff_date=None): + + def get_stories(self, offset=0, limit=6, order="newest", read_filter="all", cutoff_date=None): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - unread_ranked_stories_key = 'zU:%s:%s' % (self.user_id, self.feed_id) + unread_ranked_stories_key = "zU:%s:%s" % (self.user_id, self.feed_id) if offset and r.exists(unread_ranked_stories_key): byscorefunc = r.zrevrange if order == "oldest": byscorefunc = r.zrange - story_hashes = byscorefunc(unread_ranked_stories_key, start=offset, end=offset+limit)[:limit] + story_hashes = byscorefunc(unread_ranked_stories_key, start=offset, end=offset + limit)[:limit] else: - story_hashes = UserSubscription.story_hashes(self.user.pk, feed_ids=[self.feed.pk], - order=order, read_filter=read_filter, - offset=offset, limit=limit, - cutoff_date=cutoff_date) - - story_date_order = "%sstory_date" % ('' if order == 'oldest' else '-') + story_hashes = UserSubscription.story_hashes( + self.user.pk, + feed_ids=[self.feed.pk], + order=order, + read_filter=read_filter, + offset=offset, + limit=limit, + cutoff_date=cutoff_date, + ) + + story_date_order = "%sstory_date" % ("" if order == "oldest" else "-") mstories = MStory.objects(story_hash__in=story_hashes).order_by(story_date_order) stories = Feed.format_stories(mstories) return stories - + @classmethod - def feed_stories(cls, user_id, feed_ids=None, offset=0, limit=6, - order='newest', read_filter='all', usersubs=None, cutoff_date=None, - all_feed_ids=None, cache_prefix=""): + def feed_stories( + cls, + user_id, + feed_ids=None, + offset=0, + limit=6, + order="newest", + read_filter="all", + usersubs=None, + cutoff_date=None, + all_feed_ids=None, + cache_prefix="", + ): rt = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) across_all_feeds = False - - if order == 'oldest': + + if order == "oldest": range_func = rt.zrange else: range_func = rt.zrevrange - + if feed_ids is None: across_all_feeds = True feed_ids = [] @@ -292,17 +359,17 @@ def feed_stories(cls, user_id, feed_ids=None, offset=0, limit=6, all_feed_ids = [f for f in feed_ids] # feeds_string = "" - feeds_string = ','.join(str(f) for f in sorted(all_feed_ids))[:30] - ranked_stories_keys = '%szU:%s:feeds:%s' % (cache_prefix, user_id, feeds_string) - unread_ranked_stories_keys = '%szhU:%s:feeds:%s' % (cache_prefix, user_id, feeds_string) + feeds_string = ",".join(str(f) for f in sorted(all_feed_ids))[:30] + ranked_stories_keys = "%szU:%s:feeds:%s" % (cache_prefix, user_id, feeds_string) + unread_ranked_stories_keys = "%szhU:%s:feeds:%s" % (cache_prefix, user_id, feeds_string) stories_cached = rt.exists(ranked_stories_keys) unreads_cached = True if read_filter == "unread" else rt.exists(unread_ranked_stories_keys) if offset and stories_cached: - story_hashes = range_func(ranked_stories_keys, offset, offset+limit) + story_hashes = range_func(ranked_stories_keys, offset, offset + limit) if read_filter == "unread": unread_story_hashes = story_hashes elif unreads_cached: - unread_story_hashes = range_func(unread_ranked_stories_keys, 0, offset+limit) + unread_story_hashes = range_func(unread_ranked_stories_keys, 0, offset + limit) else: unread_story_hashes = [] return story_hashes, unread_story_hashes @@ -310,47 +377,55 @@ def feed_stories(cls, user_id, feed_ids=None, offset=0, limit=6, rt.delete(ranked_stories_keys) rt.delete(unread_ranked_stories_keys) - cls.story_hashes(user_id, feed_ids=feed_ids, - read_filter=read_filter, order=order, - include_timestamps=False, - usersubs=usersubs, - cutoff_date=cutoff_date, - across_all_feeds=across_all_feeds, - store_stories_key=ranked_stories_keys) + cls.story_hashes( + user_id, + feed_ids=feed_ids, + read_filter=read_filter, + order=order, + include_timestamps=False, + usersubs=usersubs, + cutoff_date=cutoff_date, + across_all_feeds=across_all_feeds, + store_stories_key=ranked_stories_keys, + ) story_hashes = range_func(ranked_stories_keys, offset, limit) if read_filter == "unread": unread_feed_story_hashes = story_hashes rt.zunionstore(unread_ranked_stories_keys, [ranked_stories_keys]) else: - cls.story_hashes(user_id, feed_ids=feed_ids, - read_filter="unread", order=order, - include_timestamps=True, - cutoff_date=cutoff_date, - store_stories_key=unread_ranked_stories_keys) + cls.story_hashes( + user_id, + feed_ids=feed_ids, + read_filter="unread", + order=order, + include_timestamps=True, + cutoff_date=cutoff_date, + store_stories_key=unread_ranked_stories_keys, + ) unread_feed_story_hashes = range_func(unread_ranked_stories_keys, offset, limit) - - rt.expire(ranked_stories_keys, 60*60) - rt.expire(unread_ranked_stories_keys, 60*60) - + + rt.expire(ranked_stories_keys, 60 * 60) + rt.expire(unread_ranked_stories_keys, 60 * 60) + return story_hashes, unread_feed_story_hashes - + def oldest_manual_unread_story_date(self, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - + user_manual_unread_stories_feed_key = f"uU:{self.user_id}:{self.feed_id}" oldest_manual_unread = r.zrevrange(user_manual_unread_stories_feed_key, -1, -1, withscores=True) - + return oldest_manual_unread - + @classmethod def truncate_river(cls, user_id, feed_ids, read_filter, cache_prefix=""): rt = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_TEMP_POOL) - - feeds_string = ','.join(str(f) for f in sorted(feed_ids))[:30] - ranked_stories_keys = '%szU:%s:feeds:%s' % (cache_prefix, user_id, feeds_string) - unread_ranked_stories_keys = '%szhU:%s:feeds:%s' % (cache_prefix, user_id, feeds_string) + + feeds_string = ",".join(str(f) for f in sorted(feed_ids))[:30] + ranked_stories_keys = "%szU:%s:feeds:%s" % (cache_prefix, user_id, feeds_string) + unread_ranked_stories_keys = "%szhU:%s:feeds:%s" % (cache_prefix, user_id, feeds_string) stories_cached = rt.exists(ranked_stories_keys) unreads_cached = rt.exists(unread_ranked_stories_keys) truncated = 0 @@ -359,48 +434,56 @@ def truncate_river(cls, user_id, feed_ids, read_filter, cache_prefix=""): rt.delete(ranked_stories_keys) # else: # logging.debug(" ***> ~FRNo stories cached, can't truncate: %s / %s" % (User.objects.get(pk=user_id), feed_ids)) - + if unreads_cached: truncated += rt.zcard(unread_ranked_stories_keys) rt.delete(unread_ranked_stories_keys) # else: # logging.debug(" ***> ~FRNo unread stories cached, can't truncate: %s / %s" % (User.objects.get(pk=user_id), feed_ids)) - + return truncated - + @classmethod - def add_subscription(cls, user, feed_address, folder=None, bookmarklet=False, auto_active=True, - skip_fetch=False): + def add_subscription( + cls, user, feed_address, folder=None, bookmarklet=False, auto_active=True, skip_fetch=False + ): feed = None us = None - - logging.user(user, "~FRAdding URL: ~SB%s (in %s) %s" % (feed_address, folder, - "~FCAUTO-ADD" if not auto_active else "")) - + + logging.user( + user, + "~FRAdding URL: ~SB%s (in %s) %s" + % (feed_address, folder, "~FCAUTO-ADD" if not auto_active else ""), + ) + feed = Feed.get_feed_from_url(feed_address, user=user) - if not feed: + if not feed: code = -1 if bookmarklet: message = "This site does not have an RSS feed. Nothing is linked to from this page." else: message = "This address does not point to an RSS feed or a website with an RSS feed." else: + allow_skip_resync = False + if user.profile.is_archive and feed.active_premium_subscribers != 0: + # Skip resync if there are already active archive subscribers + allow_skip_resync = True + us, subscription_created = cls.objects.get_or_create( - feed=feed, + feed=feed, user=user, defaults={ - 'needs_unread_recalc': True, - 'active': auto_active, - } + "needs_unread_recalc": True, + "active": auto_active, + }, ) code = 1 message = "" - + if us: user_sub_folders_object, created = UserSubscriptionFolders.objects.get_or_create( - user=user, - defaults={'folders': '[]'} + user=user, defaults={"folders": "[]"} ) if created: user_sub_folders = [] @@ -409,85 +492,88 @@ def add_subscription(cls, user, feed_address, folder=None, bookmarklet=False, au user_sub_folders = add_object_to_folder(feed.pk, folder, user_sub_folders) user_sub_folders_object.folders = json.encode(user_sub_folders) user_sub_folders_object.save() - + if auto_active or user.profile.is_premium: us.active = True us.save() - + if not skip_fetch and feed.last_update < datetime.datetime.utcnow() - datetime.timedelta(days=1): feed = feed.update(verbose=True) - + from apps.social.models import MActivity + MActivity.new_feed_subscription(user_id=user.pk, feed_id=feed.pk, feed_title=feed.title) - - feed.setup_feed_for_premium_subscribers() + + feed.setup_feed_for_premium_subscribers(allow_skip_resync=allow_skip_resync) feed.count_subscribers() - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(user.username, 'reload:feeds') - - + r.publish(user.username, "reload:feeds") + return code, message, us - + @classmethod def feeds_with_updated_counts(cls, user, feed_ids=None, check_fetch_status=False, force=False): feeds = {} silent = not getattr(settings, "TEST_DEBUG", False) - + # Get subscriptions for user - user_subs = cls.objects.select_related('feed').filter(user=user, active=True) - feed_ids = [f for f in feed_ids if f and not any(f.startswith(prefix) for prefix in ['river', 'saved'])] + user_subs = cls.objects.select_related("feed").filter(user=user, active=True) + feed_ids = [ + f for f in feed_ids if f and not any(f.startswith(prefix) for prefix in ["river", "saved"]) + ] if feed_ids: user_subs = user_subs.filter(feed__in=feed_ids) - + for i, sub in enumerate(user_subs): # Count unreads if subscription is stale. - if (force or - sub.needs_unread_recalc or - sub.unread_count_updated < user.profile.unread_cutoff or - sub.oldest_unread_story_date < user.profile.unread_cutoff): + if ( + force + or sub.needs_unread_recalc + or sub.unread_count_updated < user.profile.unread_cutoff + or sub.oldest_unread_story_date < user.profile.unread_cutoff + ): sub = sub.calculate_feed_scores(silent=silent, force=force) - if not sub: continue # TODO: Figure out the correct sub and give it a new feed_id + if not sub: + continue # TODO: Figure out the correct sub and give it a new feed_id feed_id = sub.feed_id feeds[feed_id] = { - 'ps': sub.unread_count_positive, - 'nt': sub.unread_count_neutral, - 'ng': sub.unread_count_negative, - 'id': feed_id, + "ps": sub.unread_count_positive, + "nt": sub.unread_count_neutral, + "ng": sub.unread_count_negative, + "id": feed_id, } if not sub.feed.fetched_once or check_fetch_status: - feeds[feed_id]['fetched_once'] = sub.feed.fetched_once - feeds[feed_id]['not_yet_fetched'] = not sub.feed.fetched_once # Legacy. Dammit. + feeds[feed_id]["fetched_once"] = sub.feed.fetched_once + feeds[feed_id]["not_yet_fetched"] = not sub.feed.fetched_once # Legacy. Dammit. if sub.feed.favicon_fetching: - feeds[feed_id]['favicon_fetching'] = True + feeds[feed_id]["favicon_fetching"] = True if sub.feed.has_feed_exception or sub.feed.has_page_exception: - feeds[feed_id]['has_exception'] = True - feeds[feed_id]['exception_type'] = 'feed' if sub.feed.has_feed_exception else 'page' - feeds[feed_id]['feed_address'] = sub.feed.feed_address - feeds[feed_id]['exception_code'] = sub.feed.exception_code + feeds[feed_id]["has_exception"] = True + feeds[feed_id]["exception_type"] = "feed" if sub.feed.has_feed_exception else "page" + feeds[feed_id]["feed_address"] = sub.feed.feed_address + feeds[feed_id]["exception_code"] = sub.feed.exception_code return feeds - + @classmethod def queue_new_feeds(cls, user, new_feeds=None): if not isinstance(user, User): user = User.objects.get(pk=user) - + if not new_feeds: - new_feeds = cls.objects.filter(user=user, - feed__fetched_once=False, - active=True).values('feed_id') - new_feeds = list(set([f['feed_id'] for f in new_feeds])) - + new_feeds = cls.objects.filter(user=user, feed__fetched_once=False, active=True).values("feed_id") + new_feeds = list(set([f["feed_id"] for f in new_feeds])) + if not new_feeds: return - + logging.user(user, "~BB~FW~SBQueueing NewFeeds: ~FC(%s) %s" % (len(new_feeds), new_feeds)) size = 4 - for t in (new_feeds[pos:pos + size] for pos in range(0, len(new_feeds), size)): + for t in (new_feeds[pos : pos + size] for pos in range(0, len(new_feeds), size)): NewFeeds.apply_async(args=(t,), queue="new_feeds") - + @classmethod def refresh_stale_feeds(cls, user, exclude_new=False): if not isinstance(user, User): @@ -496,18 +582,21 @@ def refresh_stale_feeds(cls, user, exclude_new=False): stale_cutoff = datetime.datetime.now() - datetime.timedelta(days=settings.SUBSCRIBER_EXPIRE) # TODO: Refactor below using last_update from REDIS_FEED_UPDATE_POOL - stale_feeds = UserSubscription.objects.filter(user=user, active=True, feed__last_update__lte=stale_cutoff) + stale_feeds = UserSubscription.objects.filter( + user=user, active=True, feed__last_update__lte=stale_cutoff + ) if exclude_new: stale_feeds = stale_feeds.filter(feed__fetched_once=True) - all_feeds = UserSubscription.objects.filter(user=user, active=True) - - logging.user(user, "~FG~BBRefreshing stale feeds: ~SB%s/%s" % ( - stale_feeds.count(), all_feeds.count())) + all_feeds = UserSubscription.objects.filter(user=user, active=True) + + logging.user( + user, "~FG~BBRefreshing stale feeds: ~SB%s/%s" % (stale_feeds.count(), all_feeds.count()) + ) for sub in stale_feeds: sub.feed.fetched_once = False sub.feed.save() - + if stale_feeds: stale_feeds = list(set([f.feed_id for f in stale_feeds])) cls.queue_new_feeds(user, new_feeds=stale_feeds) @@ -515,10 +604,13 @@ def refresh_stale_feeds(cls, user, exclude_new=False): @classmethod def schedule_fetch_archive_feeds_for_user(cls, user_id): from apps.profile.tasks import FetchArchiveFeedsForUser - FetchArchiveFeedsForUser.apply_async(kwargs=dict(user_id=user_id), - queue='search_indexer', - time_limit=settings.MAX_SECONDS_COMPLETE_ARCHIVE_FETCH) - + + FetchArchiveFeedsForUser.apply_async( + kwargs=dict(user_id=user_id), + queue="search_indexer", + time_limit=settings.MAX_SECONDS_COMPLETE_ARCHIVE_FETCH, + ) + # Should be run as a background task @classmethod def fetch_archive_feeds_for_user(cls, user_id): @@ -527,12 +619,11 @@ def fetch_archive_feeds_for_user(cls, user_id): start_time = time.time() user = User.objects.get(pk=user_id) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(user.username, 'fetch_archive:start') + r.publish(user.username, "fetch_archive:start") - subscriptions = UserSubscription.objects.filter(user=user).only('feed') + subscriptions = UserSubscription.objects.filter(user=user).only("feed") total = subscriptions.count() - feed_ids = [] starting_story_count = 0 for sub in subscriptions: @@ -541,25 +632,31 @@ def fetch_archive_feeds_for_user(cls, user_id): except Feed.DoesNotExist: continue starting_story_count += MStory.objects(story_feed_id=sub.feed.pk).count() - + feed_id_chunks = [c for c in chunks(feed_ids, 1)] - logging.user(user, "~FCFetching archive stories from ~SB%s feeds~SN in %s chunks..." % - (total, len(feed_id_chunks))) - - search_chunks = [FetchArchiveFeedsChunk.s(feed_ids=feed_id_chunk, - user_id=user_id - ).set(queue='search_indexer') - .set(time_limit=settings.MAX_SECONDS_ARCHIVE_FETCH_SINGLE_FEED, - soft_time_limit=settings.MAX_SECONDS_ARCHIVE_FETCH_SINGLE_FEED-30) - for feed_id_chunk in feed_id_chunks] - callback = FinishFetchArchiveFeeds.s(user_id=user_id, - start_time=start_time, - starting_story_count=starting_story_count).set(queue='search_indexer') + logging.user( + user, + "~FCFetching archive stories from ~SB%s feeds~SN in %s chunks..." % (total, len(feed_id_chunks)), + ) + + search_chunks = [ + FetchArchiveFeedsChunk.s(feed_ids=feed_id_chunk, user_id=user_id) + .set(queue="search_indexer") + .set( + time_limit=settings.MAX_SECONDS_ARCHIVE_FETCH_SINGLE_FEED, + soft_time_limit=settings.MAX_SECONDS_ARCHIVE_FETCH_SINGLE_FEED - 30, + ) + for feed_id_chunk in feed_id_chunks + ] + callback = FinishFetchArchiveFeeds.s( + user_id=user_id, start_time=start_time, starting_story_count=starting_story_count + ).set(queue="search_indexer") celery.chord(search_chunks)(callback) @classmethod def fetch_archive_feeds_chunk(cls, user_id, feed_ids): from apps.rss_feeds.models import Feed + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) user = User.objects.get(pk=user_id) @@ -567,18 +664,18 @@ def fetch_archive_feeds_chunk(cls, user_id, feed_ids): for feed_id in feed_ids: feed = Feed.get_by_id(feed_id) - if not feed: continue - + if not feed: + continue + feed.fill_out_archive_stories() - - r.publish(user.username, 'fetch_archive:feeds:%s' % - ','.join([str(f) for f in feed_ids])) + + r.publish(user.username, "fetch_archive:feeds:%s" % ",".join([str(f) for f in feed_ids])) @classmethod def finish_fetch_archive_feeds(cls, user_id, start_time, starting_story_count): r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) user = User.objects.get(pk=user_id) - subscriptions = UserSubscription.objects.filter(user=user).only('feed') + subscriptions = UserSubscription.objects.filter(user=user).only("feed") total = subscriptions.count() duration = time.time() - start_time @@ -592,46 +689,52 @@ def finish_fetch_archive_feeds(cls, user_id, start_time, starting_story_count): continue new_story_count = ending_story_count - starting_story_count - logging.user(user, f"~FCFinished archive feed fetches for ~SB~FG{subscriptions.count()} feeds~FC~SN: ~FG~SB{new_story_count:,} new~SB~FC, ~FG{ending_story_count:,} total (pre-archive: {pre_archive_count:,} stories)") - - logging.user(user, "~FCFetched archive stories from ~SB%s feeds~SN in ~FM~SB%s~FC~SN sec." % - (total, round(duration, 2))) - r.publish(user.username, 'fetch_archive:done') + logging.user( + user, + f"~FCFinished archive feed fetches for ~SB~FG{subscriptions.count()} feeds~FC~SN: ~FG~SB{new_story_count:,} new~SB~FC, ~FG{ending_story_count:,} total (pre-archive: {pre_archive_count:,} stories)", + ) + + logging.user( + user, + "~FCFetched archive stories from ~SB%s feeds~SN in ~FM~SB%s~FC~SN sec." + % (total, round(duration, 2)), + ) + r.publish(user.username, "fetch_archive:done") return ending_story_count, min(pre_archive_count, starting_story_count) - - + @classmethod def identify_deleted_feed_users(cls, old_feed_id): - users = UserSubscriptionFolders.objects.filter(folders__contains=old_feed_id).only('user') + users = UserSubscriptionFolders.objects.filter(folders__contains=old_feed_id).only("user") user_ids = [usf.user_id for usf in users] - f = open('utils/backups/users.txt', 'w') - f.write('\n'.join([str(u) for u in user_ids])) + f = open("utils/backups/users.txt", "w") + f.write("\n".join([str(u) for u in user_ids])) return user_ids @classmethod def recreate_deleted_feed(cls, new_feed_id, old_feed_id=None, skip=0): - user_ids = sorted([int(u) for u in open('utils/backups/users.txt').read().split('\n') if u]) - + user_ids = sorted([int(u) for u in open("utils/backups/users.txt").read().split("\n") if u]) + count = len(user_ids) - + for i, user_id in enumerate(user_ids): - if i < skip: continue + if i < skip: + continue if i % 1000 == 0: print("\n\n ------------------------------------------------") - print("\n ---> %s/%s (%s%%)" % (i, count, round(float(i)/count))) + print("\n ---> %s/%s (%s%%)" % (i, count, round(float(i) / count))) print("\n ------------------------------------------------\n") try: user = User.objects.get(pk=user_id) except User.DoesNotExist: print(" ***> %s has no account" % user_id) continue - us, created = UserSubscription.objects.get_or_create(user_id=user_id, feed_id=new_feed_id, defaults={ - 'needs_unread_recalc': True, - 'active': True, - 'is_trained': True - }) + us, created = UserSubscription.objects.get_or_create( + user_id=user_id, + feed_id=new_feed_id, + defaults={"needs_unread_recalc": True, "active": True, "is_trained": True}, + ) if not created: print(" ***> %s already subscribed" % user.username) try: @@ -639,7 +742,7 @@ def recreate_deleted_feed(cls, new_feed_id, old_feed_id=None, skip=0): usf.add_missing_feeds() except UserSubscriptionFolders.DoesNotExist: print(" ***> %s has no USF" % user.username) - + # Move classifiers if old_feed_id: classifier_count = 0 @@ -654,26 +757,30 @@ def recreate_deleted_feed(cls, new_feed_id, old_feed_id=None, skip=0): continue if classifier_count: print(" Moved %s classifiers for %s" % (classifier_count, user.username)) - + def trim_read_stories(self, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - + read_stories_key = "RS:%s:%s" % (self.user_id, self.feed_id) stale_story_hashes = r.sdiff(read_stories_key, "F:%s" % self.feed_id) if not stale_story_hashes: return - - logging.user(self.user, "~FBTrimming ~FR%s~FB read stories (~SB%s~SN)..." % (len(stale_story_hashes), self.feed_id)) + + logging.user( + self.user, + "~FBTrimming ~FR%s~FB read stories (~SB%s~SN)..." % (len(stale_story_hashes), self.feed_id), + ) r.srem(read_stories_key, *stale_story_hashes) r.srem("RS:%s" % self.feed_id, *stale_story_hashes) - + @classmethod def trim_user_read_stories(self, user_id): user = User.objects.get(pk=user_id) r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - subs = UserSubscription.objects.filter(user_id=user_id).only('feed') - if not subs: return + subs = UserSubscription.objects.filter(user_id=user_id).only("feed") + if not subs: + return key = "RS:%s" % user_id feeds = [f.feed_id for f in subs] @@ -687,10 +794,10 @@ def trim_user_read_stories(self, user_id): # r.expire("%s:backup" % key, 60*60*24) r.sunionstore(key, *["%s:%s" % (key, f) for f in feeds]) new_rs = r.smembers(key) - + missing_rs = [] missing_count = 0 - feed_re = re.compile(r'(\d+):.*?') + feed_re = re.compile(r"(\d+):.*?") for i, rs in enumerate(old_rs): if i and i % 1000 == 0: if missing_rs: @@ -704,47 +811,56 @@ def trim_user_read_stories(self, user_id): rs_feed_id = found.groups()[0] if int(rs_feed_id) not in feeds: missing_rs.append(rs) - + if missing_rs: r.sadd(key, *missing_rs) - missing_count += len(missing_rs) + missing_count += len(missing_rs) new_count = len(new_rs) new_total = new_count + missing_count - logging.user(user, "~FBTrimming ~FR%s~FB/%s (~SB%s sub'ed ~SN+ ~SB%s unsub'ed~SN saved)" % - (old_count - new_total, old_count, new_count, missing_count)) - - + logging.user( + user, + "~FBTrimming ~FR%s~FB/%s (~SB%s sub'ed ~SN+ ~SB%s unsub'ed~SN saved)" + % (old_count - new_total, old_count, new_count, missing_count), + ) + def mark_feed_read(self, cutoff_date=None): - if (self.unread_count_negative == 0 + if ( + self.unread_count_negative == 0 and self.unread_count_neutral == 0 and self.unread_count_positive == 0 - and not self.needs_unread_recalc): + and not self.needs_unread_recalc + ): return - + recount = True # Use the latest story to get last read time. if cutoff_date: cutoff_date = cutoff_date + datetime.timedelta(seconds=1) else: now = datetime.datetime.now() - latest_story = MStory.objects(story_feed_id=self.feed.pk, - story_date__lte=now)\ - .order_by('-story_date').only('story_date').limit(1) + latest_story = ( + MStory.objects(story_feed_id=self.feed.pk, story_date__lte=now) + .order_by("-story_date") + .only("story_date") + .limit(1) + ) if latest_story and len(latest_story) >= 1: - cutoff_date = (latest_story[0]['story_date'] - + datetime.timedelta(seconds=1)) + cutoff_date = latest_story[0]["story_date"] + datetime.timedelta(seconds=1) else: cutoff_date = datetime.datetime.utcnow() recount = False - + if cutoff_date > self.mark_read_date or cutoff_date > self.oldest_unread_story_date: self.last_read_date = cutoff_date self.mark_read_date = cutoff_date self.oldest_unread_story_date = cutoff_date else: - logging.user(self.user, "Not marking %s as read: %s > %s/%s" % - (self, cutoff_date, self.mark_read_date, self.oldest_unread_story_date)) - + logging.user( + self.user, + "Not marking %s as read: %s > %s/%s" + % (self, cutoff_date, self.mark_read_date, self.oldest_unread_story_date), + ) + if not recount: self.unread_count_negative = 0 self.unread_count_positive = 0 @@ -753,58 +869,63 @@ def mark_feed_read(self, cutoff_date=None): self.needs_unread_recalc = False else: self.needs_unread_recalc = True - + self.save() - + return True - + def mark_newer_stories_read(self, cutoff_date): - if (self.unread_count_negative == 0 + if ( + self.unread_count_negative == 0 and self.unread_count_neutral == 0 and self.unread_count_positive == 0 - and not self.needs_unread_recalc): + and not self.needs_unread_recalc + ): return - + cutoff_date = cutoff_date - datetime.timedelta(seconds=1) - story_hashes = UserSubscription.story_hashes(self.user.pk, feed_ids=[self.feed.pk], - order="newest", read_filter="unread", - cutoff_date=cutoff_date) + story_hashes = UserSubscription.story_hashes( + self.user.pk, + feed_ids=[self.feed.pk], + order="newest", + read_filter="unread", + cutoff_date=cutoff_date, + ) data = self.mark_story_ids_as_read(story_hashes, aggregated=True) return data - - + def mark_story_ids_as_read(self, story_hashes, request=None, aggregated=False): data = dict(code=0, payload=story_hashes) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - + if not request: request = self.user - + if not self.needs_unread_recalc: self.needs_unread_recalc = True - self.save(update_fields=['needs_unread_recalc']) - + self.save(update_fields=["needs_unread_recalc"]) + if len(story_hashes) > 1: logging.user(request, "~FYRead %s stories in feed: %s" % (len(story_hashes), self.feed)) else: logging.user(request, "~FYRead story (%s) in feed: %s" % (story_hashes, self.feed)) RUserStory.aggregate_mark_read(self.feed_id) - - for story_hash in set(story_hashes): + + for story_hash in set(story_hashes): # logging.user(request, "~FYRead story: %s" % (story_hash)) RUserStory.mark_read(self.user_id, self.feed_id, story_hash, aggregated=aggregated) - r.publish(self.user.username, 'story:read:%s' % story_hash) + r.publish(self.user.username, "story:read:%s" % story_hash) if self.user.profile.is_archive: RUserUnreadStory.mark_read(self.user_id, story_hash) - r.publish(self.user.username, 'feed:%s' % self.feed_id) - + r.publish(self.user.username, "feed:%s" % self.feed_id) + self.last_read_date = datetime.datetime.now() - self.save(update_fields=['last_read_date']) - + self.save(update_fields=["last_read_date"]) + return data - + def invert_read_stories_after_unread_story(self, story, request=None): data = dict(code=1) unread_cutoff = self.user.profile.unread_cutoff @@ -820,33 +941,32 @@ def invert_read_stories_after_unread_story(self, story, request=None): story_hash=story.story_hash, story_date=story.story_date, ) - data['story_hashes'] = [story.story_hash] + data["story_hashes"] = [story.story_hash] return data - + # Story is outside the mark as read range, so invert all stories before. - newer_stories = MStory.objects(story_feed_id=story.story_feed_id, - story_date__gte=story.story_date, - story_date__lte=unread_cutoff - ).only('story_hash') + newer_stories = MStory.objects( + story_feed_id=story.story_feed_id, story_date__gte=story.story_date, story_date__lte=unread_cutoff + ).only("story_hash") newer_stories = [s.story_hash for s in newer_stories] self.mark_read_date = story.story_date - datetime.timedelta(minutes=1) self.needs_unread_recalc = True self.save() - + # Mark stories as read only after the mark_read_date has been moved, otherwise # these would be ignored. data = self.mark_story_ids_as_read(newer_stories, request=request, aggregated=True) - + return data - + def calculate_feed_scores(self, silent=False, stories=None, force=False): # now = datetime.datetime.strptime("2009-07-06 22:30:03", "%Y-%m-%d %H:%M:%S") now = datetime.datetime.now() oldest_unread_story_date = now - + if self.user.profile.last_seen_on < self.user.profile.unread_cutoff and not force: if not silent and settings.DEBUG: - logging.info(' ---> [%s] SKIPPING Computing scores: %s (1 week+)' % (self.user, self.feed)) + logging.info(" ---> [%s] SKIPPING Computing scores: %s (1 week+)" % (self.user, self.feed)) return self ong = self.unread_count_negative ont = self.unread_count_neutral @@ -855,32 +975,35 @@ def calculate_feed_scores(self, silent=False, stories=None, force=False): ucu = self.unread_count_updated onur = self.needs_unread_recalc oit = self.is_trained - + # if not self.feed.fetched_once: # if not silent: # logging.info(' ---> [%s] NOT Computing scores: %s' % (self.user, self.feed)) # self.needs_unread_recalc = False # self.save() # return - + feed_scores = dict(negative=0, neutral=0, positive=0) - + # Two weeks in age. If mark_read_date is older, mark old stories as read. date_delta = self.user.profile.unread_cutoff if date_delta < self.mark_read_date: date_delta = self.mark_read_date else: self.mark_read_date = date_delta - + if self.is_trained: if not stories: - stories = cache.get('S:v3:%s' % self.feed_id) - - unread_story_hashes = self.story_hashes(user_id=self.user_id, feed_ids=[self.feed_id], - usersubs=[self], - read_filter='unread', - cutoff_date=self.user.profile.unread_cutoff) - + stories = cache.get("S:v3:%s" % self.feed_id) + + unread_story_hashes = self.story_hashes( + user_id=self.user_id, + feed_ids=[self.feed_id], + usersubs=[self], + read_filter="unread", + cutoff_date=self.user.profile.unread_cutoff, + ) + if not stories: try: stories_db = MStory.objects(story_hash__in=unread_story_hashes) @@ -891,112 +1014,144 @@ def calculate_feed_scores(self, silent=False, stories=None, force=False): except pymongo.errors.OperationFailure as e: stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:25] stories = Feed.format_stories(stories_db, self.feed_id) - + unread_stories = [] for story in stories: # if story['story_date'] < date_delta: # continue - if story['story_hash'] in unread_story_hashes: + if story["story_hash"] in unread_story_hashes: unread_stories.append(story) - if story['story_date'] < oldest_unread_story_date: - oldest_unread_story_date = story['story_date'] + if story["story_date"] < oldest_unread_story_date: + oldest_unread_story_date = story["story_date"] # if not silent: # logging.info(' ---> [%s] Format stories: %s' % (self.user, datetime.datetime.now() - now)) - - classifier_feeds = list(MClassifierFeed.objects(user_id=self.user_id, feed_id=self.feed_id, social_user_id=0)) + + classifier_feeds = list( + MClassifierFeed.objects(user_id=self.user_id, feed_id=self.feed_id, social_user_id=0) + ) classifier_authors = list(MClassifierAuthor.objects(user_id=self.user_id, feed_id=self.feed_id)) - classifier_titles = list(MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id)) - classifier_tags = list(MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id)) - - if (not len(classifier_feeds) and - not len(classifier_authors) and - not len(classifier_titles) and - not len(classifier_tags)): + classifier_titles = list(MClassifierTitle.objects(user_id=self.user_id, feed_id=self.feed_id)) + classifier_tags = list(MClassifierTag.objects(user_id=self.user_id, feed_id=self.feed_id)) + + if ( + not len(classifier_feeds) + and not len(classifier_authors) + and not len(classifier_titles) + and not len(classifier_tags) + ): self.is_trained = False - + # if not silent: # logging.info(' ---> [%s] Classifiers: %s (%s)' % (self.user, datetime.datetime.now() - now, classifier_feeds.count() + classifier_authors.count() + classifier_tags.count() + classifier_titles.count())) - + scores = { - 'feed': apply_classifier_feeds(classifier_feeds, self.feed), + "feed": apply_classifier_feeds(classifier_feeds, self.feed), } - + for story in unread_stories: - scores.update({ - 'author' : apply_classifier_authors(classifier_authors, story), - 'tags' : apply_classifier_tags(classifier_tags, story), - 'title' : apply_classifier_titles(classifier_titles, story), - }) - - max_score = max(scores['author'], scores['tags'], scores['title']) - min_score = min(scores['author'], scores['tags'], scores['title']) + scores.update( + { + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), + } + ) + + max_score = max(scores["author"], scores["tags"], scores["title"]) + min_score = min(scores["author"], scores["tags"], scores["title"]) if max_score > 0: - feed_scores['positive'] += 1 + feed_scores["positive"] += 1 elif min_score < 0: - feed_scores['negative'] += 1 + feed_scores["negative"] += 1 else: - if scores['feed'] > 0: - feed_scores['positive'] += 1 - elif scores['feed'] < 0: - feed_scores['negative'] += 1 + if scores["feed"] > 0: + feed_scores["positive"] += 1 + elif scores["feed"] < 0: + feed_scores["negative"] += 1 else: - feed_scores['neutral'] += 1 + feed_scores["neutral"] += 1 else: - unread_story_hashes = self.story_hashes(user_id=self.user_id, feed_ids=[self.feed_id], - usersubs=[self], - read_filter='unread', - include_timestamps=True, - cutoff_date=date_delta) - - feed_scores['neutral'] = len(unread_story_hashes) - if feed_scores['neutral']: + unread_story_hashes = self.story_hashes( + user_id=self.user_id, + feed_ids=[self.feed_id], + usersubs=[self], + read_filter="unread", + include_timestamps=True, + cutoff_date=date_delta, + ) + + feed_scores["neutral"] = len(unread_story_hashes) + if feed_scores["neutral"]: oldest_unread_story_date = datetime.datetime.fromtimestamp(unread_story_hashes[-1][1]) - + if not silent or settings.DEBUG: - logging.user(self.user, '~FBUnread count (~SB%s~SN%s): ~SN(~FC%s~FB/~FC%s~FB/~FC%s~FB) ~SBto~SN (~FC%s~FB/~FC%s~FB/~FC%s~FB)' % (self.feed_id, '/~FMtrained~FB' if self.is_trained else '', ong, ont, ops, feed_scores['negative'], feed_scores['neutral'], feed_scores['positive'])) + logging.user( + self.user, + "~FBUnread count (~SB%s~SN%s): ~SN(~FC%s~FB/~FC%s~FB/~FC%s~FB) ~SBto~SN (~FC%s~FB/~FC%s~FB/~FC%s~FB)" + % ( + self.feed_id, + "/~FMtrained~FB" if self.is_trained else "", + ong, + ont, + ops, + feed_scores["negative"], + feed_scores["neutral"], + feed_scores["positive"], + ), + ) - self.unread_count_positive = feed_scores['positive'] - self.unread_count_neutral = feed_scores['neutral'] - self.unread_count_negative = feed_scores['negative'] + self.unread_count_positive = feed_scores["positive"] + self.unread_count_neutral = feed_scores["neutral"] + self.unread_count_negative = feed_scores["negative"] self.unread_count_updated = datetime.datetime.now() self.oldest_unread_story_date = oldest_unread_story_date self.needs_unread_recalc = False - + update_fields = [] - if self.unread_count_positive != ops: update_fields.append('unread_count_positive') - if self.unread_count_neutral != ont: update_fields.append('unread_count_neutral') - if self.unread_count_negative != ong: update_fields.append('unread_count_negative') - if self.unread_count_updated != ucu: update_fields.append('unread_count_updated') - if self.oldest_unread_story_date != oousd: update_fields.append('oldest_unread_story_date') - if self.needs_unread_recalc != onur: update_fields.append('needs_unread_recalc') - if self.is_trained != oit: update_fields.append('is_trained') + if self.unread_count_positive != ops: + update_fields.append("unread_count_positive") + if self.unread_count_neutral != ont: + update_fields.append("unread_count_neutral") + if self.unread_count_negative != ong: + update_fields.append("unread_count_negative") + if self.unread_count_updated != ucu: + update_fields.append("unread_count_updated") + if self.oldest_unread_story_date != oousd: + update_fields.append("oldest_unread_story_date") + if self.needs_unread_recalc != onur: + update_fields.append("needs_unread_recalc") + if self.is_trained != oit: + update_fields.append("is_trained") if len(update_fields): self.save(update_fields=update_fields) - - if (self.unread_count_positive == 0 and - self.unread_count_neutral == 0): + + if self.unread_count_positive == 0 and self.unread_count_neutral == 0: self.mark_feed_read() - + if not silent: - logging.user(self.user, '~FC~SNComputing scores: %s (~SB%s~SN/~SB%s~SN/~SB%s~SN)' % (self.feed, feed_scores['negative'], feed_scores['neutral'], feed_scores['positive'])) - + logging.user( + self.user, + "~FC~SNComputing scores: %s (~SB%s~SN/~SB%s~SN/~SB%s~SN)" + % (self.feed, feed_scores["negative"], feed_scores["neutral"], feed_scores["positive"]), + ) + self.trim_read_stories() - + return self - + @staticmethod def score_story(scores): - max_score = max(scores['author'], scores['tags'], scores['title']) - min_score = min(scores['author'], scores['tags'], scores['title']) + max_score = max(scores["author"], scores["tags"], scores["title"]) + min_score = min(scores["author"], scores["tags"], scores["title"]) if max_score > 0: return 1 elif min_score < 0: return -1 - return scores['feed'] - + return scores["feed"] + def switch_feed(self, new_feed, old_feed): # Rewrite feed in subscription folders try: @@ -1004,14 +1159,12 @@ def switch_feed(self, new_feed, old_feed): except Exception as e: logging.info(" *** ---> UserSubscriptionFolders error: %s" % e) return - + logging.info(" ===> %s " % self.user) # Switch read stories - RUserStory.switch_feed(user_id=self.user_id, old_feed_id=old_feed.pk, - new_feed_id=new_feed.pk) - RUserUnreadStory.switch_feed(user_id=self.user_id, old_feed_id=old_feed.pk, - new_feed_id=new_feed.pk) + RUserStory.switch_feed(user_id=self.user_id, old_feed_id=old_feed.pk, new_feed_id=new_feed.pk) + RUserUnreadStory.switch_feed(user_id=self.user_id, old_feed_id=old_feed.pk, new_feed_id=new_feed.pk) def switch_feed_for_classifier(model): duplicates = model.objects(feed_id=old_feed.pk, user_id=self.user_id) @@ -1027,7 +1180,7 @@ def switch_feed_for_classifier(model): except (IntegrityError, OperationError): logging.info(" !!!!> %s already exists" % duplicate) duplicate.delete() - + switch_feed_for_classifier(MClassifierTitle) switch_feed_for_classifier(MClassifierAuthor) switch_feed_for_classifier(MClassifierFeed) @@ -1046,7 +1199,7 @@ def switch_feed_for_classifier(model): logging.info(" !!!!> %s already subscribed" % self.user) self.delete() return - + @classmethod def collect_orphan_feeds(cls, user): us = cls.objects.filter(user=user) @@ -1056,7 +1209,7 @@ def collect_orphan_feeds(cls, user): return us_feed_ids = set([sub.feed_id for sub in us]) folders = json.decode(usf.folders) - + def collect_ids(folders, found_ids): for item in folders: # print ' --> %s' % item @@ -1071,10 +1224,14 @@ def collect_ids(folders, found_ids): found_ids.update(collect_ids(item, found_ids)) # print ' --> Returning: %s' % found_ids return found_ids + found_ids = collect_ids(folders, set()) diff = len(us_feed_ids) - len(found_ids) if diff > 0: - logging.info(" ---> Collecting orphans on %s. %s feeds with %s orphans" % (user.username, len(us_feed_ids), diff)) + logging.info( + " ---> Collecting orphans on %s. %s feeds with %s orphans" + % (user.username, len(us_feed_ids), diff) + ) orphan_ids = us_feed_ids - found_ids folders.extend(list(orphan_ids)) usf.folders = json.encode(folders) @@ -1092,7 +1249,7 @@ def all_subs_needs_unread_recalc(cls, user_id): needed_recalc += 1 logging.debug(f" ---> Relcaculated {needed_recalc} of {total} subscriptions for user_id: {user_id}") - + @classmethod def verify_feeds_scheduled(cls, user_id): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) @@ -1102,13 +1259,13 @@ def verify_feeds_scheduled(cls, user_id): p = r.pipeline() for feed_id in feed_ids: - p.zscore('scheduled_updates', feed_id) - p.zscore('error_feeds', feed_id) + p.zscore("scheduled_updates", feed_id) + p.zscore("error_feeds", feed_id) results = p.execute() - + p = r.pipeline() for feed_id in feed_ids: - p.zscore('queued_feeds', feed_id) + p.zscore("queued_feeds", feed_id) try: results_queued = p.execute() except: @@ -1116,13 +1273,14 @@ def verify_feeds_scheduled(cls, user_id): safety_net = [] for f, feed_id in enumerate(feed_ids): - scheduled_updates = results[f*2] - error_feeds = results[f*2+1] + scheduled_updates = results[f * 2] + error_feeds = results[f * 2 + 1] queued_feeds = results_queued[f] if not scheduled_updates and not queued_feeds and not error_feeds: safety_net.append(feed_id) - if not safety_net: return + if not safety_net: + return logging.user(user, "~FBFound ~FR%s unscheduled feeds~FB, scheduling immediately..." % len(safety_net)) for feed_id in safety_net: @@ -1132,12 +1290,18 @@ def verify_feeds_scheduled(cls, user_id): @classmethod def count_subscribers_to_other_subscriptions(cls, feed_id): # feeds = defaultdict(int) - subscribing_users = cls.objects.filter(feed=feed_id).values('user', 'feed_opens').order_by('-feed_opens')[:25] + subscribing_users = ( + cls.objects.filter(feed=feed_id).values("user", "feed_opens").order_by("-feed_opens")[:25] + ) print("Got subscribing users") - subscribing_user_ids = [sub['user'] for sub in subscribing_users] + subscribing_user_ids = [sub["user"] for sub in subscribing_users] print("Got subscribing user ids") - cofeeds = cls.objects.filter(user__in=subscribing_user_ids).values('feed').annotate( - user_count=Count('user')).order_by('-user_count')[:200] + cofeeds = ( + cls.objects.filter(user__in=subscribing_user_ids) + .values("feed") + .annotate(user_count=Count("user")) + .order_by("-user_count")[:200] + ) print("Got cofeeds: %s" % len(cofeeds)) # feed_subscribers = Feed.objects.filter(pk__in=[f['feed'] for f in cofeeds]).values('pk', 'num_subscribers') # max_local_subscribers = float(max([f['user_count'] for f in cofeeds])) @@ -1155,24 +1319,25 @@ def count_subscribers_to_other_subscriptions(cls, feed_id): # pprint([(Feed.get_by_id(o[0]), o[1], o[2], o[3], o[4]) for o in orderedpctfeeds]) users_by_feeds = {} - for feed in [f['feed'] for f in cofeeds]: - users_by_feeds[feed] = [u['user'] for u in cls.objects.filter(feed=feed, user__in=subscribing_user_ids).values('user')] + for feed in [f["feed"] for f in cofeeds]: + users_by_feeds[feed] = [ + u["user"] for u in cls.objects.filter(feed=feed, user__in=subscribing_user_ids).values("user") + ] print("Got users_by_feeds") - + table = tfidf() for feed in list(users_by_feeds.keys()): table.addDocument(feed, users_by_feeds[feed]) print("Got table") - + sorted_table = sorted(table.similarities(subscribing_user_ids), key=itemgetter(1), reverse=True)[:8] pprint([(Feed.get_by_id(o[0]), o[1]) for o in sorted_table]) - + return table # return cofeeds class RUserStory: - @classmethod def mark_story_hashes_read(cls, user_id, story_hashes, username=None, r=None, s=None): if not r: @@ -1182,32 +1347,40 @@ def mark_story_hashes_read(cls, user_id, story_hashes, username=None, r=None, s= ps = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) if not username: username = User.objects.get(pk=user_id).username - + p = r.pipeline() feed_ids = set() friend_ids = set() - + if not isinstance(story_hashes, list): story_hashes = [story_hashes] - + single_story = len(story_hashes) == 1 - + for story_hash in story_hashes: feed_id, _ = MStory.split_story_hash(story_hash) feed_ids.add(feed_id) - + if single_story: cls.aggregate_mark_read(feed_id) - + # Find other social feeds with this story to update their counts friend_key = "F:%s:F" % (user_id) share_key = "S:%s" % (story_hash) friends_with_shares = [int(f) for f in s.sinter(share_key, friend_key)] friend_ids.update(friends_with_shares) - cls.mark_read(user_id, feed_id, story_hash, social_user_ids=friends_with_shares, r=p, username=username, ps=ps) - + cls.mark_read( + user_id, + feed_id, + story_hash, + social_user_ids=friends_with_shares, + r=p, + username=username, + ps=ps, + ) + p.execute() - + return list(feed_ids), list(friend_ids) @classmethod @@ -1218,7 +1391,7 @@ def mark_story_hash_unread(cls, user, story_hash, r=None, s=None, ps=None): s = redis.Redis(connection_pool=settings.REDIS_POOL) if not ps: ps = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - + friend_ids = set() feed_id, _ = MStory.split_story_hash(story_hash) @@ -1227,52 +1400,69 @@ def mark_story_hash_unread(cls, user, story_hash, r=None, s=None, ps=None): share_key = "S:%s" % (story_hash) friends_with_shares = [int(f) for f in s.sinter(share_key, friend_key)] friend_ids.update(friends_with_shares) - cls.mark_unread(user.pk, feed_id, story_hash, social_user_ids=friends_with_shares, r=r, - username=user.username, ps=ps) - + cls.mark_unread( + user.pk, + feed_id, + story_hash, + social_user_ids=friends_with_shares, + r=r, + username=user.username, + ps=ps, + ) + return feed_id, list(friend_ids) - + @classmethod def aggregate_mark_read(cls, feed_id): if not feed_id: logging.debug(" ***> ~BR~FWNo feed_id on aggregate mark read. Ignoring.") return - + r = redis.Redis(connection_pool=settings.REDIS_FEED_READ_POOL) - week_of_year = datetime.datetime.now().strftime('%Y-%U') + week_of_year = datetime.datetime.now().strftime("%Y-%U") feed_read_key = "fR:%s:%s" % (feed_id, week_of_year) - + r.incr(feed_read_key) # This settings.DAYS_OF_STORY_HASHES doesn't need to consider potential pro subscribers # because the feed_read_key is really only used for statistics and not unreads - r.expire(feed_read_key, 2*settings.DAYS_OF_STORY_HASHES*24*60*60) - + r.expire(feed_read_key, 2 * settings.DAYS_OF_STORY_HASHES * 24 * 60 * 60) + @classmethod - def mark_read(cls, user_id, story_feed_id, story_hash, social_user_ids=None, - aggregated=False, r=None, username=None, ps=None): + def mark_read( + cls, + user_id, + story_feed_id, + story_hash, + social_user_ids=None, + aggregated=False, + r=None, + username=None, + ps=None, + ): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - + story_hash = MStory.ensure_story_hash(story_hash, story_feed_id=story_feed_id) - if not story_hash: return - + if not story_hash: + return + def redis_commands(key): r.sadd(key, story_hash) - r.expire(key, Feed.days_of_story_hashes_for_feed(story_feed_id)*24*60*60) + r.expire(key, Feed.days_of_story_hashes_for_feed(story_feed_id) * 24 * 60 * 60) - all_read_stories_key = 'RS:%s' % (user_id) + all_read_stories_key = "RS:%s" % (user_id) redis_commands(all_read_stories_key) - - read_story_key = 'RS:%s:%s' % (user_id, story_feed_id) + + read_story_key = "RS:%s:%s" % (user_id, story_feed_id) redis_commands(read_story_key) - + if ps and username: - ps.publish(username, 'story:read:%s' % story_hash) - + ps.publish(username, "story:read:%s" % story_hash) + if social_user_ids: for social_user_id in social_user_ids: - social_read_story_key = 'RS:%s:B:%s' % (user_id, social_user_id) + social_read_story_key = "RS:%s:B:%s" % (user_id, social_user_id) redis_commands(social_read_story_key) feed_id, _ = MStory.split_story_hash(story_hash) @@ -1282,13 +1472,13 @@ def redis_commands(key): # unread_ranked_stories_key = f"zU:{user_id}:{story_feed_id}" # r.srem(unread_stories_key, story_hash) # r.zrem(unread_ranked_stories_key, story_hash) - + if not aggregated: - key = 'lRS:%s' % user_id + key = "lRS:%s" % user_id r.lpush(key, story_hash) r.ltrim(key, 0, 1000) - r.expire(key, Feed.days_of_story_hashes_for_feed(story_feed_id)*24*60*60) - + r.expire(key, Feed.days_of_story_hashes_for_feed(story_feed_id) * 24 * 60 * 60) + @staticmethod def story_can_be_marked_unread_by_user(story, user): message = None @@ -1297,44 +1487,51 @@ def story_can_be_marked_unread_by_user(story, user): # message = "Story is more than %s days old, change your days of unreads under Preferences." % ( # user.profile.days_of_unread) if user.profile.is_premium: - message = "Story is more than %s days old. Premium Archive accounts can mark any story as unread." % ( - settings.DAYS_OF_UNREAD) + message = ( + "Story is more than %s days old. Premium Archive accounts can mark any story as unread." + % (settings.DAYS_OF_UNREAD) + ) elif story.story_date > user.profile.unread_cutoff_premium: - message = "Story is older than %s days. Premium has %s days, and Premium Archive can mark anything unread." % ( - settings.DAYS_OF_UNREAD_FREE, settings.DAYS_OF_UNREAD) + message = ( + "Story is older than %s days. Premium has %s days, and Premium Archive can mark anything unread." + % (settings.DAYS_OF_UNREAD_FREE, settings.DAYS_OF_UNREAD) + ) else: - message = "Story is more than %s days old, only Premium Archive can mark older stories unread." % ( - settings.DAYS_OF_UNREAD_FREE) + message = ( + "Story is more than %s days old, only Premium Archive can mark older stories unread." + % (settings.DAYS_OF_UNREAD_FREE) + ) return message - + @staticmethod def mark_unread(user_id, story_feed_id, story_hash, social_user_ids=None, r=None, username=None, ps=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) story_hash = MStory.ensure_story_hash(story_hash, story_feed_id=story_feed_id) - - if not story_hash: return - + + if not story_hash: + return + def redis_commands(key): r.srem(key, story_hash) - r.expire(key, Feed.days_of_story_hashes_for_feed(story_feed_id)*24*60*60) + r.expire(key, Feed.days_of_story_hashes_for_feed(story_feed_id) * 24 * 60 * 60) - all_read_stories_key = 'RS:%s' % (user_id) + all_read_stories_key = "RS:%s" % (user_id) redis_commands(all_read_stories_key) - - read_story_key = 'RS:%s:%s' % (user_id, story_feed_id) + + read_story_key = "RS:%s:%s" % (user_id, story_feed_id) redis_commands(read_story_key) - - read_stories_list_key = 'lRS:%s' % user_id + + read_stories_list_key = "lRS:%s" % user_id r.lrem(read_stories_list_key, 1, story_hash) - + if ps and username: - ps.publish(username, 'story:unread:%s' % story_hash) - + ps.publish(username, "story:unread:%s" % story_hash) + if social_user_ids: for social_user_id in social_user_ids: - social_read_story_key = 'RS:%s:B:%s' % (user_id, social_user_id) + social_read_story_key = "RS:%s:B:%s" % (user_id, social_user_id) redis_commands(social_read_story_key) @staticmethod @@ -1343,51 +1540,52 @@ def get_stories(user_id, feed_id, r=None): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) story_hashes = r.smembers("RS:%s:%s" % (user_id, feed_id)) return story_hashes - + @staticmethod def get_read_stories(user_id, offset=0, limit=12, order="newest"): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) key = "lRS:%s" % user_id - + if order == "oldest": count = r.llen(key) - if offset >= count: return [] - offset = max(0, count - (offset+limit)) - story_hashes = r.lrange(key, offset, offset+limit) + if offset >= count: + return [] + offset = max(0, count - (offset + limit)) + story_hashes = r.lrange(key, offset, offset + limit) elif order == "newest": - story_hashes = r.lrange(key, offset, offset+limit) - + story_hashes = r.lrange(key, offset, offset + limit) + return story_hashes - + @classmethod def switch_feed(cls, user_id, old_feed_id, new_feed_id): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) p = r.pipeline() - + story_hashes = UserSubscription.story_hashes(user_id, feed_ids=[old_feed_id]) # story_hashes = cls.get_stories(user_id, old_feed_id, r=r) - + for story_hash in story_hashes: _, hash_story = MStory.split_story_hash(story_hash) new_story_hash = "%s:%s" % (new_feed_id, hash_story) read_feed_key = "RS:%s:%s" % (user_id, new_feed_id) p.sadd(read_feed_key, new_story_hash) - p.expire(read_feed_key, Feed.days_of_story_hashes_for_feed(new_feed_id)*24*60*60) + p.expire(read_feed_key, Feed.days_of_story_hashes_for_feed(new_feed_id) * 24 * 60 * 60) read_user_key = "RS:%s" % (user_id) p.sadd(read_user_key, new_story_hash) - p.expire(read_user_key, Feed.days_of_story_hashes_for_feed(new_feed_id)*24*60*60) - + p.expire(read_user_key, Feed.days_of_story_hashes_for_feed(new_feed_id) * 24 * 60 * 60) + p.execute() - + if len(story_hashes) > 0: logging.info(" ---> %s read stories" % len(story_hashes)) - + @classmethod def switch_hash(cls, feed, old_hash, new_hash): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) p = r.pipeline() - + usersubs = UserSubscription.objects.filter(feed_id=feed.pk, last_read_date__gte=feed.unread_cutoff) logging.info(" ---> ~SB%s usersubs~SN to switch read story hashes..." % len(usersubs)) for sub in usersubs: @@ -1395,14 +1593,14 @@ def switch_hash(cls, feed, old_hash, new_hash): read = r.sismember(rs_key, old_hash) if read: p.sadd(rs_key, new_hash) - p.expire(rs_key, feed.days_of_story_hashes*24*60*60) - + p.expire(rs_key, feed.days_of_story_hashes * 24 * 60 * 60) + read_user_key = "RS:%s" % sub.user.pk p.sadd(read_user_key, new_hash) - p.expire(read_user_key, feed.days_of_story_hashes*24*60*60) - + p.expire(read_user_key, feed.days_of_story_hashes * 24 * 60 * 60) + p.execute() - + @classmethod def read_story_count(cls, user_id): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) @@ -1410,22 +1608,27 @@ def read_story_count(cls, user_id): count = r.scard(key) return count + class UserSubscriptionFolders(models.Model): """ A JSON list of folders and feeds for while a user has subscribed. The list is a recursive descent of feeds and folders in folders. Used to layout the feeds and folders in the Reader's feed navigation pane. """ + user = models.OneToOneField(User, on_delete=models.CASCADE) folders = models.TextField(default="[]") - + def __str__(self): - return "[%s]: %s" % (self.user, len(self.folders),) - + return "[%s]: %s" % ( + self.user, + len(self.folders), + ) + class Meta: verbose_name_plural = "folders" verbose_name = "folder" - + @classmethod def compact_for_user(cls, user_id): user = User.objects.get(pk=user_id) @@ -1433,12 +1636,12 @@ def compact_for_user(cls, user_id): usf = UserSubscriptionFolders.objects.get(user=user) except UserSubscriptionFolders.DoesNotExist: return - + usf.compact() - + def compact(self): folders = json.decode(self.folders) - + def _compact(folder): new_folder = [] for item in folder: @@ -1449,7 +1652,9 @@ def _compact(folder): # Check every existing folder at that level to see if it already exists for ef, existing_folder in enumerate(new_folder): if type(existing_folder) == dict and list(existing_folder.keys())[0] == f_k: - existing_folder_feed_ids = [f for f in list(existing_folder.values())[0] if type(f) == int] + existing_folder_feed_ids = [ + f for f in list(existing_folder.values())[0] if type(f) == int + ] merged = [] for merge_val in existing_folder_feed_ids: merged.append(merge_val) @@ -1460,19 +1665,23 @@ def _compact(folder): else: merged.append(merge_val) if f_v != existing_folder_feed_ids: - logging.info(f" ---> ~FRFound repeat folder: {f_k} \n\t" - f"~FBExisting: {f_v}\n\t" - f"~FCMerging: {list(existing_folder.values())[0]}\n\t" - f"~FYBecomes: {merged}") + logging.info( + f" ---> ~FRFound repeat folder: {f_k} \n\t" + f"~FBExisting: {f_v}\n\t" + f"~FCMerging: {list(existing_folder.values())[0]}\n\t" + f"~FYBecomes: {merged}" + ) new_folder[ef] = {f_k: _compact(merged)} else: - logging.info(f" ---> ~FRFound repeat folder ~FY{f_k}~FR, no difference in feeds") + logging.info( + f" ---> ~FRFound repeat folder ~FY{f_k}~FR, no difference in feeds" + ) break else: # If no match, then finally we can add the folder new_folder.append({f_k: _compact(f_v)}) return new_folder - + new_folders = _compact(folders) compact_msg = " ---> Compacting from %s to %s" % (folders, new_folders) new_folders = json.encode(new_folders) @@ -1481,7 +1690,7 @@ def _compact(folder): logging.info(" ---> Compacting from %s bytes to %s bytes" % (len(self.folders), len(new_folders))) self.folders = new_folders self.save() - + def add_folder(self, parent_folder, folder): if self.folders: user_sub_folders = json.decode(self.folders) @@ -1491,9 +1700,10 @@ def add_folder(self, parent_folder, folder): user_sub_folders = add_object_to_folder(obj, parent_folder, user_sub_folders) self.folders = json.encode(user_sub_folders) self.save() - + def arranged_folders(self): user_sub_folders = json.decode(self.folders) + def _arrange_folder(folder): folder_feeds = [] folder_folders = [] @@ -1507,22 +1717,20 @@ def _arrange_folder(folder): arranged_folder = folder_feeds + folder_folders return arranged_folder - + return _arrange_folder(user_sub_folders) - + def flatten_folders(self, feeds=None, inactive_feeds=None): folders = json.decode(self.folders) flat_folders = {" ": []} if feeds and not inactive_feeds: inactive_feeds = [] - + def _flatten_folders(items, parent_folder="", depth=0): for item in items: - if (isinstance(item, int) and - (not feeds or - (item in feeds or item in inactive_feeds))): + if isinstance(item, int) and (not feeds or (item in feeds or item in inactive_feeds)): if not parent_folder: - parent_folder = ' ' + parent_folder = " " if parent_folder in flat_folders: flat_folders[parent_folder].append(item) else: @@ -1531,42 +1739,49 @@ def _flatten_folders(items, parent_folder="", depth=0): for folder_name in item: folder = item[folder_name] flat_folder_name = "%s%s%s" % ( - parent_folder if parent_folder and parent_folder != ' ' else "", - " - " if parent_folder and parent_folder != ' ' else "", - folder_name + parent_folder if parent_folder and parent_folder != " " else "", + " - " if parent_folder and parent_folder != " " else "", + folder_name, ) flat_folders[flat_folder_name] = [] - _flatten_folders(folder, flat_folder_name, depth+1) - + _flatten_folders(folder, flat_folder_name, depth + 1) + _flatten_folders(folders) - + return flat_folders def delete_feed(self, feed_id, in_folder, commit_delete=True): feed_id = int(feed_id) - def _find_feed_in_folders(old_folders, folder_name='', multiples_found=False, deleted=False): + + def _find_feed_in_folders(old_folders, folder_name="", multiples_found=False, deleted=False): new_folders = [] for k, folder in enumerate(old_folders): if isinstance(folder, int): - if (folder == feed_id and in_folder is not None and ( - (in_folder not in folder_name) or - (in_folder in folder_name and deleted))): + if ( + folder == feed_id + and in_folder is not None + and ((in_folder not in folder_name) or (in_folder in folder_name and deleted)) + ): multiples_found = True - logging.user(self.user, "~FB~SBDeleting feed, and a multiple has been found in '%s' / '%s' %s" % (folder_name, in_folder, '(deleted)' if deleted else '')) - if (folder == feed_id and - (in_folder is None or in_folder in folder_name) and - not deleted): - logging.user(self.user, "~FBDelete feed: %s'th item: %s folders/feeds" % ( - k, len(old_folders) - )) + logging.user( + self.user, + "~FB~SBDeleting feed, and a multiple has been found in '%s' / '%s' %s" + % (folder_name, in_folder, "(deleted)" if deleted else ""), + ) + if folder == feed_id and (in_folder is None or in_folder in folder_name) and not deleted: + logging.user( + self.user, "~FBDelete feed: %s'th item: %s folders/feeds" % (k, len(old_folders)) + ) deleted = True else: new_folders.append(folder) elif isinstance(folder, dict): for f_k, f_v in list(folder.items()): - nf, multiples_found, deleted = _find_feed_in_folders(f_v, f_k, multiples_found, deleted) + nf, multiples_found, deleted = _find_feed_in_folders( + f_v, f_k, multiples_found, deleted + ) new_folders.append({f_k: nf}) - + return new_folders, multiples_found, deleted user_sub_folders = self.arranged_folders() @@ -1582,8 +1797,7 @@ def _find_feed_in_folders(old_folders, folder_name='', multiples_found=False, de duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id) if duplicate_feed: try: - user_sub = UserSubscription.objects.get(user=self.user, - feed=duplicate_feed[0].feed) + user_sub = UserSubscription.objects.get(user=self.user, feed=duplicate_feed[0].feed) except (Feed.DoesNotExist, UserSubscription.DoesNotExist): return if user_sub: @@ -1600,30 +1814,38 @@ def _find_folder_in_folders(old_folders, folder_name, feeds_to_delete, deleted_f elif isinstance(folder, dict): for f_k, f_v in list(folder.items()): if f_k == folder_to_delete and (in_folder in folder_name or in_folder is None): - logging.user(self.user, "~FBDeleting folder '~SB%s~SN' in '%s': %s" % (f_k, folder_name, folder)) + logging.user( + self.user, + "~FBDeleting folder '~SB%s~SN' in '%s': %s" % (f_k, folder_name, folder), + ) deleted_folder = folder else: - nf, feeds_to_delete, deleted_folder = _find_folder_in_folders(f_v, f_k, feeds_to_delete, deleted_folder) + nf, feeds_to_delete, deleted_folder = _find_folder_in_folders( + f_v, f_k, feeds_to_delete, deleted_folder + ) new_folders.append({f_k: nf}) - + return new_folders, feeds_to_delete, deleted_folder - + user_sub_folders = json.decode(self.folders) - user_sub_folders, feeds_to_delete, deleted_folder = _find_folder_in_folders(user_sub_folders, '', feed_ids_in_folder) + user_sub_folders, feeds_to_delete, deleted_folder = _find_folder_in_folders( + user_sub_folders, "", feed_ids_in_folder + ) self.folders = json.encode(user_sub_folders) self.save() if commit_delete: UserSubscription.objects.filter(user=self.user, feed__in=feeds_to_delete).delete() - + return deleted_folder def delete_feeds_by_folder(self, feeds_by_folder): - logging.user(self.user, "~FBDeleting ~FR~SB%s~SN feeds~FB: ~SB%s" % ( - len(feeds_by_folder), feeds_by_folder)) + logging.user( + self.user, "~FBDeleting ~FR~SB%s~SN feeds~FB: ~SB%s" % (len(feeds_by_folder), feeds_by_folder) + ) for feed_id, in_folder in feeds_by_folder: self.delete_feed(feed_id, in_folder) - + return self def rename_folder(self, folder_to_rename, new_folder_name, in_folder): @@ -1636,21 +1858,25 @@ def _find_folder_in_folders(old_folders, folder_name): for f_k, f_v in list(folder.items()): nf = _find_folder_in_folders(f_v, f_k) if f_k == folder_to_rename and in_folder in folder_name: - logging.user(self.user, "~FBRenaming folder '~SB%s~SN' in '%s' to: ~SB%s" % ( - f_k, folder_name, new_folder_name)) + logging.user( + self.user, + "~FBRenaming folder '~SB%s~SN' in '%s' to: ~SB%s" + % (f_k, folder_name, new_folder_name), + ) f_k = new_folder_name new_folders.append({f_k: nf}) - + return new_folders - + user_sub_folders = json.decode(self.folders) - user_sub_folders = _find_folder_in_folders(user_sub_folders, '') + user_sub_folders = _find_folder_in_folders(user_sub_folders, "") self.folders = json.encode(user_sub_folders) self.save() - + def move_feed_to_folders(self, feed_id, in_folders=None, to_folders=None): - logging.user(self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % ( - feed_id, in_folders, to_folders)) + logging.user( + self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % (feed_id, in_folders, to_folders) + ) user_sub_folders = json.decode(self.folders) for in_folder in in_folders: self.delete_feed(feed_id, in_folder, commit_delete=False) @@ -1659,46 +1885,49 @@ def move_feed_to_folders(self, feed_id, in_folders=None, to_folders=None): user_sub_folders = add_object_to_folder(int(feed_id), to_folder, user_sub_folders) self.folders = json.encode(user_sub_folders) self.save() - + return self def move_feed_to_folder(self, feed_id, in_folder=None, to_folder=None): - logging.user(self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % ( - feed_id, in_folder, to_folder)) + logging.user( + self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % (feed_id, in_folder, to_folder) + ) user_sub_folders = json.decode(self.folders) self.delete_feed(feed_id, in_folder, commit_delete=False) user_sub_folders = json.decode(self.folders) user_sub_folders = add_object_to_folder(int(feed_id), to_folder, user_sub_folders) self.folders = json.encode(user_sub_folders) self.save() - + return self def move_folder_to_folder(self, folder_name, in_folder=None, to_folder=None): - logging.user(self.user, "~FBMoving folder '~SB%s~SN' in '%s' to: ~SB%s" % ( - folder_name, in_folder, to_folder)) + logging.user( + self.user, "~FBMoving folder '~SB%s~SN' in '%s' to: ~SB%s" % (folder_name, in_folder, to_folder) + ) user_sub_folders = json.decode(self.folders) deleted_folder = self.delete_folder(folder_name, in_folder, [], commit_delete=False) user_sub_folders = json.decode(self.folders) user_sub_folders = add_object_to_folder(deleted_folder, to_folder, user_sub_folders) self.folders = json.encode(user_sub_folders) self.save() - + return self - + def move_feeds_by_folder_to_folder(self, feeds_by_folder, to_folder): - logging.user(self.user, "~FBMoving ~SB%s~SN feeds to folder: ~SB%s" % ( - len(feeds_by_folder), to_folder)) + logging.user( + self.user, "~FBMoving ~SB%s~SN feeds to folder: ~SB%s" % (len(feeds_by_folder), to_folder) + ) for feed_id, in_folder in feeds_by_folder: feed_id = int(feed_id) self.move_feed_to_folder(feed_id, in_folder, to_folder) - + return self - + def rewrite_feed(self, original_feed, duplicate_feed): def rewrite_folders(folders, original_feed, duplicate_feed): new_folders = [] - + for k, folder in enumerate(folders): if isinstance(folder, int): if folder == duplicate_feed.pk: @@ -1711,15 +1940,15 @@ def rewrite_folders(folders, original_feed, duplicate_feed): new_folders.append({f_k: rewrite_folders(f_v, original_feed, duplicate_feed)}) return new_folders - + folders = json.decode(self.folders) folders = rewrite_folders(folders, original_feed, duplicate_feed) self.folders = json.encode(folders) self.save() - + def flat(self): folders = json.decode(self.folders) - + def _flat(folder, feeds=None): if not feeds: feeds = [] @@ -1732,10 +1961,10 @@ def _flat(folder, feeds=None): return feeds return _flat(folders) - + def feed_ids_under_folder_slug(self, slug): folders = json.decode(self.folders) - + def _feeds(folder, found=False, folder_title=None): feeds = [] local_found = False @@ -1756,16 +1985,16 @@ def _feeds(folder, found=False, folder_title=None): return feeds, folder_title return _feeds(folders) - + @classmethod def add_all_missing_feeds(cls): - usf = cls.objects.all().order_by('pk') + usf = cls.objects.all().order_by("pk") total = usf.count() - + for i, f in enumerate(usf): print("%s/%s: %s" % (i, total, f)) f.add_missing_feeds() - + @classmethod def add_missing_feeds_for_user(cls, user_id): user = User.objects.get(pk=user_id) @@ -1773,62 +2002,67 @@ def add_missing_feeds_for_user(cls, user_id): usf = UserSubscriptionFolders.objects.get(user=user) except UserSubscriptionFolders.DoesNotExist: return - + usf.add_missing_feeds() - + def add_missing_feeds(self): all_feeds = self.flat() - subs = [us.feed_id for us in - UserSubscription.objects.filter(user=self.user).only('feed')] - + subs = [us.feed_id for us in UserSubscription.objects.filter(user=self.user).only("feed")] + missing_subs = set(all_feeds) - set(subs) if missing_subs: - logging.debug(" ---> %s is missing %s subs. Adding %s..." % ( - self.user, len(missing_subs), missing_subs)) + logging.debug( + " ---> %s is missing %s subs. Adding %s..." % (self.user, len(missing_subs), missing_subs) + ) for feed_id in missing_subs: feed = Feed.get_by_id(feed_id) if feed: if feed_id != feed.pk: - logging.debug(" ---> %s doesn't match %s, rewriting to remove %s..." % ( - feed_id, feed.pk, feed_id)) + logging.debug( + " ---> %s doesn't match %s, rewriting to remove %s..." + % (feed_id, feed.pk, feed_id) + ) # Clear out duplicate sub in folders before subscribing to feed duplicate_feed = Feed.get_by_id(feed_id) duplicate_feed.pk = feed_id self.rewrite_feed(feed, duplicate_feed) - us, _ = UserSubscription.objects.get_or_create(user=self.user, feed=feed, defaults={ - 'needs_unread_recalc': True - }) + us, _ = UserSubscription.objects.get_or_create( + user=self.user, feed=feed, defaults={"needs_unread_recalc": True} + ) if not us.needs_unread_recalc: us.needs_unread_recalc = True us.save() elif feed_id and not feed: # No feed found for subscription, remove subscription - logging.debug(" ---> %s: No feed found, removing subscription: %s" % ( - self.user, feed_id)) + logging.debug(" ---> %s: No feed found, removing subscription: %s" % (self.user, feed_id)) self.delete_feed(feed_id, None, commit_delete=False) - missing_folder_feeds = set(subs) - set(all_feeds) if missing_folder_feeds: user_sub_folders = json.decode(self.folders) - logging.debug(" ---> %s is missing %s folder feeds. Adding %s..." % ( - self.user, len(missing_folder_feeds), missing_folder_feeds)) + logging.debug( + " ---> %s is missing %s folder feeds. Adding %s..." + % (self.user, len(missing_folder_feeds), missing_folder_feeds) + ) for feed_id in missing_folder_feeds: feed = Feed.get_by_id(feed_id) if feed and feed.pk == feed_id: user_sub_folders = add_object_to_folder(feed_id, "", user_sub_folders) self.folders = json.encode(user_sub_folders) self.save() - + def auto_activate(self): - if self.user.profile.is_premium: return - + if self.user.profile.is_premium: + return + active_count = UserSubscription.objects.filter(user=self.user, active=True).count() - if active_count: return - + if active_count: + return + all_feeds = self.flat() - if not all_feeds: return - + if not all_feeds: + return + for feed in all_feeds[:64]: try: sub = UserSubscription.objects.get(user=self.user, feed=feed) @@ -1844,20 +2078,22 @@ class Feature(models.Model): """ Simple blog-like feature board shown to all users on the home page. """ + description = models.TextField(default="") date = models.DateTimeField(default=datetime.datetime.now) - + def __str__(self): return "[%s] %s" % (self.date, self.description[:50]) - + class Meta: ordering = ["-date"] + class RUserUnreadStory: - """Model to store manually unread stories that are older than a user's unread_cutoff + """Model to store manually unread stories that are older than a user's unread_cutoff (same as days_of_unread). This is built for Premium Archive purposes. - If a story is marked as unread but is within the unread_cutoff, no need to add a + If a story is marked as unread but is within the unread_cutoff, no need to add a UserUnreadStory instance as it will be automatically marked as read according to the user's days_of_unread preference. """ @@ -1884,7 +2120,7 @@ def mark_read(cls, user_id, story_hashes, r=None): story_hashes = [story_hashes] if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - + pipeline = r.pipeline() for story_hash in story_hashes: feed_id, _ = MStory.split_story_hash(story_hash) @@ -1895,7 +2131,7 @@ def mark_read(cls, user_id, story_hashes, r=None): pipeline.zrem(user_manual_unread_stories_key, story_hash) pipeline.zrem(user_manual_unread_stories_feed_key, story_hash) pipeline.execute() - + @classmethod def unreads(cls, user_id, story_hash): if not isinstance(story_hash, list): @@ -1920,16 +2156,15 @@ def switch_feed(cls, user_id, old_feed_id, new_feed_id): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) p = r.pipeline() story_hashes = cls.get_stories_and_dates(user_id, old_feed_id, r=r) - - for (story_hash, story_timestamp) in story_hashes: + + for story_hash, story_timestamp in story_hashes: _, hash_story = MStory.split_story_hash(story_hash) new_story_hash = "%s:%s" % (new_feed_id, hash_story) # read_feed_key = "RS:%s:%s" % (user_id, new_feed_id) # user_manual_unread_stories_feed_key = f"uU:{user_id}:{new_feed_id}" cls.mark_unread(user_id, new_story_hash, story_timestamp, r=p) - + p.execute() - + if len(story_hashes) > 0: logging.info(" ---> %s archived unread stories" % len(story_hashes)) - diff --git a/apps/reader/tasks.py b/apps/reader/tasks.py index 0294bdf8d3..2c83929d7b 100644 --- a/apps/reader/tasks.py +++ b/apps/reader/tasks.py @@ -1,18 +1,21 @@ import datetime -from newsblur_web.celeryapp import app -from utils import log as logging -from django.contrib.auth.models import User + from django.conf import settings +from django.contrib.auth.models import User + from apps.reader.models import UserSubscription from apps.social.models import MSocialSubscription +from newsblur_web.celeryapp import app +from utils import log as logging + -@app.task(name='freshen-homepage') +@app.task(name="freshen-homepage") def FreshenHomepage(): day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1) user = User.objects.get(username=settings.HOMEPAGE_USERNAME) user.profile.last_seen_on = datetime.datetime.utcnow() user.profile.save() - + usersubs = UserSubscription.objects.filter(user=user) logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count())) for sub in usersubs: @@ -20,7 +23,7 @@ def FreshenHomepage(): sub.needs_unread_recalc = True sub.save() sub.calculate_feed_scores(silent=True) - + socialsubs = MSocialSubscription.objects.filter(user_id=user.pk) logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count())) for sub in socialsubs: @@ -29,12 +32,16 @@ def FreshenHomepage(): sub.save() sub.calculate_feed_scores(silent=True) -@app.task(name='clean-analytics', time_limit=720*10) + +@app.task(name="clean-analytics", time_limit=720 * 10) def CleanAnalytics(): - logging.debug(" ---> Cleaning analytics... %s feed fetches" % ( - settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(), - )) + logging.debug( + " ---> Cleaning analytics... %s feed fetches" + % (settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),) + ) day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1) - settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({ - "date": {"$lt": day_ago}, - }) + settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many( + { + "date": {"$lt": day_ago}, + } + ) diff --git a/apps/reader/test_reader.py b/apps/reader/test_reader.py index 7bde8b38d4..1eb5781f03 100644 --- a/apps/reader/test_reader.py +++ b/apps/reader/test_reader.py @@ -1,133 +1,198 @@ -from utils import json_functions as json -from django.test.client import Client +from django.conf import settings from django.test import TestCase +from django.test.client import Client from django.urls import reverse -from django.conf import settings from mongoengine.connection import connect, disconnect +from utils import json_functions as json + + class Test_Reader(TestCase): fixtures = [ - 'apps/rss_feeds/fixtures/initial_data.json', - 'apps/rss_feeds/fixtures/rss_feeds.json', - 'subscriptions.json', #'stories.json', - 'apps/rss_feeds/fixtures/gawker1.json'] - + "apps/rss_feeds/fixtures/initial_data.json", + "apps/rss_feeds/fixtures/rss_feeds.json", + "subscriptions.json", #'stories.json', + "apps/rss_feeds/fixtures/gawker1.json", + ] + def setUp(self): disconnect() - settings.MONGODB = connect('test_newsblur') + settings.MONGODB = connect("test_newsblur") self.client = Client() def tearDown(self): - settings.MONGODB.drop_database('test_newsblur') - + settings.MONGODB.drop_database("test_newsblur") + def test_api_feeds(self): - self.client.login(username='conesus', password='test') - - response = self.client.get(reverse('load-feeds')) + self.client.login(username="conesus", password="test") + + response = self.client.get(reverse("load-feeds")) content = json.decode(response.content) - self.assertEqual(len(content['feeds']), 10) - self.assertEqual(content['feeds']['1']['feed_title'], 'The NewsBlur Blog') - self.assertEqual(content['folders'], [{'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}, 1]) - + self.assertEqual(len(content["feeds"]), 10) + self.assertEqual(content["feeds"]["1"]["feed_title"], "The NewsBlur Blog") + self.assertEqual( + content["folders"], [{"Tech": [1, 4, 5, {"Deep Tech": [6, 7]}]}, 2, 3, 8, 9, {"Blogs": [8, 9]}, 1] + ) + def test_delete_feed(self): - self.client.login(username='conesus', password='test') - response = self.client.get(reverse('load-feeds')) + self.client.login(username="conesus", password="test") + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [{'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}, 1]) - + self.assertEqual( + feeds["folders"], [{"Tech": [1, 4, 5, {"Deep Tech": [6, 7]}]}, 2, 3, 8, 9, {"Blogs": [8, 9]}, 1] + ) + # Delete feed - response = self.client.post(reverse('delete-feed'), {'feed_id': 1, 'in_folder': ''}) + response = self.client.post(reverse("delete-feed"), {"feed_id": 1, "in_folder": ""}) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, {'Blogs': [8, 9]}]) - + self.assertEqual( + feeds["folders"], [2, 3, 8, 9, {"Tech": [1, 4, 5, {"Deep Tech": [6, 7]}]}, {"Blogs": [8, 9]}] + ) + # Delete feed - response = self.client.post(reverse('delete-feed'), {'feed_id': 9, 'in_folder': 'Blogs'}) + response = self.client.post(reverse("delete-feed"), {"feed_id": 9, "in_folder": "Blogs"}) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}]) - + self.assertEqual( + feeds["folders"], [2, 3, 8, 9, {"Tech": [1, 4, 5, {"Deep Tech": [6, 7]}]}, {"Blogs": [8]}] + ) + # Delete feed - response = self.client.post(reverse('delete-feed'), {'feed_id': 5, 'in_folder': 'Tech'}) + response = self.client.post(reverse("delete-feed"), {"feed_id": 5, "in_folder": "Tech"}) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}]) - + self.assertEqual( + feeds["folders"], [2, 3, 8, 9, {"Tech": [1, 4, {"Deep Tech": [6, 7]}]}, {"Blogs": [8]}] + ) + # Delete feed - response = self.client.post(reverse('delete-feed'), {'feed_id': 4, 'in_folder': 'Tech'}) + response = self.client.post(reverse("delete-feed"), {"feed_id": 4, "in_folder": "Tech"}) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}]) - + self.assertEqual(feeds["folders"], [2, 3, 8, 9, {"Tech": [1, {"Deep Tech": [6, 7]}]}, {"Blogs": [8]}]) + # Delete feed - response = self.client.post(reverse('delete-feed'), {'feed_id': 8, 'in_folder': ''}) + response = self.client.post(reverse("delete-feed"), {"feed_id": 8, "in_folder": ""}) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [2, 3, 9, {'Tech': [1, {'Deep Tech': [6, 7]}]}, {'Blogs': [8]}]) + self.assertEqual(feeds["folders"], [2, 3, 9, {"Tech": [1, {"Deep Tech": [6, 7]}]}, {"Blogs": [8]}]) def test_delete_feed__multiple_folders(self): - self.client.login(username='conesus', password='test') - - response = self.client.get(reverse('load-feeds')) + self.client.login(username="conesus", password="test") + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [{'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, 2, 3, 8, 9, {'Blogs': [8, 9]}, 1]) - + self.assertEqual( + feeds["folders"], [{"Tech": [1, 4, 5, {"Deep Tech": [6, 7]}]}, 2, 3, 8, 9, {"Blogs": [8, 9]}, 1] + ) + # Delete feed - response = self.client.post(reverse('delete-feed'), {'feed_id': 1}) + response = self.client.post(reverse("delete-feed"), {"feed_id": 1}) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [2, 3, 8, 9, {'Tech': [1, 4, 5, {'Deep Tech': [6, 7]}]}, {'Blogs': [8, 9]}]) - + self.assertEqual( + feeds["folders"], [2, 3, 8, 9, {"Tech": [1, 4, 5, {"Deep Tech": [6, 7]}]}, {"Blogs": [8, 9]}] + ) + def test_move_feeds_by_folder(self): - self.client.login(username='Dejal', password='test') + self.client.login(username="Dejal", password="test") - response = self.client.get(reverse('load-feeds')) + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [5299728, 644144, 1187026, {"Brainiacs & Opinion": [569, 38, 3581, 183139, 1186180, 15]}, {"Science & Technology": [731503, 140145, 1272495, 76, 161, 39, {"Hacker": [5985150, 3323431]}]}, {"Humor": [212379, 3530, 5994357]}, {"Videos": [3240, 5168]}]) - + self.assertEqual( + feeds["folders"], + [ + 5299728, + 644144, + 1187026, + {"Brainiacs & Opinion": [569, 38, 3581, 183139, 1186180, 15]}, + { + "Science & Technology": [ + 731503, + 140145, + 1272495, + 76, + 161, + 39, + {"Hacker": [5985150, 3323431]}, + ] + }, + {"Humor": [212379, 3530, 5994357]}, + {"Videos": [3240, 5168]}, + ], + ) + # Move feeds by folder - response = self.client.post(reverse('move-feeds-by-folder-to-folder'), {'feeds_by_folder': '[\n [\n "5994357",\n "Humor"\n ],\n [\n "3530",\n "Humor"\n ]\n]', 'to_folder': 'Brainiacs & Opinion'}) + response = self.client.post( + reverse("move-feeds-by-folder-to-folder"), + { + "feeds_by_folder": '[\n [\n "5994357",\n "Humor"\n ],\n [\n "3530",\n "Humor"\n ]\n]', + "to_folder": "Brainiacs & Opinion", + }, + ) response = json.decode(response.content) - self.assertEqual(response['code'], 1) - - response = self.client.get(reverse('load-feeds')) + self.assertEqual(response["code"], 1) + + response = self.client.get(reverse("load-feeds")) feeds = json.decode(response.content) - self.assertEqual(feeds['folders'], [5299728, 644144, 1187026, {"Brainiacs & Opinion": [569, 38, 3581, 183139, 1186180, 15, 5994357, 3530]}, {"Science & Technology": [731503, 140145, 1272495, 76, 161, 39, {"Hacker": [5985150, 3323431]}]}, {"Humor": [212379]}, {"Videos": [3240, 5168]}]) - + self.assertEqual( + feeds["folders"], + [ + 5299728, + 644144, + 1187026, + {"Brainiacs & Opinion": [569, 38, 3581, 183139, 1186180, 15, 5994357, 3530]}, + { + "Science & Technology": [ + 731503, + 140145, + 1272495, + 76, + 161, + 39, + {"Hacker": [5985150, 3323431]}, + ] + }, + {"Humor": [212379]}, + {"Videos": [3240, 5168]}, + ], + ) + def test_load_single_feed(self): # from django.conf import settings # from django.db import connection # settings.DEBUG = True # connection.queries = [] - self.client.login(username='conesus', password='test') - url = reverse('load-single-feed', kwargs=dict(feed_id=1)) + self.client.login(username="conesus", password="test") + url = reverse("load-single-feed", kwargs=dict(feed_id=1)) response = self.client.get(url) feed = json.decode(response.content) - self.assertEqual(len(feed['feed_tags']), 0) - self.assertEqual(len(feed['classifiers']['tags']), 0) + self.assertEqual(len(feed["feed_tags"]), 0) + self.assertEqual(len(feed["classifiers"]["tags"]), 0) # self.assert_(connection.queries) - + # settings.DEBUG = False - + def test_compact_user_subscription_folders(self): usf = UserSubscriptionFolders.objects.get(user=User.objects.all()[0]) usf.folders = '[2, 3, {"Bloglets": [423, 424, 425]}, {"Blogs": [426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, {"People": [471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 867, 946, 947, 948]}, {"Tumblrs": [529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549]}, {"Photo Blogs": [550, 551, 552, 553, 554, 555, 556]}, {"Travel": [557, 558, 559]}, {"People": [471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 522, 523, 524, 525, 526, 527, 528, 507, 520, 867]}, {"Tumblrs": [529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549]}, {"Photo Blogs": [550, 551, 552, 553, 554, 555, 556]}, {"Travel": [558, 559, 557]}, 943, {"Link Blogs": [467, 468, 469, 470]}, {"People": [471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 504, 505, 506, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 522, 523, 525, 526, 527, 528]}, {"Tumblrs": [529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549]}, {"Photo Blogs": [550, 551, 552, 553, 554, 555, 556]}, {"Travel": [558, 559]}]}, {"Code": [560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583]}, {"Cooking": [584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 873, 953]}, {"Meta": [598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608]}, {"New York": [609, 610, 611, 612, 613, 614]}, {"San Francisco": [615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 875]}, {"Tech": [635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 184, 661, 662, 663, 664, 665, 666]}, {"Comics & Cartoons": [667, 668, 669, 670, 671, 672, 673, 63, 674, 675, 676, 677, 678, 679, 680, 681, 682, 109, 683, 684, 685, 958]}, {"Hardware": [686, 687, 688, 689, 690, 691, 692]}, {"Wood": []}, {"Newsletters": [693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 724, 719, 720, 721, 722, 723, 725, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 895]}, {"Woodworking": [784, 785, 786, 787, 788, 789, 790, 791, 792, 793]}, {"Twitter": [794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 838, 915]}, {"News": [808, 809, 810, 811, 812, 813, 814, 815, 816, 817]}, {"Home": [818, 819, 820, 821, 822, 823]}, {"Facebook": [824, 825, 826]}, {"Art": [827, 828]}, {"Science": [403, 404, 405, 401, 402]}, {"Boston": [829, 830]}, {"mobility": [831, 832, 833, 834, 835, 836, 837, 963]}, {"Biking": []}, {"A Muted Folder": [1]}, 1, {"Any Broken Feeds": [916]}, {"Any Broken Feeds, Although Some of These Work Fine": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 840, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 841, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 842, 50, 51, 52, 53, 54, 843, 56, 57, 58, 59, 60, 61, 62, 63, 844, 917, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 918, 130, 131, 132, 846, 134, 135, 136, 919, 138, 139, 140, 141, 142, 143, 144, 145, 847, 147, 848, 149, 150, 151, 152, 153, 154, 849, 156, 157, 158, 936, 160, 850, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 1, 185, 186, 187, 188, 189, 851, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 852, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 853, 243, 854, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 856, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 939, 281, 282, 283, 284, 285, 940, 287, 288, 289, 857, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 858, 354, 355, 859, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 860, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, {"Ubuntu": [396, 397, 398, 399, 400]}, {"Science": [401, 402, 403, 404, 405]}, {"Music": [406, 407, 408, 409, 410, 411, 412]}, {"NYTimes": [413]}, {"Test": [414]}, {"Organizer": [415, 416, 417]}, {"Adult": [418, 419, 861, 421]}, {"Test": []}, 422]}]' @@ -137,7 +202,7 @@ def test_compact_user_subscription_folders(self): compact_folders = usf.folders self.assertNotEquals(dupe_folders, compact_folders) - + def test_compact_user_subscription_folders2(self): usf = UserSubscriptionFolders.objects.get(user=User.objects.all()[0]) usf.folders = '[2, 3, {"Bloglets": [423, 424, 425]}, {"Blogs": [426, 427, 428, 429, 430, {"Photo Blogs": [550, 551, 552, 553, 554, 555, 556]}, {"Photo Blogs": [551, 552, 553, 554, 555, 556]}, {"Travel": [557, 558]}, {"Travel": [557, 559]}, 943, {"Link Blogs": [467, 468, 469, 470, {"Travel": [557, 558]}, {"Travel": [557, 559]}]}, {"Link Blogs": [467, 468, 469, 470, {"Travel": [557, 558]}, {"Travel": [557, 559, 558]}]}]}]' diff --git a/apps/reader/urls.py b/apps/reader/urls.py index 9f829e59e1..aac4fdbbc4 100644 --- a/apps/reader/urls.py +++ b/apps/reader/urls.py @@ -1,68 +1,87 @@ from django.conf.urls import * + from apps.reader import views urlpatterns = [ - url(r'^$', views.index), - url(r'^buster', views.iframe_buster, name='iframe-buster'), - url(r'^login_as', views.login_as, name='login_as'), - url(r'^welcome', views.welcome_req, name='welcome'), - url(r'^logout', views.logout, name='welcome-logout'), - url(r'^login', views.login, name='welcome-login'), - url(r'^autologin/(?P\w+)/(?P\w+)/?', views.autologin, name='autologin'), - url(r'^signup', views.signup, name='welcome-signup'), - url(r'^feeds/?$', views.load_feeds, name='load-feeds'), - url(r'^feed/(?P\d+)', views.load_single_feed, name='load-single-feed'), - url(r'^page/(?P\d+)', views.load_feed_page, name='load-feed-page'), - url(r'^refresh_feed/(?P\d+)', views.refresh_feed, name='refresh-feed'), - url(r'^favicons', views.load_feed_favicons, name='load-feed-favicons'), - url(r'^river_stories_widget', views.load_river_stories_widget, name='load-river-stories-widget'), - url(r'^river_stories', views.load_river_stories__redis, name='load-river-stories'), - url(r'^complete_river', views.complete_river, name='complete-river'), - url(r'^refresh_feeds', views.refresh_feeds, name='refresh-feeds'), - url(r'^interactions_count', views.interactions_count, name='interactions-count'), - url(r'^feed_unread_count', views.feed_unread_count, name='feed-unread-count'), - url(r'^starred_stories', views.load_starred_stories, name='load-starred-stories'), - url(r'^read_stories', views.load_read_stories, name='load-read-stories'), - url(r'^starred_story_hashes', views.starred_story_hashes, name='starred-story-hashes'), - url(r'^starred_rss/(?P\d+)/(?P\w+)/?$', views.starred_stories_rss_feed, name='starred-stories-rss-feed'), - url(r'^starred_rss/(?P\d+)/(?P\w+)/(?P[-\w]+)?/?$', views.starred_stories_rss_feed_tag, name='starred-stories-rss-feed-tag'), - url(r'^folder_rss/(?P\d+)/(?P\w+)/(?P\w+)/(?P[-\w]+)?/?$', views.folder_rss_feed, name='folder-rss-feed'), - url(r'^unread_story_hashes', views.unread_story_hashes, name='unread-story-hashes'), - url(r'^starred_counts', views.starred_counts, name='starred-counts'), - url(r'^mark_all_as_read', views.mark_all_as_read, name='mark-all-as-read'), - url(r'^mark_story_as_read', views.mark_story_as_read, name='mark-story-as-read'), - url(r'^mark_story_hashes_as_read', views.mark_story_hashes_as_read, name='mark-story-hashes-as-read'), - url(r'^mark_feed_stories_as_read', views.mark_feed_stories_as_read, name='mark-feed-stories-as-read'), - url(r'^mark_social_stories_as_read', views.mark_social_stories_as_read, name='mark-social-stories-as-read'), - url(r'^mark_story_as_unread', views.mark_story_as_unread), - url(r'^mark_story_hash_as_unread', views.mark_story_hash_as_unread, name='mark-story-hash-as-unread'), - url(r'^mark_story_as_starred', views.mark_story_as_starred), - url(r'^mark_story_hash_as_starred', views.mark_story_hash_as_starred), - url(r'^mark_story_as_unstarred', views.mark_story_as_unstarred), - url(r'^mark_story_hash_as_unstarred', views.mark_story_hash_as_unstarred), - url(r'^mark_feed_as_read', views.mark_feed_as_read), - url(r'^delete_feed_by_url', views.delete_feed_by_url, name='delete-feed-by-url'), - url(r'^delete_feeds_by_folder', views.delete_feeds_by_folder, name='delete-feeds-by-folder'), - url(r'^delete_feed', views.delete_feed, name='delete-feed'), - url(r'^delete_folder', views.delete_folder, name='delete-folder'), - url(r'^rename_feed', views.rename_feed, name='rename-feed'), - url(r'^rename_folder', views.rename_folder, name='rename-folder'), - url(r'^move_feed_to_folders', views.move_feed_to_folders, name='move-feed-to-folders'), - url(r'^move_feed_to_folder', views.move_feed_to_folder, name='move-feed-to-folder'), - url(r'^move_folder_to_folder', views.move_folder_to_folder, name='move-folder-to-folder'), - url(r'^move_feeds_by_folder_to_folder', views.move_feeds_by_folder_to_folder, name='move-feeds-by-folder-to-folder'), - url(r'^add_url', views.add_url), - url(r'^add_folder', views.add_folder), - url(r'^add_feature', views.add_feature, name='add-feature'), - url(r'^features', views.load_features, name='load-features'), - url(r'^save_feed_order', views.save_feed_order, name='save-feed-order'), - url(r'^feeds_trainer', views.feeds_trainer, name='feeds-trainer'), - url(r'^save_feed_chooser', views.save_feed_chooser, name='save-feed-chooser'), - url(r'^send_story_email', views.send_story_email, name='send-story-email'), - url(r'^retrain_all_sites', views.retrain_all_sites, name='retrain-all-sites'), - url(r'^load_tutorial', views.load_tutorial, name='load-tutorial'), - url(r'^save_search', views.save_search, name='save-search'), - url(r'^delete_search', views.delete_search, name='delete-search'), - url(r'^save_dashboard_river', views.save_dashboard_river, name='save-dashboard-river'), - url(r'^remove_dashboard_river', views.remove_dashboard_river, name='remove-dashboard-river'), + url(r"^$", views.index), + url(r"^buster", views.iframe_buster, name="iframe-buster"), + url(r"^login_as", views.login_as, name="login_as"), + url(r"^welcome", views.welcome_req, name="welcome"), + url(r"^logout", views.logout, name="welcome-logout"), + url(r"^login", views.login, name="welcome-login"), + url(r"^autologin/(?P\w+)/(?P\w+)/?", views.autologin, name="autologin"), + url(r"^signup", views.signup, name="welcome-signup"), + url(r"^feeds/?$", views.load_feeds, name="load-feeds"), + url(r"^feed/(?P\d+)", views.load_single_feed, name="load-single-feed"), + url(r"^page/(?P\d+)", views.load_feed_page, name="load-feed-page"), + url(r"^refresh_feed/(?P\d+)", views.refresh_feed, name="refresh-feed"), + url(r"^favicons", views.load_feed_favicons, name="load-feed-favicons"), + url(r"^river_stories_widget", views.load_river_stories_widget, name="load-river-stories-widget"), + url(r"^river_stories", views.load_river_stories__redis, name="load-river-stories"), + url(r"^complete_river", views.complete_river, name="complete-river"), + url(r"^refresh_feeds", views.refresh_feeds, name="refresh-feeds"), + url(r"^interactions_count", views.interactions_count, name="interactions-count"), + url(r"^feed_unread_count", views.feed_unread_count, name="feed-unread-count"), + url(r"^starred_stories", views.load_starred_stories, name="load-starred-stories"), + url(r"^read_stories", views.load_read_stories, name="load-read-stories"), + url(r"^starred_story_hashes", views.starred_story_hashes, name="starred-story-hashes"), + url( + r"^starred_rss/(?P\d+)/(?P\w+)/?$", + views.starred_stories_rss_feed, + name="starred-stories-rss-feed", + ), + url( + r"^starred_rss/(?P\d+)/(?P\w+)/(?P[-\w]+)?/?$", + views.starred_stories_rss_feed_tag, + name="starred-stories-rss-feed-tag", + ), + url( + r"^folder_rss/(?P\d+)/(?P\w+)/(?P\w+)/(?P[-\w]+)?/?$", + views.folder_rss_feed, + name="folder-rss-feed", + ), + url(r"^unread_story_hashes", views.unread_story_hashes, name="unread-story-hashes"), + url(r"^starred_counts", views.starred_counts, name="starred-counts"), + url(r"^mark_all_as_read", views.mark_all_as_read, name="mark-all-as-read"), + url(r"^mark_story_as_read", views.mark_story_as_read, name="mark-story-as-read"), + url(r"^mark_story_hashes_as_read", views.mark_story_hashes_as_read, name="mark-story-hashes-as-read"), + url(r"^mark_feed_stories_as_read", views.mark_feed_stories_as_read, name="mark-feed-stories-as-read"), + url( + r"^mark_social_stories_as_read", views.mark_social_stories_as_read, name="mark-social-stories-as-read" + ), + url(r"^mark_story_as_unread", views.mark_story_as_unread), + url(r"^mark_story_hash_as_unread", views.mark_story_hash_as_unread, name="mark-story-hash-as-unread"), + url(r"^mark_story_as_starred", views.mark_story_as_starred), + url(r"^mark_story_hash_as_starred", views.mark_story_hash_as_starred), + url(r"^mark_story_as_unstarred", views.mark_story_as_unstarred), + url(r"^mark_story_hash_as_unstarred", views.mark_story_hash_as_unstarred), + url(r"^mark_feed_as_read", views.mark_feed_as_read), + url(r"^delete_feed_by_url", views.delete_feed_by_url, name="delete-feed-by-url"), + url(r"^delete_feeds_by_folder", views.delete_feeds_by_folder, name="delete-feeds-by-folder"), + url(r"^delete_feed", views.delete_feed, name="delete-feed"), + url(r"^delete_folder", views.delete_folder, name="delete-folder"), + url(r"^rename_feed", views.rename_feed, name="rename-feed"), + url(r"^rename_folder", views.rename_folder, name="rename-folder"), + url(r"^move_feed_to_folders", views.move_feed_to_folders, name="move-feed-to-folders"), + url(r"^move_feed_to_folder", views.move_feed_to_folder, name="move-feed-to-folder"), + url(r"^move_folder_to_folder", views.move_folder_to_folder, name="move-folder-to-folder"), + url( + r"^move_feeds_by_folder_to_folder", + views.move_feeds_by_folder_to_folder, + name="move-feeds-by-folder-to-folder", + ), + url(r"^add_url", views.add_url), + url(r"^add_folder", views.add_folder), + url(r"^add_feature", views.add_feature, name="add-feature"), + url(r"^features", views.load_features, name="load-features"), + url(r"^save_feed_order", views.save_feed_order, name="save-feed-order"), + url(r"^feeds_trainer", views.feeds_trainer, name="feeds-trainer"), + url(r"^save_feed_chooser", views.save_feed_chooser, name="save-feed-chooser"), + url(r"^send_story_email", views.send_story_email, name="send-story-email"), + url(r"^retrain_all_sites", views.retrain_all_sites, name="retrain-all-sites"), + url(r"^load_tutorial", views.load_tutorial, name="load-tutorial"), + url(r"^save_search", views.save_search, name="save-search"), + url(r"^delete_search", views.delete_search, name="delete-search"), + url(r"^save_dashboard_river", views.save_dashboard_river, name="save-dashboard-river"), + url(r"^remove_dashboard_river", views.remove_dashboard_river, name="remove-dashboard-river"), ] diff --git a/apps/reader/views.py b/apps/reader/views.py index 29a316ec42..84920ad7bf 100644 --- a/apps/reader/views.py +++ b/apps/reader/views.py @@ -1,3 +1,4 @@ +import http.client import base64 import concurrent import datetime @@ -113,229 +114,234 @@ "brentozar.com", ] ALLOWED_SUBDOMAINS = [ - 'dev', - 'www', - 'hwww', - 'dwww', + "dev", + "www", + "hwww", + "dwww", # 'beta', # Comment to redirect beta -> www, uncomment to allow beta -> staging (+ dns changes) - 'staging', - 'hstaging', - 'discovery', - 'debug', - 'debug3', - 'staging2', - 'staging3', - 'nb', + "staging", + "hstaging", + "discovery", + "debug", + "debug3", + "staging2", + "staging3", + "nb", ] + def get_subdomain(request): - host = request.META.get('HTTP_HOST') + host = request.META.get("HTTP_HOST") if host and host.count(".") >= 2: return host.split(".")[0] else: return None + @never_cache -@render_to('reader/dashboard.xhtml') +@render_to("reader/dashboard.xhtml") def index(request, **kwargs): - subdomain = get_subdomain(request) if request.method == "GET" and subdomain and subdomain not in ALLOWED_SUBDOMAINS: username = request.subdomain or subdomain - if '.' in username: - username = username.split('.')[0] + if "." in username: + username = username.split(".")[0] user = User.objects.filter(username=username) if not user: user = User.objects.filter(username__iexact=username) if user: user = user[0] if not user: - return HttpResponseRedirect('http://%s%s' % ( - Site.objects.get_current().domain, - reverse('index'))) + return HttpResponseRedirect("http://%s%s" % (Site.objects.get_current().domain, reverse("index"))) return load_social_page(request, user_id=user.pk, username=request.subdomain, **kwargs) if request.user.is_anonymous: return welcome(request, **kwargs) else: return dashboard(request, **kwargs) + def dashboard(request, **kwargs): - user = request.user - feed_count = UserSubscription.objects.filter(user=request.user).count() + user = request.user + feed_count = UserSubscription.objects.filter(user=request.user).count() # recommended_feeds = RecommendedFeed.objects.filter(is_public=True, # approved_date__lte=datetime.datetime.now() # ).select_related('feed')[:2] unmoderated_feeds = [] if user.is_staff: - unmoderated_feeds = RecommendedFeed.objects.filter(is_public=False, - declined_date__isnull=True - ).select_related('feed')[:2] - statistics = MStatistics.all() - social_profile = MSocialProfile.get_user(user.pk) - custom_styling = MCustomStyling.get_user(user.pk) - dashboard_rivers = MDashboardRiver.get_user_rivers(user.pk) - preferences = json.decode(user.profile.preferences) - + unmoderated_feeds = RecommendedFeed.objects.filter( + is_public=False, declined_date__isnull=True + ).select_related("feed")[:2] + statistics = MStatistics.all() + social_profile = MSocialProfile.get_user(user.pk) + custom_styling = MCustomStyling.get_user(user.pk) + dashboard_rivers = MDashboardRiver.get_user_rivers(user.pk) + preferences = json.decode(user.profile.preferences) + if not user.is_active: - url = "https://%s%s" % (Site.objects.get_current().domain, - reverse('stripe-form')) + url = "https://%s%s" % (Site.objects.get_current().domain, reverse("stripe-form")) return HttpResponseRedirect(url) logging.user(request, "~FBLoading dashboard") return { - 'user_profile' : user.profile, - 'preferences' : preferences, - 'feed_count' : feed_count, - 'custom_styling' : custom_styling, - 'dashboard_rivers' : dashboard_rivers, - 'account_images' : list(range(1, 4)), + "user_profile": user.profile, + "preferences": preferences, + "feed_count": feed_count, + "custom_styling": custom_styling, + "dashboard_rivers": dashboard_rivers, + "account_images": list(range(1, 4)), # 'recommended_feeds' : recommended_feeds, - 'unmoderated_feeds' : unmoderated_feeds, - 'statistics' : statistics, - 'social_profile' : social_profile, - 'debug' : settings.DEBUG, - 'debug_assets' : settings.DEBUG_ASSETS, + "unmoderated_feeds": unmoderated_feeds, + "statistics": statistics, + "social_profile": social_profile, + "debug": settings.DEBUG, + "debug_assets": settings.DEBUG_ASSETS, }, "reader/dashboard.xhtml" -@render_to('reader/dashboard.xhtml') + +@render_to("reader/dashboard.xhtml") def welcome_req(request, **kwargs): return welcome(request, **kwargs) + def welcome(request, **kwargs): - user = get_user(request) - statistics = MStatistics.all() - social_profile = MSocialProfile.get_user(user.pk) - + user = get_user(request) + statistics = MStatistics.all() + social_profile = MSocialProfile.get_user(user.pk) + if request.method == "POST": - if request.POST.get('submit', '').startswith('log'): - login_form = LoginForm(request.POST, prefix='login') - signup_form = SignupForm(prefix='signup') + if request.POST.get("submit", "").startswith("log"): + login_form = LoginForm(request.POST, prefix="login") + signup_form = SignupForm(prefix="signup") else: - signup_form = SignupForm(request.POST, prefix='signup') - return { - "form": signup_form - }, "accounts/signup.html" + signup_form = SignupForm(request.POST, prefix="signup") + return {"form": signup_form}, "accounts/signup.html" else: - login_form = LoginForm(prefix='login') - signup_form = SignupForm(prefix='signup') - + login_form = LoginForm(prefix="login") + signup_form = SignupForm(prefix="signup") + logging.user(request, "~FBLoading welcome") - + return { - 'user_profile' : hasattr(user, 'profile') and user.profile, - 'login_form' : login_form, - 'signup_form' : signup_form, - 'statistics' : statistics, - 'social_profile' : social_profile, - 'post_request' : request.method == 'POST', + "user_profile": hasattr(user, "profile") and user.profile, + "login_form": login_form, + "signup_form": signup_form, + "statistics": statistics, + "social_profile": social_profile, + "post_request": request.method == "POST", }, "reader/welcome.xhtml" + @never_cache def login(request): code = -1 message = "" if request.method == "POST": - form = LoginForm(request.POST, prefix='login') + form = LoginForm(request.POST, prefix="login") if form.is_valid(): - login_user(request, form.get_user(), backend='django.contrib.auth.backends.ModelBackend') - if request.POST.get('api'): + login_user(request, form.get_user(), backend="django.contrib.auth.backends.ModelBackend") + if request.POST.get("api"): logging.user(form.get_user(), "~FG~BB~SKiPhone Login~FW") code = 1 else: logging.user(form.get_user(), "~FG~BBLogin~FW") - next_url = request.POST.get('next', '') + next_url = request.POST.get("next", "") if next_url: return HttpResponseRedirect(next_url) - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse("index")) else: message = list(form.errors.items())[0][1][0] - if request.POST.get('api'): - return HttpResponse(json.encode(dict(code=code, message=message)), content_type='application/json') + if request.POST.get("api"): + return HttpResponse(json.encode(dict(code=code, message=message)), content_type="application/json") else: return index(request) - + + @never_cache -@render_to('accounts/signup.html') +@render_to("accounts/signup.html") def signup(request): if request.method == "POST": if settings.ENFORCE_SIGNUP_CAPTCHA: - signup_form = SignupForm(request.POST, prefix='signup') - return { - "form": signup_form - } + signup_form = SignupForm(request.POST, prefix="signup") + return {"form": signup_form} - form = SignupForm(prefix='signup', data=request.POST) + form = SignupForm(prefix="signup", data=request.POST) if form.is_valid(): new_user = form.save() - login_user(request, new_user, backend='django.contrib.auth.backends.ModelBackend') + login_user(request, new_user, backend="django.contrib.auth.backends.ModelBackend") logging.user(new_user, "~FG~SB~BBNEW SIGNUP: ~FW%s" % new_user.email) if not new_user.is_active: - url = "https://%s%s" % (Site.objects.get_current().domain, - reverse('stripe-form')) + url = "https://%s%s" % (Site.objects.get_current().domain, reverse("stripe-form")) return HttpResponseRedirect(url) else: - return HttpResponseRedirect(reverse('index')) - + return HttpResponseRedirect(reverse("index")) + return index(request) - + + @never_cache def logout(request): logging.user(request, "~FG~BBLogout~FW") logout_user(request) - - if request.GET.get('api'): - return HttpResponse(json.encode(dict(code=1)), content_type='application/json') + + if request.GET.get("api"): + return HttpResponse(json.encode(dict(code=1)), content_type="application/json") else: - return HttpResponseRedirect(reverse('index')) + return HttpResponseRedirect(reverse("index")) + def autologin(request, username, secret): - next = request.GET.get('next', '') - + next = request.GET.get("next", "") + if not username or not secret: return HttpResponseForbidden() - + profile = Profile.objects.filter(user__username=username, secret_token=secret) if not profile: return HttpResponseForbidden() user = profile[0].user user.backend = settings.AUTHENTICATION_BACKENDS[0] - login_user(request, user, backend='django.contrib.auth.backends.ModelBackend') - logging.user(user, "~FG~BB~SKAuto-Login. Next stop: %s~FW" % (next if next else 'Homepage',)) - - if next and not next.startswith('/'): - next = '?next=' + next - return HttpResponseRedirect(reverse('index') + next) + login_user(request, user, backend="django.contrib.auth.backends.ModelBackend") + logging.user(user, "~FG~BB~SKAuto-Login. Next stop: %s~FW" % (next if next else "Homepage",)) + + if next and not next.startswith("/"): + next = "?next=" + next + return HttpResponseRedirect(reverse("index") + next) elif next: return HttpResponseRedirect(next) else: - return HttpResponseRedirect(reverse('index')) - + return HttpResponseRedirect(reverse("index")) + + @ratelimit(minutes=1, requests=60) @never_cache @json.json_view def load_feeds(request): - user = get_user(request) - feeds = {} - include_favicons = is_true(request.GET.get('include_favicons', False)) - flat = is_true(request.GET.get('flat', False)) - update_counts = is_true(request.GET.get('update_counts', True)) - version = int(request.GET.get('v', 1)) - - if include_favicons == 'false': include_favicons = False - if update_counts == 'false': update_counts = False - if flat == 'false': flat = False - - if flat: return load_feeds_flat(request) + user = get_user(request) + feeds = {} + include_favicons = is_true(request.GET.get("include_favicons", False)) + flat = is_true(request.GET.get("flat", False)) + update_counts = is_true(request.GET.get("update_counts", True)) + version = int(request.GET.get("v", 1)) + + if include_favicons == "false": + include_favicons = False + if update_counts == "false": + update_counts = False + if flat == "false": + flat = False + + if flat: + return load_feeds_flat(request) platform = extract_user_agent(request) - if platform in ['iPhone', 'iPad', 'Androd']: + if platform in ["iPhone", "iPad", "Androd"]: # Remove this check once the iOS and Android updates go out which have update_counts=False # and then guarantee a refresh_feeds call update_counts = False - + try: folders = UserSubscriptionFolders.objects.get(user=user) except UserSubscriptionFolders.DoesNotExist: @@ -344,10 +350,10 @@ def load_feeds(request): except UserSubscriptionFolders.MultipleObjectsReturned: UserSubscriptionFolders.objects.filter(user=user)[1:].delete() folders = UserSubscriptionFolders.objects.get(user=user) - - user_subs = UserSubscription.objects.select_related('feed').filter(user=user) + + user_subs = UserSubscription.objects.select_related("feed").filter(user=user) notifications = MUserFeedNotification.feeds_for_user(user.pk) - + day_ago = datetime.datetime.now() - datetime.timedelta(days=1) scheduled_feeds = [] for sub in user_subs: @@ -355,8 +361,9 @@ def load_feeds(request): if update_counts and sub.needs_unread_recalc: sub.calculate_feed_scores(silent=True) feeds[pk] = sub.canonical(include_favicon=include_favicons) - - if not sub.active: continue + + if not sub.active: + continue if pk in notifications: feeds[pk].update(notifications[pk]) if not sub.feed.active and not sub.feed.has_feed_exception: @@ -365,22 +372,24 @@ def load_feeds(request): scheduled_feeds.append(sub.feed.pk) elif sub.feed.next_scheduled_update < day_ago: scheduled_feeds.append(sub.feed.pk) - + if len(scheduled_feeds) > 0 and request.user.is_authenticated: - logging.user(request, "~SN~FMTasking the scheduling immediate fetch of ~SB%s~SN feeds..." % - len(scheduled_feeds)) + logging.user( + request, + "~SN~FMTasking the scheduling immediate fetch of ~SB%s~SN feeds..." % len(scheduled_feeds), + ) ScheduleImmediateFetches.apply_async(kwargs=dict(feed_ids=scheduled_feeds, user_id=user.pk)) starred_counts, starred_count = MStarredStoryCounts.user_counts(user.pk, include_total=True) if not starred_count and len(starred_counts): starred_count = MStarredStory.objects(user_id=user.pk).count() - + saved_searches = MSavedSearch.user_searches(user.pk) - + social_params = { - 'user_id': user.pk, - 'include_favicon': include_favicons, - 'update_counts': update_counts, + "user_id": user.pk, + "include_favicon": include_favicons, + "update_counts": update_counts, } social_feeds = MSocialSubscription.feeds(**social_params) social_profile = MSocialProfile.profile(user.pk) @@ -391,74 +400,81 @@ def load_feeds(request): if not user_subs: categories = MCategory.serialize() - logging.user(request, "~FB~SBLoading ~FY%s~FB/~FM%s~FB feeds/socials%s" % ( - len(list(feeds.keys())), len(social_feeds), '. ~FCUpdating counts.' if update_counts else '')) + logging.user( + request, + "~FB~SBLoading ~FY%s~FB/~FM%s~FB feeds/socials%s" + % (len(list(feeds.keys())), len(social_feeds), ". ~FCUpdating counts." if update_counts else ""), + ) data = { - 'feeds': list(feeds.values()) if version == 2 else feeds, - 'social_feeds': social_feeds, - 'social_profile': social_profile, - 'social_services': social_services, - 'user_profile': user.profile, + "feeds": list(feeds.values()) if version == 2 else feeds, + "social_feeds": social_feeds, + "social_profile": social_profile, + "social_services": social_services, + "user_profile": user.profile, "is_staff": user.is_staff, - 'user_id': user.pk, - 'folders': json.decode(folders.folders), - 'starred_count': starred_count, - 'starred_counts': starred_counts, - 'saved_searches': saved_searches, - 'dashboard_rivers': dashboard_rivers, - 'categories': categories, - 'share_ext_token': user.profile.secret_token, + "user_id": user.pk, + "folders": json.decode(folders.folders), + "starred_count": starred_count, + "starred_counts": starred_counts, + "saved_searches": saved_searches, + "dashboard_rivers": dashboard_rivers, + "categories": categories, + "share_ext_token": user.profile.secret_token, } return data + @json.json_view def load_feed_favicons(request): user = get_user(request) - feed_ids = request.GET.getlist('feed_ids') or request.GET.getlist('feed_ids[]') - + feed_ids = request.GET.getlist("feed_ids") or request.GET.getlist("feed_ids[]") + if not feed_ids: - user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True) - feed_ids = [sub['feed__pk'] for sub in user_subs.values('feed__pk')] + user_subs = UserSubscription.objects.select_related("feed").filter(user=user, active=True) + feed_ids = [sub["feed__pk"] for sub in user_subs.values("feed__pk")] feed_icons = dict([(i.feed_id, i.data) for i in MFeedIcon.objects(feed_id__in=feed_ids)]) - + return feed_icons + def load_feeds_flat(request): user = request.user - include_favicons = is_true(request.GET.get('include_favicons', False)) - update_counts = is_true(request.GET.get('update_counts', True)) - include_inactive = is_true(request.GET.get('include_inactive', False)) - background_ios = is_true(request.GET.get('background_ios', False)) - + include_favicons = is_true(request.GET.get("include_favicons", False)) + update_counts = is_true(request.GET.get("update_counts", True)) + include_inactive = is_true(request.GET.get("include_inactive", False)) + background_ios = is_true(request.GET.get("background_ios", False)) + feeds = {} inactive_feeds = {} day_ago = datetime.datetime.now() - datetime.timedelta(days=1) scheduled_feeds = [] - iphone_version = "2.1" # Preserved forever. Don't change. + iphone_version = "2.1" # Preserved forever. Don't change. latest_ios_build = "52" latest_ios_version = "5.0.0b2" - - if include_favicons == 'false': include_favicons = False - if update_counts == 'false': update_counts = False - + + if include_favicons == "false": + include_favicons = False + if update_counts == "false": + update_counts = False + if not user.is_authenticated: return HttpResponseForbidden() - + try: folders = UserSubscriptionFolders.objects.get(user=user) except UserSubscriptionFolders.DoesNotExist: folders = [] - - user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True) + + user_subs = UserSubscription.objects.select_related("feed").filter(user=user, active=True) notifications = MUserFeedNotification.feeds_for_user(user.pk) if not user_subs and folders: folders.auto_activate() - user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True) + user_subs = UserSubscription.objects.select_related("feed").filter(user=user, active=True) if include_inactive: - inactive_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=False) - + inactive_subs = UserSubscription.objects.select_related("feed").filter(user=user, active=False) + for sub in user_subs: pk = sub.feed_id if update_counts and sub.needs_unread_recalc: @@ -472,28 +488,28 @@ def load_feeds_flat(request): scheduled_feeds.append(sub.feed.pk) if pk in notifications: feeds[pk].update(notifications[pk]) - - + if include_inactive: for sub in inactive_subs: inactive_feeds[sub.feed_id] = sub.canonical(include_favicon=include_favicons) - + if len(scheduled_feeds) > 0 and request.user.is_authenticated: - logging.user(request, "~SN~FMTasking the scheduling immediate fetch of ~SB%s~SN feeds..." % - len(scheduled_feeds)) + logging.user( + request, + "~SN~FMTasking the scheduling immediate fetch of ~SB%s~SN feeds..." % len(scheduled_feeds), + ) ScheduleImmediateFetches.apply_async(kwargs=dict(feed_ids=scheduled_feeds, user_id=user.pk)) - + flat_folders = [] flat_folders_with_inactive = [] if folders: flat_folders = folders.flatten_folders(feeds=feeds) - flat_folders_with_inactive = folders.flatten_folders(feeds=feeds, - inactive_feeds=inactive_feeds) - + flat_folders_with_inactive = folders.flatten_folders(feeds=feeds, inactive_feeds=inactive_feeds) + social_params = { - 'user_id': user.pk, - 'include_favicon': include_favicons, - 'update_counts': update_counts, + "user_id": user.pk, + "include_favicon": include_favicons, + "update_counts": update_counts, } social_feeds = MSocialSubscription.feeds(**social_params) social_profile = MSocialProfile.profile(user.pk) @@ -508,13 +524,21 @@ def load_feeds_flat(request): saved_searches = MSavedSearch.user_searches(user.pk) - logging.user(request, "~FB~SBLoading ~FY%s~FB/~FM%s~FB/~FR%s~FB feeds/socials/inactive ~FMflat~FB%s%s" % ( - len(list(feeds.keys())), len(social_feeds), len(inactive_feeds), '. ~FCUpdating counts.' if update_counts else '', - ' ~BB(background fetch)' if background_ios else '')) + logging.user( + request, + "~FB~SBLoading ~FY%s~FB/~FM%s~FB/~FR%s~FB feeds/socials/inactive ~FMflat~FB%s%s" + % ( + len(list(feeds.keys())), + len(social_feeds), + len(inactive_feeds), + ". ~FCUpdating counts." if update_counts else "", + " ~BB(background fetch)" if background_ios else "", + ), + ) data = { - "flat_folders": flat_folders, - "flat_folders_with_inactive": flat_folders_with_inactive, + "flat_folders": flat_folders, + "flat_folders_with_inactive": flat_folders_with_inactive, "feeds": feeds, "inactive_feeds": inactive_feeds if include_inactive else {"0": "Include `include_inactive=true`"}, "social_feeds": social_feeds, @@ -528,20 +552,22 @@ def load_feeds_flat(request): "latest_ios_build": latest_ios_build, "latest_ios_version": latest_ios_version, "categories": categories, - 'starred_count': starred_count, - 'starred_counts': starred_counts, - 'saved_searches': saved_searches, - 'share_ext_token': user.profile.secret_token, + "starred_count": starred_count, + "starred_counts": starred_counts, + "saved_searches": saved_searches, + "share_ext_token": user.profile.secret_token, } return data + class ratelimit_refresh_feeds(ratelimit): def should_ratelimit(self, request): - feed_ids = request.POST.getlist('feed_id') or request.POST.getlist('feed_id[]') + feed_ids = request.POST.getlist("feed_id") or request.POST.getlist("feed_id[]") if len(feed_ids) == 1: return False return True + @ratelimit_refresh_feeds(minutes=1, requests=30) @never_cache @json.json_view @@ -550,33 +576,34 @@ def refresh_feeds(request): start = datetime.datetime.now() start_time = time.time() user = get_user(request) - feed_ids = get_post.getlist('feed_id') or get_post.getlist('feed_id[]') - check_fetch_status = get_post.get('check_fetch_status') - favicons_fetching = get_post.getlist('favicons_fetching') or get_post.getlist('favicons_fetching[]') - social_feed_ids = [feed_id for feed_id in feed_ids if 'social:' in feed_id] + feed_ids = get_post.getlist("feed_id") or get_post.getlist("feed_id[]") + check_fetch_status = get_post.get("check_fetch_status") + favicons_fetching = get_post.getlist("favicons_fetching") or get_post.getlist("favicons_fetching[]") + social_feed_ids = [feed_id for feed_id in feed_ids if "social:" in feed_id] feed_ids = list(set(feed_ids) - set(social_feed_ids)) - + feeds = {} if feed_ids or (not social_feed_ids and not feed_ids): - feeds = UserSubscription.feeds_with_updated_counts(user, feed_ids=feed_ids, - check_fetch_status=check_fetch_status) + feeds = UserSubscription.feeds_with_updated_counts( + user, feed_ids=feed_ids, check_fetch_status=check_fetch_status + ) checkpoint1 = datetime.datetime.now() social_feeds = {} if social_feed_ids or (not social_feed_ids and not feed_ids): social_feeds = MSocialSubscription.feeds_with_updated_counts(user, social_feed_ids=social_feed_ids) checkpoint2 = datetime.datetime.now() - + favicons_fetching = [int(f) for f in favicons_fetching if f] feed_icons = {} if favicons_fetching: feed_icons = dict([(i.feed_id, i) for i in MFeedIcon.objects(feed_id__in=favicons_fetching)]) for feed_id, feed in list(feeds.items()): if feed_id in favicons_fetching and feed_id in feed_icons: - feeds[feed_id]['favicon'] = feed_icons[feed_id].data - feeds[feed_id]['favicon_color'] = feed_icons[feed_id].color - feeds[feed_id]['favicon_fetching'] = feed.get('favicon_fetching') + feeds[feed_id]["favicon"] = feed_icons[feed_id].data + feeds[feed_id]["favicon_color"] = feed_icons[feed_id].color + feeds[feed_id]["favicon_fetching"] = feed.get("favicon_fetching") - user_subs = UserSubscription.objects.filter(user=user, active=True).only('feed') + user_subs = UserSubscription.objects.filter(user=user, active=True).only("feed") sub_feed_ids = [s.feed_id for s in user_subs] if favicons_fetching: @@ -586,15 +613,15 @@ def refresh_feeds(request): if duplicate_feeds and duplicate_feeds[0].feed.pk in feeds: feeds[moved_feed_id] = feeds[duplicate_feeds[0].feed_id] - feeds[moved_feed_id]['dupe_feed_id'] = duplicate_feeds[0].feed_id - + feeds[moved_feed_id]["dupe_feed_id"] = duplicate_feeds[0].feed_id + if check_fetch_status: missing_feed_ids = list(set(feed_ids) - set(sub_feed_ids)) if missing_feed_ids: duplicate_feeds = DuplicateFeed.objects.filter(duplicate_feed_id__in=missing_feed_ids) for duplicate_feed in duplicate_feeds: - feeds[duplicate_feed.duplicate_feed_id] = {'id': duplicate_feed.feed_id} - + feeds[duplicate_feed.duplicate_feed_id] = {"id": duplicate_feed.feed_id} + interactions_count = MInteraction.user_unread_count(user.pk) if True or settings.DEBUG or check_fetch_status: @@ -602,21 +629,28 @@ def refresh_feeds(request): extra_fetch = "" if check_fetch_status or favicons_fetching: extra_fetch = "(%s/%s)" % (check_fetch_status, len(favicons_fetching)) - logging.user(request, "~FBRefreshing %s+%s feeds %s (%.4s/%.4s/%.4s)" % ( - len(list(feeds.keys())), len(list(social_feeds.keys())), extra_fetch, - (checkpoint1-start).total_seconds(), - (checkpoint2-start).total_seconds(), - (end-start).total_seconds(), - )) - - MAnalyticsLoader.add(page_load=time.time()-start_time) - + logging.user( + request, + "~FBRefreshing %s+%s feeds %s (%.4s/%.4s/%.4s)" + % ( + len(list(feeds.keys())), + len(list(social_feeds.keys())), + extra_fetch, + (checkpoint1 - start).total_seconds(), + (checkpoint2 - start).total_seconds(), + (end - start).total_seconds(), + ), + ) + + MAnalyticsLoader.add(page_load=time.time() - start_time) + return { - 'feeds': feeds, - 'social_feeds': social_feeds, - 'interactions_count': interactions_count, + "feeds": feeds, + "social_feeds": social_feeds, + "interactions_count": interactions_count, } + @json.json_view def interactions_count(request): user = get_user(request) @@ -624,9 +658,10 @@ def interactions_count(request): interactions_count = MInteraction.user_unread_count(user.pk) return { - 'interactions_count': interactions_count, + "interactions_count": interactions_count, } - + + @never_cache @ajax_login_required @json.json_view @@ -634,12 +669,12 @@ def feed_unread_count(request): get_post = getattr(request, request.method) start = time.time() user = request.user - feed_ids = get_post.getlist('feed_id') or get_post.getlist('feed_id[]') - - force = request.GET.get('force', False) - social_feed_ids = [feed_id for feed_id in feed_ids if 'social:' in feed_id] + feed_ids = get_post.getlist("feed_id") or get_post.getlist("feed_id[]") + + force = request.GET.get("force", False) + social_feed_ids = [feed_id for feed_id in feed_ids if "social:" in feed_id] feed_ids = list(set(feed_ids) - set(social_feed_ids)) - + feeds = {} if feed_ids: feeds = UserSubscription.feeds_with_updated_counts(user, feed_ids=feed_ids, force=force) @@ -647,71 +682,74 @@ def feed_unread_count(request): social_feeds = {} if social_feed_ids: social_feeds = MSocialSubscription.feeds_with_updated_counts(user, social_feed_ids=social_feed_ids) - + if len(feed_ids) == 1: if settings.DEBUG: feed_title = Feed.get_by_id(feed_ids[0]).feed_title else: feed_title = feed_ids[0] elif len(social_feed_ids) == 1: - social_profile = MSocialProfile.objects.get(user_id=social_feed_ids[0].replace('social:', '')) + social_profile = MSocialProfile.objects.get(user_id=social_feed_ids[0].replace("social:", "")) feed_title = social_profile.user.username if social_profile.user else "[deleted]" else: feed_title = "%s feeds" % (len(feeds) + len(social_feeds)) logging.user(request, "~FBUpdating unread count on: %s" % feed_title) - MAnalyticsLoader.add(page_load=time.time()-start) - - return {'feeds': feeds, 'social_feeds': social_feeds} - + MAnalyticsLoader.add(page_load=time.time() - start) + + return {"feeds": feeds, "social_feeds": social_feeds} + + def refresh_feed(request, feed_id): start = time.time() user = get_user(request) feed = get_object_or_404(Feed, pk=feed_id) - + feed = feed.update(force=True, compute_scores=False) usersub = UserSubscription.objects.get(user=user, feed=feed) usersub.calculate_feed_scores(silent=False) - + logging.user(request, "~FBRefreshing feed: %s" % feed) - MAnalyticsLoader.add(page_load=time.time()-start) - + MAnalyticsLoader.add(page_load=time.time() - start) + return load_single_feed(request, feed_id) - + + @never_cache @json.json_view def load_single_feed(request, feed_id): - start = time.time() - user = get_user(request) + start = time.time() + user = get_user(request) # offset = int(request.GET.get('offset', 0)) # limit = int(request.GET.get('limit', 6)) - limit = 6 - page = int(request.GET.get('page', 1)) - delay = int(request.GET.get('delay', 0)) - offset = limit * (page-1) - order = request.GET.get('order', 'newest') - read_filter = request.GET.get('read_filter', 'all') - query = request.GET.get('query', '').strip() - include_story_content = is_true(request.GET.get('include_story_content', True)) - include_hidden = is_true(request.GET.get('include_hidden', False)) - include_feeds = is_true(request.GET.get('include_feeds', False)) - message = None - user_search = None - + limit = 6 + page = int(request.GET.get("page", 1)) + delay = int(request.GET.get("delay", 0)) + offset = limit * (page - 1) + order = request.GET.get("order", "newest") + read_filter = request.GET.get("read_filter", "all") + query = request.GET.get("query", "").strip() + include_story_content = is_true(request.GET.get("include_story_content", True)) + include_hidden = is_true(request.GET.get("include_hidden", False)) + include_feeds = is_true(request.GET.get("include_feeds", False)) + message = None + user_search = None + dupe_feed_id = None user_profiles = [] now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) - if not feed_id: raise Http404 + if not feed_id: + raise Http404 - feed_address = request.GET.get('feed_address') + feed_address = request.GET.get("feed_address") feed = Feed.get_by_id(feed_id, feed_address=feed_address) if not feed: raise Http404 - + try: usersub = UserSubscription.objects.get(user=user, feed=feed) except UserSubscription.DoesNotExist: usersub = None - + if feed.is_newsletter and not usersub: # User must be subscribed to a newsletter in order to read it raise Http404 @@ -719,11 +757,11 @@ def load_single_feed(request, feed_id): if feed.num_subscribers == 1 and not usersub and not user.is_staff: # This feed could be private so user must be subscribed in order to read it raise Http404 - + if page > 400: logging.user(request, "~BR~FK~SBOver page 400 on single feed: %s" % page) raise Http404 - + if query: if user.profile.is_premium: user_search = MUserSearch.get_user(user.pk) @@ -732,178 +770,199 @@ def load_single_feed(request, feed_id): else: stories = [] message = "You must be a premium subscriber to search." - elif read_filter == 'starred': - mstories = MStarredStory.objects( - user_id=user.pk, - story_feed_id=feed_id - ).order_by('%sstarred_date' % ('-' if order == 'newest' else ''))[offset:offset+limit] - stories = Feed.format_stories(mstories) - elif usersub and read_filter == 'unread': + elif read_filter == "starred": + mstories = MStarredStory.objects(user_id=user.pk, story_feed_id=feed_id).order_by( + "%sstarred_date" % ("-" if order == "newest" else "") + )[offset : offset + limit] + stories = Feed.format_stories(mstories) + elif usersub and read_filter == "unread": stories = usersub.get_stories(order=order, read_filter=read_filter, offset=offset, limit=limit) else: stories = feed.get_stories(offset, limit, order=order) - + checkpoint1 = time.time() - + try: stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk) except redis.ConnectionError: logging.user(request, "~BR~FK~SBRedis is unavailable for shared stories.") checkpoint2 = time.time() - + # Get intelligence classifier for user - + if usersub and usersub.is_trained: - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, feed_id=feed_id, social_user_id=0)) + classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, feed_id=feed_id, social_user_id=0)) classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, feed_id=feed_id)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, feed_id=feed_id)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, feed_id=feed_id)) + classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, feed_id=feed_id)) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, feed_id=feed_id)) else: classifier_feeds = [] classifier_authors = [] classifier_titles = [] classifier_tags = [] - classifiers = get_classifiers_for_user(user, feed_id=feed_id, - classifier_feeds=classifier_feeds, - classifier_authors=classifier_authors, - classifier_titles=classifier_titles, - classifier_tags=classifier_tags) + classifiers = get_classifiers_for_user( + user, + feed_id=feed_id, + classifier_feeds=classifier_feeds, + classifier_authors=classifier_authors, + classifier_titles=classifier_titles, + classifier_tags=classifier_tags, + ) checkpoint3 = time.time() - + unread_story_hashes = [] if stories: - if (read_filter == 'all' or query) and usersub: - unread_story_hashes = UserSubscription.story_hashes(user.pk, read_filter='unread', - feed_ids=[usersub.feed_id], - usersubs=[usersub], - cutoff_date=user.profile.unread_cutoff) - story_hashes = [story['story_hash'] for story in stories if story['story_hash']] - starred_stories = MStarredStory.objects(user_id=user.pk, - story_feed_id=feed.pk, - story_hash__in=story_hashes)\ - .hint([('user_id', 1), ('story_hash', 1)]) + if (read_filter == "all" or query) and usersub: + unread_story_hashes = UserSubscription.story_hashes( + user.pk, + read_filter="unread", + feed_ids=[usersub.feed_id], + usersubs=[usersub], + cutoff_date=user.profile.unread_cutoff, + ) + story_hashes = [story["story_hash"] for story in stories if story["story_hash"]] + starred_stories = MStarredStory.objects( + user_id=user.pk, story_feed_id=feed.pk, story_hash__in=story_hashes + ).hint([("user_id", 1), ("story_hash", 1)]) shared_story_hashes = MSharedStory.check_shared_story_hashes(user.pk, story_hashes) shared_stories = [] if shared_story_hashes: - shared_stories = MSharedStory.objects(user_id=user.pk, - story_hash__in=shared_story_hashes)\ - .hint([('story_hash', 1)])\ - .only('story_hash', 'shared_date', 'comments') - starred_stories = dict([(story.story_hash, story) - for story in starred_stories]) - shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, - comments=story.comments)) - for story in shared_stories]) - + shared_stories = ( + MSharedStory.objects(user_id=user.pk, story_hash__in=shared_story_hashes) + .hint([("story_hash", 1)]) + .only("story_hash", "shared_date", "comments") + ) + starred_stories = dict([(story.story_hash, story) for story in starred_stories]) + shared_stories = dict( + [ + (story.story_hash, dict(shared_date=story.shared_date, comments=story.comments)) + for story in shared_stories + ] + ) + checkpoint4 = time.time() - + for story in stories: if not include_story_content: - del story['story_content'] - story_date = localtime_for_timezone(story['story_date'], user.profile.timezone) + del story["story_content"] + story_date = localtime_for_timezone(story["story_date"], user.profile.timezone) nowtz = localtime_for_timezone(now, user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz) + story["short_parsed_date"] = format_story_link_date__short(story_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(story_date, nowtz) if usersub: - story['read_status'] = 1 - if not user.profile.is_archive and story['story_date'] < user.profile.unread_cutoff: - story['read_status'] = 1 - elif (read_filter == 'all' or query) and usersub: - story['read_status'] = 1 if story['story_hash'] not in unread_story_hashes else 0 - elif read_filter == 'unread' and usersub: - story['read_status'] = 0 - if story['story_hash'] in starred_stories: - story['starred'] = True - starred_story = Feed.format_story(starred_stories[story['story_hash']]) - starred_date = localtime_for_timezone(starred_story['starred_date'], - user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, now) - story['starred_timestamp'] = int(starred_date.timestamp()) - story['user_tags'] = starred_story['user_tags'] - story['user_notes'] = starred_story['user_notes'] - story['highlights'] = starred_story['highlights'] - if story['story_hash'] in shared_stories: - story['shared'] = True - shared_date = localtime_for_timezone(shared_stories[story['story_hash']]['shared_date'], - user.profile.timezone) - story['shared_date'] = format_story_link_date__long(shared_date, now) - story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments']) + story["read_status"] = 1 + if not user.profile.is_archive and story["story_date"] < user.profile.unread_cutoff: + story["read_status"] = 1 + elif (read_filter == "all" or query) and usersub: + story["read_status"] = 1 if story["story_hash"] not in unread_story_hashes else 0 + elif read_filter == "unread" and usersub: + story["read_status"] = 0 + if story["story_hash"] in starred_stories: + story["starred"] = True + starred_story = Feed.format_story(starred_stories[story["story_hash"]]) + starred_date = localtime_for_timezone(starred_story["starred_date"], user.profile.timezone) + story["starred_date"] = format_story_link_date__long(starred_date, now) + story["starred_timestamp"] = int(starred_date.timestamp()) + story["user_tags"] = starred_story["user_tags"] + story["user_notes"] = starred_story["user_notes"] + story["highlights"] = starred_story["highlights"] + if story["story_hash"] in shared_stories: + story["shared"] = True + shared_date = localtime_for_timezone( + shared_stories[story["story_hash"]]["shared_date"], user.profile.timezone + ) + story["shared_date"] = format_story_link_date__long(shared_date, now) + story["shared_comments"] = strip_tags(shared_stories[story["story_hash"]]["comments"]) else: - story['read_status'] = 1 - story['intelligence'] = { - 'feed': apply_classifier_feeds(classifier_feeds, feed), - 'author': apply_classifier_authors(classifier_authors, story), - 'tags': apply_classifier_tags(classifier_tags, story), - 'title': apply_classifier_titles(classifier_titles, story), + story["read_status"] = 1 + story["intelligence"] = { + "feed": apply_classifier_feeds(classifier_feeds, feed), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } - story['score'] = UserSubscription.score_story(story['intelligence']) - + story["score"] = UserSubscription.score_story(story["intelligence"]) + # Intelligence feed_tags = json.decode(feed.data.popular_tags) if feed.data.popular_tags else [] feed_authors = json.decode(feed.data.popular_authors) if feed.data.popular_authors else [] - + if include_feeds: - feeds = Feed.objects.filter(pk__in=set([story['story_feed_id'] for story in stories])) + feeds = Feed.objects.filter(pk__in=set([story["story_feed_id"] for story in stories])) feeds = [f.canonical(include_favicon=False) for f in feeds] - + if usersub: usersub.feed_opens += 1 usersub.needs_unread_recalc = True try: - usersub.save(update_fields=['feed_opens', 'needs_unread_recalc']) + usersub.save(update_fields=["feed_opens", "needs_unread_recalc"]) except DatabaseError as e: logging.user(request, f"~BR~FK~SBNo changes in usersub, ignoring... {e}") - - diff1 = checkpoint1-start - diff2 = checkpoint2-start - diff3 = checkpoint3-start - diff4 = checkpoint4-start - timediff = time.time()-start + + diff1 = checkpoint1 - start + diff2 = checkpoint2 - start + diff3 = checkpoint3 - start + diff4 = checkpoint4 - start + timediff = time.time() - start last_update = relative_timesince(feed.last_update) time_breakdown = "" if timediff > 1 or settings.DEBUG: - time_breakdown = "~SN~FR(~SB%.4s/%.4s/%.4s/%.4s~SN)" % ( - diff1, diff2, diff3, diff4) - + time_breakdown = "~SN~FR(~SB%.4s/%.4s/%.4s/%.4s~SN)" % (diff1, diff2, diff3, diff4) + search_log = "~SN~FG(~SB%s~SN) " % query if query else "" - logging.user(request, "~FYLoading feed: ~SB%s%s (%s/%s) %s%s" % ( - feed.feed_title[:22], ('~SN/p%s' % page) if page > 1 else '', order, read_filter, search_log, time_breakdown)) - + logging.user( + request, + "~FYLoading feed: ~SB%s%s (%s/%s) %s%s" + % ( + feed.feed_title[:22], + ("~SN/p%s" % page) if page > 1 else "", + order, + read_filter, + search_log, + time_breakdown, + ), + ) + MAnalyticsLoader.add(page_load=timediff) - if hasattr(request, 'start_time'): + if hasattr(request, "start_time"): seconds = time.time() - request.start_time - RStats.add('page_load', duration=seconds) + RStats.add("page_load", duration=seconds) if not include_hidden: hidden_stories_removed = 0 new_stories = [] for story in stories: - if story['score'] >= 0: + if story["score"] >= 0: new_stories.append(story) else: hidden_stories_removed += 1 stories = new_stories - - data = dict(stories=stories, - user_profiles=user_profiles, - feed_tags=feed_tags, - feed_authors=feed_authors, - classifiers=classifiers, - updated=last_update, - user_search=user_search, - feed_id=feed.pk, - elapsed_time=round(float(timediff), 2), - message=message) - - if include_feeds: data['feeds'] = feeds - if not include_hidden: data['hidden_stories_removed'] = hidden_stories_removed - if dupe_feed_id: data['dupe_feed_id'] = dupe_feed_id + + data = dict( + stories=stories, + user_profiles=user_profiles, + feed_tags=feed_tags, + feed_authors=feed_authors, + classifiers=classifiers, + updated=last_update, + user_search=user_search, + feed_id=feed.pk, + elapsed_time=round(float(timediff), 2), + message=message, + ) + + if include_feeds: + data["feeds"] = feeds + if not include_hidden: + data["hidden_stories_removed"] = hidden_stories_removed + if dupe_feed_id: + data["dupe_feed_id"] = dupe_feed_id if not usersub: data.update(feed.canonical()) # if not usersub and feed.num_subscribers <= 1: # data = dict(code=-1, message="You must be subscribed to this feed.") - + # time.sleep(random.randint(1, 3)) if delay and user.is_staff: # time.sleep(random.randint(2, 7) / 10.0) @@ -917,13 +976,14 @@ def load_single_feed(request, feed_id): return data + def load_feed_page(request, feed_id): if not feed_id: raise Http404 - + feed = Feed.get_by_id(feed_id) if feed and feed.has_page and not feed.has_page_exception: - if settings.BACKED_BY_AWS.get('pages_on_node'): + if settings.BACKED_BY_AWS.get("pages_on_node"): domain = Site.objects.get_current().domain url = "https://%s/original_page/%s" % ( domain, @@ -936,180 +996,193 @@ def load_feed_page(request, feed_id): page_response = None if page_response and page_response.status_code == 200: response = HttpResponse(page_response.content, content_type="text/html; charset=utf-8") - response['Content-Encoding'] = 'deflate' - response['Last-Modified'] = page_response.headers.get('Last-modified') - response['Etag'] = page_response.headers.get('Etag') - response['Content-Length'] = str(len(page_response.content)) - logging.user(request, "~FYLoading original page (%s), proxied from node: ~SB%s bytes" % - (feed_id, len(page_response.content))) + response["Content-Encoding"] = "deflate" + response["Last-Modified"] = page_response.headers.get("Last-modified") + response["Etag"] = page_response.headers.get("Etag") + response["Content-Length"] = str(len(page_response.content)) + logging.user( + request, + "~FYLoading original page (%s), proxied from node: ~SB%s bytes" + % (feed_id, len(page_response.content)), + ) return response - - if settings.BACKED_BY_AWS['pages_on_s3'] and feed.s3_page: + + if settings.BACKED_BY_AWS["pages_on_s3"] and feed.s3_page: if settings.PROXY_S3_PAGES: key = settings.S3_CONN.Bucket(settings.S3_PAGES_BUCKET_NAME).Object(key=feed.s3_pages_key) if key: compressed_data = key.get()["Body"] response = HttpResponse(compressed_data, content_type="text/html; charset=utf-8") - response['Content-Encoding'] = 'gzip' - - logging.user(request, "~FYLoading original page, proxied: ~SB%s bytes" % - (len(compressed_data))) + response["Content-Encoding"] = "gzip" + + logging.user( + request, "~FYLoading original page, proxied: ~SB%s bytes" % (len(compressed_data)) + ) return response else: logging.user(request, "~FYLoading original page, non-proxied") - return HttpResponseRedirect('//%s/%s' % (settings.S3_PAGES_BUCKET_NAME, - feed.s3_pages_key)) - + return HttpResponseRedirect("//%s/%s" % (settings.S3_PAGES_BUCKET_NAME, feed.s3_pages_key)) + data = MFeedPage.get_data(feed_id=feed_id) - + if not data or not feed or not feed.has_page or feed.has_page_exception: logging.user(request, "~FYLoading original page, ~FRmissing") - return render(request, 'static/404_original_page.xhtml', {}, - content_type='text/html', - status=404) - + return render(request, "static/404_original_page.xhtml", {}, content_type="text/html", status=404) + logging.user(request, "~FYLoading original page, from the db") return HttpResponse(data, content_type="text/html; charset=utf-8") + @json.json_view def load_starred_stories(request): - user = get_user(request) - offset = int(request.GET.get('offset', 0)) - limit = int(request.GET.get('limit', 10)) - page = int(request.GET.get('page', 0)) - query = request.GET.get('query', '').strip() - order = request.GET.get('order', 'newest') - tag = request.GET.get('tag') - highlights = is_true(request.GET.get('highlights', False)) - story_hashes = request.GET.getlist('h') or request.GET.getlist('h[]') + user = get_user(request) + offset = int(request.GET.get("offset", 0)) + limit = int(request.GET.get("limit", 10)) + page = int(request.GET.get("page", 0)) + query = request.GET.get("query", "").strip() + order = request.GET.get("order", "newest") + tag = request.GET.get("tag") + highlights = is_true(request.GET.get("highlights", False)) + story_hashes = request.GET.getlist("h") or request.GET.getlist("h[]") story_hashes = story_hashes[:100] - version = int(request.GET.get('v', 1)) - now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) - message = None - order_by = '-' if order == "newest" else "" - if page: offset = limit * (page - 1) - + version = int(request.GET.get("v", 1)) + now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) + message = None + order_by = "-" if order == "newest" else "" + if page: + offset = limit * (page - 1) + if query: - # results = SearchStarredStory.query(user.pk, query) - # story_ids = [result.db_id for result in results] + # results = SearchStarredStory.query(user.pk, query) + # story_ids = [result.db_id for result in results] if user.profile.is_premium: - stories = MStarredStory.find_stories(query, user.pk, tag=tag, offset=offset, limit=limit, - order=order) + stories = MStarredStory.find_stories( + query, user.pk, tag=tag, offset=offset, limit=limit, order=order + ) else: stories = [] message = "You must be a premium subscriber to search." elif highlights: if user.profile.is_premium: mstories = MStarredStory.objects( - user_id=user.pk, - highlights__exists=True, - __raw__={"$where": "this.highlights.length > 0"} - ).order_by('%sstarred_date' % order_by)[offset:offset+limit] - stories = Feed.format_stories(mstories) + user_id=user.pk, highlights__exists=True, __raw__={"$where": "this.highlights.length > 0"} + ).order_by("%sstarred_date" % order_by)[offset : offset + limit] + stories = Feed.format_stories(mstories) else: stories = [] message = "You must be a premium subscriber to read through saved story highlights." elif tag: if user.profile.is_premium: - mstories = MStarredStory.objects( - user_id=user.pk, - user_tags__contains=tag - ).order_by('%sstarred_date' % order_by)[offset:offset+limit] - stories = Feed.format_stories(mstories) + mstories = MStarredStory.objects(user_id=user.pk, user_tags__contains=tag).order_by( + "%sstarred_date" % order_by + )[offset : offset + limit] + stories = Feed.format_stories(mstories) else: stories = [] message = "You must be a premium subscriber to read saved stories by tag." elif story_hashes: limit = 100 - mstories = MStarredStory.objects( - user_id=user.pk, - story_hash__in=story_hashes - ).order_by('%sstarred_date' % order_by)[offset:offset+limit] + mstories = MStarredStory.objects(user_id=user.pk, story_hash__in=story_hashes).order_by( + "%sstarred_date" % order_by + )[offset : offset + limit] stories = Feed.format_stories(mstories) else: - mstories = MStarredStory.objects( - user_id=user.pk - ).order_by('%sstarred_date' % order_by)[offset:offset+limit] + mstories = MStarredStory.objects(user_id=user.pk).order_by("%sstarred_date" % order_by)[ + offset : offset + limit + ] stories = Feed.format_stories(mstories) - + stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk, check_all=True) - - story_hashes = [story['story_hash'] for story in stories] - story_feed_ids = list(set(s['story_feed_id'] for s in stories)) - usersub_ids = UserSubscription.objects.filter(user__pk=user.pk, feed__pk__in=story_feed_ids).values('feed__pk') - usersub_ids = [us['feed__pk'] for us in usersub_ids] + + story_hashes = [story["story_hash"] for story in stories] + story_feed_ids = list(set(s["story_feed_id"] for s in stories)) + usersub_ids = UserSubscription.objects.filter(user__pk=user.pk, feed__pk__in=story_feed_ids).values( + "feed__pk" + ) + usersub_ids = [us["feed__pk"] for us in usersub_ids] unsub_feed_ids = list(set(story_feed_ids).difference(set(usersub_ids))) - unsub_feeds = Feed.objects.filter(pk__in=unsub_feed_ids) - unsub_feeds = dict((feed.pk, feed.canonical(include_favicon=False)) for feed in unsub_feeds) + unsub_feeds = Feed.objects.filter(pk__in=unsub_feed_ids) + unsub_feeds = dict((feed.pk, feed.canonical(include_favicon=False)) for feed in unsub_feeds) for story in stories: - if story['story_feed_id'] in unsub_feeds: continue - duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=story['story_feed_id']) - if not duplicate_feed: continue + if story["story_feed_id"] in unsub_feeds: + continue + duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=story["story_feed_id"]) + if not duplicate_feed: + continue feed_id = duplicate_feed[0].feed_id try: - saved_story = MStarredStory.objects.get(user_id=user.pk, story_hash=story['story_hash']) + saved_story = MStarredStory.objects.get(user_id=user.pk, story_hash=story["story_hash"]) saved_story.feed_id = feed_id - _, story_hash = MStory.split_story_hash(story['story_hash']) + _, story_hash = MStory.split_story_hash(story["story_hash"]) saved_story.story_hash = "%s:%s" % (feed_id, story_hash) saved_story.story_feed_id = feed_id - story['story_hash'] = saved_story.story_hash - story['story_feed_id'] = saved_story.story_feed_id + story["story_hash"] = saved_story.story_hash + story["story_feed_id"] = saved_story.story_feed_id saved_story.save() - logging.user(request, "~FCSaving new feed for starred story: ~SB%s -> %s" % (story['story_hash'], feed_id)) + logging.user( + request, "~FCSaving new feed for starred story: ~SB%s -> %s" % (story["story_hash"], feed_id) + ) except (MStarredStory.DoesNotExist, MStarredStory.MultipleObjectsReturned): - logging.user(request, "~FCCan't find feed for starred story: ~SB%s" % (story['story_hash'])) + logging.user(request, "~FCCan't find feed for starred story: ~SB%s" % (story["story_hash"])) continue - + shared_story_hashes = MSharedStory.check_shared_story_hashes(user.pk, story_hashes) shared_stories = [] if shared_story_hashes: - shared_stories = MSharedStory.objects(user_id=user.pk, - story_hash__in=shared_story_hashes)\ - .hint([('story_hash', 1)])\ - .only('story_hash', 'shared_date', 'comments') - shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, - comments=story.comments)) - for story in shared_stories]) + shared_stories = ( + MSharedStory.objects(user_id=user.pk, story_hash__in=shared_story_hashes) + .hint([("story_hash", 1)]) + .only("story_hash", "shared_date", "comments") + ) + shared_stories = dict( + [ + (story.story_hash, dict(shared_date=story.shared_date, comments=story.comments)) + for story in shared_stories + ] + ) nowtz = localtime_for_timezone(now, user.profile.timezone) for story in stories: - story_date = localtime_for_timezone(story['story_date'], user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz) - starred_date = localtime_for_timezone(story['starred_date'], user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, nowtz) - story['starred_timestamp'] = int(starred_date.timestamp()) - story['read_status'] = 1 - story['starred'] = True - story['intelligence'] = { - 'feed': 1, - 'author': 0, - 'tags': 0, - 'title': 0, + story_date = localtime_for_timezone(story["story_date"], user.profile.timezone) + story["short_parsed_date"] = format_story_link_date__short(story_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(story_date, nowtz) + starred_date = localtime_for_timezone(story["starred_date"], user.profile.timezone) + story["starred_date"] = format_story_link_date__long(starred_date, nowtz) + story["starred_timestamp"] = int(starred_date.timestamp()) + story["read_status"] = 1 + story["starred"] = True + story["intelligence"] = { + "feed": 1, + "author": 0, + "tags": 0, + "title": 0, } - if story['story_hash'] in shared_stories: - story['shared'] = True - story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments']) - + if story["story_hash"] in shared_stories: + story["shared"] = True + story["shared_comments"] = strip_tags(shared_stories[story["story_hash"]]["comments"]) + search_log = "~SN~FG(~SB%s~SN)" % query if query else "" logging.user(request, "~FCLoading starred stories: ~SB%s stories %s" % (len(stories), search_log)) - + return { "stories": stories, "user_profiles": user_profiles, - 'feeds': list(unsub_feeds.values()) if version == 2 else unsub_feeds, + "feeds": list(unsub_feeds.values()) if version == 2 else unsub_feeds, "message": message, } + @json.json_view def starred_story_hashes(request): - user = get_user(request) - include_timestamps = is_true(request.GET.get('include_timestamps', False)) - - mstories = MStarredStory.objects( - user_id=user.pk - ).only('story_hash', 'starred_date', 'starred_updated').order_by('-starred_date') - + user = get_user(request) + include_timestamps = is_true(request.GET.get("include_timestamps", False)) + + mstories = ( + MStarredStory.objects(user_id=user.pk) + .only("story_hash", "starred_date", "starred_updated") + .order_by("-starred_date") + ) + if include_timestamps: story_hashes = [] for s in mstories: @@ -1119,21 +1192,22 @@ def starred_story_hashes(request): story_hashes.append((s.story_hash, date.strftime("%s"))) else: story_hashes = [s.story_hash for s in mstories] - - logging.user(request, "~FYLoading ~FCstarred story hashes~FY: %s story hashes" % - (len(story_hashes))) + + logging.user(request, "~FYLoading ~FCstarred story hashes~FY: %s story hashes" % (len(story_hashes))) return dict(starred_story_hashes=story_hashes) + def starred_stories_rss_feed(request, user_id, secret_token): return starred_stories_rss_feed_tag(request, user_id, secret_token, tag_slug=None) + def starred_stories_rss_feed_tag(request, user_id, secret_token, tag_slug): try: user = User.objects.get(pk=user_id) except User.DoesNotExist: raise Http404 - + if tag_slug: try: tag_counts = MStarredStoryCounts.objects.get(user_id=user_id, slug=tag_slug) @@ -1143,160 +1217,181 @@ def starred_stories_rss_feed_tag(request, user_id, secret_token, tag_slug): raise Http404 else: _, starred_count = MStarredStoryCounts.user_counts(user.pk, include_total=True) - + data = {} if tag_slug: - data['title'] = "Saved Stories - %s" % tag_counts.tag + data["title"] = "Saved Stories - %s" % tag_counts.tag else: - data['title'] = "Saved Stories" - data['link'] = "%s%s" % ( + data["title"] = "Saved Stories" + data["link"] = "%s%s" % ( settings.NEWSBLUR_URL, - reverse('saved-stories-tag', kwargs=dict(tag_name=tag_slug))) + reverse("saved-stories-tag", kwargs=dict(tag_name=tag_slug)), + ) if tag_slug: - data['description'] = "Stories saved by %s on NewsBlur with the tag \"%s\"." % (user.username, - tag_counts.tag) + data["description"] = 'Stories saved by %s on NewsBlur with the tag "%s".' % ( + user.username, + tag_counts.tag, + ) else: - data['description'] = "Stories saved by %s on NewsBlur." % (user.username) - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur - %s' % settings.NEWSBLUR_URL - data['docs'] = None - data['author_name'] = user.username - data['feed_url'] = "%s%s" % ( + data["description"] = "Stories saved by %s on NewsBlur." % (user.username) + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur - %s" % settings.NEWSBLUR_URL + data["docs"] = None + data["author_name"] = user.username + data["feed_url"] = "%s%s" % ( settings.NEWSBLUR_URL, - reverse('starred-stories-rss-feed-tag', - kwargs=dict(user_id=user_id, secret_token=secret_token, tag_slug=tag_slug)), + reverse( + "starred-stories-rss-feed-tag", + kwargs=dict(user_id=user_id, secret_token=secret_token, tag_slug=tag_slug), + ), ) rss = feedgenerator.Atom1Feed(**data) if not tag_slug or not tag_counts.tag: - starred_stories = MStarredStory.objects( - user_id=user.pk - ).order_by('-starred_date').limit(25) + starred_stories = MStarredStory.objects(user_id=user.pk).order_by("-starred_date").limit(25) elif tag_counts.is_highlights: - starred_stories = MStarredStory.objects( - user_id=user.pk, - highlights__exists=True, - __raw__={"$where": "this.highlights.length > 0"} - ).order_by('-starred_date').limit(25) + starred_stories = ( + MStarredStory.objects( + user_id=user.pk, highlights__exists=True, __raw__={"$where": "this.highlights.length > 0"} + ) + .order_by("-starred_date") + .limit(25) + ) else: - starred_stories = MStarredStory.objects( - user_id=user.pk, - user_tags__contains=tag_counts.tag - ).order_by('-starred_date').limit(25) + starred_stories = ( + MStarredStory.objects(user_id=user.pk, user_tags__contains=tag_counts.tag) + .order_by("-starred_date") + .limit(25) + ) starred_stories = Feed.format_stories(starred_stories) for starred_story in starred_stories: story_data = { - 'title': smart_str(starred_story['story_title']), - 'link': starred_story['story_permalink'], - 'description': smart_str(starred_story['story_content']), - 'author_name': starred_story['story_authors'], - 'categories': starred_story['story_tags'], - 'unique_id': starred_story['story_permalink'], - 'pubdate': starred_story['starred_date'], + "title": smart_str(starred_story["story_title"]), + "link": starred_story["story_permalink"], + "description": smart_str(starred_story["story_content"]), + "author_name": starred_story["story_authors"], + "categories": starred_story["story_tags"], + "unique_id": starred_story["story_permalink"], + "pubdate": starred_story["starred_date"], } rss.add_item(**story_data) - - logging.user(request, "~FBGenerating ~SB%s~SN's saved story RSS feed (%s, %s stories): ~FM%s" % ( - user.username, - tag_counts.tag if tag_slug else "[All stories]", - tag_counts.count if tag_slug else starred_count, - request.META.get('HTTP_USER_AGENT', "")[:24] - )) - return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml') + + logging.user( + request, + "~FBGenerating ~SB%s~SN's saved story RSS feed (%s, %s stories): ~FM%s" + % ( + user.username, + tag_counts.tag if tag_slug else "[All stories]", + tag_counts.count if tag_slug else starred_count, + request.META.get("HTTP_USER_AGENT", "")[:24], + ), + ) + return HttpResponse(rss.writeString("utf-8"), content_type="application/rss+xml") + def folder_rss_feed(request, user_id, secret_token, unread_filter, folder_slug): domain = Site.objects.get_current().domain - date_hack_2023 = (datetime.datetime.now() > datetime.datetime(2023, 7, 1)) + date_hack_2023 = datetime.datetime.now() > datetime.datetime(2023, 7, 1) try: user = User.objects.get(pk=user_id) except User.DoesNotExist: raise Http404 - + user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=user) feed_ids, folder_title = user_sub_folders.feed_ids_under_folder_slug(folder_slug) - + usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids) if feed_ids and ((user.profile.is_archive and date_hack_2023) or (not date_hack_2023)): params = { - "user_id": user.pk, + "user_id": user.pk, "feed_ids": feed_ids, "offset": 0, "limit": 20, - "order": 'newest', - "read_filter": 'all', - "cache_prefix": "RSS:" + "order": "newest", + "read_filter": "all", + "cache_prefix": "RSS:", } story_hashes, unread_feed_story_hashes = UserSubscription.feed_stories(**params) else: story_hashes = [] - mstories = MStory.objects(story_hash__in=story_hashes).order_by('-story_date') + mstories = MStory.objects(story_hash__in=story_hashes).order_by("-story_date") stories = Feed.format_stories(mstories) - + filtered_stories = [] - found_feed_ids = list(set([story['story_feed_id'] for story in stories])) + found_feed_ids = list(set([story["story_feed_id"] for story in stories])) trained_feed_ids = [sub.feed_id for sub in usersubs if sub.is_trained] - found_trained_feed_ids = list(set(trained_feed_ids) & set(found_feed_ids)) + found_trained_feed_ids = list(set(trained_feed_ids) & set(found_feed_ids)) if found_trained_feed_ids: - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids, - social_user_id=0)) - classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) + classifier_feeds = list( + MClassifierFeed.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids, social_user_id=0) + ) + classifier_authors = list( + MClassifierAuthor.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids) + ) + classifier_titles = list( + MClassifierTitle.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids) + ) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids)) else: classifier_feeds = [] classifier_authors = [] classifier_titles = [] classifier_tags = [] - - sort_classifiers_by_feed(user=user, feed_ids=found_feed_ids, - classifier_feeds=classifier_feeds, - classifier_authors=classifier_authors, - classifier_titles=classifier_titles, - classifier_tags=classifier_tags) + + sort_classifiers_by_feed( + user=user, + feed_ids=found_feed_ids, + classifier_feeds=classifier_feeds, + classifier_authors=classifier_authors, + classifier_titles=classifier_titles, + classifier_tags=classifier_tags, + ) for story in stories: - story['intelligence'] = { - 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']), - 'author': apply_classifier_authors(classifier_authors, story), - 'tags': apply_classifier_tags(classifier_tags, story), - 'title': apply_classifier_titles(classifier_titles, story), + story["intelligence"] = { + "feed": apply_classifier_feeds(classifier_feeds, story["story_feed_id"]), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } - story['score'] = UserSubscription.score_story(story['intelligence']) - if unread_filter == 'focus' and story['score'] >= 1: + story["score"] = UserSubscription.score_story(story["intelligence"]) + if unread_filter == "focus" and story["score"] >= 1: filtered_stories.append(story) - elif unread_filter == 'unread' and story['score'] >= 0: + elif unread_filter == "unread" and story["score"] >= 0: filtered_stories.append(story) stories = filtered_stories - + data = {} - data['title'] = "%s from %s (%s sites)" % (folder_title, user.username, len(feed_ids)) - data['link'] = "https://%s%s" % ( - domain, - reverse('folder', kwargs=dict(folder_name=folder_title))) - data['description'] = "Unread stories in %s on NewsBlur. From %s's account and contains %s sites." % ( + data["title"] = "%s from %s (%s sites)" % (folder_title, user.username, len(feed_ids)) + data["link"] = "https://%s%s" % (domain, reverse("folder", kwargs=dict(folder_name=folder_title))) + data["description"] = "Unread stories in %s on NewsBlur. From %s's account and contains %s sites." % ( folder_title, user.username, - len(feed_ids)) - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur - %s' % settings.NEWSBLUR_URL - data['docs'] = None - data['author_name'] = user.username - data['feed_url'] = "https://%s%s" % ( + len(feed_ids), + ) + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur - %s" % settings.NEWSBLUR_URL + data["docs"] = None + data["author_name"] = user.username + data["feed_url"] = "https://%s%s" % ( domain, - reverse('folder-rss-feed', - kwargs=dict(user_id=user_id, secret_token=secret_token, unread_filter=unread_filter, folder_slug=folder_slug)), + reverse( + "folder-rss-feed", + kwargs=dict( + user_id=user_id, + secret_token=secret_token, + unread_filter=unread_filter, + folder_slug=folder_slug, + ), + ), ) rss = feedgenerator.Atom1Feed(**data) for story in stories: - feed = Feed.get_by_id(story['story_feed_id']) + feed = Feed.get_by_id(story["story_feed_id"]) feed_title = feed.feed_title if feed else "" try: usersub = UserSubscription.objects.get(user=user, feed=feed) @@ -1304,58 +1399,59 @@ def folder_rss_feed(request, user_id, secret_token, unread_filter, folder_slug): feed_title = usersub.user_title except UserSubscription.DoesNotExist: usersub = None - + story_content = """%s

%s""" % ( - smart_str(story['story_content']), + smart_str(story["story_content"]), Site.objects.get_current().domain, - story['story_feed_id'], + story["story_feed_id"], feed_title, ) - story_content = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F]', '', story_content) - story_title = "%s%s" % (("%s: " % feed_title) if feed_title else "", story['story_title']) + story_content = re.sub(r"[\x00-\x08\x0B-\x0C\x0E-\x1F]", "", story_content) + story_title = "%s%s" % (("%s: " % feed_title) if feed_title else "", story["story_title"]) story_data = { - 'title': story_title, - 'link': story['story_permalink'], - 'description': story_content, - 'categories': story['story_tags'], - 'unique_id': 'https://%s/site/%s/%s/' % (domain, story['story_feed_id'], story['guid_hash']), - 'pubdate': localtime_for_timezone(story['story_date'], user.profile.timezone), + "title": story_title, + "link": story["story_permalink"], + "description": story_content, + "categories": story["story_tags"], + "unique_id": "https://%s/site/%s/%s/" % (domain, story["story_feed_id"], story["guid_hash"]), + "pubdate": localtime_for_timezone(story["story_date"], user.profile.timezone), } - if story['story_authors']: - story_data['author_name'] = story['story_authors'] + if story["story_authors"]: + story_data["author_name"] = story["story_authors"] rss.add_item(**story_data) # TODO: Remove below date hack to accomodate users who paid for premium but want folder rss if not user.profile.is_archive and date_hack_2023: story_data = { - 'title': "You must have a premium archive subscription on NewsBlur to have RSS feeds for folders.", - 'link': "https://%s/?next=premium" % domain, - 'description': "You must have a premium archive subscription on NewsBlur to have RSS feeds for folders.", - 'unique_id': "https://%s/premium_only" % domain, - 'pubdate': localtime_for_timezone(datetime.datetime.now(), user.profile.timezone), + "title": "You must have a premium archive subscription on NewsBlur to have RSS feeds for folders.", + "link": "https://%s/?next=premium" % domain, + "description": "You must have a premium archive subscription on NewsBlur to have RSS feeds for folders.", + "unique_id": "https://%s/premium_only" % domain, + "pubdate": localtime_for_timezone(datetime.datetime.now(), user.profile.timezone), } rss.add_item(**story_data) - - logging.user(request, "~FBGenerating ~SB%s~SN's folder RSS feed (%s, %s stories): ~FM%s" % ( - user.username, - folder_title, - len(stories), - request.META.get('HTTP_USER_AGENT', "")[:24] - )) - return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml') + + logging.user( + request, + "~FBGenerating ~SB%s~SN's folder RSS feed (%s, %s stories): ~FM%s" + % (user.username, folder_title, len(stories), request.META.get("HTTP_USER_AGENT", "")[:24]), + ) + return HttpResponse(rss.writeString("utf-8"), content_type="application/rss+xml") + @json.json_view def load_read_stories(request): - user = get_user(request) - offset = int(request.GET.get('offset', 0)) - limit = int(request.GET.get('limit', 10)) - page = int(request.GET.get('page', 0)) - order = request.GET.get('order', 'newest') - query = request.GET.get('query', '').strip() - now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) + user = get_user(request) + offset = int(request.GET.get("offset", 0)) + limit = int(request.GET.get("limit", 10)) + page = int(request.GET.get("page", 0)) + order = request.GET.get("order", "newest") + query = request.GET.get("query", "").strip() + now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) message = None - if page: offset = limit * (page - 1) - + if page: + offset = limit * (page - 1) + if query: stories = [] message = "Not implemented yet." @@ -1368,58 +1464,65 @@ def load_read_stories(request): story_hashes = RUserStory.get_read_stories(user.pk, offset=offset, limit=limit, order=order) mstories = MStory.objects(story_hash__in=story_hashes) stories = Feed.format_stories(mstories) - stories = sorted(stories, key=lambda story: story_hashes.index(story['story_hash']), - reverse=bool(order=="oldest")) - + stories = sorted( + stories, + key=lambda story: story_hashes.index(story["story_hash"]), + reverse=bool(order == "oldest"), + ) + stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk, check_all=True) - - story_hashes = [story['story_hash'] for story in stories] - story_feed_ids = list(set(s['story_feed_id'] for s in stories)) - usersub_ids = UserSubscription.objects.filter(user__pk=user.pk, feed__pk__in=story_feed_ids).values('feed__pk') - usersub_ids = [us['feed__pk'] for us in usersub_ids] + + story_hashes = [story["story_hash"] for story in stories] + story_feed_ids = list(set(s["story_feed_id"] for s in stories)) + usersub_ids = UserSubscription.objects.filter(user__pk=user.pk, feed__pk__in=story_feed_ids).values( + "feed__pk" + ) + usersub_ids = [us["feed__pk"] for us in usersub_ids] unsub_feed_ids = list(set(story_feed_ids).difference(set(usersub_ids))) - unsub_feeds = Feed.objects.filter(pk__in=unsub_feed_ids) - unsub_feeds = [feed.canonical(include_favicon=False) for feed in unsub_feeds] - - shared_stories = MSharedStory.objects(user_id=user.pk, - story_hash__in=story_hashes)\ - .hint([('story_hash', 1)])\ - .only('story_hash', 'shared_date', 'comments') - shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, - comments=story.comments)) - for story in shared_stories]) - starred_stories = MStarredStory.objects(user_id=user.pk, - story_hash__in=story_hashes)\ - .hint([('user_id', 1), ('story_hash', 1)]) - starred_stories = dict([(story.story_hash, story) - for story in starred_stories]) - + unsub_feeds = Feed.objects.filter(pk__in=unsub_feed_ids) + unsub_feeds = [feed.canonical(include_favicon=False) for feed in unsub_feeds] + + shared_stories = ( + MSharedStory.objects(user_id=user.pk, story_hash__in=story_hashes) + .hint([("story_hash", 1)]) + .only("story_hash", "shared_date", "comments") + ) + shared_stories = dict( + [ + (story.story_hash, dict(shared_date=story.shared_date, comments=story.comments)) + for story in shared_stories + ] + ) + starred_stories = MStarredStory.objects(user_id=user.pk, story_hash__in=story_hashes).hint( + [("user_id", 1), ("story_hash", 1)] + ) + starred_stories = dict([(story.story_hash, story) for story in starred_stories]) + nowtz = localtime_for_timezone(now, user.profile.timezone) for story in stories: - story_date = localtime_for_timezone(story['story_date'], user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz) - story['read_status'] = 1 - story['intelligence'] = { - 'feed': 1, - 'author': 0, - 'tags': 0, - 'title': 0, + story_date = localtime_for_timezone(story["story_date"], user.profile.timezone) + story["short_parsed_date"] = format_story_link_date__short(story_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(story_date, nowtz) + story["read_status"] = 1 + story["intelligence"] = { + "feed": 1, + "author": 0, + "tags": 0, + "title": 0, } - if story['story_hash'] in starred_stories: - story['starred'] = True - starred_story = Feed.format_story(starred_stories[story['story_hash']]) - starred_date = localtime_for_timezone(starred_story['starred_date'], - user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, now) - story['starred_timestamp'] = int(starred_date.timestamp()) - if story['story_hash'] in shared_stories: - story['shared'] = True - story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments']) - + if story["story_hash"] in starred_stories: + story["starred"] = True + starred_story = Feed.format_story(starred_stories[story["story_hash"]]) + starred_date = localtime_for_timezone(starred_story["starred_date"], user.profile.timezone) + story["starred_date"] = format_story_link_date__long(starred_date, now) + story["starred_timestamp"] = int(starred_date.timestamp()) + if story["story_hash"] in shared_stories: + story["shared"] = True + story["shared_comments"] = strip_tags(shared_stories[story["story_hash"]]["comments"]) + search_log = "~SN~FG(~SB%s~SN)" % query if query else "" logging.user(request, "~FCLoading read stories: ~SB%s stories %s" % (len(stories), search_log)) - + return { "stories": stories, "user_profiles": user_profiles, @@ -1427,41 +1530,42 @@ def load_read_stories(request): "message": message, } + @json.json_view def load_river_stories__redis(request): # get_post is request.REQUEST, since this endpoint needs to handle either # GET or POST requests, since the parameters for this endpoint can be # very long, at which point the max size of a GET url request is exceeded. - get_post = getattr(request, request.method) - limit = int(get_post.get('limit', 12)) - start = time.time() - user = get_user(request) - message = None - feed_ids = get_post.getlist('feeds') or get_post.getlist('feeds[]') - feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id] + get_post = getattr(request, request.method) + limit = int(get_post.get("limit", 12)) + start = time.time() + user = get_user(request) + message = None + feed_ids = get_post.getlist("feeds") or get_post.getlist("feeds[]") + feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id] if not feed_ids: - feed_ids = get_post.getlist('f') or get_post.getlist('f[]') - feed_ids = [int(feed_id) for feed_id in get_post.getlist('f') if feed_id] - story_hashes = get_post.getlist('h') or get_post.getlist('h[]') - story_hashes = story_hashes[:100] - requested_hashes = len(story_hashes) + feed_ids = get_post.getlist("f") or get_post.getlist("f[]") + feed_ids = [int(feed_id) for feed_id in get_post.getlist("f") if feed_id] + story_hashes = get_post.getlist("h") or get_post.getlist("h[]") + story_hashes = story_hashes[:100] + requested_hashes = len(story_hashes) original_feed_ids = list(feed_ids) - page = int(get_post.get('page', 1)) - order = get_post.get('order', 'newest') - read_filter = get_post.get('read_filter', 'unread') - query = get_post.get('query', '').strip() - include_hidden = is_true(get_post.get('include_hidden', False)) - include_feeds = is_true(get_post.get('include_feeds', False)) - on_dashboard = is_true(get_post.get('dashboard', False)) or is_true(get_post.get('on_dashboard', False)) - infrequent = is_true(get_post.get('infrequent', False)) + page = int(get_post.get("page", 1)) + order = get_post.get("order", "newest") + read_filter = get_post.get("read_filter", "unread") + query = get_post.get("query", "").strip() + include_hidden = is_true(get_post.get("include_hidden", False)) + include_feeds = is_true(get_post.get("include_feeds", False)) + on_dashboard = is_true(get_post.get("dashboard", False)) or is_true(get_post.get("on_dashboard", False)) + infrequent = is_true(get_post.get("infrequent", False)) if infrequent: - infrequent = get_post.get('infrequent') - now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) - usersubs = [] - code = 1 - user_search = None - offset = (page-1) * limit - story_date_order = "%sstory_date" % ('' if order == 'oldest' else '-') + infrequent = get_post.get("infrequent") + now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) + usersubs = [] + code = 1 + user_search = None + offset = (page - 1) * limit + story_date_order = "%sstory_date" % ("" if order == "oldest" else "-") if user.pk == 86178: # Disable Michael_Novakhov account @@ -1470,46 +1574,47 @@ def load_river_stories__redis(request): if infrequent: feed_ids = Feed.low_volume_feeds(feed_ids, stories_per_month=infrequent) - + if story_hashes: unread_feed_story_hashes = None - read_filter = 'all' + read_filter = "all" mstories = MStory.objects(story_hash__in=story_hashes).order_by(story_date_order) stories = Feed.format_stories(mstories) elif query: if user.profile.is_premium: user_search = MUserSearch.get_user(user.pk) user_search.touch_search_date() - usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, - read_filter='all') + usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, read_filter="all") feed_ids = [sub.feed_id for sub in usersubs] if infrequent: feed_ids = Feed.low_volume_feeds(feed_ids, stories_per_month=infrequent) stories = Feed.find_feed_stories(feed_ids, query, order=order, offset=offset, limit=limit) mstories = stories - unread_feed_story_hashes = UserSubscription.story_hashes(user.pk, feed_ids=feed_ids, - read_filter="unread", order=order, - cutoff_date=user.profile.unread_cutoff) + unread_feed_story_hashes = UserSubscription.story_hashes( + user.pk, + feed_ids=feed_ids, + read_filter="unread", + order=order, + cutoff_date=user.profile.unread_cutoff, + ) else: stories = [] mstories = [] message = "You must be a premium subscriber to search." - elif read_filter == 'starred': - mstories = MStarredStory.objects( - user_id=user.pk, - story_feed_id__in=feed_ids - ).order_by('%sstarred_date' % ('-' if order == 'newest' else ''))[offset:offset+limit] - stories = Feed.format_stories(mstories) + elif read_filter == "starred": + mstories = MStarredStory.objects(user_id=user.pk, story_feed_id__in=feed_ids).order_by( + "%sstarred_date" % ("-" if order == "newest" else "") + )[offset : offset + limit] + stories = Feed.format_stories(mstories) else: - usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, - read_filter=read_filter) + usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, read_filter=read_filter) all_feed_ids = [f for f in feed_ids] feed_ids = [sub.feed_id for sub in usersubs] if infrequent: feed_ids = Feed.low_volume_feeds(feed_ids, stories_per_month=infrequent) if feed_ids: params = { - "user_id": user.pk, + "user_id": user.pk, "feed_ids": feed_ids, "all_feed_ids": all_feed_ids, "offset": offset, @@ -1527,91 +1632,101 @@ def load_river_stories__redis(request): mstories = MStory.objects(story_hash__in=story_hashes[:limit]).order_by(story_date_order) stories = Feed.format_stories(mstories) - - found_feed_ids = list(set([story['story_feed_id'] for story in stories])) + + found_feed_ids = list(set([story["story_feed_id"] for story in stories])) stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk) - + if not usersubs: - usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=found_feed_ids, - read_filter=read_filter) - + usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=found_feed_ids, read_filter=read_filter) + trained_feed_ids = [sub.feed_id for sub in usersubs if sub.is_trained] found_trained_feed_ids = list(set(trained_feed_ids) & set(found_feed_ids)) # Find starred stories if found_feed_ids: - if read_filter == 'starred': + if read_filter == "starred": starred_stories = mstories else: - story_hashes = [s['story_hash'] for s in stories] - starred_stories = MStarredStory.objects( - user_id=user.pk, - story_hash__in=story_hashes) - starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date, - user_tags=story.user_tags, - highlights=story.highlights, - user_notes=story.user_notes)) - for story in starred_stories]) + story_hashes = [s["story_hash"] for s in stories] + starred_stories = MStarredStory.objects(user_id=user.pk, story_hash__in=story_hashes) + starred_stories = dict( + [ + ( + story.story_hash, + dict( + starred_date=story.starred_date, + user_tags=story.user_tags, + highlights=story.highlights, + user_notes=story.user_notes, + ), + ) + for story in starred_stories + ] + ) else: starred_stories = {} - + # Intelligence classifiers for all feeds involved if found_trained_feed_ids: - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids, - social_user_id=0)) - classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, - feed_id__in=found_trained_feed_ids)) + classifier_feeds = list( + MClassifierFeed.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids, social_user_id=0) + ) + classifier_authors = list( + MClassifierAuthor.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids) + ) + classifier_titles = list( + MClassifierTitle.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids) + ) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, feed_id__in=found_trained_feed_ids)) else: classifier_feeds = [] classifier_authors = [] classifier_titles = [] classifier_tags = [] - classifiers = sort_classifiers_by_feed(user=user, feed_ids=found_feed_ids, - classifier_feeds=classifier_feeds, - classifier_authors=classifier_authors, - classifier_titles=classifier_titles, - classifier_tags=classifier_tags) - + classifiers = sort_classifiers_by_feed( + user=user, + feed_ids=found_feed_ids, + classifier_feeds=classifier_feeds, + classifier_authors=classifier_authors, + classifier_titles=classifier_titles, + classifier_tags=classifier_tags, + ) + # Just need to format stories nowtz = localtime_for_timezone(now, user.profile.timezone) for story in stories: - if read_filter == 'starred': - story['read_status'] = 1 + if read_filter == "starred": + story["read_status"] = 1 else: - story['read_status'] = 0 - if read_filter == 'all' or query: - if (unread_feed_story_hashes is not None and - story['story_hash'] not in unread_feed_story_hashes): - story['read_status'] = 1 - story_date = localtime_for_timezone(story['story_date'], user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz) - if story['story_hash'] in starred_stories: - story['starred'] = True - starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'], - user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, now) - story['starred_timestamp'] = int(starred_date.timestamp()) - story['user_tags'] = starred_stories[story['story_hash']]['user_tags'] - story['user_notes'] = starred_stories[story['story_hash']]['user_notes'] - story['highlights'] = starred_stories[story['story_hash']]['highlights'] - story['intelligence'] = { - 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']), - 'author': apply_classifier_authors(classifier_authors, story), - 'tags': apply_classifier_tags(classifier_tags, story), - 'title': apply_classifier_titles(classifier_titles, story), + story["read_status"] = 0 + if read_filter == "all" or query: + if unread_feed_story_hashes is not None and story["story_hash"] not in unread_feed_story_hashes: + story["read_status"] = 1 + story_date = localtime_for_timezone(story["story_date"], user.profile.timezone) + story["short_parsed_date"] = format_story_link_date__short(story_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(story_date, nowtz) + if story["story_hash"] in starred_stories: + story["starred"] = True + starred_date = localtime_for_timezone( + starred_stories[story["story_hash"]]["starred_date"], user.profile.timezone + ) + story["starred_date"] = format_story_link_date__long(starred_date, now) + story["starred_timestamp"] = int(starred_date.timestamp()) + story["user_tags"] = starred_stories[story["story_hash"]]["user_tags"] + story["user_notes"] = starred_stories[story["story_hash"]]["user_notes"] + story["highlights"] = starred_stories[story["story_hash"]]["highlights"] + story["intelligence"] = { + "feed": apply_classifier_feeds(classifier_feeds, story["story_feed_id"]), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } - story['score'] = UserSubscription.score_story(story['intelligence']) - + story["score"] = UserSubscription.score_story(story["intelligence"]) + if include_feeds: - feeds = Feed.objects.filter(pk__in=set([story['story_feed_id'] for story in stories])) + feeds = Feed.objects.filter(pk__in=set([story["story_feed_id"] for story in stories])) feeds = [feed.canonical(include_favicon=False) for feed in feeds] - + if not user.profile.is_premium and not include_feeds: message = "The full River of News is a premium feature." code = 0 @@ -1623,57 +1738,79 @@ def load_river_stories__redis(request): hidden_stories_removed = 0 new_stories = [] for story in stories: - if story['score'] >= 0: + if story["score"] >= 0: new_stories.append(story) else: hidden_stories_removed += 1 stories = new_stories - + # if page > 1: # import random # time.sleep(random.randint(10, 16)) - + diff = time.time() - start timediff = round(float(diff), 2) if requested_hashes and story_hashes: - logging.user(request, "~FB%sLoading ~FC%s~FB stories: %s%s" % - ("~FBAuto-" if on_dashboard else "", - requested_hashes, story_hashes[:3], f"...(+{len(story_hashes)-3})" if len(story_hashes) > 3 else "")) + logging.user( + request, + "~FB%sLoading ~FC%s~FB stories: %s%s" + % ( + "~FBAuto-" if on_dashboard else "", + requested_hashes, + story_hashes[:3], + f"...(+{len(story_hashes)-3})" if len(story_hashes) > 3 else "", + ), + ) else: - logging.user(request, "~FY%sLoading ~FC%sriver stories~FY: ~SBp%s~SN (%s/%s " - "stories, ~SN%s/%s/%s feeds, %s/%s)" % - ("~FCAuto-" if on_dashboard else "", - "~FB~SBinfrequent~SN~FC " if infrequent else "", - page, len(stories), len(mstories), len(found_feed_ids), - len(feed_ids), len(original_feed_ids), order, read_filter)) - - if not on_dashboard and not (requested_hashes and story_hashes): - MAnalyticsLoader.add(page_load=diff) # Only count full pages, not individual stories - if hasattr(request, 'start_time'): - seconds = time.time() - request.start_time - RStats.add('page_load', duration=seconds) + logging.user( + request, + "~FY%sLoading ~FC%sriver stories~FY: ~SBp%s~SN (%s/%s " + "stories, ~SN%s/%s/%s feeds, %s/%s)" + % ( + "~FCAuto-" if on_dashboard else "", + "~FB~SBinfrequent~SN~FC " if infrequent else "", + page, + len(stories), + len(mstories), + len(found_feed_ids), + len(feed_ids), + len(original_feed_ids), + order, + read_filter, + ), + ) - data = dict(code=code, - message=message, - stories=stories, - classifiers=classifiers, - elapsed_time=timediff, - user_search=user_search, - user_profiles=user_profiles) - - if include_feeds: data['feeds'] = feeds - if not include_hidden: data['hidden_stories_removed'] = hidden_stories_removed + if not on_dashboard and not (requested_hashes and story_hashes): + MAnalyticsLoader.add(page_load=diff) # Only count full pages, not individual stories + if hasattr(request, "start_time"): + seconds = time.time() - request.start_time + RStats.add("page_load", duration=seconds) + + data = dict( + code=code, + message=message, + stories=stories, + classifiers=classifiers, + elapsed_time=timediff, + user_search=user_search, + user_profiles=user_profiles, + ) + if include_feeds: + data["feeds"] = feeds + if not include_hidden: + data["hidden_stories_removed"] = hidden_stories_removed return data + @json.json_view def load_river_stories_widget(request): logging.user(request, "Widget load") river_stories_data = json.decode(load_river_stories__redis(request).content) timeout = 3 start = time.time() - + def load_url(url): original_url = url url = urllib.parse.urljoin(settings.NEWSBLUR_URL, url) @@ -1682,109 +1819,126 @@ def load_url(url): conn = None try: conn = urllib.request.urlopen(url, context=scontext, timeout=timeout) - except (urllib.error.URLError, socket.timeout): + except (urllib.error.URLError, socket.timeout, UnicodeEncodeError, http.client.InvalidURL): pass if not conn: # logging.user(request.user, '"%s" wasn\'t fetched, trying again: %s' % (url, e)) - url = url.replace('localhost', 'haproxy') + url = url.replace("localhost", "haproxy") try: conn = urllib.request.urlopen(url, context=scontext, timeout=timeout) except (urllib.error.HTTPError, urllib.error.URLError, socket.timeout) as e: - logging.user(request.user, '~FB"%s" ~FRnot fetched~FB in %ss: ~SB%s' % (url, (time.time() - start), e)) + logging.user( + request.user, '~FB"%s" ~FRnot fetched~FB in %ss: ~SB%s' % (url, (time.time() - start), e) + ) return None data = conn.read() if not url.startswith("data:"): - data = base64.b64encode(data).decode('utf-8') + data = base64.b64encode(data).decode("utf-8") logging.user(request.user, '~FB"%s" ~SBfetched~SN in ~SB%ss' % (url, (time.time() - start))) return dict(url=original_url, data=data) - + # Find the image thumbnails and download in parallel thumbnail_urls = [] - for story in river_stories_data['stories']: - thumbnail_values = list(story['secure_image_thumbnails'].values()) + for story in river_stories_data["stories"]: + thumbnail_values = list(story["secure_image_thumbnails"].values()) for thumbnail_value in thumbnail_values: - if 'data:' in thumbnail_value: + if "data:" in thumbnail_value: continue thumbnail_urls.append(thumbnail_value) break with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: pages = executor.map(load_url, thumbnail_urls) - + # Reassemble thumbnails back into stories thumbnail_data = dict() for page in pages: - if not page: continue - thumbnail_data[page['url']] = page['data'] - for story in river_stories_data['stories']: - thumbnail_values = list(story['secure_image_thumbnails'].values()) + if not page: + continue + thumbnail_data[page["url"]] = page["data"] + for story in river_stories_data["stories"]: + thumbnail_values = list(story["secure_image_thumbnails"].values()) if thumbnail_values and thumbnail_values[0] in thumbnail_data: page_url = thumbnail_values[0] - story['select_thumbnail_data'] = thumbnail_data[page_url] - + story["select_thumbnail_data"] = thumbnail_data[page_url] + logging.user(request, ("Elapsed Time: %ss" % (time.time() - start))) - + return river_stories_data - + + @json.json_view def complete_river(request): - user = get_user(request) - feed_ids = request.POST.getlist('feeds') or request.POST.getlist('feeds[]') - feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id and feed_id.isnumeric()] - page = int(request.POST.get('page', 1)) - read_filter = request.POST.get('read_filter', 'unread') + user = get_user(request) + feed_ids = request.POST.getlist("feeds") or request.POST.getlist("feeds[]") + feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id and feed_id.isnumeric()] + page = int(request.POST.get("page", 1)) + read_filter = request.POST.get("read_filter", "unread") stories_truncated = 0 - - usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, - read_filter=read_filter) + + usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids, read_filter=read_filter) feed_ids = [sub.feed_id for sub in usersubs] if feed_ids: - stories_truncated = UserSubscription.truncate_river(user.pk, feed_ids, read_filter, cache_prefix="dashboard:") - + stories_truncated = UserSubscription.truncate_river( + user.pk, feed_ids, read_filter, cache_prefix="dashboard:" + ) + if page >= 1: - logging.user(request, "~FC~BBRiver complete on page ~SB%s~SN, truncating ~SB%s~SN stories from ~SB%s~SN feeds" % (page, stories_truncated, len(feed_ids))) - + logging.user( + request, + "~FC~BBRiver complete on page ~SB%s~SN, truncating ~SB%s~SN stories from ~SB%s~SN feeds" + % (page, stories_truncated, len(feed_ids)), + ) + return dict(code=1, message="Truncated %s stories from %s" % (stories_truncated, len(feed_ids))) + @json.json_view def unread_story_hashes(request): - user = get_user(request) - feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]') - feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id] - include_timestamps = is_true(request.GET.get('include_timestamps', False)) - order = request.GET.get('order', 'newest') - read_filter = request.GET.get('read_filter', 'unread') - - story_hashes = UserSubscription.story_hashes(user.pk, feed_ids=feed_ids, - order=order, read_filter=read_filter, - include_timestamps=include_timestamps, - group_by_feed=True, - cutoff_date=user.profile.unread_cutoff) - - logging.user(request, "~FYLoading ~FCunread story hashes~FY: ~SB%s feeds~SN (%s story hashes)" % - (len(feed_ids), len(story_hashes))) + user = get_user(request) + feed_ids = request.GET.getlist("feed_id") or request.GET.getlist("feed_id[]") + feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id] + include_timestamps = is_true(request.GET.get("include_timestamps", False)) + order = request.GET.get("order", "newest") + read_filter = request.GET.get("read_filter", "unread") + + story_hashes = UserSubscription.story_hashes( + user.pk, + feed_ids=feed_ids, + order=order, + read_filter=read_filter, + include_timestamps=include_timestamps, + group_by_feed=True, + cutoff_date=user.profile.unread_cutoff, + ) + + logging.user( + request, + "~FYLoading ~FCunread story hashes~FY: ~SB%s feeds~SN (%s story hashes)" + % (len(feed_ids), len(story_hashes)), + ) return dict(unread_feed_story_hashes=story_hashes) + @ajax_login_required @json.json_view def mark_all_as_read(request): code = 1 try: - days = int(request.POST.get('days', 0)) + days = int(request.POST.get("days", 0)) except ValueError: - return dict(code=-1, message="Days parameter must be an integer, not: %s" % - request.POST.get('days')) + return dict(code=-1, message="Days parameter must be an integer, not: %s" % request.POST.get("days")) read_date = datetime.datetime.utcnow() - datetime.timedelta(days=days) - + feeds = UserSubscription.objects.filter(user=request.user) - infrequent = is_true(request.POST.get('infrequent', False)) + infrequent = is_true(request.POST.get("infrequent", False)) if infrequent: - infrequent = request.POST.get('infrequent') + infrequent = request.POST.get("infrequent") feed_ids = Feed.low_volume_feeds([usersub.feed.pk for usersub in feeds], stories_per_month=infrequent) feeds = UserSubscription.objects.filter(user=request.user, feed_id__in=feed_ids) - + socialsubs = MSocialSubscription.objects.filter(user_id=request.user.pk) for subtype in [feeds, socialsubs]: for sub in subtype: @@ -1795,39 +1949,45 @@ def mark_all_as_read(request): sub.needs_unread_recalc = True sub.mark_read_date = read_date sub.save() - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') - - logging.user(request, "~FMMarking %s as read: ~SB%s days" % (("all" if not infrequent else "infrequent stories"), days,)) + r.publish(request.user.username, "reload:feeds") + + logging.user( + request, + "~FMMarking %s as read: ~SB%s days" + % ( + ("all" if not infrequent else "infrequent stories"), + days, + ), + ) return dict(code=code) - + + @ajax_login_required @json.json_view def mark_story_as_read(request): - story_ids = request.POST.getlist('story_id') or request.POST.getlist('story_id[]') + story_ids = request.POST.getlist("story_id") or request.POST.getlist("story_id[]") try: - feed_id = int(get_argument_or_404(request, 'feed_id')) + feed_id = int(get_argument_or_404(request, "feed_id")) except ValueError: - return dict(code=-1, errors=["You must pass a valid feed_id: %s" % - request.POST.get('feed_id')]) - + return dict(code=-1, errors=["You must pass a valid feed_id: %s" % request.POST.get("feed_id")]) + try: - usersub = UserSubscription.objects.select_related('feed').get(user=request.user, feed=feed_id) + usersub = UserSubscription.objects.select_related("feed").get(user=request.user, feed=feed_id) except Feed.DoesNotExist: duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id) if duplicate_feed: feed_id = duplicate_feed[0].feed_id try: - usersub = UserSubscription.objects.get(user=request.user, - feed=duplicate_feed[0].feed) - except (Feed.DoesNotExist): + usersub = UserSubscription.objects.get(user=request.user, feed=duplicate_feed[0].feed) + except Feed.DoesNotExist: return dict(code=-1, errors=["No feed exists for feed_id %d." % feed_id]) else: return dict(code=-1, errors=["No feed exists for feed_id %d." % feed_id]) except UserSubscription.DoesNotExist: usersub = None - + if usersub: data = usersub.mark_story_ids_as_read(story_ids, request=request) else: @@ -1835,30 +1995,33 @@ def mark_story_as_read(request): return data + @ajax_login_required @json.json_view def mark_story_hashes_as_read(request): - retrying_failed = is_true(request.POST.get('retrying_failed', False)) + retrying_failed = is_true(request.POST.get("retrying_failed", False)) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) try: - story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]') + story_hashes = request.POST.getlist("story_hash") or request.POST.getlist("story_hash[]") except UnreadablePostError: return dict(code=-1, message="Missing `story_hash` list parameter.") - - feed_ids, friend_ids = RUserStory.mark_story_hashes_read(request.user.pk, story_hashes, username=request.user.username) + + feed_ids, friend_ids = RUserStory.mark_story_hashes_read( + request.user.pk, story_hashes, username=request.user.username + ) if request.user.profile.is_archive: RUserUnreadStory.mark_read(request.user.pk, story_hashes) - + if friend_ids: socialsubs = MSocialSubscription.objects.filter( - user_id=request.user.pk, - subscription_user_id__in=friend_ids) + user_id=request.user.pk, subscription_user_id__in=friend_ids + ) for socialsub in socialsubs: if not socialsub.needs_unread_recalc: socialsub.needs_unread_recalc = True socialsub.save() - r.publish(request.user.username, 'social:%s' % socialsub.subscription_user_id) + r.publish(request.user.username, "social:%s" % socialsub.subscription_user_id) # Also count on original subscription for feed_id in feed_ids: @@ -1868,55 +2031,59 @@ def mark_story_hashes_as_read(request): usersub.last_read_date = datetime.datetime.now() if not usersub.needs_unread_recalc: usersub.needs_unread_recalc = True - usersub.save(update_fields=['needs_unread_recalc', 'last_read_date']) + usersub.save(update_fields=["needs_unread_recalc", "last_read_date"]) else: - usersub.save(update_fields=['last_read_date']) - r.publish(request.user.username, 'feed:%s' % feed_id) - + usersub.save(update_fields=["last_read_date"]) + r.publish(request.user.username, "feed:%s" % feed_id) + hash_count = len(story_hashes) - logging.user(request, "~FYRead %s %s: %s %s" % ( - hash_count, 'story' if hash_count == 1 else 'stories', - story_hashes, - '(retrying failed)' if retrying_failed else '')) + logging.user( + request, + "~FYRead %s %s: %s %s" + % ( + hash_count, + "story" if hash_count == 1 else "stories", + story_hashes, + "(retrying failed)" if retrying_failed else "", + ), + ) + + return dict(code=1, story_hashes=story_hashes, feed_ids=feed_ids, friend_user_ids=friend_ids) - return dict(code=1, story_hashes=story_hashes, - feed_ids=feed_ids, friend_user_ids=friend_ids) @ajax_login_required @json.json_view def mark_feed_stories_as_read(request): r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - feeds_stories = request.POST.get('feeds_stories', "{}") + feeds_stories = request.POST.get("feeds_stories", "{}") feeds_stories = json.decode(feeds_stories) - data = { - 'code': -1, - 'message': 'Nothing was marked as read' - } - + data = {"code": -1, "message": "Nothing was marked as read"} + for feed_id, story_ids in list(feeds_stories.items()): try: feed_id = int(feed_id) except ValueError: continue try: - usersub = UserSubscription.objects.select_related('feed').get(user=request.user, feed=feed_id) + usersub = UserSubscription.objects.select_related("feed").get(user=request.user, feed=feed_id) data = usersub.mark_story_ids_as_read(story_ids, request=request) except UserSubscription.DoesNotExist: return dict(code=-1, error="You are not subscribed to this feed_id: %d" % feed_id) except Feed.DoesNotExist: duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id) try: - if not duplicate_feed: raise Feed.DoesNotExist - usersub = UserSubscription.objects.get(user=request.user, - feed=duplicate_feed[0].feed) + if not duplicate_feed: + raise Feed.DoesNotExist + usersub = UserSubscription.objects.get(user=request.user, feed=duplicate_feed[0].feed) data = usersub.mark_story_ids_as_read(story_ids, request=request) except (UserSubscription.DoesNotExist, Feed.DoesNotExist): return dict(code=-1, error="No feed exists for feed_id: %d" % feed_id) - r.publish(request.user.username, 'feed:%s' % feed_id) - + r.publish(request.user.username, "feed:%s" % feed_id) + return data - + + @ajax_login_required @json.json_view def mark_social_stories_as_read(request): @@ -1924,103 +2091,113 @@ def mark_social_stories_as_read(request): errors = [] data = {} r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - users_feeds_stories = request.POST.get('users_feeds_stories', "{}") + users_feeds_stories = request.POST.get("users_feeds_stories", "{}") users_feeds_stories = json.decode(users_feeds_stories) for social_user_id, feeds in list(users_feeds_stories.items()): for feed_id, story_ids in list(feeds.items()): feed_id = int(feed_id) try: - socialsub = MSocialSubscription.objects.get(user_id=request.user.pk, - subscription_user_id=social_user_id) + socialsub = MSocialSubscription.objects.get( + user_id=request.user.pk, subscription_user_id=social_user_id + ) data = socialsub.mark_story_ids_as_read(story_ids, feed_id, request=request) except OperationError as e: code = -1 errors.append("Already read story: %s" % e) except MSocialSubscription.DoesNotExist: - MSocialSubscription.mark_unsub_story_ids_as_read(request.user.pk, social_user_id, - story_ids, feed_id, - request=request) + MSocialSubscription.mark_unsub_story_ids_as_read( + request.user.pk, social_user_id, story_ids, feed_id, request=request + ) except Feed.DoesNotExist: duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id) if duplicate_feed: try: - socialsub = MSocialSubscription.objects.get(user_id=request.user.pk, - subscription_user_id=social_user_id) - data = socialsub.mark_story_ids_as_read(story_ids, duplicate_feed[0].feed.pk, request=request) + socialsub = MSocialSubscription.objects.get( + user_id=request.user.pk, subscription_user_id=social_user_id + ) + data = socialsub.mark_story_ids_as_read( + story_ids, duplicate_feed[0].feed.pk, request=request + ) except (UserSubscription.DoesNotExist, Feed.DoesNotExist): code = -1 errors.append("No feed exists for feed_id %d." % feed_id) else: continue - r.publish(request.user.username, 'feed:%s' % feed_id) - r.publish(request.user.username, 'social:%s' % social_user_id) + r.publish(request.user.username, "feed:%s" % feed_id) + r.publish(request.user.username, "social:%s" % social_user_id) data.update(code=code, errors=errors) return data - -@required_params('story_id', feed_id=int) + + +@required_params("story_id", feed_id=int) @ajax_login_required @json.json_view def mark_story_as_unread(request): - story_id = request.POST.get('story_id', None) - feed_id = int(request.POST.get('feed_id', 0)) - + story_id = request.POST.get("story_id", None) + feed_id = int(request.POST.get("feed_id", 0)) + try: - usersub = UserSubscription.objects.select_related('feed').get(user=request.user, feed=feed_id) + usersub = UserSubscription.objects.select_related("feed").get(user=request.user, feed=feed_id) feed = usersub.feed except UserSubscription.DoesNotExist: usersub = None feed = Feed.get_by_id(feed_id) - + if usersub and not usersub.needs_unread_recalc: usersub.needs_unread_recalc = True - usersub.save(update_fields=['needs_unread_recalc']) - + usersub.save(update_fields=["needs_unread_recalc"]) + data = dict(code=0, payload=dict(story_id=story_id)) - + story, found_original = MStory.find_story(feed_id, story_id) - + if not story: logging.user(request, "~FY~SBUnread~SN story in feed: %s (NOT FOUND)" % (feed)) return dict(code=-1, message="Story not found.") message = RUserStory.story_can_be_marked_unread_by_user(story, request.user) if message: - data['code'] = -1 - data['message'] = message + data["code"] = -1 + data["message"] = message return data - + if usersub: data = usersub.invert_read_stories_after_unread_story(story, request) - - social_subs = MSocialSubscription.mark_dirty_sharing_story(user_id=request.user.pk, - story_feed_id=feed_id, - story_guid_hash=story.guid_hash) + + social_subs = MSocialSubscription.mark_dirty_sharing_story( + user_id=request.user.pk, story_feed_id=feed_id, story_guid_hash=story.guid_hash + ) dirty_count = social_subs and social_subs.count() dirty_count = ("(%s social_subs)" % dirty_count) if dirty_count else "" RUserStory.mark_story_hash_unread(request.user, story_hash=story.story_hash) - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'feed:%s' % feed_id) + r.publish(request.user.username, "feed:%s" % feed_id) logging.user(request, "~FY~SBUnread~SN story in feed: %s %s" % (feed, dirty_count)) - + return data + @ajax_login_required @json.json_view -@required_params('story_hash') +@required_params("story_hash") def mark_story_hash_as_unread(request): r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]') + story_hashes = request.POST.getlist("story_hash") or request.POST.getlist("story_hash[]") is_list = len(story_hashes) > 1 datas = [] for story_hash in story_hashes: feed_id, _ = MStory.split_story_hash(story_hash) story, _ = MStory.find_story(feed_id, story_hash) if not story: - data = dict(code=-1, message="That story has been removed from the feed, no need to mark it unread.", story_hash=story_hash) + data = dict( + code=-1, + message="That story has been removed from the feed, no need to mark it unread.", + story_hash=story_hash, + ) if not is_list: return data else: @@ -2032,28 +2209,28 @@ def mark_story_hash_as_unread(request): return data else: datas.append(data) - + # Also count on original subscription usersubs = UserSubscription.objects.filter(user=request.user.pk, feed=feed_id) if usersubs: usersub = usersubs[0] if not usersub.needs_unread_recalc: usersub.needs_unread_recalc = True - usersub.save(update_fields=['needs_unread_recalc']) + usersub.save(update_fields=["needs_unread_recalc"]) data = usersub.invert_read_stories_after_unread_story(story, request) - r.publish(request.user.username, 'feed:%s' % feed_id) + r.publish(request.user.username, "feed:%s" % feed_id) feed_id, friend_ids = RUserStory.mark_story_hash_unread(request.user, story_hash) if friend_ids: socialsubs = MSocialSubscription.objects.filter( - user_id=request.user.pk, - subscription_user_id__in=friend_ids) + user_id=request.user.pk, subscription_user_id__in=friend_ids + ) for socialsub in socialsubs: if not socialsub.needs_unread_recalc: socialsub.needs_unread_recalc = True socialsub.save() - r.publish(request.user.username, 'social:%s' % socialsub.subscription_user_id) + r.publish(request.user.username, "social:%s" % socialsub.subscription_user_id) logging.user(request, "~FYUnread story in feed/socialsubs: %s/%s" % (feed_id, friend_ids)) @@ -2065,35 +2242,38 @@ def mark_story_hash_as_unread(request): return datas + @ajax_login_required @json.json_view def mark_feed_as_read(request): r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - feed_ids = request.POST.getlist('feed_id') or request.POST.getlist('feed_id[]') - cutoff_timestamp = int(request.POST.get('cutoff_timestamp', 0)) - direction = request.POST.get('direction', 'older') - infrequent = is_true(request.POST.get('infrequent', False)) + feed_ids = request.POST.getlist("feed_id") or request.POST.getlist("feed_id[]") + cutoff_timestamp = int(request.POST.get("cutoff_timestamp", 0)) + direction = request.POST.get("direction", "older") + infrequent = is_true(request.POST.get("infrequent", False)) if infrequent: - infrequent = request.POST.get('infrequent') + infrequent = request.POST.get("infrequent") multiple = len(feed_ids) > 1 code = 1 errors = [] cutoff_date = datetime.datetime.fromtimestamp(cutoff_timestamp) if cutoff_timestamp else None - + if infrequent: feed_ids = Feed.low_volume_feeds(feed_ids, stories_per_month=infrequent) - feed_ids = [str(f) for f in feed_ids] # This method expects strings - + feed_ids = [str(f) for f in feed_ids] # This method expects strings + if cutoff_date: - logging.user(request, "~FMMark %s feeds read, %s - cutoff: %s/%s" % - (len(feed_ids), direction, cutoff_timestamp, cutoff_date)) - + logging.user( + request, + "~FMMark %s feeds read, %s - cutoff: %s/%s" + % (len(feed_ids), direction, cutoff_timestamp, cutoff_date), + ) + for feed_id in feed_ids: - if 'social:' in feed_id: - user_id = int(feed_id.replace('social:', '')) + if "social:" in feed_id: + user_id = int(feed_id.replace("social:", "")) try: - sub = MSocialSubscription.objects.get(user_id=request.user.pk, - subscription_user_id=user_id) + sub = MSocialSubscription.objects.get(user_id=request.user.pk, subscription_user_id=user_id) except MSocialSubscription.DoesNotExist: logging.user(request, "~FRCouldn't find socialsub: %s" % user_id) continue @@ -2109,61 +2289,63 @@ def mark_feed_as_read(request): except (Feed.DoesNotExist, UserSubscription.DoesNotExist) as e: errors.append("User not subscribed: %s" % e) continue - except (ValueError) as e: + except ValueError as e: errors.append("Invalid feed_id: %s" % e) continue if not sub: errors.append("User not subscribed: %s" % feed_id) continue - + try: if direction == "older": marked_read = sub.mark_feed_read(cutoff_date=cutoff_date) else: marked_read = sub.mark_newer_stories_read(cutoff_date=cutoff_date) if marked_read and not multiple: - r.publish(request.user.username, 'feed:%s' % feed_id) + r.publish(request.user.username, "feed:%s" % feed_id) except IntegrityError as e: errors.append("Could not mark feed as read: %s" % e) code = -1 - + if multiple: logging.user(request, "~FMMarking ~SB%s~SN feeds as read" % len(feed_ids)) - r.publish(request.user.username, 'refresh:%s' % ','.join(feed_ids)) - + r.publish(request.user.username, "refresh:%s" % ",".join(feed_ids)) + if errors: logging.user(request, "~FMMarking read had errors: ~FR%s" % errors) - + return dict(code=code, errors=errors, cutoff_date=cutoff_date, direction=direction) + def _parse_user_info(user): return { - 'user_info': { - 'is_anonymous': json.encode(user.is_anonymous), - 'is_authenticated': json.encode(user.is_authenticated), - 'username': json.encode(user.username if user.is_authenticated else 'Anonymous') + "user_info": { + "is_anonymous": json.encode(user.is_anonymous), + "is_authenticated": json.encode(user.is_authenticated), + "username": json.encode(user.username if user.is_authenticated else "Anonymous"), } } + @ajax_login_required @json.json_view def add_url(request): code = 0 - url = request.POST['url'] - folder = request.POST.get('folder', '').replace('river:', '') - new_folder = request.POST.get('new_folder', '').replace('river:', '') - auto_active = is_true(request.POST.get('auto_active', 1)) - skip_fetch = is_true(request.POST.get('skip_fetch', False)) + url = request.POST["url"] + folder = request.POST.get("folder", "").replace("river:", "") + new_folder = request.POST.get("new_folder", "").replace("river:", "") + auto_active = is_true(request.POST.get("auto_active", 1)) + skip_fetch = is_true(request.POST.get("skip_fetch", False)) feed = None - + if not url: code = -1 - message = 'Enter in the website address or the feed URL.' + message = "Enter in the website address or the feed URL." elif any([(banned_url in url) for banned_url in BANNED_URLS]): code = -1 message = "The publisher of this website has banned NewsBlur." - elif re.match('(https?://)?twitter.com/\w+/?$', url): + elif re.match("(https?://)?twitter.com/\w+/?$", url): if not request.user.profile.is_premium: message = "You must be a premium subscriber to add Twitter feeds." code = -1 @@ -2177,7 +2359,7 @@ def add_url(request): except tweepy.TweepError: code = -1 message = "Your Twitter connection isn't setup. Go to Manage - Friends/Followers and reconnect Twitter." - + if code == -1: return dict(code=code, message=message) @@ -2186,25 +2368,26 @@ def add_url(request): usf.add_folder(folder, new_folder) folder = new_folder - code, message, us = UserSubscription.add_subscription(user=request.user, feed_address=url, - folder=folder, auto_active=auto_active, - skip_fetch=skip_fetch) + code, message, us = UserSubscription.add_subscription( + user=request.user, feed_address=url, folder=folder, auto_active=auto_active, skip_fetch=skip_fetch + ) feed = us and us.feed if feed: r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:%s' % feed.pk) + r.publish(request.user.username, "reload:%s" % feed.pk) MUserSearch.schedule_index_feeds_for_search(feed.pk, request.user.pk) - + return dict(code=code, message=message, feed=feed) + @ajax_login_required @json.json_view def add_folder(request): - folder = request.POST['folder'].replace('river:', '') - parent_folder = request.POST.get('parent_folder', '').replace('river:', '') + folder = request.POST["folder"].replace("river:", "") + parent_folder = request.POST.get("parent_folder", "").replace("river:", "") folders = None logging.user(request, "~FRAdding Folder: ~SB%s (in %s)" % (folder, parent_folder)) - + if folder: code = 1 message = "" @@ -2212,43 +2395,45 @@ def add_folder(request): user_sub_folders_object.add_folder(parent_folder, folder) folders = json.decode(user_sub_folders_object.folders) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') + r.publish(request.user.username, "reload:feeds") else: code = -1 message = "Gotta write in a folder name." - + return dict(code=code, message=message, folders=folders) + @ajax_login_required @json.json_view def delete_feed(request): - feed_id = int(request.POST['feed_id']) - in_folder = request.POST.get('in_folder', '').replace('river:', '') - if not in_folder or in_folder == ' ': + feed_id = int(request.POST["feed_id"]) + in_folder = request.POST.get("in_folder", "").replace("river:", "") + if not in_folder or in_folder == " ": in_folder = "" - + user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) user_sub_folders.delete_feed(feed_id, in_folder) - + feed = Feed.objects.filter(pk=feed_id) if feed: feed[0].count_subscribers() - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') - + r.publish(request.user.username, "reload:feeds") + return dict(code=1, message="Removed %s from '%s'." % (feed, in_folder)) + @ajax_login_required @json.json_view def delete_feed_by_url(request): message = "" code = 0 - url = request.POST['url'] - in_folder = request.POST.get('in_folder', '').replace('river:', '') - if in_folder == ' ': + url = request.POST["url"] + in_folder = request.POST.get("in_folder", "").replace("river:", "") + if in_folder == " ": in_folder = "" - + logging.user(request.user, "~FBFinding feed (delete_feed_by_url): %s" % url) feed = Feed.get_feed_from_url(url, create=False) if feed: @@ -2261,19 +2446,22 @@ def delete_feed_by_url(request): else: code = -1 message = "URL not found." - + return dict(code=code, message=message) - + + @ajax_login_required @json.json_view def delete_folder(request): - folder_to_delete = request.POST.get('folder_name') or request.POST.get('folder_to_delete') - in_folder = request.POST.get('in_folder', None) - feed_ids_in_folder = request.POST.getlist('feed_id') or request.POST.getlist('feed_id[]') + folder_to_delete = request.POST.get("folder_name") or request.POST.get("folder_to_delete") + in_folder = request.POST.get("in_folder", None) + feed_ids_in_folder = request.POST.getlist("feed_id") or request.POST.getlist("feed_id[]") feed_ids_in_folder = [int(f) for f in feed_ids_in_folder if f] - request.user.profile.send_opml_export_email(reason="You have deleted an entire folder of feeds, so here's a backup of all of your subscriptions just in case.") - + request.user.profile.send_opml_export_email( + reason="You have deleted an entire folder of feeds, so here's a backup of all of your subscriptions just in case." + ) + # Works piss poor with duplicate folder titles, if they are both in the same folder. # Deletes all, but only in the same folder parent. But nobody should be doing that, right? user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) @@ -2281,19 +2469,21 @@ def delete_folder(request): folders = json.decode(user_sub_folders.folders) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') - + r.publish(request.user.username, "reload:feeds") + return dict(code=1, folders=folders) -@required_params('feeds_by_folder') +@required_params("feeds_by_folder") @ajax_login_required @json.json_view def delete_feeds_by_folder(request): - feeds_by_folder = json.decode(request.POST['feeds_by_folder']) + feeds_by_folder = json.decode(request.POST["feeds_by_folder"]) + + request.user.profile.send_opml_export_email( + reason="You have deleted a number of feeds at once, so here's a backup of all of your subscriptions just in case." + ) - request.user.profile.send_opml_export_email(reason="You have deleted a number of feeds at once, so here's a backup of all of your subscriptions just in case.") - # Works piss poor with duplicate folder titles, if they are both in the same folder. # Deletes all, but only in the same folder parent. But nobody should be doing that, right? user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) @@ -2301,38 +2491,40 @@ def delete_feeds_by_folder(request): folders = json.decode(user_sub_folders.folders) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') - + r.publish(request.user.username, "reload:feeds") + return dict(code=1, folders=folders) + @ajax_login_required @json.json_view def rename_feed(request): - feed = get_object_or_404(Feed, pk=int(request.POST['feed_id'])) + feed = get_object_or_404(Feed, pk=int(request.POST["feed_id"])) try: user_sub = UserSubscription.objects.get(user=request.user, feed=feed) except UserSubscription.DoesNotExist: return dict(code=-1, message=f"You are not subscribed to {feed.feed_title}") - - feed_title = request.POST['feed_title'] - - logging.user(request, "~FRRenaming feed '~SB%s~SN' to: ~SB%s" % ( - feed.feed_title, feed_title)) - + + feed_title = request.POST["feed_title"] + + logging.user(request, "~FRRenaming feed '~SB%s~SN' to: ~SB%s" % (feed.feed_title, feed_title)) + user_sub.user_title = feed_title user_sub.save() - + return dict(code=1) - + + @ajax_login_required @json.json_view def rename_folder(request): - folder_to_rename = request.POST.get('folder_name') or request.POST.get('folder_to_rename') - new_folder_name = request.POST['new_folder_name'] - in_folder = request.POST.get('in_folder', '').replace('river:', '') - if 'Top Level' in in_folder: in_folder = '' + folder_to_rename = request.POST.get("folder_name") or request.POST.get("folder_to_rename") + new_folder_name = request.POST["new_folder_name"] + in_folder = request.POST.get("in_folder", "").replace("river:", "") + if "Top Level" in in_folder: + in_folder = "" code = 0 - + # Works piss poor with duplicate folder titles, if they are both in the same folder. # renames all, but only in the same folder parent. But nobody should be doing that, right? if folder_to_rename and new_folder_name: @@ -2341,66 +2533,74 @@ def rename_folder(request): code = 1 else: code = -1 - + return dict(code=code) - + + @ajax_login_required @json.json_view def move_feed_to_folders(request): - feed_id = int(request.POST['feed_id']) - in_folders = request.POST.getlist('in_folders', '') or request.POST.getlist('in_folders[]', '') - to_folders = request.POST.getlist('to_folders', '') or request.POST.getlist('to_folders[]', '') + feed_id = int(request.POST["feed_id"]) + in_folders = request.POST.getlist("in_folders", "") or request.POST.getlist("in_folders[]", "") + to_folders = request.POST.getlist("to_folders", "") or request.POST.getlist("to_folders[]", "") user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) - user_sub_folders = user_sub_folders.move_feed_to_folders(feed_id, in_folders=in_folders, - to_folders=to_folders) - + user_sub_folders = user_sub_folders.move_feed_to_folders( + feed_id, in_folders=in_folders, to_folders=to_folders + ) + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') + r.publish(request.user.username, "reload:feeds") return dict(code=1, folders=json.decode(user_sub_folders.folders)) - + + @ajax_login_required @json.json_view def move_feed_to_folder(request): - feed_id = int(request.POST['feed_id']) - in_folder = request.POST.get('in_folder', '') - to_folder = request.POST.get('to_folder', '') + feed_id = int(request.POST["feed_id"]) + in_folder = request.POST.get("in_folder", "") + to_folder = request.POST.get("to_folder", "") user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) - user_sub_folders = user_sub_folders.move_feed_to_folder(feed_id, in_folder=in_folder, - to_folder=to_folder) - + user_sub_folders = user_sub_folders.move_feed_to_folder(feed_id, in_folder=in_folder, to_folder=to_folder) + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') + r.publish(request.user.username, "reload:feeds") return dict(code=1, folders=json.decode(user_sub_folders.folders)) - + + @ajax_login_required @json.json_view def move_folder_to_folder(request): - folder_name = request.POST['folder_name'] - in_folder = request.POST.get('in_folder', '') - to_folder = request.POST.get('to_folder', '') - + folder_name = request.POST["folder_name"] + in_folder = request.POST.get("in_folder", "") + to_folder = request.POST.get("to_folder", "") + user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) - user_sub_folders = user_sub_folders.move_folder_to_folder(folder_name, in_folder=in_folder, to_folder=to_folder) - + user_sub_folders = user_sub_folders.move_folder_to_folder( + folder_name, in_folder=in_folder, to_folder=to_folder + ) + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') + r.publish(request.user.username, "reload:feeds") return dict(code=1, folders=json.decode(user_sub_folders.folders)) -@required_params('feeds_by_folder', 'to_folder') + +@required_params("feeds_by_folder", "to_folder") @ajax_login_required @json.json_view def move_feeds_by_folder_to_folder(request): - feeds_by_folder = json.decode(request.POST['feeds_by_folder']) - to_folder = request.POST['to_folder'] - new_folder = request.POST.get('new_folder', None) + feeds_by_folder = json.decode(request.POST["feeds_by_folder"]) + to_folder = request.POST["to_folder"] + new_folder = request.POST.get("new_folder", None) + + request.user.profile.send_opml_export_email( + reason="You have moved a number of feeds at once, so here's a backup of all of your subscriptions just in case." + ) - request.user.profile.send_opml_export_email(reason="You have moved a number of feeds at once, so here's a backup of all of your subscriptions just in case.") - user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user) if new_folder: @@ -2408,44 +2608,50 @@ def move_feeds_by_folder_to_folder(request): to_folder = new_folder user_sub_folders = user_sub_folders.move_feeds_by_folder_to_folder(feeds_by_folder, to_folder) - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') + r.publish(request.user.username, "reload:feeds") return dict(code=1, folders=json.decode(user_sub_folders.folders)) - + + @login_required def add_feature(request): if not request.user.is_staff: return HttpResponseForbidden() - code = -1 + code = -1 form = FeatureForm(request.POST) - + if form.is_valid(): form.save() code = 1 - return HttpResponseRedirect(reverse('index')) - + return HttpResponseRedirect(reverse("index")) + return dict(code=code) - + + @json.json_view def load_features(request): user = get_user(request) - page = max(int(request.GET.get('page', 0)), 0) + page = max(int(request.GET.get("page", 0)), 0) if page > 1: - logging.user(request, "~FBBrowse features: ~SBPage #%s" % (page+1)) - features = list(Feature.objects.all()[page*3:(page+1)*3+1].values()) - features = [{ - 'description': f['description'], - 'date': localtime_for_timezone(f['date'], user.profile.timezone).strftime("%b %d, %Y") - } for f in features] + logging.user(request, "~FBBrowse features: ~SBPage #%s" % (page + 1)) + features = list(Feature.objects.all()[page * 3 : (page + 1) * 3 + 1].values()) + features = [ + { + "description": f["description"], + "date": localtime_for_timezone(f["date"], user.profile.timezone).strftime("%b %d, %Y"), + } + for f in features + ] return features + @ajax_login_required @json.json_view def save_feed_order(request): - folders = request.POST.get('folders') + folders = request.POST.get("folders") if folders: # Test that folders can be JSON decoded folders_list = json.decode(folders) @@ -2454,44 +2660,50 @@ def save_feed_order(request): user_sub_folders = UserSubscriptionFolders.objects.get(user=request.user) user_sub_folders.folders = folders user_sub_folders.save() - + return {} + @json.json_view def feeds_trainer(request): classifiers = [] - feed_id = request.GET.get('feed_id') + feed_id = request.GET.get("feed_id") user = get_user(request) usersubs = UserSubscription.objects.filter(user=user, active=True) - + if feed_id: feed = get_object_or_404(Feed, pk=feed_id) usersubs = usersubs.filter(feed=feed) - usersubs = usersubs.select_related('feed').order_by('-feed__stories_last_month') - + usersubs = usersubs.select_related("feed").order_by("-feed__stories_last_month") + for us in usersubs: if (not us.is_trained and us.feed.stories_last_month > 0) or feed_id: classifier = dict() - classifier['classifiers'] = get_classifiers_for_user(user, feed_id=us.feed.pk) - classifier['feed_id'] = us.feed_id - classifier['stories_last_month'] = us.feed.stories_last_month - classifier['num_subscribers'] = us.feed.num_subscribers - classifier['feed_tags'] = json.decode(us.feed.data.popular_tags) if us.feed.data.popular_tags else [] - classifier['feed_authors'] = json.decode(us.feed.data.popular_authors) if us.feed.data.popular_authors else [] + classifier["classifiers"] = get_classifiers_for_user(user, feed_id=us.feed.pk) + classifier["feed_id"] = us.feed_id + classifier["stories_last_month"] = us.feed.stories_last_month + classifier["num_subscribers"] = us.feed.num_subscribers + classifier["feed_tags"] = ( + json.decode(us.feed.data.popular_tags) if us.feed.data.popular_tags else [] + ) + classifier["feed_authors"] = ( + json.decode(us.feed.data.popular_authors) if us.feed.data.popular_authors else [] + ) classifiers.append(classifier) - + user.profile.has_trained_intelligence = True user.profile.save() - + logging.user(user, "~FGLoading Trainer: ~SB%s feeds" % (len(classifiers))) - + return classifiers + @ajax_login_required @json.json_view def save_feed_chooser(request): is_premium = request.user.profile.is_premium - approved_feeds = request.POST.getlist('approved_feeds') or request.POST.getlist('approved_feeds[]') + approved_feeds = request.POST.getlist("approved_feeds") or request.POST.getlist("approved_feeds[]") approved_feeds = [int(feed_id) for feed_id in approved_feeds if feed_id] approve_all = False if not is_premium: @@ -2500,7 +2712,7 @@ def save_feed_chooser(request): approve_all = True activated = 0 usersubs = UserSubscription.objects.filter(user=request.user) - + for sub in usersubs: try: if sub.feed_id in approved_feeds or approve_all: @@ -2515,32 +2727,31 @@ def save_feed_chooser(request): sub.save() except Feed.DoesNotExist: pass - + UserSubscription.queue_new_feeds(request.user) UserSubscription.refresh_stale_feeds(request.user, exclude_new=True) - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'reload:feeds') - - logging.user(request, "~BB~FW~SBFeed chooser: ~FC%s~SN/~SB%s" % ( - activated, - usersubs.count() - )) - - return {'activated': activated} + r.publish(request.user.username, "reload:feeds") + + logging.user(request, "~BB~FW~SBFeed chooser: ~FC%s~SN/~SB%s" % (activated, usersubs.count())) + + return {"activated": activated} + @ajax_login_required def retrain_all_sites(request): for sub in UserSubscription.objects.filter(user=request.user): sub.is_trained = False sub.save() - + return feeds_trainer(request) - + + @login_required def activate_premium_account(request): try: - usersubs = UserSubscription.objects.select_related('feed').filter(user=request.user) + usersubs = UserSubscription.objects.select_related("feed").filter(user=request.user) for sub in usersubs: sub.active = True sub.save() @@ -2549,11 +2760,12 @@ def activate_premium_account(request): sub.feed.schedule_feed_fetch_immediately() except Exception as e: logging.user(request, "~BR~FWPremium activation failed: {e} {usersubs}") - + request.user.profile.is_premium = True request.user.profile.save() - - return HttpResponseRedirect(reverse('index')) + + return HttpResponseRedirect(reverse("index")) + @login_required def login_as(request): @@ -2561,69 +2773,74 @@ def login_as(request): logging.user(request, "~SKNON-STAFF LOGGING IN AS ANOTHER USER!") assert False return HttpResponseForbidden() - username = request.GET['user'] + username = request.GET["user"] user = get_object_or_404(User, username__iexact=username) user.backend = settings.AUTHENTICATION_BACKENDS[0] - login_user(request, user, backend='django.contrib.auth.backends.ModelBackend') - return HttpResponseRedirect(reverse('index')) - + login_user(request, user, backend="django.contrib.auth.backends.ModelBackend") + return HttpResponseRedirect(reverse("index")) + + def iframe_buster(request): logging.user(request, "~FB~SBiFrame bust!") return HttpResponse(status=204) -@required_params('story_id', feed_id=int) + +@required_params("story_id", feed_id=int) @ajax_login_required @json.json_view def mark_story_as_starred(request): return _mark_story_as_starred(request) - -@required_params('story_hash') + + +@required_params("story_hash") @ajax_login_required @json.json_view def mark_story_hash_as_starred(request): return _mark_story_as_starred(request) - + + def _mark_story_as_starred(request): - code = 1 - feed_id = int(request.POST.get('feed_id', 0)) - story_id = request.POST.get('story_id', None) - user_tags = request.POST.getlist('user_tags') or request.POST.getlist('user_tags[]') - user_notes = request.POST.get('user_notes', None) - highlights = request.POST.getlist('highlights') or request.POST.getlist('highlights[]') or [] - message = "" - story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]') + code = 1 + feed_id = int(request.POST.get("feed_id", 0)) + story_id = request.POST.get("story_id", None) + user_tags = request.POST.getlist("user_tags") or request.POST.getlist("user_tags[]") + user_notes = request.POST.get("user_notes", None) + highlights = request.POST.getlist("highlights") or request.POST.getlist("highlights[]") or [] + message = "" + story_hashes = request.POST.getlist("story_hash") or request.POST.getlist("story_hash[]") is_list = len(story_hashes) > 1 datas = [] if not len(story_hashes): - story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id) + story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id) if story: story_hashes = [story.story_hash] - + if not len(story_hashes): - return {'code': -1, 'message': "Could not find story to save."} - + return {"code": -1, "message": "Could not find story to save."} + for story_hash in story_hashes: - story, _ = MStory.find_story(story_hash=story_hash) + story, _ = MStory.find_story(story_hash=story_hash) if not story: logging.user(request, "~FCStarring ~FRfailed~FC: %s not found" % (story_hash)) - datas.append({'code': -1, 'message': "Could not save story, not found", 'story_hash': story_hash}) + datas.append({"code": -1, "message": "Could not save story, not found", "story_hash": story_hash}) continue feed_id = story and story.story_feed_id - - story_db = dict([(k, v) for k, v in list(story._data.items()) - if k is not None and v is not None]) + + story_db = dict([(k, v) for k, v in list(story._data.items()) if k is not None and v is not None]) # Pop all existing user-specific fields because we don't want to reuse them from the found story # in case MStory.find_story uses somebody else's saved/shared story (because the original is deleted) - story_db.pop('user_id', None) - story_db.pop('starred_date', None) - story_db.pop('id', None) - story_db.pop('user_tags', None) - story_db.pop('highlights', None) - story_db.pop('user_notes', None) - + story_db.pop("user_id", None) + story_db.pop("starred_date", None) + story_db.pop("id", None) + story_db.pop("user_tags", None) + story_db.pop("highlights", None) + story_db.pop("user_notes", None) + now = datetime.datetime.now() - story_values = dict(starred_date=now, user_tags=user_tags, highlights=highlights, user_notes=user_notes, **story_db) + story_values = dict( + starred_date=now, user_tags=user_tags, highlights=highlights, user_notes=user_notes, **story_db + ) params = dict(story_guid=story.story_guid, user_id=request.user.pk) starred_story = MStarredStory.objects(**params).limit(1) created = False @@ -2632,19 +2849,25 @@ def _mark_story_as_starred(request): removed_highlights = [] if not starred_story: params.update(story_values) - if 'story_latest_content_z' in params: - params.pop('story_latest_content_z') + if "story_latest_content_z" in params: + params.pop("story_latest_content_z") try: starred_story = MStarredStory.objects.create(**params) except OperationError as e: - logging.user(request, "~FCStarring ~FRfailed~FC: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], e)) - datas.append({'code': -1, 'message': "Could not save story due to: %s" % e, 'story_hash': story_hash}) - + logging.user( + request, "~FCStarring ~FRfailed~FC: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], e) + ) + datas.append( + {"code": -1, "message": "Could not save story due to: %s" % e, "story_hash": story_hash} + ) + created = True - MActivity.new_starred_story(user_id=request.user.pk, - story_title=story.story_title, - story_feed_id=feed_id, - story_id=starred_story.story_guid) + MActivity.new_starred_story( + user_id=request.user.pk, + story_title=story.story_title, + story_feed_id=feed_id, + story_id=starred_story.story_guid, + ) new_user_tags = user_tags new_highlights = highlights changed_user_notes = bool(user_notes) @@ -2660,57 +2883,74 @@ def _mark_story_as_starred(request): starred_story.highlights = highlights starred_story.user_notes = user_notes starred_story.save() - + if len(highlights) == 1 and len(new_highlights) == 1: MStarredStoryCounts.adjust_count(request.user.pk, highlights=True, amount=1) elif len(highlights) == 0 and len(removed_highlights): MStarredStoryCounts.adjust_count(request.user.pk, highlights=True, amount=-1) - + for tag in new_user_tags: MStarredStoryCounts.adjust_count(request.user.pk, tag=tag, amount=1) for tag in removed_user_tags: MStarredStoryCounts.adjust_count(request.user.pk, tag=tag, amount=-1) - + if random.random() < 0.01: MStarredStoryCounts.schedule_count_tags_for_user(request.user.pk) MStarredStoryCounts.count_for_user(request.user.pk, total_only=True) starred_counts, starred_count = MStarredStoryCounts.user_counts(request.user.pk, include_total=True) if not starred_count and len(starred_counts): - starred_count = MStarredStory.objects(user_id=request.user.pk).count() - + starred_count = MStarredStory.objects(user_id=request.user.pk).count() + if not changed_user_notes: r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'story:starred:%s' % story.story_hash) - + r.publish(request.user.username, "story:starred:%s" % story.story_hash) + if created: - logging.user(request, "~FCStarring: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], starred_story.user_tags)) + logging.user( + request, + "~FCStarring: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], starred_story.user_tags), + ) else: - logging.user(request, "~FCUpdating starred:~SN~FC ~SB%s~SN (~FM~SB%s~FC~SN/~FM%s~FC)" % (story.story_title[:32], starred_story.user_tags, starred_story.user_notes)) - - datas.append({'code': code, 'message': message, 'starred_count': starred_count, 'starred_counts': starred_counts}) - + logging.user( + request, + "~FCUpdating starred:~SN~FC ~SB%s~SN (~FM~SB%s~FC~SN/~FM%s~FC)" + % (story.story_title[:32], starred_story.user_tags, starred_story.user_notes), + ) + + datas.append( + { + "code": code, + "message": message, + "starred_count": starred_count, + "starred_counts": starred_counts, + } + ) + if len(datas) >= 2: return datas elif len(datas) == 1: return datas[0] return datas - -@required_params('story_id') + + +@required_params("story_id") @ajax_login_required @json.json_view def mark_story_as_unstarred(request): return _mark_story_as_unstarred(request) - -@required_params('story_hash') + + +@required_params("story_hash") @ajax_login_required @json.json_view def mark_story_hash_as_unstarred(request): return _mark_story_as_unstarred(request) + def _mark_story_as_unstarred(request): - code = 1 - story_id = request.POST.get('story_id', None) - story_hashes = request.POST.getlist('story_hash') or request.POST.getlist('story_hash[]') + code = 1 + story_id = request.POST.get("story_id", None) + story_hashes = request.POST.getlist("story_hash") or request.POST.getlist("story_hash[]") starred_counts = None starred_story = None if story_id: @@ -2720,28 +2960,32 @@ def _mark_story_as_unstarred(request): story_hashes = [starred_story.story_hash] else: story_hashes = [story_id] - + datas = [] for story_hash in story_hashes: starred_story = MStarredStory.objects(user_id=request.user.pk, story_hash=story_hash) if not starred_story: logging.user(request, "~FCUnstarring ~FRfailed~FC: %s not found" % (story_hash)) - datas.append({'code': -1, 'message': "Could not unsave story, not found", 'story_hash': story_hash}) + datas.append( + {"code": -1, "message": "Could not unsave story, not found", "story_hash": story_hash} + ) continue - + starred_story = starred_story[0] logging.user(request, "~FCUnstarring: ~SB%s" % (starred_story.story_title[:50])) user_tags = starred_story.user_tags feed_id = starred_story.story_feed_id - MActivity.remove_starred_story(user_id=request.user.pk, - story_feed_id=starred_story.story_feed_id, - story_id=starred_story.story_guid) + MActivity.remove_starred_story( + user_id=request.user.pk, + story_feed_id=starred_story.story_feed_id, + story_id=starred_story.story_guid, + ) starred_story.user_id = 0 try: starred_story.save() except NotUniqueError: starred_story.delete() - + MStarredStoryCounts.adjust_count(request.user.pk, feed_id=feed_id, amount=-1) for tag in user_tags: @@ -2752,27 +2996,32 @@ def _mark_story_as_unstarred(request): MStarredStoryCounts.schedule_count_tags_for_user(request.user.pk) MStarredStoryCounts.count_for_user(request.user.pk, total_only=True) starred_counts = MStarredStoryCounts.user_counts(request.user.pk) - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(request.user.username, 'story:unstarred:%s' % starred_story.story_hash) - + r.publish(request.user.username, "story:unstarred:%s" % starred_story.story_hash) + if not story_hashes: datas.append(dict(code=-1, message=f"Failed to find {story_hashes}")) - - return {'code': code, 'starred_counts': starred_counts, 'messages': datas} - + + return {"code": code, "starred_counts": starred_counts, "messages": datas} + + @ajax_login_required @json.json_view def starred_counts(request): starred_counts, starred_count = MStarredStoryCounts.user_counts(request.user.pk, include_total=True) - logging.user(request, "~FCRequesting starred counts: ~SB%s stories (%s tags)" % (starred_count, len([s for s in starred_counts if s['tag']]))) + logging.user( + request, + "~FCRequesting starred counts: ~SB%s stories (%s tags)" + % (starred_count, len([s for s in starred_counts if s["tag"]])), + ) + + return {"starred_count": starred_count, "starred_counts": starred_counts} + - return {'starred_count': starred_count, 'starred_counts': starred_counts} - @ajax_login_required @json.json_view def send_story_email(request): - def validate_email_as_bool(email): try: validate_email(email) @@ -2780,46 +3029,49 @@ def validate_email_as_bool(email): except: return False - code = 1 - message = 'OK' - user = get_user(request) - story_id = request.POST['story_id'] - feed_id = request.POST['feed_id'] - to_addresses = request.POST.get('to', '').replace(',', ' ').replace(' ', ' ').strip().split(' ') - from_name = request.POST['from_name'] - from_email = request.POST['from_email'] - email_cc = is_true(request.POST.get('email_cc', 'true')) - comments = request.POST['comments'] - comments = comments[:2048] # Separated due to PyLint - from_address = 'share@newsblur.com' + code = 1 + message = "OK" + user = get_user(request) + story_id = request.POST["story_id"] + feed_id = request.POST["feed_id"] + to_addresses = request.POST.get("to", "").replace(",", " ").replace(" ", " ").strip().split(" ") + from_name = request.POST["from_name"] + from_email = request.POST["from_email"] + email_cc = is_true(request.POST.get("email_cc", "true")) + comments = request.POST["comments"] + comments = comments[:2048] # Separated due to PyLint + from_address = "share@newsblur.com" share_user_profile = MSocialProfile.get_user(request.user.pk) - + quota = 32 if user.profile.is_premium else 1 if share_user_profile.over_story_email_quota(quota=quota): code = -1 if user.profile.is_premium: - message = 'You can only send %s stories per day by email.' % quota + message = "You can only send %s stories per day by email." % quota else: - message = 'Upgrade to a premium subscription to send more than one story per day by email.' - logging.user(request, '~BRNOT ~BMSharing story by email to %s recipient, over quota: %s/%s' % - (len(to_addresses), story_id, feed_id)) + message = "Upgrade to a premium subscription to send more than one story per day by email." + logging.user( + request, + "~BRNOT ~BMSharing story by email to %s recipient, over quota: %s/%s" + % (len(to_addresses), story_id, feed_id), + ) elif not to_addresses: code = -1 - message = 'Please provide at least one email address.' + message = "Please provide at least one email address." elif not all(validate_email_as_bool(to_address) for to_address in to_addresses if to_addresses): code = -1 - message = 'You need to send the email to a valid email address.' + message = "You need to send the email to a valid email address." elif not validate_email_as_bool(from_email): code = -1 - message = 'You need to provide your email address.' + message = "You need to provide your email address." elif not from_name: code = -1 - message = 'You need to provide your name.' + message = "You need to provide your name." else: story, _ = MStory.find_story(feed_id, story_id) - story = Feed.format_story(story, feed_id, text=True) - feed = Feed.get_by_id(story['story_feed_id']) - params = { + story = Feed.format_story(story, feed_id, text=True) + feed = Feed.get_by_id(story["story_feed_id"]) + params = { "to_addresses": to_addresses, "from_name": from_name, "from_email": from_email, @@ -2830,79 +3082,92 @@ def validate_email_as_bool(email): "feed": feed, "share_user_profile": share_user_profile, } - text = render_to_string('mail/email_story.txt', params) - html = render_to_string('mail/email_story.xhtml', params) - subject = '%s' % (story['story_title']) - cc = None + text = render_to_string("mail/email_story.txt", params) + html = render_to_string("mail/email_story.xhtml", params) + subject = "%s" % (story["story_title"]) + cc = None if email_cc: - cc = ['%s <%s>' % (from_name, from_email)] - subject = subject.replace('\n', ' ') - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % from_address, - to=to_addresses, - cc=cc, - headers={'Reply-To': "%s <%s>" % (from_name, from_email)}) + cc = ["%s <%s>" % (from_name, from_email)] + subject = subject.replace("\n", " ") + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % from_address, + to=to_addresses, + cc=cc, + headers={"Reply-To": "%s <%s>" % (from_name, from_email)}, + ) msg.attach_alternative(html, "text/html") # try: msg.send() # except boto.ses.connection.BotoServerError as e: # code = -1 # message = "Email error: %s" % str(e) - + share_user_profile.save_sent_email() - - logging.user(request, '~BMSharing story by email to %s recipient%s (%s): ~FY~SB%s~SN~BM~FY/~SB%s' % - (len(to_addresses), '' if len(to_addresses) == 1 else 's', to_addresses, - story['story_title'][:50], feed and feed.feed_title[:50])) - - return {'code': code, 'message': message} + + logging.user( + request, + "~BMSharing story by email to %s recipient%s (%s): ~FY~SB%s~SN~BM~FY/~SB%s" + % ( + len(to_addresses), + "" if len(to_addresses) == 1 else "s", + to_addresses, + story["story_title"][:50], + feed and feed.feed_title[:50], + ), + ) + + return {"code": code, "message": message} + @json.json_view def load_tutorial(request): - if request.GET.get('finished'): - logging.user(request, '~BY~FW~SBFinishing Tutorial') + if request.GET.get("finished"): + logging.user(request, "~BY~FW~SBFinishing Tutorial") return {} else: - newsblur_feed = Feed.objects.filter(feed_address__icontains='blog.newsblur.com').order_by('-pk')[0] - logging.user(request, '~BY~FW~SBLoading Tutorial') - return { - 'newsblur_feed': newsblur_feed.canonical() - } + newsblur_feed = Feed.objects.filter(feed_address__icontains="blog.newsblur.com").order_by("-pk")[0] + logging.user(request, "~BY~FW~SBLoading Tutorial") + return {"newsblur_feed": newsblur_feed.canonical()} + -@required_params('query', 'feed_id') +@required_params("query", "feed_id") @json.json_view def save_search(request): - feed_id = request.POST['feed_id'] - query = request.POST['query'] - + feed_id = request.POST["feed_id"] + query = request.POST["query"] + MSavedSearch.save_search(user_id=request.user.pk, feed_id=feed_id, query=query) - + saved_searches = MSavedSearch.user_searches(request.user.pk) - + return { - 'saved_searches': saved_searches, + "saved_searches": saved_searches, } -@required_params('query', 'feed_id') + +@required_params("query", "feed_id") @json.json_view def delete_search(request): - feed_id = request.POST['feed_id'] - query = request.POST['query'] + feed_id = request.POST["feed_id"] + query = request.POST["query"] MSavedSearch.delete_search(user_id=request.user.pk, feed_id=feed_id, query=query) saved_searches = MSavedSearch.user_searches(request.user.pk) return { - 'saved_searches': saved_searches, + "saved_searches": saved_searches, } -@required_params('river_id', 'river_side', 'river_order') + +@required_params("river_id", "river_side", "river_order") @json.json_view def save_dashboard_river(request): - river_id = request.POST['river_id'] - river_side = request.POST['river_side'] - river_order = int(request.POST['river_order']) + river_id = request.POST["river_id"] + river_side = request.POST["river_side"] + river_order = int(request.POST["river_order"]) logging.user(request, "~FCSaving dashboard river: ~SB%s~SN (%s %s)" % (river_id, river_side, river_order)) @@ -2910,21 +3175,24 @@ def save_dashboard_river(request): dashboard_rivers = MDashboardRiver.get_user_rivers(request.user.pk) return { - 'dashboard_rivers': dashboard_rivers, + "dashboard_rivers": dashboard_rivers, } -@required_params('river_id', 'river_side', 'river_order') + +@required_params("river_id", "river_side", "river_order") @json.json_view def remove_dashboard_river(request): - river_id = request.POST['river_id'] - river_side = request.POST['river_side'] - river_order = int(request.POST['river_order']) + river_id = request.POST["river_id"] + river_side = request.POST["river_side"] + river_order = int(request.POST["river_order"]) - logging.user(request, "~FRRemoving~FC dashboard river: ~SB%s~SN (%s %s)" % (river_id, river_side, river_order)) + logging.user( + request, "~FRRemoving~FC dashboard river: ~SB%s~SN (%s %s)" % (river_id, river_side, river_order) + ) MDashboardRiver.remove_river(request.user.pk, river_side, river_order) dashboard_rivers = MDashboardRiver.get_user_rivers(request.user.pk) return { - 'dashboard_rivers': dashboard_rivers, + "dashboard_rivers": dashboard_rivers, } diff --git a/apps/recommendations/migrations/0001_initial.py b/apps/recommendations/migrations/0001_initial.py index 5d1623cf9f..645d17caa9 100644 --- a/apps/recommendations/migrations/0001_initial.py +++ b/apps/recommendations/migrations/0001_initial.py @@ -1,45 +1,78 @@ # Generated by Django 2.0 on 2020-06-16 06:52 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ - ('rss_feeds', '0001_initial'), + ("rss_feeds", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='RecommendedFeed', + name="RecommendedFeed", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField(blank=True, null=True)), - ('is_public', models.BooleanField(default=False)), - ('created_date', models.DateField(auto_now_add=True)), - ('approved_date', models.DateField(null=True)), - ('declined_date', models.DateField(null=True)), - ('twitter', models.CharField(blank=True, max_length=50, null=True)), - ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recommendations', to='rss_feeds.Feed')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recommendations', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("description", models.TextField(blank=True, null=True)), + ("is_public", models.BooleanField(default=False)), + ("created_date", models.DateField(auto_now_add=True)), + ("approved_date", models.DateField(null=True)), + ("declined_date", models.DateField(null=True)), + ("twitter", models.CharField(blank=True, max_length=50, null=True)), + ( + "feed", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="recommendations", + to="rss_feeds.Feed", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="recommendations", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'ordering': ['-approved_date', '-created_date'], + "ordering": ["-approved_date", "-created_date"], }, ), migrations.CreateModel( - name='RecommendedFeedUserFeedback', + name="RecommendedFeedUserFeedback", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('score', models.IntegerField(default=0)), - ('created_date', models.DateField(auto_now_add=True)), - ('recommendation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='recommendations.RecommendedFeed')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feed_feedback', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("score", models.IntegerField(default=0)), + ("created_date", models.DateField(auto_now_add=True)), + ( + "recommendation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="feedback", + to="recommendations.RecommendedFeed", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="feed_feedback", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/apps/recommendations/models.py b/apps/recommendations/models.py index 5914fed7a0..afe00c3d12 100644 --- a/apps/recommendations/models.py +++ b/apps/recommendations/models.py @@ -1,72 +1,76 @@ +from collections import defaultdict + import mongoengine as mongo -from django.db import models from django.contrib.auth.models import User -from apps.rss_feeds.models import Feed +from django.db import models + from apps.reader.models import UserSubscription, UserSubscriptionFolders +from apps.rss_feeds.models import Feed from utils import json_functions as json -from collections import defaultdict + class RecommendedFeed(models.Model): - feed = models.ForeignKey(Feed, related_name='recommendations', on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name='recommendations', on_delete=models.CASCADE) - description = models.TextField(null=True, blank=True) - is_public = models.BooleanField(default=False) - created_date = models.DateField(auto_now_add=True) + feed = models.ForeignKey(Feed, related_name="recommendations", on_delete=models.CASCADE) + user = models.ForeignKey(User, related_name="recommendations", on_delete=models.CASCADE) + description = models.TextField(null=True, blank=True) + is_public = models.BooleanField(default=False) + created_date = models.DateField(auto_now_add=True) approved_date = models.DateField(null=True) declined_date = models.DateField(null=True) - twitter = models.CharField(max_length=50, null=True, blank=True) - + twitter = models.CharField(max_length=50, null=True, blank=True) + def __str__(self): return "%s (%s)" % (self.feed, self.approved_date or self.created_date) - + class Meta: - ordering = ['-approved_date', '-created_date'] + ordering = ["-approved_date", "-created_date"] class RecommendedFeedUserFeedback(models.Model): - recommendation = models.ForeignKey(RecommendedFeed, related_name='feedback', on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name='feed_feedback', on_delete=models.CASCADE) - score = models.IntegerField(default=0) - created_date = models.DateField(auto_now_add=True) + recommendation = models.ForeignKey(RecommendedFeed, related_name="feedback", on_delete=models.CASCADE) + user = models.ForeignKey(User, related_name="feed_feedback", on_delete=models.CASCADE) + score = models.IntegerField(default=0) + created_date = models.DateField(auto_now_add=True) + class MFeedFolder(mongo.Document): feed_id = mongo.IntField() folder = mongo.StringField() count = mongo.IntField() - + meta = { - 'collection': 'feed_folders', - 'indexes': ['feed_id', 'folder'], - 'allow_inheritance': False, + "collection": "feed_folders", + "indexes": ["feed_id", "folder"], + "allow_inheritance": False, } - + def __str__(self): feed = Feed.get_by_id(self.feed_id) return "%s - %s (%s)" % (feed, self.folder, self.count) - + @classmethod def count_feed(cls, feed_id): feed = Feed.get_by_id(feed_id) print(feed) found_folders = defaultdict(int) - user_ids = [sub['user_id'] for sub in UserSubscription.objects.filter(feed=feed).values('user_id')] + user_ids = [sub["user_id"] for sub in UserSubscription.objects.filter(feed=feed).values("user_id")] usf = UserSubscriptionFolders.objects.filter(user_id__in=user_ids) for sub in usf: user_sub_folders = json.decode(sub.folders) folder_title = cls.feed_folder_parent(user_sub_folders, feed.pk) - if not folder_title: continue + if not folder_title: + continue found_folders[folder_title.lower()] += 1 # print "%-20s - %s" % (folder_title if folder_title != '' else '[Top]', sub.user_id) print(sorted(list(found_folders.items()), key=lambda f: f[1], reverse=True)) - - + @classmethod - def feed_folder_parent(cls, folders, feed_id, folder_title=''): + def feed_folder_parent(cls, folders, feed_id, folder_title=""): for item in folders: if isinstance(item, int) and item == feed_id: return folder_title elif isinstance(item, dict): for f_k, f_v in list(item.items()): sub_folder_title = cls.feed_folder_parent(f_v, feed_id, f_k) - if sub_folder_title: + if sub_folder_title: return sub_folder_title diff --git a/apps/recommendations/templatetags/recommendations_tags.py b/apps/recommendations/templatetags/recommendations_tags.py index 3978381de8..2df4d1bb47 100644 --- a/apps/recommendations/templatetags/recommendations_tags.py +++ b/apps/recommendations/templatetags/recommendations_tags.py @@ -1,31 +1,32 @@ import datetime + from django import template + from apps.reader.models import UserSubscription -from utils.user_functions import get_user from apps.rss_feeds.models import MFeedIcon - +from utils.user_functions import get_user register = template.Library() -@register.inclusion_tag('recommendations/render_recommended_feed.xhtml', takes_context=True) + +@register.inclusion_tag("recommendations/render_recommended_feed.xhtml", takes_context=True) def render_recommended_feed(context, recommended_feeds, unmoderated=False): - user = get_user(context['user']) - + user = get_user(context["user"]) + usersub = None - if context['user'].is_authenticated: + if context["user"].is_authenticated: usersub = UserSubscription.objects.filter(user=user, feed=recommended_feeds[0].feed) recommended_feed = recommended_feeds and recommended_feeds[0] feed_icon = MFeedIcon.objects(feed_id=recommended_feed.feed_id) - + if recommended_feed: return { - 'recommended_feed' : recommended_feed, - 'description' : recommended_feed.description or recommended_feed.feed.data.feed_tagline, - 'usersub' : usersub, - 'feed_icon' : feed_icon and feed_icon[0], - 'user' : context['user'], - 'has_next_page' : len(recommended_feeds) > 1, - 'unmoderated' : unmoderated, - 'today' : datetime.datetime.now(), + "recommended_feed": recommended_feed, + "description": recommended_feed.description or recommended_feed.feed.data.feed_tagline, + "usersub": usersub, + "feed_icon": feed_icon and feed_icon[0], + "user": context["user"], + "has_next_page": len(recommended_feeds) > 1, + "unmoderated": unmoderated, + "today": datetime.datetime.now(), } - \ No newline at end of file diff --git a/apps/recommendations/tests.py b/apps/recommendations/tests.py index c7c4668e12..f51d798ffd 100644 --- a/apps/recommendations/tests.py +++ b/apps/recommendations/tests.py @@ -7,6 +7,7 @@ from django.test import TestCase + class SimpleTest(TestCase): def test_basic_addition(self): """ @@ -14,10 +15,12 @@ def test_basic_addition(self): """ self.assertEqual(1 + 1, 2) -__test__ = {"doctest": """ + +__test__ = { + "doctest": """ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True -"""} - +""" +} diff --git a/apps/recommendations/urls.py b/apps/recommendations/urls.py index 481b618e29..3899d33d86 100644 --- a/apps/recommendations/urls.py +++ b/apps/recommendations/urls.py @@ -1,10 +1,11 @@ from django.conf.urls import * + from apps.recommendations import views urlpatterns = [ - url(r'^load_recommended_feed', views.load_recommended_feed, name='load-recommended-feed'), - url(r'^save_recommended_feed', views.save_recommended_feed, name='save-recommended-feed'), - url(r'^approve_feed', views.approve_feed, name='approve-recommended-feed'), - url(r'^decline_feed', views.decline_feed, name='decline-recommended-feed'), - url(r'^load_feed_info/(?P\d+)', views.load_feed_info, name='load-recommended-feed-info'), + url(r"^load_recommended_feed", views.load_recommended_feed, name="load-recommended-feed"), + url(r"^save_recommended_feed", views.save_recommended_feed, name="save-recommended-feed"), + url(r"^approve_feed", views.approve_feed, name="approve-recommended-feed"), + url(r"^decline_feed", views.decline_feed, name="decline-recommended-feed"), + url(r"^load_feed_info/(?P\d+)", views.load_feed_info, name="load-recommended-feed-info"), ] diff --git a/apps/recommendations/views.py b/apps/recommendations/views.py index ae9ac60654..c5c1f99a14 100644 --- a/apps/recommendations/views.py +++ b/apps/recommendations/views.py @@ -1,53 +1,64 @@ -import re import datetime -from utils import log as logging +import re + from django.http import HttpResponse -from django.shortcuts import render, get_object_or_404 -from apps.recommendations.models import RecommendedFeed +from django.shortcuts import get_object_or_404, render + from apps.reader.models import UserSubscription +from apps.recommendations.models import RecommendedFeed from apps.rss_feeds.models import Feed, MFeedIcon from utils import json_functions as json -from utils.user_functions import get_user, ajax_login_required, admin_only +from utils import log as logging +from utils.user_functions import admin_only, ajax_login_required, get_user def load_recommended_feed(request): - user = get_user(request) - page = max(int(request.GET.get('page', 0)), 0) - usersub = None - refresh = request.GET.get('refresh') - now = datetime.datetime.now() - unmoderated = request.GET.get('unmoderated', False) == 'true' - + user = get_user(request) + page = max(int(request.GET.get("page", 0)), 0) + usersub = None + refresh = request.GET.get("refresh") + now = datetime.datetime.now() + unmoderated = request.GET.get("unmoderated", False) == "true" + if unmoderated: - recommended_feeds = RecommendedFeed.objects.filter(is_public=False, declined_date__isnull=True)[page:page+2] + recommended_feeds = RecommendedFeed.objects.filter(is_public=False, declined_date__isnull=True)[ + page : page + 2 + ] else: - recommended_feeds = RecommendedFeed.objects.filter(is_public=True, approved_date__lte=now)[page:page+2] + recommended_feeds = RecommendedFeed.objects.filter(is_public=True, approved_date__lte=now)[ + page : page + 2 + ] if recommended_feeds and request.user.is_authenticated: usersub = UserSubscription.objects.filter(user=user, feed=recommended_feeds[0].feed) - if refresh != 'true' and page > 0: - logging.user(request, "~FBBrowse recommended feed: ~SBPage #%s" % (page+1)) - + if refresh != "true" and page > 0: + logging.user(request, "~FBBrowse recommended feed: ~SBPage #%s" % (page + 1)) + recommended_feed = recommended_feeds and recommended_feeds[0] if not recommended_feeds: return HttpResponse("") - + feed_icon = MFeedIcon.objects(feed_id=recommended_feed.feed_id) - + if recommended_feed: - return render(request, 'recommendations/render_recommended_feed.xhtml', { - 'recommended_feed' : recommended_feed, - 'description' : recommended_feed.description or recommended_feed.feed.data.feed_tagline, - 'usersub' : usersub, - 'feed_icon' : feed_icon and feed_icon[0], - 'has_next_page' : len(recommended_feeds) > 1, - 'has_previous_page' : page != 0, - 'unmoderated' : unmoderated, - 'today' : datetime.datetime.now(), - 'page' : page, - }) + return render( + request, + "recommendations/render_recommended_feed.xhtml", + { + "recommended_feed": recommended_feed, + "description": recommended_feed.description or recommended_feed.feed.data.feed_tagline, + "usersub": usersub, + "feed_icon": feed_icon and feed_icon[0], + "has_next_page": len(recommended_feeds) > 1, + "has_previous_page": page != 0, + "unmoderated": unmoderated, + "today": datetime.datetime.now(), + "page": page, + }, + ) else: return HttpResponse("") - + + @json.json_view def load_feed_info(request, feed_id): feed = get_object_or_404(Feed, pk=feed_id) @@ -56,58 +67,56 @@ def load_feed_info(request, feed_id): recommended_feed = RecommendedFeed.objects.filter(user=request.user, feed=feed) if recommended_feed: previous_recommendation = recommended_feed[0].created_date - + return { - 'num_subscribers': feed.num_subscribers, - 'tagline': feed.data.feed_tagline, - 'previous_recommendation': previous_recommendation + "num_subscribers": feed.num_subscribers, + "tagline": feed.data.feed_tagline, + "previous_recommendation": previous_recommendation, } - + + @ajax_login_required @json.json_view def save_recommended_feed(request): - feed_id = request.POST['feed_id'] - feed = get_object_or_404(Feed, pk=int(feed_id)) - tagline = request.POST['tagline'] - twitter = request.POST.get('twitter') - code = 1 - + feed_id = request.POST["feed_id"] + feed = get_object_or_404(Feed, pk=int(feed_id)) + tagline = request.POST["tagline"] + twitter = request.POST.get("twitter") + code = 1 + recommended_feed, created = RecommendedFeed.objects.get_or_create( - feed=feed, - user=request.user, - defaults=dict( - description=tagline, - twitter=twitter - ) + feed=feed, user=request.user, defaults=dict(description=tagline, twitter=twitter) ) return dict(code=code if created else -1) - + + @admin_only @ajax_login_required def approve_feed(request): - feed_id = request.POST['feed_id'] - feed = get_object_or_404(Feed, pk=int(feed_id)) - date = request.POST['date'] + feed_id = request.POST["feed_id"] + feed = get_object_or_404(Feed, pk=int(feed_id)) + date = request.POST["date"] recommended_feed = RecommendedFeed.objects.filter(feed=feed)[0] - - year, month, day = re.search(r'(\d{4})-(\d{1,2})-(\d{1,2})', date).groups() + + year, month, day = re.search(r"(\d{4})-(\d{1,2})-(\d{1,2})", date).groups() recommended_feed.is_public = True recommended_feed.approved_date = datetime.date(int(year), int(month), int(day)) recommended_feed.save() - + return load_recommended_feed(request) + @admin_only @ajax_login_required def decline_feed(request): - feed_id = request.GET['feed_id'] - feed = get_object_or_404(Feed, pk=int(feed_id)) + feed_id = request.GET["feed_id"] + feed = get_object_or_404(Feed, pk=int(feed_id)) recommended_feeds = RecommendedFeed.objects.filter(feed=feed) - + for recommended_feed in recommended_feeds: recommended_feed.is_public = False recommended_feed.declined_date = datetime.datetime.now() recommended_feed.save() - + return load_recommended_feed(request) diff --git a/apps/rss_feeds/admin.py b/apps/rss_feeds/admin.py index c53b4799c3..10a33a8a37 100644 --- a/apps/rss_feeds/admin.py +++ b/apps/rss_feeds/admin.py @@ -1,4 +1,5 @@ -from apps.rss_feeds.models import Feed from django.contrib import admin +from apps.rss_feeds.models import Feed + admin.site.register(Feed) diff --git a/apps/rss_feeds/factories.py b/apps/rss_feeds/factories.py index 74abbdcc7e..613ca2a0df 100644 --- a/apps/rss_feeds/factories.py +++ b/apps/rss_feeds/factories.py @@ -1,31 +1,35 @@ -from faker import Faker import factory +from django.conf import settings from factory.django import DjangoModelFactory from factory.fuzzy import FuzzyAttribute +from faker import Faker + from apps.rss_feeds.models import DuplicateFeed, Feed -from django.conf import settings NEWSBLUR_DIR = settings.NEWSBLUR_DIR fake = Faker() + def generate_address(): return f"{NEWSBLUR_DIR}/apps/analyzer/fixtures/{fake.word()}.xml" + class FeedFactory(DjangoModelFactory): feed_address = FuzzyAttribute(generate_address) feed_link = FuzzyAttribute(generate_address) - creation = factory.Faker('date') - feed_title = factory.Faker('sentence') - last_update = factory.Faker('date_time') - next_scheduled_update = factory.Faker('date_time') - last_story_date = factory.Faker('date_time') + creation = factory.Faker("date") + feed_title = factory.Faker("sentence") + last_update = factory.Faker("date_time") + next_scheduled_update = factory.Faker("date_time") + last_story_date = factory.Faker("date_time") min_to_decay = 1 - last_modified = factory.Faker('date_time') + last_modified = factory.Faker("date_time") hash_address_and_link = fake.sha1() class Meta: model = Feed + class DuplicateFeedFactory(DjangoModelFactory): class Meta: - model = DuplicateFeed \ No newline at end of file + model = DuplicateFeed diff --git a/apps/rss_feeds/icon_importer.py b/apps/rss_feeds/icon_importer.py index 95108b55c4..340db6342b 100644 --- a/apps/rss_feeds/icon_importer.py +++ b/apps/rss_feeds/icon_importer.py @@ -10,6 +10,7 @@ from io import BytesIO from socket import error as SocketError +import numpy as np import boto3 import lxml.html import numpy @@ -33,7 +34,6 @@ class IconImporter(object): - def __init__(self, feed, page_data=None, force=False): self.feed = feed self.force = force @@ -45,27 +45,27 @@ def save(self): # print 'Not found, skipping...' return if ( - not self.force - and not self.feed.favicon_not_found - and self.feed_icon.icon_url - and self.feed.s3_icon + not self.force + and not self.feed.favicon_not_found + and self.feed_icon.icon_url + and self.feed.s3_icon ): # print 'Found, but skipping...' return - if 'facebook.com' in self.feed.feed_address: + if "facebook.com" in self.feed.feed_address: image, image_file, icon_url = self.fetch_facebook_image() else: image, image_file, icon_url = self.fetch_image_from_page_data() if not image: image, image_file, icon_url = self.fetch_image_from_path(force=self.force) - + if not image: self.feed_icon.not_found = True self.feed_icon.save() self.feed.favicon_not_found = True self.feed.save() return False - + image = self.normalize_image(image) try: color = self.determine_dominant_color_in_image(image) @@ -79,49 +79,53 @@ def save(self): if len(image_str) > 500000: image = None - if (image and - (self.force or - self.feed_icon.data != image_str or - self.feed_icon.icon_url != icon_url or - self.feed_icon.not_found or - (settings.BACKED_BY_AWS.get('icons_on_s3') and not self.feed.s3_icon))): - logging.debug(" ---> [%-30s] ~SN~FBIcon difference:~FY color:%s (%s/%s) data:%s url:%s notfound:%s no-s3:%s" % ( - self.feed.log_title[:30], - self.feed_icon.color != color, self.feed_icon.color, color, - self.feed_icon.data != image_str, - self.feed_icon.icon_url != icon_url, - self.feed_icon.not_found, - settings.BACKED_BY_AWS.get('icons_on_s3') and not self.feed.s3_icon)) + if image and ( + self.force + or self.feed_icon.data != image_str + or self.feed_icon.icon_url != icon_url + or self.feed_icon.not_found + or (settings.BACKED_BY_AWS.get("icons_on_s3") and not self.feed.s3_icon) + ): + logging.debug( + " ---> [%-30s] ~SN~FBIcon difference:~FY color:%s (%s/%s) data:%s url:%s notfound:%s no-s3:%s" + % ( + self.feed.log_title[:30], + self.feed_icon.color != color, + self.feed_icon.color, + color, + self.feed_icon.data != image_str, + self.feed_icon.icon_url != icon_url, + self.feed_icon.not_found, + settings.BACKED_BY_AWS.get("icons_on_s3") and not self.feed.s3_icon, + ) + ) self.feed_icon.data = image_str self.feed_icon.icon_url = icon_url self.feed_icon.color = color self.feed_icon.not_found = False self.feed_icon.save() - if settings.BACKED_BY_AWS.get('icons_on_s3'): + if settings.BACKED_BY_AWS.get("icons_on_s3"): self.save_to_s3(image_str) if self.feed.favicon_color != color: self.feed.favicon_color = color self.feed.favicon_not_found = False - self.feed.save(update_fields=['favicon_color', 'favicon_not_found']) - + self.feed.save(update_fields=["favicon_color", "favicon_not_found"]) + return not self.feed.favicon_not_found def save_to_s3(self, image_str): expires = datetime.datetime.now() + datetime.timedelta(days=60) expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT") base64.b64decode(image_str) - settings.S3_CONN.Object(settings.S3_ICONS_BUCKET_NAME, - self.feed.s3_icons_key).put(Body=base64.b64decode(image_str), - ContentType='image/png', - Expires=expires, - ACL='public-read' - ) + settings.S3_CONN.Object(settings.S3_ICONS_BUCKET_NAME, self.feed.s3_icons_key).put( + Body=base64.b64decode(image_str), ContentType="image/png", Expires=expires, ACL="public-read" + ) self.feed.s3_icon = True self.feed.save() def load_icon(self, image_file, index=None): - ''' + """ DEPRECATED Load Windows ICO image. @@ -130,10 +134,10 @@ def load_icon(self, image_file, index=None): description. Cribbed and modified from http://djangosnippets.org/snippets/1287/ - ''' + """ try: image_file.seek(0) - header = struct.unpack('<3H', image_file.read(6)) + header = struct.unpack("<3H", image_file.read(6)) except Exception: return @@ -144,7 +148,7 @@ def load_icon(self, image_file, index=None): # Collect icon directories directories = [] for i in range(header[2]): - directory = list(struct.unpack('<4B2H2I', image_file.read(16))) + directory = list(struct.unpack("<4B2H2I", image_file.read(16))) for j in range(3): if not directory[j]: directory[j] = 256 @@ -175,7 +179,7 @@ def load_icon(self, image_file, index=None): image = BmpImagePlugin.DibImageFile(image_file) except IOError: return - if image.mode == 'RGBA': + if image.mode == "RGBA": # Windows XP 32-bit color depth icon without AND bitmap pass else: @@ -194,10 +198,9 @@ def load_icon(self, image_file, index=None): # Load AND bitmap image_file.seek(offset) string = image_file.read(size) - mask = Image.frombytes('1', image.size, string, 'raw', - ('1;I', stride, -1)) + mask = Image.frombytes("1", image.size, string, "raw", ("1;I", stride, -1)) - image = image.convert('RGBA') + image = image.convert("RGBA") image.putalpha(mask) return image @@ -208,7 +211,7 @@ def fetch_image_from_page_data(self): content = None if self.page_data: content = self.page_data - elif settings.BACKED_BY_AWS.get('pages_on_node'): + elif settings.BACKED_BY_AWS.get("pages_on_node"): domain = "node-page.service.consul:8008" if settings.DOCKERBUILD: domain = "node:8008" @@ -222,7 +225,7 @@ def fetch_image_from_page_data(self): content = page_response.content except requests.ConnectionError: pass - elif settings.BACKED_BY_AWS.get('pages_on_s3') and self.feed.s3_page: + elif settings.BACKED_BY_AWS.get("pages_on_s3") and self.feed.s3_page: key = settings.S3_CONN.Bucket(settings.S3_PAGES_BUCKET_NAME).Object(key=self.feed.s3_pages_key) compressed_content = key.get()["Body"].read() stream = BytesIO(compressed_content) @@ -238,28 +241,35 @@ def fetch_image_from_page_data(self): try: content = requests.get(self.cleaned_feed_link, timeout=10).content url = self._url_from_html(content) - except (AttributeError, SocketError, requests.ConnectionError, - requests.models.MissingSchema, requests.sessions.InvalidSchema, - requests.sessions.TooManyRedirects, - requests.models.InvalidURL, - requests.models.ChunkedEncodingError, - requests.models.ContentDecodingError, - http.client.IncompleteRead, - requests.adapters.ReadTimeout, - LocationParseError, OpenSSLError, PyAsn1Error, - ValueError) as e: + except ( + AttributeError, + SocketError, + requests.ConnectionError, + requests.models.MissingSchema, + requests.sessions.InvalidSchema, + requests.sessions.TooManyRedirects, + requests.models.InvalidURL, + requests.models.ChunkedEncodingError, + requests.models.ContentDecodingError, + http.client.IncompleteRead, + requests.adapters.ReadTimeout, + LocationParseError, + OpenSSLError, + PyAsn1Error, + ValueError, + ) as e: logging.debug(" ---> ~SN~FRFailed~FY to fetch ~FGfeed icon~FY: %s" % e) if url: image, image_file = self.get_image_from_url(url) return image, image_file, url - + @property def cleaned_feed_link(self): - if self.feed.feed_link.startswith('http'): + if self.feed.feed_link.startswith("http"): return self.feed.feed_link - return 'http://' + self.feed.feed_link - - def fetch_image_from_path(self, path='favicon.ico', force=False): + return "http://" + self.feed.feed_link + + def fetch_image_from_path(self, path="favicon.ico", force=False): image = None url = None @@ -267,7 +277,7 @@ def fetch_image_from_path(self, path='favicon.ico', force=False): url = self.feed_icon.icon_url if not url and self.feed.feed_link and len(self.feed.feed_link) > 6: try: - url = urllib.parse.urljoin(self.feed.feed_link, 'favicon.ico') + url = urllib.parse.urljoin(self.feed.feed_link, "favicon.ico") except ValueError: url = None if not url: @@ -275,21 +285,21 @@ def fetch_image_from_path(self, path='favicon.ico', force=False): image, image_file = self.get_image_from_url(url) if not image: - url = urllib.parse.urljoin(self.feed.feed_link, '/favicon.ico') + url = urllib.parse.urljoin(self.feed.feed_link, "/favicon.ico") image, image_file = self.get_image_from_url(url) # print 'Found: %s - %s' % (url, image) return image, image_file, url - + def fetch_facebook_image(self): facebook_fetcher = FacebookFetcher(self.feed) url = facebook_fetcher.favicon_url() image, image_file = self.get_image_from_url(url) if not image: - url = urllib.parse.urljoin(self.feed.feed_link, '/favicon.ico') + url = urllib.parse.urljoin(self.feed.feed_link, "/favicon.ico") image, image_file = self.get_image_from_url(url) # print 'Found: %s - %s' % (url, image) return image, image_file, url - + def get_image_from_url(self, url): # print 'Requesting: %s' % url if not url: @@ -298,15 +308,15 @@ def get_image_from_url(self, url): @timelimit(30) def _1(url): headers = { - 'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s %s' % - ( - self.feed.num_subscribers, - 's' if self.feed.num_subscribers != 1 else '', - self.feed.permalink, - self.feed.fake_user_agent, - ), - 'Connection': 'close', - 'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8' + "User-Agent": "NewsBlur Favicon Fetcher - %s subscriber%s - %s %s" + % ( + self.feed.num_subscribers, + "s" if self.feed.num_subscribers != 1 else "", + self.feed.permalink, + self.feed.fake_user_agent, + ), + "Connection": "close", + "Accept": "image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8", } try: request = urllib.request.Request(url, headers=headers) @@ -314,6 +324,7 @@ def _1(url): except Exception: return None return icon + try: icon = _1(url) except TimeoutError: @@ -333,7 +344,7 @@ def _url_from_html(self, content): return url try: if isinstance(content, str): - content = content.encode('utf-8') + content = content.encode("utf-8") icon_path = lxml.html.fromstring(content).xpath( '//link[@rel="icon" or @rel="shortcut icon"]/@href' ) @@ -341,7 +352,7 @@ def _url_from_html(self, content): return url if icon_path: - if str(icon_path[0]).startswith('http'): + if str(icon_path[0]).startswith("http"): url = icon_path[0] else: url = urllib.parse.urljoin(self.feed.feed_link, icon_path[0]) @@ -350,9 +361,9 @@ def _url_from_html(self, content): def normalize_image(self, image): # if image.size != (16, 16): # image = image.resize((16, 16), Image.BICUBIC) - if image.mode != 'RGBA': + if image.mode != "RGBA": try: - image = image.convert('RGBA') + image = image.convert("RGBA") except IOError: pass @@ -362,26 +373,33 @@ def determine_dominant_color_in_image(self, image): NUM_CLUSTERS = 5 # Convert image into array of values for each point. - if image.mode == '1': - image.convert('L') + if image.mode == "1": + image.convert("L") ar = numpy.array(image) # ar = scipy.misc.fromimage(image) shape = ar.shape # Reshape array of values to merge color bands. [[R], [G], [B], [A]] => [R, G, B, A] if len(shape) > 2: - ar = ar.reshape(scipy.product(shape[:2]), shape[2]) - + ar = ar.reshape(np.product(shape[:2]), shape[2]) + # Get NUM_CLUSTERS worth of centroids. - ar = ar.astype(numpy.float) + ar = ar.astype(float) codes, _ = scipy.cluster.vq.kmeans(ar, NUM_CLUSTERS) # Pare centroids, removing blacks and whites and shades of really dark and really light. original_codes = codes for low, hi in [(60, 200), (35, 230), (10, 250)]: - codes = scipy.array([code for code in codes - if not ((code[0] < low and code[1] < low and code[2] < low) or - (code[0] > hi and code[1] > hi and code[2] > hi))]) + codes = np.array( + [ + code + for code in codes + if not ( + (code[0] < low and code[1] < low and code[2] < low) + or (code[0] > hi and code[1] > hi and code[2] > hi) + ) + ] + ) if not len(codes): codes = original_codes else: @@ -392,7 +410,7 @@ def determine_dominant_color_in_image(self, image): vecs, _ = scipy.cluster.vq.vq(ar, codes) # Count occurences of each clustered vector. - counts, bins = scipy.histogram(vecs, len(codes)) + counts, bins = np.histogram(vecs, len(codes)) # Show colors for each code in its hex value. # colors = [''.join(chr(c) for c in code).encode('hex') for code in codes] @@ -400,7 +418,7 @@ def determine_dominant_color_in_image(self, image): # print dict(zip(colors, [count/float(total) for count in counts])) # Find the most frequent color, based on the counts. - index_max = scipy.argmax(counts) + index_max = np.argmax(counts) peak = codes.astype(int)[index_max] color = "{:02x}{:02x}{:02x}".format(peak[0], peak[1], peak[2]) color = self.feed.adjust_color(color[:6], 21) @@ -409,7 +427,7 @@ def determine_dominant_color_in_image(self, image): def string_from_image(self, image): output = BytesIO() - image.save(output, 'png', quality=95) + image.save(output, "png", quality=95) contents = output.getvalue() output.close() return base64.b64encode(contents).decode() diff --git a/apps/rss_feeds/management/commands/calculate_scores.py b/apps/rss_feeds/management/commands/calculate_scores.py index 8f914f28c4..f281404f64 100644 --- a/apps/rss_feeds/management/commands/calculate_scores.py +++ b/apps/rss_feeds/management/commands/calculate_scores.py @@ -1,61 +1,75 @@ -from django.core.management.base import BaseCommand -from apps.reader.models import UserSubscription -from django.conf import settings -from django.contrib.auth.models import User -import os +import datetime import errno +import os import re -import datetime -class Command(BaseCommand): +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from apps.reader.models import UserSubscription + +class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument("-a", "--all", dest="all", action="store_true", help="All feeds, need it or not (can be combined with a user)"), - parser.add_argument("-s", "--silent", dest="silent", default=False, action="store_true", help="Inverse verbosity."), + parser.add_argument( + "-a", + "--all", + dest="all", + action="store_true", + help="All feeds, need it or not (can be combined with a user)", + ), + parser.add_argument( + "-s", "--silent", dest="silent", default=False, action="store_true", help="Inverse verbosity." + ), parser.add_argument("-u", "--user", dest="user", nargs=1, help="Specify user id or username"), parser.add_argument("-d", "--daemon", dest="daemonize", action="store_true"), - parser.add_argument("-D", "--days", dest="days", nargs=1, default=1, type='int'), - parser.add_argument("-O", "--offset", dest="offset", nargs=1, default=0, type='int'), + parser.add_argument("-D", "--days", dest="days", nargs=1, default=1, type="int"), + parser.add_argument("-O", "--offset", dest="offset", nargs=1, default=0, type="int"), def handle(self, *args, **options): settings.LOG_TO_STREAM = True - if options['daemonize']: + if options["daemonize"]: daemonize() - if options['user']: - if re.match(r"([0-9]+)", options['user']): - users = User.objects.filter(pk=int(options['user'])) + if options["user"]: + if re.match(r"([0-9]+)", options["user"]): + users = User.objects.filter(pk=int(options["user"])) else: - users = User.objects.filter(username=options['user']) + users = User.objects.filter(username=options["user"]) else: - users = User.objects.filter(profile__last_seen_on__gte=datetime.datetime.now()-datetime.timedelta(days=options['days'])).order_by('pk') - + users = User.objects.filter( + profile__last_seen_on__gte=datetime.datetime.now() - datetime.timedelta(days=options["days"]) + ).order_by("pk") + user_count = users.count() for i, u in enumerate(users): - if i < options['offset']: continue - if options['all']: + if i < options["offset"]: + continue + if options["all"]: usersubs = UserSubscription.objects.filter(user=u, active=True) else: usersubs = UserSubscription.objects.filter(user=u, needs_unread_recalc=True) - print((" ---> %s has %s feeds (%s/%s)" % (u.username, usersubs.count(), i+1, user_count))) + print((" ---> %s has %s feeds (%s/%s)" % (u.username, usersubs.count(), i + 1, user_count))) for sub in usersubs: try: - sub.calculate_feed_scores(silent=options['silent']) + sub.calculate_feed_scores(silent=options["silent"]) except Exception as e: print((" ***> Exception: %s" % e)) continue - + + def daemonize(): """ Detach from the terminal and continue as a daemon. """ # swiped from twisted/scripts/twistd.py # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16 - if os.fork(): # launch child and... - os._exit(0) # kill off parent + if os.fork(): # launch child and... + os._exit(0) # kill off parent os.setsid() - if os.fork(): # launch child and... - os._exit(0) # kill off parent again. + if os.fork(): # launch child and... + os._exit(0) # kill off parent again. os.umask(0o77) null = os.open("/dev/null", os.O_RDWR) for i in range(3): @@ -64,4 +78,4 @@ def daemonize(): except OSError as e: if e.errno != errno.EBADF: raise - os.close(null) \ No newline at end of file + os.close(null) diff --git a/apps/rss_feeds/management/commands/clean_txt_records.py b/apps/rss_feeds/management/commands/clean_txt_records.py new file mode 100644 index 0000000000..f72b653bfd --- /dev/null +++ b/apps/rss_feeds/management/commands/clean_txt_records.py @@ -0,0 +1,56 @@ +import requests +from django.core.management.base import BaseCommand +from django.conf import settings + +class Command(BaseCommand): + help = 'Delete old TXT records for Let\'s Encrypt from DNSimple' + + def handle(self, *args, **kwargs): + API_TOKEN = settings.DNSIMPLE_API_TOKEN + ACCOUNT_ID = settings.DNSIMPLE_ACCOUNT_ID + DOMAIN = "newsblur.com" + LETSECRYPT_PREFIX = '_acme-challenge' + + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + + def get_txt_records(): + records = [] + page = 1 + while True: + url = f'https://api.dnsimple.com/v2/{ACCOUNT_ID}/zones/{DOMAIN}/records?page={page}' + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json().get('data', []) + records.extend(data) + if 'pagination' in response.json(): + pagination = response.json()['pagination'] + if pagination['current_page'] < pagination['total_pages']: + page += 1 + else: + break + else: + break + else: + self.stderr.write(f"Failed to fetch records: {response.status_code} {response.text}") + break + return records + + def delete_record(record_id): + url = f'https://api.dnsimple.com/v2/{ACCOUNT_ID}/zones/{DOMAIN}/records/{record_id}' + response = requests.delete(url, headers=headers) + if response.status_code == 204: + self.stdout.write(f"Deleted record {record_id}") + else: + self.stderr.write(f"Failed to delete record {record_id}: {response.status_code} {response.text}") + + records = get_txt_records() + self.stdout.write(f"Found {len(records)} records") + for record in records: + # self.stdout.write(f"Record: {record}") + if record['type'] == 'TXT' and record['name'].startswith(LETSECRYPT_PREFIX): + self.stdout.write(f"Deleting record {record['id']} {record['name']} {record['content']}") + delete_record(record['id']) diff --git a/apps/rss_feeds/management/commands/count_stories.py b/apps/rss_feeds/management/commands/count_stories.py index 06d7ba91bd..b9679bc2e5 100644 --- a/apps/rss_feeds/management/commands/count_stories.py +++ b/apps/rss_feeds/management/commands/count_stories.py @@ -1,23 +1,24 @@ from django.core.management.base import BaseCommand + from apps.rss_feeds.models import Feed -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-f", "--feed", dest="feed", default=None) parser.add_argument("-t", "--title", dest="title", default=None) parser.add_argument("-V", "--verbose", dest="verbose", action="store_true") - + def handle(self, *args, **options): - if options['title']: - feeds = Feed.objects.filter(feed_title__icontains=options['title']) - elif options['feed']: - feeds = Feed.objects.filter(pk=options['feed']) + if options["title"]: + feeds = Feed.objects.filter(feed_title__icontains=options["title"]) + elif options["feed"]: + feeds = Feed.objects.filter(pk=options["feed"]) else: feeds = Feed.objects.all() # Count stories in past month to calculate next scheduled update for feed in feeds: - feed.count_stories(verbose=options['verbose']) - - print(("\nCounted %s feeds" % feeds.count())) \ No newline at end of file + feed.count_stories(verbose=options["verbose"]) + + print(("\nCounted %s feeds" % feeds.count())) diff --git a/apps/rss_feeds/management/commands/count_subscribers.py b/apps/rss_feeds/management/commands/count_subscribers.py index 40757a44d2..7a5adb0fd9 100644 --- a/apps/rss_feeds/management/commands/count_subscribers.py +++ b/apps/rss_feeds/management/commands/count_subscribers.py @@ -1,34 +1,35 @@ from django.core.management.base import BaseCommand + from apps.rss_feeds.models import Feed -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-f", "--feed", dest="feed", default=None) parser.add_argument("-t", "--title", dest="title", default=None) parser.add_argument("-V", "--verbose", dest="verbose", action="store_true") parser.add_argument("-D", "--delete", dest="delete", action="store_true") - + def handle(self, *args, **options): - if options['title']: - feeds = Feed.objects.filter(feed_title__icontains=options['title']) - elif options['feed']: - feeds = Feed.objects.filter(pk=options['feed']) + if options["title"]: + feeds = Feed.objects.filter(feed_title__icontains=options["title"]) + elif options["feed"]: + feeds = Feed.objects.filter(pk=options["feed"]) else: feeds = Feed.objects.all() - + feeds_count = feeds.count() - + for i in range(0, feeds_count, 100): - feeds = Feed.objects.all()[i:i+100] + feeds = Feed.objects.all()[i : i + 100] for feed in feeds.iterator(): - feed.count_subscribers(verbose=options['verbose']) - - if options['delete']: + feed.count_subscribers(verbose=options["verbose"]) + + if options["delete"]: print("# Deleting old feeds...") old_feeds = Feed.objects.filter(num_subscribers=0) for feed in old_feeds: feed.count_subscribers(verbose=True) if feed.num_subscribers == 0: - print((' ---> Deleting: [%s] %s' % (feed.pk, feed))) - feed.delete() \ No newline at end of file + print((" ---> Deleting: [%s] %s" % (feed.pk, feed))) + feed.delete() diff --git a/apps/rss_feeds/management/commands/mark_read.py b/apps/rss_feeds/management/commands/mark_read.py index f72d4f5f37..29eda767ae 100644 --- a/apps/rss_feeds/management/commands/mark_read.py +++ b/apps/rss_feeds/management/commands/mark_read.py @@ -1,30 +1,34 @@ -from django.core.management.base import BaseCommand +import datetime + from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + from apps.reader.models import UserSubscription -import datetime -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-d", "--days", dest="days", nargs=1, default=1, help="Days of unread") parser.add_argument("-u", "--username", dest="username", nargs=1, help="Specify user id or username") parser.add_argument("-U", "--userid", dest="userid", nargs=1, help="Specify user id or username") - + def handle(self, *args, **options): - if options['userid']: - user = User.objects.filter(pk=options['userid'])[0] - elif options['username']: - user = User.objects.get(username__icontains=options['username']) + if options["userid"]: + user = User.objects.filter(pk=options["userid"])[0] + elif options["username"]: + user = User.objects.get(username__icontains=options["username"]) else: raise Exception("Need username or user id.") - + user.profile.last_seen_on = datetime.datetime.utcnow() user.profile.save() feeds = UserSubscription.objects.filter(user=user) for sub in feeds: - if options['days'] == 0: + if options["days"] == 0: sub.mark_feed_read() else: - sub.mark_read_date = datetime.datetime.utcnow() - datetime.timedelta(days=int(options['days'])) + sub.mark_read_date = datetime.datetime.utcnow() - datetime.timedelta( + days=int(options["days"]) + ) sub.needs_unread_recalc = True - sub.save() \ No newline at end of file + sub.save() diff --git a/apps/rss_feeds/management/commands/query_popularity.py b/apps/rss_feeds/management/commands/query_popularity.py index 65acd70fc6..4e2b3ff598 100644 --- a/apps/rss_feeds/management/commands/query_popularity.py +++ b/apps/rss_feeds/management/commands/query_popularity.py @@ -1,15 +1,17 @@ -from django.core.management.base import BaseCommand -from apps.reader.models import UserSubscription +import datetime +import errno +import os +import re + from django.conf import settings from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from apps.reader.models import UserSubscription from apps.rss_feeds.models import Feed -import os -import errno -import re -import datetime -class Command(BaseCommand): +class Command(BaseCommand): def add_argument(self, parser): parser.add_argument("-q", "--query", dest="query", help="Search query") parser.add_argument("-l", "--limit", dest="limit", type="int", default=1000, help="Limit of stories") @@ -18,4 +20,4 @@ def handle(self, *args, **options): # settings.LOG_TO_STREAM = True # Feed.query_popularity(options['query'], limit=options['limit']) - Feed.xls_query_popularity(options['query'], limit=options['limit']) \ No newline at end of file + Feed.xls_query_popularity(options["query"], limit=options["limit"]) diff --git a/apps/rss_feeds/management/commands/refresh_feed.py b/apps/rss_feeds/management/commands/refresh_feed.py index 63527d548d..9dd4ab5871 100644 --- a/apps/rss_feeds/management/commands/refresh_feed.py +++ b/apps/rss_feeds/management/commands/refresh_feed.py @@ -1,9 +1,10 @@ from django.core.management.base import BaseCommand + from apps.rss_feeds.models import Feed from utils.management_functions import daemonize -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-f", "--feed", dest="feed", default=None) parser.add_argument("-F", "--force", dest="force", action="store_true") @@ -11,11 +12,11 @@ def add_arguments(self, parser): parser.add_argument("-d", "--daemon", dest="daemonize", action="store_true") def handle(self, *args, **options): - if options['daemonize']: + if options["daemonize"]: daemonize() - - if options['title']: - feed = Feed.objects.get(feed_title__icontains=options['title']) + + if options["title"]: + feed = Feed.objects.get(feed_title__icontains=options["title"]) else: - feed = Feed.get_by_id(options['feed']) - feed.update(force=options['force'], single_threaded=True, verbose=True) + feed = Feed.get_by_id(options["feed"]) + feed.update(force=options["force"], single_threaded=True, verbose=True) diff --git a/apps/rss_feeds/management/commands/refresh_feeds.py b/apps/rss_feeds/management/commands/refresh_feeds.py index 44b2ed5054..3cac6d02db 100644 --- a/apps/rss_feeds/management/commands/refresh_feeds.py +++ b/apps/rss_feeds/management/commands/refresh_feeds.py @@ -1,88 +1,99 @@ -from django.core.management.base import BaseCommand +import datetime +import socket +from optparse import make_option + +import django from django.conf import settings from django.contrib.auth.models import User -from apps.statistics.models import MStatistics -from apps.rss_feeds.models import Feed +from django.core.management.base import BaseCommand + from apps.reader.models import UserSubscription -from optparse import make_option +from apps.rss_feeds.models import Feed +from apps.statistics.models import MStatistics from utils import feed_fetcher from utils.management_functions import daemonize -import django -import socket -import datetime class Command(BaseCommand): - def add_arguments(self, parser): parser.add_argument("-f", "--feed", default=None) parser.add_argument("-d", "--daemon", dest="daemonize", action="store_true") parser.add_argument("-F", "--force", dest="force", action="store_true") parser.add_argument("-s", "--single_threaded", dest="single_threaded", action="store_true") - parser.add_argument('-t', '--timeout', type=int, default=10, - help='Wait timeout in seconds when connecting to feeds.') - parser.add_argument('-u', '--username', type=str, dest='username') - parser.add_argument('-V', '--verbose', action='store_true', - dest='verbose', default=False, help='Verbose output.') - parser.add_argument('-S', '--skip', type=int, - dest='skip', default=0, help='Skip stories per month < #.') - parser.add_argument('-w', '--workerthreads', type=int, default=4, - help='Worker threads that will fetch feeds in parallel.') + parser.add_argument( + "-t", "--timeout", type=int, default=10, help="Wait timeout in seconds when connecting to feeds." + ) + parser.add_argument("-u", "--username", type=str, dest="username") + parser.add_argument( + "-V", "--verbose", action="store_true", dest="verbose", default=False, help="Verbose output." + ) + parser.add_argument( + "-S", "--skip", type=int, dest="skip", default=0, help="Skip stories per month < #." + ) + parser.add_argument( + "-w", + "--workerthreads", + type=int, + default=4, + help="Worker threads that will fetch feeds in parallel.", + ) def handle(self, *args, **options): - if options['daemonize']: + if options["daemonize"]: daemonize() - + settings.LOG_TO_STREAM = True now = datetime.datetime.utcnow() - - if options['skip']: - feeds = Feed.objects.filter(next_scheduled_update__lte=now, - average_stories_per_month__lt=options['skip'], - active=True) + + if options["skip"]: + feeds = Feed.objects.filter( + next_scheduled_update__lte=now, average_stories_per_month__lt=options["skip"], active=True + ) print(" ---> Skipping %s feeds" % feeds.count()) for feed in feeds: feed.set_next_scheduled_update() - print('.', end=' ') + print(".", end=" ") return - - socket.setdefaulttimeout(options['timeout']) - if options['force']: + + socket.setdefaulttimeout(options["timeout"]) + if options["force"]: feeds = Feed.objects.all() - elif options['username']: - usersubs = UserSubscription.objects.filter(user=User.objects.get(username=options['username']), active=True) - feeds = Feed.objects.filter(pk__in=usersubs.values('feed_id')) - elif options['feed']: - feeds = Feed.objects.filter(pk=options['feed']) + elif options["username"]: + usersubs = UserSubscription.objects.filter( + user=User.objects.get(username=options["username"]), active=True + ) + feeds = Feed.objects.filter(pk__in=usersubs.values("feed_id")) + elif options["feed"]: + feeds = Feed.objects.filter(pk=options["feed"]) else: feeds = Feed.objects.filter(next_scheduled_update__lte=now, active=True) - - feeds = feeds.order_by('?') - + + feeds = feeds.order_by("?") + for f in feeds: f.set_next_scheduled_update() - - num_workers = min(len(feeds), options['workerthreads']) - if options['single_threaded']: + + num_workers = min(len(feeds), options["workerthreads"]) + if options["single_threaded"]: num_workers = 1 - - options['compute_scores'] = True - options['quick'] = float(MStatistics.get('quick_fetch', 0)) - options['updates_off'] = MStatistics.get('updates_off', False) - - disp = feed_fetcher.Dispatcher(options, num_workers) - + + options["compute_scores"] = True + options["quick"] = float(MStatistics.get("quick_fetch", 0)) + options["updates_off"] = MStatistics.get("updates_off", False) + + disp = feed_fetcher.Dispatcher(options, num_workers) + feeds_queue = [] for _ in range(num_workers): feeds_queue.append([]) - + i = 0 for feed in feeds: - feeds_queue[i%num_workers].append(feed.pk) + feeds_queue[i % num_workers].append(feed.pk) i += 1 disp.add_jobs(feeds_queue, i) - + django.db.connection.close() - + print(" ---> Fetching %s feeds..." % feeds.count()) disp.run_jobs() diff --git a/apps/rss_feeds/management/commands/task_feeds.py b/apps/rss_feeds/management/commands/task_feeds.py index 794d8184bd..095ee896eb 100644 --- a/apps/rss_feeds/management/commands/task_feeds.py +++ b/apps/rss_feeds/management/commands/task_feeds.py @@ -1,20 +1,28 @@ -from django.core.management.base import BaseCommand -from django.conf import settings -from apps.rss_feeds.tasks import TaskFeeds, TaskBrokenFeeds import datetime +from django.conf import settings +from django.core.management.base import BaseCommand + +from apps.rss_feeds.tasks import TaskBrokenFeeds, TaskFeeds -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-f", "--feed", default=None) - parser.add_argument("-a", "--all", default=False, action='store_true') - parser.add_argument("-b", "--broken", help="Task broken feeds that havent been fetched in a day.", default=False, action='store_true') - parser.add_argument('-V', '--verbose', action='store_true', - dest='verbose', default=False, help='Verbose output.') - + parser.add_argument("-a", "--all", default=False, action="store_true") + parser.add_argument( + "-b", + "--broken", + help="Task broken feeds that havent been fetched in a day.", + default=False, + action="store_true", + ) + parser.add_argument( + "-V", "--verbose", action="store_true", dest="verbose", default=False, help="Verbose output." + ) + def handle(self, *args, **options): - if options['broken']: + if options["broken"]: TaskBrokenFeeds.apply() else: TaskFeeds.apply() diff --git a/apps/rss_feeds/management/commands/trim_feeds.py b/apps/rss_feeds/management/commands/trim_feeds.py index 6d6c1090bc..d4e3522ea3 100644 --- a/apps/rss_feeds/management/commands/trim_feeds.py +++ b/apps/rss_feeds/management/commands/trim_feeds.py @@ -1,28 +1,26 @@ +import gc + from django.core.management.base import BaseCommand + from apps.rss_feeds.models import Feed -import gc -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-f", "--feed", dest="feed", default=None), def handle(self, *args, **options): - if not options['feed']: - feeds = Feed.objects.filter( - fetched_once=True, - active_subscribers=0, - premium_subscribers=0 - ) + if not options["feed"]: + feeds = Feed.objects.filter(fetched_once=True, active_subscribers=0, premium_subscribers=0) else: - feeds = Feed.objects.filter(feed_id=options['feed']) + feeds = Feed.objects.filter(feed_id=options["feed"]) for f in queryset_iterator(feeds): f.trim_feed(verbose=True) - + def queryset_iterator(queryset, chunksize=100): - ''' + """ Iterate over a Django Queryset ordered by the primary key This method loads a maximum of chunksize (default: 1000) rows in it's @@ -31,12 +29,12 @@ def queryset_iterator(queryset, chunksize=100): classes. Note that the implementation of the iterator does not support ordered query sets. - ''' - last_pk = queryset.order_by('-pk')[0].pk - queryset = queryset.order_by('pk') + """ + last_pk = queryset.order_by("-pk")[0].pk + queryset = queryset.order_by("pk") pk = queryset[0].pk while pk < last_pk: for row in queryset.filter(pk__gte=pk, pk__lte=last_pk)[:chunksize]: yield row pk += chunksize - gc.collect() \ No newline at end of file + gc.collect() diff --git a/apps/rss_feeds/migrations/0001_initial.py b/apps/rss_feeds/migrations/0001_initial.py index e2e0255cd3..c3beb2c42e 100644 --- a/apps/rss_feeds/migrations/0001_initial.py +++ b/apps/rss_feeds/migrations/0001_initial.py @@ -1,88 +1,113 @@ # Generated by Django 2.0 on 2020-06-16 06:52 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + import utils.fields class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='DuplicateFeed', + name="DuplicateFeed", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('duplicate_address', models.CharField(db_index=True, max_length=764)), - ('duplicate_link', models.CharField(db_index=True, max_length=764, null=True)), - ('duplicate_feed_id', models.CharField(db_index=True, max_length=255, null=True)), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("duplicate_address", models.CharField(db_index=True, max_length=764)), + ("duplicate_link", models.CharField(db_index=True, max_length=764, null=True)), + ("duplicate_feed_id", models.CharField(db_index=True, max_length=255, null=True)), ], ), migrations.CreateModel( - name='Feed', + name="Feed", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('feed_address', models.URLField(db_index=True, max_length=764)), - ('feed_address_locked', models.NullBooleanField(default=False)), - ('feed_link', models.URLField(blank=True, default='', max_length=1000, null=True)), - ('feed_link_locked', models.BooleanField(default=False)), - ('hash_address_and_link', models.CharField(max_length=64, unique=True)), - ('feed_title', models.CharField(blank=True, default='[Untitled]', max_length=255, null=True)), - ('is_push', models.NullBooleanField(default=False)), - ('active', models.BooleanField(db_index=True, default=True)), - ('num_subscribers', models.IntegerField(default=-1)), - ('active_subscribers', models.IntegerField(db_index=True, default=-1)), - ('premium_subscribers', models.IntegerField(default=-1)), - ('active_premium_subscribers', models.IntegerField(default=-1)), - ('last_update', models.DateTimeField(db_index=True)), - ('next_scheduled_update', models.DateTimeField()), - ('last_story_date', models.DateTimeField(blank=True, null=True)), - ('fetched_once', models.BooleanField(default=False)), - ('known_good', models.BooleanField(default=False)), - ('has_feed_exception', models.BooleanField(db_index=True, default=False)), - ('has_page_exception', models.BooleanField(db_index=True, default=False)), - ('has_page', models.BooleanField(default=True)), - ('exception_code', models.IntegerField(default=0)), - ('errors_since_good', models.IntegerField(default=0)), - ('min_to_decay', models.IntegerField(default=0)), - ('days_to_trim', models.IntegerField(default=90)), - ('creation', models.DateField(auto_now_add=True)), - ('etag', models.CharField(blank=True, max_length=255, null=True)), - ('last_modified', models.DateTimeField(blank=True, null=True)), - ('stories_last_month', models.IntegerField(default=0)), - ('average_stories_per_month', models.IntegerField(default=0)), - ('last_load_time', models.IntegerField(default=0)), - ('favicon_color', models.CharField(blank=True, max_length=6, null=True)), - ('favicon_not_found', models.BooleanField(default=False)), - ('s3_page', models.NullBooleanField(default=False)), - ('s3_icon', models.NullBooleanField(default=False)), - ('search_indexed', models.NullBooleanField(default=None)), - ('branch_from_feed', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='rss_feeds.Feed')), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("feed_address", models.URLField(db_index=True, max_length=764)), + ("feed_address_locked", models.NullBooleanField(default=False)), + ("feed_link", models.URLField(blank=True, default="", max_length=1000, null=True)), + ("feed_link_locked", models.BooleanField(default=False)), + ("hash_address_and_link", models.CharField(max_length=64, unique=True)), + ("feed_title", models.CharField(blank=True, default="[Untitled]", max_length=255, null=True)), + ("is_push", models.NullBooleanField(default=False)), + ("active", models.BooleanField(db_index=True, default=True)), + ("num_subscribers", models.IntegerField(default=-1)), + ("active_subscribers", models.IntegerField(db_index=True, default=-1)), + ("premium_subscribers", models.IntegerField(default=-1)), + ("active_premium_subscribers", models.IntegerField(default=-1)), + ("last_update", models.DateTimeField(db_index=True)), + ("next_scheduled_update", models.DateTimeField()), + ("last_story_date", models.DateTimeField(blank=True, null=True)), + ("fetched_once", models.BooleanField(default=False)), + ("known_good", models.BooleanField(default=False)), + ("has_feed_exception", models.BooleanField(db_index=True, default=False)), + ("has_page_exception", models.BooleanField(db_index=True, default=False)), + ("has_page", models.BooleanField(default=True)), + ("exception_code", models.IntegerField(default=0)), + ("errors_since_good", models.IntegerField(default=0)), + ("min_to_decay", models.IntegerField(default=0)), + ("days_to_trim", models.IntegerField(default=90)), + ("creation", models.DateField(auto_now_add=True)), + ("etag", models.CharField(blank=True, max_length=255, null=True)), + ("last_modified", models.DateTimeField(blank=True, null=True)), + ("stories_last_month", models.IntegerField(default=0)), + ("average_stories_per_month", models.IntegerField(default=0)), + ("last_load_time", models.IntegerField(default=0)), + ("favicon_color", models.CharField(blank=True, max_length=6, null=True)), + ("favicon_not_found", models.BooleanField(default=False)), + ("s3_page", models.NullBooleanField(default=False)), + ("s3_icon", models.NullBooleanField(default=False)), + ("search_indexed", models.NullBooleanField(default=None)), + ( + "branch_from_feed", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="rss_feeds.Feed", + ), + ), ], options={ - 'db_table': 'feeds', - 'ordering': ['feed_title'], + "db_table": "feeds", + "ordering": ["feed_title"], }, ), migrations.CreateModel( - name='FeedData', + name="FeedData", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('feed_tagline', models.CharField(blank=True, max_length=1024, null=True)), - ('story_count_history', models.TextField(blank=True, null=True)), - ('feed_classifier_counts', models.TextField(blank=True, null=True)), - ('popular_tags', models.CharField(blank=True, max_length=1024, null=True)), - ('popular_authors', models.CharField(blank=True, max_length=2048, null=True)), - ('feed', utils.fields.AutoOneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='data', to='rss_feeds.Feed')), + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("feed_tagline", models.CharField(blank=True, max_length=1024, null=True)), + ("story_count_history", models.TextField(blank=True, null=True)), + ("feed_classifier_counts", models.TextField(blank=True, null=True)), + ("popular_tags", models.CharField(blank=True, max_length=1024, null=True)), + ("popular_authors", models.CharField(blank=True, max_length=2048, null=True)), + ( + "feed", + utils.fields.AutoOneToOneField( + on_delete=django.db.models.deletion.CASCADE, related_name="data", to="rss_feeds.Feed" + ), + ), ], ), migrations.AddField( - model_name='duplicatefeed', - name='feed', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='duplicate_addresses', to='rss_feeds.Feed'), + model_name="duplicatefeed", + name="feed", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="duplicate_addresses", + to="rss_feeds.Feed", + ), ), ] diff --git a/apps/rss_feeds/migrations/0002_remove_mongo_types.py b/apps/rss_feeds/migrations/0002_remove_mongo_types.py index a6b98c7ba2..5acb061b90 100644 --- a/apps/rss_feeds/migrations/0002_remove_mongo_types.py +++ b/apps/rss_feeds/migrations/0002_remove_mongo_types.py @@ -1,7 +1,8 @@ # Generated by Django 3.1.4 on 2021-01-06 19:27 -from django.db import migrations from django.conf import settings +from django.db import migrations + def remove_mongo_types(apps, schema_editor): db = settings.MONGODB.newsblur_dev @@ -9,22 +10,20 @@ def remove_mongo_types(apps, schema_editor): for collection_name in collections: collection = db[collection_name] print(" ---> %s..." % (collection_name)) - if 'system' in collection_name: continue + if "system" in collection_name: + continue collection.update({}, {"$unset": {"_types": 1}}, multi=True) index_information = collection.index_information() - indexes_to_drop = [key for key, value in index_information.items() - if 'types' in value] + indexes_to_drop = [key for key, value in index_information.items() if "types" in value] # print(index_information, indexes_) for index in indexes_to_drop: print(" ---> Dropping mongo index %s on %s..." % (index, collection_name)) collection.drop_index(index) -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('rss_feeds', '0001_initial'), + ("rss_feeds", "0001_initial"), ] - operations = [ - migrations.RunPython(remove_mongo_types, migrations.RunPython.noop) - ] + operations = [migrations.RunPython(remove_mongo_types, migrations.RunPython.noop)] diff --git a/apps/rss_feeds/migrations/0003_auto_20220110_2105.py b/apps/rss_feeds/migrations/0003_auto_20220110_2105.py index 9986d3c7ec..e13a80de41 100644 --- a/apps/rss_feeds/migrations/0003_auto_20220110_2105.py +++ b/apps/rss_feeds/migrations/0003_auto_20220110_2105.py @@ -4,35 +4,34 @@ class Migration(migrations.Migration): - dependencies = [ - ('rss_feeds', '0002_remove_mongo_types'), + ("rss_feeds", "0002_remove_mongo_types"), ] operations = [ migrations.AlterField( - model_name='feed', - name='feed_address_locked', + model_name="feed", + name="feed_address_locked", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='feed', - name='is_push', + model_name="feed", + name="is_push", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='feed', - name='s3_icon', + model_name="feed", + name="s3_icon", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='feed', - name='s3_page', + model_name="feed", + name="s3_page", field=models.BooleanField(blank=True, default=False, null=True), ), migrations.AlterField( - model_name='feed', - name='search_indexed', + model_name="feed", + name="search_indexed", field=models.BooleanField(blank=True, default=None, null=True), ), ] diff --git a/apps/rss_feeds/migrations/0003_mongo_version_4_0.py b/apps/rss_feeds/migrations/0003_mongo_version_4_0.py index 9a2999904d..8164901374 100644 --- a/apps/rss_feeds/migrations/0003_mongo_version_4_0.py +++ b/apps/rss_feeds/migrations/0003_mongo_version_4_0.py @@ -1,7 +1,8 @@ # Generated by Django 3.1.10 on 2022-05-17 13:35 -from django.db import migrations from django.conf import settings +from django.db import migrations + def set_mongo_feature_compatibility_version(apps, schema_editor): new_version = "4.0" @@ -13,14 +14,11 @@ def set_mongo_feature_compatibility_version(apps, schema_editor): if old_version != new_version: db.command({"setFeatureCompatibilityVersion": new_version}) print(f" ---> Updated MongoDB featureCompatibilityVersion: {new_version}") - -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ - ('rss_feeds', '0002_remove_mongo_types'), + ("rss_feeds", "0002_remove_mongo_types"), ] - operations = [ - migrations.RunPython(set_mongo_feature_compatibility_version, migrations.RunPython.noop) - ] + operations = [migrations.RunPython(set_mongo_feature_compatibility_version, migrations.RunPython.noop)] diff --git a/apps/rss_feeds/migrations/0004_feed_pro_subscribers.py b/apps/rss_feeds/migrations/0004_feed_pro_subscribers.py index 7579e56ff1..35bc6e6d41 100644 --- a/apps/rss_feeds/migrations/0004_feed_pro_subscribers.py +++ b/apps/rss_feeds/migrations/0004_feed_pro_subscribers.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('rss_feeds', '0003_auto_20220110_2105'), + ("rss_feeds", "0003_auto_20220110_2105"), ] operations = [ migrations.AddField( - model_name='feed', - name='pro_subscribers', + model_name="feed", + name="pro_subscribers", field=models.IntegerField(blank=True, default=0, null=True), ), ] diff --git a/apps/rss_feeds/migrations/0005_feed_archive_subscribers.py b/apps/rss_feeds/migrations/0005_feed_archive_subscribers.py index 1d8152591f..3d877b2bd0 100644 --- a/apps/rss_feeds/migrations/0005_feed_archive_subscribers.py +++ b/apps/rss_feeds/migrations/0005_feed_archive_subscribers.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('rss_feeds', '0004_feed_pro_subscribers'), + ("rss_feeds", "0004_feed_pro_subscribers"), ] operations = [ migrations.AddField( - model_name='feed', - name='archive_subscribers', + model_name="feed", + name="archive_subscribers", field=models.IntegerField(blank=True, default=0, null=True), ), ] diff --git a/apps/rss_feeds/migrations/0006_feed_fs_size_bytes.py b/apps/rss_feeds/migrations/0006_feed_fs_size_bytes.py index cebc86363a..4123d3f8fb 100644 --- a/apps/rss_feeds/migrations/0006_feed_fs_size_bytes.py +++ b/apps/rss_feeds/migrations/0006_feed_fs_size_bytes.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('rss_feeds', '0005_feed_archive_subscribers'), + ("rss_feeds", "0005_feed_archive_subscribers"), ] operations = [ migrations.AddField( - model_name='feed', - name='fs_size_bytes', + model_name="feed", + name="fs_size_bytes", field=models.IntegerField(blank=True, null=True), ), ] diff --git a/apps/rss_feeds/migrations/0007_merge_20220517_1355.py b/apps/rss_feeds/migrations/0007_merge_20220517_1355.py index f9e6e7bdeb..ae30775afc 100644 --- a/apps/rss_feeds/migrations/0007_merge_20220517_1355.py +++ b/apps/rss_feeds/migrations/0007_merge_20220517_1355.py @@ -4,11 +4,9 @@ class Migration(migrations.Migration): - dependencies = [ - ('rss_feeds', '0006_feed_fs_size_bytes'), - ('rss_feeds', '0003_mongo_version_4_0'), + ("rss_feeds", "0006_feed_fs_size_bytes"), + ("rss_feeds", "0003_mongo_version_4_0"), ] - operations = [ - ] + operations = [] diff --git a/apps/rss_feeds/migrations/0008_feed_archive_count.py b/apps/rss_feeds/migrations/0008_feed_archive_count.py index 0450de50f7..bc7becf63c 100644 --- a/apps/rss_feeds/migrations/0008_feed_archive_count.py +++ b/apps/rss_feeds/migrations/0008_feed_archive_count.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('rss_feeds', '0007_merge_20220517_1355'), + ("rss_feeds", "0007_merge_20220517_1355"), ] operations = [ migrations.AddField( - model_name='feed', - name='archive_count', + model_name="feed", + name="archive_count", field=models.IntegerField(blank=True, null=True), ), ] diff --git a/apps/rss_feeds/models.py b/apps/rss_feeds/models.py index 41c71fa07a..a4eca13422 100755 --- a/apps/rss_feeds/models.py +++ b/apps/rss_feeds/models.py @@ -1,55 +1,64 @@ -import difflib -import bson -import requests +import base64 import datetime -import time -import random -import re -import math -import mongoengine as mongo -import zlib +import difflib import hashlib -import redis -import base64 -import pymongo import html +import math +import random +import re +import time import urllib.parse +import zlib from collections import defaultdict from operator import itemgetter -from bson.objectid import ObjectId + +import bson +import mongoengine as mongo +import pymongo +import redis +import requests from bs4 import BeautifulSoup -# from nltk.collocations import TrigramCollocationFinder, BigramCollocationFinder, TrigramAssocMeasures, BigramAssocMeasures -from django.db import models -from django.db import IntegrityError +from bson.objectid import ObjectId from django.conf import settings -from django.db.models.query import QuerySet -from django.db.utils import DatabaseError -from django.urls import reverse from django.contrib.auth.models import User from django.contrib.sites.models import Site + +# from nltk.collocations import TrigramCollocationFinder, BigramCollocationFinder, TrigramAssocMeasures, BigramAssocMeasures +from django.db import IntegrityError, models +from django.db.models.query import QuerySet +from django.db.utils import DatabaseError from django.template.defaultfilters import slugify -from django.utils.encoding import smart_bytes, smart_str -from django.utils.encoding import DjangoUnicodeDecodeError -from mongoengine.queryset import OperationError, Q, NotUniqueError +from django.urls import reverse +from django.utils.encoding import DjangoUnicodeDecodeError, smart_bytes, smart_str from mongoengine.errors import ValidationError -from vendor.timezones.utilities import localtime_for_timezone -from apps.rss_feeds.tasks import UpdateFeeds, PushFeeds, ScheduleCountTagsForUser +from mongoengine.queryset import NotUniqueError, OperationError, Q + +from apps.rss_feeds.tasks import PushFeeds, ScheduleCountTagsForUser, UpdateFeeds from apps.rss_feeds.text_importer import TextImporter -from apps.search.models import SearchStory, SearchFeed +from apps.search.models import SearchFeed, SearchStory from apps.statistics.rstats import RStats +from utils import feedfinder_forman, feedfinder_pilgrim from utils import json_functions as json -from utils import feedfinder_forman -from utils import feedfinder_pilgrim -from utils import urlnorm from utils import log as logging +from utils import urlnorm +from utils.feed_functions import ( + TimeoutError, + levenshtein_distance, + relative_timesince, + seconds_timesince, + strip_underscore_from_feed_address, + timelimit, +) from utils.fields import AutoOneToOneField -from utils.feed_functions import levenshtein_distance -from utils.feed_functions import timelimit, TimeoutError -from utils.feed_functions import relative_timesince -from utils.feed_functions import seconds_timesince -from utils.story_functions import strip_tags, htmldiff, strip_comments, strip_comments__lxml -from utils.story_functions import prep_for_search -from utils.story_functions import create_imageproxy_signed_url +from utils.story_functions import ( + create_imageproxy_signed_url, + htmldiff, + prep_for_search, + strip_comments, + strip_comments__lxml, + strip_tags, +) +from vendor.timezones.utilities import localtime_for_timezone ENTRY_NEW, ENTRY_UPDATED, ENTRY_SAME, ENTRY_ERR = list(range(4)) @@ -69,7 +78,9 @@ class Feed(models.Model): archive_subscribers = models.IntegerField(default=0, null=True, blank=True) pro_subscribers = models.IntegerField(default=0, null=True, blank=True) active_premium_subscribers = models.IntegerField(default=-1) - branch_from_feed = models.ForeignKey('Feed', blank=True, null=True, db_index=True, on_delete=models.CASCADE) + branch_from_feed = models.ForeignKey( + "Feed", blank=True, null=True, db_index=True, on_delete=models.CASCADE + ) last_update = models.DateTimeField(db_index=True) next_scheduled_update = models.DateTimeField() last_story_date = models.DateTimeField(null=True, blank=True) @@ -97,18 +108,18 @@ class Feed(models.Model): archive_count = models.IntegerField(null=True, blank=True) class Meta: - db_table="feeds" - ordering=["feed_title"] + db_table = "feeds" + ordering = ["feed_title"] # unique_together=[('feed_address', 'feed_link')] - + def __str__(self): if not self.feed_title: self.feed_title = "[Untitled]" self.save() return "%s%s: %s - %s/%s/%s/%s/%s %s stories (%s bytes)" % ( - self.pk, + self.pk, (" [B: %s]" % self.branch_from_feed.pk if self.branch_from_feed else ""), - self.feed_title, + self.feed_title, self.num_subscribers, self.active_subscribers, self.active_premium_subscribers, @@ -116,46 +127,43 @@ def __str__(self): self.pro_subscribers, self.archive_count, self.fs_size_bytes, - ) - + ) + @property def title(self): title = self.feed_title or "[Untitled]" if self.active_premium_subscribers >= 1: title = "%s*" % title[:29] return title - + @property def log_title(self): return self.__str__() - + @property def permalink(self): return "%s/site/%s/%s" % (settings.NEWSBLUR_URL, self.pk, slugify(self.feed_title.lower()[:50])) - + @property def favicon_url(self): - if settings.BACKED_BY_AWS['icons_on_s3'] and self.s3_icon: + if settings.BACKED_BY_AWS["icons_on_s3"] and self.s3_icon: return "https://s3.amazonaws.com/%s/%s.png" % (settings.S3_ICONS_BUCKET_NAME, self.pk) - return reverse('feed-favicon', kwargs={'feed_id': self.pk}) - + return reverse("feed-favicon", kwargs={"feed_id": self.pk}) + @property def favicon_url_fqdn(self): - if settings.BACKED_BY_AWS['icons_on_s3'] and self.s3_icon: + if settings.BACKED_BY_AWS["icons_on_s3"] and self.s3_icon: return self.favicon_url - return "https://%s%s" % ( - Site.objects.get_current().domain, - self.favicon_url - ) - + return "https://%s%s" % (Site.objects.get_current().domain, self.favicon_url) + @property def s3_pages_key(self): return "%s.gz.html" % self.pk - + @property def s3_icons_key(self): return "%s.png" % self.pk - + @property def unread_cutoff(self): if self.archive_subscribers and self.archive_subscribers > 0: @@ -164,117 +172,121 @@ def unread_cutoff(self): return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD) return datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD_FREE) - + @classmethod def days_of_story_hashes_for_feed(cls, feed_id): try: - feed = cls.objects.only('archive_subscribers').get(pk=feed_id) + feed = cls.objects.only("archive_subscribers").get(pk=feed_id) return feed.days_of_story_hashes except cls.DoesNotExist: return settings.DAYS_OF_STORY_HASHES - + @property def days_of_story_hashes(self): if self.archive_subscribers and self.archive_subscribers > 0: return settings.DAYS_OF_STORY_HASHES_ARCHIVE return settings.DAYS_OF_STORY_HASHES - + @property def story_hashes_in_unread_cutoff(self): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - current_time = int(time.time() + 60*60*24) - unread_cutoff = self.unread_cutoff.strftime('%s') - story_hashes = r.zrevrangebyscore('zF:%s' % self.pk, current_time, unread_cutoff) + current_time = int(time.time() + 60 * 60 * 24) + unread_cutoff = self.unread_cutoff.strftime("%s") + story_hashes = r.zrevrangebyscore("zF:%s" % self.pk, current_time, unread_cutoff) return story_hashes - + @classmethod def generate_hash_address_and_link(cls, feed_address, feed_link): - if not feed_address: feed_address = "" - if not feed_link: feed_link = "" - return hashlib.sha1((feed_address+feed_link).encode(encoding='utf-8')).hexdigest() - + if not feed_address: + feed_address = "" + if not feed_link: + feed_link = "" + return hashlib.sha1((feed_address + feed_link).encode(encoding="utf-8")).hexdigest() + @property def is_newsletter(self): - return self.feed_address.startswith('newsletter:') or self.feed_address.startswith('http://newsletter:') - + return self.feed_address.startswith("newsletter:") or self.feed_address.startswith( + "http://newsletter:" + ) + def canonical(self, full=False, include_favicon=True): feed = { - 'id': self.pk, - 'feed_title': self.feed_title, - 'feed_address': self.feed_address, - 'feed_link': self.feed_link, - 'num_subscribers': self.num_subscribers, - 'updated': relative_timesince(self.last_update), - 'updated_seconds_ago': seconds_timesince(self.last_update), - 'fs_size_bytes': self.fs_size_bytes, - 'archive_count': self.archive_count, - 'last_story_date': self.last_story_date, - 'last_story_seconds_ago': seconds_timesince(self.last_story_date), - 'stories_last_month': self.stories_last_month, - 'average_stories_per_month': self.average_stories_per_month, - 'min_to_decay': self.min_to_decay, - 'subs': self.num_subscribers, - 'is_push': self.is_push, - 'is_newsletter': self.is_newsletter, - 'fetched_once': self.fetched_once, - 'search_indexed': self.search_indexed, - 'not_yet_fetched': not self.fetched_once, # Legacy. Doh. - 'favicon_color': self.favicon_color, - 'favicon_fade': self.favicon_fade(), - 'favicon_border': self.favicon_border(), - 'favicon_text_color': self.favicon_text_color(), - 'favicon_fetching': self.favicon_fetching, - 'favicon_url': self.favicon_url, - 's3_page': self.s3_page, - 's3_icon': self.s3_icon, - 'disabled_page': not self.has_page, + "id": self.pk, + "feed_title": self.feed_title, + "feed_address": self.feed_address, + "feed_link": self.feed_link, + "num_subscribers": self.num_subscribers, + "updated": relative_timesince(self.last_update), + "updated_seconds_ago": seconds_timesince(self.last_update), + "fs_size_bytes": self.fs_size_bytes, + "archive_count": self.archive_count, + "last_story_date": self.last_story_date, + "last_story_seconds_ago": seconds_timesince(self.last_story_date), + "stories_last_month": self.stories_last_month, + "average_stories_per_month": self.average_stories_per_month, + "min_to_decay": self.min_to_decay, + "subs": self.num_subscribers, + "is_push": self.is_push, + "is_newsletter": self.is_newsletter, + "fetched_once": self.fetched_once, + "search_indexed": self.search_indexed, + "not_yet_fetched": not self.fetched_once, # Legacy. Doh. + "favicon_color": self.favicon_color, + "favicon_fade": self.favicon_fade(), + "favicon_border": self.favicon_border(), + "favicon_text_color": self.favicon_text_color(), + "favicon_fetching": self.favicon_fetching, + "favicon_url": self.favicon_url, + "s3_page": self.s3_page, + "s3_icon": self.s3_icon, + "disabled_page": not self.has_page, } - + if include_favicon: try: feed_icon = MFeedIcon.objects.get(feed_id=self.pk) - feed['favicon'] = feed_icon.data + feed["favicon"] = feed_icon.data except MFeedIcon.DoesNotExist: pass if self.has_page_exception or self.has_feed_exception: - feed['has_exception'] = True - feed['exception_type'] = 'feed' if self.has_feed_exception else 'page' - feed['exception_code'] = self.exception_code + feed["has_exception"] = True + feed["exception_type"] = "feed" if self.has_feed_exception else "page" + feed["exception_code"] = self.exception_code elif full: - feed['has_exception'] = False - feed['exception_type'] = None - feed['exception_code'] = self.exception_code - + feed["has_exception"] = False + feed["exception_type"] = None + feed["exception_code"] = self.exception_code + if full: - feed['average_stories_per_month'] = self.average_stories_per_month - feed['tagline'] = self.data.feed_tagline - feed['feed_tags'] = json.decode(self.data.popular_tags) if self.data.popular_tags else [] - feed['feed_authors'] = json.decode(self.data.popular_authors) if self.data.popular_authors else [] - + feed["average_stories_per_month"] = self.average_stories_per_month + feed["tagline"] = self.data.feed_tagline + feed["feed_tags"] = json.decode(self.data.popular_tags) if self.data.popular_tags else [] + feed["feed_authors"] = json.decode(self.data.popular_authors) if self.data.popular_authors else [] + return feed - + def save(self, *args, **kwargs): if not self.last_update: self.last_update = datetime.datetime.utcnow() if not self.next_scheduled_update: self.next_scheduled_update = datetime.datetime.utcnow() self.fix_google_alerts_urls() - + feed_address = self.feed_address or "" feed_link = self.feed_link or "" self.hash_address_and_link = self.generate_hash_address_and_link(feed_address, feed_link) - - max_feed_title = Feed._meta.get_field('feed_title').max_length + + max_feed_title = Feed._meta.get_field("feed_title").max_length if len(self.feed_title) > max_feed_title: self.feed_title = self.feed_title[:max_feed_title] - max_feed_address = Feed._meta.get_field('feed_address').max_length + max_feed_address = Feed._meta.get_field("feed_address").max_length if len(feed_address) > max_feed_address: self.feed_address = feed_address[:max_feed_address] - max_feed_link = Feed._meta.get_field('feed_link').max_length + max_feed_link = Feed._meta.get_field("feed_link").max_length if len(feed_link) > max_feed_link: self.feed_link = feed_link[:max_feed_link] - + try: super(Feed, self).save(*args, **kwargs) except IntegrityError as e: @@ -284,108 +296,123 @@ def save(self, *args, **kwargs): hash_address_and_link = self.generate_hash_address_and_link(feed_address, feed_link) logging.debug(" ---> ~FRNo dupes, checking hash collision: %s" % hash_address_and_link) duplicate_feeds = Feed.objects.filter(hash_address_and_link=hash_address_and_link) - + if not duplicate_feeds: - duplicate_feeds = Feed.objects.filter(feed_address=self.feed_address, - feed_link=self.feed_link) + duplicate_feeds = Feed.objects.filter( + feed_address=self.feed_address, feed_link=self.feed_link + ) if not duplicate_feeds: # Feed has been deleted. Just ignore it. - logging.debug(" ***> Changed to: %s - %s: %s" % (self.feed_address, self.feed_link, duplicate_feeds)) - logging.debug(' ***> [%-30s] Feed deleted (%s).' % (self.log_title[:30], self.pk)) + logging.debug( + " ***> Changed to: %s - %s: %s" % (self.feed_address, self.feed_link, duplicate_feeds) + ) + logging.debug(" ***> [%-30s] Feed deleted (%s)." % (self.log_title[:30], self.pk)) return - + for duplicate_feed in duplicate_feeds: if duplicate_feed.pk != self.pk: - logging.debug(" ---> ~FRFound different feed (%s), merging %s in..." % (duplicate_feeds[0], self.pk)) + logging.debug( + " ---> ~FRFound different feed (%s), merging %s in..." % (duplicate_feeds[0], self.pk) + ) feed = Feed.get_by_id(merge_feeds(duplicate_feeds[0].pk, self.pk)) return feed else: logging.debug(" ---> ~FRFeed is its own dupe? %s == %s" % (self, duplicate_feeds)) except DatabaseError as e: - logging.debug(" ---> ~FBFeed update failed, no change: %s / %s..." % (kwargs.get('update_fields', None), e)) + logging.debug( + " ---> ~FBFeed update failed, no change: %s / %s..." % (kwargs.get("update_fields", None), e) + ) pass - + return self - + @classmethod def index_all_for_search(cls, offset=0, subscribers=2): if not offset: SearchFeed.create_elasticsearch_mapping(delete=True) - - last_pk = cls.objects.latest('pk').pk + + last_pk = cls.objects.latest("pk").pk for f in range(offset, last_pk, 1000): - print(" ---> {f} / {last_pk} ({pct}%)".format(f=f, last_pk=last_pk, pct=str(float(f)/last_pk*100)[:2])) - feeds = Feed.objects.filter(pk__in=range(f, f+1000), - active=True, - active_subscribers__gte=subscribers)\ - .values_list('pk') - for feed_id, in feeds: + print( + " ---> {f} / {last_pk} ({pct}%)".format( + f=f, last_pk=last_pk, pct=str(float(f) / last_pk * 100)[:2] + ) + ) + feeds = Feed.objects.filter( + pk__in=range(f, f + 1000), active=True, active_subscribers__gte=subscribers + ).values_list("pk") + for (feed_id,) in feeds: Feed.objects.get(pk=feed_id).index_feed_for_search() - + def index_feed_for_search(self): min_subscribers = 1 if settings.DEBUG: min_subscribers = 0 if self.num_subscribers > min_subscribers and not self.branch_from_feed and not self.is_newsletter: - SearchFeed.index(feed_id=self.pk, - title=self.feed_title, - address=self.feed_address, - link=self.feed_link, - num_subscribers=self.num_subscribers) - + SearchFeed.index( + feed_id=self.pk, + title=self.feed_title, + address=self.feed_address, + link=self.feed_link, + num_subscribers=self.num_subscribers, + ) + def index_stories_for_search(self): - if self.search_indexed: return - + if self.search_indexed: + return + stories = MStory.objects(story_feed_id=self.pk) for story in stories: story.index_story_for_search() self.search_indexed = True self.save() - - def sync_redis(self): - return MStory.sync_feed_redis(self.pk) - + + def sync_redis(self, allow_skip_resync=False): + return MStory.sync_feed_redis(self.pk, allow_skip_resync=allow_skip_resync) + def expire_redis(self, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - r.expire('F:%s' % self.pk, self.days_of_story_hashes*24*60*60) - r.expire('zF:%s' % self.pk, self.days_of_story_hashes*24*60*60) - + r.expire("F:%s" % self.pk, self.days_of_story_hashes * 24 * 60 * 60) + r.expire("zF:%s" % self.pk, self.days_of_story_hashes * 24 * 60 * 60) + @classmethod def low_volume_feeds(cls, feed_ids, stories_per_month=30): try: stories_per_month = int(stories_per_month) except ValueError: stories_per_month = 30 - feeds = Feed.objects.filter(pk__in=feed_ids, average_stories_per_month__lte=stories_per_month).only('pk') - + feeds = Feed.objects.filter(pk__in=feed_ids, average_stories_per_month__lte=stories_per_month).only( + "pk" + ) + return [f.pk for f in feeds] - + @classmethod def autocomplete(self, prefix, limit=5): results = SearchFeed.query(prefix) - feed_ids = [result['_source']['feed_id'] for result in results[:5]] + feed_ids = [result["_source"]["feed_id"] for result in results[:5]] # results = SearchQuerySet().autocomplete(address=prefix).order_by('-num_subscribers')[:limit] - # + # # if len(results) < limit: # results += SearchQuerySet().autocomplete(title=prefix).order_by('-num_subscribers')[:limit-len(results)] - # + # return feed_ids - + @classmethod def find_or_create(cls, feed_address, feed_link, defaults=None, **kwargs): feeds = cls.objects.filter(feed_address=feed_address, feed_link=feed_link) if feeds: return feeds[0], False - if feed_link and feed_link.endswith('/'): + if feed_link and feed_link.endswith("/"): feeds = cls.objects.filter(feed_address=feed_address, feed_link=feed_link[:-1]) if feeds: return feeds[0], False - + try: feed = cls.objects.get(feed_address=feed_address, feed_link=feed_link) return feed, False @@ -393,34 +420,33 @@ def find_or_create(cls, feed_address, feed_link, defaults=None, **kwargs): feed = cls(**defaults) feed = feed.save() return feed, True - + @classmethod def merge_feeds(cls, *args, **kwargs): return merge_feeds(*args, **kwargs) - + def fix_google_alerts_urls(self): - if (self.feed_address.startswith('http://user/') and - '/state/com.google/alerts/' in self.feed_address): + if self.feed_address.startswith("http://user/") and "/state/com.google/alerts/" in self.feed_address: match = re.match(r"http://user/(\d+)/state/com.google/alerts/(\d+)", self.feed_address) if match: user_id, alert_id = match.groups() self.feed_address = "http://www.google.com/alerts/feeds/%s/%s" % (user_id, alert_id) - + @classmethod def schedule_feed_fetches_immediately(cls, feed_ids, user_id=None): if settings.DEBUG: - logging.info(" ---> ~SN~FMSkipping the scheduling immediate fetch of ~SB%s~SN feeds (in DEBUG)..." % - len(feed_ids)) + logging.info( + " ---> ~SN~FMSkipping the scheduling immediate fetch of ~SB%s~SN feeds (in DEBUG)..." + % len(feed_ids) + ) return - + if user_id: user = User.objects.get(pk=user_id) - logging.user(user, "~SN~FMScheduling immediate fetch of ~SB%s~SN feeds..." % - len(feed_ids)) + logging.user(user, "~SN~FMScheduling immediate fetch of ~SB%s~SN feeds..." % len(feed_ids)) else: - logging.debug(" ---> ~SN~FMScheduling immediate fetch of ~SB%s~SN feeds..." % - len(feed_ids)) - + logging.debug(" ---> ~SN~FMScheduling immediate fetch of ~SB%s~SN feeds..." % len(feed_ids)) + if len(feed_ids) > 100: logging.debug(" ---> ~SN~FMFeeds scheduled: %s" % feed_ids) day_ago = datetime.datetime.now() - datetime.timedelta(days=1) @@ -430,72 +456,78 @@ def schedule_feed_fetches_immediately(cls, feed_ids, user_id=None): feed.count_subscribers() if not feed.active or feed.next_scheduled_update < day_ago: feed.schedule_feed_fetch_immediately(verbose=False) - + @property def favicon_fetching(self): return bool(not (self.favicon_not_found or self.favicon_color)) - + @classmethod def get_feed_by_url(self, *args, **kwargs): return self.get_feed_from_url(*args, **kwargs) - + @classmethod - def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False): + def get_feed_from_url( + cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False + ): feed = None without_rss = False original_url = url - - if url and url.startswith('newsletter:'): + + if url and url.startswith("newsletter:"): try: return cls.objects.get(feed_address=url) except cls.MultipleObjectsReturned: return cls.objects.filter(feed_address=url)[0] - if url and re.match('(https?://)?twitter.com/\w+/?', url): + if url and re.match("(https?://)?twitter.com/\w+/?", url): without_rss = True - if url and re.match(r'(https?://)?(www\.)?facebook.com/\w+/?$', url): + if url and re.match(r"(https?://)?(www\.)?facebook.com/\w+/?$", url): without_rss = True # Turn url @username@domain.com into domain.com/users/username.rss - if url and url.startswith('@') and '@' in url[1:]: - username, domain = url[1:].split('@') + if url and url.startswith("@") and "@" in url[1:]: + username, domain = url[1:].split("@") url = f"https://{domain}/users/{username}.rss" - if url and 'youtube.com/user/' in url: - username = re.search('youtube.com/user/(\w+)', url).group(1) + if url and "youtube.com/user/" in url: + username = re.search("youtube.com/user/(\w+)", url).group(1) url = "http://gdata.youtube.com/feeds/base/users/%s/uploads" % username without_rss = True - if url and 'youtube.com/@' in url: - username = url.split('youtube.com/@')[1] + if url and "youtube.com/@" in url: + username = url.split("youtube.com/@")[1] url = "http://gdata.youtube.com/feeds/base/users/%s/uploads" % username without_rss = True - if url and 'youtube.com/channel/' in url: - channel_id = re.search('youtube.com/channel/([-_\w]+)', url).group(1) + if url and "youtube.com/channel/" in url: + channel_id = re.search("youtube.com/channel/([-_\w]+)", url).group(1) url = "https://www.youtube.com/feeds/videos.xml?channel_id=%s" % channel_id without_rss = True - if url and 'youtube.com/feeds' in url: + if url and "youtube.com/feeds" in url: without_rss = True - if url and 'youtube.com/playlist' in url: + if url and "youtube.com/playlist" in url: without_rss = True - + def criteria(key, value): if aggressive: - return {'%s__icontains' % key: value} + return {"%s__icontains" % key: value} else: - return {'%s' % key: value} - + return {"%s" % key: value} + def by_url(address): - feed = cls.objects.filter( - branch_from_feed=None - ).filter(**criteria('feed_address', address)).order_by('-num_subscribers') + feed = ( + cls.objects.filter(branch_from_feed=None) + .filter(**criteria("feed_address", address)) + .order_by("-num_subscribers") + ) if not feed: - duplicate_feed = DuplicateFeed.objects.filter(**criteria('duplicate_address', address)) + duplicate_feed = DuplicateFeed.objects.filter(**criteria("duplicate_address", address)) if duplicate_feed and len(duplicate_feed) > offset: feed = [duplicate_feed[offset].feed] if not feed and aggressive: - feed = cls.objects.filter( - branch_from_feed=None - ).filter(**criteria('feed_link', address)).order_by('-num_subscribers') - + feed = ( + cls.objects.filter(branch_from_feed=None) + .filter(**criteria("feed_link", address)) + .order_by("-num_subscribers") + ) + return feed - + @timelimit(10) def _feedfinder_forman(url): found_feed_urls = feedfinder_forman.find_feeds(url) @@ -505,19 +537,21 @@ def _feedfinder_forman(url): def _feedfinder_pilgrim(url): found_feed_urls = feedfinder_pilgrim.feeds(url) return found_feed_urls - + # Normalize and check for feed_address, dupes, and feed_link url = urlnorm.normalize(url) if not url: logging.debug(" ---> ~FRCouldn't normalize url: ~SB%s" % url) return - + feed = by_url(url) found_feed_urls = [] - + if interactive: - import pdb; pdb.set_trace() - + import pdb + + pdb.set_trace() + # Create if it looks good if feed and len(feed) > offset: feed = feed[offset] @@ -525,15 +559,15 @@ def _feedfinder_pilgrim(url): try: found_feed_urls = _feedfinder_forman(url) except TimeoutError: - logging.debug(' ---> Feed finder timed out...') + logging.debug(" ---> Feed finder timed out...") found_feed_urls = [] if not found_feed_urls: try: found_feed_urls = _feedfinder_pilgrim(url) except TimeoutError: - logging.debug(' ---> Feed finder old timed out...') + logging.debug(" ---> Feed finder old timed out...") found_feed_urls = [] - + if len(found_feed_urls): feed_finder_url = found_feed_urls[0] logging.debug(" ---> Found feed URLs for %s: %s" % (url, found_feed_urls)) @@ -550,17 +584,17 @@ def _feedfinder_pilgrim(url): logging.debug(" ---> Found without_rss feed: %s / %s" % (url, original_url)) feed = cls.objects.create(feed_address=url, feed_link=original_url) feed = feed.update(requesting_user_id=user.pk if user else None) - + # Check for JSON feed if not feed and fetch and create: try: r = requests.get(url) except (requests.ConnectionError, requests.models.InvalidURL): r = None - if r and 'application/json' in r.headers.get('Content-Type'): + if r and "application/json" in r.headers.get("Content-Type"): feed = cls.objects.create(feed_address=url) feed = feed.update() - + # Still nothing? Maybe the URL has some clues. if not feed and fetch and len(found_feed_urls): feed_finder_url = found_feed_urls[0] @@ -570,17 +604,18 @@ def _feedfinder_pilgrim(url): feed = feed.update() elif feed and len(feed) > offset: feed = feed[offset] - + # Not created and not within bounds, so toss results. if isinstance(feed, QuerySet): logging.debug(" ---> ~FRNot created and not within bounds, tossing: ~SB%s" % feed) return - + return feed - + @classmethod def task_feeds(cls, feeds, queue_size=12, verbose=True): - if not feeds: return + if not feeds: + return r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) if isinstance(feeds, Feed): @@ -589,50 +624,50 @@ def task_feeds(cls, feeds, queue_size=12, verbose=True): feeds = [feeds.pk] elif verbose: logging.debug(" ---> ~SN~FBTasking ~SB~FC%s~FB~SN feeds..." % len(feeds)) - + if isinstance(feeds, QuerySet): feeds = [f.pk for f in feeds] - - r.srem('queued_feeds', *feeds) + + r.srem("queued_feeds", *feeds) now = datetime.datetime.now().strftime("%s") p = r.pipeline() for feed_id in feeds: - p.zadd('tasked_feeds', { feed_id: now }) + p.zadd("tasked_feeds", {feed_id: now}) p.execute() - + # for feed_ids in (feeds[pos:pos + queue_size] for pos in xrange(0, len(feeds), queue_size)): for feed_id in feeds: - UpdateFeeds.apply_async(args=(feed_id,), queue='update_feeds') - + UpdateFeeds.apply_async(args=(feed_id,), queue="update_feeds") + @classmethod def drain_task_feeds(cls): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - tasked_feeds = r.zrange('tasked_feeds', 0, -1) + tasked_feeds = r.zrange("tasked_feeds", 0, -1) if tasked_feeds: logging.debug(" ---> ~FRDraining %s tasked feeds..." % len(tasked_feeds)) - r.sadd('queued_feeds', *tasked_feeds) - r.zremrangebyrank('tasked_feeds', 0, -1) + r.sadd("queued_feeds", *tasked_feeds) + r.zremrangebyrank("tasked_feeds", 0, -1) else: logging.debug(" ---> No tasked feeds to drain") - - errored_feeds = r.zrange('error_feeds', 0, -1) + + errored_feeds = r.zrange("error_feeds", 0, -1) if errored_feeds: logging.debug(" ---> ~FRDraining %s errored feeds..." % len(errored_feeds)) - r.sadd('queued_feeds', *errored_feeds) - r.zremrangebyrank('error_feeds', 0, -1) + r.sadd("queued_feeds", *errored_feeds) + r.zremrangebyrank("error_feeds", 0, -1) else: logging.debug(" ---> No errored feeds to drain") def update_all_statistics(self, has_new_stories=False, force=False): - recount = not self.counts_converted_to_redis + recount = not self.counts_converted_to_redis count_extra = False if random.random() < 0.01 or not self.data.popular_tags or not self.data.popular_authors: count_extra = True - + self.count_subscribers(recount=recount) self.calculate_last_story_date() - + if force or has_new_stories or count_extra: self.save_feed_stories_last_month() @@ -642,15 +677,19 @@ def update_all_statistics(self, has_new_stories=False, force=False): if force or (has_new_stories and count_extra): self.save_popular_authors() self.save_popular_tags() - self.save_feed_story_history_statistics() - + self.save_feed_story_history_statistics() + def calculate_last_story_date(self): last_story_date = None try: - latest_story = MStory.objects( - story_feed_id=self.pk - ).limit(1).order_by('-story_date').only('story_date').first() + latest_story = ( + MStory.objects(story_feed_id=self.pk) + .limit(1) + .order_by("-story_date") + .only("story_date") + .first() + ) if latest_story: last_story_date = latest_story.story_date except MStory.DoesNotExist: @@ -658,24 +697,24 @@ def calculate_last_story_date(self): if not last_story_date or seconds_timesince(last_story_date) < 0: last_story_date = datetime.datetime.now() - + if last_story_date != self.last_story_date: self.last_story_date = last_story_date - self.save(update_fields=['last_story_date']) - + self.save(update_fields=["last_story_date"]) + @classmethod def setup_feeds_for_premium_subscribers(cls, feed_ids): logging.info(f" ---> ~SN~FMScheduling immediate premium setup of ~SB{len(feed_ids)}~SN feeds...") - + feeds = Feed.objects.filter(pk__in=feed_ids) for feed in feeds: feed.setup_feed_for_premium_subscribers() - def setup_feed_for_premium_subscribers(self): + def setup_feed_for_premium_subscribers(self, allow_skip_resync=False): self.count_subscribers() self.set_next_scheduled_update(verbose=settings.DEBUG) - self.sync_redis() - + self.sync_redis(allow_skip_resync=allow_skip_resync) + def check_feed_link_for_feed_address(self): @timelimit(10) def _1(): @@ -693,17 +732,20 @@ def _1(): found_feed_urls = feedfinder_forman.find_feeds(self.feed_link) if len(found_feed_urls) and found_feed_urls[0] != self.feed_address: feed_address = found_feed_urls[0] - + if feed_address: - if any(ignored_domain in feed_address for ignored_domain in [ - 'feedburner.com/atom.xml', - 'feedburner.com/feed/', - 'feedsportal.com', - ]): + if any( + ignored_domain in feed_address + for ignored_domain in [ + "feedburner.com/atom.xml", + "feedburner.com/feed/", + "feedsportal.com", + ] + ): logging.debug(" ---> Feed points to 'Wierdo' or 'feedsportal', ignoring.") return False, self try: - self.feed_address = feed_address + self.feed_address = strip_underscore_from_feed_address(feed_address) feed = self.save() feed.count_subscribers() # feed.schedule_feed_fetch_immediately() # Don't fetch as it can get stuck in a loop @@ -717,135 +759,140 @@ def _1(): original_feed.save() merge_feeds(original_feed.pk, self.pk) return feed_address, feed - + if self.feed_address_locked: return False, self - + try: feed_address, feed = _1() except TimeoutError as e: - logging.debug(' ---> [%-30s] Feed address check timed out...' % (self.log_title[:30])) - self.save_feed_history(505, 'Timeout', e) + logging.debug(" ---> [%-30s] Feed address check timed out..." % (self.log_title[:30])) + self.save_feed_history(505, "Timeout", e) feed = self feed_address = None - + return bool(feed_address), feed def save_feed_history(self, status_code, message, exception=None, date=None): - fetch_history = MFetchHistory.add(feed_id=self.pk, - fetch_type='feed', - code=int(status_code), - date=date, - message=message, - exception=exception) - + fetch_history = MFetchHistory.add( + feed_id=self.pk, + fetch_type="feed", + code=int(status_code), + date=date, + message=message, + exception=exception, + ) + if status_code not in (200, 304): self.errors_since_good += 1 - self.count_errors_in_history('feed', status_code, fetch_history=fetch_history) + self.count_errors_in_history("feed", status_code, fetch_history=fetch_history) self.set_next_scheduled_update(verbose=settings.DEBUG) elif self.has_feed_exception or self.errors_since_good: self.errors_since_good = 0 self.has_feed_exception = False self.active = True self.save() - + def save_page_history(self, status_code, message, exception=None, date=None): - fetch_history = MFetchHistory.add(feed_id=self.pk, - fetch_type='page', - code=int(status_code), - date=date, - message=message, - exception=exception) - + fetch_history = MFetchHistory.add( + feed_id=self.pk, + fetch_type="page", + code=int(status_code), + date=date, + message=message, + exception=exception, + ) + if status_code not in (200, 304): - self.count_errors_in_history('page', status_code, fetch_history=fetch_history) + self.count_errors_in_history("page", status_code, fetch_history=fetch_history) elif self.has_page_exception or not self.has_page: self.has_page_exception = False self.has_page = True self.active = True self.save() - + def save_raw_feed(self, raw_feed, fetch_date): - MFetchHistory.add(feed_id=self.pk, - fetch_type='raw_feed', - code=200, - message=raw_feed, - date=fetch_date) - - def count_errors_in_history(self, exception_type='feed', status_code=None, fetch_history=None): + MFetchHistory.add(feed_id=self.pk, fetch_type="raw_feed", code=200, message=raw_feed, date=fetch_date) + + def count_errors_in_history(self, exception_type="feed", status_code=None, fetch_history=None): if not fetch_history: fetch_history = MFetchHistory.feed(self.pk) - fh = fetch_history[exception_type + '_fetch_history'] - non_errors = [h for h in fh if h['status_code'] and int(h['status_code']) in (200, 304)] - errors = [h for h in fh if h['status_code'] and int(h['status_code']) not in (200, 304)] - + fh = fetch_history[exception_type + "_fetch_history"] + non_errors = [h for h in fh if h["status_code"] and int(h["status_code"]) in (200, 304)] + errors = [h for h in fh if h["status_code"] and int(h["status_code"]) not in (200, 304)] + if len(non_errors) == 0 and len(errors) > 1: self.active = True - if exception_type == 'feed': + if exception_type == "feed": self.has_feed_exception = True # self.active = False # No longer, just geometrically fetch - elif exception_type == 'page': + elif exception_type == "page": self.has_page_exception = True self.exception_code = status_code or int(errors[0]) self.save() elif self.exception_code > 0: self.active = True self.exception_code = 0 - if exception_type == 'feed': + if exception_type == "feed": self.has_feed_exception = False - elif exception_type == 'page': + elif exception_type == "page": self.has_page_exception = False self.save() - - logging.debug(' ---> [%-30s] ~FBCounting any errors in history: %s (%s non errors)' % - (self.log_title[:30], len(errors), len(non_errors))) - + + logging.debug( + " ---> [%-30s] ~FBCounting any errors in history: %s (%s non errors)" + % (self.log_title[:30], len(errors), len(non_errors)) + ) + return errors, non_errors - def count_redirects_in_history(self, fetch_type='feed', fetch_history=None): - logging.debug(' ---> [%-30s] Counting redirects in history...' % (self.log_title[:30])) + def count_redirects_in_history(self, fetch_type="feed", fetch_history=None): + logging.debug(" ---> [%-30s] Counting redirects in history..." % (self.log_title[:30])) if not fetch_history: fetch_history = MFetchHistory.feed(self.pk) - fh = fetch_history[fetch_type+'_fetch_history'] - redirects = [h for h in fh if h['status_code'] and int(h['status_code']) in (301, 302)] - non_redirects = [h for h in fh if h['status_code'] and int(h['status_code']) not in (301, 302)] - + fh = fetch_history[fetch_type + "_fetch_history"] + redirects = [h for h in fh if h["status_code"] and int(h["status_code"]) in (301, 302)] + non_redirects = [h for h in fh if h["status_code"] and int(h["status_code"]) not in (301, 302)] + return redirects, non_redirects - + @property def original_feed_id(self): if self.branch_from_feed: return self.branch_from_feed.pk else: return self.pk - + @property def counts_converted_to_redis(self): SUBSCRIBER_EXPIRE_DATE = datetime.datetime.now() - datetime.timedelta(days=settings.SUBSCRIBER_EXPIRE) - subscriber_expire = int(SUBSCRIBER_EXPIRE_DATE.strftime('%s')) + subscriber_expire = int(SUBSCRIBER_EXPIRE_DATE.strftime("%s")) r = redis.Redis(connection_pool=settings.REDIS_FEED_SUB_POOL) total_key = "s:%s" % self.original_feed_id premium_key = "sp:%s" % self.original_feed_id - last_recount = r.zscore(total_key, -1) # Need to subtract this extra when counting subs + last_recount = r.zscore(total_key, -1) # Need to subtract this extra when counting subs # Check for expired feeds with no active users who would have triggered a cleanup if last_recount and last_recount > subscriber_expire: return True elif last_recount: - logging.info(" ---> [%-30s] ~SN~FBFeed has expired redis subscriber counts (%s < %s), clearing..." % ( - self.log_title[:30], last_recount, subscriber_expire)) + logging.info( + " ---> [%-30s] ~SN~FBFeed has expired redis subscriber counts (%s < %s), clearing..." + % (self.log_title[:30], last_recount, subscriber_expire) + ) r.delete(total_key, -1) r.delete(premium_key, -1) - + return False - + def count_subscribers(self, recount=True, verbose=False): if recount or not self.counts_converted_to_redis: from apps.profile.models import Profile + Profile.count_feed_subscribers(feed_id=self.pk) SUBSCRIBER_EXPIRE_DATE = datetime.datetime.now() - datetime.timedelta(days=settings.SUBSCRIBER_EXPIRE) - subscriber_expire = int(SUBSCRIBER_EXPIRE_DATE.strftime('%s')) - now = int(datetime.datetime.now().strftime('%s')) + subscriber_expire = int(SUBSCRIBER_EXPIRE_DATE.strftime("%s")) + now = int(datetime.datetime.now().strftime("%s")) r = redis.Redis(connection_pool=settings.REDIS_FEED_SUB_POOL) total = 0 active = 0 @@ -853,9 +900,9 @@ def count_subscribers(self, recount=True, verbose=False): archive = 0 pro = 0 active_premium = 0 - + # Include all branched feeds in counts - feed_ids = [f['id'] for f in Feed.objects.filter(branch_from_feed=self.original_feed_id).values('id')] + feed_ids = [f["id"] for f in Feed.objects.filter(branch_from_feed=self.original_feed_id).values("id")] feed_ids.append(self.original_feed_id) feed_ids = list(set(feed_ids)) @@ -863,21 +910,21 @@ def count_subscribers(self, recount=True, verbose=False): # For each branched feed, count different subscribers for feed_id in feed_ids: pipeline = r.pipeline() - + # now+1 ensures `-1` flag will be corrected for later with - 1 total_key = "s:%s" % feed_id premium_key = "sp:%s" % feed_id archive_key = "sarchive:%s" % feed_id pro_key = "spro:%s" % feed_id pipeline.zcard(total_key) - pipeline.zcount(total_key, subscriber_expire, now+1) + pipeline.zcount(total_key, subscriber_expire, now + 1) pipeline.zcard(premium_key) - pipeline.zcount(premium_key, subscriber_expire, now+1) + pipeline.zcount(premium_key, subscriber_expire, now + 1) pipeline.zcard(archive_key) pipeline.zcard(pro_key) results = pipeline.execute() - + # -1 due to counts_converted_to_redis using key=-1 for last_recount date total += max(0, results[0] - 1) active += max(0, results[1] - 1) @@ -885,64 +932,69 @@ def count_subscribers(self, recount=True, verbose=False): active_premium += max(0, results[3] - 1) archive += max(0, results[4] - 1) pro += max(0, results[5] - 1) - + original_num_subscribers = self.num_subscribers original_active_subs = self.active_subscribers original_premium_subscribers = self.premium_subscribers original_active_premium_subscribers = self.active_premium_subscribers original_archive_subscribers = self.archive_subscribers original_pro_subscribers = self.pro_subscribers - logging.info(" ---> [%-30s] ~SN~FBCounting subscribers from ~FCredis~FB: ~FMt:~SB~FM%s~SN a:~SB%s~SN p:~SB%s~SN ap:~SB%s~SN archive:~SB%s~SN pro:~SB%s ~SN~FC%s" % - (self.log_title[:30], total, active, premium, active_premium, archive, pro, "(%s branches)" % (len(feed_ids)-1) if len(feed_ids)>1 else "")) + logging.info( + " ---> [%-30s] ~SN~FBCounting subscribers from ~FCredis~FB: ~FMt:~SB~FM%s~SN a:~SB%s~SN p:~SB%s~SN ap:~SB%s~SN archive:~SB%s~SN pro:~SB%s ~SN~FC%s" + % ( + self.log_title[:30], + total, + active, + premium, + active_premium, + archive, + pro, + "(%s branches)" % (len(feed_ids) - 1) if len(feed_ids) > 1 else "", + ) + ) else: from apps.reader.models import UserSubscription - + subs = UserSubscription.objects.filter(feed__in=feed_ids) original_num_subscribers = self.num_subscribers total = subs.count() - + active_subs = UserSubscription.objects.filter( - feed__in=feed_ids, - active=True, - user__profile__last_seen_on__gte=SUBSCRIBER_EXPIRE_DATE + feed__in=feed_ids, active=True, user__profile__last_seen_on__gte=SUBSCRIBER_EXPIRE_DATE ) original_active_subs = self.active_subscribers active = active_subs.count() - + premium_subs = UserSubscription.objects.filter( - feed__in=feed_ids, - active=True, - user__profile__is_premium=True + feed__in=feed_ids, active=True, user__profile__is_premium=True ) original_premium_subscribers = self.premium_subscribers premium = premium_subs.count() - + archive_subs = UserSubscription.objects.filter( - feed__in=feed_ids, - active=True, - user__profile__is_archive=True + feed__in=feed_ids, active=True, user__profile__is_archive=True ) original_archive_subscribers = self.archive_subscribers archive = archive_subs.count() - + pro_subs = UserSubscription.objects.filter( - feed__in=feed_ids, - active=True, - user__profile__is_pro=True + feed__in=feed_ids, active=True, user__profile__is_pro=True ) original_pro_subscribers = self.pro_subscribers pro = pro_subs.count() - + active_premium_subscribers = UserSubscription.objects.filter( - feed__in=feed_ids, + feed__in=feed_ids, active=True, user__profile__is_premium=True, - user__profile__last_seen_on__gte=SUBSCRIBER_EXPIRE_DATE + user__profile__last_seen_on__gte=SUBSCRIBER_EXPIRE_DATE, ) original_active_premium_subscribers = self.active_premium_subscribers active_premium = active_premium_subscribers.count() - logging.debug(" ---> [%-30s] ~SN~FBCounting subscribers from ~FYpostgres~FB: ~FMt:~SB~FM%s~SN a:~SB%s~SN p:~SB%s~SN ap:~SB%s~SN archive:~SB%s~SN pro:~SB%s" % - (self.log_title[:30], total, active, premium, active_premium, archive, pro)) + logging.debug( + " ---> [%-30s] ~SN~FBCounting subscribers from ~FYpostgres~FB: ~FMt:~SB~FM%s~SN a:~SB%s~SN p:~SB%s~SN ap:~SB%s~SN archive:~SB%s~SN pro:~SB%s" + % (self.log_title[:30], total, active, premium, active_premium, archive, pro) + ) if settings.DOCKERBUILD: # Local installs enjoy 100% active feeds @@ -955,42 +1007,55 @@ def count_subscribers(self, recount=True, verbose=False): self.active_premium_subscribers = active_premium self.archive_subscribers = archive self.pro_subscribers = pro - if (self.num_subscribers != original_num_subscribers or - self.active_subscribers != original_active_subs or - self.premium_subscribers != original_premium_subscribers or - self.active_premium_subscribers != original_active_premium_subscribers or - self.archive_subscribers != original_archive_subscribers or - self.pro_subscribers != original_pro_subscribers): + if ( + self.num_subscribers != original_num_subscribers + or self.active_subscribers != original_active_subs + or self.premium_subscribers != original_premium_subscribers + or self.active_premium_subscribers != original_active_premium_subscribers + or self.archive_subscribers != original_archive_subscribers + or self.pro_subscribers != original_pro_subscribers + ): if original_premium_subscribers == -1 or original_active_premium_subscribers == -1: self.save() else: - self.save(update_fields=['num_subscribers', 'active_subscribers', - 'premium_subscribers', 'active_premium_subscribers', - 'archive_subscribers', 'pro_subscribers']) - + self.save( + update_fields=[ + "num_subscribers", + "active_subscribers", + "premium_subscribers", + "active_premium_subscribers", + "archive_subscribers", + "pro_subscribers", + ] + ) + if verbose: if self.num_subscribers <= 1: print(".", end=" ") else: - print("\n %s> %s subscriber%s: %s" % ( - '-' * min(self.num_subscribers, 20), - self.num_subscribers, - '' if self.num_subscribers == 1 else 's', - self.feed_title, - ), end=' ') - + print( + "\n %s> %s subscriber%s: %s" + % ( + "-" * min(self.num_subscribers, 20), + self.num_subscribers, + "" if self.num_subscribers == 1 else "s", + self.feed_title, + ), + end=" ", + ) + def _split_favicon_color(self, color=None): if not color: color = self.favicon_color if not color: return None, None, None - splitter = lambda s, p: [s[i:i+p] for i in range(0, len(s), p)] + splitter = lambda s, p: [s[i : i + p] for i in range(0, len(s), p)] red, green, blue = splitter(color[:6], 2) return red, green, blue - + def favicon_fade(self): return self.adjust_color(adjust=30) - + def adjust_color(self, color=None, adjust=0): red, green, blue = self._split_favicon_color(color=color) if red and green and blue: @@ -1002,11 +1067,11 @@ def adjust_color(self, color=None, adjust=0): def favicon_border(self): red, green, blue = self._split_favicon_color() if red and green and blue: - fade_red = hex(min(int(int(red, 16) * .75), 255))[2:].zfill(2) - fade_green = hex(min(int(int(green, 16) * .75), 255))[2:].zfill(2) - fade_blue = hex(min(int(int(blue, 16) * .75), 255))[2:].zfill(2) + fade_red = hex(min(int(int(red, 16) * 0.75), 255))[2:].zfill(2) + fade_green = hex(min(int(int(green, 16) * 0.75), 255))[2:].zfill(2) + fade_blue = hex(min(int(int(blue, 16) * 0.75), 255))[2:].zfill(2) return "%s%s%s" % (fade_red, fade_green, fade_blue) - + def favicon_text_color(self): # Color format: {r: 1, g: .5, b: 0} def contrast(color1, color2): @@ -1018,10 +1083,10 @@ def contrast(color1, color2): return (lum2 + 0.05) / (lum1 + 0.05) def luminosity(color): - r = color['red'] - g = color['green'] - b = color['blue'] - val = lambda c: c/12.92 if c <= 0.02928 else math.pow(((c + 0.055)/1.055), 2.4) + r = color["red"] + g = color["green"] + b = color["blue"] + val = lambda c: c / 12.92 if c <= 0.02928 else math.pow(((c + 0.055) / 1.055), 2.4) red = val(r) green = val(g) blue = val(b) @@ -1030,25 +1095,25 @@ def luminosity(color): red, green, blue = self._split_favicon_color() if red and green and blue: color = { - 'red': int(red, 16) / 256.0, - 'green': int(green, 16) / 256.0, - 'blue': int(blue, 16) / 256.0, + "red": int(red, 16) / 256.0, + "green": int(green, 16) / 256.0, + "blue": int(blue, 16) / 256.0, } white = { - 'red': 1, - 'green': 1, - 'blue': 1, + "red": 1, + "green": 1, + "blue": 1, } grey = { - 'red': 0.5, - 'green': 0.5, - 'blue': 0.5, + "red": 0.5, + "green": 0.5, + "blue": 0.5, } - + if contrast(color, white) > contrast(color, grey): - return 'white' + return "white" else: - return 'black' + return "black" def fill_out_archive_stories(self, force=False, starting_page=1): """ @@ -1058,33 +1123,34 @@ def fill_out_archive_stories(self, force=False, starting_page=1): before_story_count = MStory.objects(story_feed_id=self.pk).count() if not force and not self.archive_subscribers: - logging.debug(" ---> [%-30s] ~FBNot filling out archive stories, no archive subscribers" % ( - self.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FBNot filling out archive stories, no archive subscribers" + % (self.log_title[:30]) + ) return before_story_count, before_story_count self.update(archive_page=starting_page) after_story_count = MStory.objects(story_feed_id=self.pk).count() - logging.debug(" ---> [%-30s] ~FCFilled out archive, ~FM~SB%s~SN new stories~FC, total of ~SB%s~SN stories" % ( - self.log_title[:30], - after_story_count - before_story_count, - after_story_count)) - + logging.debug( + " ---> [%-30s] ~FCFilled out archive, ~FM~SB%s~SN new stories~FC, total of ~SB%s~SN stories" + % (self.log_title[:30], after_story_count - before_story_count, after_story_count) + ) + def save_feed_stories_last_month(self, verbose=False): month_ago = datetime.datetime.utcnow() - datetime.timedelta(days=30) - stories_last_month = MStory.objects(story_feed_id=self.pk, - story_date__gte=month_ago).count() + stories_last_month = MStory.objects(story_feed_id=self.pk, story_date__gte=month_ago).count() if self.stories_last_month != stories_last_month: self.stories_last_month = stories_last_month - self.save(update_fields=['stories_last_month']) - + self.save(update_fields=["stories_last_month"]) + if verbose: print(f" ---> {self.feed} [{self.pk}]: {self.stories_last_month} stories last month") - + def save_feed_story_history_statistics(self, current_counts=None): """ Fills in missing months between earlier occurances and now. - + Save format: [('YYYY-MM, #), ...] Example output: [(2010-12, 123), (2011-01, 146)] """ @@ -1096,7 +1162,7 @@ def save_feed_story_history_statistics(self, current_counts=None): current_counts = self.data.story_count_history and json.decode(self.data.story_count_history) if isinstance(current_counts, dict): - current_counts = current_counts['months'] + current_counts = current_counts["months"] if not current_counts: current_counts = [] @@ -1118,15 +1184,15 @@ def save_feed_story_history_statistics(self, current_counts=None): dates = defaultdict(int) hours = defaultdict(int) days = defaultdict(int) - results = MStory.objects(story_feed_id=self.pk).map_reduce(map_f, reduce_f, output='inline') + results = MStory.objects(story_feed_id=self.pk).map_reduce(map_f, reduce_f, output="inline") for result in results: - dates[result.value['month']] += 1 - hours[int(result.value['hour'])] += 1 - days[int(result.value['day'])] += 1 - year = int(re.findall(r"(\d{4})-\d{1,2}", result.value['month'])[0]) + dates[result.value["month"]] += 1 + hours[int(result.value["hour"])] += 1 + days[int(result.value["day"])] += 1 + year = int(re.findall(r"(\d{4})-\d{1,2}", result.value["month"])[0]) if year < min_year and year > 2000: min_year = year - + # Add on to existing months, always amending up, never down. (Current month # is guaranteed to be accurate, since trim_feeds won't delete it until after # a month. Hacker News can have 1,000+ and still be counted.) @@ -1136,38 +1202,42 @@ def save_feed_story_history_statistics(self, current_counts=None): dates[current_month] = current_count if year < min_year and year > 2000: min_year = year - - # Assemble a list with 0's filled in for missing months, + + # Assemble a list with 0's filled in for missing months, # trimming left and right 0's. months = [] start = False - for year in range(min_year, now.year+1): - for month in range(1, 12+1): + for year in range(min_year, now.year + 1): + for month in range(1, 12 + 1): if datetime.datetime(year, month, 1) < now: - key = '%s-%s' % (year, month) + key = "%s-%s" % (year, month) if dates.get(key) or start: start = True months.append((key, dates.get(key, 0))) total += dates.get(key, 0) if dates.get(key, 0) > 0: - month_count += 1 # Only count months that have stories for the average + month_count += 1 # Only count months that have stories for the average original_story_count_history = self.data.story_count_history - self.data.story_count_history = json.encode({'months': months, 'hours': hours, 'days': days}) + self.data.story_count_history = json.encode({"months": months, "hours": hours, "days": days}) if self.data.story_count_history != original_story_count_history: - self.data.save(update_fields=['story_count_history']) - + self.data.save(update_fields=["story_count_history"]) + original_average_stories_per_month = self.average_stories_per_month if not total or not month_count: self.average_stories_per_month = 0 else: self.average_stories_per_month = int(round(total / float(month_count))) if self.average_stories_per_month != original_average_stories_per_month: - self.save(update_fields=['average_stories_per_month']) - - + self.save(update_fields=["average_stories_per_month"]) + def save_classifier_counts(self): - from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag - + from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + ) + def calculate_scores(cls, facet): map_f = """ function() { @@ -1176,7 +1246,9 @@ def calculate_scores(cls, facet): neg: this.score<0 ? Math.abs(this.score) : 0 }); } - """ % (facet) + """ % ( + facet + ) reduce_f = """ function(key, values) { var result = {pos: 0, neg: 0}; @@ -1188,68 +1260,72 @@ def calculate_scores(cls, facet): } """ scores = [] - res = cls.objects(feed_id=self.pk).map_reduce(map_f, reduce_f, output='inline') + res = cls.objects(feed_id=self.pk).map_reduce(map_f, reduce_f, output="inline") for r in res: - facet_values = dict([(k, int(v)) for k,v in r.value.items()]) + facet_values = dict([(k, int(v)) for k, v in r.value.items()]) facet_values[facet] = r.key - if facet_values['pos'] + facet_values['neg'] >= 1: + if facet_values["pos"] + facet_values["neg"] >= 1: scores.append(facet_values) - scores = sorted(scores, key=lambda v: v['neg'] - v['pos']) + scores = sorted(scores, key=lambda v: v["neg"] - v["pos"]) return scores - + scores = {} - for cls, facet in [(MClassifierTitle, 'title'), - (MClassifierAuthor, 'author'), - (MClassifierTag, 'tag'), - (MClassifierFeed, 'feed_id')]: + for cls, facet in [ + (MClassifierTitle, "title"), + (MClassifierAuthor, "author"), + (MClassifierTag, "tag"), + (MClassifierFeed, "feed_id"), + ]: scores[facet] = calculate_scores(cls, facet) - if facet == 'feed_id' and scores[facet]: - scores['feed'] = scores[facet] - del scores['feed_id'] + if facet == "feed_id" and scores[facet]: + scores["feed"] = scores[facet] + del scores["feed_id"] elif not scores[facet]: del scores[facet] - + if scores: self.data.feed_classifier_counts = json.encode(scores) self.data.save() - + return scores - + @property def user_agent(self): feed_parts = urllib.parse.urlparse(self.feed_address) - if feed_parts.netloc.find('.tumblr.com') != -1: + if feed_parts.netloc.find(".tumblr.com") != -1: # Certain tumblr feeds will redirect to tumblr's login page when fetching. # A known workaround is using facebook's user agent. - return 'facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)' + return "facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)" - ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s %s' % ( - self.num_subscribers, - 's' if self.num_subscribers != 1 else '', - self.permalink, - self.fake_user_agent, - )) + ua = "NewsBlur Feed Fetcher - %s subscriber%s - %s %s" % ( + self.num_subscribers, + "s" if self.num_subscribers != 1 else "", + self.permalink, + self.fake_user_agent, + ) return ua - + @property def fake_user_agent(self): - ua = ('("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' - 'AppleWebKit/605.1.15 (KHTML, like Gecko) ' - 'Version/14.0.1 Safari/605.1.15")') - + ua = ( + '("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + 'Version/14.0.1 Safari/605.1.15")' + ) + return ua - + def fetch_headers(self, fake=False): headers = { - 'User-Agent': self.user_agent if not fake else self.fake_user_agent, - 'Accept': 'application/atom+xml, application/rss+xml, application/xml;q=0.8, text/xml;q=0.6, */*;q=0.2', - 'Accept-Encoding': 'gzip, deflate', + "User-Agent": self.user_agent if not fake else self.fake_user_agent, + "Accept": "application/atom+xml, application/rss+xml, application/xml;q=0.8, text/xml;q=0.6, */*;q=0.2", + "Accept-Encoding": "gzip, deflate", } - + return headers - + def update(self, **kwargs): try: from utils import feed_fetcher @@ -1260,24 +1336,24 @@ def update(self, **kwargs): original_feed_id = int(self.pk) options = { - 'verbose': kwargs.get('verbose'), - 'timeout': 10, - 'single_threaded': kwargs.get('single_threaded', True), - 'force': kwargs.get('force'), - 'force_fp': kwargs.get('force_fp'), - 'compute_scores': kwargs.get('compute_scores', True), - 'mongodb_replication_lag': kwargs.get('mongodb_replication_lag', None), - 'fake': kwargs.get('fake'), - 'quick': kwargs.get('quick'), - 'updates_off': kwargs.get('updates_off'), - 'debug': kwargs.get('debug'), - 'fpf': kwargs.get('fpf'), - 'feed_xml': kwargs.get('feed_xml'), - 'requesting_user_id': kwargs.get('requesting_user_id', None), - 'archive_page': kwargs.get('archive_page', None), + "verbose": kwargs.get("verbose"), + "timeout": 10, + "single_threaded": kwargs.get("single_threaded", True), + "force": kwargs.get("force"), + "force_fp": kwargs.get("force_fp"), + "compute_scores": kwargs.get("compute_scores", True), + "mongodb_replication_lag": kwargs.get("mongodb_replication_lag", None), + "fake": kwargs.get("fake"), + "quick": kwargs.get("quick"), + "updates_off": kwargs.get("updates_off"), + "debug": kwargs.get("debug"), + "fpf": kwargs.get("fpf"), + "feed_xml": kwargs.get("feed_xml"), + "requesting_user_id": kwargs.get("requesting_user_id", None), + "archive_page": kwargs.get("archive_page", None), } - - if getattr(settings, 'TEST_DEBUG', False) and "NEWSBLUR_DIR" in self.feed_address: + + if getattr(settings, "TEST_DEBUG", False) and "NEWSBLUR_DIR" in self.feed_address: print(" ---> Testing feed fetch: %s" % self.log_title) # options['force_fp'] = True # No, why would this be needed? original_feed_address = self.feed_address @@ -1286,39 +1362,42 @@ def update(self, **kwargs): if self.feed_link: self.feed_link = self.feed_link.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR) if self.feed_address != original_feed_address or self.feed_link != original_feed_link: - self.save(update_fields=['feed_address', 'feed_link']) - + self.save(update_fields=["feed_address", "feed_link"]) + if self.is_newsletter: feed = self.update_newsletter_icon() else: - disp = feed_fetcher.Dispatcher(options, 1) + disp = feed_fetcher.Dispatcher(options, 1) disp.add_jobs([[self.pk]]) feed = disp.run_jobs() - + if feed: feed = Feed.get_by_id(feed.pk) if feed: feed.last_update = datetime.datetime.utcnow() feed.set_next_scheduled_update(verbose=settings.DEBUG) - r.zadd('fetched_feeds_last_hour', { feed.pk: int(datetime.datetime.now().strftime('%s')) }) - + r.zadd("fetched_feeds_last_hour", {feed.pk: int(datetime.datetime.now().strftime("%s"))}) + if not feed or original_feed_id != feed.pk: - logging.info(" ---> ~FRFeed changed id, removing %s from tasked_feeds queue..." % original_feed_id) - r.zrem('tasked_feeds', original_feed_id) - r.zrem('error_feeds', original_feed_id) + logging.info( + " ---> ~FRFeed changed id, removing %s from tasked_feeds queue..." % original_feed_id + ) + r.zrem("tasked_feeds", original_feed_id) + r.zrem("error_feeds", original_feed_id) if feed: - r.zrem('tasked_feeds', feed.pk) - r.zrem('error_feeds', feed.pk) - + r.zrem("tasked_feeds", feed.pk) + r.zrem("error_feeds", feed.pk) + return feed - + def update_newsletter_icon(self): from apps.rss_feeds.icon_importer import IconImporter + icon_importer = IconImporter(self) icon_importer.save() - + return self - + @classmethod def get_by_id(cls, feed_id, feed_address=None): try: @@ -1333,41 +1412,43 @@ def get_by_id(cls, feed_id, feed_address=None): duplicate_feeds = DuplicateFeed.objects.filter(duplicate_address=feed_address) if duplicate_feeds: return duplicate_feeds[0].feed - + @classmethod def get_by_name(cls, query, limit=1): results = SearchFeed.query(query) feed_ids = [result.feed_id for result in results] - + if limit == 1: return Feed.get_by_id(feed_ids[0]) else: return [Feed.get_by_id(f) for f in feed_ids][:limit] - + def add_update_stories(self, stories, existing_stories, verbose=False, updates_off=False): ret_values = dict(new=0, updated=0, same=0, error=0) error_count = self.error_count - new_story_hashes = [s.get('story_hash') for s in stories] - + new_story_hashes = [s.get("story_hash") for s in stories] + if settings.DEBUG or verbose: - logging.debug(" ---> [%-30s] ~FBChecking ~SB%s~SN new/updated against ~SB%s~SN stories" % ( - self.log_title[:30], - len(stories), - len(list(existing_stories.keys())))) + logging.debug( + " ---> [%-30s] ~FBChecking ~SB%s~SN new/updated against ~SB%s~SN stories" + % (self.log_title[:30], len(stories), len(list(existing_stories.keys()))) + ) + @timelimit(5) def _1(story, story_content, existing_stories, new_story_hashes): - existing_story, story_has_changed = self._exists_story(story, story_content, - existing_stories, new_story_hashes) + existing_story, story_has_changed = self._exists_story( + story, story_content, existing_stories, new_story_hashes + ) return existing_story, story_has_changed - + for story in stories: if verbose: - logging.debug(" ---> [%-30s] ~FBChecking ~SB%s~SN / ~SB%s" % ( - self.log_title[:30], - story.get('title'), - story.get('guid'))) - - story_content = story.get('story_content') + logging.debug( + " ---> [%-30s] ~FBChecking ~SB%s~SN / ~SB%s" + % (self.log_title[:30], story.get("title"), story.get("guid")) + ) + + story_content = story.get("story_content") if error_count: story_content = strip_comments__lxml(story_content) else: @@ -1375,39 +1456,49 @@ def _1(story, story_content, existing_stories, new_story_hashes): story_tags = self.get_tags(story) story_link = self.get_permalink(story) replace_story_date = False - + try: - existing_story, story_has_changed = _1(story, story_content, - existing_stories, new_story_hashes) + existing_story, story_has_changed = _1( + story, story_content, existing_stories, new_story_hashes + ) except TimeoutError: - logging.debug(' ---> [%-30s] ~SB~FRExisting story check timed out...' % (self.log_title[:30])) + logging.debug( + " ---> [%-30s] ~SB~FRExisting story check timed out..." % (self.log_title[:30]) + ) existing_story = None story_has_changed = False - + if existing_story is None: if settings.DEBUG and False: - logging.debug(' ---> New story in feed (%s - %s): %s' % (self.feed_title, story.get('title'), len(story_content))) - - s = MStory(story_feed_id = self.pk, - story_date = story.get('published'), - story_title = story.get('title'), - story_content = story_content, - story_author_name = story.get('author'), - story_permalink = story_link, - story_guid = story.get('guid'), - story_tags = story_tags + logging.debug( + " ---> New story in feed (%s - %s): %s" + % (self.feed_title, story.get("title"), len(story_content)) + ) + + s = MStory( + story_feed_id=self.pk, + story_date=story.get("published"), + story_title=story.get("title"), + story_content=story_content, + story_author_name=story.get("author"), + story_permalink=story_link, + story_guid=story.get("guid"), + story_tags=story_tags, ) try: s.save() - ret_values['new'] += 1 + ret_values["new"] += 1 s.publish_to_subscribers() except (IntegrityError, OperationError) as e: - ret_values['error'] += 1 + ret_values["error"] += 1 if settings.DEBUG: - logging.info(' ---> [%-30s] ~SN~FRIntegrityError on new story: %s - %s' % (self.feed_title[:30], story.get('guid'), e)) + logging.info( + " ---> [%-30s] ~SN~FRIntegrityError on new story: %s - %s" + % (self.feed_title[:30], story.get("guid"), e) + ) if self.search_indexed: s.index_story_for_search() - elif existing_story and story_has_changed and not updates_off and ret_values['updated'] < 3: + elif existing_story and story_has_changed and not updates_off and ret_values["updated"] < 3: # update story original_content = None try: @@ -1415,19 +1506,22 @@ def _1(story, story_content, existing_stories, new_story_hashes): try: existing_story = MStory.objects.get(id=existing_story.id) except ValidationError: - existing_story, _ = MStory.find_story(existing_story.story_feed_id, - existing_story.id, - original_only=True) + existing_story, _ = MStory.find_story( + existing_story.story_feed_id, existing_story.id, original_only=True + ) elif existing_story and existing_story.story_hash: - existing_story, _ = MStory.find_story(existing_story.story_feed_id, - existing_story.story_hash, - original_only=True) + existing_story, _ = MStory.find_story( + existing_story.story_feed_id, existing_story.story_hash, original_only=True + ) else: raise MStory.DoesNotExist except (MStory.DoesNotExist, OperationError) as e: - ret_values['error'] += 1 + ret_values["error"] += 1 if verbose: - logging.info(' ---> [%-30s] ~SN~FROperation on existing story: %s - %s' % (self.feed_title[:30], story.get('guid'), e)) + logging.info( + " ---> [%-30s] ~SN~FROperation on existing story: %s - %s" + % (self.feed_title[:30], story.get("guid"), e) + ) continue if existing_story.story_original_content_z: original_content = zlib.decompress(existing_story.story_original_content_z) @@ -1445,60 +1539,71 @@ def _1(story, story_content, existing_stories, new_story_hashes): # logging.debug("\t\tDiff content: %s" % diff.getDiff()) # if existing_story.story_title != story.get('title'): # logging.debug('\tExisting title / New: : \n\t\t- %s\n\t\t- %s' % (existing_story.story_title, story.get('title'))) - if existing_story.story_hash != story.get('story_hash'): - self.update_story_with_new_guid(existing_story, story.get('guid')) + if existing_story.story_hash != story.get("story_hash"): + self.update_story_with_new_guid(existing_story, story.get("guid")) if verbose: - logging.debug('- Updated story in feed (%s - %s): %s / %s' % (self.feed_title, story.get('title'), len(story_content_diff), len(story_content))) - + logging.debug( + "- Updated story in feed (%s - %s): %s / %s" + % (self.feed_title, story.get("title"), len(story_content_diff), len(story_content)) + ) + existing_story.story_feed = self.pk - existing_story.story_title = story.get('title') + existing_story.story_title = story.get("title") existing_story.story_content = story_content_diff existing_story.story_latest_content = story_content existing_story.story_original_content = original_content - existing_story.story_author_name = story.get('author') + existing_story.story_author_name = story.get("author") existing_story.story_permalink = story_link - existing_story.story_guid = story.get('guid') + existing_story.story_guid = story.get("guid") existing_story.story_tags = story_tags - existing_story.original_text_z = None # Reset Text view cache + existing_story.original_text_z = None # Reset Text view cache # Do not allow publishers to change the story date once a story is published. # Leads to incorrect unread story counts. if replace_story_date: - existing_story.story_date = story.get('published') # Really shouldn't do this. + existing_story.story_date = story.get("published") # Really shouldn't do this. existing_story.extract_image_urls(force=True) try: existing_story.save() - ret_values['updated'] += 1 + ret_values["updated"] += 1 except (IntegrityError, OperationError): - ret_values['error'] += 1 + ret_values["error"] += 1 if verbose: - logging.info(' ---> [%-30s] ~SN~FRIntegrityError on updated story: %s' % (self.feed_title[:30], story.get('title')[:30])) + logging.info( + " ---> [%-30s] ~SN~FRIntegrityError on updated story: %s" + % (self.feed_title[:30], story.get("title")[:30]) + ) except ValidationError: - ret_values['error'] += 1 + ret_values["error"] += 1 if verbose: - logging.info(' ---> [%-30s] ~SN~FRValidationError on updated story: %s' % (self.feed_title[:30], story.get('title')[:30])) + logging.info( + " ---> [%-30s] ~SN~FRValidationError on updated story: %s" + % (self.feed_title[:30], story.get("title")[:30]) + ) if self.search_indexed: existing_story.index_story_for_search() else: - ret_values['same'] += 1 + ret_values["same"] += 1 if verbose: - logging.debug("Unchanged story (%s): %s / %s " % (story.get('story_hash'), story.get('guid'), story.get('title'))) - + logging.debug( + "Unchanged story (%s): %s / %s " + % (story.get("story_hash"), story.get("guid"), story.get("title")) + ) + return ret_values - + def update_story_with_new_guid(self, existing_story, new_story_guid): from apps.reader.models import RUserStory from apps.social.models import MSharedStory existing_story.remove_from_redis() existing_story.remove_from_search_index() - + old_hash = existing_story.story_hash new_hash = MStory.ensure_story_hash(new_story_guid, self.pk) RUserStory.switch_hash(feed=self, old_hash=old_hash, new_hash=new_hash) - - shared_stories = MSharedStory.objects.filter(story_feed_id=self.pk, - story_hash=old_hash) + + shared_stories = MSharedStory.objects.filter(story_feed_id=self.pk, story_hash=old_hash) for story in shared_stories: story.story_guid = new_story_guid story.story_hash = new_hash @@ -1507,18 +1612,19 @@ def update_story_with_new_guid(self, existing_story, new_story_guid): except NotUniqueError: # Story is already shared, skip. pass - + def save_popular_tags(self, feed_tags=None, verbose=False): if not feed_tags: - all_tags = MStory.objects(story_feed_id=self.pk, - story_tags__exists=True).item_frequencies('story_tags') - feed_tags = sorted([(k, v) for k, v in list(all_tags.items()) if int(v) > 0], - key=itemgetter(1), - reverse=True)[:25] + all_tags = MStory.objects(story_feed_id=self.pk, story_tags__exists=True).item_frequencies( + "story_tags" + ) + feed_tags = sorted( + [(k, v) for k, v in list(all_tags.items()) if int(v) > 0], key=itemgetter(1), reverse=True + )[:25] popular_tags = json.encode(feed_tags) if verbose: print("Found %s tags: %s" % (len(feed_tags), popular_tags)) - + # TODO: This len() bullshit will be gone when feeds move to mongo # On second thought, it might stay, because we don't want # popular tags the size of a small planet. I'm looking at you @@ -1526,7 +1632,7 @@ def save_popular_tags(self, feed_tags=None, verbose=False): if len(popular_tags) < 1024: if self.data.popular_tags != popular_tags: self.data.popular_tags = popular_tags - self.data.save(update_fields=['popular_tags']) + self.data.save(update_fields=["popular_tags"]) return tags_list = [] @@ -1534,21 +1640,21 @@ def save_popular_tags(self, feed_tags=None, verbose=False): tags_list = json.decode(feed_tags) if len(tags_list) >= 1: self.save_popular_tags(tags_list[:-1]) - + def save_popular_authors(self, feed_authors=None): if not feed_authors: authors = defaultdict(int) - for story in MStory.objects(story_feed_id=self.pk).only('story_author_name'): + for story in MStory.objects(story_feed_id=self.pk).only("story_author_name"): authors[story.story_author_name] += 1 - feed_authors = sorted([(k, v) for k, v in list(authors.items()) if k], - key=itemgetter(1), - reverse=True)[:20] + feed_authors = sorted( + [(k, v) for k, v in list(authors.items()) if k], key=itemgetter(1), reverse=True + )[:20] popular_authors = json.encode(feed_authors) if len(popular_authors) < 1023: if self.data.popular_authors != popular_authors: self.data.popular_authors = popular_authors - self.data.save(update_fields=['popular_authors']) + self.data.save(update_fields=["popular_authors"]) return if len(feed_authors) > 1: @@ -1558,19 +1664,24 @@ def save_popular_authors(self, feed_authors=None): def trim_old_stories(cls, start=0, verbose=True, dryrun=False, total=0, end=None): now = datetime.datetime.now() month_ago = now - datetime.timedelta(days=settings.DAYS_OF_STORY_HASHES) - feed_count = end or Feed.objects.latest('pk').pk + feed_count = end or Feed.objects.latest("pk").pk for feed_id in range(start, feed_count): if feed_id % 1000 == 0: - print("\n\n -------------------------- %s (%s deleted so far) --------------------------\n\n" % (feed_id, total)) + print( + "\n\n -------------------------- %s (%s deleted so far) --------------------------\n\n" + % (feed_id, total) + ) try: feed = Feed.objects.get(pk=feed_id) except Feed.DoesNotExist: continue # Ensure only feeds with no active subscribers are being trimmed - if (feed.active_subscribers <= 0 and - (not feed.archive_subscribers or feed.archive_subscribers <= 0) and - (not feed.last_story_date or feed.last_story_date < month_ago)): + if ( + feed.active_subscribers <= 0 + and (not feed.archive_subscribers or feed.archive_subscribers <= 0) + and (not feed.last_story_date or feed.last_story_date < month_ago) + ): # 1 month since last story = keep 5 stories, >6 months since, only keep 1 story months_ago = 6 if feed.last_story_date: @@ -1585,18 +1696,17 @@ def trim_old_stories(cls, start=0, verbose=True, dryrun=False, total=0, end=None print(" DRYRUN: %s/%s cutoff - %s" % (cutoff, feed.story_cutoff, feed)) else: total += feed.trim_feed(verbose=verbose) - - + print(" ---> Deleted %s stories in total." % total) - + @property def story_cutoff(self): return self.number_of_stories_to_store() - + def number_of_stories_to_store(self, pre_archive=False): if self.archive_subscribers and self.archive_subscribers > 0 and not pre_archive: return 10000 - + cutoff = 500 if self.active_subscribers <= 0: cutoff = 25 @@ -1612,21 +1722,25 @@ def number_of_stories_to_store(self, pre_archive=False): cutoff = 400 elif self.active_premium_subscribers <= 20: cutoff = 450 - + if self.active_subscribers and self.average_stories_per_month < 5 and self.stories_last_month < 5: cutoff /= 2 - if self.active_premium_subscribers <= 1 and self.average_stories_per_month <= 1 and self.stories_last_month <= 1: + if ( + self.active_premium_subscribers <= 1 + and self.average_stories_per_month <= 1 + and self.stories_last_month <= 1 + ): cutoff /= 2 - + r = redis.Redis(connection_pool=settings.REDIS_FEED_READ_POOL) pipeline = r.pipeline() read_stories_per_week = [] now = datetime.datetime.now() # Check to see how many stories have been read each week since the feed's days of story hashes - for weeks_back in range(2*int(math.floor(settings.DAYS_OF_STORY_HASHES/7))): - weeks_ago = now - datetime.timedelta(days=7*weeks_back) - week_of_year = weeks_ago.strftime('%Y-%U') + for weeks_back in range(2 * int(math.floor(settings.DAYS_OF_STORY_HASHES / 7))): + weeks_ago = now - datetime.timedelta(days=7 * weeks_back) + week_of_year = weeks_ago.strftime("%Y-%U") feed_read_key = "fR:%s:%s" % (self.pk, week_of_year) pipeline.get(feed_read_key) read_stories_per_week = pipeline.execute() @@ -1635,16 +1749,28 @@ def number_of_stories_to_store(self, pre_archive=False): original_cutoff = cutoff cutoff = min(cutoff, 10) try: - logging.debug(" ---> [%-30s] ~FBTrimming down to ~SB%s (instead of %s)~SN stories (~FM%s~FB)" % (self.log_title[:30], cutoff, original_cutoff, self.last_story_date.strftime("%Y-%m-%d") if self.last_story_date else "No last story date")) + logging.debug( + " ---> [%-30s] ~FBTrimming down to ~SB%s (instead of %s)~SN stories (~FM%s~FB)" + % ( + self.log_title[:30], + cutoff, + original_cutoff, + ( + self.last_story_date.strftime("%Y-%m-%d") + if self.last_story_date + else "No last story date" + ), + ) + ) except ValueError as e: logging.debug(" ***> [%-30s] Error trimming: %s" % (self.log_title[:30], e)) pass - - if getattr(settings, 'OVERRIDE_STORY_COUNT_MAX', None): + + if getattr(settings, "OVERRIDE_STORY_COUNT_MAX", None): cutoff = settings.OVERRIDE_STORY_COUNT_MAX - + return int(cutoff) - + def trim_feed(self, verbose=False, cutoff=None): if not cutoff: cutoff = self.story_cutoff @@ -1664,21 +1790,25 @@ def count_fs_size_bytes(self): for story in stories: count += 1 story_with_content = story.to_mongo() - if story_with_content.get('story_content_z', None): - story_with_content['story_content'] = zlib.decompress(story_with_content['story_content_z']) - del story_with_content['story_content_z'] - if story_with_content.get('original_page_z', None): - story_with_content['original_page'] = zlib.decompress(story_with_content['original_page_z']) - del story_with_content['original_page_z'] - if story_with_content.get('original_text_z', None): - story_with_content['original_text'] = zlib.decompress(story_with_content['original_text_z']) - del story_with_content['original_text_z'] - if story_with_content.get('story_latest_content_z', None): - story_with_content['story_latest_content'] = zlib.decompress(story_with_content['story_latest_content_z']) - del story_with_content['story_latest_content_z'] - if story_with_content.get('story_original_content_z', None): - story_with_content['story_original_content'] = zlib.decompress(story_with_content['story_original_content_z']) - del story_with_content['story_original_content_z'] + if story_with_content.get("story_content_z", None): + story_with_content["story_content"] = zlib.decompress(story_with_content["story_content_z"]) + del story_with_content["story_content_z"] + if story_with_content.get("original_page_z", None): + story_with_content["original_page"] = zlib.decompress(story_with_content["original_page_z"]) + del story_with_content["original_page_z"] + if story_with_content.get("original_text_z", None): + story_with_content["original_text"] = zlib.decompress(story_with_content["original_text_z"]) + del story_with_content["original_text_z"] + if story_with_content.get("story_latest_content_z", None): + story_with_content["story_latest_content"] = zlib.decompress( + story_with_content["story_latest_content_z"] + ) + del story_with_content["story_latest_content_z"] + if story_with_content.get("story_original_content_z", None): + story_with_content["story_original_content"] = zlib.decompress( + story_with_content["story_original_content_z"] + ) + del story_with_content["story_original_content_z"] sum_bytes += len(bson.BSON.encode(story_with_content)) self.fs_size_bytes = sum_bytes @@ -1686,7 +1816,7 @@ def count_fs_size_bytes(self): self.save() return sum_bytes - + def purge_feed_stories(self, update=True): MStory.purge_feed_stories(feed=self, cutoff=self.story_cutoff) if update: @@ -1695,15 +1825,21 @@ def purge_feed_stories(self, update=True): def purge_author(self, author): all_stories = MStory.objects.filter(story_feed_id=self.pk) author_stories = MStory.objects.filter(story_feed_id=self.pk, story_author_name__iexact=author) - logging.debug(" ---> Deleting %s of %s stories in %s by '%s'." % (author_stories.count(), all_stories.count(), self, author)) + logging.debug( + " ---> Deleting %s of %s stories in %s by '%s'." + % (author_stories.count(), all_stories.count(), self, author) + ) author_stories.delete() def purge_tag(self, tag): all_stories = MStory.objects.filter(story_feed_id=self.pk) tagged_stories = MStory.objects.filter(story_feed_id=self.pk, story_tags__icontains=tag) - logging.debug(" ---> Deleting %s of %s stories in %s by '%s'." % (tagged_stories.count(), all_stories.count(), self, tag)) + logging.debug( + " ---> Deleting %s of %s stories in %s by '%s'." + % (tagged_stories.count(), all_stories.count(), self, tag) + ) tagged_stories.delete() - + # @staticmethod # def clean_invalid_ids(): # history = MFeedFetchHistory.objects(status_code=500, exception__contains='InvalidId:') @@ -1711,43 +1847,42 @@ def purge_tag(self, tag): # for h in history: # u = re.split('InvalidId: (.*?) is not a valid ObjectId\\n$', h.exception)[1] # urls.add((h.feed_id, u)) - # + # # for f, u in urls: # print "db.stories.remove({\"story_feed_id\": %s, \"_id\": \"%s\"})" % (f, u) - def get_stories(self, offset=0, limit=25, order="neweat", force=False): if order == "newest": - stories_db = MStory.objects(story_feed_id=self.pk)[offset:offset+limit] + stories_db = MStory.objects(story_feed_id=self.pk)[offset : offset + limit] elif order == "oldest": - stories_db = MStory.objects(story_feed_id=self.pk).order_by('story_date')[offset:offset+limit] + stories_db = MStory.objects(story_feed_id=self.pk).order_by("story_date")[offset : offset + limit] stories = self.format_stories(stories_db, self.pk) - + return stories - + @classmethod def find_feed_stories(cls, feed_ids, query, order="newest", offset=0, limit=25): - story_ids = SearchStory.query(feed_ids=feed_ids, query=query, order=order, - offset=offset, limit=limit) - stories_db = MStory.objects( - story_hash__in=story_ids - ).order_by('-story_date' if order == "newest" else 'story_date') + story_ids = SearchStory.query(feed_ids=feed_ids, query=query, order=order, offset=offset, limit=limit) + stories_db = MStory.objects(story_hash__in=story_ids).order_by( + "-story_date" if order == "newest" else "story_date" + ) stories = cls.format_stories(stories_db) - + return stories - + @classmethod - def query_popularity(cls, query, limit, order='newest'): + def query_popularity(cls, query, limit, order="newest"): popularity = {} seen_feeds = set() feed_title_to_id = dict() - + # Collect stories, sort by feed story_ids = SearchStory.global_query(query, order=order, offset=0, limit=limit) for story_hash in story_ids: feed_id, story_id = MStory.split_story_hash(story_hash) feed = Feed.get_by_id(feed_id) - if not feed: continue + if not feed: + continue if feed.feed_title in seen_feeds: feed_id = feed_title_to_id[feed.feed_title] else: @@ -1758,250 +1893,349 @@ def query_popularity(cls, query, limit, order='newest'): # classifiers = feed.save_classifier_counts() well_read_score = feed.well_read_score() popularity[feed_id] = { - 'feed_title': feed.feed_title, - 'feed_url': feed.feed_link, - 'num_subscribers': feed.num_subscribers, - 'feed_id': feed.pk, - 'story_ids': [], - 'authors': {}, - 'read_pct': well_read_score['read_pct'], - 'reader_count': well_read_score['reader_count'], - 'story_count': well_read_score['story_count'], - 'reach_score': well_read_score['reach_score'], - 'share_count': well_read_score['share_count'], - 'ps': 0, - 'ng': 0, - 'classifiers': json.decode(feed.data.feed_classifier_counts), + "feed_title": feed.feed_title, + "feed_url": feed.feed_link, + "num_subscribers": feed.num_subscribers, + "feed_id": feed.pk, + "story_ids": [], + "authors": {}, + "read_pct": well_read_score["read_pct"], + "reader_count": well_read_score["reader_count"], + "story_count": well_read_score["story_count"], + "reach_score": well_read_score["reach_score"], + "share_count": well_read_score["share_count"], + "ps": 0, + "ng": 0, + "classifiers": json.decode(feed.data.feed_classifier_counts), } - if popularity[feed_id]['classifiers']: - for classifier in popularity[feed_id]['classifiers'].get('feed', []): - if int(classifier['feed_id']) == int(feed_id): - popularity[feed_id]['ps'] = classifier['pos'] - popularity[feed_id]['ng'] = -1 * classifier['neg'] - popularity[feed_id]['story_ids'].append(story_hash) - - sorted_popularity = sorted(list(popularity.values()), key=lambda x: x['reach_score'], - reverse=True) - + if popularity[feed_id]["classifiers"]: + for classifier in popularity[feed_id]["classifiers"].get("feed", []): + if int(classifier["feed_id"]) == int(feed_id): + popularity[feed_id]["ps"] = classifier["pos"] + popularity[feed_id]["ng"] = -1 * classifier["neg"] + popularity[feed_id]["story_ids"].append(story_hash) + + sorted_popularity = sorted(list(popularity.values()), key=lambda x: x["reach_score"], reverse=True) + # Extract story authors from feeds for feed in sorted_popularity: - story_ids = feed['story_ids'] + story_ids = feed["story_ids"] stories_db = MStory.objects(story_hash__in=story_ids) stories = cls.format_stories(stories_db) for story in stories: - story['story_permalink'] = story['story_permalink'][:250] - if story['story_authors'] not in feed['authors']: - feed['authors'][story['story_authors']] = { - 'name': story['story_authors'], - 'count': 0, - 'ps': 0, - 'ng': 0, - 'tags': {}, - 'stories': [], + story["story_permalink"] = story["story_permalink"][:250] + if story["story_authors"] not in feed["authors"]: + feed["authors"][story["story_authors"]] = { + "name": story["story_authors"], + "count": 0, + "ps": 0, + "ng": 0, + "tags": {}, + "stories": [], } - author = feed['authors'][story['story_authors']] + author = feed["authors"][story["story_authors"]] seen = False - for seen_story in author['stories']: - if seen_story['url'] == story['story_permalink']: + for seen_story in author["stories"]: + if seen_story["url"] == story["story_permalink"]: seen = True break else: - author['stories'].append({ - 'title': story['story_title'], - 'url': story['story_permalink'], - 'date': story['story_date'], - }) - author['count'] += 1 - if seen: continue # Don't recount tags - - if feed['classifiers']: - for classifier in feed['classifiers'].get('author', []): - if classifier['author'] == author['name']: - author['ps'] = classifier['pos'] - author['ng'] = -1 * classifier['neg'] - - for tag in story['story_tags']: - if tag not in author['tags']: - author['tags'][tag] = {'name': tag, 'count': 0, 'ps': 0, 'ng': 0} - author['tags'][tag]['count'] += 1 - if feed['classifiers']: - for classifier in feed['classifiers'].get('tag', []): - if classifier['tag'] == tag: - author['tags'][tag]['ps'] = classifier['pos'] - author['tags'][tag]['ng'] = -1 * classifier['neg'] - - sorted_authors = sorted(list(feed['authors'].values()), key=lambda x: x['count']) - feed['authors'] = sorted_authors - + author["stories"].append( + { + "title": story["story_title"], + "url": story["story_permalink"], + "date": story["story_date"], + } + ) + author["count"] += 1 + if seen: + continue # Don't recount tags + + if feed["classifiers"]: + for classifier in feed["classifiers"].get("author", []): + if classifier["author"] == author["name"]: + author["ps"] = classifier["pos"] + author["ng"] = -1 * classifier["neg"] + + for tag in story["story_tags"]: + if tag not in author["tags"]: + author["tags"][tag] = {"name": tag, "count": 0, "ps": 0, "ng": 0} + author["tags"][tag]["count"] += 1 + if feed["classifiers"]: + for classifier in feed["classifiers"].get("tag", []): + if classifier["tag"] == tag: + author["tags"][tag]["ps"] = classifier["pos"] + author["tags"][tag]["ng"] = -1 * classifier["neg"] + + sorted_authors = sorted(list(feed["authors"].values()), key=lambda x: x["count"]) + feed["authors"] = sorted_authors + # pprint(sorted_popularity) return sorted_popularity - + def well_read_score(self): """Average percentage of stories read vs published across recently active subscribers""" from apps.reader.models import UserSubscription from apps.social.models import MSharedStory - + r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) p = r.pipeline() - + shared_stories = MSharedStory.objects(story_feed_id=self.pk).count() - - subscribing_users = UserSubscription.objects.filter(feed_id=self.pk).values('user_id') - subscribing_user_ids = [sub['user_id'] for sub in subscribing_users] - + + subscribing_users = UserSubscription.objects.filter(feed_id=self.pk).values("user_id") + subscribing_user_ids = [sub["user_id"] for sub in subscribing_users] + for user_id in subscribing_user_ids: user_rs = "RS:%s:%s" % (user_id, self.pk) p.scard(user_rs) - + counts = p.execute() counts = [c for c in counts if c > 0] reader_count = len(counts) - - now = datetime.datetime.now().strftime('%s') - unread_cutoff = self.unread_cutoff.strftime('%s') + + now = datetime.datetime.now().strftime("%s") + unread_cutoff = self.unread_cutoff.strftime("%s") story_count = len(r.zrangebyscore("zF:%s" % self.pk, max=now, min=unread_cutoff)) if reader_count and story_count: average_pct = (sum(counts) / float(reader_count)) / float(story_count) else: average_pct = 0 - + reach_score = average_pct * reader_count * story_count - - return {'read_pct': average_pct, 'reader_count': reader_count, - 'reach_score': reach_score, 'story_count': story_count, - 'share_count': shared_stories} - + + return { + "read_pct": average_pct, + "reader_count": reader_count, + "reach_score": reach_score, + "story_count": story_count, + "share_count": shared_stories, + } + @classmethod def xls_query_popularity(cls, queries, limit): import xlsxwriter from xlsxwriter.utility import xl_rowcol_to_cell if isinstance(queries, str): - queries = [q.strip() for q in queries.split(',')] - - title = 'NewsBlur-%s.xlsx' % slugify('-'.join(queries)) + queries = [q.strip() for q in queries.split(",")] + + title = "NewsBlur-%s.xlsx" % slugify("-".join(queries)) workbook = xlsxwriter.Workbook(title) - bold = workbook.add_format({'bold': 1}) - date_format = workbook.add_format({'num_format': 'mmm d yyyy'}) - unread_format = workbook.add_format({'font_color': '#E0E0E0'}) - + bold = workbook.add_format({"bold": 1}) + date_format = workbook.add_format({"num_format": "mmm d yyyy"}) + unread_format = workbook.add_format({"font_color": "#E0E0E0"}) + for query in queries: worksheet = workbook.add_worksheet(query) row = 1 col = 0 - worksheet.write(0, col, 'Publisher', bold) - worksheet.set_column(col, col, 15); col += 1 - worksheet.write(0, col, 'Feed URL', bold) - worksheet.set_column(col, col, 20); col += 1 - worksheet.write(0, col, 'Reach score', bold) - worksheet.write_comment(0, col, 'Feeds are sorted based on this score. It\'s simply the # of readers * # of stories in the past 30 days * the percentage of stories that are actually read.') - worksheet.set_column(col, col, 9); col += 1 - worksheet.write(0, col, '# subs', bold) - worksheet.write_comment(0, col, 'Total number of subscribers on NewsBlur, not necessarily active') - worksheet.set_column(col, col, 5); col += 1 - worksheet.write(0, col, '# readers', bold) - worksheet.write_comment(0, col, 'Total number of active subscribers who have read a story from the feed in the past 30 days.') - worksheet.set_column(col, col, 8); col += 1 + worksheet.write(0, col, "Publisher", bold) + worksheet.set_column(col, col, 15) + col += 1 + worksheet.write(0, col, "Feed URL", bold) + worksheet.set_column(col, col, 20) + col += 1 + worksheet.write(0, col, "Reach score", bold) + worksheet.write_comment( + 0, + col, + "Feeds are sorted based on this score. It's simply the # of readers * # of stories in the past 30 days * the percentage of stories that are actually read.", + ) + worksheet.set_column(col, col, 9) + col += 1 + worksheet.write(0, col, "# subs", bold) + worksheet.write_comment(0, col, "Total number of subscribers on NewsBlur, not necessarily active") + worksheet.set_column(col, col, 5) + col += 1 + worksheet.write(0, col, "# readers", bold) + worksheet.write_comment( + 0, + col, + "Total number of active subscribers who have read a story from the feed in the past 30 days.", + ) + worksheet.set_column(col, col, 8) + col += 1 worksheet.write(0, col, "read pct", bold) - worksheet.write_comment(0, col, "Of the active subscribers reading this feed in the past 30 days, this is the percentage of stories the average subscriber reads. Values over 100 pct signify that the feed has many shared stories, which throws off the number slightly but not significantly.") - worksheet.set_column(col, col, 8); col += 1 - worksheet.write(0, col, '# stories 30d', bold) - worksheet.write_comment(0, col, "It's important to ignore feeds that haven't published anything in the last 30 days, which is why this is part of the Reach Score.") - worksheet.set_column(col, col, 10); col += 1 - worksheet.write(0, col, '# shared', bold) - worksheet.write_comment(0, col, 'Number of stories from this feed that were shared on NewsBlur. This is a strong signal of interest although it is not included in the Reach Score.') - worksheet.set_column(col, col, 7); col += 1 - worksheet.write(0, col, '# feed pos', bold) - worksheet.write_comment(0, col, 'Number of times this feed was trained with a thumbs up. Users use training to hide stories they don\'t want to see while highlighting those that they do.') - worksheet.set_column(col, col, 8); col += 1 - worksheet.write(0, col, '# feed neg', bold) - worksheet.write_comment(0, col, 'Number of times this feed was trained with a thumbs down. Users use training to hide stories they don\'t want to see while highlighting those that they do.') - worksheet.set_column(col, col, 8); col += 1 - worksheet.write(0, col, 'Author', bold) - worksheet.set_column(col, col, 15); col += 1 - worksheet.write(0, col, '# author pos', bold) - worksheet.write_comment(0, col, 'Number of times this author was trained with a thumbs up. Users use training to hide stories they don\'t want to see while highlighting those that they do.') - worksheet.set_column(col, col, 10); col += 1 - worksheet.write(0, col, '# author neg', bold) - worksheet.write_comment(0, col, 'Number of times this author was trained with a thumbs down. Users use training to hide stories they don\'t want to see while highlighting those that they do.') - worksheet.set_column(col, col, 10); col += 1 - worksheet.write(0, col, 'Story title', bold) - worksheet.set_column(col, col, 30); col += 1 - worksheet.write(0, col, 'Story URL', bold) - worksheet.set_column(col, col, 20); col += 1 - worksheet.write(0, col, 'Story date', bold) - worksheet.set_column(col, col, 10); col += 1 - worksheet.write(0, col, 'Tag', bold) - worksheet.set_column(col, col, 15); col += 1 - worksheet.write(0, col, 'Tag count', bold) - worksheet.write_comment(0, col, 'Number of times this tag is used in other stories that also contain the search query.') - worksheet.set_column(col, col, 8); col += 1 - worksheet.write(0, col, '# tag pos', bold) - worksheet.write_comment(0, col, 'Number of times this tag was trained with a thumbs up. Users use training to hide stories they don\'t want to see while highlighting those that they do.') - worksheet.set_column(col, col, 7); col += 1 - worksheet.write(0, col, '# tag neg', bold) - worksheet.write_comment(0, col, 'Number of times this tag was trained with a thumbs down. Users use training to hide stories they don\'t want to see while highlighting those that they do.') - worksheet.set_column(col, col, 7); col += 1 + worksheet.write_comment( + 0, + col, + "Of the active subscribers reading this feed in the past 30 days, this is the percentage of stories the average subscriber reads. Values over 100 pct signify that the feed has many shared stories, which throws off the number slightly but not significantly.", + ) + worksheet.set_column(col, col, 8) + col += 1 + worksheet.write(0, col, "# stories 30d", bold) + worksheet.write_comment( + 0, + col, + "It's important to ignore feeds that haven't published anything in the last 30 days, which is why this is part of the Reach Score.", + ) + worksheet.set_column(col, col, 10) + col += 1 + worksheet.write(0, col, "# shared", bold) + worksheet.write_comment( + 0, + col, + "Number of stories from this feed that were shared on NewsBlur. This is a strong signal of interest although it is not included in the Reach Score.", + ) + worksheet.set_column(col, col, 7) + col += 1 + worksheet.write(0, col, "# feed pos", bold) + worksheet.write_comment( + 0, + col, + "Number of times this feed was trained with a thumbs up. Users use training to hide stories they don't want to see while highlighting those that they do.", + ) + worksheet.set_column(col, col, 8) + col += 1 + worksheet.write(0, col, "# feed neg", bold) + worksheet.write_comment( + 0, + col, + "Number of times this feed was trained with a thumbs down. Users use training to hide stories they don't want to see while highlighting those that they do.", + ) + worksheet.set_column(col, col, 8) + col += 1 + worksheet.write(0, col, "Author", bold) + worksheet.set_column(col, col, 15) + col += 1 + worksheet.write(0, col, "# author pos", bold) + worksheet.write_comment( + 0, + col, + "Number of times this author was trained with a thumbs up. Users use training to hide stories they don't want to see while highlighting those that they do.", + ) + worksheet.set_column(col, col, 10) + col += 1 + worksheet.write(0, col, "# author neg", bold) + worksheet.write_comment( + 0, + col, + "Number of times this author was trained with a thumbs down. Users use training to hide stories they don't want to see while highlighting those that they do.", + ) + worksheet.set_column(col, col, 10) + col += 1 + worksheet.write(0, col, "Story title", bold) + worksheet.set_column(col, col, 30) + col += 1 + worksheet.write(0, col, "Story URL", bold) + worksheet.set_column(col, col, 20) + col += 1 + worksheet.write(0, col, "Story date", bold) + worksheet.set_column(col, col, 10) + col += 1 + worksheet.write(0, col, "Tag", bold) + worksheet.set_column(col, col, 15) + col += 1 + worksheet.write(0, col, "Tag count", bold) + worksheet.write_comment( + 0, + col, + "Number of times this tag is used in other stories that also contain the search query.", + ) + worksheet.set_column(col, col, 8) + col += 1 + worksheet.write(0, col, "# tag pos", bold) + worksheet.write_comment( + 0, + col, + "Number of times this tag was trained with a thumbs up. Users use training to hide stories they don't want to see while highlighting those that they do.", + ) + worksheet.set_column(col, col, 7) + col += 1 + worksheet.write(0, col, "# tag neg", bold) + worksheet.write_comment( + 0, + col, + "Number of times this tag was trained with a thumbs down. Users use training to hide stories they don't want to see while highlighting those that they do.", + ) + worksheet.set_column(col, col, 7) + col += 1 popularity = cls.query_popularity(query, limit=limit) - + for feed in popularity: col = 0 - worksheet.write(row, col, feed['feed_title']); col += 1 - worksheet.write_url(row, col, feed.get('feed_url') or ""); col += 1 - worksheet.conditional_format(row, col, row, col+8, {'type': 'cell', - 'criteria': '==', - 'value': 0, - 'format': unread_format}) - worksheet.write(row, col, "=%s*%s*%s" % ( - xl_rowcol_to_cell(row, col+2), - xl_rowcol_to_cell(row, col+3), - xl_rowcol_to_cell(row, col+4), - )); col += 1 - worksheet.write(row, col, feed['num_subscribers']); col += 1 - worksheet.write(row, col, feed['reader_count']); col += 1 - worksheet.write(row, col, feed['read_pct']); col += 1 - worksheet.write(row, col, feed['story_count']); col += 1 - worksheet.write(row, col, feed['share_count']); col += 1 - worksheet.write(row, col, feed['ps']); col += 1 - worksheet.write(row, col, feed['ng']); col += 1 - for author in feed['authors']: + worksheet.write(row, col, feed["feed_title"]) + col += 1 + worksheet.write_url(row, col, feed.get("feed_url") or "") + col += 1 + worksheet.conditional_format( + row, + col, + row, + col + 8, + {"type": "cell", "criteria": "==", "value": 0, "format": unread_format}, + ) + worksheet.write( + row, + col, + "=%s*%s*%s" + % ( + xl_rowcol_to_cell(row, col + 2), + xl_rowcol_to_cell(row, col + 3), + xl_rowcol_to_cell(row, col + 4), + ), + ) + col += 1 + worksheet.write(row, col, feed["num_subscribers"]) + col += 1 + worksheet.write(row, col, feed["reader_count"]) + col += 1 + worksheet.write(row, col, feed["read_pct"]) + col += 1 + worksheet.write(row, col, feed["story_count"]) + col += 1 + worksheet.write(row, col, feed["share_count"]) + col += 1 + worksheet.write(row, col, feed["ps"]) + col += 1 + worksheet.write(row, col, feed["ng"]) + col += 1 + for author in feed["authors"]: row += 1 - worksheet.conditional_format(row, col, row, col+2, {'type': 'cell', - 'criteria': '==', - 'value': 0, - 'format': unread_format}) - worksheet.write(row, col, author['name']) - worksheet.write(row, col+1, author['ps']) - worksheet.write(row, col+2, author['ng']) - for story in author['stories']: - worksheet.write(row, col+3, story['title']) - worksheet.write_url(row, col+4, story['url']) - worksheet.write_datetime(row, col+5, story['date'], date_format) + worksheet.conditional_format( + row, + col, + row, + col + 2, + {"type": "cell", "criteria": "==", "value": 0, "format": unread_format}, + ) + worksheet.write(row, col, author["name"]) + worksheet.write(row, col + 1, author["ps"]) + worksheet.write(row, col + 2, author["ng"]) + for story in author["stories"]: + worksheet.write(row, col + 3, story["title"]) + worksheet.write_url(row, col + 4, story["url"]) + worksheet.write_datetime(row, col + 5, story["date"], date_format) row += 1 - for tag in list(author['tags'].values()): - worksheet.conditional_format(row, col+7, row, col+9, {'type': 'cell', - 'criteria': '==', - 'value': 0, - 'format': unread_format}) - worksheet.write(row, col+6, tag['name']) - worksheet.write(row, col+7, tag['count']) - worksheet.write(row, col+8, tag['ps']) - worksheet.write(row, col+9, tag['ng']) + for tag in list(author["tags"].values()): + worksheet.conditional_format( + row, + col + 7, + row, + col + 9, + {"type": "cell", "criteria": "==", "value": 0, "format": unread_format}, + ) + worksheet.write(row, col + 6, tag["name"]) + worksheet.write(row, col + 7, tag["count"]) + worksheet.write(row, col + 8, tag["ps"]) + worksheet.write(row, col + 9, tag["ng"]) row += 1 workbook.close() return title - + def find_stories(self, query, order="newest", offset=0, limit=25): - story_ids = SearchStory.query(feed_ids=[self.pk], query=query, order=order, - offset=offset, limit=limit) - stories_db = MStory.objects( - story_hash__in=story_ids - ).order_by('-story_date' if order == "newest" else 'story_date') + story_ids = SearchStory.query( + feed_ids=[self.pk], query=query, order=order, offset=offset, limit=limit + ) + stories_db = MStory.objects(story_hash__in=story_ids).order_by( + "-story_date" if order == "newest" else "story_date" + ) stories = self.format_stories(stories_db, self.pk) - + return stories - + @classmethod def format_stories(cls, stories_db, feed_id=None, include_permalinks=False): stories = [] @@ -2009,33 +2243,34 @@ def format_stories(cls, stories_db, feed_id=None, include_permalinks=False): for story_db in stories_db: story = cls.format_story(story_db, feed_id, include_permalinks=include_permalinks) stories.append(story) - + return stories - + @classmethod - def format_story(cls, story_db, feed_id=None, text=False, include_permalinks=False, - show_changes=False): + def format_story(cls, story_db, feed_id=None, text=False, include_permalinks=False, show_changes=False): if isinstance(story_db.story_content_z, str): story_db.story_content_z = base64.b64decode(story_db.story_content_z) - - story_content = '' + + story_content = "" latest_story_content = None has_changes = False - if (not show_changes and - hasattr(story_db, 'story_latest_content_z') and - story_db.story_latest_content_z): + if ( + not show_changes + and hasattr(story_db, "story_latest_content_z") + and story_db.story_latest_content_z + ): try: latest_story_content = smart_str(zlib.decompress(story_db.story_latest_content_z)) except DjangoUnicodeDecodeError: latest_story_content = zlib.decompress(story_db.story_latest_content_z) if story_db.story_content_z: story_content = smart_str(zlib.decompress(story_db.story_content_z)) - - if ' 80: - story_title = story_title[:80] + '...' - - story = {} - story['story_hash'] = getattr(story_db, 'story_hash', None) - story['story_tags'] = story_db.story_tags or [] - story['story_date'] = story_db.story_date.replace(tzinfo=None) - story['story_timestamp'] = story_db.story_date.strftime('%s') - story['story_authors'] = story_db.story_author_name or "" - story['story_title'] = story_title + story_title = story_title[:80] + "..." + + story = {} + story["story_hash"] = getattr(story_db, "story_hash", None) + story["story_tags"] = story_db.story_tags or [] + story["story_date"] = story_db.story_date.replace(tzinfo=None) + story["story_timestamp"] = story_db.story_date.strftime("%s") + story["story_authors"] = story_db.story_author_name or "" + story["story_title"] = story_title if blank_story_title: - story['story_title_blank'] = True - story['story_content'] = story_content - story['story_permalink'] = story_db.story_permalink - story['image_urls'] = story_db.image_urls - story['secure_image_urls']= cls.secure_image_urls(story_db.image_urls) - story['secure_image_thumbnails']= cls.secure_image_thumbnails(story_db.image_urls) - story['story_feed_id'] = feed_id or story_db.story_feed_id - story['has_modifications']= has_changes - story['comment_count'] = story_db.comment_count if hasattr(story_db, 'comment_count') else 0 - story['comment_user_ids'] = story_db.comment_user_ids if hasattr(story_db, 'comment_user_ids') else [] - story['share_count'] = story_db.share_count if hasattr(story_db, 'share_count') else 0 - story['share_user_ids'] = story_db.share_user_ids if hasattr(story_db, 'share_user_ids') else [] - story['guid_hash'] = story_db.guid_hash if hasattr(story_db, 'guid_hash') else None - if hasattr(story_db, 'source_user_id'): - story['source_user_id'] = story_db.source_user_id - story['id'] = story_db.story_guid or story_db.story_date - if hasattr(story_db, 'starred_date'): - story['starred_date'] = story_db.starred_date - if hasattr(story_db, 'user_tags'): - story['user_tags'] = story_db.user_tags - if hasattr(story_db, 'user_notes'): - story['user_notes'] = story_db.user_notes - if hasattr(story_db, 'highlights'): - story['highlights'] = story_db.highlights - if hasattr(story_db, 'shared_date'): - story['shared_date'] = story_db.shared_date - if hasattr(story_db, 'comments'): - story['comments'] = story_db.comments - if hasattr(story_db, 'user_id'): - story['user_id'] = story_db.user_id - if include_permalinks and hasattr(story_db, 'blurblog_permalink'): - story['blurblog_permalink'] = story_db.blurblog_permalink() + story["story_title_blank"] = True + story["story_content"] = story_content + story["story_permalink"] = story_db.story_permalink + story["image_urls"] = story_db.image_urls + story["secure_image_urls"] = cls.secure_image_urls(story_db.image_urls) + story["secure_image_thumbnails"] = cls.secure_image_thumbnails(story_db.image_urls) + story["story_feed_id"] = feed_id or story_db.story_feed_id + story["has_modifications"] = has_changes + story["comment_count"] = story_db.comment_count if hasattr(story_db, "comment_count") else 0 + story["comment_user_ids"] = story_db.comment_user_ids if hasattr(story_db, "comment_user_ids") else [] + story["share_count"] = story_db.share_count if hasattr(story_db, "share_count") else 0 + story["share_user_ids"] = story_db.share_user_ids if hasattr(story_db, "share_user_ids") else [] + story["guid_hash"] = story_db.guid_hash if hasattr(story_db, "guid_hash") else None + if hasattr(story_db, "source_user_id"): + story["source_user_id"] = story_db.source_user_id + story["id"] = story_db.story_guid or story_db.story_date + if hasattr(story_db, "starred_date"): + story["starred_date"] = story_db.starred_date + if hasattr(story_db, "user_tags"): + story["user_tags"] = story_db.user_tags + if hasattr(story_db, "user_notes"): + story["user_notes"] = story_db.user_notes + if hasattr(story_db, "highlights"): + story["highlights"] = story_db.highlights + if hasattr(story_db, "shared_date"): + story["shared_date"] = story_db.shared_date + if hasattr(story_db, "comments"): + story["comments"] = story_db.comments + if hasattr(story_db, "user_id"): + story["user_id"] = story_db.user_id + if include_permalinks and hasattr(story_db, "blurblog_permalink"): + story["blurblog_permalink"] = story_db.blurblog_permalink() if text: - soup = BeautifulSoup(story['story_content'], features="lxml") - text = ''.join(soup.findAll(text=True)) - text = re.sub(r'\n+', '\n\n', text) - text = re.sub(r'\t+', '\t', text) - story['text'] = text - + soup = BeautifulSoup(story["story_content"], features="lxml") + text = "".join(soup.findAll(text=True)) + text = re.sub(r"\n+", "\n\n", text) + text = re.sub(r"\t+", "\t", text) + story["text"] = text + return story - + @classmethod def secure_image_urls(cls, urls): - signed_urls = [create_imageproxy_signed_url(settings.IMAGES_URL, - settings.IMAGES_SECRET_KEY, - url) for url in urls] + signed_urls = [ + create_imageproxy_signed_url(settings.IMAGES_URL, settings.IMAGES_SECRET_KEY, url) for url in urls + ] return dict(zip(urls, signed_urls)) - + @classmethod def secure_image_thumbnails(cls, urls, size=192): - signed_urls = [create_imageproxy_signed_url(settings.IMAGES_URL, - settings.IMAGES_SECRET_KEY, - url, - size) for url in urls] + signed_urls = [ + create_imageproxy_signed_url(settings.IMAGES_URL, settings.IMAGES_SECRET_KEY, url, size) + for url in urls + ] return dict(zip(urls, signed_urls)) - + def get_tags(self, entry): fcat = [] - if 'tags' in entry: + if "tags" in entry: for tcat in entry.tags: term = None - if hasattr(tcat, 'label') and tcat.label: + if hasattr(tcat, "label") and tcat.label: term = tcat.label - elif hasattr(tcat, 'term') and tcat.term: + elif hasattr(tcat, "term") and tcat.term: term = tcat.term if not term or "CDATA" in term: continue qcat = term.strip() - if ',' in qcat or '/' in qcat: - qcat = qcat.replace(',', '/').split('/') + if "," in qcat or "/" in qcat: + qcat = qcat.replace(",", "/").split("/") else: qcat = [qcat] for zcat in qcat: tagname = zcat.lower() - while ' ' in tagname: - tagname = tagname.replace(' ', ' ') + while " " in tagname: + tagname = tagname.replace(" ", " ") tagname = tagname.strip() - if not tagname or tagname == ' ': + if not tagname or tagname == " ": continue fcat.append(tagname) fcat = [strip_tags(t)[:250] for t in fcat[:12]] return fcat - + @classmethod def get_permalink(cls, entry): - link = entry.get('link') + link = entry.get("link") if not link: - links = entry.get('links') + links = entry.get("links") if links: - link = links[0].get('href') + link = links[0].get("href") if not link: - link = entry.get('id') + link = entry.get("id") return link - + def _exists_story(self, story, story_content, existing_stories, new_story_hashes, lightweight=False): story_in_system = None story_has_changed = False story_link = self.get_permalink(story) existing_stories_hashes = list(existing_stories.keys()) - story_pub_date = story.get('published') + story_pub_date = story.get("published") # story_published_now = story.get('published_now', False) # start_date = story_pub_date - datetime.timedelta(hours=8) # end_date = story_pub_date + datetime.timedelta(hours=8) @@ -2166,110 +2401,146 @@ def _exists_story(self, story, story_content, existing_stories, new_story_hashes if isinstance(existing_story.id, str): # Correcting a MongoDB bug existing_story.story_guid = existing_story.id - - if story.get('story_hash') == existing_story.story_hash: + + if story.get("story_hash") == existing_story.story_hash: story_in_system = existing_story - elif (story.get('story_hash') in existing_stories_hashes and - story.get('story_hash') != existing_story.story_hash): + elif ( + story.get("story_hash") in existing_stories_hashes + and story.get("story_hash") != existing_story.story_hash + ): # Story already exists but is not this one continue - elif (existing_story.story_hash in new_story_hashes and - story.get('story_hash') != existing_story.story_hash): - # Story coming up later + elif ( + existing_story.story_hash in new_story_hashes + and story.get("story_hash") != existing_story.story_hash + ): + # Story coming up later continue - if 'story_latest_content_z' in existing_story: + if "story_latest_content_z" in existing_story: existing_story_content = smart_str(zlib.decompress(existing_story.story_latest_content_z)) - elif 'story_latest_content' in existing_story: + elif "story_latest_content" in existing_story: existing_story_content = existing_story.story_latest_content - elif 'story_content_z' in existing_story: + elif "story_content_z" in existing_story: existing_story_content = smart_str(zlib.decompress(existing_story.story_content_z)) - elif 'story_content' in existing_story: + elif "story_content" in existing_story: existing_story_content = existing_story.story_content else: - existing_story_content = '' - - + existing_story_content = "" + # Title distance + content distance, checking if story changed - story_title_difference = abs(levenshtein_distance(story.get('title'), - existing_story.story_title)) - - title_ratio = difflib.SequenceMatcher(None, story.get('title', ""), - existing_story.story_title).ratio() - if title_ratio < .75: continue - + story_title_difference = abs(levenshtein_distance(story.get("title"), existing_story.story_title)) + + title_ratio = difflib.SequenceMatcher( + None, story.get("title", ""), existing_story.story_title + ).ratio() + if title_ratio < 0.75: + continue + story_timedelta = existing_story.story_date - story_pub_date # logging.debug('Story pub date: %s %s (%s, %s)' % (existing_story.story_date, story_pub_date, title_ratio, story_timedelta)) - if abs(story_timedelta.days) >= 2: continue - + if abs(story_timedelta.days) >= 2: + continue + seq = difflib.SequenceMatcher(None, story_content, existing_story_content) - + similiar_length_min = 1000 - if (existing_story.story_permalink == story_link and - existing_story.story_title == story.get('title')): + if existing_story.story_permalink == story_link and existing_story.story_title == story.get( + "title" + ): similiar_length_min = 20 - + # Skip content check if already failed due to a timeout. This way we catch titles - if lightweight: continue + if lightweight: + continue - if (seq + if ( + seq and story_content and len(story_content) > similiar_length_min and existing_story_content - and seq.real_quick_ratio() > .9 - and seq.quick_ratio() > .95): + and seq.real_quick_ratio() > 0.9 + and seq.quick_ratio() > 0.95 + ): content_ratio = seq.ratio() - if story_title_difference > 0 and content_ratio > .98: + if story_title_difference > 0 and content_ratio > 0.98: story_in_system = existing_story if story_title_difference > 0 or content_ratio < 1.0: if settings.DEBUG: - logging.debug(" ---> Title difference - %s/%s (%s): %s" % (story.get('title'), existing_story.story_title, story_title_difference, content_ratio)) + logging.debug( + " ---> Title difference - %s/%s (%s): %s" + % ( + story.get("title"), + existing_story.story_title, + story_title_difference, + content_ratio, + ) + ) story_has_changed = True break - + # More restrictive content distance, still no story match - if not story_in_system and content_ratio > .98: + if not story_in_system and content_ratio > 0.98: if settings.DEBUG: - logging.debug(" ---> Content difference - %s/%s (%s): %s" % (story.get('title'), existing_story.story_title, story_title_difference, content_ratio)) + logging.debug( + " ---> Content difference - %s/%s (%s): %s" + % ( + story.get("title"), + existing_story.story_title, + story_title_difference, + content_ratio, + ) + ) story_in_system = existing_story story_has_changed = True break - + if story_in_system and not story_has_changed: if story_content != existing_story_content: if settings.DEBUG: - logging.debug(" ---> Content difference - %s (%s)/%s (%s)" % (story.get('title'), len(story_content), existing_story.story_title, len(existing_story_content))) + logging.debug( + " ---> Content difference - %s (%s)/%s (%s)" + % ( + story.get("title"), + len(story_content), + existing_story.story_title, + len(existing_story_content), + ) + ) story_has_changed = True if story_link != existing_story.story_permalink: if settings.DEBUG: - logging.debug(" ---> Permalink difference - %s/%s" % (story_link, existing_story.story_permalink)) + logging.debug( + " ---> Permalink difference - %s/%s" + % (story_link, existing_story.story_permalink) + ) story_has_changed = True # if story_pub_date != existing_story.story_date: # story_has_changed = True break - - + # if story_has_changed or not story_in_system: - # print 'New/updated story: %s' % (story), + # print 'New/updated story: %s' % (story), return story_in_system, story_has_changed - + def get_next_scheduled_update(self, force=False, verbose=True, premium_speed=False, pro_speed=False): if self.min_to_decay and not force and not premium_speed: return self.min_to_decay - + from apps.notifications.models import MUserFeedNotification - + if premium_speed: self.active_premium_subscribers += 1 if pro_speed: self.pro_subscribers += 1 - - spd = self.stories_last_month / 30.0 - subs = (self.active_premium_subscribers + - ((self.active_subscribers - self.active_premium_subscribers) / 10.0)) + + spd = self.stories_last_month / 30.0 + subs = self.active_premium_subscribers + ( + (self.active_subscribers - self.active_premium_subscribers) / 10.0 + ) notification_count = MUserFeedNotification.objects.filter(feed_id=self.pk).count() - # Calculate sub counts: + # Calculate sub counts: # SELECT COUNT(*) FROM feeds WHERE active_premium_subscribers > 10 AND stories_last_month >= 30; # SELECT COUNT(*) FROM feeds WHERE active_premium_subscribers > 1 AND active_premium_subscribers < 10 AND stories_last_month >= 30; # SELECT COUNT(*) FROM feeds WHERE active_premium_subscribers = 1 AND stories_last_month >= 30; @@ -2295,7 +2566,7 @@ def get_next_scheduled_update(self, force=False, verbose=True, premium_speed=Fal if subs > 1: total = 60 - (spd * 60) else: - total = 60*6 - (spd * 60*6) + total = 60 * 6 - (spd * 60 * 6) elif spd == 0: if subs > 1: total = 60 * 6 @@ -2303,7 +2574,7 @@ def get_next_scheduled_update(self, force=False, verbose=True, premium_speed=Fal total = 60 * 12 else: total = 60 * 24 - months_since_last_story = seconds_timesince(self.last_story_date) / (60*60*24*30) + months_since_last_story = seconds_timesince(self.last_story_date) / (60 * 60 * 24 * 30) total *= max(1, months_since_last_story) # updates_per_day_delay = 3 * 60 / max(.25, ((max(0, self.active_subscribers)**.2) # * (self.stories_last_month**0.25))) @@ -2324,27 +2595,27 @@ def get_next_scheduled_update(self, force=False, verbose=True, premium_speed=Fal if self.is_push: fetch_history = MFetchHistory.feed(self.pk) - if len(fetch_history['push_history']): + if len(fetch_history["push_history"]): total = total * 12 - + # Any notifications means a 30 min minumum if notification_count > 0: total = min(total, 30) # 4 hour max for premiums, 48 hour max for free if subs >= 1: - total = min(total, 60*4*1) + total = min(total, 60 * 4 * 1) else: - total = min(total, 60*24*2) + total = min(total, 60 * 24 * 2) # Craigslist feeds get 6 hours minimum - if 'craigslist' in self.feed_address: - total = max(total, 60*6) + if "craigslist" in self.feed_address: + total = max(total, 60 * 6) # Twitter feeds get 2 hours minimum - if 'twitter' in self.feed_address: - total = max(total, 60*2) - + if "twitter" in self.feed_address: + total = max(total, 60 * 2) + # Pro subscribers get absolute minimum if self.pro_subscribers and self.pro_subscribers >= 1: if self.stories_last_month == 0: @@ -2353,72 +2624,80 @@ def get_next_scheduled_update(self, force=False, verbose=True, premium_speed=Fal total = min(total, settings.PRO_MINUTES_BETWEEN_FETCHES) if verbose: - logging.debug(" ---> [%-30s] Fetched every %s min - Subs: %s/%s/%s/%s/%s Stories/day: %s" % ( - self.log_title[:30], total, - self.num_subscribers, - self.active_subscribers, - self.active_premium_subscribers, - self.archive_subscribers, - self.pro_subscribers, - spd)) + logging.debug( + " ---> [%-30s] Fetched every %s min - Subs: %s/%s/%s/%s/%s Stories/day: %s" + % ( + self.log_title[:30], + total, + self.num_subscribers, + self.active_subscribers, + self.active_premium_subscribers, + self.archive_subscribers, + self.pro_subscribers, + spd, + ) + ) return total - + def set_next_scheduled_update(self, verbose=False, skip_scheduling=False): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) total = self.get_next_scheduled_update(force=True, verbose=verbose) error_count = self.error_count - + if error_count: total = total * error_count - total = min(total, 60*24*7) + total = min(total, 60 * 24 * 7) if verbose: - logging.debug(' ---> [%-30s] ~FBScheduling feed fetch geometrically: ' - '~SB%s errors. Time: %s min' % ( - self.log_title[:30], self.errors_since_good, total)) - + logging.debug( + " ---> [%-30s] ~FBScheduling feed fetch geometrically: " + "~SB%s errors. Time: %s min" % (self.log_title[:30], self.errors_since_good, total) + ) + random_factor = random.randint(0, int(total)) / 4 - next_scheduled_update = datetime.datetime.utcnow() + datetime.timedelta( - minutes = total + random_factor) + next_scheduled_update = datetime.datetime.utcnow() + datetime.timedelta(minutes=total + random_factor) original_min_to_decay = self.min_to_decay self.min_to_decay = total - + delta = self.next_scheduled_update - datetime.datetime.now() minutes_to_next_fetch = (delta.seconds + (delta.days * 24 * 3600)) / 60 if minutes_to_next_fetch > self.min_to_decay or not skip_scheduling: self.next_scheduled_update = next_scheduled_update if self.active_subscribers >= 1: - r.zadd('scheduled_updates', { self.pk: self.next_scheduled_update.strftime('%s') }) - r.zrem('tasked_feeds', self.pk) - r.srem('queued_feeds', self.pk) - - updated_fields = ['last_update', 'next_scheduled_update'] + r.zadd("scheduled_updates", {self.pk: self.next_scheduled_update.strftime("%s")}) + r.zrem("tasked_feeds", self.pk) + r.srem("queued_feeds", self.pk) + + updated_fields = ["last_update", "next_scheduled_update"] if self.min_to_decay != original_min_to_decay: - updated_fields.append('min_to_decay') + updated_fields.append("min_to_decay") self.save(update_fields=updated_fields) - + @property def error_count(self): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - fetch_errors = int(r.zscore('error_feeds', self.pk) or 0) - + fetch_errors = int(r.zscore("error_feeds", self.pk) or 0) + return fetch_errors + self.errors_since_good - + def schedule_feed_fetch_immediately(self, verbose=True): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) if not self.num_subscribers: - logging.debug(' ---> [%-30s] Not scheduling feed fetch immediately, no subs.' % (self.log_title[:30])) + logging.debug( + " ---> [%-30s] Not scheduling feed fetch immediately, no subs." % (self.log_title[:30]) + ) return self - + if verbose: - logging.debug(' ---> [%-30s] Scheduling feed fetch immediately...' % (self.log_title[:30])) - + logging.debug(" ---> [%-30s] Scheduling feed fetch immediately..." % (self.log_title[:30])) + self.next_scheduled_update = datetime.datetime.utcnow() - r.zadd('scheduled_updates', { self.pk: self.next_scheduled_update.strftime('%s') }) + r.zadd("scheduled_updates", {self.pk: self.next_scheduled_update.strftime("%s")}) return self.save() - + def setup_push(self): from apps.push.models import PushSubscription + try: push = self.push except PushSubscription.DoesNotExist: @@ -2426,35 +2705,38 @@ def setup_push(self): else: self.is_push = push.verified self.save() - + def queue_pushed_feed_xml(self, xml, latest_push_date_delta=None): r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) queue_size = r.llen("push_feeds") - + if latest_push_date_delta: - latest_push_date_delta = "%s" % str(latest_push_date_delta).split('.', 2)[0] + latest_push_date_delta = "%s" % str(latest_push_date_delta).split(".", 2)[0] if queue_size > 1000: self.schedule_feed_fetch_immediately() else: - logging.debug(' ---> [%-30s] [%s] ~FB~SBQueuing pushed stories, last pushed %s...' % (self.log_title[:30], self.pk, latest_push_date_delta)) + logging.debug( + " ---> [%-30s] [%s] ~FB~SBQueuing pushed stories, last pushed %s..." + % (self.log_title[:30], self.pk, latest_push_date_delta) + ) self.set_next_scheduled_update() - PushFeeds.apply_async(args=(self.pk, xml), queue='push_feeds') - + PushFeeds.apply_async(args=(self.pk, xml), queue="push_feeds") + # def calculate_collocations_story_content(self, # collocation_measures=TrigramAssocMeasures, # collocation_finder=TrigramCollocationFinder): # stories = MStory.objects.filter(story_feed_id=self.pk) # story_content = ' '.join([s.story_content for s in stories if s.story_content]) # return self.calculate_collocations(story_content, collocation_measures, collocation_finder) - # + # # def calculate_collocations_story_title(self, # collocation_measures=BigramAssocMeasures, # collocation_finder=BigramCollocationFinder): # stories = MStory.objects.filter(story_feed_id=self.pk) # story_titles = ' '.join([s.story_title for s in stories if s.story_title]) # return self.calculate_collocations(story_titles, collocation_measures, collocation_finder) - # + # # def calculate_collocations(self, content, # collocation_measures=TrigramAssocMeasures, # collocation_finder=TrigramCollocationFinder): @@ -2467,35 +2749,37 @@ def queue_pushed_feed_xml(self, xml, latest_push_date_delta=None): # print "ValueError, ignoring: %s" % e # content = re.sub(r']*>', '', content) # content = re.split(r"[^A-Za-z-'&]+", content) - # + # # finder = collocation_finder.from_words(content) # finder.apply_freq_filter(3) # best = finder.nbest(collocation_measures.pmi, 10) # phrases = [' '.join(phrase) for phrase in best] - # + # # return phrases # class FeedCollocations(models.Model): # feed = models.ForeignKey(Feed) # phrase = models.CharField(max_length=500) - + + class FeedData(models.Model): - feed = AutoOneToOneField(Feed, related_name='data', on_delete=models.CASCADE) + feed = AutoOneToOneField(Feed, related_name="data", on_delete=models.CASCADE) feed_tagline = models.CharField(max_length=1024, blank=True, null=True) story_count_history = models.TextField(blank=True, null=True) feed_classifier_counts = models.TextField(blank=True, null=True) popular_tags = models.CharField(max_length=1024, blank=True, null=True) popular_authors = models.CharField(max_length=2048, blank=True, null=True) - + def save(self, *args, **kwargs): if self.feed_tagline and len(self.feed_tagline) >= 1000: self.feed_tagline = self.feed_tagline[:1000] - - try: + + try: super(FeedData, self).save(*args, **kwargs) except (IntegrityError, OperationError): - if hasattr(self, 'id') and self.id: self.delete() + if hasattr(self, "id") and self.id: + self.delete() except DatabaseError as e: # Nothing updated logging.debug(" ---> ~FRNothing updated in FeedData (%s): %s" % (self.feed, e)) @@ -2503,49 +2787,49 @@ def save(self, *args, **kwargs): class MFeedIcon(mongo.Document): - feed_id = mongo.IntField(primary_key=True) - color = mongo.StringField(max_length=6) - data = mongo.StringField() - icon_url = mongo.StringField() - not_found = mongo.BooleanField(default=False) - + feed_id = mongo.IntField(primary_key=True) + color = mongo.StringField(max_length=6) + data = mongo.StringField() + icon_url = mongo.StringField() + not_found = mongo.BooleanField(default=False) + meta = { - 'collection' : 'feed_icons', - 'allow_inheritance' : False, + "collection": "feed_icons", + "allow_inheritance": False, } - + @classmethod def get_feed(cls, feed_id, create=True): try: - feed_icon = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY)\ - .get(feed_id=feed_id) + feed_icon = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY).get(feed_id=feed_id) except cls.DoesNotExist: if create: feed_icon = cls.objects.create(feed_id=feed_id) else: feed_icon = None - + return feed_icon - + def save(self, *args, **kwargs): if self.icon_url: self.icon_url = str(self.icon_url) - try: + try: return super(MFeedIcon, self).save(*args, **kwargs) except (IntegrityError, OperationError): # print "Error on Icon: %s" % e - if hasattr(self, '_id'): self.delete() + if hasattr(self, "_id"): + self.delete() class MFeedPage(mongo.Document): feed_id = mongo.IntField(primary_key=True) page_data = mongo.BinaryField() - + meta = { - 'collection': 'feed_pages', - 'allow_inheritance': False, + "collection": "feed_pages", + "allow_inheritance": False, } - + def page(self): try: return zlib.decompress(self.page_data) @@ -2553,8 +2837,8 @@ def page(self): logging.debug(" ***> Zlib decompress error: %s" % e) self.page_data = None self.save() - return - + return + @classmethod def get_data(cls, feed_id): data = None @@ -2568,8 +2852,8 @@ def get_data(cls, feed_id): logging.debug(" ***> Zlib decompress error: %s" % e) self.page_data = None self.save() - return - + return + if not data: dupe_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id) if dupe_feed: @@ -2582,66 +2866,71 @@ def get_data(cls, feed_id): return data + class MStory(mongo.Document): - '''A feed item''' - story_feed_id = mongo.IntField() - story_date = mongo.DateTimeField() - story_title = mongo.StringField(max_length=1024) - story_content = mongo.StringField() - story_content_z = mongo.BinaryField() - story_original_content = mongo.StringField() + """A feed item""" + + story_feed_id = mongo.IntField() + story_date = mongo.DateTimeField() + story_title = mongo.StringField(max_length=1024) + story_content = mongo.StringField() + story_content_z = mongo.BinaryField() + story_original_content = mongo.StringField() story_original_content_z = mongo.BinaryField() - story_latest_content = mongo.StringField() - story_latest_content_z = mongo.BinaryField() - original_text_z = mongo.BinaryField() - original_page_z = mongo.BinaryField() - story_content_type = mongo.StringField(max_length=255) - story_author_name = mongo.StringField() - story_permalink = mongo.StringField() - story_guid = mongo.StringField() - story_hash = mongo.StringField() - image_urls = mongo.ListField(mongo.StringField(max_length=1024)) - story_tags = mongo.ListField(mongo.StringField(max_length=250)) - comment_count = mongo.IntField() - comment_user_ids = mongo.ListField(mongo.IntField()) - share_count = mongo.IntField() - share_user_ids = mongo.ListField(mongo.IntField()) + story_latest_content = mongo.StringField() + story_latest_content_z = mongo.BinaryField() + original_text_z = mongo.BinaryField() + original_page_z = mongo.BinaryField() + story_content_type = mongo.StringField(max_length=255) + story_author_name = mongo.StringField() + story_permalink = mongo.StringField() + story_guid = mongo.StringField() + story_hash = mongo.StringField() + image_urls = mongo.ListField(mongo.StringField(max_length=1024)) + story_tags = mongo.ListField(mongo.StringField(max_length=250)) + comment_count = mongo.IntField() + comment_user_ids = mongo.ListField(mongo.IntField()) + share_count = mongo.IntField() + share_user_ids = mongo.ListField(mongo.IntField()) meta = { - 'collection': 'stories', - 'indexes': [('story_feed_id', '-story_date'), - {'fields': ['story_hash'], - 'unique': True, - }], - 'ordering': ['-story_date'], - 'allow_inheritance': False, - 'cascade': False, - 'strict': False, + "collection": "stories", + "indexes": [ + ("story_feed_id", "-story_date"), + { + "fields": ["story_hash"], + "unique": True, + }, + ], + "ordering": ["-story_date"], + "allow_inheritance": False, + "cascade": False, + "strict": False, } - + RE_STORY_HASH = re.compile(r"^(\d{1,10}):(\w{6})$") RE_RS_KEY = re.compile(r"^RS:(\d+):(\d+)$") def __str__(self): content = self.story_content_z if self.story_content_z else "" return f"{self.story_hash}: {self.story_title[:20]} ({len(self.story_content_z) if self.story_content_z else 0} bytes)" - + @property def guid_hash(self): - return hashlib.sha1((self.story_guid).encode(encoding='utf-8')).hexdigest()[:6] + return hashlib.sha1((self.story_guid).encode(encoding="utf-8")).hexdigest()[:6] @classmethod def guid_hash_unsaved(self, guid): - return hashlib.sha1(guid.encode(encoding='utf-8')).hexdigest()[:6] + return hashlib.sha1(guid.encode(encoding="utf-8")).hexdigest()[:6] @property def feed_guid_hash(self): return "%s:%s" % (self.story_feed_id, self.guid_hash) - + @classmethod def feed_guid_hash_unsaved(cls, feed_id, guid): return "%s:%s" % (feed_id, cls.guid_hash_unsaved(guid)) - + @property def decoded_story_title(self): return html.unescape(self.story_title) @@ -2653,17 +2942,16 @@ def story_content_str(self): story_content = smart_str(zlib.decompress(self.story_content_z)) else: story_content = smart_str(story_content) - + return story_content - def save(self, *args, **kwargs): - story_title_max = MStory._fields['story_title'].max_length - story_content_type_max = MStory._fields['story_content_type'].max_length + story_title_max = MStory._fields["story_title"].max_length + story_content_type_max = MStory._fields["story_content_type"].max_length self.story_hash = self.feed_guid_hash - + self.extract_image_urls() - + if self.story_content: self.story_content_z = zlib.compress(smart_bytes(self.story_content)) self.story_content = None @@ -2677,48 +2965,52 @@ def save(self, *args, **kwargs): self.story_title = self.story_title[:story_title_max] if self.story_content_type and len(self.story_content_type) > story_content_type_max: self.story_content_type = self.story_content_type[:story_content_type_max] - + super(MStory, self).save(*args, **kwargs) - + self.sync_redis() - + return self - + def delete(self, *args, **kwargs): self.remove_from_redis() self.remove_from_search_index() - + super(MStory, self).delete(*args, **kwargs) - + def publish_to_subscribers(self): try: r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish("%s:story" % (self.story_feed_id), '%s,%s' % (self.story_hash, self.story_date.strftime('%s'))) + r.publish( + "%s:story" % (self.story_feed_id), "%s,%s" % (self.story_hash, self.story_date.strftime("%s")) + ) except redis.ConnectionError: - logging.debug(" ***> [%-30s] ~BMRedis is unavailable for real-time." % (Feed.get_by_id(self.story_feed_id).title[:30],)) - + logging.debug( + " ***> [%-30s] ~BMRedis is unavailable for real-time." + % (Feed.get_by_id(self.story_feed_id).title[:30],) + ) + @classmethod def purge_feed_stories(cls, feed, cutoff, verbose=True): stories = cls.objects(story_feed_id=feed.pk) logging.debug(" ---> Deleting %s stories from %s" % (stories.count(), feed)) - if stories.count() > cutoff*1.25: + if stories.count() > cutoff * 1.25: logging.debug(" ***> ~FRToo many stories in %s, not purging..." % (feed)) return stories.delete() - + @classmethod def index_all_for_search(cls, offset=0): if not offset: SearchStory.create_elasticsearch_mapping(delete=True) - - last_pk = Feed.objects.latest('pk').pk + + last_pk = Feed.objects.latest("pk").pk for f in range(offset, last_pk, 1000): - print(" ---> %s / %s (%.2s%%)" % (f, last_pk, float(f)/last_pk*100)) - feeds = Feed.objects.filter(pk__in=list(range(f, f+1000)), - active=True, - active_subscribers__gte=1)\ - .values_list('pk') - for f, in feeds: + print(" ---> %s / %s (%.2s%%)" % (f, last_pk, float(f) / last_pk * 100)) + feeds = Feed.objects.filter( + pk__in=list(range(f, f + 1000)), active=True, active_subscribers__gte=1 + ).values_list("pk") + for (f,) in feeds: stories = cls.objects.filter(story_feed_id=f) if not len(stories): continue @@ -2730,14 +3022,16 @@ def index_story_for_search(self): story_content = self.story_content or "" if self.story_content_z: story_content = zlib.decompress(self.story_content_z) - SearchStory.index(story_hash=self.story_hash, - story_title=self.story_title, - story_content=prep_for_search(story_content), - story_tags=self.story_tags, - story_author=self.story_author_name, - story_feed_id=self.story_feed_id, - story_date=self.story_date) - + SearchStory.index( + story_hash=self.story_hash, + story_title=self.story_title, + story_content=prep_for_search(story_content), + story_tags=self.story_tags, + story_author=self.story_author_name, + story_feed_id=self.story_feed_id, + story_date=self.story_date, + ) + def remove_from_search_index(self): try: SearchStory.remove(self.story_hash) @@ -2750,50 +3044,50 @@ def trim_feed(cls, cutoff, feed_id=None, feed=None, verbose=True): cutoff = int(cutoff) if not feed_id and not feed: return extra_stories_count - + if not feed_id: feed_id = feed.pk if not feed: feed = feed_id - - stories = cls.objects( - story_feed_id=feed_id - ).only('story_date').order_by('-story_date') - + + stories = cls.objects(story_feed_id=feed_id).only("story_date").order_by("-story_date") + if stories.count() > cutoff: - logging.debug(' ---> [%-30s] ~FMFound %s stories. Trimming to ~SB%s~SN...' % - (str(feed)[:30], stories.count(), cutoff)) + logging.debug( + " ---> [%-30s] ~FMFound %s stories. Trimming to ~SB%s~SN..." + % (str(feed)[:30], stories.count(), cutoff) + ) try: story_trim_date = stories[cutoff].story_date if story_trim_date == stories[0].story_date: # Handle case where every story is the same time story_trim_date = story_trim_date - datetime.timedelta(seconds=1) except IndexError as e: - logging.debug(' ***> [%-30s] ~BRError trimming feed: %s' % (str(feed)[:30], e)) + logging.debug(" ***> [%-30s] ~BRError trimming feed: %s" % (str(feed)[:30], e)) return extra_stories_count - - extra_stories = cls.objects(story_feed_id=feed_id, - story_date__lte=story_trim_date) + + extra_stories = cls.objects(story_feed_id=feed_id, story_date__lte=story_trim_date) extra_stories_count = extra_stories.count() shared_story_count = 0 for story in extra_stories: - if story.share_count: + if story.share_count: shared_story_count += 1 extra_stories_count -= 1 continue story.delete() if verbose: existing_story_count = cls.objects(story_feed_id=feed_id).count() - logging.debug(" ---> Deleted %s stories, %s (%s shared) left." % ( - extra_stories_count, - existing_story_count, - shared_story_count)) + logging.debug( + " ---> Deleted %s stories, %s (%s shared) left." + % (extra_stories_count, existing_story_count, shared_story_count) + ) return extra_stories_count - + @classmethod def find_story(cls, story_feed_id=None, story_id=None, story_hash=None, original_only=False): from apps.social.models import MSharedStory + original_found = False if story_hash: story_id = story_hash @@ -2804,61 +3098,73 @@ def find_story(cls, story_feed_id=None, story_id=None, story_hash=None, original story = cls.objects(id=story_id).limit(1).first() else: story = cls.objects(story_hash=story_hash).limit(1).first() - + if story: original_found = True if not story and not original_only: - story = MSharedStory.objects.filter(story_feed_id=story_feed_id, - story_hash=story_hash).limit(1).first() + story = ( + MSharedStory.objects.filter(story_feed_id=story_feed_id, story_hash=story_hash) + .limit(1) + .first() + ) if not story and not original_only: - story = MStarredStory.objects.filter(story_feed_id=story_feed_id, - story_hash=story_hash).limit(1).first() - + story = ( + MStarredStory.objects.filter(story_feed_id=story_feed_id, story_hash=story_hash) + .limit(1) + .first() + ) + return story, original_found - + @classmethod def find_by_id(cls, story_ids): from apps.social.models import MSharedStory + count = len(story_ids) multiple = isinstance(story_ids, list) or isinstance(story_ids, tuple) - + stories = list(cls.objects(id__in=story_ids)) if len(stories) < count: shared_stories = list(MSharedStory.objects(id__in=story_ids)) stories.extend(shared_stories) - + if not multiple: stories = stories[0] - + return stories - + @classmethod def find_by_story_hashes(cls, story_hashes): from apps.social.models import MSharedStory + count = len(story_hashes) multiple = isinstance(story_hashes, list) or isinstance(story_hashes, tuple) - + stories = list(cls.objects(story_hash__in=story_hashes)) if len(stories) < count: hashes_found = [s.story_hash for s in stories] remaining_hashes = list(set(story_hashes) - set(hashes_found)) - story_feed_ids = [h.split(':')[0] for h in remaining_hashes] - shared_stories = list(MSharedStory.objects(story_feed_id__in=story_feed_ids, - story_hash__in=remaining_hashes)) + story_feed_ids = [h.split(":")[0] for h in remaining_hashes] + shared_stories = list( + MSharedStory.objects(story_feed_id__in=story_feed_ids, story_hash__in=remaining_hashes) + ) stories.extend(shared_stories) - + if not multiple: stories = stories[0] - + return stories - + @classmethod def ensure_story_hash(cls, story_id, story_feed_id): if not cls.RE_STORY_HASH.match(story_id): - story_id = "%s:%s" % (story_feed_id, hashlib.sha1(story_id.encode(encoding='utf-8')).hexdigest()[:6]) - + story_id = "%s:%s" % ( + story_feed_id, + hashlib.sha1(story_id.encode(encoding="utf-8")).hexdigest()[:6], + ) + return story_id - + @classmethod def split_story_hash(cls, story_hash): matches = cls.RE_STORY_HASH.match(story_hash) @@ -2866,7 +3172,7 @@ def split_story_hash(cls, story_hash): groups = matches.groups() return groups[0], groups[1] return None, None - + @classmethod def split_rs_key(cls, rs_key): matches = cls.RE_RS_KEY.match(rs_key) @@ -2874,72 +3180,83 @@ def split_rs_key(cls, rs_key): groups = matches.groups() return groups[0], groups[1] return None, None - + @classmethod def story_hashes(cls, story_ids): story_hashes = [] for story_id in story_ids: story_hash = cls.ensure_story_hash(story_id) - if not story_hash: continue + if not story_hash: + continue story_hashes.append(story_hash) - + return story_hashes - + def sync_redis(self, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) feed = Feed.get_by_id(self.story_feed_id) if self.id and self.story_date > feed.unread_cutoff: - feed_key = 'F:%s' % self.story_feed_id + feed_key = "F:%s" % self.story_feed_id r.sadd(feed_key, self.story_hash) - r.expire(feed_key, feed.days_of_story_hashes*24*60*60) - - r.zadd('z' + feed_key, { self.story_hash: time.mktime(self.story_date.timetuple()) }) - r.expire('z' + feed_key, feed.days_of_story_hashes*24*60*60) - + r.expire(feed_key, feed.days_of_story_hashes * 24 * 60 * 60) + + r.zadd("z" + feed_key, {self.story_hash: time.mktime(self.story_date.timetuple())}) + r.expire("z" + feed_key, feed.days_of_story_hashes * 24 * 60 * 60) + def remove_from_redis(self, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) if self.id: - r.srem('F:%s' % self.story_feed_id, self.story_hash) - r.zrem('zF:%s' % self.story_feed_id, self.story_hash) + r.srem("F:%s" % self.story_feed_id, self.story_hash) + r.zrem("zF:%s" % self.story_feed_id, self.story_hash) @classmethod - def sync_feed_redis(cls, story_feed_id): + def sync_feed_redis(cls, story_feed_id, allow_skip_resync=False): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) feed = Feed.get_by_id(story_feed_id) stories = cls.objects.filter(story_feed_id=story_feed_id, story_date__gte=feed.unread_cutoff) + if allow_skip_resync and stories.count() > 1000: + logging.debug( + f" ---> [{feed.log_title[:30]}] ~FYSkipping resync of ~SB{stories.count()}~SN stories because it already had archive subscribers" + ) + return + # Don't delete redis keys because they take time to rebuild and subs can # be counted incorrectly during that time. # r.delete('F:%s' % story_feed_id) # r.delete('zF:%s' % story_feed_id) - logging.info(" ---> [%-30s] ~FMSyncing ~SB%s~SN stories to redis" % (feed and feed.log_title[:30] or story_feed_id, stories.count())) + logging.info( + " ---> [%-30s] ~FMSyncing ~SB%s~SN stories to redis" + % (feed and feed.log_title[:30] or story_feed_id, stories.count()) + ) p = r.pipeline() for story in stories: story.sync_redis(r=p) p.execute() - + def count_comments(self): from apps.social.models import MSharedStory + params = { - 'story_guid': self.story_guid, - 'story_feed_id': self.story_feed_id, + "story_guid": self.story_guid, + "story_feed_id": self.story_feed_id, } - comments = MSharedStory.objects.filter(has_comments=True, **params).only('user_id') - shares = MSharedStory.objects.filter(**params).only('user_id') + comments = MSharedStory.objects.filter(has_comments=True, **params).only("user_id") + shares = MSharedStory.objects.filter(**params).only("user_id") self.comment_count = comments.count() - self.comment_user_ids = [c['user_id'] for c in comments] + self.comment_user_ids = [c["user_id"] for c in comments] self.share_count = shares.count() - self.share_user_ids = [s['user_id'] for s in shares] + self.share_user_ids = [s["user_id"] for s in shares] self.save() - + def extract_image_urls(self, force=False, text=False): if self.image_urls and not force and not text: return self.image_urls - + story_content = None if not text: story_content = self.story_content_str @@ -2948,7 +3265,7 @@ def extract_image_urls(self, force=False, text=False): story_content = smart_str(zlib.decompress(self.original_text_z)) if not story_content: return - + try: soup = BeautifulSoup(story_content, features="lxml") except UserWarning as e: @@ -2960,27 +3277,29 @@ def extract_image_urls(self, force=False, text=False): else: return - images = soup.findAll('img') - - # Add youtube thumbnail and insert appropriately before/after images. + images = soup.findAll("img") + + # Add youtube thumbnail and insert appropriately before/after images. # Give the Youtube a bit of an edge. - video_thumbnails = soup.findAll('iframe', src=lambda x: x and any(y in x for y in ['youtube.com', 'ytimg.com'])) + video_thumbnails = soup.findAll( + "iframe", src=lambda x: x and any(y in x for y in ["youtube.com", "ytimg.com"]) + ) for video_thumbnail in video_thumbnails: - video_src = video_thumbnail.get('src') - video_id = re.search('.*?youtube.com/embed/([A-Za-z0-9\-_]+)', video_src) + video_src = video_thumbnail.get("src") + video_id = re.search(".*?youtube.com/embed/([A-Za-z0-9\-_]+)", video_src) if not video_id: - video_id = re.search('.*?youtube.com/v/([A-Za-z0-9\-_]+)', video_src) + video_id = re.search(".*?youtube.com/v/([A-Za-z0-9\-_]+)", video_src) if not video_id: - video_id = re.search('.*?ytimg.com/vi/([A-Za-z0-9\-_]+)', video_src) + video_id = re.search(".*?ytimg.com/vi/([A-Za-z0-9\-_]+)", video_src) if not video_id: - video_id = re.search('.*?youtube.com/watch\?v=([A-Za-z0-9\-_]+)', video_src) + video_id = re.search(".*?youtube.com/watch\?v=([A-Za-z0-9\-_]+)", video_src) if not video_id: logging.debug(f" ***> Couldn't find youtube url in {video_thumbnail}: {video_src}") continue video_img_url = f"https://img.youtube.com/vi/{video_id.groups()[0]}/0.jpg" - iframe_index = story_content.index('= 1024: continue - if 'feedburner.com' in image_url: + if "feedburner.com" in image_url: + continue + try: + image_url = urllib.parse.urljoin(self.story_permalink, image_url) + except ValueError: continue - image_url = urllib.parse.urljoin(self.story_permalink, image_url) image_urls.append(image_url) - + if not image_urls: if not text: return self.extract_image_urls(force=force, text=True) else: return - + if text: urls = [] for url in image_urls: - if 'http://' in url[1:] or 'https://' in url[1:]: + if "http://" in url[1:] or "https://" in url[1:]: continue urls.append(url) image_urls = urls - + ordered_image_urls = [] for image_url in list(set(image_urls)): - if 'feedburner' in image_url: + if "feedburner" in image_url: ordered_image_urls.append(image_url) else: ordered_image_urls.insert(0, image_url) image_urls = ordered_image_urls - + if len(image_urls): self.image_urls = [u for u in image_urls if u] else: return - + max_length = MStory.image_urls.field.max_length - while len(''.join(self.image_urls)) > max_length: + while len("".join(self.image_urls)) > max_length: if len(self.image_urls) <= 1: - self.image_urls[0] = self.image_urls[0][:max_length-1] + self.image_urls[0] = self.image_urls[0][: max_length - 1] break else: self.image_urls.pop() @@ -3051,23 +3373,24 @@ def extract_image_urls(self, force=False, text=False): def fetch_original_text(self, force=False, request=None, debug=False): original_text_z = self.original_text_z - + if not original_text_z or force: feed = Feed.get_by_id(self.story_feed_id) self.extract_image_urls(force=force, text=False) ti = TextImporter(self, feed=feed, request=request, debug=debug) original_doc = ti.fetch(return_document=True) - original_text = original_doc.get('content') if original_doc else None + original_text = original_doc.get("content") if original_doc else None self.extract_image_urls(force=force, text=True) self.save() else: logging.user(request, "~FYFetching ~FGoriginal~FY story text, ~SBfound.") original_text = zlib.decompress(original_text_z) - + return original_text def fetch_original_page(self, force=False, request=None, debug=False): from apps.rss_feeds.page_importer import PageImporter + if not self.original_page_z or force: feed = Feed.get_by_id(self.story_feed_id) importer = PageImporter(request=request, feed=feed, story=self) @@ -3075,42 +3398,47 @@ def fetch_original_page(self, force=False, request=None, debug=False): else: logging.user(request, "~FYFetching ~FGoriginal~FY story page, ~SBfound.") original_page = zlib.decompress(self.original_page_z) - + return original_page class MStarredStory(mongo.DynamicDocument): """Like MStory, but not inherited due to large overhead of _cls and _type in - mongoengine's inheritance model on every single row.""" - user_id = mongo.IntField(unique_with=('story_guid',)) - starred_date = mongo.DateTimeField() - starred_updated = mongo.DateTimeField() - story_feed_id = mongo.IntField() - story_date = mongo.DateTimeField() - story_title = mongo.StringField(max_length=1024) - story_content = mongo.StringField() - story_content_z = mongo.BinaryField() - story_original_content = mongo.StringField() + mongoengine's inheritance model on every single row.""" + + user_id = mongo.IntField(unique_with=("story_guid",)) + starred_date = mongo.DateTimeField() + starred_updated = mongo.DateTimeField() + story_feed_id = mongo.IntField() + story_date = mongo.DateTimeField() + story_title = mongo.StringField(max_length=1024) + story_content = mongo.StringField() + story_content_z = mongo.BinaryField() + story_original_content = mongo.StringField() story_original_content_z = mongo.BinaryField() - original_text_z = mongo.BinaryField() - story_content_type = mongo.StringField(max_length=255) - story_author_name = mongo.StringField() - story_permalink = mongo.StringField() - story_guid = mongo.StringField() - story_hash = mongo.StringField() - story_tags = mongo.ListField(mongo.StringField(max_length=250)) - user_notes = mongo.StringField() - user_tags = mongo.ListField(mongo.StringField(max_length=128)) - highlights = mongo.ListField(mongo.StringField(max_length=16384)) - image_urls = mongo.ListField(mongo.StringField(max_length=1024)) + original_text_z = mongo.BinaryField() + story_content_type = mongo.StringField(max_length=255) + story_author_name = mongo.StringField() + story_permalink = mongo.StringField() + story_guid = mongo.StringField() + story_hash = mongo.StringField() + story_tags = mongo.ListField(mongo.StringField(max_length=250)) + user_notes = mongo.StringField() + user_tags = mongo.ListField(mongo.StringField(max_length=128)) + highlights = mongo.ListField(mongo.StringField(max_length=16384)) + image_urls = mongo.ListField(mongo.StringField(max_length=1024)) meta = { - 'collection': 'starred_stories', - 'indexes': [('user_id', '-starred_date'), ('user_id', 'story_feed_id'), - ('user_id', 'story_hash'), 'story_feed_id'], - 'ordering': ['-starred_date'], - 'allow_inheritance': False, - 'strict': False, + "collection": "starred_stories", + "indexes": [ + ("user_id", "-starred_date"), + ("user_id", "story_feed_id"), + ("user_id", "story_hash"), + "story_feed_id", + ], + "ordering": ["-starred_date"], + "allow_inheritance": False, + "strict": False, } def __unicode__(self): @@ -3118,11 +3446,9 @@ def __unicode__(self): user = User.objects.get(pk=self.user_id) username = user.username except User.DoesNotExist: - username = '[deleted]' - return "%s: %s (%s)" % (username, - self.story_title[:20], - self.story_feed_id) - + username = "[deleted]" + return "%s: %s (%s)" % (username, self.story_title[:20], self.story_feed_id) + def save(self, *args, **kwargs): if self.story_content: self.story_content_z = zlib.compress(smart_bytes(self.story_content)) @@ -3134,100 +3460,106 @@ def save(self, *args, **kwargs): self.starred_updated = datetime.datetime.now() return super(MStarredStory, self).save(*args, **kwargs) - + @classmethod def find_stories(cls, query, user_id, tag=None, offset=0, limit=25, order="newest"): stories_db = cls.objects( - Q(user_id=user_id) & - (Q(story_title__icontains=query) | - Q(story_author_name__icontains=query) | - Q(story_tags__icontains=query)) + Q(user_id=user_id) + & ( + Q(story_title__icontains=query) + | Q(story_author_name__icontains=query) + | Q(story_tags__icontains=query) + ) ) if tag: stories_db = stories_db.filter(user_tags__contains=tag) - - stories_db = stories_db.order_by('%sstarred_date' % - ('-' if order == "newest" else ""))[offset:offset+limit] + + stories_db = stories_db.order_by("%sstarred_date" % ("-" if order == "newest" else ""))[ + offset : offset + limit + ] stories = Feed.format_stories(stories_db) - + return stories - + @classmethod def find_stories_by_user_tag(cls, user_tag, user_id, offset=0, limit=25): - stories_db = cls.objects( - Q(user_id=user_id), - Q(user_tags__icontains=user_tag) - ).order_by('-starred_date')[offset:offset+limit] + stories_db = cls.objects(Q(user_id=user_id), Q(user_tags__icontains=user_tag)).order_by( + "-starred_date" + )[offset : offset + limit] stories = Feed.format_stories(stories_db) - + return stories @classmethod def trim_old_stories(cls, stories=10, days=90, dryrun=False): print(" ---> Fetching starred story counts...") - stats = settings.MONGODB.newsblur.starred_stories.aggregate([{ - "$group": { - "_id": "$user_id", - "stories": {"$sum": 1}, - }, - }, { - "$match": { - "stories": {"$gte": stories} - }, - }]) + stats = settings.MONGODB.newsblur.starred_stories.aggregate( + [ + { + "$group": { + "_id": "$user_id", + "stories": {"$sum": 1}, + }, + }, + { + "$match": {"stories": {"$gte": stories}}, + }, + ] + ) month_ago = datetime.datetime.now() - datetime.timedelta(days=days) user_ids = list(stats) - user_ids = sorted(user_ids, key=lambda x:x['stories'], reverse=True) + user_ids = sorted(user_ids, key=lambda x: x["stories"], reverse=True) print(" ---> Found %s users with more than %s starred stories" % (len(user_ids), stories)) total = 0 for stat in user_ids: try: - user = User.objects.select_related('profile').get(pk=stat['_id']) + user = User.objects.select_related("profile").get(pk=stat["_id"]) except User.DoesNotExist: user = None - + if user and (user.profile.is_premium or user.profile.last_seen_on > month_ago): continue - - total += stat['stories'] - username = "%s (%s)" % (user and user.username or " - ", stat['_id']) - print(" ---> %19.19s: %-20.20s %s stories" % (user and user.profile.last_seen_on or "Deleted", - username, - stat['stories'])) - if not dryrun and stat['_id']: - cls.objects.filter(user_id=stat['_id']).delete() - elif not dryrun and stat['_id'] == 0: + + total += stat["stories"] + username = "%s (%s)" % (user and user.username or " - ", stat["_id"]) + print( + " ---> %19.19s: %-20.20s %s stories" + % (user and user.profile.last_seen_on or "Deleted", username, stat["stories"]) + ) + if not dryrun and stat["_id"]: + cls.objects.filter(user_id=stat["_id"]).delete() + elif not dryrun and stat["_id"] == 0: print(" ---> Deleting unstarred stories (user_id = 0)") - cls.objects.filter(user_id=stat['_id']).delete() - - + cls.objects.filter(user_id=stat["_id"]).delete() + print(" ---> Deleted %s stories in total." % total) @property def guid_hash(self): - return hashlib.sha1(self.story_guid.encode(encoding='utf-8')).hexdigest()[:6] + return hashlib.sha1(self.story_guid.encode(encoding="utf-8")).hexdigest()[:6] @property def feed_guid_hash(self): return "%s:%s" % (self.story_feed_id or "0", self.guid_hash) - + def fetch_original_text(self, force=False, request=None, debug=False): original_text_z = self.original_text_z feed = Feed.get_by_id(self.story_feed_id) - + if not original_text_z or force: ti = TextImporter(self, feed=feed, request=request, debug=debug) original_text = ti.fetch() else: logging.user(request, "~FYFetching ~FGoriginal~FY story text, ~SBfound.") original_text = zlib.decompress(original_text_z) - + return original_text - + def fetch_original_page(self, force=False, request=None, debug=False): return None - + + class MStarredStoryCounts(mongo.Document): user_id = mongo.IntField() tag = mongo.StringField(max_length=128) @@ -3237,12 +3569,12 @@ class MStarredStoryCounts(mongo.Document): count = mongo.IntField(default=0) meta = { - 'collection': 'starred_stories_counts', - 'indexes': ['user_id'], - 'ordering': ['tag'], - 'allow_inheritance': False, + "collection": "starred_stories_counts", + "indexes": ["user_id"], + "ordering": ["tag"], + "allow_inheritance": False, } - + def __unicode__(self): if self.tag: return "Tag: %s (%s)" % (self.tag, self.count) @@ -3250,69 +3582,74 @@ def __unicode__(self): return "Feed: %s (%s)" % (self.feed_id, self.count) elif self.is_highlights: return "Highlights: %s (%s)" % (self.is_highlights, self.count) - + return "%s/%s/%s" % (self.tag, self.feed_id, self.is_highlights) @property def rss_url(self, secret_token=None): if self.feed_id: return - + if not secret_token: - user = User.objects.select_related('profile').get(pk=self.user_id) + user = User.objects.select_related("profile").get(pk=self.user_id) secret_token = user.profile.secret_token - + slug = self.slug if self.slug else "" if not self.slug and self.tag: slug = slugify(self.tag) self.slug = slug self.save() - return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id, - secret_token, slug) - + return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id, secret_token, slug) + @classmethod def user_counts(cls, user_id, include_total=False, try_counting=True): counts = cls.objects.filter(user_id=user_id) - counts = sorted([{'tag': c.tag, - 'count': c.count, - 'is_highlights': c.is_highlights, - 'feed_address': c.rss_url, - 'active': True, - 'feed_id': c.feed_id} - for c in counts], - key=lambda x: (x.get('tag', '') or '').lower()) - + counts = sorted( + [ + { + "tag": c.tag, + "count": c.count, + "is_highlights": c.is_highlights, + "feed_address": c.rss_url, + "active": True, + "feed_id": c.feed_id, + } + for c in counts + ], + key=lambda x: (x.get("tag", "") or "").lower(), + ) + total = 0 feed_total = 0 for c in counts: - if not c['tag'] and not c['feed_id'] and not c['is_highlights']: - total = c['count'] - if c['feed_id']: - feed_total += c['count'] - + if not c["tag"] and not c["feed_id"] and not c["is_highlights"]: + total = c["count"] + if c["feed_id"]: + feed_total += c["count"] + if try_counting and (total != feed_total or not len(counts)): user = User.objects.get(pk=user_id) - logging.user(user, "~FC~SBCounting~SN saved stories (%s total vs. %s counted)..." % - (total, feed_total)) + logging.user( + user, "~FC~SBCounting~SN saved stories (%s total vs. %s counted)..." % (total, feed_total) + ) cls.count_for_user(user_id) - return cls.user_counts(user_id, include_total=include_total, - try_counting=False) - + return cls.user_counts(user_id, include_total=include_total, try_counting=False) + if include_total: return counts, total return counts - + @classmethod def schedule_count_tags_for_user(cls, user_id): ScheduleCountTagsForUser.apply_async(kwargs=dict(user_id=user_id)) - + @classmethod def count_for_user(cls, user_id, total_only=False): user_tags = [] user_feeds = [] highlights = 0 - + if not total_only: cls.objects(user_id=user_id).delete() try: @@ -3323,45 +3660,47 @@ def count_for_user(cls, user_id, total_only=False): logging.debug(" ---> ~FBOperationError on mongo: ~SB%s" % e) total_stories_count = MStarredStory.objects(user_id=user_id).count() - cls.objects(user_id=user_id, tag=None, feed_id=None, is_highlights=None).update_one(set__count=total_stories_count, - upsert=True) + cls.objects(user_id=user_id, tag=None, feed_id=None, is_highlights=None).update_one( + set__count=total_stories_count, upsert=True + ) return dict(total=total_stories_count, tags=user_tags, feeds=user_feeds, highlights=highlights) @classmethod def count_tags_for_user(cls, user_id): - all_tags = MStarredStory.objects(user_id=user_id, - user_tags__exists=True).item_frequencies('user_tags') - user_tags = sorted([(k, v) for k, v in list(all_tags.items()) if int(v) > 0 and k], - key=lambda x: x[0].lower(), - reverse=True) - + all_tags = MStarredStory.objects(user_id=user_id, user_tags__exists=True).item_frequencies( + "user_tags" + ) + user_tags = sorted( + [(k, v) for k, v in list(all_tags.items()) if int(v) > 0 and k], + key=lambda x: x[0].lower(), + reverse=True, + ) + for tag, count in list(dict(user_tags).items()): - cls.objects(user_id=user_id, tag=tag, slug=slugify(tag)).update_one(set__count=count, - upsert=True) - + cls.objects(user_id=user_id, tag=tag, slug=slugify(tag)).update_one(set__count=count, upsert=True) + return user_tags - + @classmethod def count_highlights_for_user(cls, user_id): - highlighted_count = MStarredStory.objects(user_id=user_id, - highlights__exists=True, - __raw__={"$where": "this.highlights.length > 0"}).count() + highlighted_count = MStarredStory.objects( + user_id=user_id, highlights__exists=True, __raw__={"$where": "this.highlights.length > 0"} + ).count() if highlighted_count > 0: - cls.objects(user_id=user_id, - is_highlights=True, - slug="highlights" - ).update_one(set__count=highlighted_count, upsert=True) + cls.objects(user_id=user_id, is_highlights=True, slug="highlights").update_one( + set__count=highlighted_count, upsert=True + ) else: cls.objects(user_id=user_id, is_highlights=True, slug="highlights").delete() - + return highlighted_count - + @classmethod def count_feeds_for_user(cls, user_id): - all_feeds = MStarredStory.objects(user_id=user_id).item_frequencies('story_feed_id') + all_feeds = MStarredStory.objects(user_id=user_id).item_frequencies("story_feed_id") user_feeds = dict([(k, v) for k, v in list(all_feeds.items()) if v]) - + # Clean up None'd and 0'd feed_ids, so they can be counted against the total if user_feeds.get(None, False): user_feeds[0] = user_feeds.get(0, 0) @@ -3370,26 +3709,26 @@ def count_feeds_for_user(cls, user_id): if user_feeds.get(0, False): user_feeds[-1] = user_feeds.get(0, 0) del user_feeds[0] - + too_many_feeds = False if len(user_feeds) < 1000 else True for feed_id, count in list(user_feeds.items()): - if too_many_feeds and count <= 1: continue - cls.objects(user_id=user_id, - feed_id=feed_id, - slug="feed:%s" % feed_id).update_one(set__count=count, - upsert=True) - + if too_many_feeds and count <= 1: + continue + cls.objects(user_id=user_id, feed_id=feed_id, slug="feed:%s" % feed_id).update_one( + set__count=count, upsert=True + ) + return user_feeds - + @classmethod def adjust_count(cls, user_id, feed_id=None, tag=None, highlights=None, amount=0): params = dict(user_id=user_id) if feed_id: - params['feed_id'] = feed_id + params["feed_id"] = feed_id if tag: - params['tag'] = tag + params["tag"] = tag if highlights: - params['is_highlights'] = True + params["is_highlights"] = True cls.objects(**params).update_one(inc__count=amount, upsert=True) try: @@ -3399,6 +3738,7 @@ def adjust_count(cls, user_id, feed_id=None, tag=None, highlights=None, amount=0 if story_count and story_count.count <= 0: story_count.delete() + class MSavedSearch(mongo.Document): user_id = mongo.IntField() query = mongo.StringField(max_length=1024) @@ -3406,58 +3746,61 @@ class MSavedSearch(mongo.Document): slug = mongo.StringField(max_length=128) meta = { - 'collection': 'saved_searches', - 'indexes': ['user_id', - {'fields': ['user_id', 'feed_id', 'query'], - 'unique': True, - }], - 'ordering': ['query'], - 'allow_inheritance': False, + "collection": "saved_searches", + "indexes": [ + "user_id", + { + "fields": ["user_id", "feed_id", "query"], + "unique": True, + }, + ], + "ordering": ["query"], + "allow_inheritance": False, } @property def rss_url(self, secret_token=None): if not secret_token: - user = User.objects.select_related('profile').get(pk=self.user_id) + user = User.objects.select_related("profile").get(pk=self.user_id) secret_token = user.profile.secret_token - + slug = self.slug if self.slug else "" - return "%s/reader/saved_search/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id, - secret_token, slug) - + return "%s/reader/saved_search/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id, secret_token, slug) + @classmethod def user_searches(cls, user_id): searches = cls.objects.filter(user_id=user_id) - searches = sorted([{'query': s.query, - 'feed_address': s.rss_url, - 'feed_id': s.feed_id, - 'active': True, - } for s in searches], - key=lambda x: (x.get('query', '') or '').lower()) + searches = sorted( + [ + { + "query": s.query, + "feed_address": s.rss_url, + "feed_id": s.feed_id, + "active": True, + } + for s in searches + ], + key=lambda x: (x.get("query", "") or "").lower(), + ) return searches - + @classmethod def save_search(cls, user_id, feed_id, query): user = User.objects.get(pk=user_id) - params = dict(user_id=user_id, - feed_id=feed_id, - query=query, - slug=slugify(query)) + params = dict(user_id=user_id, feed_id=feed_id, query=query, slug=slugify(query)) try: saved_search = cls.objects.get(**params) logging.user(user, "~FRSaved search already exists: ~SB%s" % query) except cls.DoesNotExist: logging.user(user, "~FCCreating a saved search: ~SB%s~SN/~SB%s" % (feed_id, query)) saved_search = cls.objects.create(**params) - + return saved_search - + @classmethod def delete_search(cls, user_id, feed_id, query): user = User.objects.get(pk=user_id) - params = dict(user_id=user_id, - feed_id=feed_id, - query=query) + params = dict(user_id=user_id, feed_id=feed_id, query=query) try: saved_search = cls.objects.get(**params) logging.user(user, "~FCDeleting saved search: ~SB%s" % query) @@ -3465,89 +3808,90 @@ def delete_search(cls, user_id, feed_id, query): except cls.DoesNotExist: logging.user(user, "~FRCan't delete saved search, missing: ~SB%s~SN/~SB%s" % (feed_id, query)) except cls.MultipleObjectsReturned: - logging.user(user, "~FRFound multiple saved searches, deleting: ~SB%s~SN/~SB%s" % (feed_id, query)) + logging.user( + user, "~FRFound multiple saved searches, deleting: ~SB%s~SN/~SB%s" % (feed_id, query) + ) cls.objects(**params).delete() - - + + class MFetchHistory(mongo.Document): feed_id = mongo.IntField(unique=True) feed_fetch_history = mongo.DynamicField() page_fetch_history = mongo.DynamicField() push_history = mongo.DynamicField() raw_feed_history = mongo.DynamicField() - + meta = { - 'db_alias': 'nbanalytics', - 'collection': 'fetch_history', - 'allow_inheritance': False, + "db_alias": "nbanalytics", + "collection": "fetch_history", + "allow_inheritance": False, } @classmethod def feed(cls, feed_id, timezone=None, fetch_history=None): if not fetch_history: try: - fetch_history = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY)\ - .get(feed_id=feed_id) + fetch_history = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY).get( + feed_id=feed_id + ) except cls.DoesNotExist: fetch_history = cls.objects.create(feed_id=feed_id) history = {} - for fetch_type in ['feed_fetch_history', 'page_fetch_history', 'push_history']: + for fetch_type in ["feed_fetch_history", "page_fetch_history", "push_history"]: history[fetch_type] = getattr(fetch_history, fetch_type) if not history[fetch_type]: history[fetch_type] = [] for f, fetch in enumerate(history[fetch_type]): - date_key = 'push_date' if fetch_type == 'push_history' else 'fetch_date' + date_key = "push_date" if fetch_type == "push_history" else "fetch_date" history[fetch_type][f] = { - date_key: localtime_for_timezone(fetch[0], - timezone).strftime("%Y-%m-%d %H:%M:%S"), - 'status_code': fetch[1], - 'message': fetch[2] + date_key: localtime_for_timezone(fetch[0], timezone).strftime("%Y-%m-%d %H:%M:%S"), + "status_code": fetch[1], + "message": fetch[2], } return history - + @classmethod def add(cls, feed_id, fetch_type, date=None, message=None, code=None, exception=None): if not date: date = datetime.datetime.now() try: - fetch_history = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY)\ - .get(feed_id=feed_id) + fetch_history = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY).get(feed_id=feed_id) except cls.DoesNotExist: fetch_history = cls.objects.create(feed_id=feed_id) - - if fetch_type == 'feed': + + if fetch_type == "feed": history = fetch_history.feed_fetch_history or [] - elif fetch_type == 'page': + elif fetch_type == "page": history = fetch_history.page_fetch_history or [] - elif fetch_type == 'push': + elif fetch_type == "push": history = fetch_history.push_history or [] - elif fetch_type == 'raw_feed': + elif fetch_type == "raw_feed": history = fetch_history.raw_feed_history or [] history = [[date, code, message]] + history any_exceptions = any([c for d, c, m in history if c not in [200, 304]]) if any_exceptions: history = history[:25] - elif fetch_type == 'raw_feed': + elif fetch_type == "raw_feed": history = history[:10] else: history = history[:5] - if fetch_type == 'feed': + if fetch_type == "feed": fetch_history.feed_fetch_history = history - elif fetch_type == 'page': + elif fetch_type == "page": fetch_history.page_fetch_history = history - elif fetch_type == 'push': + elif fetch_type == "push": fetch_history.push_history = history - elif fetch_type == 'raw_feed': + elif fetch_type == "raw_feed": fetch_history.raw_feed_history = history - + fetch_history.save() - - if fetch_type == 'feed': - RStats.add('feed_fetch') - + + if fetch_type == "feed": + RStats.add("feed_fetch") + return cls.feed(feed_id, fetch_history=fetch_history) @@ -3555,33 +3899,34 @@ class DuplicateFeed(models.Model): duplicate_address = models.CharField(max_length=764, db_index=True) duplicate_link = models.CharField(max_length=764, null=True, db_index=True) duplicate_feed_id = models.CharField(max_length=255, null=True, db_index=True) - feed = models.ForeignKey(Feed, related_name='duplicate_addresses', on_delete=models.CASCADE) - + feed = models.ForeignKey(Feed, related_name="duplicate_addresses", on_delete=models.CASCADE) + def __str__(self): return "%s: %s / %s" % (self.feed, self.duplicate_address, self.duplicate_link) - + def canonical(self): return { - 'duplicate_address': self.duplicate_address, - 'duplicate_link': self.duplicate_link, - 'duplicate_feed_id': self.duplicate_feed_id, - 'feed_id': self.feed_id + "duplicate_address": self.duplicate_address, + "duplicate_link": self.duplicate_link, + "duplicate_feed_id": self.duplicate_feed_id, + "feed_id": self.feed_id, } - + def save(self, *args, **kwargs): - max_address = DuplicateFeed._meta.get_field('duplicate_address').max_length + max_address = DuplicateFeed._meta.get_field("duplicate_address").max_length if len(self.duplicate_address) > max_address: self.duplicate_address = self.duplicate_address[:max_address] - max_link = DuplicateFeed._meta.get_field('duplicate_link').max_length + max_link = DuplicateFeed._meta.get_field("duplicate_link").max_length if self.duplicate_link and len(self.duplicate_link) > max_link: self.duplicate_link = self.duplicate_link[:max_link] - + super(DuplicateFeed, self).save(*args, **kwargs) + def merge_feeds(original_feed_id, duplicate_feed_id, force=False): from apps.reader.models import UserSubscription from apps.social.models import MSharedStory - + if original_feed_id == duplicate_feed_id: logging.info(" ***> Merging the same feed. Ignoring...") return original_feed_id @@ -3591,82 +3936,94 @@ def merge_feeds(original_feed_id, duplicate_feed_id, force=False): except Feed.DoesNotExist: logging.info(" ***> Already deleted feed: %s" % duplicate_feed_id) return original_feed_id - + heavier_dupe = original_feed.num_subscribers < duplicate_feed.num_subscribers branched_original = original_feed.branch_from_feed and not duplicate_feed.branch_from_feed if (heavier_dupe or branched_original) and not force: original_feed, duplicate_feed = duplicate_feed, original_feed original_feed_id, duplicate_feed_id = duplicate_feed_id, original_feed_id if branched_original: - original_feed.feed_address = duplicate_feed.feed_address - - logging.info(" ---> Feed: [%s - %s] %s - %s" % (original_feed_id, duplicate_feed_id, - original_feed, original_feed.feed_link)) - logging.info(" Orig ++> %s: (%s subs) %s / %s %s" % (original_feed.pk, - original_feed.num_subscribers, - original_feed.feed_address, - original_feed.feed_link, - " [B: %s]" % original_feed.branch_from_feed.pk if original_feed.branch_from_feed else "")) - logging.info(" Dupe --> %s: (%s subs) %s / %s %s" % (duplicate_feed.pk, - duplicate_feed.num_subscribers, - duplicate_feed.feed_address, - duplicate_feed.feed_link, - " [B: %s]" % duplicate_feed.branch_from_feed.pk if duplicate_feed.branch_from_feed else "")) + original_feed.feed_address = strip_underscore_from_feed_address(duplicate_feed.feed_address) + + logging.info( + " ---> Feed: [%s - %s] %s - %s" + % (original_feed_id, duplicate_feed_id, original_feed, original_feed.feed_link) + ) + logging.info( + " Orig ++> %s: (%s subs) %s / %s %s" + % ( + original_feed.pk, + original_feed.num_subscribers, + original_feed.feed_address, + original_feed.feed_link, + " [B: %s]" % original_feed.branch_from_feed.pk if original_feed.branch_from_feed else "", + ) + ) + logging.info( + " Dupe --> %s: (%s subs) %s / %s %s" + % ( + duplicate_feed.pk, + duplicate_feed.num_subscribers, + duplicate_feed.feed_address, + duplicate_feed.feed_link, + " [B: %s]" % duplicate_feed.branch_from_feed.pk if duplicate_feed.branch_from_feed else "", + ) + ) original_feed.branch_from_feed = None - - user_subs = UserSubscription.objects.filter(feed=duplicate_feed).order_by('-pk') + + user_subs = UserSubscription.objects.filter(feed=duplicate_feed).order_by("-pk") for user_sub in user_subs: user_sub.switch_feed(original_feed, duplicate_feed) - def delete_story_feed(model, feed_field='feed_id'): + def delete_story_feed(model, feed_field="feed_id"): duplicate_stories = model.objects(**{feed_field: duplicate_feed.pk}) # if duplicate_stories.count(): # logging.info(" ---> Deleting %s %s" % (duplicate_stories.count(), model)) duplicate_stories.delete() - - delete_story_feed(MStory, 'story_feed_id') - delete_story_feed(MFeedPage, 'feed_id') + + delete_story_feed(MStory, "story_feed_id") + delete_story_feed(MFeedPage, "feed_id") try: DuplicateFeed.objects.create( duplicate_address=duplicate_feed.feed_address, duplicate_link=duplicate_feed.feed_link, duplicate_feed_id=duplicate_feed.pk, - feed=original_feed + feed=original_feed, ) except (IntegrityError, OperationError) as e: logging.info(" ***> Could not save DuplicateFeed: %s" % e) - + # Switch this dupe feed's dupe feeds over to the new original. duplicate_feeds_duplicate_feeds = DuplicateFeed.objects.filter(feed=duplicate_feed) for dupe_feed in duplicate_feeds_duplicate_feeds: dupe_feed.feed = original_feed dupe_feed.duplicate_feed_id = duplicate_feed.pk dupe_feed.save() - - logging.debug(' ---> Dupe subscribers (%s): %s, Original subscribers (%s): %s' % - (duplicate_feed.pk, duplicate_feed.num_subscribers, - original_feed.pk, original_feed.num_subscribers)) + + logging.debug( + " ---> Dupe subscribers (%s): %s, Original subscribers (%s): %s" + % (duplicate_feed.pk, duplicate_feed.num_subscribers, original_feed.pk, original_feed.num_subscribers) + ) if duplicate_feed.pk != original_feed.pk: duplicate_feed.delete() else: logging.debug(" ***> Duplicate feed is the same as original feed. Panic!") - logging.debug(' ---> Deleted duplicate feed: %s/%s' % (duplicate_feed, duplicate_feed_id)) + logging.debug(" ---> Deleted duplicate feed: %s/%s" % (duplicate_feed, duplicate_feed_id)) original_feed.branch_from_feed = None original_feed.count_subscribers() original_feed.save() - logging.debug(' ---> Now original subscribers: %s' % - (original_feed.num_subscribers)) - - + logging.debug(" ---> Now original subscribers: %s" % (original_feed.num_subscribers)) + MSharedStory.switch_feed(original_feed_id, duplicate_feed_id) - + return original_feed_id - + + def rewrite_folders(folders, original_feed, duplicate_feed): new_folders = [] - + for k, folder in enumerate(folders): if isinstance(folder, int): if folder == duplicate_feed.pk: diff --git a/apps/rss_feeds/page_importer.py b/apps/rss_feeds/page_importer.py index a3d2f321e4..58b11cda30 100644 --- a/apps/rss_feeds/page_importer.py +++ b/apps/rss_feeds/page_importer.py @@ -26,51 +26,55 @@ # from utils.feed_functions import mail_feed_error_to_admin BROKEN_PAGES = [ - 'tag:', - 'info:', - 'uuid:', - 'urn:', - '[]', + "tag:", + "info:", + "uuid:", + "urn:", + "[]", ] # Also change in reader_utils.js. BROKEN_PAGE_URLS = [ - 'nytimes.com', - 'github.com', - 'washingtonpost.com', - 'stackoverflow.com', - 'stackexchange.com', - 'twitter.com', - 'rankexploits', - 'gamespot.com', - 'espn.com', - 'royalroad.com', + "nytimes.com", + "github.com", + "washingtonpost.com", + "stackoverflow.com", + "stackexchange.com", + "twitter.com", + "rankexploits", + "gamespot.com", + "espn.com", + "royalroad.com", ] + class PageImporter(object): - def __init__(self, feed, story=None, request=None): self.feed = feed self.story = story self.request = request - + @property def headers(self): return { - 'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s %s' % ( + "User-Agent": "NewsBlur Page Fetcher - %s subscriber%s - %s %s" + % ( self.feed.num_subscribers, - 's' if self.feed.num_subscribers != 1 else '', + "s" if self.feed.num_subscribers != 1 else "", self.feed.permalink, self.feed.fake_user_agent, ), } - + def fetch_page(self, urllib_fallback=False, requests_exception=None): try: self.fetch_page_timeout(urllib_fallback=urllib_fallback, requests_exception=requests_exception) except TimeoutError: - logging.user(self.request, ' ***> [%-30s] ~FBPage fetch ~SN~FRfailed~FB due to timeout' % (self.feed.log_title[:30])) - + logging.user( + self.request, + " ***> [%-30s] ~FBPage fetch ~SN~FRfailed~FB due to timeout" % (self.feed.log_title[:30]), + ) + @timelimit(10) def fetch_page_timeout(self, urllib_fallback=False, requests_exception=None): html = None @@ -79,8 +83,8 @@ def fetch_page_timeout(self, urllib_fallback=False, requests_exception=None): self.save_no_page(reason="No feed link") return - if feed_link.startswith('www'): - self.feed.feed_link = 'http://' + feed_link + if feed_link.startswith("www"): + self.feed.feed_link = "http://" + feed_link try: if any(feed_link.startswith(s) for s in BROKEN_PAGES): self.save_no_page(reason="Broken page") @@ -88,36 +92,45 @@ def fetch_page_timeout(self, urllib_fallback=False, requests_exception=None): elif any(s in feed_link.lower() for s in BROKEN_PAGE_URLS): self.save_no_page(reason="Banned") return - elif feed_link.startswith('http'): + elif feed_link.startswith("http"): if urllib_fallback: request = urllib.request.Request(feed_link, headers=self.headers) response = urllib.request.urlopen(request) - time.sleep(0.01) # Grrr, GIL. - data = response.read().decode(response.headers.get_content_charset() or 'utf-8') + time.sleep(0.01) # Grrr, GIL. + data = response.read().decode(response.headers.get_content_charset() or "utf-8") else: try: response = requests.get(feed_link, headers=self.headers, timeout=10) response.connection.close() except requests.exceptions.TooManyRedirects: response = requests.get(feed_link, timeout=10) - except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError, - requests.adapters.ReadTimeout) as e: - logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed.log_title[:30], e)) + except ( + AttributeError, + SocketError, + OpenSSLError, + PyAsn1Error, + TypeError, + requests.adapters.ReadTimeout, + ) as e: + logging.debug( + " ***> [%-30s] Page fetch failed using requests: %s" + % (self.feed.log_title[:30], e) + ) self.save_no_page(reason="Page fetch failed") return data = response.text - if response.encoding and response.encoding.lower() != 'utf-8': + if response.encoding and response.encoding.lower() != "utf-8": logging.debug(f" -> ~FBEncoding is {response.encoding}, re-encoding...") try: - data = data.encode('utf-8').decode('utf-8') + data = data.encode("utf-8").decode("utf-8") except (LookupError, UnicodeEncodeError): logging.debug(f" -> ~FRRe-encoding failed!") pass else: try: - data = open(feed_link, 'r').read() + data = open(feed_link, "r").read() except IOError: - self.feed.feed_link = 'http://' + feed_link + self.feed.feed_link = "http://" + feed_link self.fetch_page(urllib_fallback=True) return if data: @@ -130,40 +143,45 @@ def fetch_page_timeout(self, urllib_fallback=False, requests_exception=None): else: self.save_no_page(reason="No data found") return - except (ValueError, urllib.error.URLError, http.client.BadStatusLine, http.client.InvalidURL, - requests.exceptions.ConnectionError) as e: - logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e)) + except ( + ValueError, + urllib.error.URLError, + http.client.BadStatusLine, + http.client.InvalidURL, + requests.exceptions.ConnectionError, + ) as e: + logging.debug(" ***> [%-30s] Page fetch failed: %s" % (self.feed.log_title[:30], e)) self.feed.save_page_history(401, "Bad URL", e) try: fp = feedparser.parse(self.feed.feed_address) except (urllib.error.HTTPError, urllib.error.URLError) as e: return html - feed_link = fp.feed.get('link', "") + feed_link = fp.feed.get("link", "") self.feed.save() - except (http.client.IncompleteRead) as e: - logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e)) + except http.client.IncompleteRead as e: + logging.debug(" ***> [%-30s] Page fetch failed: %s" % (self.feed.log_title[:30], e)) self.feed.save_page_history(500, "IncompleteRead", e) - except (requests.exceptions.RequestException, - requests.packages.urllib3.exceptions.HTTPError) as e: - logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed.log_title[:30], e)) + except (requests.exceptions.RequestException, requests.packages.urllib3.exceptions.HTTPError) as e: + logging.debug( + " ***> [%-30s] Page fetch failed using requests: %s" % (self.feed.log_title[:30], e) + ) # mail_feed_error_to_admin(self.feed, e, local_vars=locals()) return self.fetch_page(urllib_fallback=True, requests_exception=e) except Exception as e: - logging.debug('[%d] ! -------------------------' % (self.feed.id,)) + logging.debug("[%d] ! -------------------------" % (self.feed.id,)) tb = traceback.format_exc() logging.debug(tb) - logging.debug('[%d] ! -------------------------' % (self.feed.id,)) + logging.debug("[%d] ! -------------------------" % (self.feed.id,)) self.feed.save_page_history(500, "Error", tb) # mail_feed_error_to_admin(self.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 urllib_fallback: self.fetch_page(urllib_fallback=True) else: self.feed.save_page_history(200, "OK") - + return html def fetch_story(self): @@ -174,62 +192,75 @@ def fetch_story(self): logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal story~FY: timed out") except requests.exceptions.TooManyRedirects: logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal story~FY: too many redirects") - + return html @timelimit(10) def _fetch_story(self): html = None story_permalink = self.story.story_permalink - + if not self.feed: return if any(story_permalink.startswith(s) for s in BROKEN_PAGES): return if any(s in story_permalink.lower() for s in BROKEN_PAGE_URLS): return - if not story_permalink.startswith('http'): + if not story_permalink.startswith("http"): return try: response = requests.get(story_permalink, headers=self.headers, timeout=10) response.connection.close() - except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, - requests.exceptions.ConnectionError, - requests.exceptions.TooManyRedirects, - requests.adapters.ReadTimeout) as e: + except ( + AttributeError, + SocketError, + OpenSSLError, + PyAsn1Error, + requests.exceptions.ConnectionError, + requests.exceptions.TooManyRedirects, + requests.adapters.ReadTimeout, + ) as e: try: response = requests.get(story_permalink, timeout=10) - except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, - requests.exceptions.ConnectionError, - requests.exceptions.TooManyRedirects, - requests.adapters.ReadTimeout) as e: - logging.debug(' ***> [%-30s] Original story fetch failed using requests: %s' % (self.feed.log_title[:30], e)) + except ( + AttributeError, + SocketError, + OpenSSLError, + PyAsn1Error, + requests.exceptions.ConnectionError, + requests.exceptions.TooManyRedirects, + requests.adapters.ReadTimeout, + ) as e: + logging.debug( + " ***> [%-30s] Original story fetch failed using requests: %s" + % (self.feed.log_title[:30], e) + ) return # try: data = response.text # except (LookupError, TypeError): # data = response.content - # import pdb; pdb.set_trace() + # import pdb; pdb.set_trace() - if response.encoding and response.encoding.lower() != 'utf-8': + if response.encoding and response.encoding.lower() != "utf-8": logging.debug(f" -> ~FBEncoding is {response.encoding}, re-encoding...") try: - data = data.encode('utf-8').decode('utf-8') + data = data.encode("utf-8").decode("utf-8") except (LookupError, UnicodeEncodeError): logging.debug(f" -> ~FRRe-encoding failed!") pass if data: - data = data.replace("\xc2\xa0", " ") # Non-breaking space, is mangled when encoding is not utf-8 - data = data.replace("\\u00a0", " ") # Non-breaking space, is mangled when encoding is not utf-8 + data = data.replace("\xc2\xa0", " ") # Non-breaking space, is mangled when encoding is not utf-8 + data = data.replace("\\u00a0", " ") # Non-breaking space, is mangled when encoding is not utf-8 html = self.rewrite_page(data) if not html: return self.save_story(html) - + return html - + def save_story(self, html): self.story.original_page_z = zlib.compress(smart_bytes(html)) try: @@ -237,77 +268,83 @@ def save_story(self, html): except NotUniqueError: pass - def save_no_page(self, reason=None): - logging.debug(' ---> [%-30s] ~FYNo original page: %s / %s' % (self.feed.log_title[:30], reason, self.feed.feed_link)) + logging.debug( + " ---> [%-30s] ~FYNo original page: %s / %s" + % (self.feed.log_title[:30], reason, self.feed.feed_link) + ) self.feed.has_page = False self.feed.save() self.feed.save_page_history(404, f"Feed has no original page: {reason}") def rewrite_page(self, response): - BASE_RE = re.compile(r'', re.I) + BASE_RE = re.compile(r"", re.I) base_code = '' % (self.feed.feed_link,) - html = BASE_RE.sub(' '+base_code, response) - - if ' " + base_code, response) + + if " tags. You know, like # Google Analytics. Ugh. - + FIND_RE = re.compile(r'\b(href|src)\s*=\s*("[^"]*"|\'[^\']*\'|[^"\'<>=\s]+)') ret = [] last_end = 0 - + for match in FIND_RE.finditer(document): url = match.group(2) if url[0] in "\"'": url = url.strip(url[0]) parsed = urllib.parse.urlparse(url) - if parsed.scheme == parsed.netloc == '': #relative to domain + if parsed.scheme == parsed.netloc == "": # relative to domain url = urllib.parse.urljoin(self.feed.feed_link, url) - ret.append(document[last_end:match.start(2)]) + ret.append(document[last_end : match.start(2)]) ret.append('"%s"' % (url,)) last_end = match.end(2) ret.append(document[last_end:]) - - return ''.join(ret) - + + return "".join(ret) + def save_page(self, html): saved = False - + if not html or len(html) < 100: return - - if settings.BACKED_BY_AWS.get('pages_on_node'): + + if settings.BACKED_BY_AWS.get("pages_on_node"): saved = self.save_page_node(html) - if saved and self.feed.s3_page and settings.BACKED_BY_AWS.get('pages_on_s3'): + if saved and self.feed.s3_page and settings.BACKED_BY_AWS.get("pages_on_s3"): self.delete_page_s3() - - if settings.BACKED_BY_AWS.get('pages_on_s3') and not saved: + + if settings.BACKED_BY_AWS.get("pages_on_s3") and not saved: saved = self.save_page_s3(html) - + if not saved: try: feed_page = MFeedPage.objects.get(feed_id=self.feed.pk) # feed_page.page_data = html.encode('utf-8') if feed_page.page() == html: - logging.debug(' ---> [%-30s] ~FYNo change in page data: %s' % (self.feed.log_title[:30], self.feed.feed_link)) + logging.debug( + " ---> [%-30s] ~FYNo change in page data: %s" + % (self.feed.log_title[:30], self.feed.feed_link) + ) else: # logging.debug(' ---> [%-30s] ~FYChange in page data: %s (%s/%s %s/%s)' % (self.feed.log_title[:30], self.feed.feed_link, type(html), type(feed_page.page()), len(html), len(feed_page.page()))) feed_page.page_data = zlib.compress(smart_bytes(html)) feed_page.save() except MFeedPage.DoesNotExist: - feed_page = MFeedPage.objects.create(feed_id=self.feed.pk, - page_data=zlib.compress(smart_bytes(html))) + feed_page = MFeedPage.objects.create( + feed_id=self.feed.pk, page_data=zlib.compress(smart_bytes(html)) + ) return feed_page - + def save_page_node(self, html): domain = "node-page.service.consul:8008" if settings.DOCKERBUILD: @@ -317,42 +354,47 @@ def save_page_node(self, html): self.feed.pk, ) compressed_html = zlib.compress(smart_bytes(html)) - response = requests.post(url, files={ - 'original_page': compressed_html, - # 'original_page': html, - }) + response = requests.post( + url, + files={ + "original_page": compressed_html, + # 'original_page': html, + }, + ) if response.status_code == 200: return True else: - logging.debug(' ---> [%-30s] ~FRFailed to save page to node: %s (%s bytes)' % (self.feed.log_title[:30], response.status_code, len(compressed_html))) + logging.debug( + " ---> [%-30s] ~FRFailed to save page to node: %s (%s bytes)" + % (self.feed.log_title[:30], response.status_code, len(compressed_html)) + ) - def save_page_s3(self, html): - s3_object = settings.S3_CONN.Object(settings.S3_PAGES_BUCKET_NAME, - self.feed.s3_pages_key) - s3_object.put(Body=compress_string_with_gzip(html.encode('utf-8')), - ContentType='text/html', - ContentEncoding='gzip', - Expires=expires, - ACL='public-read' - ) - + s3_object = settings.S3_CONN.Object(settings.S3_PAGES_BUCKET_NAME, self.feed.s3_pages_key) + s3_object.put( + Body=compress_string_with_gzip(html.encode("utf-8")), + ContentType="text/html", + ContentEncoding="gzip", + Expires=expires, + ACL="public-read", + ) + try: feed_page = MFeedPage.objects.get(feed_id=self.feed.pk) feed_page.delete() - logging.debug(' ---> [%-30s] ~FYTransfering page data to S3...' % (self.feed.log_title[:30])) + logging.debug(" ---> [%-30s] ~FYTransfering page data to S3..." % (self.feed.log_title[:30])) except MFeedPage.DoesNotExist: pass - + if not self.feed.s3_page: self.feed.s3_page = True self.feed.save() - + return True - + def delete_page_s3(self): k = settings.S3_CONN.Bucket(settings.S3_PAGES_BUCKET_NAME).Object(key=self.feed.s3_pages_key) k.delete() - + self.feed.s3_page = False self.feed.save() diff --git a/apps/rss_feeds/tasks.py b/apps/rss_feeds/tasks.py index 1ad6003584..2340e55b5e 100644 --- a/apps/rss_feeds/tasks.py +++ b/apps/rss_feeds/tasks.py @@ -14,204 +14,227 @@ FEED_TASKING_MAX = 10000 -@app.task(name='task-feeds') + +@app.task(name="task-feeds") def TaskFeeds(): - from apps.rss_feeds.models import Feed + from apps.rss_feeds.models import Feed + settings.LOG_TO_STREAM = True now = datetime.datetime.utcnow() start = time.time() r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - tasked_feeds_size = r.zcard('tasked_feeds') - + tasked_feeds_size = r.zcard("tasked_feeds") + hour_ago = now - datetime.timedelta(hours=1) - r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s'))) - + r.zremrangebyscore("fetched_feeds_last_hour", 0, int(hour_ago.strftime("%s"))) + now_timestamp = int(now.strftime("%s")) - queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp) - r.zremrangebyscore('scheduled_updates', 0, now_timestamp) + queued_feeds = r.zrangebyscore("scheduled_updates", 0, now_timestamp) + r.zremrangebyscore("scheduled_updates", 0, now_timestamp) if not queued_feeds: logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...") return - - r.sadd('queued_feeds', *queued_feeds) - logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % ( - len(queued_feeds), - r.zcard('tasked_feeds'), - r.scard('queued_feeds'), - r.zcard('scheduled_updates'))) - + + r.sadd("queued_feeds", *queued_feeds) + logging.debug( + " ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" + % (len(queued_feeds), r.zcard("tasked_feeds"), r.scard("queued_feeds"), r.zcard("scheduled_updates")) + ) + # Regular feeds if tasked_feeds_size < FEED_TASKING_MAX: - feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX) + feeds = r.srandmember("queued_feeds", FEED_TASKING_MAX) Feed.task_feeds(feeds, verbose=True) active_count = len(feeds) else: logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size) active_count = 0 feeds = [] - - logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % ( - active_count, - int((time.time() - start)), - r.zcard('tasked_feeds'), - r.scard('queued_feeds'), - r.zcard('scheduled_updates'))) + + logging.debug( + " ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" + % ( + active_count, + int((time.time() - start)), + r.zcard("tasked_feeds"), + r.scard("queued_feeds"), + r.zcard("scheduled_updates"), + ) + ) logging.debug(" ---> ~FBFeeds being tasked: ~SB%s" % feeds) -@app.task(name='task-broken-feeds') + +@app.task(name="task-broken-feeds") def TaskBrokenFeeds(): - from apps.rss_feeds.models import Feed + from apps.rss_feeds.models import Feed + settings.LOG_TO_STREAM = True now = datetime.datetime.utcnow() start = time.time() r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - + logging.debug(" ---> ~SN~FBQueuing broken feeds...") - + # Force refresh feeds - refresh_feeds = Feed.objects.filter( - active=True, - fetched_once=False, - active_subscribers__gte=1 - ).order_by('?')[:100] + refresh_feeds = Feed.objects.filter(active=True, fetched_once=False, active_subscribers__gte=1).order_by( + "?" + )[:100] refresh_count = refresh_feeds.count() cp1 = time.time() - + logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count) # Mistakenly inactive feeds - hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s') - old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago) + hours_ago = (now - datetime.timedelta(minutes=10)).strftime("%s") + old_tasked_feeds = r.zrangebyscore("tasked_feeds", 0, hours_ago) inactive_count = len(old_tasked_feeds) if inactive_count: - r.zremrangebyscore('tasked_feeds', 0, hours_ago) + r.zremrangebyscore("tasked_feeds", 0, hours_ago) # r.sadd('queued_feeds', *old_tasked_feeds) for feed_id in old_tasked_feeds: - r.zincrby('error_feeds', 1, feed_id) + r.zincrby("error_feeds", 1, feed_id) feed = Feed.get_by_id(feed_id) feed.set_next_scheduled_update() - logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % ( - inactive_count, - r.scard('queued_feeds'), - r.zcard('tasked_feeds'))) + logging.debug( + " ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" + % (inactive_count, r.scard("queued_feeds"), r.zcard("tasked_feeds")) + ) cp2 = time.time() - + old = now - datetime.timedelta(days=1) - old_feeds = Feed.objects.filter( - next_scheduled_update__lte=old, - active_subscribers__gte=1 - ).order_by('?')[:500] + old_feeds = Feed.objects.filter(next_scheduled_update__lte=old, active_subscribers__gte=1).order_by("?")[ + :500 + ] old_count = old_feeds.count() cp3 = time.time() - - logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % ( - refresh_count, - inactive_count, - old_count, - cp1 - start, - cp2 - cp1, - cp3 - cp2, - )) - + + logging.debug( + " ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" + % ( + refresh_count, + inactive_count, + old_count, + cp1 - start, + cp2 - cp1, + cp3 - cp2, + ) + ) + Feed.task_feeds(refresh_feeds, verbose=False) Feed.task_feeds(old_feeds, verbose=False) - - logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % ( - int((time.time() - start)), - r.zcard('tasked_feeds'), - r.scard('queued_feeds'), - r.zcard('scheduled_updates'))) - -@app.task(name='update-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True) + + logging.debug( + " ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" + % ( + int((time.time() - start)), + r.zcard("tasked_feeds"), + r.scard("queued_feeds"), + r.zcard("scheduled_updates"), + ) + ) + + +@app.task(name="update-feeds", time_limit=10 * 60, soft_time_limit=9 * 60, ignore_result=True) def UpdateFeeds(feed_pks): from apps.rss_feeds.models import Feed from apps.statistics.models import MStatistics + r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0)) + mongodb_replication_lag = int(MStatistics.get("mongodb_replication_lag", 0)) compute_scores = bool(mongodb_replication_lag < 10) - + profiler = DBProfilerMiddleware() profiler_activated = profiler.process_celery() if profiler_activated: settings.MONGO_COMMAND_LOGGER.process_celery(profiler) redis_middleware = RedisDumpMiddleware() redis_middleware.process_celery(profiler) - + options = { - 'quick': float(MStatistics.get('quick_fetch', 0)), - 'updates_off': MStatistics.get('updates_off', False), - 'compute_scores': compute_scores, - 'mongodb_replication_lag': mongodb_replication_lag, + "quick": float(MStatistics.get("quick_fetch", 0)), + "updates_off": MStatistics.get("updates_off", False), + "compute_scores": compute_scores, + "mongodb_replication_lag": mongodb_replication_lag, } - + if not isinstance(feed_pks, list): feed_pks = [feed_pks] - + for feed_pk in feed_pks: feed = Feed.get_by_id(feed_pk) if not feed or feed.pk != int(feed_pk): - logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk)) - r.zrem('tasked_feeds', feed_pk) + logging.info( + " ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." + % (feed_pk, feed and feed.pk) + ) + r.zrem("tasked_feeds", feed_pk) if not feed: continue try: feed.update(**options) except SoftTimeLimitExceeded as e: - feed.save_feed_history(505, 'Timeout', e) + feed.save_feed_history(505, "Timeout", e) logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed) - if profiler_activated: profiler.process_celery_finished() + if profiler_activated: + profiler.process_celery_finished() + -@app.task(name='new-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True) +@app.task(name="new-feeds", time_limit=10 * 60, soft_time_limit=9 * 60, ignore_result=True) def NewFeeds(feed_pks): from apps.rss_feeds.models import Feed + if not isinstance(feed_pks, list): feed_pks = [feed_pks] - + options = {} for feed_pk in feed_pks: feed = Feed.get_by_id(feed_pk) - if not feed: continue + if not feed: + continue feed.update(options=options) -@app.task(name='push-feeds', ignore_result=True) + +@app.task(name="push-feeds", ignore_result=True) def PushFeeds(feed_id, xml): from apps.rss_feeds.models import Feed from apps.statistics.models import MStatistics - - mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0)) + + mongodb_replication_lag = int(MStatistics.get("mongodb_replication_lag", 0)) compute_scores = bool(mongodb_replication_lag < 60) - + options = { - 'feed_xml': xml, - 'compute_scores': compute_scores, - 'mongodb_replication_lag': mongodb_replication_lag, + "feed_xml": xml, + "compute_scores": compute_scores, + "mongodb_replication_lag": mongodb_replication_lag, } feed = Feed.get_by_id(feed_id) if feed: feed.update(options=options) + @app.task() def ScheduleImmediateFetches(feed_ids, user_id=None): from apps.rss_feeds.models import Feed - + if not isinstance(feed_ids, list): feed_ids = [feed_ids] - + Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id) @app.task() def SchedulePremiumSetup(feed_ids): from apps.rss_feeds.models import Feed - + if not isinstance(feed_ids, list): feed_ids = [feed_ids] - + Feed.setup_feeds_for_premium_subscribers(feed_ids) - + + @app.task() def ScheduleCountTagsForUser(user_id): from apps.rss_feeds.models import MStarredStoryCounts - + MStarredStoryCounts.count_for_user(user_id) diff --git a/apps/rss_feeds/test_rss_feeds.py b/apps/rss_feeds/test_rss_feeds.py index 8e3ca41e22..4e167d816e 100644 --- a/apps/rss_feeds/test_rss_feeds.py +++ b/apps/rss_feeds/test_rss_feeds.py @@ -1,40 +1,44 @@ import redis -from utils import json_functions as json -from django.test.client import Client -from django.test import TestCase +from django.conf import settings from django.core import management +from django.test import TestCase +from django.test.client import Client from django.urls import reverse -from django.conf import settings -from apps.rss_feeds.models import Feed, MStory from mongoengine.connection import connect, disconnect +from apps.rss_feeds.models import Feed, MStory +from utils import json_functions as json -class Test_Feed(TestCase): - fixtures = ['initial_data.json'] +class Test_Feed(TestCase): + fixtures = ["initial_data.json"] def setUp(self): disconnect() - settings.MONGODB = connect('test_newsblur') - settings.REDIS_STORY_HASH_POOL = redis.ConnectionPool(host=settings.REDIS_STORY['host'], port=6379, db=10) - settings.REDIS_FEED_READ_POOL = redis.ConnectionPool(host=settings.REDIS_SESSIONS['host'], port=6379, db=10) + settings.MONGODB = connect("test_newsblur") + settings.REDIS_STORY_HASH_POOL = redis.ConnectionPool( + host=settings.REDIS_STORY["host"], port=6379, db=10 + ) + settings.REDIS_FEED_READ_POOL = redis.ConnectionPool( + host=settings.REDIS_SESSIONS["host"], port=6379, db=10 + ) r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - r.delete('RS:1') - r.delete('lRS:1') - r.delete('RS:1:766') - r.delete('zF:766') - r.delete('F:766') - + r.delete("RS:1") + r.delete("lRS:1") + r.delete("RS:1:766") + r.delete("zF:766") + r.delete("F:766") + self.client = Client() def tearDown(self): - settings.MONGODB.drop_database('test_newsblur') + settings.MONGODB.drop_database("test_newsblur") def test_load_feeds__gawker(self): - self.client.login(username='conesus', password='test') + self.client.login(username="conesus", password="test") - management.call_command('loaddata', 'gawker1.json', verbosity=0, skip_checks=False) + management.call_command("loaddata", "gawker1.json", verbosity=0, skip_checks=False) feed = Feed.objects.get(pk=10) stories = MStory.objects(story_feed_id=feed.pk) @@ -45,7 +49,7 @@ def test_load_feeds__gawker(self): stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 38) - management.call_command('loaddata', 'gawker2.json', verbosity=0, skip_checks=False) + management.call_command("loaddata", "gawker2.json", verbosity=0, skip_checks=False) feed.update(force=True) @@ -53,16 +57,16 @@ def test_load_feeds__gawker(self): stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 38) - url = reverse('load-single-feed', kwargs=dict(feed_id=10)) + url = reverse("load-single-feed", kwargs=dict(feed_id=10)) response = self.client.get(url) feed = json.decode(response.content) - self.assertEqual(len(feed['stories']), 6) + self.assertEqual(len(feed["stories"]), 6) def test_load_feeds__gothamist(self): - self.client.login(username='conesus', password='test') + self.client.login(username="conesus", password="test") - management.call_command('loaddata', 'gothamist_aug_2009_1.json', verbosity=0, skip_checks=False) - feed = Feed.objects.get(feed_link__contains='gothamist') + management.call_command("loaddata", "gothamist_aug_2009_1.json", verbosity=0, skip_checks=False) + feed = Feed.objects.get(feed_link__contains="gothamist") stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 0) @@ -71,177 +75,179 @@ def test_load_feeds__gothamist(self): stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 42) - url = reverse('load-single-feed', kwargs=dict(feed_id=4)) + url = reverse("load-single-feed", kwargs=dict(feed_id=4)) response = self.client.get(url) content = json.decode(response.content) - self.assertEqual(len(content['stories']), 6) + self.assertEqual(len(content["stories"]), 6) - management.call_command('loaddata', 'gothamist_aug_2009_2.json', verbosity=0, skip_checks=False) + management.call_command("loaddata", "gothamist_aug_2009_2.json", verbosity=0, skip_checks=False) feed.update(force=True) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 42) - url = reverse('load-single-feed', kwargs=dict(feed_id=4)) + url = reverse("load-single-feed", kwargs=dict(feed_id=4)) response = self.client.get(url) # print [c['story_title'] for c in json.decode(response.content)] content = json.decode(response.content) # Test: 1 changed char in title - self.assertEqual(len(content['stories']), 6) + self.assertEqual(len(content["stories"]), 6) def test_load_feeds__slashdot(self): - self.client.login(username='conesus', password='test') + self.client.login(username="conesus", password="test") old_story_guid = "tag:google.com,2005:reader/item/4528442633bc7b2b" - management.call_command('loaddata', 'slashdot1.json', verbosity=0, skip_checks=False) + management.call_command("loaddata", "slashdot1.json", verbosity=0, skip_checks=False) - feed = Feed.objects.get(feed_link__contains='slashdot') + feed = Feed.objects.get(feed_link__contains="slashdot") stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 0) - management.call_command('refresh_feed', force=1, feed=5, daemonize=False, skip_checks=False) + management.call_command("refresh_feed", force=1, feed=5, daemonize=False, skip_checks=False) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 38) - response = self.client.get(reverse('load-feeds')) + response = self.client.get(reverse("load-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds']['5']['nt'], 38) + self.assertEqual(content["feeds"]["5"]["nt"], 38) - self.client.post(reverse('mark-story-as-read'), {'story_id': old_story_guid, 'feed_id': 5}) + self.client.post(reverse("mark-story-as-read"), {"story_id": old_story_guid, "feed_id": 5}) - response = self.client.get(reverse('refresh-feeds')) + response = self.client.get(reverse("refresh-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds']['5']['nt'], 37) + self.assertEqual(content["feeds"]["5"]["nt"], 37) - management.call_command('loaddata', 'slashdot2.json', verbosity=0, skip_checks=False) - management.call_command('refresh_feed', force=1, feed=5, daemonize=False, skip_checks=False) + management.call_command("loaddata", "slashdot2.json", verbosity=0, skip_checks=False) + management.call_command("refresh_feed", force=1, feed=5, daemonize=False, skip_checks=False) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 38) - url = reverse('load-single-feed', kwargs=dict(feed_id=5)) + url = reverse("load-single-feed", kwargs=dict(feed_id=5)) response = self.client.get(url) # pprint([c['story_title'] for c in json.decode(response.content)]) feed = json.decode(response.content) # Test: 1 changed char in title - self.assertEqual(len(feed['stories']), 6) + self.assertEqual(len(feed["stories"]), 6) - response = self.client.get(reverse('refresh-feeds')) + response = self.client.get(reverse("refresh-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds']['5']['nt'], 37) + self.assertEqual(content["feeds"]["5"]["nt"], 37) def test_load_feeds__motherjones(self): - self.client.login(username='conesus', password='test') + self.client.login(username="conesus", password="test") - management.call_command('loaddata', 'motherjones1.json', verbosity=0, skip_checks=False) + management.call_command("loaddata", "motherjones1.json", verbosity=0, skip_checks=False) - feed = Feed.objects.get(feed_link__contains='motherjones') + feed = Feed.objects.get(feed_link__contains="motherjones") stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 0) - management.call_command('refresh_feed', force=1, feed=feed.pk, daemonize=False, skip_checks=False) + management.call_command("refresh_feed", force=1, feed=feed.pk, daemonize=False, skip_checks=False) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 10) - response = self.client.get(reverse('load-feeds')) + response = self.client.get(reverse("load-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds'][str(feed.pk)]['nt'], 10) + self.assertEqual(content["feeds"][str(feed.pk)]["nt"], 10) - self.client.post(reverse('mark-story-as-read'), {'story_id': stories[0].story_guid, 'feed_id': feed.pk}) + self.client.post( + reverse("mark-story-as-read"), {"story_id": stories[0].story_guid, "feed_id": feed.pk} + ) - response = self.client.get(reverse('refresh-feeds')) + response = self.client.get(reverse("refresh-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds'][str(feed.pk)]['nt'], 9) + self.assertEqual(content["feeds"][str(feed.pk)]["nt"], 9) - management.call_command('loaddata', 'motherjones2.json', verbosity=0, skip_checks=False) - management.call_command('refresh_feed', force=1, feed=feed.pk, daemonize=False, skip_checks=False) + management.call_command("loaddata", "motherjones2.json", verbosity=0, skip_checks=False) + management.call_command("refresh_feed", force=1, feed=feed.pk, daemonize=False, skip_checks=False) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 10) - url = reverse('load-single-feed', kwargs=dict(feed_id=feed.pk)) + url = reverse("load-single-feed", kwargs=dict(feed_id=feed.pk)) response = self.client.get(url) # pprint([c['story_title'] for c in json.decode(response.content)]) feed = json.decode(response.content) # Test: 1 changed char in title - self.assertEqual(len(feed['stories']), 6) + self.assertEqual(len(feed["stories"]), 6) - response = self.client.get(reverse('refresh-feeds')) + response = self.client.get(reverse("refresh-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds'][str(feed['feed_id'])]['nt'], 9) + self.assertEqual(content["feeds"][str(feed["feed_id"])]["nt"], 9) def test_load_feeds__google(self): # Freezegun the date to 2017-04-30 - - self.client.login(username='conesus', password='test') + + self.client.login(username="conesus", password="test") old_story_guid = "blog.google:443/topics/inside-google/google-earths-incredible-3d-imagery-explained/" - management.call_command('loaddata', 'google1.json', verbosity=1, skip_checks=False) + management.call_command("loaddata", "google1.json", verbosity=1, skip_checks=False) print((Feed.objects.all())) feed = Feed.objects.get(pk=766) print((" Testing test_load_feeds__google: %s" % feed)) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 0) - management.call_command('refresh_feed', force=False, feed=766, daemonize=False, skip_checks=False) + management.call_command("refresh_feed", force=False, feed=766, daemonize=False, skip_checks=False) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 20) - response = self.client.get(reverse('load-feeds')+"?update_counts=true") + response = self.client.get(reverse("load-feeds") + "?update_counts=true") content = json.decode(response.content) - self.assertEqual(content['feeds']['766']['nt'], 20) + self.assertEqual(content["feeds"]["766"]["nt"], 20) old_story = MStory.objects.get(story_feed_id=feed.pk, story_guid__contains=old_story_guid) - self.client.post(reverse('mark-story-hashes-as-read'), {'story_hash': old_story.story_hash}) + self.client.post(reverse("mark-story-hashes-as-read"), {"story_hash": old_story.story_hash}) - response = self.client.get(reverse('refresh-feeds')) + response = self.client.get(reverse("refresh-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds']['766']['nt'], 19) + self.assertEqual(content["feeds"]["766"]["nt"], 19) - management.call_command('loaddata', 'google2.json', verbosity=1, skip_checks=False) - management.call_command('refresh_feed', force=False, feed=766, daemonize=False, skip_checks=False) + management.call_command("loaddata", "google2.json", verbosity=1, skip_checks=False) + management.call_command("refresh_feed", force=False, feed=766, daemonize=False, skip_checks=False) stories = MStory.objects(story_feed_id=feed.pk) self.assertEqual(stories.count(), 20) - url = reverse('load-single-feed', kwargs=dict(feed_id=766)) + url = reverse("load-single-feed", kwargs=dict(feed_id=766)) response = self.client.get(url) # pprint([c['story_title'] for c in json.decode(response.content)]) feed = json.decode(response.content) # Test: 1 changed char in title - self.assertEqual(len(feed['stories']), 6) + self.assertEqual(len(feed["stories"]), 6) - response = self.client.get(reverse('refresh-feeds')) + response = self.client.get(reverse("refresh-feeds")) content = json.decode(response.content) - self.assertEqual(content['feeds']['766']['nt'], 19) - + self.assertEqual(content["feeds"]["766"]["nt"], 19) + def test_load_feeds__brokelyn__invalid_xml(self): BROKELYN_FEED_ID = 16 - self.client.login(username='conesus', password='test') - management.call_command('loaddata', 'brokelyn.json', verbosity=0) + self.client.login(username="conesus", password="test") + management.call_command("loaddata", "brokelyn.json", verbosity=0) self.assertEquals(Feed.objects.get(pk=BROKELYN_FEED_ID).pk, BROKELYN_FEED_ID) - management.call_command('refresh_feed', force=1, feed=BROKELYN_FEED_ID, daemonize=False) + management.call_command("refresh_feed", force=1, feed=BROKELYN_FEED_ID, daemonize=False) - management.call_command('loaddata', 'brokelyn.json', verbosity=0, skip_checks=False) - management.call_command('refresh_feed', force=1, feed=16, daemonize=False, skip_checks=False) + management.call_command("loaddata", "brokelyn.json", verbosity=0, skip_checks=False) + management.call_command("refresh_feed", force=1, feed=16, daemonize=False, skip_checks=False) - url = reverse('load-single-feed', kwargs=dict(feed_id=BROKELYN_FEED_ID)) + url = reverse("load-single-feed", kwargs=dict(feed_id=BROKELYN_FEED_ID)) response = self.client.get(url) # pprint([c['story_title'] for c in json.decode(response.content)]) feed = json.decode(response.content) # Test: 1 changed char in title - self.assertEqual(len(feed['stories']), 6) + self.assertEqual(len(feed["stories"]), 6) def test_all_feeds(self): pass diff --git a/apps/rss_feeds/text_importer.py b/apps/rss_feeds/text_importer.py index d89d8af05e..91436cd81f 100644 --- a/apps/rss_feeds/text_importer.py +++ b/apps/rss_feeds/text_importer.py @@ -1,32 +1,32 @@ -import requests -import urllib3 import zlib -from vendor import readability -from simplejson.decoder import JSONDecodeError -from requests.packages.urllib3.exceptions import LocationParseError from socket import error as SocketError -from mongoengine.queryset import NotUniqueError +from urllib.parse import urljoin + +import requests +import urllib3 +from bs4 import BeautifulSoup +from django.conf import settings +from django.contrib.sites.models import Site +from django.utils.encoding import smart_bytes, smart_str from lxml.etree import ParserError -from vendor.readability.readability import Unparseable -from utils import log as logging -from utils.feed_functions import timelimit, TimeoutError +from mongoengine.queryset import NotUniqueError from OpenSSL.SSL import Error as OpenSSLError from pyasn1.error import PyAsn1Error -from django.utils.encoding import smart_str -from django.conf import settings -from django.utils.encoding import smart_bytes -from django.contrib.sites.models import Site -from bs4 import BeautifulSoup -from urllib.parse import urljoin - +from requests.packages.urllib3.exceptions import LocationParseError +from simplejson.decoder import JSONDecodeError + +from utils import log as logging +from utils.feed_functions import TimeoutError, timelimit +from vendor import readability +from vendor.readability.readability import Unparseable + BROKEN_URLS = [ "gamespot.com", - 'thedailyskip.com', + "thedailyskip.com", ] class TextImporter: - def __init__(self, story=None, feed=None, story_url=None, request=None, debug=False): self.story = story self.story_url = story_url @@ -38,31 +38,36 @@ def __init__(self, story=None, feed=None, story_url=None, request=None, debug=Fa @property def headers(self): - num_subscribers = getattr(self.feed, 'num_subscribers', 0) + num_subscribers = getattr(self.feed, "num_subscribers", 0) return { - 'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s %s' % ( - num_subscribers, - 's' if num_subscribers != 1 else '', - getattr(self.feed, 'permalink', ''), - getattr(self.feed, 'fake_user_agent', ''), - ), + "User-Agent": "NewsBlur Content Fetcher - %s subscriber%s - %s %s" + % ( + num_subscribers, + "s" if num_subscribers != 1 else "", + getattr(self.feed, "permalink", ""), + getattr(self.feed, "fake_user_agent", ""), + ), } def fetch(self, skip_save=False, return_document=False, use_mercury=True): if self.story_url and any(broken_url in self.story_url for broken_url in BROKEN_URLS): logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: banned") return - + if use_mercury: results = self.fetch_mercury(skip_save=skip_save, return_document=return_document) - + if not use_mercury or not results: - logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY with Mercury, trying readability...", warn_color=False) + logging.user( + self.request, + "~SN~FRFailed~FY to fetch ~FGoriginal text~FY with Mercury, trying readability...", + warn_color=False, + ) results = self.fetch_manually(skip_save=skip_save, return_document=return_document) - + return results - + def fetch_mercury(self, skip_save=False, return_document=False): try: resp = self.fetch_request(use_mercury=True) @@ -72,29 +77,35 @@ def fetch_mercury(self, skip_save=False, return_document=False): except requests.exceptions.TooManyRedirects: logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: too many redirects") resp = None - + if not resp: return - + try: doc = resp.json() except JSONDecodeError: doc = None - if not doc or doc.get('error', False): - logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % (doc and doc.get('messages', None) or "[unknown mercury error]")) + if not doc or doc.get("error", False): + logging.user( + self.request, + "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" + % (doc and doc.get("messages", None) or "[unknown mercury error]"), + ) return - - text = doc['content'] - title = doc['title'] - url = doc['url'] - image = doc['lead_image_url'] - - if image and ('http://' in image[1:] or 'https://' in image[1:]): + + text = doc["content"] + title = doc["title"] + url = doc["url"] + image = doc["lead_image_url"] + + if image and ("http://" in image[1:] or "https://" in image[1:]): logging.user(self.request, "~SN~FRRemoving broken image from text: %s" % image) image = None - - return self.process_content(text, title, url, image, skip_save=skip_save, return_document=return_document) - + + return self.process_content( + text, title, url, image, skip_save=skip_save, return_document=return_document + ) + def fetch_manually(self, skip_save=False, return_document=False): try: resp = self.fetch_request(use_mercury=False) @@ -115,15 +126,16 @@ def extract_text(resp): except (LookupError, TypeError): text = resp.content return text + try: text = extract_text(resp) except TimeoutError: logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: timed out on resp.text") return - + # if self.debug: # logging.user(self.request, "~FBOriginal text's website: %s" % text) - + # if resp.encoding and resp.encoding != 'utf-8': # try: # text = text.encode(resp.encoding) @@ -131,11 +143,12 @@ def extract_text(resp): # pass if text: - text = text.replace("\xc2\xa0", " ") # Non-breaking space, is mangled when encoding is not utf-8 - text = text.replace("\\u00a0", " ") # Non-breaking space, is mangled when encoding is not utf-8 + text = text.replace("\xc2\xa0", " ") # Non-breaking space, is mangled when encoding is not utf-8 + text = text.replace("\\u00a0", " ") # Non-breaking space, is mangled when encoding is not utf-8 - original_text_doc = readability.Document(text, url=resp.url, - positive_keywords="post, entry, postProp, article, postContent, postField") + original_text_doc = readability.Document( + text, url=resp.url, positive_keywords="post, entry, postProp, article, postContent, postField" + ) try: content = original_text_doc.summary(html_partial=True) except (ParserError, Unparseable) as e: @@ -148,18 +161,29 @@ def extract_text(resp): title = "" url = resp.url - - return self.process_content(content, title, url, image=None, skip_save=skip_save, return_document=return_document, - original_text_doc=original_text_doc) - - def process_content(self, content, title, url, image, skip_save=False, return_document=False, original_text_doc=None): - original_story_content = self.story and self.story.story_content_z and zlib.decompress(self.story.story_content_z) + + return self.process_content( + content, + title, + url, + image=None, + skip_save=skip_save, + return_document=return_document, + original_text_doc=original_text_doc, + ) + + def process_content( + self, content, title, url, image, skip_save=False, return_document=False, original_text_doc=None + ): + original_story_content = ( + self.story and self.story.story_content_z and zlib.decompress(self.story.story_content_z) + ) if not original_story_content: original_story_content = "" story_image_urls = self.story and self.story.image_urls if not story_image_urls: story_image_urls = [] - + content = self.add_hero_image(content, story_image_urls) if content: content = self.rewrite_content(content) @@ -169,25 +193,36 @@ def process_content(self, content, title, url, image, skip_save=False, return_do full_content_is_longer = True elif len(content) > len(original_story_content): full_content_is_longer = True - + if content and full_content_is_longer: if self.story and not skip_save: self.story.original_text_z = zlib.compress(smart_bytes(content)) try: self.story.save() except NotUniqueError as e: - logging.user(self.request, ("~SN~FYFetched ~FGoriginal text~FY: %s" % (e)), warn_color=False) + logging.user( + self.request, ("~SN~FYFetched ~FGoriginal text~FY: %s" % (e)), warn_color=False + ) pass - logging.user(self.request, ("~SN~FYFetched ~FGoriginal text~FY: now ~SB%s bytes~SN vs. was ~SB%s bytes" % ( - len(content), - len(original_story_content) - )), warn_color=False) + logging.user( + self.request, + ( + "~SN~FYFetched ~FGoriginal text~FY: now ~SB%s bytes~SN vs. was ~SB%s bytes" + % (len(content), len(original_story_content)) + ), + warn_color=False, + ) else: - logging.user(self.request, ("~SN~FRFailed~FY to fetch ~FGoriginal text~FY: was ~SB%s bytes" % ( - len(original_story_content) - )), warn_color=False) + logging.user( + self.request, + ( + "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: was ~SB%s bytes" + % (len(original_story_content)) + ), + warn_color=False, + ) return - + if return_document: return dict(content=content, title=title, url=url, doc=original_text_doc, image=image) @@ -195,21 +230,22 @@ def process_content(self, content, title, url, image, skip_save=False, return_do def add_hero_image(self, content, image_urls): # Need to have images in the original story to add to the text that may not have any images - if not len(image_urls): + if not len(image_urls): return content - + content_soup = BeautifulSoup(content, features="lxml") - content_imgs = content_soup.findAll('img') + content_imgs = content_soup.findAll("img") for img in content_imgs: # Since NewsBlur proxies all http images over https, the url can change, so acknowledge urls # that are https on the original text but http on the feed - if not img.get('src'): continue - if img.get('src') in image_urls: - image_urls.remove(img.get('src')) - elif img.get('src').replace('https:', 'http:') in image_urls: - image_urls.remove(img.get('src').replace('https:', 'http:')) - + if not img.get("src"): + continue + if img.get("src") in image_urls: + image_urls.remove(img.get("src")) + elif img.get("src").replace("https:", "http:") in image_urls: + image_urls.remove(img.get("src").replace("https:", "http:")) + if len(image_urls): image_content = f'' content = f"{image_content}\n {content}" @@ -218,48 +254,55 @@ def add_hero_image(self, content, image_urls): def rewrite_content(self, content): soup = BeautifulSoup(content, features="lxml") - - for noscript in soup.findAll('noscript'): + + for noscript in soup.findAll("noscript"): if len(noscript.contents) > 0: noscript.replaceWith(noscript.contents[0]) - + content = str(soup) - - images = set([img.attrs['src'] for img in soup.findAll('img') if 'src' in img.attrs]) + + images = set([img.attrs["src"] for img in soup.findAll("img") if "src" in img.attrs]) for image_url in images: abs_image_url = urljoin(self.story_url, image_url) content = content.replace(image_url, abs_image_url) - + return content - + @timelimit(10) def fetch_request(self, use_mercury=True): headers = self.headers url = self.story_url - + if use_mercury: - mercury_api_key = getattr(settings, 'MERCURY_PARSER_API_KEY', 'abc123') + mercury_api_key = getattr(settings, "MERCURY_PARSER_API_KEY", "abc123") headers["content-type"] = "application/json" headers["x-api-key"] = mercury_api_key domain = Site.objects.get_current().domain protocol = "https" if settings.DOCKERBUILD: - domain = 'haproxy' + domain = "haproxy" protocol = "http" url = f"{protocol}://{domain}/rss_feeds/original_text_fetcher?url={url}" - + try: r = requests.get(url, headers=headers, timeout=15) r.connection.close() - except (AttributeError, SocketError, requests.ConnectionError, - requests.models.MissingSchema, requests.sessions.InvalidSchema, - requests.sessions.TooManyRedirects, - requests.models.InvalidURL, - requests.models.ChunkedEncodingError, - requests.models.ContentDecodingError, - requests.adapters.ReadTimeout, - urllib3.exceptions.LocationValueError, - LocationParseError, OpenSSLError, PyAsn1Error) as e: + except ( + AttributeError, + SocketError, + requests.ConnectionError, + requests.models.MissingSchema, + requests.sessions.InvalidSchema, + requests.sessions.TooManyRedirects, + requests.models.InvalidURL, + requests.models.ChunkedEncodingError, + requests.models.ContentDecodingError, + requests.adapters.ReadTimeout, + urllib3.exceptions.LocationValueError, + LocationParseError, + OpenSSLError, + PyAsn1Error, + ) as e: logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % e) return return r diff --git a/apps/rss_feeds/urls.py b/apps/rss_feeds/urls.py index f1d7e1f7fb..30d304f1f7 100644 --- a/apps/rss_feeds/urls.py +++ b/apps/rss_feeds/urls.py @@ -1,20 +1,29 @@ from django.conf.urls import url + from apps.rss_feeds import views urlpatterns = [ - url(r'^feed_autocomplete', views.feed_autocomplete, name='feed-autocomplete'), - url(r'^search_feed', views.search_feed, name='search-feed'), - url(r'^statistics/(?P\d+)', views.load_feed_statistics, name='feed-statistics'), - url(r'^statistics_embedded/(?P\d+)', views.load_feed_statistics_embedded, name='feed-statistics-embedded'), - url(r'^feed_settings/(?P\d+)', views.load_feed_settings, name='feed-settings'), - url(r'^feed/(?P\d+)/?', views.load_single_feed, name='feed-info'), - url(r'^icon/(?P\d+)/?', views.load_feed_favicon, name='feed-favicon'), - url(r'^exception_retry', views.exception_retry, name='exception-retry'), - url(r'^exception_change_feed_address', views.exception_change_feed_address, name='exception-change-feed-address'), - url(r'^exception_change_feed_link', views.exception_change_feed_link, name='exception-change-feed-link'), - url(r'^status', views.status, name='status'), - url(r'^load_single_feed', views.load_single_feed, name='feed-canonical'), - url(r'^original_text', views.original_text, name='original-text'), - url(r'^original_story', views.original_story, name='original-story'), - url(r'^story_changes', views.story_changes, name='story-changes'), + url(r"^feed_autocomplete", views.feed_autocomplete, name="feed-autocomplete"), + url(r"^search_feed", views.search_feed, name="search-feed"), + url(r"^statistics/(?P\d+)", views.load_feed_statistics, name="feed-statistics"), + url( + r"^statistics_embedded/(?P\d+)", + views.load_feed_statistics_embedded, + name="feed-statistics-embedded", + ), + url(r"^feed_settings/(?P\d+)", views.load_feed_settings, name="feed-settings"), + url(r"^feed/(?P\d+)/?", views.load_single_feed, name="feed-info"), + url(r"^icon/(?P\d+)/?", views.load_feed_favicon, name="feed-favicon"), + url(r"^exception_retry", views.exception_retry, name="exception-retry"), + url( + r"^exception_change_feed_address", + views.exception_change_feed_address, + name="exception-change-feed-address", + ), + url(r"^exception_change_feed_link", views.exception_change_feed_link, name="exception-change-feed-link"), + url(r"^status", views.status, name="status"), + url(r"^load_single_feed", views.load_single_feed, name="feed-canonical"), + url(r"^original_text", views.original_text, name="original-text"), + url(r"^original_story", views.original_story, name="original-story"), + url(r"^story_changes", views.story_changes, name="story-changes"), ] diff --git a/apps/rss_feeds/views.py b/apps/rss_feeds/views.py index 91a916b4a1..0a98985a88 100644 --- a/apps/rss_feeds/views.py +++ b/apps/rss_feeds/views.py @@ -1,31 +1,34 @@ -import datetime import base64 -import redis +import datetime from urllib.parse import urlparse -from utils import log as logging -from django.shortcuts import get_object_or_404, render -from django.views.decorators.http import condition -from django.http import HttpResponseForbidden, HttpResponseRedirect, HttpResponse, Http404 + +import redis from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -# from django.db import IntegrityError -from apps.rss_feeds.models import Feed, merge_feeds -from apps.rss_feeds.models import MFetchHistory -from apps.rss_feeds.models import MFeedIcon -from apps.push.models import PushSubscription +from django.http import ( + Http404, + HttpResponse, + HttpResponseForbidden, + HttpResponseRedirect, +) +from django.shortcuts import get_object_or_404, render +from django.views.decorators.http import condition + from apps.analyzer.models import get_classifiers_for_user +from apps.push.models import PushSubscription from apps.reader.models import UserSubscription -from apps.rss_feeds.models import MStory -from utils.user_functions import ajax_login_required -from utils import json_functions as json, feedfinder_forman as feedfinder -from utils.feed_functions import relative_timeuntil, relative_timesince -from utils.user_functions import get_user -from utils.view_functions import get_argument_or_404 -from utils.view_functions import required_params -from utils.view_functions import is_true -from vendor.timezones.utilities import localtime_for_timezone + +# from django.db import IntegrityError +from apps.rss_feeds.models import Feed, MFeedIcon, MFetchHistory, MStory, merge_feeds +from utils import feedfinder_forman as feedfinder +from utils import json_functions as json +from utils import log as logging +from utils.feed_functions import relative_timesince, relative_timeuntil from utils.ratelimit import ratelimit +from utils.user_functions import ajax_login_required, get_user +from utils.view_functions import get_argument_or_404, is_true, required_params +from vendor.timezones.utilities import localtime_for_timezone IGNORE_AUTOCOMPLETE = [ "facebook.com/feeds/notifications.php", @@ -35,16 +38,17 @@ "latitude", ] + @ajax_login_required @json.json_view def search_feed(request): - address = request.GET.get('address') - offset = int(request.GET.get('offset', 0)) + address = request.GET.get("address") + offset = int(request.GET.get("offset", 0)) if not address: return dict(code=-1, message="Please provide a URL/address.") - + logging.user(request.user, "~FBFinding feed (search_feed): %s" % address) - ip = request.META.get('HTTP_X_FORWARDED_FOR', None) or request.META['REMOTE_ADDR'] + ip = request.META.get("HTTP_X_FORWARDED_FOR", None) or request.META["REMOTE_ADDR"] logging.user(request.user, "~FBIP: %s" % ip) aggressive = request.user.is_authenticated feed = Feed.get_feed_from_url(address, create=False, aggressive=aggressive, offset=offset) @@ -52,7 +56,8 @@ def search_feed(request): return feed.canonical() else: return dict(code=-1, message="No feed found matching that XML or website address.") - + + @json.json_view def load_single_feed(request, feed_id): user = get_user(request) @@ -60,18 +65,20 @@ def load_single_feed(request, feed_id): classifiers = get_classifiers_for_user(user, feed_id=feed.pk) payload = feed.canonical(full=True) - payload['classifiers'] = classifiers + payload["classifiers"] = classifiers return payload + def feed_favicon_etag(request, feed_id): try: feed_icon = MFeedIcon.objects.get(feed_id=feed_id) except MFeedIcon.DoesNotExist: return - + return feed_icon.color - + + @condition(etag_func=feed_favicon_etag) def load_feed_favicon(request, feed_id): not_found = False @@ -80,112 +87,126 @@ def load_feed_favicon(request, feed_id): except MFeedIcon.DoesNotExist: logging.user(request, "~FBNo feed icon found: %s" % feed_id) not_found = True - + if not_found or not feed_icon.data: - return HttpResponseRedirect(settings.MEDIA_URL + 'img/icons/nouns/world.svg') - + return HttpResponseRedirect(settings.MEDIA_URL + "img/icons/nouns/world.svg") + icon_data = base64.b64decode(feed_icon.data) - return HttpResponse(icon_data, content_type='image/png') + return HttpResponse(icon_data, content_type="image/png") + @json.json_view def feed_autocomplete(request): - query = request.GET.get('term') or request.GET.get('query') - version = int(request.GET.get('v', 1)) - autocomplete_format = request.GET.get('format', 'autocomplete') - + query = request.GET.get("term") or request.GET.get("query") + version = int(request.GET.get("v", 1)) + autocomplete_format = request.GET.get("format", "autocomplete") + # user = get_user(request) # if True or not user.profile.is_premium: # return dict(code=-1, message="Overloaded, no autocomplete results.", feeds=[], term=query) - + if not query: return dict(code=-1, message="Specify a search 'term'.", feeds=[], term=query) - - if '.' in query: + + if "." in query: try: parts = urlparse(query) - if not parts.hostname and not query.startswith('http'): - parts = urlparse('http://%s' % query) + if not parts.hostname and not query.startswith("http"): + parts = urlparse("http://%s" % query) if parts.hostname: query = [parts.hostname] - query.extend([p for p in parts.path.split('/') if p]) - query = ' '.join(query) + query.extend([p for p in parts.path.split("/") if p]) + query = " ".join(query) except: logging.user(request, "~FGAdd search, could not parse url in ~FR%s" % query) - - query_params = query.split(' ') + + query_params = query.split(" ") tries_left = 5 while len(query_params) and tries_left: tries_left -= 1 - feed_ids = Feed.autocomplete(' '.join(query_params)) + feed_ids = Feed.autocomplete(" ".join(query_params)) if feed_ids: break else: query_params = query_params[:-1] - + feeds = list(set([Feed.get_by_id(feed_id) for feed_id in feed_ids])) feeds = [feed for feed in feeds if feed and not feed.branch_from_feed] feeds = [feed for feed in feeds if all([x not in feed.feed_address for x in IGNORE_AUTOCOMPLETE])] - - if autocomplete_format == 'autocomplete': - feeds = [{ - 'id': feed.pk, - 'value': feed.feed_address, - 'label': feed.feed_title, - 'tagline': feed.data and feed.data.feed_tagline, - 'num_subscribers': feed.num_subscribers, - } for feed in feeds] + + if autocomplete_format == "autocomplete": + feeds = [ + { + "id": feed.pk, + "value": feed.feed_address, + "label": feed.feed_title, + "tagline": feed.data and feed.data.feed_tagline, + "num_subscribers": feed.num_subscribers, + } + for feed in feeds + ] else: feeds = [feed.canonical(full=True) for feed in feeds] - feeds = sorted(feeds, key=lambda f: -1 * f['num_subscribers']) - - feed_ids = [f['id'] for f in feeds] + feeds = sorted(feeds, key=lambda f: -1 * f["num_subscribers"]) + + feed_ids = [f["id"] for f in feeds] feed_icons = dict((icon.feed_id, icon) for icon in MFeedIcon.objects.filter(feed_id__in=feed_ids)) - + for feed in feeds: - if feed['id'] in feed_icons: - feed_icon = feed_icons[feed['id']] + if feed["id"] in feed_icons: + feed_icon = feed_icons[feed["id"]] if feed_icon.data: - feed['favicon_color'] = feed_icon.color - feed['favicon'] = feed_icon.data + feed["favicon_color"] = feed_icon.color + feed["favicon"] = feed_icon.data + + logging.user( + request, + "~FGAdd Search: ~SB%s ~SN(%s matches)" + % ( + query, + len(feeds), + ), + ) - logging.user(request, "~FGAdd Search: ~SB%s ~SN(%s matches)" % (query, len(feeds),)) - if version > 1: return { - 'feeds': feeds, - 'term': query, + "feeds": feeds, + "term": query, } else: return feeds - + + @ratelimit(minutes=1, requests=30) @json.json_view def load_feed_statistics(request, feed_id): user = get_user(request) feed = get_object_or_404(Feed, pk=feed_id) stats = assemble_statistics(user, feed_id) - + logging.user(request, "~FBStatistics: ~SB%s" % (feed)) return stats + def load_feed_statistics_embedded(request, feed_id): user = get_user(request) feed = get_object_or_404(Feed, pk=feed_id) stats = assemble_statistics(user, feed_id) - + logging.user(request, "~FBStatistics (~FCembedded~FB): ~SB%s" % (feed)) - + return render( request, - 'rss_feeds/statistics.xhtml', + "rss_feeds/statistics.xhtml", { - 'stats': json.json_encode(stats), - 'feed_js': json.json_encode(feed.canonical()), - 'feed': feed, - } + "stats": json.json_encode(stats), + "feed_js": json.json_encode(feed.canonical()), + "feed": feed, + }, ) + def assemble_statistics(user, feed_id): user_timezone = user.profile.timezone stats = dict() @@ -194,76 +215,82 @@ def assemble_statistics(user, feed_id): feed.set_next_scheduled_update(verbose=True, skip_scheduling=True) feed.save_feed_story_history_statistics() feed.save_classifier_counts() - + # Dates of last and next update - stats['active'] = feed.active - stats['last_update'] = relative_timesince(feed.last_update) - stats['next_update'] = relative_timeuntil(feed.next_scheduled_update) - stats['push'] = feed.is_push - stats['fs_size_bytes'] = feed.fs_size_bytes - stats['archive_count'] = feed.archive_count + stats["active"] = feed.active + stats["last_update"] = relative_timesince(feed.last_update) + stats["next_update"] = relative_timeuntil(feed.next_scheduled_update) + stats["push"] = feed.is_push + stats["fs_size_bytes"] = feed.fs_size_bytes + stats["archive_count"] = feed.archive_count if feed.is_push: try: - stats['push_expires'] = localtime_for_timezone(feed.push.lease_expires, - user_timezone).strftime("%Y-%m-%d %H:%M:%S") + stats["push_expires"] = localtime_for_timezone(feed.push.lease_expires, user_timezone).strftime( + "%Y-%m-%d %H:%M:%S" + ) except PushSubscription.DoesNotExist: - stats['push_expires'] = 'Missing push' + stats["push_expires"] = "Missing push" feed.is_push = False feed.save() # Minutes between updates update_interval_minutes = feed.get_next_scheduled_update(force=True, verbose=False) - stats['update_interval_minutes'] = update_interval_minutes + stats["update_interval_minutes"] = update_interval_minutes original_active_premium_subscribers = feed.active_premium_subscribers original_premium_subscribers = feed.premium_subscribers - feed.active_premium_subscribers = max(feed.active_premium_subscribers+1, 1) + feed.active_premium_subscribers = max(feed.active_premium_subscribers + 1, 1) feed.premium_subscribers += 1 - premium_update_interval_minutes = feed.get_next_scheduled_update(force=True, verbose=False, - premium_speed=True) + premium_update_interval_minutes = feed.get_next_scheduled_update( + force=True, verbose=False, premium_speed=True + ) feed.active_premium_subscribers = original_active_premium_subscribers feed.premium_subscribers = original_premium_subscribers - stats['premium_update_interval_minutes'] = premium_update_interval_minutes - stats['errors_since_good'] = feed.errors_since_good - + stats["premium_update_interval_minutes"] = premium_update_interval_minutes + stats["errors_since_good"] = feed.errors_since_good + # Stories per month - average and month-by-month breakout - average_stories_per_month, story_count_history = feed.average_stories_per_month, feed.data.story_count_history - stats['average_stories_per_month'] = average_stories_per_month + average_stories_per_month, story_count_history = ( + feed.average_stories_per_month, + feed.data.story_count_history, + ) + stats["average_stories_per_month"] = average_stories_per_month story_count_history = story_count_history and json.decode(story_count_history) if story_count_history and isinstance(story_count_history, dict): - stats['story_count_history'] = story_count_history['months'] - stats['story_days_history'] = story_count_history['days'] - stats['story_hours_history'] = story_count_history['hours'] + stats["story_count_history"] = story_count_history["months"] + stats["story_days_history"] = story_count_history["days"] + stats["story_hours_history"] = story_count_history["hours"] else: - stats['story_count_history'] = story_count_history - + stats["story_count_history"] = story_count_history + # Rotate hours to match user's timezone offset localoffset = user_timezone.utcoffset(datetime.datetime.utcnow()) hours_offset = int(localoffset.total_seconds() / 3600) rotated_hours = {} - for hour, value in list(stats['story_hours_history'].items()): - rotated_hours[str(int(hour)+hours_offset)] = value - stats['story_hours_history'] = rotated_hours - + for hour, value in list(stats["story_hours_history"].items()): + rotated_hours[str(int(hour) + hours_offset)] = value + stats["story_hours_history"] = rotated_hours + # Subscribers - stats['subscriber_count'] = feed.num_subscribers - stats['num_subscribers'] = feed.num_subscribers - stats['stories_last_month'] = feed.stories_last_month - stats['last_load_time'] = feed.last_load_time - stats['premium_subscribers'] = feed.premium_subscribers - stats['active_subscribers'] = feed.active_subscribers - stats['active_premium_subscribers'] = feed.active_premium_subscribers + stats["subscriber_count"] = feed.num_subscribers + stats["num_subscribers"] = feed.num_subscribers + stats["stories_last_month"] = feed.stories_last_month + stats["last_load_time"] = feed.last_load_time + stats["premium_subscribers"] = feed.premium_subscribers + stats["active_subscribers"] = feed.active_subscribers + stats["active_premium_subscribers"] = feed.active_premium_subscribers # Classifier counts - stats['classifier_counts'] = json.decode(feed.data.feed_classifier_counts) - + stats["classifier_counts"] = json.decode(feed.data.feed_classifier_counts) + # Fetch histories fetch_history = MFetchHistory.feed(feed_id, timezone=user_timezone) - stats['feed_fetch_history'] = fetch_history['feed_fetch_history'] - stats['page_fetch_history'] = fetch_history['page_fetch_history'] - stats['feed_push_history'] = fetch_history['push_history'] - + stats["feed_fetch_history"] = fetch_history["feed_fetch_history"] + stats["page_fetch_history"] = fetch_history["page_fetch_history"] + stats["feed_push_history"] = fetch_history["push_history"] + return stats + @json.json_view def load_feed_settings(request, feed_id): stats = dict() @@ -272,25 +299,26 @@ def load_feed_settings(request, feed_id): timezone = user.profile.timezone fetch_history = MFetchHistory.feed(feed_id, timezone=timezone) - stats['feed_fetch_history'] = fetch_history['feed_fetch_history'] - stats['page_fetch_history'] = fetch_history['page_fetch_history'] - stats['feed_push_history'] = fetch_history['push_history'] - stats['duplicate_addresses'] = feed.duplicate_addresses.all() - + stats["feed_fetch_history"] = fetch_history["feed_fetch_history"] + stats["page_fetch_history"] = fetch_history["page_fetch_history"] + stats["feed_push_history"] = fetch_history["push_history"] + stats["duplicate_addresses"] = feed.duplicate_addresses.all() + return stats + @ratelimit(minutes=1, requests=30) @json.json_view def exception_retry(request): user = get_user(request) - feed_id = get_argument_or_404(request, 'feed_id') - reset_fetch = json.decode(request.POST['reset_fetch']) + feed_id = get_argument_or_404(request, "feed_id") + reset_fetch = json.decode(request.POST["reset_fetch"]) feed = Feed.get_by_id(feed_id) original_feed = feed - + if not feed: raise Http404 - + feed.schedule_feed_fetch_immediately() changed = False if feed.has_page_exception: @@ -303,18 +331,18 @@ def exception_retry(request): changed = True feed.active = True if changed: - feed.save(update_fields=['has_page_exception', 'has_feed_exception', 'active']) - + feed.save(update_fields=["has_page_exception", "has_feed_exception", "active"]) + original_fetched_once = feed.fetched_once if reset_fetch: logging.user(request, "~FRRefreshing exception feed: ~SB%s" % (feed)) feed.fetched_once = False else: logging.user(request, "~FRForcing refreshing feed: ~SB%s" % (feed)) - + feed.fetched_once = True if feed.fetched_once != original_fetched_once: - feed.save(update_fields=['fetched_once']) + feed.save(update_fields=["fetched_once"]) feed = feed.update(force=True, compute_scores=False, verbose=True) feed = Feed.get_by_id(feed.pk) @@ -327,26 +355,30 @@ def exception_retry(request): usersub = usersubs[0] usersub.switch_feed(feed, original_feed) else: - return {'code': -1} + return {"code": -1} usersub.calculate_feed_scores(silent=False) - + feeds = {feed.pk: usersub and usersub.canonical(full=True), feed_id: usersub.canonical(full=True)} - return {'code': 1, 'feeds': feeds} - - + return {"code": 1, "feeds": feeds} + + @ajax_login_required @json.json_view def exception_change_feed_address(request): - feed_id = request.POST['feed_id'] + feed_id = request.POST["feed_id"] feed = get_object_or_404(Feed, pk=feed_id) original_feed = feed - feed_address = request.POST['feed_address'] + feed_address = request.POST["feed_address"] timezone = request.user.profile.timezone code = -1 if False and (feed.has_page_exception or feed.has_feed_exception): # Fix broken feed - logging.user(request, "~FRFixing feed exception by address: %s - ~SB%s~SN to ~SB%s" % (feed, feed.feed_address, feed_address)) + logging.user( + request, + "~FRFixing feed exception by address: %s - ~SB%s~SN to ~SB%s" + % (feed, feed.feed_address, feed_address), + ) feed.has_feed_exception = False feed.active = True feed.fetched_once = False @@ -364,9 +396,13 @@ def exception_change_feed_address(request): merge_feeds(new_feed.pk, feed.pk) else: # Branch good feed - logging.user(request, "~FRBranching feed by address: ~SB%s~SN to ~SB%s" % (feed.feed_address, feed_address)) + logging.user( + request, "~FRBranching feed by address: ~SB%s~SN to ~SB%s" % (feed.feed_address, feed_address) + ) try: - feed = Feed.objects.get(hash_address_and_link=Feed.generate_hash_address_and_link(feed_address, feed.feed_link)) + feed = Feed.objects.get( + hash_address_and_link=Feed.generate_hash_address_and_link(feed_address, feed.feed_link) + ) except Feed.DoesNotExist: feed = Feed.objects.create(feed_address=feed_address, feed_link=feed.feed_link) code = 1 @@ -390,47 +426,50 @@ def exception_change_feed_address(request): else: fetch_history = MFetchHistory.feed(feed_id, timezone=timezone) return { - 'code': -1, - 'feed_fetch_history': fetch_history['feed_fetch_history'], - 'page_fetch_history': fetch_history['page_fetch_history'], - 'push_history': fetch_history['push_history'], + "code": -1, + "feed_fetch_history": fetch_history["feed_fetch_history"], + "page_fetch_history": fetch_history["page_fetch_history"], + "push_history": fetch_history["push_history"], } usersub.calculate_feed_scores(silent=False) - + feed.update_all_statistics() classifiers = get_classifiers_for_user(usersub.user, feed_id=usersub.feed_id) - + feeds = { - original_feed.pk: usersub and usersub.canonical(full=True, classifiers=classifiers), + original_feed.pk: usersub and usersub.canonical(full=True, classifiers=classifiers), } - + if feed and feed.has_feed_exception: code = -1 fetch_history = MFetchHistory.feed(feed_id, timezone=timezone) return { - 'code': code, - 'feeds': feeds, - 'new_feed_id': usersub.feed_id, - 'feed_fetch_history': fetch_history['feed_fetch_history'], - 'page_fetch_history': fetch_history['page_fetch_history'], - 'push_history': fetch_history['push_history'], + "code": code, + "feeds": feeds, + "new_feed_id": usersub.feed_id, + "feed_fetch_history": fetch_history["feed_fetch_history"], + "page_fetch_history": fetch_history["page_fetch_history"], + "push_history": fetch_history["push_history"], } - + + @ajax_login_required @json.json_view def exception_change_feed_link(request): - feed_id = request.POST['feed_id'] + feed_id = request.POST["feed_id"] feed = get_object_or_404(Feed, pk=feed_id) original_feed = feed - feed_link = request.POST['feed_link'] + feed_link = request.POST["feed_link"] timezone = request.user.profile.timezone code = -1 - + if False and (feed.has_page_exception or feed.has_feed_exception): # Fix broken feed - logging.user(request, "~FRFixing feed exception by link: ~SB%s~SN to ~SB%s" % (feed.feed_link, feed_link)) + logging.user( + request, "~FRFixing feed exception by link: ~SB%s~SN to ~SB%s" % (feed.feed_link, feed_link) + ) found_feed_urls = feedfinder.find_feeds(feed_link) if len(found_feed_urls): code = 1 @@ -451,7 +490,9 @@ def exception_change_feed_link(request): # Branch good feed logging.user(request, "~FRBranching feed by link: ~SB%s~SN to ~SB%s" % (feed.feed_link, feed_link)) try: - feed = Feed.objects.get(hash_address_and_link=Feed.generate_hash_address_and_link(feed.feed_address, feed_link)) + feed = Feed.objects.get( + hash_address_and_link=Feed.generate_hash_address_and_link(feed.feed_address, feed_link) + ) except Feed.DoesNotExist: feed = Feed.objects.create(feed_address=feed.feed_address, feed_link=feed_link) code = 1 @@ -476,81 +517,82 @@ def exception_change_feed_link(request): else: fetch_history = MFetchHistory.feed(feed_id, timezone=timezone) return { - 'code': -1, - 'feed_fetch_history': fetch_history['feed_fetch_history'], - 'page_fetch_history': fetch_history['page_fetch_history'], - 'push_history': fetch_history['push_history'], + "code": -1, + "feed_fetch_history": fetch_history["feed_fetch_history"], + "page_fetch_history": fetch_history["page_fetch_history"], + "push_history": fetch_history["push_history"], } - + usersub.calculate_feed_scores(silent=False) - + feed.update_all_statistics() classifiers = get_classifiers_for_user(usersub.user, feed_id=usersub.feed_id) - + if feed and feed.has_feed_exception: code = -1 - + feeds = { - original_feed.pk: usersub.canonical(full=True, classifiers=classifiers), + original_feed.pk: usersub.canonical(full=True, classifiers=classifiers), } fetch_history = MFetchHistory.feed(feed_id, timezone=timezone) return { - 'code': code, - 'feeds': feeds, - 'new_feed_id': usersub.feed_id, - 'feed_fetch_history': fetch_history['feed_fetch_history'], - 'page_fetch_history': fetch_history['page_fetch_history'], - 'push_history': fetch_history['push_history'], + "code": code, + "feeds": feeds, + "new_feed_id": usersub.feed_id, + "feed_fetch_history": fetch_history["feed_fetch_history"], + "page_fetch_history": fetch_history["page_fetch_history"], + "push_history": fetch_history["push_history"], } + @login_required def status(request): if not request.user.is_staff and not settings.DEBUG: logging.user(request, "~SKNON-STAFF VIEWING RSS FEEDS STATUS!") assert False return HttpResponseForbidden() - minutes = int(request.GET.get('minutes', 1)) - now = datetime.datetime.now() + minutes = int(request.GET.get("minutes", 1)) + now = datetime.datetime.now() hour_ago = now + datetime.timedelta(minutes=minutes) - username = request.GET.get('user', '') or request.GET.get('username', '') + username = request.GET.get("user", "") or request.GET.get("username", "") if username == "all": - feeds = Feed.objects.filter(next_scheduled_update__lte=hour_ago).order_by('next_scheduled_update') + feeds = Feed.objects.filter(next_scheduled_update__lte=hour_ago).order_by("next_scheduled_update") else: if username: user = User.objects.get(username=username) else: user = request.user usersubs = UserSubscription.objects.filter(user=user) - feed_ids = usersubs.values('feed_id') + feed_ids = usersubs.values("feed_id") if minutes > 0: - feeds = Feed.objects.filter(pk__in=feed_ids, next_scheduled_update__lte=hour_ago).order_by('next_scheduled_update') + feeds = Feed.objects.filter(pk__in=feed_ids, next_scheduled_update__lte=hour_ago).order_by( + "next_scheduled_update" + ) else: - feeds = Feed.objects.filter(pk__in=feed_ids, last_update__gte=hour_ago).order_by('-last_update') - + feeds = Feed.objects.filter(pk__in=feed_ids, last_update__gte=hour_ago).order_by("-last_update") + r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) queues = { - 'tasked_feeds': r.zcard('tasked_feeds'), - 'queued_feeds': r.scard('queued_feeds'), - 'scheduled_updates': r.zcard('scheduled_updates'), + "tasked_feeds": r.zcard("tasked_feeds"), + "queued_feeds": r.scard("queued_feeds"), + "scheduled_updates": r.zcard("scheduled_updates"), } - return render(request, 'rss_feeds/status.xhtml', { - 'feeds': feeds, - 'queues': queues - }) + return render(request, "rss_feeds/status.xhtml", {"feeds": feeds, "queues": queues}) + @json.json_view def original_text(request): # iOS sends a POST, web sends a GET GET_POST = getattr(request, request.method) - story_id = GET_POST.get('story_id') - feed_id = GET_POST.get('feed_id') - story_hash = GET_POST.get('story_hash', None) - force = GET_POST.get('force', False) - debug = GET_POST.get('debug', False) + story_id = GET_POST.get("story_id") + feed_id = GET_POST.get("feed_id") + story_hash = GET_POST.get("story_hash", None) + force = GET_POST.get("force", False) + debug = GET_POST.get("debug", False) if not story_hash and not story_id: - return {'code': -1, 'message': 'Missing story_hash.', 'original_text': None, 'failed': True} - + return {"code": -1, "message": "Missing story_hash.", "original_text": None, "failed": True} + if story_hash: story, _ = MStory.find_story(story_hash=story_hash) else: @@ -558,25 +600,26 @@ def original_text(request): if not story: logging.user(request, "~FYFetching ~FGoriginal~FY story text: ~FRstory not found") - return {'code': -1, 'message': 'Story not found.', 'original_text': None, 'failed': True} - + return {"code": -1, "message": "Story not found.", "original_text": None, "failed": True} + original_text = story.fetch_original_text(force=force, request=request, debug=debug) return { - 'feed_id': story.story_feed_id, - 'story_hash': story.story_hash, - 'story_id': story.story_guid, - 'image_urls': story.image_urls, - 'secure_image_urls': Feed.secure_image_urls(story.image_urls), - 'original_text': original_text, - 'failed': not original_text or len(original_text) < 100, + "feed_id": story.story_feed_id, + "story_hash": story.story_hash, + "story_id": story.story_guid, + "image_urls": story.image_urls, + "secure_image_urls": Feed.secure_image_urls(story.image_urls), + "original_text": original_text, + "failed": not original_text or len(original_text) < 100, } -@required_params('story_hash', method="GET") + +@required_params("story_hash", method="GET") def original_story(request): - story_hash = request.GET.get('story_hash') - force = request.GET.get('force', False) - debug = request.GET.get('debug', False) + story_hash = request.GET.get("story_hash") + force = request.GET.get("force", False) + debug = request.GET.get("debug", False) story, _ = MStory.find_story(story_hash=story_hash) @@ -584,22 +627,20 @@ def original_story(request): logging.user(request, "~FYFetching ~FGoriginal~FY story page: ~FRstory not found") # return {'code': -1, 'message': 'Story not found.', 'original_page': None, 'failed': True} raise Http404 - + original_page = story.fetch_original_page(force=force, request=request, debug=debug) return HttpResponse(original_page or "") -@required_params('story_hash', method="GET") + +@required_params("story_hash", method="GET") @json.json_view def story_changes(request): - story_hash = request.GET.get('story_hash', None) - show_changes = is_true(request.GET.get('show_changes', True)) + story_hash = request.GET.get("story_hash", None) + show_changes = is_true(request.GET.get("show_changes", True)) story, _ = MStory.find_story(story_hash=story_hash) if not story: logging.user(request, "~FYFetching ~FGoriginal~FY story page: ~FRstory not found") - return {'code': -1, 'message': 'Story not found.', 'original_page': None, 'failed': True} - - return { - 'story': Feed.format_story(story, show_changes=show_changes) - } - \ No newline at end of file + return {"code": -1, "message": "Story not found.", "original_page": None, "failed": True} + + return {"story": Feed.format_story(story, show_changes=show_changes)} diff --git a/apps/search/management/commands/index_feeds.py b/apps/search/management/commands/index_feeds.py index c3e2ee37dc..028d62abac 100644 --- a/apps/search/management/commands/index_feeds.py +++ b/apps/search/management/commands/index_feeds.py @@ -1,14 +1,23 @@ from django.core.management.base import BaseCommand + from apps.rss_feeds.models import Feed -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument("-o", "--offset", dest="offset", type=int, default=0, help="Specify offset to start at") - parser.add_argument("-s", "--subscribers", dest="subscribers", type=int, default=2, help="Specify minimum number of subscribers") + parser.add_argument( + "-o", "--offset", dest="offset", type=int, default=0, help="Specify offset to start at" + ) + parser.add_argument( + "-s", + "--subscribers", + dest="subscribers", + type=int, + default=2, + help="Specify minimum number of subscribers", + ) def handle(self, *args, **options): - offset = options['offset'] - subscribers = options.get('subscribers', None) + offset = options["offset"] + subscribers = options.get("subscribers", None) Feed.index_all_for_search(offset=offset, subscribers=subscribers) - \ No newline at end of file diff --git a/apps/search/management/commands/index_stories.py b/apps/search/management/commands/index_stories.py index b63faa98c4..61b24c117d 100644 --- a/apps/search/management/commands/index_stories.py +++ b/apps/search/management/commands/index_stories.py @@ -1,36 +1,38 @@ import re -from django.core.management.base import BaseCommand + from django.contrib.auth.models import User -from apps.rss_feeds.models import Feed, MStory +from django.core.management.base import BaseCommand + from apps.reader.models import UserSubscription +from apps.rss_feeds.models import Feed, MStory -class Command(BaseCommand): +class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-u", "--user", dest="user", type=str, help="Specify user id or username") - parser.add_argument("-R", "--reindex", dest="reindex", action="store_true", help="Drop index and reindex all stories.") - + parser.add_argument( + "-R", "--reindex", dest="reindex", action="store_true", help="Drop index and reindex all stories." + ) def handle(self, *args, **options): - if options['reindex']: + if options["reindex"]: MStory.index_all_for_search() return - - if not options['user']: + + if not options["user"]: print("Missing user. Did you want to reindex everything? Use -R.") return - - if re.match(r"([0-9]+)", options['user']): - user = User.objects.get(pk=int(options['user'])) + + if re.match(r"([0-9]+)", options["user"]): + user = User.objects.get(pk=int(options["user"])) else: - user = User.objects.get(username=options['user']) - + user = User.objects.get(username=options["user"]) + subscriptions = UserSubscription.objects.filter(user=user) print(" ---> Indexing %s feeds..." % subscriptions.count()) - + for sub in subscriptions: try: sub.feed.index_stories_for_search() except Feed.DoesNotExist: print(" ***> Couldn't find %s" % sub.feed_id) - \ No newline at end of file diff --git a/apps/search/models.py b/apps/search/models.py index d4a6b3fe5b..949c675420 100644 --- a/apps/search/models.py +++ b/apps/search/models.py @@ -1,48 +1,53 @@ +import datetime +import html import re import time -import datetime -import pymongo + +import celery import elasticsearch +import mongoengine as mongo +import pymongo import redis import urllib3 -import celery -import html -import mongoengine as mongo from django.conf import settings from django.contrib.auth.models import User -from apps.search.tasks import IndexSubscriptionsForSearch -from apps.search.tasks import FinishIndexSubscriptionsForSearch -from apps.search.tasks import IndexSubscriptionsChunkForSearch -from apps.search.tasks import IndexFeedsForSearch + +from apps.search.tasks import ( + FinishIndexSubscriptionsForSearch, + IndexFeedsForSearch, + IndexSubscriptionsChunkForSearch, + IndexSubscriptionsForSearch, +) from utils import log as logging from utils.feed_functions import chunks + class MUserSearch(mongo.Document): - '''Search index state of a user's subscriptions.''' - user_id = mongo.IntField(unique=True) - last_search_date = mongo.DateTimeField() - subscriptions_indexed = mongo.BooleanField() - subscriptions_indexing = mongo.BooleanField() - + """Search index state of a user's subscriptions.""" + + user_id = mongo.IntField(unique=True) + last_search_date = mongo.DateTimeField() + subscriptions_indexed = mongo.BooleanField() + subscriptions_indexing = mongo.BooleanField() + meta = { - 'collection': 'user_search', - 'indexes': ['user_id'], - 'allow_inheritance': False, + "collection": "user_search", + "indexes": ["user_id"], + "allow_inheritance": False, } - + @classmethod def get_user(cls, user_id, create=True): try: - user_search = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY)\ - .get(user_id=user_id) + user_search = cls.objects.read_preference(pymongo.ReadPreference.PRIMARY).get(user_id=user_id) except cls.DoesNotExist: if create: user_search = cls.objects.create(user_id=user_id) else: user_search = None - + return user_search - + def touch_search_date(self): if not self.subscriptions_indexed and not self.subscriptions_indexing: self.schedule_index_subscriptions_for_search() @@ -52,62 +57,63 @@ def touch_search_date(self): self.save() def schedule_index_subscriptions_for_search(self): - IndexSubscriptionsForSearch.apply_async(kwargs=dict(user_id=self.user_id), - queue='search_indexer') - + IndexSubscriptionsForSearch.apply_async(kwargs=dict(user_id=self.user_id), queue="search_indexer") + # Should be run as a background task def index_subscriptions_for_search(self): - from apps.rss_feeds.models import Feed from apps.reader.models import UserSubscription - + from apps.rss_feeds.models import Feed + SearchStory.create_elasticsearch_mapping() - + start = time.time() user = User.objects.get(pk=self.user_id) r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish(user.username, 'search_index_complete:start') - - subscriptions = UserSubscription.objects.filter(user=user).only('feed') + r.publish(user.username, "search_index_complete:start") + + subscriptions = UserSubscription.objects.filter(user=user).only("feed") total = subscriptions.count() - + feed_ids = [] for sub in subscriptions: try: feed_ids.append(sub.feed.pk) except Feed.DoesNotExist: continue - + feed_id_chunks = [c for c in chunks(feed_ids, 6)] - logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." % - (total, len(feed_id_chunks))) - - search_chunks = [IndexSubscriptionsChunkForSearch.s(feed_ids=feed_id_chunk, - user_id=self.user_id - ).set(queue='search_indexer') - for feed_id_chunk in feed_id_chunks] - callback = FinishIndexSubscriptionsForSearch.s(user_id=self.user_id, - start=start).set(queue='search_indexer') + logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." % (total, len(feed_id_chunks))) + + search_chunks = [ + IndexSubscriptionsChunkForSearch.s(feed_ids=feed_id_chunk, user_id=self.user_id).set( + queue="search_indexer" + ) + for feed_id_chunk in feed_id_chunks + ] + callback = FinishIndexSubscriptionsForSearch.s(user_id=self.user_id, start=start).set( + queue="search_indexer" + ) celery.chord(search_chunks)(callback) def finish_index_subscriptions_for_search(self, start): from apps.reader.models import UserSubscription - + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) user = User.objects.get(pk=self.user_id) - subscriptions = UserSubscription.objects.filter(user=user).only('feed') + subscriptions = UserSubscription.objects.filter(user=user).only("feed") total = subscriptions.count() duration = time.time() - start - logging.user(user, "~FCIndexed ~SB%s feeds~SN in ~FM~SB%s~FC~SN sec." % - (total, round(duration, 2))) - r.publish(user.username, 'search_index_complete:done') - + logging.user(user, "~FCIndexed ~SB%s feeds~SN in ~FM~SB%s~FC~SN sec." % (total, round(duration, 2))) + r.publish(user.username, "search_index_complete:done") + self.subscriptions_indexed = True self.subscriptions_indexing = False self.save() - + def index_subscriptions_chunk_for_search(self, feed_ids): from apps.rss_feeds.models import Feed + r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) user = User.objects.get(pk=self.user_id) @@ -115,40 +121,41 @@ def index_subscriptions_chunk_for_search(self, feed_ids): for feed_id in feed_ids: feed = Feed.get_by_id(feed_id) - if not feed: continue - + if not feed: + continue + feed.index_stories_for_search() - - r.publish(user.username, 'search_index_complete:feeds:%s' % - ','.join([str(f) for f in feed_ids])) - + + r.publish(user.username, "search_index_complete:feeds:%s" % ",".join([str(f) for f in feed_ids])) + @classmethod def schedule_index_feeds_for_search(cls, feed_ids, user_id): user_search = cls.get_user(user_id, create=False) - if (not user_search or - not user_search.subscriptions_indexed or - user_search.subscriptions_indexing): + if not user_search or not user_search.subscriptions_indexed or user_search.subscriptions_indexing: # User hasn't searched before. return - + if not isinstance(feed_ids, list): feed_ids = [feed_ids] - IndexFeedsForSearch.apply_async(kwargs=dict(feed_ids=feed_ids, user_id=user_id), - queue='search_indexer') - + IndexFeedsForSearch.apply_async( + kwargs=dict(feed_ids=feed_ids, user_id=user_id), queue="search_indexer" + ) + @classmethod def index_feeds_for_search(cls, feed_ids, user_id): from apps.rss_feeds.models import Feed + user = User.objects.get(pk=user_id) logging.user(user, "~SB~FCIndexing %s~FC by request..." % feed_ids) for feed_id in feed_ids: feed = Feed.get_by_id(feed_id) - if not feed: continue - + if not feed: + continue + feed.index_stories_for_search() - + @classmethod def remove_all(cls, drop_index=False): # You only need to drop the index if there is data you want to clear. @@ -156,7 +163,7 @@ def remove_all(cls, drop_index=False): if drop_index: logging.info(" ---> ~FRRemoving stories search index...") SearchStory.drop() - + user_searches = cls.objects.all() logging.info(" ---> ~SN~FRRemoving ~SB%s~SN user searches..." % user_searches.count()) for user_search in user_searches: @@ -164,16 +171,16 @@ def remove_all(cls, drop_index=False): user_search.remove() except Exception as e: print(" ****> Error on search removal: %s" % e) - + def remove(self): - from apps.rss_feeds.models import Feed from apps.reader.models import UserSubscription + from apps.rss_feeds.models import Feed user = User.objects.get(pk=self.user_id) subscriptions = UserSubscription.objects.filter(user=self.user_id) total = subscriptions.count() removed = 0 - + for sub in subscriptions: try: feed = sub.feed @@ -184,33 +191,36 @@ def remove(self): feed.search_indexed = False feed.save() removed += 1 - - logging.user(user, "~FCRemoved ~SB%s/%s feed's search indexes~SN for ~SB~FB%s~FC~SN." % - (removed, total, user.username)) + + logging.user( + user, + "~FCRemoved ~SB%s/%s feed's search indexes~SN for ~SB~FB%s~FC~SN." + % (removed, total, user.username), + ) self.delete() + class SearchStory: - _es_client = None name = "stories" - + @classmethod def ES(cls): if cls._es_client is None: cls._es_client = elasticsearch.Elasticsearch(settings.ELASTICSEARCH_STORY_HOST) cls.create_elasticsearch_mapping() return cls._es_client - + @classmethod def index_name(cls): return "%s-index" % cls.name - + @classmethod def doc_type(cls): - if settings.DOCKERBUILD or getattr(settings, 'ES_IGNORE_TYPE', True): + if settings.DOCKERBUILD or getattr(settings, "ES_IGNORE_TYPE", True): return None return "%s-type" % cls.name - + @classmethod def create_elasticsearch_mapping(cls, delete=False): if delete: @@ -222,83 +232,76 @@ def create_elasticsearch_mapping(cls, delete=False): if cls.ES().indices.exists(cls.index_name()): return - + try: cls.ES().indices.create(cls.index_name()) logging.debug(" ---> ~FCCreating search index for ~FM%s" % cls.index_name()) except elasticsearch.exceptions.RequestError as e: logging.debug(" ***> ~FRCould not create search index for ~FM%s: %s" % (cls.index_name(), e)) return - except (elasticsearch.exceptions.ConnectionError, - urllib3.exceptions.NewConnectionError, - urllib3.exceptions.ConnectTimeoutError) as e: - logging.debug( - f" ***> ~FRNo search server available for creating story mapping: {e}") + except ( + elasticsearch.exceptions.ConnectionError, + urllib3.exceptions.NewConnectionError, + urllib3.exceptions.ConnectTimeoutError, + ) as e: + logging.debug(f" ***> ~FRNo search server available for creating story mapping: {e}") return mapping = { - 'title': { - 'store': False, - 'type': 'text', - 'analyzer': 'snowball', + "title": { + "store": False, + "type": "text", + "analyzer": "snowball", "term_vector": "yes", }, - 'content': { - 'store': False, - 'type': 'text', - 'analyzer': 'snowball', + "content": { + "store": False, + "type": "text", + "analyzer": "snowball", "term_vector": "yes", }, - 'tags': { - 'store': False, + "tags": { + "store": False, "type": "text", - "fields": { - "raw": { - "type": "text", - "analyzer": "keyword", - "term_vector": "yes" - } - } + "fields": {"raw": {"type": "text", "analyzer": "keyword", "term_vector": "yes"}}, }, - 'author': { - 'store': False, - 'type': 'text', - 'analyzer': 'default', + "author": { + "store": False, + "type": "text", + "analyzer": "default", }, - 'feed_id': { - 'store': False, - 'type': 'integer' + "feed_id": {"store": False, "type": "integer"}, + "date": { + "store": False, + "type": "date", }, - 'date': { - 'store': False, - 'type': 'date', - } } - cls.ES().indices.put_mapping(body={ - 'properties': mapping, - }, index=cls.index_name()) + cls.ES().indices.put_mapping( + body={ + "properties": mapping, + }, + index=cls.index_name(), + ) cls.ES().indices.flush(cls.index_name()) @classmethod - def index(cls, story_hash, story_title, story_content, story_tags, story_author, story_feed_id, - story_date): + def index( + cls, story_hash, story_title, story_content, story_tags, story_author, story_feed_id, story_date + ): cls.create_elasticsearch_mapping() doc = { "content": story_content, "title": story_title, - "tags": ', '.join(story_tags), + "tags": ", ".join(story_tags), "author": story_author, "feed_id": story_feed_id, "date": story_date, } try: - cls.ES().create(index=cls.index_name(), id=story_hash, - body=doc, doc_type=cls.doc_type()) - except (elasticsearch.exceptions.ConnectionError, - urllib3.exceptions.NewConnectionError) as e: - logging.debug( - f" ***> ~FRNo search server available for story indexing: {e}") + cls.ES().create(index=cls.index_name(), id=story_hash, body=doc, doc_type=cls.doc_type()) + except (elasticsearch.exceptions.ConnectionError, urllib3.exceptions.NewConnectionError) as e: + logging.debug(f" ***> ~FRNo search server available for story indexing: {e}") except elasticsearch.exceptions.ConflictError as e: logging.debug(f" ***> ~FBAlready indexed story: {e}") # if settings.DEBUG: @@ -312,10 +315,10 @@ def remove(cls, story_hash): try: cls.ES().delete(index=cls.index_name(), id=story_hash, doc_type=cls.doc_type()) except elasticsearch.exceptions.NotFoundError: - cls.ES().delete(index=cls.index_name(), id=story_hash, doc_type='story-type') + cls.ES().delete(index=cls.index_name(), id=story_hash, doc_type="story-type") except elasticsearch.exceptions.NotFoundError as e: logging.debug(f" ***> ~FRNo search server available for story deletion: {e}") - + @classmethod def drop(cls): try: @@ -323,7 +326,6 @@ def drop(cls): except elasticsearch.exceptions.NotFoundError: logging.debug(" ***> ~FBNo index found, nothing to drop.") - @classmethod def query(cls, feed_ids, query, order, offset, limit, strip=False): try: @@ -331,26 +333,26 @@ def query(cls, feed_ids, query, order, offset, limit, strip=False): except elasticsearch.exceptions.NotFoundError as e: logging.debug(f" ***> ~FRNo search server available: {e}") return [] - + if strip: - query = re.sub(r'([^\s\w_\-])+', ' ', query) # Strip non-alphanumeric + query = re.sub(r"([^\s\w_\-])+", " ", query) # Strip non-alphanumeric query = html.unescape(query) body = { "query": { "bool": { "must": [ - {"query_string": { "query": query, "default_operator": "AND" }}, - {"terms": { "feed_id": feed_ids[:2000] }}, + {"query_string": {"query": query, "default_operator": "AND"}}, + {"terms": {"feed_id": feed_ids[:2000]}}, ] } }, - 'sort': [{'date': {'order': 'desc' if order == "newest" else "asc"}}], - 'from': offset, - 'size': limit + "sort": [{"date": {"order": "desc" if order == "newest" else "asc"}}], + "from": offset, + "size": limit, } try: - results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) + results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) except elasticsearch.exceptions.RequestError as e: logging.debug(" ***> ~FRNo search server available for querying: %s" % e) return [] @@ -373,44 +375,46 @@ def query(cls, feed_ids, query, order, offset, limit, strip=False): # logging.debug(" ***> ~FRNo search server available.") # return [] - logging.info(" ---> ~FG~SNSearch ~FCstories~FG for: ~SB%s~SN, ~SB%s~SN results (across %s feed%s)" % - (query, len(results['hits']['hits']), len(feed_ids), 's' if len(feed_ids) != 1 else '')) - + logging.info( + " ---> ~FG~SNSearch ~FCstories~FG for: ~SB%s~SN, ~SB%s~SN results (across %s feed%s)" + % (query, len(results["hits"]["hits"]), len(feed_ids), "s" if len(feed_ids) != 1 else "") + ) + try: - result_ids = [r['_id'] for r in results['hits']['hits']] + result_ids = [r["_id"] for r in results["hits"]["hits"]] except Exception as e: - logging.info(" ---> ~FRInvalid search query \"%s\": %s" % (query, e)) + logging.info(' ---> ~FRInvalid search query "%s": %s' % (query, e)) return [] - + return result_ids - + @classmethod def global_query(cls, query, order, offset, limit, strip=False): cls.create_elasticsearch_mapping() cls.ES().indices.flush() - + if strip: - query = re.sub(r'([^\s\w_\-])+', ' ', query) # Strip non-alphanumeric + query = re.sub(r"([^\s\w_\-])+", " ", query) # Strip non-alphanumeric query = html.unescape(query) body = { "query": { "bool": { "must": [ - {"query_string": { "query": query, "default_operator": "AND" }}, + {"query_string": {"query": query, "default_operator": "AND"}}, ] } }, - 'sort': [{'date': {'order': 'desc' if order == "newest" else "asc"}}], - 'from': offset, - 'size': limit + "sort": [{"date": {"order": "desc" if order == "newest" else "asc"}}], + "from": offset, + "size": limit, } try: - results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) + results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) except elasticsearch.exceptions.RequestError as e: logging.debug(" ***> ~FRNo search server available for querying: %s" % e) return [] - + # sort = "date:desc" if order == "newest" else "date:asc" # string_q = pyes.query.QueryStringQuery(query, default_operator="AND") # try: @@ -420,17 +424,16 @@ def global_query(cls, query, order, offset, limit, strip=False): # logging.debug(" ***> ~FRNo search server available.") # return [] - logging.info(" ---> ~FG~SNSearch ~FCstories~FG for: ~SB%s~SN (across all feeds)" % - (query)) - + logging.info(" ---> ~FG~SNSearch ~FCstories~FG for: ~SB%s~SN (across all feeds)" % (query)) + try: - result_ids = [r['_id'] for r in results['hits']['hits']] + result_ids = [r["_id"] for r in results["hits"]["hits"]] except Exception as e: - logging.info(" ---> ~FRInvalid search query \"%s\": %s" % (query, e)) + logging.info(' ---> ~FRInvalid search query "%s": %s' % (query, e)) return [] - + return result_ids - + @classmethod def more_like_this(cls, feed_ids, story_hash, order, offset, limit): try: @@ -438,52 +441,54 @@ def more_like_this(cls, feed_ids, story_hash, order, offset, limit): except elasticsearch.exceptions.NotFoundError as e: logging.debug(f" ***> ~FRNo search server available: {e}") return [] - + body = { "query": { "bool": { - "filter": [{ - "more_like_this": { - "fields": [ "title", "content" ], - "like": [ - { - "_index": cls.index_name(), - "_id": story_hash, - } - ], - "min_term_freq": 3, - "min_doc_freq": 2, - "min_word_length": 4, + "filter": [ + { + "more_like_this": { + "fields": ["title", "content"], + "like": [ + { + "_index": cls.index_name(), + "_id": story_hash, + } + ], + "min_term_freq": 3, + "min_doc_freq": 2, + "min_word_length": 4, + }, }, - },{ - "terms": { "feed_id": feed_ids[:2000] } - }], + {"terms": {"feed_id": feed_ids[:2000]}}, + ], } }, - 'sort': [{'date': {'order': 'desc' if order == "newest" else "asc"}}], - 'from': offset, - 'size': limit + "sort": [{"date": {"order": "desc" if order == "newest" else "asc"}}], + "from": offset, + "size": limit, } try: - results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) + results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) except elasticsearch.exceptions.RequestError as e: logging.debug(" ***> ~FRNo search server available for querying: %s" % e) return [] - logging.info(" ---> ~FG~SNMore like this ~FCstories~FG for: ~SB%s~SN, ~SB%s~SN results (across %s feed%s)" % - (story_hash, len(results['hits']['hits']), len(feed_ids), 's' if len(feed_ids) != 1 else '')) - + logging.info( + " ---> ~FG~SNMore like this ~FCstories~FG for: ~SB%s~SN, ~SB%s~SN results (across %s feed%s)" + % (story_hash, len(results["hits"]["hits"]), len(feed_ids), "s" if len(feed_ids) != 1 else "") + ) + try: - result_ids = [r['_id'] for r in results['hits']['hits']] + result_ids = [r["_id"] for r in results["hits"]["hits"]] except Exception as e: - logging.info(" ---> ~FRInvalid search query \"%s\": %s" % (query, e)) + logging.info(' ---> ~FRInvalid search query "%s": %s' % (query, e)) return [] - + return result_ids class SearchFeed: - _es_client = None name = "feeds" @@ -493,18 +498,18 @@ def ES(cls): cls._es_client = elasticsearch.Elasticsearch(settings.ELASTICSEARCH_FEED_HOST) cls.create_elasticsearch_mapping() return cls._es_client - + @classmethod def index_name(cls): # feeds-index return "%s-index" % cls.name - + @classmethod def doc_type(cls): - if settings.DOCKERBUILD or getattr(settings, 'ES_IGNORE_TYPE', True): + if settings.DOCKERBUILD or getattr(settings, "ES_IGNORE_TYPE", True): return None return "%s-type" % cls.name - + @classmethod def create_elasticsearch_mapping(cls, delete=False): if delete: @@ -518,22 +523,18 @@ def create_elasticsearch_mapping(cls, delete=False): return index_settings = { - "index" : { + "index": { "analysis": { "analyzer": { "edgengram_analyzer": { "filter": ["edgengram_analyzer"], "tokenizer": "lowercase", - "type": "custom" + "type": "custom", }, }, "filter": { - "edgengram_analyzer": { - "max_gram": "15", - "min_gram": "1", - "type": "edge_ngram" - }, - } + "edgengram_analyzer": {"max_gram": "15", "min_gram": "1", "type": "edge_ngram"}, + }, } } } @@ -544,43 +545,42 @@ def create_elasticsearch_mapping(cls, delete=False): except elasticsearch.exceptions.RequestError as e: logging.debug(" ***> ~FRCould not create search index for ~FM%s: %s" % (cls.index_name(), e)) return - except (elasticsearch.exceptions.ConnectionError, - urllib3.exceptions.NewConnectionError, - urllib3.exceptions.ConnectTimeoutError) as e: + except ( + elasticsearch.exceptions.ConnectionError, + urllib3.exceptions.NewConnectionError, + urllib3.exceptions.ConnectTimeoutError, + ) as e: logging.debug(f" ***> ~FRNo search server available for creating feed mapping: {e}") return - + mapping = { "feed_address": { - 'analyzer': 'snowball', + "analyzer": "snowball", "store": False, "term_vector": "with_positions_offsets", - "type": "text" - }, - "feed_id": { - "store": True, - "type": "text" - }, - "num_subscribers": { - "store": True, - "type": "long" + "type": "text", }, + "feed_id": {"store": True, "type": "text"}, + "num_subscribers": {"store": True, "type": "long"}, "title": { "analyzer": "snowball", "store": False, "term_vector": "with_positions_offsets", - "type": "text" + "type": "text", }, "link": { "analyzer": "snowball", "store": False, "term_vector": "with_positions_offsets", - "type": "text" - } + "type": "text", + }, } - cls.ES().indices.put_mapping(body={ - 'properties': mapping, - }, index=cls.index_name()) + cls.ES().indices.put_mapping( + body={ + "properties": mapping, + }, + index=cls.index_name(), + ) cls.ES().indices.flush(cls.index_name()) @classmethod @@ -594,8 +594,7 @@ def index(cls, feed_id, title, address, link, num_subscribers): } try: cls.ES().create(index=cls.index_name(), id=feed_id, body=doc, doc_type=cls.doc_type()) - except (elasticsearch.exceptions.ConnectionError, - urllib3.exceptions.NewConnectionError) as e: + except (elasticsearch.exceptions.ConnectionError, urllib3.exceptions.NewConnectionError) as e: logging.debug(f" ***> ~FRNo search server available for feed indexing: {e}") @classmethod @@ -615,21 +614,45 @@ def query(cls, text, max_subscribers=5): if settings.DEBUG: max_subscribers = 1 - + body = { "query": { "bool": { "should": [ - {"match": { "address": { "query": text, 'cutoff_frequency': "0.0005", 'minimum_should_match': "75%" } }}, - {"match": { "title": { "query": text, 'cutoff_frequency': "0.0005", 'minimum_should_match': "75%" } }}, - {"match": { "link": { "query": text, 'cutoff_frequency': "0.0005", 'minimum_should_match': "75%" } }}, + { + "match": { + "address": { + "query": text, + "cutoff_frequency": "0.0005", + "minimum_should_match": "75%", + } + } + }, + { + "match": { + "title": { + "query": text, + "cutoff_frequency": "0.0005", + "minimum_should_match": "75%", + } + } + }, + { + "match": { + "link": { + "query": text, + "cutoff_frequency": "0.0005", + "minimum_should_match": "75%", + } + } + }, ] } }, - 'sort': [{'num_subscribers': {'order': 'desc'}}], + "sort": [{"num_subscribers": {"order": "desc"}}], } try: - results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) + results = cls.ES().search(body=body, index=cls.index_name(), doc_type=cls.doc_type()) except elasticsearch.exceptions.RequestError as e: logging.debug(" ***> ~FRNo search server available for querying: %s" % e) return [] @@ -651,19 +674,23 @@ def query(cls, text, max_subscribers=5): # q.add_should(pyes.query.MatchQuery('title', text, analyzer="simple", cutoff_frequency=0.0005, minimum_should_match="75%")) # q = pyes.Search(q, min_score=1) # results = cls.ES().search(query=q, size=max_subscribers, sort="num_subscribers:desc") - - logging.info("~FGSearch ~FCfeeds~FG: ~SB%s~SN, ~SB%s~SN results" % (text, len(results['hits']['hits']))) - return results['hits']['hits'] - + logging.info( + "~FGSearch ~FCfeeds~FG: ~SB%s~SN, ~SB%s~SN results" % (text, len(results["hits"]["hits"])) + ) + + return results["hits"]["hits"] + @classmethod def export_csv(cls): import djqscsv + from apps.rss_feeds.models import Feed - qs = Feed.objects.filter(num_subscribers__gte=20).values('id', 'feed_title', 'feed_address', 'feed_link', 'num_subscribers') + qs = Feed.objects.filter(num_subscribers__gte=20).values( + "id", "feed_title", "feed_address", "feed_link", "num_subscribers" + ) csv = djqscsv.render_to_csv_response(qs).content - f = open('feeds.csv', 'w+') + f = open("feeds.csv", "w+") f.write(csv) f.close() - diff --git a/apps/search/tasks.py b/apps/search/tasks.py index 3ae7acf846..d56c892cf0 100644 --- a/apps/search/tasks.py +++ b/apps/search/tasks.py @@ -1,27 +1,31 @@ from newsblur_web.celeryapp import app from utils import log as logging + @app.task() def IndexSubscriptionsForSearch(user_id): from apps.search.models import MUserSearch - + user_search = MUserSearch.get_user(user_id) user_search.index_subscriptions_for_search() + @app.task() def IndexSubscriptionsChunkForSearch(feed_ids, user_id): logging.debug(" ---> Indexing: %s for %s" % (feed_ids, user_id)) from apps.search.models import MUserSearch - + user_search = MUserSearch.get_user(user_id) user_search.index_subscriptions_chunk_for_search(feed_ids) + @app.task() def IndexFeedsForSearch(feed_ids, user_id): from apps.search.models import MUserSearch - + MUserSearch.index_feeds_for_search(feed_ids, user_id) + @app.task() def FinishIndexSubscriptionsForSearch(results, user_id, start): logging.debug(" ---> Indexing finished for %s" % (user_id)) diff --git a/apps/search/urls.py b/apps/search/urls.py index e29e860a9b..284e6ba2cc 100644 --- a/apps/search/urls.py +++ b/apps/search/urls.py @@ -1,7 +1,8 @@ from django.conf.urls import * + from apps.search import views urlpatterns = [ # url(r'^$', views.index), - url(r'^more_like_this', views.more_like_this, name='more-like-this'), + url(r"^more_like_this", views.more_like_this, name="more-like-this"), ] diff --git a/apps/search/views.py b/apps/search/views.py index e067315714..e723a0d047 100644 --- a/apps/search/views.py +++ b/apps/search/views.py @@ -1,27 +1,28 @@ -from apps.rss_feeds.models import Feed, MStory from apps.reader.models import UserSubscription +from apps.rss_feeds.models import Feed, MStory from apps.search.models import SearchStory from utils import json_functions as json +from utils.user_functions import ajax_login_required, get_user from utils.view_functions import required_params -from utils.user_functions import get_user, ajax_login_required + # @required_params('story_hash') @json.json_view def more_like_this(request): user = get_user(request) get_post = getattr(request, request.method) - order = get_post.get('order', 'newest') - page = int(get_post.get('page', 1)) - limit = int(get_post.get('limit', 10)) - offset = limit * (page-1) - story_hash = get_post.get('story_hash') - + order = get_post.get("order", "newest") + page = int(get_post.get("page", 1)) + limit = int(get_post.get("limit", 10)) + offset = limit * (page - 1) + story_hash = get_post.get("story_hash") + feed_ids = [us.feed_id for us in UserSubscription.objects.filter(user=user)] feed_ids, _ = MStory.split_story_hash(story_hash) story_ids = SearchStory.more_like_this([feed_ids], story_hash, order, offset=offset, limit=limit) - stories_db = MStory.objects( - story_hash__in=story_ids - ).order_by('-story_date' if order == "newest" else 'story_date') + stories_db = MStory.objects(story_hash__in=story_ids).order_by( + "-story_date" if order == "newest" else "story_date" + ) stories = Feed.format_stories(stories_db) return { diff --git a/apps/social/management/commands/popular_stories.py b/apps/social/management/commands/popular_stories.py index 93cb394e09..5747cddd85 100644 --- a/apps/social/management/commands/popular_stories.py +++ b/apps/social/management/commands/popular_stories.py @@ -1,7 +1,8 @@ from django.core.management.base import BaseCommand + from apps.social.models import MSharedStory -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): - MSharedStory.share_popular_stories() \ No newline at end of file + MSharedStory.share_popular_stories() diff --git a/apps/social/migrations/0001_username_unique.py b/apps/social/migrations/0001_username_unique.py index f9f45fb478..c165ae0a17 100644 --- a/apps/social/migrations/0001_username_unique.py +++ b/apps/social/migrations/0001_username_unique.py @@ -1,22 +1,20 @@ # Generated by Django 3.1.4 on 2021-04-12 20:59 -from django.db import migrations -from django.conf import settings import pymongo +from django.conf import settings +from django.db import migrations + def remove_unique_index(apps, schema_editor): - social_profile = sp = settings.MONGODB[settings.MONGO_DB_NAME].social_profile + social_profile = sp = settings.MONGODB[settings.MONGO_DB_NAME].social_profile try: - social_profile.drop_index('username_1') + social_profile.drop_index("username_1") except pymongo.errors.OperationFailure: print(" ***> Couldn't delete username_1 index on social_profile collection. Already deleted?") pass -class Migration(migrations.Migration): - dependencies = [ - ] +class Migration(migrations.Migration): + dependencies = [] - operations = [ - migrations.RunPython(remove_unique_index) - ] + operations = [migrations.RunPython(remove_unique_index)] diff --git a/apps/social/models.py b/apps/social/models.py index 2b62e679b5..c34832e2c8 100644 --- a/apps/social/models.py +++ b/apps/social/models.py @@ -1,44 +1,54 @@ -import os -import urllib.parse import datetime -import time -import zlib import hashlib import html as pyhtml -import redis +import html.parser as html_parser +import os +import random import re +import time +import urllib.parse +import zlib +from collections import defaultdict + import mongoengine as mongo -import random +import pynliner +import redis import requests -import html.parser as html_parser import tweepy -import pynliner -from collections import defaultdict from bs4 import BeautifulSoup -from mongoengine.queryset import Q from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site -from django.urls import reverse -from django.template.loader import render_to_string -from django.template.defaultfilters import slugify from django.core.mail import EmailMultiAlternatives +from django.template.defaultfilters import slugify +from django.template.loader import render_to_string +from django.urls import reverse from django.utils.encoding import smart_bytes, smart_str -from apps.reader.models import UserSubscription, RUserStory -from apps.analyzer.models import MClassifierFeed, MClassifierAuthor, MClassifierTag, MClassifierTitle -from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds, apply_classifier_authors, apply_classifier_tags +from mongoengine.queryset import Q + +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + apply_classifier_authors, + apply_classifier_feeds, + apply_classifier_tags, + apply_classifier_titles, +) +from apps.profile.models import MSentEmail, Profile +from apps.reader.models import RUserStory, UserSubscription from apps.rss_feeds.models import Feed, MStory -from apps.rss_feeds.text_importer import TextImporter from apps.rss_feeds.page_importer import PageImporter -from apps.profile.models import Profile, MSentEmail -from vendor import facebook -from utils import log as logging +from apps.rss_feeds.text_importer import TextImporter from utils import json_functions as json -from utils.feed_functions import relative_timesince, chunks -from utils.story_functions import truncate_chars, strip_tags, linkify +from utils import log as logging +from utils import s3_utils +from utils.feed_functions import chunks, relative_timesince from utils.image_functions import ImageOps from utils.scrubber import SelectiveScriptScrubber -from utils import s3_utils +from utils.story_functions import linkify, strip_tags, truncate_chars +from vendor import facebook try: from apps.social.spam import detect_spammers @@ -47,36 +57,35 @@ pass RECOMMENDATIONS_LIMIT = 5 -IGNORE_IMAGE_SOURCES = [ - "http://feeds.feedburner.com" -] +IGNORE_IMAGE_SOURCES = ["http://feeds.feedburner.com"] + class MRequestInvite(mongo.Document): - email = mongo.EmailField() - request_date = mongo.DateTimeField(default=datetime.datetime.now) - invite_sent = mongo.BooleanField(default=False) + email = mongo.EmailField() + request_date = mongo.DateTimeField(default=datetime.datetime.now) + invite_sent = mongo.BooleanField(default=False) invite_sent_date = mongo.DateTimeField() meta = { - 'collection': 'social_invites', - 'allow_inheritance': False, + "collection": "social_invites", + "allow_inheritance": False, } - + def __str__(self): - return "%s%s" % (self.email, '*' if self.invite_sent else '') - + return "%s%s" % (self.email, "*" if self.invite_sent else "") + @classmethod def blast(cls): invites = cls.objects.filter(email_sent=None) - print(' ---> Found %s invites...' % invites.count()) - + print(" ---> Found %s invites..." % invites.count()) + for invite in invites: try: invite.send_email() except: - print(' ***> Could not send invite to: %s. Deleting.' % invite.username) + print(" ***> Could not send invite to: %s. Deleting." % invite.username) invite.delete() - + def send_email(self): user = User.objects.filter(username__iexact=self.username) if not user: @@ -86,84 +95,88 @@ def send_email(self): email = user.email or self.username else: user = { - 'username': self.username, - 'profile': { - 'autologin_url': '/', - } + "username": self.username, + "profile": { + "autologin_url": "/", + }, } email = self.username params = { - 'user': user, + "user": user, } - text = render_to_string('mail/email_social_beta.txt', params) - html = render_to_string('mail/email_social_beta.xhtml', params) + text = render_to_string("mail/email_social_beta.txt", params) + html = render_to_string("mail/email_social_beta.xhtml", params) subject = "Psst, you're in..." - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['<%s>' % (email)]) + msg = EmailMultiAlternatives( + subject, text, from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, to=["<%s>" % (email)] + ) msg.attach_alternative(html, "text/html") msg.send() - + self.email_sent = True self.save() - + logging.debug(" ---> ~BB~FM~SBSending email for social beta: %s" % self.username) class MSocialProfile(mongo.Document): - user_id = mongo.IntField(unique=True) - username = mongo.StringField(max_length=30) - email = mongo.StringField() - bio = mongo.StringField(max_length=160) - blurblog_title = mongo.StringField(max_length=256) - custom_bgcolor = mongo.StringField(max_length=50) - custom_css = mongo.StringField() - photo_url = mongo.StringField() - photo_service = mongo.StringField() - location = mongo.StringField(max_length=40) - website = mongo.StringField(max_length=200) - bb_permalink_direct = mongo.BooleanField() - subscription_count = mongo.IntField(default=0) + user_id = mongo.IntField(unique=True) + username = mongo.StringField(max_length=30) + email = mongo.StringField() + bio = mongo.StringField(max_length=160) + blurblog_title = mongo.StringField(max_length=256) + custom_bgcolor = mongo.StringField(max_length=50) + custom_css = mongo.StringField() + photo_url = mongo.StringField() + photo_service = mongo.StringField() + location = mongo.StringField(max_length=40) + website = mongo.StringField(max_length=200) + bb_permalink_direct = mongo.BooleanField() + subscription_count = mongo.IntField(default=0) shared_stories_count = mongo.IntField(default=0) - following_count = mongo.IntField(default=0) - follower_count = mongo.IntField(default=0) - following_user_ids = mongo.ListField(mongo.IntField()) - follower_user_ids = mongo.ListField(mongo.IntField()) - unfollowed_user_ids = mongo.ListField(mongo.IntField()) + following_count = mongo.IntField(default=0) + follower_count = mongo.IntField(default=0) + following_user_ids = mongo.ListField(mongo.IntField()) + follower_user_ids = mongo.ListField(mongo.IntField()) + unfollowed_user_ids = mongo.ListField(mongo.IntField()) requested_follow_user_ids = mongo.ListField(mongo.IntField()) - muting_user_ids = mongo.ListField(mongo.IntField()) - muted_by_user_ids = mongo.ListField(mongo.IntField()) - popular_publishers = mongo.StringField() - stories_last_month = mongo.IntField(default=0) + muting_user_ids = mongo.ListField(mongo.IntField()) + muted_by_user_ids = mongo.ListField(mongo.IntField()) + popular_publishers = mongo.StringField() + stories_last_month = mongo.IntField(default=0) average_stories_per_month = mongo.IntField(default=0) - story_count_history = mongo.ListField() - story_days_history = mongo.DictField() - story_hours_history = mongo.DictField() - story_email_history = mongo.ListField() + story_count_history = mongo.ListField() + story_days_history = mongo.DictField() + story_hours_history = mongo.DictField() + story_email_history = mongo.ListField() feed_classifier_counts = mongo.DictField() - favicon_color = mongo.StringField(max_length=6) - protected = mongo.BooleanField() - private = mongo.BooleanField() - + favicon_color = mongo.StringField(max_length=6) + protected = mongo.BooleanField() + private = mongo.BooleanField() + meta = { - 'collection': 'social_profile', - 'indexes': [ - 'user_id', - 'username', - 'following_user_ids', - 'follower_user_ids', - 'unfollowed_user_ids', - 'requested_follow_user_ids', - 'muting_user_ids', - 'muted_by_user_ids', + "collection": "social_profile", + "indexes": [ + "user_id", + "username", + "following_user_ids", + "follower_user_ids", + "unfollowed_user_ids", + "requested_follow_user_ids", + "muting_user_ids", + "muted_by_user_ids", ], - 'allow_inheritance': False, + "allow_inheritance": False, } - + def __str__(self): - return "%s following %s/%s, shared %s" % (self.user, - self.following_count, self.follower_count, self.shared_stories_count) - + return "%s following %s/%s, shared %s" % ( + self.user, + self.following_count, + self.follower_count, + self.shared_stories_count, + ) + @classmethod def get_user(cls, user_id): try: @@ -176,7 +189,7 @@ def get_user(cls, user_id): profile.save() return profile - + @property def user(self): try: @@ -199,35 +212,38 @@ def save(self, *args, **kwargs): self.location = strip_tags(self.location) if self.custom_css: self.custom_css = strip_tags(self.custom_css) - + super(MSocialProfile, self).save(*args, **kwargs) if self.user_id not in self.following_user_ids: self.follow_user(self.user_id, force=True) self.count_follows() - + return self - + @property def blurblog_url(self): - return "https://%s.%s/" % ( - self.username_slug, - Site.objects.get_current().domain.replace('www.', '')) - + return "https://%s.%s/" % (self.username_slug, Site.objects.get_current().domain.replace("www.", "")) + @property def blurblog_rss(self): - return "%s%s" % (self.blurblog_url, reverse('shared-stories-rss-feed', - kwargs={'user_id': self.user_id, - 'username': self.username_slug})) + return "%s%s" % ( + self.blurblog_url, + reverse( + "shared-stories-rss-feed", kwargs={"user_id": self.user_id, "username": self.username_slug} + ), + ) def find_stories(self, query, offset=0, limit=25): stories_db = MSharedStory.objects( - Q(user_id=self.user_id) & - (Q(story_title__icontains=query) | - Q(story_author_name__icontains=query) | - Q(story_tags__icontains=query)) - ).order_by('-shared_date')[offset:offset+limit] + Q(user_id=self.user_id) + & ( + Q(story_title__icontains=query) + | Q(story_author_name__icontains=query) + | Q(story_tags__icontains=query) + ) + ).order_by("-shared_date")[offset : offset + limit] stories = Feed.format_stories(stories_db) - + return stories def recommended_users(self): @@ -235,13 +251,21 @@ def recommended_users(self): following_key = "F:%s:F" % (self.user_id) social_follow_key = "FF:%s:F" % (self.user_id) profile_user_ids = [] - + # Find potential twitter/fb friends services = MSocialServices.get_user(self.user_id) - facebook_user_ids = [u.user_id for u in - MSocialServices.objects.filter(facebook_uid__in=services.facebook_friend_ids).only('user_id')] - twitter_user_ids = [u.user_id for u in - MSocialServices.objects.filter(twitter_uid__in=services.twitter_friend_ids).only('user_id')] + facebook_user_ids = [ + u.user_id + for u in MSocialServices.objects.filter(facebook_uid__in=services.facebook_friend_ids).only( + "user_id" + ) + ] + twitter_user_ids = [ + u.user_id + for u in MSocialServices.objects.filter(twitter_uid__in=services.twitter_friend_ids).only( + "user_id" + ) + ] social_user_ids = facebook_user_ids + twitter_user_ids # Find users not currently followed by this user r.delete(social_follow_key) @@ -251,10 +275,10 @@ def recommended_users(self): nonfriend_user_ids = r.sdiff(social_follow_key, following_key) profile_user_ids = [int(f) for f in nonfriend_user_ids] r.delete(social_follow_key) - + # Not enough? Grab popular users. if len(nonfriend_user_ids) < RECOMMENDATIONS_LIMIT: - homepage_user = User.objects.get(username='popular') + homepage_user = User.objects.get(username="popular") suggested_users_list = r.sdiff("F:%s:F" % homepage_user.pk, following_key) suggested_users_list = [int(f) for f in suggested_users_list] suggested_user_ids = [] @@ -262,32 +286,40 @@ def recommended_users(self): for slot in range(slots_left): suggested_user_ids.append(random.choice(suggested_users_list)) profile_user_ids.extend(suggested_user_ids) - + # Sort by shared story count - profiles = MSocialProfile.profiles(profile_user_ids).order_by('-shared_stories_count')[:RECOMMENDATIONS_LIMIT] + profiles = MSocialProfile.profiles(profile_user_ids).order_by("-shared_stories_count")[ + :RECOMMENDATIONS_LIMIT + ] return profiles - + @property def username_slug(self): return slugify(self.user.username if self.user else "[deleted]") - + def count_stories(self): # Popular Publishers self.save_popular_publishers() - + def save_popular_publishers(self, feed_publishers=None): if not feed_publishers: publishers = defaultdict(int) - for story in MSharedStory.objects(user_id=self.user_id).only('story_feed_id')[:500]: + for story in MSharedStory.objects(user_id=self.user_id).only("story_feed_id")[:500]: publishers[story.story_feed_id] += 1 - feed_titles = dict((f.id, f.feed_title) - for f in Feed.objects.filter(pk__in=list(publishers.keys())).only('id', 'feed_title')) - feed_publishers = sorted([{'id': k, 'feed_title': feed_titles[k], 'story_count': v} - for k, v in list(publishers.items()) - if k in feed_titles], - key=lambda f: f['story_count'], - reverse=True)[:20] + feed_titles = dict( + (f.id, f.feed_title) + for f in Feed.objects.filter(pk__in=list(publishers.keys())).only("id", "feed_title") + ) + feed_publishers = sorted( + [ + {"id": k, "feed_title": feed_titles[k], "story_count": v} + for k, v in list(publishers.items()) + if k in feed_titles + ], + key=lambda f: f["story_count"], + reverse=True, + )[:20] popular_publishers = json.encode(feed_publishers) if len(popular_publishers) < 1023: @@ -297,12 +329,12 @@ def save_popular_publishers(self, feed_publishers=None): if len(popular_publishers) > 1: self.save_popular_publishers(feed_publishers=feed_publishers[:-1]) - + @classmethod def profile(cls, user_id, include_follows=True): profile = cls.get_user(user_id) return profile.canonical(include_follows=True) - + @classmethod def profiles(cls, user_ids): profiles = cls.objects.filter(user_id__in=user_ids) @@ -313,148 +345,180 @@ def profile_feeds(cls, user_ids): profiles = cls.objects.filter(user_id__in=user_ids) profiles = dict((p.user_id, p.feed()) for p in profiles) return profiles - + @classmethod def sync_all_redis(cls): for profile in cls.objects.all(): profile.sync_redis(force=True) - + def sync_redis(self, force=False): self.following_user_ids = list(set(self.following_user_ids)) self.save() - + for user_id in self.following_user_ids: self.follow_user(user_id, force=force) - + self.follow_user(self.user_id, force=force) - + @property def title(self): - return self.blurblog_title if self.blurblog_title else (self.user.username if self.user else "[deleted]") + "'s blurblog" - + return ( + self.blurblog_title + if self.blurblog_title + else (self.user.username if self.user else "[deleted]") + "'s blurblog" + ) + def feed(self): params = self.canonical(compact=True) - params.update({ - 'feed_title': self.title, - 'page_url': reverse('load-social-page', kwargs={'user_id': self.user_id, 'username': self.username_slug}), - 'shared_stories_count': self.shared_stories_count, - }) + params.update( + { + "feed_title": self.title, + "page_url": reverse( + "load-social-page", kwargs={"user_id": self.user_id, "username": self.username_slug} + ), + "shared_stories_count": self.shared_stories_count, + } + ) return params - + def page(self): params = self.canonical(include_follows=True) - params.update({ - 'feed_title': self.title, - 'custom_css': self.custom_css, - }) + params.update( + { + "feed_title": self.title, + "custom_css": self.custom_css, + } + ) return params - + @property def profile_photo_url(self): if self.photo_url: return self.photo_url - return settings.MEDIA_URL + 'img/reader/default_profile_photo.png' - + return settings.MEDIA_URL + "img/reader/default_profile_photo.png" + @property def large_photo_url(self): photo_url = self.email_photo_url - if 'graph.facebook.com' in photo_url: - return photo_url + '?type=large' - elif 'twimg' in photo_url: - return photo_url.replace('_normal', '') - elif '/avatars/' in photo_url: - return photo_url.replace('thumbnail_', 'large_') + if "graph.facebook.com" in photo_url: + return photo_url + "?type=large" + elif "twimg" in photo_url: + return photo_url.replace("_normal", "") + elif "/avatars/" in photo_url: + return photo_url.replace("thumbnail_", "large_") return photo_url - + @property def email_photo_url(self): if self.photo_url: - if self.photo_url.startswith('//'): - self.photo_url = 'https:' + self.photo_url + if self.photo_url.startswith("//"): + self.photo_url = "https:" + self.photo_url return self.photo_url domain = Site.objects.get_current().domain - return 'https://' + domain + settings.MEDIA_URL + 'img/reader/default_profile_photo.png' - - def canonical(self, compact=False, include_follows=False, common_follows_with_user=None, - include_settings=False, include_following_user=None): + return "https://" + domain + settings.MEDIA_URL + "img/reader/default_profile_photo.png" + + def canonical( + self, + compact=False, + include_follows=False, + common_follows_with_user=None, + include_settings=False, + include_following_user=None, + ): domain = Site.objects.get_current().domain params = { - 'id': 'social:%s' % self.user_id, - 'user_id': self.user_id, - 'username': self.user.username if self.user else "[deleted]", - 'photo_url': self.email_photo_url, - 'large_photo_url': self.large_photo_url, - 'location': self.location, - 'num_subscribers': self.follower_count, - 'feed_title': self.title, - 'feed_address': "http://%s%s" % (domain, reverse('shared-stories-rss-feed', - kwargs={'user_id': self.user_id, 'username': self.username_slug})), - 'feed_link': self.blurblog_url, - 'protected': self.protected, - 'private': self.private, - 'active': True, + "id": "social:%s" % self.user_id, + "user_id": self.user_id, + "username": self.user.username if self.user else "[deleted]", + "photo_url": self.email_photo_url, + "large_photo_url": self.large_photo_url, + "location": self.location, + "num_subscribers": self.follower_count, + "feed_title": self.title, + "feed_address": "http://%s%s" + % ( + domain, + reverse( + "shared-stories-rss-feed", + kwargs={"user_id": self.user_id, "username": self.username_slug}, + ), + ), + "feed_link": self.blurblog_url, + "protected": self.protected, + "private": self.private, + "active": True, } if not compact: - params.update({ - 'large_photo_url': self.large_photo_url, - 'bio': self.bio, - 'website': self.website, - 'shared_stories_count': self.shared_stories_count, - 'following_count': self.following_count, - 'follower_count': self.follower_count, - 'popular_publishers': json.decode(self.popular_publishers), - 'stories_last_month': self.stories_last_month, - 'average_stories_per_month': self.average_stories_per_month, - }) + params.update( + { + "large_photo_url": self.large_photo_url, + "bio": self.bio, + "website": self.website, + "shared_stories_count": self.shared_stories_count, + "following_count": self.following_count, + "follower_count": self.follower_count, + "popular_publishers": json.decode(self.popular_publishers), + "stories_last_month": self.stories_last_month, + "average_stories_per_month": self.average_stories_per_month, + } + ) if include_settings: - params.update({ - 'custom_css': self.custom_css, - 'custom_bgcolor': self.custom_bgcolor, - 'bb_permalink_direct': self.bb_permalink_direct, - }) + params.update( + { + "custom_css": self.custom_css, + "custom_bgcolor": self.custom_bgcolor, + "bb_permalink_direct": self.bb_permalink_direct, + } + ) if include_follows: - params.update({ - 'photo_service': self.photo_service, - 'following_user_ids': self.following_user_ids_without_self[:48], - 'follower_user_ids': self.follower_user_ids_without_self[:48], - }) + params.update( + { + "photo_service": self.photo_service, + "following_user_ids": self.following_user_ids_without_self[:48], + "follower_user_ids": self.follower_user_ids_without_self[:48], + } + ) if common_follows_with_user: FOLLOWERS_LIMIT = 128 with_user = MSocialProfile.get_user(common_follows_with_user) - followers_youknow, followers_everybody = with_user.common_follows(self.user_id, direction='followers') - following_youknow, following_everybody = with_user.common_follows(self.user_id, direction='following') - params['followers_youknow'] = followers_youknow[:FOLLOWERS_LIMIT] - params['followers_everybody'] = followers_everybody[:FOLLOWERS_LIMIT] - params['following_youknow'] = following_youknow[:FOLLOWERS_LIMIT] - params['following_everybody'] = following_everybody[:FOLLOWERS_LIMIT] - params['requested_follow'] = common_follows_with_user in self.requested_follow_user_ids + followers_youknow, followers_everybody = with_user.common_follows( + self.user_id, direction="followers" + ) + following_youknow, following_everybody = with_user.common_follows( + self.user_id, direction="following" + ) + params["followers_youknow"] = followers_youknow[:FOLLOWERS_LIMIT] + params["followers_everybody"] = followers_everybody[:FOLLOWERS_LIMIT] + params["following_youknow"] = following_youknow[:FOLLOWERS_LIMIT] + params["following_everybody"] = following_everybody[:FOLLOWERS_LIMIT] + params["requested_follow"] = common_follows_with_user in self.requested_follow_user_ids if include_following_user or common_follows_with_user: if not include_following_user: include_following_user = common_follows_with_user if include_following_user != self.user_id: - params['followed_by_you'] = bool(self.is_followed_by_user(include_following_user)) - params['following_you'] = self.is_following_user(include_following_user) - params['muted'] = include_following_user in self.muted_by_user_ids + params["followed_by_you"] = bool(self.is_followed_by_user(include_following_user)) + params["following_you"] = self.is_following_user(include_following_user) + params["muted"] = include_following_user in self.muted_by_user_ids return params - + @property def following_user_ids_without_self(self): if self.user_id in self.following_user_ids: return [u for u in self.following_user_ids if u != self.user_id] return self.following_user_ids - + @property def follower_user_ids_without_self(self): if self.user_id in self.follower_user_ids: return [u for u in self.follower_user_ids if u != self.user_id] return self.follower_user_ids - + def import_user_fields(self): user = User.objects.get(pk=self.user_id) self.username = user.username self.email = user.email - + def count_follows(self, skip_save=False): self.subscription_count = UserSubscription.objects.filter(user__pk=self.user_id).count() self.shared_stories_count = MSharedStory.objects.filter(user_id=self.user_id).count() @@ -462,31 +526,31 @@ def count_follows(self, skip_save=False): self.follower_count = len(self.follower_user_ids_without_self) if not skip_save: self.save() - + def follow_user(self, user_id, check_unfollowed=False, force=False): r = redis.Redis(connection_pool=settings.REDIS_POOL) - + if check_unfollowed and user_id in self.unfollowed_user_ids: return - + if self.user_id == user_id: followee = self else: followee = MSocialProfile.get_user(user_id) - + logging.debug(" ---> ~FB~SB%s~SN (%s) following %s" % (self.user.username, self.user_id, user_id)) - + if not followee.protected or force: if user_id not in self.following_user_ids: self.following_user_ids.append(user_id) elif not force: return - + if user_id in self.unfollowed_user_ids: self.unfollowed_user_ids.remove(user_id) self.count_follows() self.save() - + if followee.protected and user_id != self.user_id and not force: if self.user_id not in followee.requested_follow_user_ids: followee.requested_follow_user_ids.append(self.user_id) @@ -498,11 +562,13 @@ def follow_user(self, user_id, check_unfollowed=False, force=False): if followee.protected and user_id != self.user_id and not force: from apps.social.tasks import EmailFollowRequest - EmailFollowRequest.apply_async(kwargs=dict(follower_user_id=self.user_id, - followee_user_id=user_id), - countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS) + + EmailFollowRequest.apply_async( + kwargs=dict(follower_user_id=self.user_id, followee_user_id=user_id), + countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS, + ) return - + following_key = "F:%s:F" % (self.user_id) r.sadd(following_key, user_id) follower_key = "F:%s:f" % (user_id) @@ -511,7 +577,7 @@ def follow_user(self, user_id, check_unfollowed=False, force=False): if user_id != self.user_id: MInteraction.new_follow(follower_user_id=self.user_id, followee_user_id=user_id) MActivity.new_follow(follower_user_id=self.user_id, followee_user_id=user_id) - + params = dict(user_id=self.user_id, subscription_user_id=user_id) try: socialsub = MSocialSubscription.objects.get(**params) @@ -519,31 +585,33 @@ def follow_user(self, user_id, check_unfollowed=False, force=False): socialsub = MSocialSubscription.objects.create(**params) socialsub.needs_unread_recalc = True socialsub.save() - + MFollowRequest.remove(self.user_id, user_id) - + if not force: from apps.social.tasks import EmailNewFollower - EmailNewFollower.apply_async(kwargs=dict(follower_user_id=self.user_id, - followee_user_id=user_id), - countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS) - + + EmailNewFollower.apply_async( + kwargs=dict(follower_user_id=self.user_id, followee_user_id=user_id), + countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS, + ) + return socialsub - + def is_following_user(self, user_id): # XXX TODO: Outsource to redis return user_id in self.following_user_ids - + def is_followed_by_user(self, user_id): # XXX TODO: Outsource to redis return user_id in self.follower_user_ids - + def unfollow_user(self, user_id): r = redis.Redis(connection_pool=settings.REDIS_POOL) - + if not isinstance(user_id, int): user_id = int(user_id) - + if user_id == self.user_id: # Only unfollow other people, not yourself. return @@ -554,7 +622,7 @@ def unfollow_user(self, user_id): self.unfollowed_user_ids.append(user_id) self.count_follows() self.save() - + followee = MSocialProfile.get_user(user_id) if self.user_id in followee.follower_user_ids: followee.follower_user_ids.remove(self.user_id) @@ -565,34 +633,34 @@ def unfollow_user(self, user_id): followee.count_follows() followee.save() MFollowRequest.remove(self.user_id, user_id) - + following_key = "F:%s:F" % (self.user_id) r.srem(following_key, user_id) follower_key = "F:%s:f" % (user_id) r.srem(follower_key, self.user_id) - + try: MSocialSubscription.objects.get(user_id=self.user_id, subscription_user_id=user_id).delete() except MSocialSubscription.DoesNotExist: return False - - def common_follows(self, user_id, direction='followers'): + + def common_follows(self, user_id, direction="followers"): r = redis.Redis(connection_pool=settings.REDIS_POOL) - - my_followers = "F:%s:%s" % (self.user_id, 'F' if direction == 'followers' else 'F') - their_followers = "F:%s:%s" % (user_id, 'f' if direction == 'followers' else 'F') - follows_inter = r.sinter(their_followers, my_followers) - follows_diff = r.sdiff(their_followers, my_followers) - follows_inter = [int(f) for f in follows_inter] - follows_diff = [int(f) for f in follows_diff] - + + my_followers = "F:%s:%s" % (self.user_id, "F" if direction == "followers" else "F") + their_followers = "F:%s:%s" % (user_id, "f" if direction == "followers" else "F") + follows_inter = r.sinter(their_followers, my_followers) + follows_diff = r.sdiff(their_followers, my_followers) + follows_inter = [int(f) for f in follows_inter] + follows_diff = [int(f) for f in follows_diff] + if user_id in follows_inter: follows_inter.remove(user_id) if user_id in follows_diff: follows_diff.remove(user_id) - + return follows_inter, follows_diff - + def send_email_for_new_follower(self, follower_user_id): user = User.objects.get(pk=self.user_id) if follower_user_id not in self.follower_user_ids: @@ -606,45 +674,49 @@ def send_email_for_new_follower(self, follower_user_id): return if self.user_id == follower_user_id: return - - emails_sent = MSentEmail.objects.filter(receiver_user_id=user.pk, - sending_user_id=follower_user_id, - email_type='new_follower') + + emails_sent = MSentEmail.objects.filter( + receiver_user_id=user.pk, sending_user_id=follower_user_id, email_type="new_follower" + ) day_ago = datetime.datetime.now() - datetime.timedelta(days=1) for email in emails_sent: if email.date_sent > day_ago: logging.user(user, "~SK~FMNot sending new follower email, already sent before. NBD.") return - + follower_profile = MSocialProfile.get_user(follower_user_id) - common_followers, _ = self.common_follows(follower_user_id, direction='followers') - common_followings, _ = self.common_follows(follower_user_id, direction='following') + common_followers, _ = self.common_follows(follower_user_id, direction="followers") + common_followings, _ = self.common_follows(follower_user_id, direction="following") if self.user_id in common_followers: common_followers.remove(self.user_id) if self.user_id in common_followings: common_followings.remove(self.user_id) common_followers = MSocialProfile.profiles(common_followers) common_followings = MSocialProfile.profiles(common_followings) - + data = { - 'user': user, - 'follower_profile': follower_profile, - 'common_followers': common_followers, - 'common_followings': common_followings, + "user": user, + "follower_profile": follower_profile, + "common_followers": common_followers, + "common_followings": common_followings, } - - text = render_to_string('mail/email_new_follower.txt', data) - html = render_to_string('mail/email_new_follower.xhtml', data) + + text = render_to_string("mail/email_new_follower.txt", data) + html = render_to_string("mail/email_new_follower.xhtml", data) subject = "%s is now following your Blurblog on NewsBlur!" % follower_profile.user.username - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user.username, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user.username, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - MSentEmail.record(receiver_user_id=user.pk, sending_user_id=follower_user_id, - email_type='new_follower') - + + MSentEmail.record( + receiver_user_id=user.pk, sending_user_id=follower_user_id, email_type="new_follower" + ) + logging.user(user, "~BB~FM~SBSending email for new follower: %s" % follower_profile.user.username) def send_email_for_follow_request(self, follower_user_id): @@ -660,57 +732,61 @@ def send_email_for_follow_request(self, follower_user_id): return if self.user_id == follower_user_id: return - - emails_sent = MSentEmail.objects.filter(receiver_user_id=user.pk, - sending_user_id=follower_user_id, - email_type='follow_request') + + emails_sent = MSentEmail.objects.filter( + receiver_user_id=user.pk, sending_user_id=follower_user_id, email_type="follow_request" + ) day_ago = datetime.datetime.now() - datetime.timedelta(days=1) for email in emails_sent: if email.date_sent > day_ago: logging.user(user, "~SK~FMNot sending follow request email, already sent before. NBD.") return - + follower_profile = MSocialProfile.get_user(follower_user_id) - common_followers, _ = self.common_follows(follower_user_id, direction='followers') - common_followings, _ = self.common_follows(follower_user_id, direction='following') + common_followers, _ = self.common_follows(follower_user_id, direction="followers") + common_followings, _ = self.common_follows(follower_user_id, direction="following") if self.user_id in common_followers: common_followers.remove(self.user_id) if self.user_id in common_followings: common_followings.remove(self.user_id) common_followers = MSocialProfile.profiles(common_followers) common_followings = MSocialProfile.profiles(common_followings) - + data = { - 'user': user, - 'follower_profile': follower_profile, - 'common_followers': common_followers, - 'common_followings': common_followings, + "user": user, + "follower_profile": follower_profile, + "common_followers": common_followers, + "common_followings": common_followings, } - - text = render_to_string('mail/email_follow_request.txt', data) - html = render_to_string('mail/email_follow_request.xhtml', data) + + text = render_to_string("mail/email_follow_request.txt", data) + html = render_to_string("mail/email_follow_request.xhtml", data) subject = "%s has requested to follow your Blurblog on NewsBlur" % follower_profile.user.username - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user.username, user.email)]) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user.username, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - - MSentEmail.record(receiver_user_id=user.pk, sending_user_id=follower_user_id, - email_type='follow_request') - + + MSentEmail.record( + receiver_user_id=user.pk, sending_user_id=follower_user_id, email_type="follow_request" + ) + logging.user(user, "~BB~FM~SBSending email for follow request: %s" % follower_profile.user.username) - + def mute_user(self, muting_user_id): if muting_user_id not in self.muting_user_ids: self.muting_user_ids.append(muting_user_id) self.save() - + muting_user_profile = MSocialProfile.get_user(muting_user_id) if self.user_id not in muting_user_profile.muted_by_user_ids: muting_user_profile.muted_by_user_ids.append(self.user_id) muting_user_profile.save() - + def unmute_user(self, muting_user_id): if muting_user_id in self.muting_user_ids: self.muting_user_ids.remove(muting_user_id) @@ -720,11 +796,11 @@ def unmute_user(self, muting_user_id): if self.user_id in muting_user_profile.muted_by_user_ids: muting_user_profile.muted_by_user_ids.remove(self.user_id) muting_user_profile.save() - + def save_feed_story_history_statistics(self): """ Fills in missing months between earlier occurances and now. - + Save format: [('YYYY-MM, #), ...] Example output: [(2010-12, 123), (2011-01, 146)] """ @@ -750,23 +826,23 @@ def save_feed_story_history_statistics(self): dates = defaultdict(int) hours = defaultdict(int) days = defaultdict(int) - results = MSharedStory.objects(user_id=self.user_id).map_reduce(map_f, reduce_f, output='inline') + results = MSharedStory.objects(user_id=self.user_id).map_reduce(map_f, reduce_f, output="inline") for result in results: - dates[result.value['month']] += 1 - hours[str(int(result.value['hour']))] += 1 - days[str(int(result.value['day']))] += 1 - year = int(re.findall(r"(\d{4})-\d{1,2}", result.value['month'])[0]) + dates[result.value["month"]] += 1 + hours[str(int(result.value["hour"]))] += 1 + days[str(int(result.value["day"]))] += 1 + year = int(re.findall(r"(\d{4})-\d{1,2}", result.value["month"])[0]) if year < min_year: min_year = year - - # Assemble a list with 0's filled in for missing months, + + # Assemble a list with 0's filled in for missing months, # trimming left and right 0's. months = [] start = False - for year in range(min_year, now.year+1): - for month in range(1, 12+1): + for year in range(min_year, now.year + 1): + for month in range(1, 12 + 1): if datetime.datetime(year, month, 1) < now: - key = '%s-%s' % (year, month) + key = "%s-%s" % (year, month) if dates.get(key) or start: start = True months.append((key, dates.get(key, 0))) @@ -778,9 +854,8 @@ def save_feed_story_history_statistics(self): self.story_hours_history = hours self.average_stories_per_month = total / max(1, month_count) self.save() - + def save_classifier_counts(self): - def calculate_scores(cls, facet): map_f = """ function() { @@ -789,7 +864,9 @@ def calculate_scores(cls, facet): neg: this.score<0 ? Math.abs(this.score) : 0 }); } - """ % (facet) + """ % ( + facet + ) reduce_f = """ function(key, values) { var result = {pos: 0, neg: 0}; @@ -801,40 +878,42 @@ def calculate_scores(cls, facet): } """ scores = [] - res = cls.objects(social_user_id=self.user_id).map_reduce(map_f, reduce_f, output='inline') + res = cls.objects(social_user_id=self.user_id).map_reduce(map_f, reduce_f, output="inline") for r in res: - facet_values = dict([(k, int(v)) for k,v in list(r.value.items())]) + facet_values = dict([(k, int(v)) for k, v in list(r.value.items())]) facet_values[facet] = r.key scores.append(facet_values) - scores = sorted(scores, key=lambda v: v['neg'] - v['pos']) + scores = sorted(scores, key=lambda v: v["neg"] - v["pos"]) return scores - + scores = {} - for cls, facet in [(MClassifierTitle, 'title'), - (MClassifierAuthor, 'author'), - (MClassifierTag, 'tag'), - (MClassifierFeed, 'feed_id')]: + for cls, facet in [ + (MClassifierTitle, "title"), + (MClassifierAuthor, "author"), + (MClassifierTag, "tag"), + (MClassifierFeed, "feed_id"), + ]: scores[facet] = calculate_scores(cls, facet) - if facet == 'feed_id' and scores[facet]: - scores['feed'] = scores[facet] - del scores['feed_id'] + if facet == "feed_id" and scores[facet]: + scores["feed"] = scores[facet] + del scores["feed_id"] elif not scores[facet]: del scores[facet] - + if scores: self.feed_classifier_counts = scores self.save() - + def save_sent_email(self, max_quota=20): if not self.story_email_history: self.story_email_history = [] - + self.story_email_history.insert(0, datetime.datetime.now()) self.story_email_history = self.story_email_history[:max_quota] - + self.save() - + def over_story_email_quota(self, quota=1, hours=24): counted = 0 day_ago = datetime.datetime.now() - datetime.timedelta(hours=hours) @@ -846,17 +925,18 @@ def over_story_email_quota(self, quota=1, hours=24): for sent_date in sent_emails: if sent_date > day_ago: counted += 1 - + if counted >= quota: return True - + return False - + + class MSocialSubscription(mongo.Document): UNREAD_CUTOFF = datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD) user_id = mongo.IntField() - subscription_user_id = mongo.IntField(unique_with='user_id') + subscription_user_id = mongo.IntField(unique_with="user_id") follow_date = mongo.DateTimeField(default=datetime.datetime.utcnow()) last_read_date = mongo.DateTimeField(default=UNREAD_CUTOFF) mark_read_date = mongo.DateTimeField(default=UNREAD_CUTOFF) @@ -869,24 +949,31 @@ class MSocialSubscription(mongo.Document): feed_opens = mongo.IntField(default=0) is_trained = mongo.BooleanField(default=False) active = mongo.BooleanField(default=True) - + meta = { - 'collection': 'social_subscription', - 'indexes': [('user_id', 'subscription_user_id')], - 'allow_inheritance': False, - 'strict': False, + "collection": "social_subscription", + "indexes": [("user_id", "subscription_user_id")], + "allow_inheritance": False, + "strict": False, } def __str__(self): user = User.objects.get(pk=self.user_id) subscription_user = User.objects.get(pk=self.subscription_user_id) return "Socialsub %s:%s" % (user, subscription_user) - + @classmethod - def feeds(cls, user_id=None, subscription_user_id=None, calculate_all_scores=False, - update_counts=False, *args, **kwargs): + def feeds( + cls, + user_id=None, + subscription_user_id=None, + calculate_all_scores=False, + update_counts=False, + *args, + **kwargs, + ): params = { - 'user_id': user_id, + "user_id": user_id, } if subscription_user_id: params["subscription_user_id"] = subscription_user_id @@ -895,125 +982,139 @@ def feeds(cls, user_id=None, subscription_user_id=None, calculate_all_scores=Fal social_feeds = [] if social_subs: if calculate_all_scores: - for s in social_subs: s.calculate_feed_scores() + for s in social_subs: + s.calculate_feed_scores() # Fetch user profiles of subscriptions social_user_ids = [sub.subscription_user_id for sub in social_subs] social_profiles = MSocialProfile.profile_feeds(social_user_ids) for social_sub in social_subs: user_id = social_sub.subscription_user_id - if social_profiles[user_id]['shared_stories_count'] <= 0: + if social_profiles[user_id]["shared_stories_count"] <= 0: continue if update_counts and social_sub.needs_unread_recalc: social_sub.calculate_feed_scores() - + # Combine subscription read counts with feed/user info feed = dict(list(social_sub.canonical().items()) + list(social_profiles[user_id].items())) social_feeds.append(feed) return social_feeds - + @classmethod def feeds_with_updated_counts(cls, user, social_feed_ids=None): feeds = {} - + # Get social subscriptions for user user_subs = cls.objects.filter(user_id=user.pk) if social_feed_ids: - social_user_ids = [int(f.replace('social:', '')) for f in social_feed_ids] + social_user_ids = [int(f.replace("social:", "")) for f in social_feed_ids] user_subs = user_subs.filter(subscription_user_id__in=social_user_ids) profiles = MSocialProfile.objects.filter(user_id__in=social_user_ids) profiles = dict((p.user_id, p) for p in profiles) - + for i, sub in enumerate(user_subs): # Count unreads if subscription is stale. - if (sub.needs_unread_recalc or - (sub.unread_count_updated and - sub.unread_count_updated < user.profile.unread_cutoff) or - (sub.oldest_unread_story_date and - sub.oldest_unread_story_date < user.profile.unread_cutoff)): + if ( + sub.needs_unread_recalc + or (sub.unread_count_updated and sub.unread_count_updated < user.profile.unread_cutoff) + or ( + sub.oldest_unread_story_date and sub.oldest_unread_story_date < user.profile.unread_cutoff + ) + ): sub = sub.calculate_feed_scores(force=True, silent=True) feed_id = "social:%s" % sub.subscription_user_id feeds[feed_id] = { - 'ps': sub.unread_count_positive, - 'nt': sub.unread_count_neutral, - 'ng': sub.unread_count_negative, - 'id': feed_id, + "ps": sub.unread_count_positive, + "nt": sub.unread_count_neutral, + "ng": sub.unread_count_negative, + "id": feed_id, } if social_feed_ids and sub.subscription_user_id in profiles: - feeds[feed_id]['shared_stories_count'] = profiles[sub.subscription_user_id].shared_stories_count + feeds[feed_id]["shared_stories_count"] = profiles[ + sub.subscription_user_id + ].shared_stories_count return feeds - + def canonical(self): return { - 'user_id': self.user_id, - 'active': self.active, - 'subscription_user_id': self.subscription_user_id, - 'nt': self.unread_count_neutral, - 'ps': self.unread_count_positive, - 'ng': self.unread_count_negative, - 'is_trained': self.is_trained, - 'feed_opens': self.feed_opens, + "user_id": self.user_id, + "active": self.active, + "subscription_user_id": self.subscription_user_id, + "nt": self.unread_count_neutral, + "ps": self.unread_count_positive, + "ng": self.unread_count_negative, + "is_trained": self.is_trained, + "feed_opens": self.feed_opens, } @classmethod def subs_for_users(cls, user_id, subscription_user_ids=None, read_filter="unread"): socialsubs = cls.objects if read_filter == "unread": - socialsubs = socialsubs.filter(Q(unread_count_neutral__gt=0) | - Q(unread_count_positive__gt=0)) + socialsubs = socialsubs.filter(Q(unread_count_neutral__gt=0) | Q(unread_count_positive__gt=0)) if not subscription_user_ids: - socialsubs = socialsubs.filter(user_id=user_id)\ - .only('subscription_user_id', 'mark_read_date', 'is_trained') + socialsubs = socialsubs.filter(user_id=user_id).only( + "subscription_user_id", "mark_read_date", "is_trained" + ) else: - socialsubs = socialsubs.filter(user_id=user_id, - subscription_user_id__in=subscription_user_ids)\ - .only('subscription_user_id', 'mark_read_date', 'is_trained') - + socialsubs = socialsubs.filter( + user_id=user_id, subscription_user_id__in=subscription_user_ids + ).only("subscription_user_id", "mark_read_date", "is_trained") + return socialsubs @classmethod - def story_hashes(cls, user_id, relative_user_id, subscription_user_ids=None, socialsubs=None, - read_filter="unread", order="newest", - include_timestamps=False, group_by_user=True, cutoff_date=None): + def story_hashes( + cls, + user_id, + relative_user_id, + subscription_user_ids=None, + socialsubs=None, + read_filter="unread", + order="newest", + include_timestamps=False, + group_by_user=True, + cutoff_date=None, + ): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) pipeline = r.pipeline() story_hashes = {} if group_by_user else [] if not socialsubs: - socialsubs = cls.subs_for_users(relative_user_id, - subscription_user_ids=subscription_user_ids, - read_filter=read_filter) + socialsubs = cls.subs_for_users( + relative_user_id, subscription_user_ids=subscription_user_ids, read_filter=read_filter + ) subscription_user_ids = [sub.subscription_user_id for sub in socialsubs] if not subscription_user_ids: return story_hashes - - current_time = int(time.time() + 60*60*24) + + current_time = int(time.time() + 60 * 60 * 24) if not cutoff_date: cutoff_date = datetime.datetime.now() - datetime.timedelta(days=settings.DAYS_OF_STORY_HASHES) - unread_timestamp = int(time.mktime(cutoff_date.timetuple()))-1000 + unread_timestamp = int(time.mktime(cutoff_date.timetuple())) - 1000 feed_counter = 0 read_dates = dict() for us in socialsubs: - read_dates[us.subscription_user_id] = int(max(us.mark_read_date, cutoff_date).strftime('%s')) + read_dates[us.subscription_user_id] = int(max(us.mark_read_date, cutoff_date).strftime("%s")) for sub_user_id_group in chunks(subscription_user_ids, 20): pipeline = r.pipeline() for sub_user_id in sub_user_id_group: - stories_key = 'B:%s' % (sub_user_id) - sorted_stories_key = 'zB:%s' % (sub_user_id) - read_stories_key = 'RS:%s' % (user_id) - read_social_stories_key = 'RS:%s:B:%s' % (user_id, sub_user_id) - unread_stories_key = 'UB:%s:%s' % (user_id, sub_user_id) - sorted_stories_key = 'zB:%s' % (sub_user_id) - unread_ranked_stories_key = 'zUB:%s:%s' % (user_id, sub_user_id) + stories_key = "B:%s" % (sub_user_id) + sorted_stories_key = "zB:%s" % (sub_user_id) + read_stories_key = "RS:%s" % (user_id) + read_social_stories_key = "RS:%s:B:%s" % (user_id, sub_user_id) + unread_stories_key = "UB:%s:%s" % (user_id, sub_user_id) + sorted_stories_key = "zB:%s" % (sub_user_id) + unread_ranked_stories_key = "zUB:%s:%s" % (user_id, sub_user_id) expire_unread_stories_key = False - + max_score = current_time - if read_filter == 'unread': + if read_filter == "unread": # +1 for the intersection b/w zF and F, which carries an implicit score of 1. min_score = read_dates[sub_user_id] + 1 pipeline.sdiffstore(unread_stories_key, stories_key, read_stories_key) @@ -1023,67 +1124,78 @@ def story_hashes(cls, user_id, relative_user_id, subscription_user_ids=None, soc min_score = unread_timestamp unread_stories_key = stories_key - if order == 'oldest': + if order == "oldest": byscorefunc = pipeline.zrangebyscore else: byscorefunc = pipeline.zrevrangebyscore min_score, max_score = max_score, min_score - + pipeline.zinterstore(unread_ranked_stories_key, [sorted_stories_key, unread_stories_key]) byscorefunc(unread_ranked_stories_key, min_score, max_score, withscores=include_timestamps) pipeline.delete(unread_ranked_stories_key) if expire_unread_stories_key: pipeline.delete(unread_stories_key) - results = pipeline.execute() - + for hashes in results: - if not isinstance(hashes, list): continue + if not isinstance(hashes, list): + continue if group_by_user: story_hashes[subscription_user_ids[feed_counter]] = hashes feed_counter += 1 else: story_hashes.extend(hashes) - + return story_hashes - - def get_stories(self, offset=0, limit=6, order='newest', read_filter='all', - withscores=False, hashes_only=False, cutoff_date=None, - mark_read_complement=False): + + def get_stories( + self, + offset=0, + limit=6, + order="newest", + read_filter="all", + withscores=False, + hashes_only=False, + cutoff_date=None, + mark_read_complement=False, + ): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) ignore_user_stories = False - - stories_key = 'B:%s' % (self.subscription_user_id) - read_stories_key = 'RS:%s' % (self.user_id) - read_social_stories_key = 'RS:%s:B:%s' % (self.user_id, self.subscription_user_id) - unread_stories_key = 'UB:%s:%s' % (self.user_id, self.subscription_user_id) + + stories_key = "B:%s" % (self.subscription_user_id) + read_stories_key = "RS:%s" % (self.user_id) + read_social_stories_key = "RS:%s:B:%s" % (self.user_id, self.subscription_user_id) + unread_stories_key = "UB:%s:%s" % (self.user_id, self.subscription_user_id) if not r.exists(stories_key): return [] - elif read_filter != 'unread' or not r.exists(read_stories_key): + elif read_filter != "unread" or not r.exists(read_stories_key): ignore_user_stories = True unread_stories_key = stories_key else: r.sdiffstore(unread_stories_key, stories_key, read_stories_key) r.sdiffstore(unread_stories_key, unread_stories_key, read_social_stories_key) - sorted_stories_key = 'zB:%s' % (self.subscription_user_id) - unread_ranked_stories_key = 'z%sUB:%s:%s' % ('h' if hashes_only else '', - self.user_id, self.subscription_user_id) + sorted_stories_key = "zB:%s" % (self.subscription_user_id) + unread_ranked_stories_key = "z%sUB:%s:%s" % ( + "h" if hashes_only else "", + self.user_id, + self.subscription_user_id, + ) r.zinterstore(unread_ranked_stories_key, [sorted_stories_key, unread_stories_key]) - - now = datetime.datetime.now() - current_time = int(time.time() + 60*60*24) - mark_read_time = int(time.mktime(self.mark_read_date.timetuple())) + 1 + + now = datetime.datetime.now() + current_time = int(time.time() + 60 * 60 * 24) + mark_read_time = int(time.mktime(self.mark_read_date.timetuple())) + 1 if cutoff_date: - mark_read_time = int(time.mktime(cutoff_date.timetuple())) + 1 - - if order == 'oldest': + mark_read_time = int(time.mktime(cutoff_date.timetuple())) + 1 + + if order == "oldest": byscorefunc = r.zrangebyscore min_score = mark_read_time max_score = current_time - else: # newest + else: # newest byscorefunc = r.zrevrangebyscore min_score = current_time if mark_read_complement: @@ -1092,44 +1204,58 @@ def get_stories(self, offset=0, limit=6, order='newest', read_filter='all', unread_cutoff = cutoff_date if not unread_cutoff: unread_cutoff = now - datetime.timedelta(days=settings.DAYS_OF_UNREAD) - max_score = int(time.mktime(unread_cutoff.timetuple()))-1 + max_score = int(time.mktime(unread_cutoff.timetuple())) - 1 + + story_ids = byscorefunc( + unread_ranked_stories_key, min_score, max_score, start=offset, num=limit, withscores=withscores + ) - story_ids = byscorefunc(unread_ranked_stories_key, min_score, - max_score, start=offset, num=limit, - withscores=withscores) - if withscores: story_ids = [(s[0], int(s[1])) for s in story_ids] - - r.expire(unread_ranked_stories_key, 1*60*60) + + r.expire(unread_ranked_stories_key, 1 * 60 * 60) if not ignore_user_stories: r.delete(unread_stories_key) return story_ids - + @classmethod - def feed_stories(cls, user_id, social_user_ids, offset=0, limit=6, - order='newest', read_filter='all', relative_user_id=None, cache=True, - socialsubs=None, cutoff_date=None, dashboard_global=False): + def feed_stories( + cls, + user_id, + social_user_ids, + offset=0, + limit=6, + order="newest", + read_filter="all", + relative_user_id=None, + cache=True, + socialsubs=None, + cutoff_date=None, + dashboard_global=False, + ): rt = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_TEMP_POOL) - + if not relative_user_id: relative_user_id = user_id - - if order == 'oldest': + + if order == "oldest": range_func = rt.zrange else: range_func = rt.zrevrange - + if not isinstance(social_user_ids, list): social_user_ids = [social_user_ids] - ranked_stories_keys = 'zU:%s:social' % (user_id) - unread_ranked_stories_keys = 'zhU:%s:social' % (user_id) - if ((offset or dashboard_global) and cache and - rt.exists(ranked_stories_keys) and - rt.exists(unread_ranked_stories_keys)): + ranked_stories_keys = "zU:%s:social" % (user_id) + unread_ranked_stories_keys = "zhU:%s:social" % (user_id) + if ( + (offset or dashboard_global) + and cache + and rt.exists(ranked_stories_keys) + and rt.exists(unread_ranked_stories_keys) + ): story_hashes_and_dates = range_func(ranked_stories_keys, offset, limit, withscores=True) if not story_hashes_and_dates: return [], [], [] @@ -1137,22 +1263,26 @@ def feed_stories(cls, user_id, social_user_ids, offset=0, limit=6, if read_filter == "unread": unread_story_hashes = story_hashes else: - unread_story_hashes = range_func(unread_ranked_stories_keys, 0, offset+limit) + unread_story_hashes = range_func(unread_ranked_stories_keys, 0, offset + limit) return story_hashes, story_dates, unread_story_hashes else: rt.delete(ranked_stories_keys) rt.delete(unread_ranked_stories_keys) - - story_hashes = cls.story_hashes(user_id, relative_user_id, - subscription_user_ids=social_user_ids, - read_filter=read_filter, order=order, - include_timestamps=True, - group_by_user=False, - socialsubs=socialsubs, - cutoff_date=cutoff_date) + + story_hashes = cls.story_hashes( + user_id, + relative_user_id, + subscription_user_ids=social_user_ids, + read_filter=read_filter, + order=order, + include_timestamps=True, + group_by_user=False, + socialsubs=socialsubs, + cutoff_date=cutoff_date, + ) if not story_hashes: return [], [], [] - + pipeline = rt.pipeline() for story_hash_group in chunks(story_hashes, 100): pipeline.zadd(ranked_stories_keys, dict(story_hash_group)) @@ -1166,85 +1296,100 @@ def feed_stories(cls, user_id, social_user_ids, offset=0, limit=6, unread_feed_story_hashes = story_hashes rt.zunionstore(unread_ranked_stories_keys, [ranked_stories_keys]) else: - unread_story_hashes = cls.story_hashes(user_id, relative_user_id, - subscription_user_ids=social_user_ids, - read_filter="unread", order=order, - include_timestamps=True, - group_by_user=False, - socialsubs=socialsubs, - cutoff_date=cutoff_date) + unread_story_hashes = cls.story_hashes( + user_id, + relative_user_id, + subscription_user_ids=social_user_ids, + read_filter="unread", + order=order, + include_timestamps=True, + group_by_user=False, + socialsubs=socialsubs, + cutoff_date=cutoff_date, + ) if unread_story_hashes: pipeline = rt.pipeline() for unread_story_hash_group in chunks(unread_story_hashes, 100): pipeline.zadd(unread_ranked_stories_keys, dict(unread_story_hash_group)) pipeline.execute() unread_feed_story_hashes = range_func(unread_ranked_stories_keys, offset, limit) - - rt.expire(ranked_stories_keys, 60*60) - rt.expire(unread_ranked_stories_keys, 60*60) - + + rt.expire(ranked_stories_keys, 60 * 60) + rt.expire(unread_ranked_stories_keys, 60 * 60) + return story_hashes, story_dates, unread_feed_story_hashes def mark_newer_stories_read(self, cutoff_date): - if (self.unread_count_negative == 0 + if ( + self.unread_count_negative == 0 and self.unread_count_neutral == 0 and self.unread_count_positive == 0 - and not self.needs_unread_recalc): + and not self.needs_unread_recalc + ): return - + cutoff_date = cutoff_date - datetime.timedelta(seconds=1) - story_hashes = self.get_stories(limit=500, order="newest", cutoff_date=cutoff_date, - read_filter="unread", hashes_only=True) + story_hashes = self.get_stories( + limit=500, order="newest", cutoff_date=cutoff_date, read_filter="unread", hashes_only=True + ) data = self.mark_story_ids_as_read(story_hashes, aggregated=True) return data - - def mark_story_ids_as_read(self, story_hashes, feed_id=None, mark_all_read=False, request=None, aggregated=False): + + def mark_story_ids_as_read( + self, story_hashes, feed_id=None, mark_all_read=False, request=None, aggregated=False + ): data = dict(code=0, payload=story_hashes) r = redis.Redis(connection_pool=settings.REDIS_POOL) - + if not request: request = User.objects.get(pk=self.user_id) - + if not self.needs_unread_recalc and not mark_all_read: self.needs_unread_recalc = True self.save() - + sub_username = User.objects.get(pk=self.subscription_user_id).username - + if len(story_hashes) > 1: - logging.user(request, "~FYRead %s stories in social subscription: %s" % (len(story_hashes), sub_username)) + logging.user( + request, "~FYRead %s stories in social subscription: %s" % (len(story_hashes), sub_username) + ) else: logging.user(request, "~FYRead story in social subscription: %s" % (sub_username)) - - + for story_hash in set(story_hashes): if feed_id is not None: story_hash = MStory.ensure_story_hash(story_hash, story_feed_id=feed_id) if feed_id is None: feed_id, _ = MStory.split_story_hash(story_hash) - + if len(story_hashes) == 1: RUserStory.aggregate_mark_read(feed_id) - + # Find other social feeds with this story to update their counts friend_key = "F:%s:F" % (self.user_id) share_key = "S:%s" % (story_hash) friends_with_shares = [int(f) for f in r.sinter(share_key, friend_key)] - - RUserStory.mark_read(self.user_id, feed_id, story_hash, social_user_ids=friends_with_shares, - aggregated=(mark_all_read or aggregated)) - + + RUserStory.mark_read( + self.user_id, + feed_id, + story_hash, + social_user_ids=friends_with_shares, + aggregated=(mark_all_read or aggregated), + ) + if self.user_id in friends_with_shares: friends_with_shares.remove(self.user_id) if friends_with_shares: socialsubs = MSocialSubscription.objects.filter( - user_id=self.user_id, - subscription_user_id__in=friends_with_shares) + user_id=self.user_id, subscription_user_id__in=friends_with_shares + ) for socialsub in socialsubs: if not socialsub.needs_unread_recalc and not mark_all_read: socialsub.needs_unread_recalc = True socialsub.save() - + # Also count on original subscription usersubs = UserSubscription.objects.filter(user=self.user_id, feed=feed_id) if usersubs: @@ -1252,38 +1397,37 @@ def mark_story_ids_as_read(self, story_hashes, feed_id=None, mark_all_read=False if not usersub.needs_unread_recalc: usersub.needs_unread_recalc = True usersub.save() - + return data - + @classmethod - def mark_unsub_story_ids_as_read(cls, user_id, social_user_id, story_ids, feed_id=None, - request=None): + def mark_unsub_story_ids_as_read(cls, user_id, social_user_id, story_ids, feed_id=None, request=None): data = dict(code=0, payload=story_ids) r = redis.Redis(connection_pool=settings.REDIS_POOL) if not request: request = User.objects.get(pk=user_id) - + if len(story_ids) > 1: logging.user(request, "~FYRead %s social stories from global" % (len(story_ids))) else: logging.user(request, "~FYRead social story from global") - + for story_id in set(story_ids): try: - story = MSharedStory.objects.get(user_id=social_user_id, - story_guid=story_id) + story = MSharedStory.objects.get(user_id=social_user_id, story_guid=story_id) except MSharedStory.DoesNotExist: continue - + # Find other social feeds with this story to update their counts friend_key = "F:%s:F" % (user_id) share_key = "S:%s" % (story.story_hash) friends_with_shares = [int(f) for f in r.sinter(share_key, friend_key)] - - RUserStory.mark_read(user_id, story.story_feed_id, story.story_hash, - social_user_ids=friends_with_shares) - + + RUserStory.mark_read( + user_id, story.story_feed_id, story.story_hash, social_user_ids=friends_with_shares + ) + # Also count on original subscription usersubs = UserSubscription.objects.filter(user=user_id, feed=story.story_feed_id) if usersubs: @@ -1293,26 +1437,32 @@ def mark_unsub_story_ids_as_read(cls, user_id, social_user_id, story_ids, feed_i usersub.save() # XXX TODO: Real-time notification, just for this user return data - + def mark_feed_read(self, cutoff_date=None): user_profile = Profile.objects.get(user_id=self.user_id) recount = True - + if cutoff_date: cutoff_date = cutoff_date + datetime.timedelta(seconds=1) else: # Use the latest story to get last read time. now = datetime.datetime.now() - latest_shared_story = MSharedStory.objects(user_id=self.subscription_user_id, - shared_date__gte=user_profile.unread_cutoff, - story_date__lte=now - ).order_by('-shared_date').only('shared_date').first() + latest_shared_story = ( + MSharedStory.objects( + user_id=self.subscription_user_id, + shared_date__gte=user_profile.unread_cutoff, + story_date__lte=now, + ) + .order_by("-shared_date") + .only("shared_date") + .first() + ) if latest_shared_story: - cutoff_date = latest_shared_story['shared_date'] + datetime.timedelta(seconds=1) + cutoff_date = latest_shared_story["shared_date"] + datetime.timedelta(seconds=1) else: cutoff_date = datetime.datetime.utcnow() recount = False - + self.last_read_date = cutoff_date self.mark_read_date = cutoff_date self.oldest_unread_story_date = cutoff_date @@ -1324,18 +1474,19 @@ def mark_feed_read(self, cutoff_date=None): self.needs_unread_recalc = False else: self.needs_unread_recalc = True - + # Manually mark all shared stories as read. - unread_story_hashes = self.get_stories(read_filter='unread', limit=500, hashes_only=True, - mark_read_complement=True) + unread_story_hashes = self.get_stories( + read_filter="unread", limit=500, hashes_only=True, mark_read_complement=True + ) self.mark_story_ids_as_read(unread_story_hashes, mark_all_read=True) - + self.save() - + def calculate_feed_scores(self, force=False, silent=False): if not self.needs_unread_recalc and not force: return self - + now = datetime.datetime.now() user_profile = Profile.objects.get(user_id=self.user_id) @@ -1343,9 +1494,9 @@ def calculate_feed_scores(self, force=False, silent=False): # if not silent: # logging.info(' ---> [%s] SKIPPING Computing scores: %s (1 week+)' % (self.user, self.feed)) return self - + feed_scores = dict(negative=0, neutral=0, positive=0) - + # Two weeks in age. If mark_read_date is older, mark old stories as read. date_delta = user_profile.unread_cutoff if date_delta < self.mark_read_date: @@ -1353,95 +1504,117 @@ def calculate_feed_scores(self, force=False, silent=False): else: self.mark_read_date = date_delta - unread_story_hashes = self.get_stories(read_filter='unread', limit=500, hashes_only=True, - cutoff_date=date_delta) - stories_db = MSharedStory.objects(user_id=self.subscription_user_id, - story_hash__in=unread_story_hashes) + unread_story_hashes = self.get_stories( + read_filter="unread", limit=500, hashes_only=True, cutoff_date=date_delta + ) + stories_db = MSharedStory.objects( + user_id=self.subscription_user_id, story_hash__in=unread_story_hashes + ) story_feed_ids = set() for s in stories_db: - story_feed_ids.add(s['story_feed_id']) + story_feed_ids.add(s["story_feed_id"]) story_feed_ids = list(story_feed_ids) usersubs = UserSubscription.objects.filter(user__pk=self.user_id, feed__pk__in=story_feed_ids) usersubs_map = dict((sub.feed_id, sub) for sub in usersubs) - + oldest_unread_story_date = now unread_stories_db = [] for story in stories_db: - if story['story_hash'] not in unread_story_hashes: + if story["story_hash"] not in unread_story_hashes: continue feed_id = story.story_feed_id if usersubs_map.get(feed_id) and story.shared_date < usersubs_map[feed_id].mark_read_date: continue - + unread_stories_db.append(story) if story.shared_date < oldest_unread_story_date: oldest_unread_story_date = story.shared_date stories = Feed.format_stories(unread_stories_db) - classifier_feeds = list(MClassifierFeed.objects(user_id=self.user_id, social_user_id=self.subscription_user_id)) - classifier_authors = list(MClassifierAuthor.objects(user_id=self.user_id, social_user_id=self.subscription_user_id)) - classifier_titles = list(MClassifierTitle.objects(user_id=self.user_id, social_user_id=self.subscription_user_id)) - classifier_tags = list(MClassifierTag.objects(user_id=self.user_id, social_user_id=self.subscription_user_id)) + classifier_feeds = list( + MClassifierFeed.objects(user_id=self.user_id, social_user_id=self.subscription_user_id) + ) + classifier_authors = list( + MClassifierAuthor.objects(user_id=self.user_id, social_user_id=self.subscription_user_id) + ) + classifier_titles = list( + MClassifierTitle.objects(user_id=self.user_id, social_user_id=self.subscription_user_id) + ) + classifier_tags = list( + MClassifierTag.objects(user_id=self.user_id, social_user_id=self.subscription_user_id) + ) # Merge with feed specific classifiers if story_feed_ids: - classifier_feeds = classifier_feeds + list(MClassifierFeed.objects(user_id=self.user_id, - feed_id__in=story_feed_ids)) - classifier_authors = classifier_authors + list(MClassifierAuthor.objects(user_id=self.user_id, - feed_id__in=story_feed_ids)) - classifier_titles = classifier_titles + list(MClassifierTitle.objects(user_id=self.user_id, - feed_id__in=story_feed_ids)) - classifier_tags = classifier_tags + list(MClassifierTag.objects(user_id=self.user_id, - feed_id__in=story_feed_ids)) + classifier_feeds = classifier_feeds + list( + MClassifierFeed.objects(user_id=self.user_id, feed_id__in=story_feed_ids) + ) + classifier_authors = classifier_authors + list( + MClassifierAuthor.objects(user_id=self.user_id, feed_id__in=story_feed_ids) + ) + classifier_titles = classifier_titles + list( + MClassifierTitle.objects(user_id=self.user_id, feed_id__in=story_feed_ids) + ) + classifier_tags = classifier_tags + list( + MClassifierTag.objects(user_id=self.user_id, feed_id__in=story_feed_ids) + ) for story in stories: scores = { - 'feed' : apply_classifier_feeds(classifier_feeds, story['story_feed_id'], - social_user_ids=self.subscription_user_id), - 'author' : apply_classifier_authors(classifier_authors, story), - 'tags' : apply_classifier_tags(classifier_tags, story), - 'title' : apply_classifier_titles(classifier_titles, story), + "feed": apply_classifier_feeds( + classifier_feeds, story["story_feed_id"], social_user_ids=self.subscription_user_id + ), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } - - max_score = max(scores['author'], scores['tags'], scores['title']) - min_score = min(scores['author'], scores['tags'], scores['title']) - + + max_score = max(scores["author"], scores["tags"], scores["title"]) + min_score = min(scores["author"], scores["tags"], scores["title"]) + if max_score > 0: - feed_scores['positive'] += 1 + feed_scores["positive"] += 1 elif min_score < 0: - feed_scores['negative'] += 1 + feed_scores["negative"] += 1 else: - if scores['feed'] > 0: - feed_scores['positive'] += 1 - elif scores['feed'] < 0: - feed_scores['negative'] += 1 + if scores["feed"] > 0: + feed_scores["positive"] += 1 + elif scores["feed"] < 0: + feed_scores["negative"] += 1 else: - feed_scores['neutral'] += 1 - - - self.unread_count_positive = feed_scores['positive'] - self.unread_count_neutral = feed_scores['neutral'] - self.unread_count_negative = feed_scores['negative'] + feed_scores["neutral"] += 1 + + self.unread_count_positive = feed_scores["positive"] + self.unread_count_neutral = feed_scores["neutral"] + self.unread_count_negative = feed_scores["negative"] self.unread_count_updated = datetime.datetime.now() self.oldest_unread_story_date = oldest_unread_story_date self.needs_unread_recalc = False - + self.save() - if (self.unread_count_positive == 0 and - self.unread_count_neutral == 0): + if self.unread_count_positive == 0 and self.unread_count_neutral == 0: self.mark_feed_read() - + if not silent: - logging.info(' ---> [%s] Computing social scores: %s (%s/%s/%s)' % (user_profile, self.subscription_user_id, feed_scores['negative'], feed_scores['neutral'], feed_scores['positive'])) - + logging.info( + " ---> [%s] Computing social scores: %s (%s/%s/%s)" + % ( + user_profile, + self.subscription_user_id, + feed_scores["negative"], + feed_scores["neutral"], + feed_scores["positive"], + ) + ) + return self - + @classmethod def mark_dirty_sharing_story(cls, user_id, story_feed_id, story_guid_hash): r = redis.Redis(connection_pool=settings.REDIS_POOL) - + friends_key = "F:%s:F" % (user_id) share_key = "S:%s:%s" % (story_feed_id, story_guid_hash) following_user_ids = r.sinter(friends_key, share_key) @@ -1455,90 +1628,99 @@ def mark_dirty_sharing_story(cls, user_id, story_feed_id, story_guid_hash): social_sub.save() return social_subs + class MCommentReply(mongo.EmbeddedDocument): - reply_id = mongo.ObjectIdField() - user_id = mongo.IntField() - publish_date = mongo.DateTimeField() - comments = mongo.StringField() - email_sent = mongo.BooleanField(default=False) - liking_users = mongo.ListField(mongo.IntField()) - + reply_id = mongo.ObjectIdField() + user_id = mongo.IntField() + publish_date = mongo.DateTimeField() + comments = mongo.StringField() + email_sent = mongo.BooleanField(default=False) + liking_users = mongo.ListField(mongo.IntField()) + def canonical(self): reply = { - 'reply_id': self.reply_id, - 'user_id': self.user_id, - 'publish_date': relative_timesince(self.publish_date), - 'date': self.publish_date, - 'comments': self.comments, + "reply_id": self.reply_id, + "user_id": self.user_id, + "publish_date": relative_timesince(self.publish_date), + "date": self.publish_date, + "comments": self.comments, } return reply - + meta = { - 'ordering': ['publish_date'], - 'id_field': 'reply_id', - 'allow_inheritance': False, - 'strict': False, + "ordering": ["publish_date"], + "id_field": "reply_id", + "allow_inheritance": False, + "strict": False, } class MSharedStory(mongo.DynamicDocument): - user_id = mongo.IntField() - shared_date = mongo.DateTimeField() - comments = mongo.StringField() - has_comments = mongo.BooleanField(default=False) - has_replies = mongo.BooleanField(default=False) - replies = mongo.ListField(mongo.EmbeddedDocumentField(MCommentReply)) - source_user_id = mongo.IntField() - story_hash = mongo.StringField() - story_feed_id = mongo.IntField() - story_date = mongo.DateTimeField() - story_title = mongo.StringField(max_length=1024) - story_content = mongo.StringField() - story_content_z = mongo.BinaryField() - story_original_content = mongo.StringField() + user_id = mongo.IntField() + shared_date = mongo.DateTimeField() + comments = mongo.StringField() + has_comments = mongo.BooleanField(default=False) + has_replies = mongo.BooleanField(default=False) + replies = mongo.ListField(mongo.EmbeddedDocumentField(MCommentReply)) + source_user_id = mongo.IntField() + story_hash = mongo.StringField() + story_feed_id = mongo.IntField() + story_date = mongo.DateTimeField() + story_title = mongo.StringField(max_length=1024) + story_content = mongo.StringField() + story_content_z = mongo.BinaryField() + story_original_content = mongo.StringField() story_original_content_z = mongo.BinaryField() - original_text_z = mongo.BinaryField() - original_page_z = mongo.BinaryField() - story_content_type = mongo.StringField(max_length=255) - story_author_name = mongo.StringField() - story_permalink = mongo.StringField() - story_guid = mongo.StringField(unique_with=('user_id',)) - story_guid_hash = mongo.StringField(max_length=6) - image_urls = mongo.ListField(mongo.StringField(max_length=1024)) - story_tags = mongo.ListField(mongo.StringField(max_length=250)) - posted_to_services = mongo.ListField(mongo.StringField(max_length=20)) - mute_email_users = mongo.ListField(mongo.IntField()) - liking_users = mongo.ListField(mongo.IntField()) - emailed_reshare = mongo.BooleanField(default=False) - emailed_replies = mongo.ListField(mongo.ObjectIdField()) - image_count = mongo.IntField() - image_sizes = mongo.ListField(mongo.DictField()) - + original_text_z = mongo.BinaryField() + original_page_z = mongo.BinaryField() + story_content_type = mongo.StringField(max_length=255) + story_author_name = mongo.StringField() + story_permalink = mongo.StringField() + story_guid = mongo.StringField(unique_with=("user_id",)) + story_guid_hash = mongo.StringField(max_length=6) + image_urls = mongo.ListField(mongo.StringField(max_length=1024)) + story_tags = mongo.ListField(mongo.StringField(max_length=250)) + posted_to_services = mongo.ListField(mongo.StringField(max_length=20)) + mute_email_users = mongo.ListField(mongo.IntField()) + liking_users = mongo.ListField(mongo.IntField()) + emailed_reshare = mongo.BooleanField(default=False) + emailed_replies = mongo.ListField(mongo.ObjectIdField()) + image_count = mongo.IntField() + image_sizes = mongo.ListField(mongo.DictField()) + meta = { - 'collection': 'shared_stories', - 'indexes': [('user_id', '-shared_date'), ('user_id', 'story_feed_id'), - 'shared_date', 'story_guid', 'story_feed_id', 'story_hash'], - 'ordering': ['-shared_date'], - 'allow_inheritance': False, - 'strict': False, + "collection": "shared_stories", + "indexes": [ + ("user_id", "-shared_date"), + ("user_id", "story_feed_id"), + "shared_date", + "story_guid", + "story_feed_id", + "story_hash", + ], + "ordering": ["-shared_date"], + "allow_inheritance": False, + "strict": False, } def __str__(self): user = User.objects.get(pk=self.user_id) - return "%s: %s (%s)%s%s" % (user.username, - self.decoded_story_title[:20], - self.story_feed_id, - ': ' if self.has_comments else '', - self.comments[:20]) + return "%s: %s (%s)%s%s" % ( + user.username, + self.decoded_story_title[:20], + self.story_feed_id, + ": " if self.has_comments else "", + self.comments[:20], + ) @property def guid_hash(self): - return hashlib.sha1(self.story_guid.encode('utf-8')).hexdigest()[:6] - + return hashlib.sha1(self.story_guid.encode("utf-8")).hexdigest()[:6] + @property def feed_guid_hash(self): return "%s:%s" % (self.story_feed_id or "0", self.guid_hash) - + @property def decoded_story_title(self): return pyhtml.unescape(self.story_title) @@ -1550,7 +1732,7 @@ def story_content_str(self): story_content = smart_str(zlib.decompress(self.story_content_z)) else: story_content = smart_str(story_content) - + return story_content def canonical(self): @@ -1561,7 +1743,7 @@ def canonical(self): "story_content": self.story_content_z and zlib.decompress(self.story_content_z), "comments": self.comments, } - + def save(self, *args, **kwargs): scrubber = SelectiveScriptScrubber() @@ -1583,23 +1765,29 @@ def save(self, *args, **kwargs): self.shared_date = self.shared_date or datetime.datetime.utcnow() self.has_replies = bool(len(self.replies)) - + super(MSharedStory, self).save(*args, **kwargs) - + author = MSocialProfile.get_user(self.user_id) author.count_follows() - + self.sync_redis() - - MActivity.new_shared_story(user_id=self.user_id, source_user_id=self.source_user_id, - story_title=self.story_title, - comments=self.comments, story_feed_id=self.story_feed_id, - story_id=self.story_guid, share_date=self.shared_date) + + MActivity.new_shared_story( + user_id=self.user_id, + source_user_id=self.source_user_id, + story_title=self.story_title, + comments=self.comments, + story_feed_id=self.story_feed_id, + story_id=self.story_guid, + share_date=self.shared_date, + ) return self - + def delete(self, *args, **kwargs): - MActivity.remove_shared_story(user_id=self.user_id, story_feed_id=self.story_feed_id, - story_id=self.story_guid) + MActivity.remove_shared_story( + user_id=self.user_id, story_feed_id=self.story_feed_id, story_id=self.story_guid + ) self.remove_from_redis() @@ -1608,48 +1796,52 @@ def delete(self, *args, **kwargs): @classmethod def trim_old_stories(cls, stories=10, days=90, dryrun=False): print(" ---> Fetching shared story counts...") - stats = settings.MONGODB.newsblur.shared_stories.aggregate([{ - "$group": { - "_id": "$user_id", - "stories": {"$sum": 1}, - }, - }, { - "$match": { - "stories": {"$gte": stories} - }, - }]) + stats = settings.MONGODB.newsblur.shared_stories.aggregate( + [ + { + "$group": { + "_id": "$user_id", + "stories": {"$sum": 1}, + }, + }, + { + "$match": {"stories": {"$gte": stories}}, + }, + ] + ) month_ago = datetime.datetime.now() - datetime.timedelta(days=days) user_ids = list(stats) - user_ids = sorted(user_ids, key=lambda x:x['stories'], reverse=True) + user_ids = sorted(user_ids, key=lambda x: x["stories"], reverse=True) print(" ---> Found %s users with more than %s starred stories" % (len(user_ids), stories)) total = 0 for stat in user_ids: try: - user = User.objects.select_related('profile').get(pk=stat['_id']) + user = User.objects.select_related("profile").get(pk=stat["_id"]) except User.DoesNotExist: user = None - + if user and (user.profile.is_premium or user.profile.last_seen_on > month_ago): continue - - total += stat['stories'] - username = "%s (%s)" % (user and user.username or " - ", stat['_id']) - print(" ---> %19.19s: %-20.20s %s stories" % (user and user.profile.last_seen_on or "Deleted", - username, - stat['stories'])) - if not dryrun and stat['_id']: - cls.objects.filter(user_id=stat['_id']).delete() - elif not dryrun and stat['_id'] == 0: + + total += stat["stories"] + username = "%s (%s)" % (user and user.username or " - ", stat["_id"]) + print( + " ---> %19.19s: %-20.20s %s stories" + % (user and user.profile.last_seen_on or "Deleted", username, stat["stories"]) + ) + if not dryrun and stat["_id"]: + cls.objects.filter(user_id=stat["_id"]).delete() + elif not dryrun and stat["_id"] == 0: print(" ---> Deleting unshared stories (user_id = 0)") - cls.objects.filter(user_id=stat['_id']).delete() - - + cls.objects.filter(user_id=stat["_id"]).delete() + print(" ---> Deleted %s stories in total." % total) - + def unshare_story(self): - socialsubs = MSocialSubscription.objects.filter(subscription_user_id=self.user_id, - needs_unread_recalc=False) + socialsubs = MSocialSubscription.objects.filter( + subscription_user_id=self.user_id, needs_unread_recalc=False + ) for socialsub in socialsubs: socialsub.needs_unread_recalc = True socialsub.save() @@ -1660,23 +1852,30 @@ def publish_to_subscribers(self): feed = Feed.get_by_id(self.story_feed_id) try: r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) - r.publish("social:%s:story" % (self.user_id), '%s,%s' % (self.story_hash, self.shared_date.strftime('%s'))) - logging.debug(" ***> [%-30s] ~BMPublishing to Redis for real-time." % (feed.title[:30] if feed else "NO FEED")) + r.publish( + "social:%s:story" % (self.user_id), + "%s,%s" % (self.story_hash, self.shared_date.strftime("%s")), + ) + logging.debug( + " ***> [%-30s] ~BMPublishing to Redis for real-time." + % (feed.title[:30] if feed else "NO FEED") + ) except redis.ConnectionError: - logging.debug(" ***> [%-30s] ~BMRedis is unavailable for real-time." % (feed.title[:30] if feed else "NO FEED")) - + logging.debug( + " ***> [%-30s] ~BMRedis is unavailable for real-time." + % (feed.title[:30] if feed else "NO FEED") + ) + @classmethod def feed_quota(cls, user_id, story_hash, feed_id=None, days=1, quota=1): - day_ago = datetime.datetime.now()-datetime.timedelta(days=days) - params = dict(user_id=user_id, - shared_date__gte=day_ago, - story_hash__nin=[story_hash]) + day_ago = datetime.datetime.now() - datetime.timedelta(days=days) + params = dict(user_id=user_id, shared_date__gte=day_ago, story_hash__nin=[story_hash]) if feed_id: - params['story_feed_id'] = feed_id + params["story_feed_id"] = feed_id shared_count = cls.objects.filter(**params).count() return shared_count >= quota - + @classmethod def count_potential_spammers(cls, days=1, destroy=False): try: @@ -1684,50 +1883,59 @@ def count_potential_spammers(cls, days=1, destroy=False): except NameError: logging.debug(" ---> ~FR~SNMissing ~SBspam.py~SN") guaranteed_spammers = [] - + return guaranteed_spammers - + @classmethod def get_shared_stories_from_site(cls, feed_id, user_id, story_url, limit=3): - your_story = cls.objects.filter(story_feed_id=feed_id, - story_permalink=story_url, - user_id=user_id).limit(1).first() - same_stories = cls.objects.filter(story_feed_id=feed_id, - story_permalink=story_url, - user_id__ne=user_id - ).order_by('-shared_date') - - same_stories = [{ - "user_id": story.user_id, - "comments": story.comments, - "relative_date": relative_timesince(story.shared_date), - "blurblog_permalink": story.blurblog_permalink(), - } for story in same_stories] - - other_stories = [] - if feed_id: - other_stories = cls.objects.filter(story_feed_id=feed_id, - story_permalink__ne=story_url - ).order_by('-shared_date').limit(limit) - other_stories = [{ + your_story = ( + cls.objects.filter(story_feed_id=feed_id, story_permalink=story_url, user_id=user_id) + .limit(1) + .first() + ) + same_stories = cls.objects.filter( + story_feed_id=feed_id, story_permalink=story_url, user_id__ne=user_id + ).order_by("-shared_date") + + same_stories = [ + { "user_id": story.user_id, - "story_title": story.story_title, - "story_permalink": story.story_permalink, "comments": story.comments, "relative_date": relative_timesince(story.shared_date), "blurblog_permalink": story.blurblog_permalink(), - } for story in other_stories] - + } + for story in same_stories + ] + + other_stories = [] + if feed_id: + other_stories = ( + cls.objects.filter(story_feed_id=feed_id, story_permalink__ne=story_url) + .order_by("-shared_date") + .limit(limit) + ) + other_stories = [ + { + "user_id": story.user_id, + "story_title": story.story_title, + "story_permalink": story.story_permalink, + "comments": story.comments, + "relative_date": relative_timesince(story.shared_date), + "blurblog_permalink": story.blurblog_permalink(), + } + for story in other_stories + ] + return your_story, same_stories, other_stories - + def set_source_user_id(self, source_user_id): if source_user_id == self.user_id: return - + def find_source(source_user_id, seen_user_ids): - parent_shared_story = MSharedStory.objects.filter(user_id=source_user_id, - story_guid=self.story_guid, - story_feed_id=self.story_feed_id).limit(1) + parent_shared_story = MSharedStory.objects.filter( + user_id=source_user_id, story_guid=self.story_guid, story_feed_id=self.story_feed_id + ).limit(1) if parent_shared_story and parent_shared_story[0].source_user_id: user_id = parent_shared_story[0].source_user_id if user_id in seen_user_ids: @@ -1737,7 +1945,7 @@ def find_source(source_user_id, seen_user_ids): return find_source(user_id, seen_user_ids) else: return source_user_id - + if source_user_id: source_user_id = find_source(source_user_id, []) if source_user_id == self.user_id: @@ -1746,19 +1954,21 @@ def find_source(source_user_id, seen_user_ids): self.source_user_id = source_user_id logging.debug(" ---> Re-share from %s." % source_user_id) self.save() - - MInteraction.new_reshared_story(user_id=self.source_user_id, - reshare_user_id=self.user_id, - comments=self.comments, - story_title=self.story_title, - story_feed_id=self.story_feed_id, - story_id=self.story_guid) - + + MInteraction.new_reshared_story( + user_id=self.source_user_id, + reshare_user_id=self.user_id, + comments=self.comments, + story_title=self.story_title, + story_feed_id=self.story_feed_id, + story_id=self.story_guid, + ) + def mute_for_user(self, user_id): if user_id not in self.mute_email_users: self.mute_email_users.append(user_id) self.save() - + @classmethod def switch_feed(cls, original_feed_id, duplicate_feed_id): shared_stories = cls.objects.filter(story_feed_id=duplicate_feed_id) @@ -1766,7 +1976,7 @@ def switch_feed(cls, original_feed_id, duplicate_feed_id): for story in shared_stories: story.story_feed_id = original_feed_id story.save() - + @classmethod def collect_popular_stories(cls, cutoff=None, days=None, shared_feed_ids=None): if not days: @@ -1778,7 +1988,7 @@ def collect_popular_stories(cls, cutoff=None, days=None, shared_feed_ids=None): # shared_stories_count = sum(json.decode(MStatistics.get('stories_shared'))) # cutoff = cutoff or max(math.floor(.025 * shared_stories_count), 3) today = datetime.datetime.now() - datetime.timedelta(days=days) - + map_f = """ function() { emit(this.story_hash, { @@ -1809,74 +2019,82 @@ def collect_popular_stories(cls, cutoff=None, days=None, shared_feed_ids=None): return value; } } - """ % {'cutoff': cutoff, 'shared_feed_ids': ', '.join(shared_feed_ids)} - res = cls.objects(shared_date__gte=today).map_reduce(map_f, reduce_f, - finalize_f=finalize_f, - output='inline') + """ % { + "cutoff": cutoff, + "shared_feed_ids": ", ".join(shared_feed_ids), + } + res = cls.objects(shared_date__gte=today).map_reduce( + map_f, reduce_f, finalize_f=finalize_f, output="inline" + ) stories = dict([(r.key, r.value) for r in res if r.value]) return stories, cutoff - + @classmethod def share_popular_stories(cls, cutoff=None, days=None, interactive=True): publish_new_stories = False - popular_profile = MSocialProfile.objects.get(user_id=User.objects.get(username='popular').pk) + popular_profile = MSocialProfile.objects.get(user_id=User.objects.get(username="popular").pk) popular_user = User.objects.get(pk=popular_profile.user_id) week_ago = datetime.datetime.now() - datetime.timedelta(days=7) - shared_feed_ids = [str(s.story_feed_id) - for s in MSharedStory.objects(user_id=popular_profile.user_id, - shared_date__gte=week_ago).only('story_feed_id')] - shared_stories_today, cutoff = cls.collect_popular_stories(cutoff=cutoff, days=days, - shared_feed_ids=shared_feed_ids) + shared_feed_ids = [ + str(s.story_feed_id) + for s in MSharedStory.objects(user_id=popular_profile.user_id, shared_date__gte=week_ago).only( + "story_feed_id" + ) + ] + shared_stories_today, cutoff = cls.collect_popular_stories( + cutoff=cutoff, days=days, shared_feed_ids=shared_feed_ids + ) shared = 0 - + for story_hash, story_info in list(shared_stories_today.items()): - story, _ = MStory.find_story(story_info['feed_id'], story_info['story_hash']) + story, _ = MStory.find_story(story_info["feed_id"], story_info["story_hash"]) if not story: logging.user(popular_user, "~FRPopular stories, story not found: %s" % story_info) continue if story.story_feed_id in shared_feed_ids: logging.user(popular_user, "~FRPopular stories, story feed just shared: %s" % story_info) continue - + if interactive: feed = Feed.get_by_id(story.story_feed_id) accept_story = eval(input("%s / %s [Y/n]: " % (story.decoded_story_title, feed.title))) - if accept_story in ['n', 'N']: continue - - story_db = dict([(k, v) for k, v in list(story._data.items()) - if k is not None and v is not None]) - story_db.pop('user_id', None) - story_db.pop('id', None) - story_db.pop('comments', None) - story_db.pop('replies', None) - story_db['has_comments'] = False - story_db['has_replies'] = False - story_db['shared_date'] = datetime.datetime.now() + if accept_story in ["n", "N"]: + continue + + story_db = dict([(k, v) for k, v in list(story._data.items()) if k is not None and v is not None]) + story_db.pop("user_id", None) + story_db.pop("id", None) + story_db.pop("comments", None) + story_db.pop("replies", None) + story_db["has_comments"] = False + story_db["has_replies"] = False + story_db["shared_date"] = datetime.datetime.now() story_values = { - 'user_id': popular_profile.user_id, - 'story_guid': story_db['story_guid'], + "user_id": popular_profile.user_id, + "story_guid": story_db["story_guid"], } try: shared_story = MSharedStory.objects.get(**story_values) except MSharedStory.DoesNotExist: story_values.update(story_db) shared_story = MSharedStory.objects.create(**story_values) - shared_story.post_to_service('twitter') + shared_story.post_to_service("twitter") shared += 1 shared_feed_ids.append(story.story_feed_id) publish_new_stories = True - logging.user(popular_user, "~FCSharing: ~SB~FM%s (%s shares, %s min)" % ( - story.decoded_story_title[:50], - story_info['count'], - cutoff)) - + logging.user( + popular_user, + "~FCSharing: ~SB~FM%s (%s shares, %s min)" + % (story.decoded_story_title[:50], story_info["count"], cutoff), + ) + if publish_new_stories: socialsubs = MSocialSubscription.objects.filter(subscription_user_id=popular_user.pk) for socialsub in socialsubs: socialsub.needs_unread_recalc = True socialsub.save() shared_story.publish_update_to_subscribers() - + return shared @staticmethod @@ -1884,13 +2102,13 @@ def check_shared_story_hashes(user_id, story_hashes, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_POOL) pipeline = r.pipeline() - + for story_hash in story_hashes: feed_id, guid_hash = MStory.split_story_hash(story_hash) share_key = "S:%s:%s" % (feed_id, guid_hash) pipeline.sismember(share_key, user_id) shared_hashes = pipeline.execute() - + return [story_hash for s, story_hash in enumerate(story_hashes) if shared_hashes[s]] @classmethod @@ -1907,7 +2125,7 @@ def sync_all_redis(cls, drop=False): for story in cls.objects.all(): story.sync_redis_shares(r=r) story.sync_redis_story(r=h) - + def sync_redis(self): self.sync_redis_shares() self.sync_redis_story() @@ -1915,8 +2133,8 @@ def sync_redis(self): def sync_redis_shares(self, r=None): if not r: r = redis.Redis(connection_pool=settings.REDIS_POOL) - - share_key = "S:%s:%s" % (self.story_feed_id, self.guid_hash) + + share_key = "S:%s:%s" % (self.story_feed_id, self.guid_hash) comment_key = "C:%s:%s" % (self.story_feed_id, self.guid_hash) r.sadd(share_key, self.user_id) if self.has_comments: @@ -1929,20 +2147,18 @@ def sync_redis_story(self, r=None): r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) # if not r2: # r2 = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL2) - - r.sadd('B:%s' % self.user_id, self.feed_guid_hash) + + r.sadd("B:%s" % self.user_id, self.feed_guid_hash) # r2.sadd('B:%s' % self.user_id, self.feed_guid_hash) - redis_data = { - self.feed_guid_hash : time.mktime(self.shared_date.timetuple()) - } - r.zadd('zB:%s' % self.user_id, redis_data) + redis_data = {self.feed_guid_hash: time.mktime(self.shared_date.timetuple())} + r.zadd("zB:%s" % self.user_id, redis_data) # r2.zadd('zB:%s' % self.user_id, {self.feed_guid_hash: # time.mktime(self.shared_date.timetuple())}) - r.expire('B:%s' % self.user_id, settings.DAYS_OF_STORY_HASHES*24*60*60) + r.expire("B:%s" % self.user_id, settings.DAYS_OF_STORY_HASHES * 24 * 60 * 60) # r2.expire('B:%s' % self.user_id, settings.DAYS_OF_STORY_HASHES*24*60*60) - r.expire('zB:%s' % self.user_id, settings.DAYS_OF_STORY_HASHES*24*60*60) + r.expire("zB:%s" % self.user_id, settings.DAYS_OF_STORY_HASHES * 24 * 60 * 60) # r2.expire('zB:%s' % self.user_id, settings.DAYS_OF_STORY_HASHES*24*60*60) - + def remove_from_redis(self): r = redis.Redis(connection_pool=settings.REDIS_POOL) share_key = "S:%s:%s" % (self.story_feed_id, self.guid_hash) @@ -1953,16 +2169,16 @@ def remove_from_redis(self): h = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) # h2 = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL2) - h.srem('B:%s' % self.user_id, self.feed_guid_hash) + h.srem("B:%s" % self.user_id, self.feed_guid_hash) # h2.srem('B:%s' % self.user_id, self.feed_guid_hash) - h.zrem('zB:%s' % self.user_id, self.feed_guid_hash) + h.zrem("zB:%s" % self.user_id, self.feed_guid_hash) # h2.zrem('zB:%s' % self.user_id, self.feed_guid_hash) def publish_update_to_subscribers(self): try: r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL) feed_id = "social:%s" % self.user_id - listeners_count = r.publish("%s:story" % feed_id, 'story:new:%s' % self.story_hash) + listeners_count = r.publish("%s:story" % feed_id, "story:new:%s" % self.story_hash) if listeners_count: logging.debug(" ---> ~FMPublished to %s subscribers" % (listeners_count)) except redis.ConnectionError: @@ -1970,178 +2186,191 @@ def publish_update_to_subscribers(self): def comments_with_author(self): comments = { - 'id': self.id, - 'user_id': self.user_id, - 'comments': self.comments, - 'shared_date': relative_timesince(self.shared_date), - 'date': self.shared_date, - 'replies': [reply.canonical() for reply in self.replies], - 'liking_users': self.liking_users and list(self.liking_users), - 'source_user_id': self.source_user_id, + "id": self.id, + "user_id": self.user_id, + "comments": self.comments, + "shared_date": relative_timesince(self.shared_date), + "date": self.shared_date, + "replies": [reply.canonical() for reply in self.replies], + "liking_users": self.liking_users and list(self.liking_users), + "source_user_id": self.source_user_id, } return comments - + def comment_with_author_and_profiles(self): comment = self.comments_with_author() - profile_user_ids = set([comment['user_id']]) - reply_user_ids = [reply['user_id'] for reply in comment['replies']] + profile_user_ids = set([comment["user_id"]]) + reply_user_ids = [reply["user_id"] for reply in comment["replies"]] profile_user_ids = profile_user_ids.union(reply_user_ids) - profile_user_ids = profile_user_ids.union(comment['liking_users']) - if comment['source_user_id']: - profile_user_ids.add(comment['source_user_id']) + profile_user_ids = profile_user_ids.union(comment["liking_users"]) + if comment["source_user_id"]: + profile_user_ids.add(comment["source_user_id"]) profiles = MSocialProfile.objects.filter(user_id__in=list(profile_user_ids)) profiles = [profile.canonical(compact=True) for profile in profiles] return comment, profiles - + @classmethod def stories_with_comments_and_profiles(cls, stories, user_id, check_all=False): r = redis.Redis(connection_pool=settings.REDIS_POOL) friend_key = "F:%s:F" % (user_id) profile_user_ids = set() - for story in stories: - story['friend_comments'] = [] - story['friend_shares'] = [] - story['public_comments'] = [] - story['reply_count'] = 0 - if check_all or story['comment_count']: - comment_key = "C:%s:%s" % (story['story_feed_id'], story['guid_hash']) - story['comment_count'] = r.scard(comment_key) + for story in stories: + story["friend_comments"] = [] + story["friend_shares"] = [] + story["public_comments"] = [] + story["reply_count"] = 0 + if check_all or story["comment_count"]: + comment_key = "C:%s:%s" % (story["story_feed_id"], story["guid_hash"]) + story["comment_count"] = r.scard(comment_key) friends_with_comments = [int(f) for f in r.sinter(comment_key, friend_key)] sharer_user_ids = [int(f) for f in r.smembers(comment_key)] shared_stories = [] if sharer_user_ids: params = { - 'story_hash': story['story_hash'], - 'user_id__in': sharer_user_ids, + "story_hash": story["story_hash"], + "user_id__in": sharer_user_ids, } - if 'story_db_id' in params: - params.pop('story_db_id') - shared_stories = cls.objects.filter(**params)\ - .hint([('story_hash', 1)]) + if "story_db_id" in params: + params.pop("story_db_id") + shared_stories = cls.objects.filter(**params).hint([("story_hash", 1)]) for shared_story in shared_stories: comments = shared_story.comments_with_author() - story['reply_count'] += len(comments['replies']) + story["reply_count"] += len(comments["replies"]) if shared_story.user_id in friends_with_comments: - story['friend_comments'].append(comments) + story["friend_comments"].append(comments) else: - story['public_comments'].append(comments) - if comments.get('source_user_id'): - profile_user_ids.add(comments['source_user_id']) - if comments.get('liking_users'): - profile_user_ids = profile_user_ids.union(comments['liking_users']) - all_comments = story['friend_comments'] + story['public_comments'] - profile_user_ids = profile_user_ids.union([reply['user_id'] - for c in all_comments - for reply in c['replies']]) - if story.get('source_user_id'): - profile_user_ids.add(story['source_user_id']) - story['comment_count_friends'] = len(friends_with_comments) - story['comment_count_public'] = story['comment_count'] - len(friends_with_comments) - - if check_all or story['share_count']: - share_key = "S:%s:%s" % (story['story_feed_id'], story['guid_hash']) - story['share_count'] = r.scard(share_key) + story["public_comments"].append(comments) + if comments.get("source_user_id"): + profile_user_ids.add(comments["source_user_id"]) + if comments.get("liking_users"): + profile_user_ids = profile_user_ids.union(comments["liking_users"]) + all_comments = story["friend_comments"] + story["public_comments"] + profile_user_ids = profile_user_ids.union( + [reply["user_id"] for c in all_comments for reply in c["replies"]] + ) + if story.get("source_user_id"): + profile_user_ids.add(story["source_user_id"]) + story["comment_count_friends"] = len(friends_with_comments) + story["comment_count_public"] = story["comment_count"] - len(friends_with_comments) + + if check_all or story["share_count"]: + share_key = "S:%s:%s" % (story["story_feed_id"], story["guid_hash"]) + story["share_count"] = r.scard(share_key) friends_with_shares = [int(f) for f in r.sinter(share_key, friend_key)] nonfriend_user_ids = [int(f) for f in r.sdiff(share_key, friend_key)] profile_user_ids.update(nonfriend_user_ids) profile_user_ids.update(friends_with_shares) - story['commented_by_public'] = [c['user_id'] for c in story['public_comments']] - story['commented_by_friends'] = [c['user_id'] for c in story['friend_comments']] - story['shared_by_public'] = list(set(nonfriend_user_ids) - - set(story['commented_by_public'])) - story['shared_by_friends'] = list(set(friends_with_shares) - - set(story['commented_by_friends'])) - story['share_count_public'] = story['share_count'] - len(friends_with_shares) - story['share_count_friends'] = len(friends_with_shares) - story['friend_user_ids'] = list(set(story['commented_by_friends'] + story['shared_by_friends'])) - story['public_user_ids'] = list(set(story['commented_by_public'] + story['shared_by_public'])) - if not story['share_user_ids']: - story['share_user_ids'] = story['friend_user_ids'] + story['public_user_ids'] - if story.get('source_user_id'): - profile_user_ids.add(story['source_user_id']) + story["commented_by_public"] = [c["user_id"] for c in story["public_comments"]] + story["commented_by_friends"] = [c["user_id"] for c in story["friend_comments"]] + story["shared_by_public"] = list(set(nonfriend_user_ids) - set(story["commented_by_public"])) + story["shared_by_friends"] = list( + set(friends_with_shares) - set(story["commented_by_friends"]) + ) + story["share_count_public"] = story["share_count"] - len(friends_with_shares) + story["share_count_friends"] = len(friends_with_shares) + story["friend_user_ids"] = list( + set(story["commented_by_friends"] + story["shared_by_friends"]) + ) + story["public_user_ids"] = list(set(story["commented_by_public"] + story["shared_by_public"])) + if not story["share_user_ids"]: + story["share_user_ids"] = story["friend_user_ids"] + story["public_user_ids"] + if story.get("source_user_id"): + profile_user_ids.add(story["source_user_id"]) shared_stories = [] - if story['shared_by_friends']: + if story["shared_by_friends"]: params = { - 'story_hash': story['story_hash'], - 'user_id__in': story['shared_by_friends'], + "story_hash": story["story_hash"], + "user_id__in": story["shared_by_friends"], } - shared_stories = cls.objects.filter(**params)\ - .hint([('story_hash', 1)]) + shared_stories = cls.objects.filter(**params).hint([("story_hash", 1)]) for shared_story in shared_stories: comments = shared_story.comments_with_author() - story['reply_count'] += len(comments['replies']) - story['friend_shares'].append(comments) - profile_user_ids = profile_user_ids.union([reply['user_id'] - for reply in comments['replies']]) - if comments.get('source_user_id'): - profile_user_ids.add(comments['source_user_id']) - if comments.get('liking_users'): - profile_user_ids = profile_user_ids.union(comments['liking_users']) - + story["reply_count"] += len(comments["replies"]) + story["friend_shares"].append(comments) + profile_user_ids = profile_user_ids.union( + [reply["user_id"] for reply in comments["replies"]] + ) + if comments.get("source_user_id"): + profile_user_ids.add(comments["source_user_id"]) + if comments.get("liking_users"): + profile_user_ids = profile_user_ids.union(comments["liking_users"]) + profiles = MSocialProfile.objects.filter(user_id__in=list(profile_user_ids)) - + # Toss public comments by private profiles and muted users - profiles_dict = dict((profile['user_id'], profile) for profile in profiles) + profiles_dict = dict((profile["user_id"], profile) for profile in profiles) for story in stories: - commented_by_public = story.get('commented_by_public') or [c['user_id'] for c in story['public_comments']] + commented_by_public = story.get("commented_by_public") or [ + c["user_id"] for c in story["public_comments"] + ] for comment_user_id in commented_by_public: private = profiles_dict[comment_user_id].private muted = user_id in profiles_dict[comment_user_id].muted_by_user_ids if private or muted: - story['public_comments'] = [c for c in story['public_comments'] if c['user_id'] != comment_user_id] - story['comment_count_public'] -= 1 + story["public_comments"] = [ + c for c in story["public_comments"] if c["user_id"] != comment_user_id + ] + story["comment_count_public"] -= 1 profiles = [profile.canonical(compact=True) for profile in profiles] - + return stories, profiles - + @staticmethod def attach_users_to_stories(stories, profiles): - profiles = dict([(p['user_id'], p) for p in profiles]) + profiles = dict([(p["user_id"], p) for p in profiles]) for s, story in enumerate(stories): - for u, user_id in enumerate(story['shared_by_friends']): - if user_id not in profiles: continue - stories[s]['shared_by_friends'][u] = profiles[user_id] - for u, user_id in enumerate(story['shared_by_public']): - if user_id not in profiles: continue - stories[s]['shared_by_public'][u] = profiles[user_id] - for comment_set in ['friend_comments', 'public_comments', 'friend_shares']: + for u, user_id in enumerate(story["shared_by_friends"]): + if user_id not in profiles: + continue + stories[s]["shared_by_friends"][u] = profiles[user_id] + for u, user_id in enumerate(story["shared_by_public"]): + if user_id not in profiles: + continue + stories[s]["shared_by_public"][u] = profiles[user_id] + for comment_set in ["friend_comments", "public_comments", "friend_shares"]: for c, comment in enumerate(story[comment_set]): - if comment['user_id'] not in profiles: continue - stories[s][comment_set][c]['user'] = profiles[comment['user_id']] - if comment['source_user_id'] and comment['source_user_id'] in profiles: - stories[s][comment_set][c]['source_user'] = profiles[comment['source_user_id']] - for r, reply in enumerate(comment['replies']): - if reply['user_id'] not in profiles: continue - stories[s][comment_set][c]['replies'][r]['user'] = profiles[reply['user_id']] - stories[s][comment_set][c]['liking_user_ids'] = list(comment['liking_users']) - for u, user_id in enumerate(comment['liking_users']): - if user_id not in profiles: continue - stories[s][comment_set][c]['liking_users'][u] = profiles[user_id] + if comment["user_id"] not in profiles: + continue + stories[s][comment_set][c]["user"] = profiles[comment["user_id"]] + if comment["source_user_id"] and comment["source_user_id"] in profiles: + stories[s][comment_set][c]["source_user"] = profiles[comment["source_user_id"]] + for r, reply in enumerate(comment["replies"]): + if reply["user_id"] not in profiles: + continue + stories[s][comment_set][c]["replies"][r]["user"] = profiles[reply["user_id"]] + stories[s][comment_set][c]["liking_user_ids"] = list(comment["liking_users"]) + for u, user_id in enumerate(comment["liking_users"]): + if user_id not in profiles: + continue + stories[s][comment_set][c]["liking_users"][u] = profiles[user_id] return stories - + @staticmethod def attach_users_to_comment(comment, profiles): - profiles = dict([(p['user_id'], p) for p in profiles]) + profiles = dict([(p["user_id"], p) for p in profiles]) - if comment['user_id'] not in profiles: return comment - comment['user'] = profiles[comment['user_id']] + if comment["user_id"] not in profiles: + return comment + comment["user"] = profiles[comment["user_id"]] - if comment['source_user_id']: - comment['source_user'] = profiles[comment['source_user_id']] + if comment["source_user_id"]: + comment["source_user"] = profiles[comment["source_user_id"]] - for r, reply in enumerate(comment['replies']): - if reply['user_id'] not in profiles: continue - comment['replies'][r]['user'] = profiles[reply['user_id']] - comment['liking_user_ids'] = list(comment['liking_users']) - for u, user_id in enumerate(comment['liking_users']): - if user_id not in profiles: continue - comment['liking_users'][u] = profiles[user_id] + for r, reply in enumerate(comment["replies"]): + if reply["user_id"] not in profiles: + continue + comment["replies"][r]["user"] = profiles[reply["user_id"]] + comment["liking_user_ids"] = list(comment["liking_users"]) + for u, user_id in enumerate(comment["liking_users"]): + if user_id not in profiles: + continue + comment["liking_users"][u] = profiles[user_id] return comment - + def add_liking_user(self, user_id): if user_id not in self.liking_users: self.liking_users.append(user_id) @@ -2151,15 +2380,11 @@ def remove_liking_user(self, user_id): if user_id in self.liking_users: self.liking_users.remove(user_id) self.save() - + def blurblog_permalink(self): profile = MSocialProfile.get_user(self.user_id) - return "%sstory/%s/%s" % ( - profile.blurblog_url, - slugify(self.story_title)[:20], - self.story_hash - ) - + return "%sstory/%s/%s" % (profile.blurblog_url, slugify(self.story_title)[:20], self.story_hash) + def generate_post_to_service_message(self, truncate=None, include_url=True): message = strip_tags(self.comments) if not message or len(message) < 1: @@ -2178,55 +2403,56 @@ def generate_post_to_service_message(self, truncate=None, include_url=True): if truncate: message = truncate_chars(message, truncate - 24) message += " " + self.blurblog_permalink() - + return message - + def post_to_service(self, service): user = User.objects.get(pk=self.user_id) - + if service in self.posted_to_services: logging.user(user, "~BM~FRAlready posted to %s." % (service)) return - - posts_last_hour = MSharedStory.objects.filter(user_id=self.user_id, - posted_to_services__contains=service, - shared_date__gte=datetime.datetime.now() - - datetime.timedelta(hours=1)).count() + + posts_last_hour = MSharedStory.objects.filter( + user_id=self.user_id, + posted_to_services__contains=service, + shared_date__gte=datetime.datetime.now() - datetime.timedelta(hours=1), + ).count() if posts_last_hour >= 3: logging.user(user, "~BM~FRPosted to %s > 3 times in past hour" % service) return - + posted = False social_service = MSocialServices.objects.get(user_id=self.user_id) - + message = self.generate_post_to_service_message() logging.user(user, "~BM~FGPosting to %s: ~SB%s" % (service, message)) - - if service == 'twitter': + + if service == "twitter": posted = social_service.post_to_twitter(self) - elif service == 'facebook': + elif service == "facebook": posted = social_service.post_to_facebook(self) - + if posted: self.posted_to_services.append(service) self.save() - + def notify_user_ids(self, include_parent=True): user_ids = set() for reply in self.replies: if reply.user_id not in self.mute_email_users: user_ids.add(reply.user_id) - + if include_parent and self.user_id not in self.mute_email_users: user_ids.add(self.user_id) - + return list(user_ids) - + def reply_for_id(self, reply_id): for reply in self.replies: if reply.reply_id == reply_id: return reply - + def send_emails_for_new_reply(self, reply_id): if reply_id in self.emailed_replies: logging.debug(" ***> Already sent reply email: %s on %s" % (reply_id, self)) @@ -2236,7 +2462,7 @@ def send_emails_for_new_reply(self, reply_id): if not reply: logging.debug(" ***> Reply doesn't exist: %s on %s" % (reply_id, self)) return - + notify_user_ids = self.notify_user_ids() if reply.user_id in notify_user_ids: notify_user_ids.remove(reply.user_id) @@ -2246,15 +2472,15 @@ def send_emails_for_new_reply(self, reply_id): story_feed = Feed.get_by_id(self.story_feed_id) comment = self.comments_with_author() - profile_user_ids = set([comment['user_id']]) - reply_user_ids = list(r['user_id'] for r in comment['replies']) + profile_user_ids = set([comment["user_id"]]) + reply_user_ids = list(r["user_id"] for r in comment["replies"]) profile_user_ids = profile_user_ids.union(reply_user_ids) if self.source_user_id: profile_user_ids.add(self.source_user_id) profiles = MSocialProfile.objects.filter(user_id__in=list(profile_user_ids)) profiles = [profile.canonical(compact=True) for profile in profiles] comment = MSharedStory.attach_users_to_comment(comment, profiles) - + for user_id in notify_user_ids: user = User.objects.get(pk=user_id) @@ -2264,170 +2490,190 @@ def send_emails_for_new_reply(self, reply_id): elif not user.profile.send_emails: logging.user(user, "~FMDisabled emails, skipping.") continue - + mute_url = "http://%s%s" % ( Site.objects.get_current().domain, - reverse('social-mute-story', kwargs={ - 'secret_token': user.profile.secret_token, - 'shared_story_id': self.id, - }) + reverse( + "social-mute-story", + kwargs={ + "secret_token": user.profile.secret_token, + "shared_story_id": self.id, + }, + ), ) data = { - 'reply_user_profile': reply_user_profile, - 'comment': comment, - 'shared_story': self, - 'story_feed': story_feed, - 'mute_url': mute_url, + "reply_user_profile": reply_user_profile, + "comment": comment, + "shared_story": self, + "story_feed": story_feed, + "mute_url": mute_url, } - story_title = self.decoded_story_title.replace('\n', ' ') - - text = render_to_string('mail/email_reply.txt', data) - html = pynliner.fromString(render_to_string('mail/email_reply.xhtml', data)) - subject = "%s replied to you on \"%s\" on NewsBlur" % (reply_user.username, story_title) - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (user.username, user.email)]) + story_title = self.decoded_story_title.replace("\n", " ") + + text = render_to_string("mail/email_reply.txt", data) + html = pynliner.fromString(render_to_string("mail/email_reply.xhtml", data)) + subject = '%s replied to you on "%s" on NewsBlur' % (reply_user.username, story_title) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (user.username, user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() sent_emails += 1 - - logging.user(reply_user, "~BB~FM~SBSending %s/%s email%s for new reply: %s" % ( - sent_emails, len(notify_user_ids), - '' if len(notify_user_ids) == 1 else 's', - self.decoded_story_title[:30])) - + + logging.user( + reply_user, + "~BB~FM~SBSending %s/%s email%s for new reply: %s" + % ( + sent_emails, + len(notify_user_ids), + "" if len(notify_user_ids) == 1 else "s", + self.decoded_story_title[:30], + ), + ) + self.emailed_replies.append(reply.reply_id) self.save() - + def send_email_for_reshare(self): if self.emailed_reshare: logging.debug(" ***> Already sent reply email: %s" % self) return - + reshare_user = User.objects.get(pk=self.user_id) reshare_user_profile = MSocialProfile.get_user(self.user_id) original_user = User.objects.get(pk=self.source_user_id) - original_shared_story = MSharedStory.objects.get(user_id=self.source_user_id, - story_guid=self.story_guid) - + original_shared_story = MSharedStory.objects.get( + user_id=self.source_user_id, story_guid=self.story_guid + ) + if not original_user.email or not original_user.profile.send_emails: if not original_user.email: logging.user(original_user, "~FMNo email to send to, skipping.") elif not original_user.profile.send_emails: logging.user(original_user, "~FMDisabled emails, skipping.") return - + story_feed = Feed.get_by_id(self.story_feed_id) comment = self.comments_with_author() - profile_user_ids = set([comment['user_id']]) - reply_user_ids = [reply['user_id'] for reply in comment['replies']] + profile_user_ids = set([comment["user_id"]]) + reply_user_ids = [reply["user_id"] for reply in comment["replies"]] profile_user_ids = profile_user_ids.union(reply_user_ids) if self.source_user_id: profile_user_ids.add(self.source_user_id) profiles = MSocialProfile.objects.filter(user_id__in=list(profile_user_ids)) profiles = [profile.canonical(compact=True) for profile in profiles] comment = MSharedStory.attach_users_to_comment(comment, profiles) - + mute_url = "http://%s%s" % ( Site.objects.get_current().domain, - reverse('social-mute-story', kwargs={ - 'secret_token': original_user.profile.secret_token, - 'shared_story_id': original_shared_story.id, - }) + reverse( + "social-mute-story", + kwargs={ + "secret_token": original_user.profile.secret_token, + "shared_story_id": original_shared_story.id, + }, + ), ) data = { - 'comment': comment, - 'shared_story': self, - 'reshare_user_profile': reshare_user_profile, - 'original_shared_story': original_shared_story, - 'story_feed': story_feed, - 'mute_url': mute_url, + "comment": comment, + "shared_story": self, + "reshare_user_profile": reshare_user_profile, + "original_shared_story": original_shared_story, + "story_feed": story_feed, + "mute_url": mute_url, } - story_title = self.decoded_story_title.replace('\n', ' ') - - text = render_to_string('mail/email_reshare.txt', data) - html = pynliner.fromString(render_to_string('mail/email_reshare.xhtml', data)) - subject = "%s re-shared \"%s\" from you on NewsBlur" % (reshare_user.username, story_title) - msg = EmailMultiAlternatives(subject, text, - from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, - to=['%s <%s>' % (original_user.username, original_user.email)]) + story_title = self.decoded_story_title.replace("\n", " ") + + text = render_to_string("mail/email_reshare.txt", data) + html = pynliner.fromString(render_to_string("mail/email_reshare.xhtml", data)) + subject = '%s re-shared "%s" from you on NewsBlur' % (reshare_user.username, story_title) + msg = EmailMultiAlternatives( + subject, + text, + from_email="NewsBlur <%s>" % settings.HELLO_EMAIL, + to=["%s <%s>" % (original_user.username, original_user.email)], + ) msg.attach_alternative(html, "text/html") msg.send() - + self.emailed_reshare = True self.save() - - logging.user(reshare_user, "~BB~FM~SBSending %s email for story re-share: %s" % ( - original_user.username, - self.decoded_story_title[:30])) - + + logging.user( + reshare_user, + "~BB~FM~SBSending %s email for story re-share: %s" + % (original_user.username, self.decoded_story_title[:30]), + ) + def extract_image_urls(self, force=False): if not self.story_content_z: return if self.image_urls and not force: return - + soup = BeautifulSoup(zlib.decompress(self.story_content_z), features="lxml") - image_sources = [img.get('src') for img in soup.findAll('img') if img and img.get('src')] + image_sources = [img.get("src") for img in soup.findAll("img") if img and img.get("src")] if len(image_sources) > 0: self.image_urls = image_sources max_length = MSharedStory.image_urls.field.max_length - while len(''.join(self.image_urls)) > max_length: + while len("".join(self.image_urls)) > max_length: if len(self.image_urls) <= 1: - self.image_urls[0] = self.image_urls[0][:max_length-1] + self.image_urls[0] = self.image_urls[0][: max_length - 1] break else: self.image_urls.pop() self.save() - + def calculate_image_sizes(self, force=False): if not self.story_content_z: return - + if not force and self.image_count: return self.image_sizes - + headers = { - 'User-Agent': 'NewsBlur Image Fetcher - %s' % ( - settings.NEWSBLUR_URL - ), + "User-Agent": "NewsBlur Image Fetcher - %s" % (settings.NEWSBLUR_URL), } - + self.extract_image_urls() image_sizes = [] - + for image_source in self.image_urls[:10]: if any(ignore in image_source for ignore in IGNORE_IMAGE_SOURCES): continue width, height = ImageOps.image_size(image_source, headers=headers) # if width <= 16 or height <= 16: # continue - image_sizes.append({'src': image_source, 'size': (width, height)}) - + image_sizes.append({"src": image_source, "size": (width, height)}) + if image_sizes: - image_sizes = sorted(image_sizes, key=lambda i: i['size'][0] * i['size'][1], - reverse=True) + image_sizes = sorted(image_sizes, key=lambda i: i["size"][0] * i["size"][1], reverse=True) self.image_sizes = image_sizes self.image_count = len(image_sizes) self.save() - - logging.debug(" ---> ~SN~FGFetched image sizes on shared story: ~SB%s/%s images" % - (self.image_count, len(self.image_urls))) - + + logging.debug( + " ---> ~SN~FGFetched image sizes on shared story: ~SB%s/%s images" + % (self.image_count, len(self.image_urls)) + ) + return image_sizes - + def fetch_original_text(self, force=False, request=None, debug=False): original_text_z = self.original_text_z feed = Feed.get_by_id(self.story_feed_id) - + if not original_text_z or force: ti = TextImporter(self, feed=feed, request=request, debug=False) original_text = ti.fetch() else: logging.user(request, "~FYFetching ~FGoriginal~FY story text, ~SBfound.") original_text = zlib.decompress(original_text_z) - + return original_text def fetch_original_page(self, force=False, request=None, debug=False): @@ -2438,62 +2684,61 @@ def fetch_original_page(self, force=False, request=None, debug=False): else: logging.user(request, "~FYFetching ~FGoriginal~FY story page, ~SBfound.") original_page = zlib.decompress(self.original_page_z) - + return original_page + class MSocialServices(mongo.Document): - user_id = mongo.IntField() - autofollow = mongo.BooleanField(default=True) - twitter_uid = mongo.StringField() - twitter_access_key = mongo.StringField() + user_id = mongo.IntField() + autofollow = mongo.BooleanField(default=True) + twitter_uid = mongo.StringField() + twitter_access_key = mongo.StringField() twitter_access_secret = mongo.StringField() - twitter_friend_ids = mongo.ListField(mongo.StringField()) - twitter_picture_url = mongo.StringField() - twitter_username = mongo.StringField() - twitter_refresh_date = mongo.DateTimeField() - facebook_uid = mongo.StringField() + twitter_friend_ids = mongo.ListField(mongo.StringField()) + twitter_picture_url = mongo.StringField() + twitter_username = mongo.StringField() + twitter_refresh_date = mongo.DateTimeField() + facebook_uid = mongo.StringField() facebook_access_token = mongo.StringField() - facebook_friend_ids = mongo.ListField(mongo.StringField()) - facebook_picture_url = mongo.StringField() + facebook_friend_ids = mongo.ListField(mongo.StringField()) + facebook_picture_url = mongo.StringField() facebook_refresh_date = mongo.DateTimeField() - upload_picture_url = mongo.StringField() - syncing_twitter = mongo.BooleanField(default=False) - syncing_facebook = mongo.BooleanField(default=False) - + upload_picture_url = mongo.StringField() + syncing_twitter = mongo.BooleanField(default=False) + syncing_facebook = mongo.BooleanField(default=False) + meta = { - 'collection': 'social_services', - 'indexes': ['user_id', 'twitter_friend_ids', 'facebook_friend_ids', 'twitter_uid', 'facebook_uid'], - 'allow_inheritance': False, - 'strict': False, + "collection": "social_services", + "indexes": ["user_id", "twitter_friend_ids", "facebook_friend_ids", "twitter_uid", "facebook_uid"], + "allow_inheritance": False, + "strict": False, } - + def __str__(self): user = User.objects.get(pk=self.user_id) return "%s (Twitter: %s, FB: %s)" % (user.username, self.twitter_uid, self.facebook_uid) - + def canonical(self): user = User.objects.get(pk=self.user_id) return { - 'twitter': { - 'twitter_username': self.twitter_username, - 'twitter_picture_url': self.twitter_picture_url, - 'twitter_uid': self.twitter_uid, - 'syncing': self.syncing_twitter, + "twitter": { + "twitter_username": self.twitter_username, + "twitter_picture_url": self.twitter_picture_url, + "twitter_uid": self.twitter_uid, + "syncing": self.syncing_twitter, }, - 'facebook': { - 'facebook_uid': self.facebook_uid, - 'facebook_picture_url': self.facebook_picture_url, - 'syncing': self.syncing_facebook, + "facebook": { + "facebook_uid": self.facebook_uid, + "facebook_picture_url": self.facebook_picture_url, + "syncing": self.syncing_facebook, }, - 'gravatar': { - 'gravatar_picture_url': "https://www.gravatar.com/avatar/" + \ - hashlib.md5(user.email.lower().encode('utf-8')).hexdigest() + "gravatar": { + "gravatar_picture_url": "https://www.gravatar.com/avatar/" + + hashlib.md5(user.email.lower().encode("utf-8")).hexdigest() }, - 'upload': { - 'upload_picture_url': self.upload_picture_url - } + "upload": {"upload_picture_url": self.upload_picture_url}, } - + @classmethod def get_user(cls, user_id): try: @@ -2513,26 +2758,26 @@ def get_user(cls, user_id): if created: profile.save() return profile - + @classmethod def profile(cls, user_id): profile = cls.get_user(user_id=user_id) return profile.canonical() - + def save_uploaded_photo(self, photo): photo_body = photo.read() filename = photo.name s3 = s3_utils.S3Store() image_name = s3.save_profile_picture(self.user_id, filename, photo_body) - if image_name: + if image_name: self.upload_picture_url = "https://s3.amazonaws.com/%s/avatars/%s/thumbnail_%s" % ( settings.S3_AVATARS_BUCKET_NAME, self.user_id, image_name, ) self.save() - + return image_name and self.upload_picture_url def twitter_api(self): @@ -2542,42 +2787,42 @@ def twitter_api(self): auth.set_access_token(self.twitter_access_key, self.twitter_access_secret) api = tweepy.API(auth) return api - + def facebook_api(self): graph = facebook.GraphAPI(access_token=self.facebook_access_token, version="3.1") return graph - + def sync_twitter_friends(self): user = User.objects.get(pk=self.user_id) logging.user(user, "~BG~FMTwitter import starting...") - + api = self.twitter_api() try: twitter_user = api.me() except tweepy.TweepError as e: api = None - + if not api: logging.user(user, "~BG~FMTwitter import ~SBfailed~SN: no api access.") self.syncing_twitter = False self.save() return - + self.twitter_picture_url = twitter_user.profile_image_url_https self.twitter_username = twitter_user.screen_name self.twitter_refreshed_date = datetime.datetime.utcnow() self.syncing_twitter = False self.save() - + profile = MSocialProfile.get_user(self.user_id) profile.location = profile.location or twitter_user.location profile.bio = profile.bio or twitter_user.description profile.website = profile.website or twitter_user.url profile.save() profile.count_follows() - + if not profile.photo_url or not profile.photo_service: - self.set_photo('twitter') + self.set_photo("twitter") try: friend_ids = list(str(friend.id) for friend in list(tweepy.Cursor(api.friends).items())) @@ -2588,17 +2833,17 @@ def sync_twitter_friends(self): logging.user(user, "~BG~FMTwitter import ~SBfailed~SN: no friend_ids.") self.twitter_friend_ids = friend_ids self.save() - + following = self.follow_twitter_friends() - + if not following: logging.user(user, "~BG~FMTwitter import finished.") - + def follow_twitter_friends(self): social_profile = MSocialProfile.get_user(self.user_id) following = [] followers = 0 - + if not self.autofollow: return following @@ -2609,7 +2854,7 @@ def follow_twitter_friends(self): socialsub = social_profile.follow_user(followee_user_id) if socialsub: following.append(followee_user_id) - + # Friends already on NewsBlur should follow back # following_users = MSocialServices.objects.filter(twitter_friend_ids__contains=self.twitter_uid) # for following_user in following_users: @@ -2617,16 +2862,20 @@ def follow_twitter_friends(self): # following_user_profile = MSocialProfile.get_user(following_user.user_id) # following_user_profile.follow_user(self.user_id, check_unfollowed=True) # followers += 1 - + user = User.objects.get(pk=self.user_id) - logging.user(user, "~BG~FMTwitter import: %s users, now following ~SB%s~SN with ~SB%s~SN follower-backs" % (len(self.twitter_friend_ids), len(following), followers)) - + logging.user( + user, + "~BG~FMTwitter import: %s users, now following ~SB%s~SN with ~SB%s~SN follower-backs" + % (len(self.twitter_friend_ids), len(following), followers), + ) + return following - + def sync_facebook_friends(self): user = User.objects.get(pk=self.user_id) logging.user(user, "~BG~FMFacebook import starting...") - + graph = self.facebook_api() if not graph: logging.user(user, "~BG~FMFacebook import ~SBfailed~SN: no api access.") @@ -2647,25 +2896,27 @@ def sync_facebook_friends(self): self.facebook_picture_url = "https://graph.facebook.com/%s/picture" % self.facebook_uid self.syncing_facebook = False self.save() - - facebook_user = graph.request('me', args={'fields':'website,about,location'}) + + facebook_user = graph.request("me", args={"fields": "website,about,location"}) profile = MSocialProfile.get_user(self.user_id) - profile.location = profile.location or (facebook_user.get('location') and facebook_user['location']['name']) - profile.bio = profile.bio or facebook_user.get('about') - if not profile.website and facebook_user.get('website'): - profile.website = facebook_user.get('website').split()[0] + profile.location = profile.location or ( + facebook_user.get("location") and facebook_user["location"]["name"] + ) + profile.bio = profile.bio or facebook_user.get("about") + if not profile.website and facebook_user.get("website"): + profile.website = facebook_user.get("website").split()[0] profile.save() profile.count_follows() if not profile.photo_url or not profile.photo_service: - self.set_photo('facebook') - + self.set_photo("facebook") + self.follow_facebook_friends() - + def follow_facebook_friends(self): social_profile = MSocialProfile.get_user(self.user_id) following = [] followers = 0 - + if not self.autofollow: return following @@ -2676,7 +2927,7 @@ def follow_facebook_friends(self): socialsub = social_profile.follow_user(followee_user_id) if socialsub: following.append(followee_user_id) - + # Friends already on NewsBlur should follow back # following_users = MSocialServices.objects.filter(facebook_friend_ids__contains=self.facebook_uid) # for following_user in following_users: @@ -2684,47 +2935,52 @@ def follow_facebook_friends(self): # following_user_profile = MSocialProfile.get_user(following_user.user_id) # following_user_profile.follow_user(self.user_id, check_unfollowed=True) # followers += 1 - + user = User.objects.get(pk=self.user_id) - logging.user(user, "~BG~FMFacebook import: %s users, now following ~SB%s~SN with ~SB%s~SN follower-backs" % (len(self.facebook_friend_ids), len(following), followers)) - + logging.user( + user, + "~BG~FMFacebook import: %s users, now following ~SB%s~SN with ~SB%s~SN follower-backs" + % (len(self.facebook_friend_ids), len(following), followers), + ) + return following - + def disconnect_twitter(self): self.syncing_twitter = False self.twitter_uid = None self.save() - + def disconnect_facebook(self): self.syncing_facebook = False self.facebook_uid = None self.save() - + def set_photo(self, service): profile = MSocialProfile.get_user(self.user_id) - if service == 'nothing': + if service == "nothing": service = None profile.photo_service = service if not service: profile.photo_url = None - elif service == 'twitter': + elif service == "twitter": profile.photo_url = self.twitter_picture_url - elif service == 'facebook': + elif service == "facebook": profile.photo_url = self.facebook_picture_url - elif service == 'upload': + elif service == "upload": profile.photo_url = self.upload_picture_url - elif service == 'gravatar': + elif service == "gravatar": user = User.objects.get(pk=self.user_id) - profile.photo_url = "https://www.gravatar.com/avatar/" + \ - hashlib.md5(user.email.encode('utf-8')).hexdigest() + profile.photo_url = ( + "https://www.gravatar.com/avatar/" + hashlib.md5(user.email.encode("utf-8")).hexdigest() + ) profile.save() return profile - + @classmethod def sync_all_twitter_photos(cls, days=14, everybody=False): if everybody: - sharers = [ss.user_id for ss in MSocialServices.objects.all().only('user_id')] + sharers = [ss.user_id for ss in MSocialServices.objects.all().only("user_id")] elif days: week_ago = datetime.datetime.now() - datetime.timedelta(days=days) shares = MSharedStory.objects.filter(shared_date__gte=week_ago) @@ -2736,7 +2992,8 @@ def sync_all_twitter_photos(cls, days=14, everybody=False): profile = MSocialProfile.objects.get(user_id=user_id) except MSocialProfile.DoesNotExist: continue - if not profile.photo_service == 'twitter': continue + if not profile.photo_service == "twitter": + continue ss = MSocialServices.objects.get(user_id=user_id) try: ss.sync_twitter_photo() @@ -2749,10 +3006,10 @@ def sync_twitter_photo(self): if profile.photo_service != "twitter": return - + user = User.objects.get(pk=self.user_id) logging.user(user, "~FCSyncing Twitter profile photo...") - + try: api = self.twitter_api() me = api.me() @@ -2764,12 +3021,12 @@ def sync_twitter_photo(self): self.twitter_picture_url = me.profile_image_url_https self.save() - self.set_photo('twitter') - + self.set_photo("twitter") + def post_to_twitter(self, shared_story): message = shared_story.generate_post_to_service_message(truncate=280) shared_story.calculate_image_sizes() - + try: api = self.twitter_api() filename = self.fetch_image_file_for_twitter(shared_story) @@ -2782,93 +3039,101 @@ def post_to_twitter(self, shared_story): user = User.objects.get(pk=self.user_id) logging.user(user, "~FRTwitter error: ~SB%s" % e) return - + return True - + def fetch_image_file_for_twitter(self, shared_story): - if not shared_story.image_urls: return + if not shared_story.image_urls: + return user = User.objects.get(pk=self.user_id) logging.user(user, "~FCFetching image for twitter: ~SB%s" % shared_story.image_urls[0]) - + url = shared_story.image_urls[0] image_filename = os.path.basename(urllib.parse.urlparse(url).path) req = requests.get(url, stream=True, timeout=10) filename = "/tmp/%s-%s" % (shared_story.story_hash, image_filename) - + if req.status_code == 200: f = open(filename, "wb") for chunk in req: f.write(chunk) f.close() - + return filename - + def post_to_facebook(self, shared_story): message = shared_story.generate_post_to_service_message(include_url=False) shared_story.calculate_image_sizes() content = zlib.decompress(shared_story.story_content_z)[:1024] - + try: api = self.facebook_api() # api.put_wall_post(message=message) - api.put_object('me', '%s:share' % settings.FACEBOOK_NAMESPACE, - link=shared_story.blurblog_permalink(), - type="link", - name=shared_story.decoded_story_title, - description=content, - website=shared_story.blurblog_permalink(), - message=message, - ) + api.put_object( + "me", + "%s:share" % settings.FACEBOOK_NAMESPACE, + link=shared_story.blurblog_permalink(), + type="link", + name=shared_story.decoded_story_title, + description=content, + website=shared_story.blurblog_permalink(), + message=message, + ) except facebook.GraphAPIError as e: logging.debug("---> ~SN~FMFacebook posting error, disconnecting: ~SB~FR%s" % e) self.disconnect_facebook() return - + return True - + class MInteraction(mongo.Document): - user_id = mongo.IntField() - date = mongo.DateTimeField(default=datetime.datetime.now) - category = mongo.StringField() - title = mongo.StringField() - content = mongo.StringField() + user_id = mongo.IntField() + date = mongo.DateTimeField(default=datetime.datetime.now) + category = mongo.StringField() + title = mongo.StringField() + content = mongo.StringField() with_user_id = mongo.IntField() - feed_id = mongo.DynamicField() - story_feed_id= mongo.IntField() - content_id = mongo.StringField() - + feed_id = mongo.DynamicField() + story_feed_id = mongo.IntField() + content_id = mongo.StringField() + meta = { - 'collection': 'interactions', - 'indexes': [('user_id', '-date'), 'category', 'with_user_id'], - 'allow_inheritance': False, - 'ordering': ['-date'], + "collection": "interactions", + "indexes": [("user_id", "-date"), "category", "with_user_id"], + "allow_inheritance": False, + "ordering": ["-date"], } - + def __str__(self): user = User.objects.get(pk=self.user_id) with_user = self.with_user_id and User.objects.get(pk=self.with_user_id) - return "<%s> %s on %s: %s - %s" % (user.username, with_user and with_user.username, self.date, - self.category, self.content and self.content[:20]) - + return "<%s> %s on %s: %s - %s" % ( + user.username, + with_user and with_user.username, + self.date, + self.category, + self.content and self.content[:20], + ) + def canonical(self): story_hash = None if self.story_feed_id: story_hash = MStory.ensure_story_hash(self.content_id, story_feed_id=self.story_feed_id) return { - 'date': self.date, - 'category': self.category, - 'title': self.title, - 'content': self.content, - 'with_user_id': self.with_user_id, - 'feed_id': self.feed_id, - 'story_feed_id': self.story_feed_id, - 'content_id': self.content_id, - 'story_hash': story_hash, + "date": self.date, + "category": self.category, + "title": self.title, + "content": self.content, + "with_user_id": self.with_user_id, + "feed_id": self.feed_id, + "story_feed_id": self.story_feed_id, + "content_id": self.content_id, + "story_hash": story_hash, } - + @classmethod def trim(cls, user_id, limit=100): user = User.objects.get(pk=user_id) @@ -2877,22 +3142,24 @@ def trim(cls, user_id, limit=100): if interaction_count == 0: interaction_count = cls.objects.filter(user_id=user_id).count() - logging.user(user, "~FBNot trimming interactions, only ~SB%s~SN interactions found" % interaction_count) + logging.user( + user, "~FBNot trimming interactions, only ~SB%s~SN interactions found" % interaction_count + ) return - + logging.user(user, "~FBTrimming ~SB%s~SN interactions..." % interaction_count) for interaction in interactions: interaction.delete() logging.user(user, "~FBDone trimming ~SB%s~SN interactions" % interaction_count) - + @classmethod def publish_update_to_subscribers(self, user_id): user = User.objects.get(pk=user_id) try: r = redis.Redis(connection_pool=settings.REDIS_POOL) - listeners_count = r.publish(user.username, 'interaction:new') + listeners_count = r.publish(user.username, "interaction:new") if listeners_count: logging.debug(" ---> ~FMPublished to %s subscribers" % (listeners_count)) except redis.ConnectionError: @@ -2904,74 +3171,85 @@ def user(cls, user_id, page=1, limit=None, categories=None): dashboard_date = user_profile.dashboard_date or user_profile.last_seen_on page = max(1, page) limit = int(limit) if limit else 4 - offset = (page-1) * limit - + offset = (page - 1) * limit + interactions_db = cls.objects.filter(user_id=user_id) if categories: interactions_db = interactions_db.filter(category__in=categories) - interactions_db = interactions_db[offset:offset+limit+1] - + interactions_db = interactions_db[offset : offset + limit + 1] + has_next_page = len(interactions_db) > limit - interactions_db = interactions_db[offset:offset+limit] + interactions_db = interactions_db[offset : offset + limit] with_user_ids = [i.with_user_id for i in interactions_db if i.with_user_id] - social_profiles = dict((p.user_id, p) for p in MSocialProfile.objects.filter(user_id__in=with_user_ids)) - + social_profiles = dict( + (p.user_id, p) for p in MSocialProfile.objects.filter(user_id__in=with_user_ids) + ) + interactions = [] for interaction_db in interactions_db: interaction = interaction_db.canonical() social_profile = social_profiles.get(interaction_db.with_user_id) if social_profile: - interaction['photo_url'] = social_profile.profile_photo_url - interaction['with_user'] = social_profiles.get(interaction_db.with_user_id) - interaction['time_since'] = relative_timesince(interaction_db.date) - interaction['date'] = interaction_db.date - interaction['is_new'] = interaction_db.date > dashboard_date + interaction["photo_url"] = social_profile.profile_photo_url + interaction["with_user"] = social_profiles.get(interaction_db.with_user_id) + interaction["time_since"] = relative_timesince(interaction_db.date) + interaction["date"] = interaction_db.date + interaction["is_new"] = interaction_db.date > dashboard_date interactions.append(interaction) return interactions, has_next_page - + @classmethod def user_unread_count(cls, user_id): user_profile = Profile.objects.get(user=user_id) dashboard_date = user_profile.dashboard_date or user_profile.last_seen_on - + interactions_count = cls.objects.filter(user_id=user_id, date__gte=dashboard_date).count() - + return interactions_count - + @classmethod def new_follow(cls, follower_user_id, followee_user_id): params = { - 'user_id': followee_user_id, - 'with_user_id': follower_user_id, - 'category': 'follow', + "user_id": followee_user_id, + "with_user_id": follower_user_id, + "category": "follow", } try: cls.objects.get(**params) except cls.DoesNotExist: cls.objects.create(**params) except cls.MultipleObjectsReturned: - dupes = cls.objects.filter(**params).order_by('-date') + dupes = cls.objects.filter(**params).order_by("-date") logging.debug(" ---> ~FRDeleting dupe follow interactions. %s found." % dupes.count()) for dupe in dupes[1:]: dupe.delete() - + cls.publish_update_to_subscribers(followee_user_id) - + @classmethod - def new_comment_reply(cls, user_id, reply_user_id, reply_content, story_id, story_feed_id, story_title=None, original_message=None): + def new_comment_reply( + cls, + user_id, + reply_user_id, + reply_content, + story_id, + story_feed_id, + story_title=None, + original_message=None, + ): params = { - 'user_id': user_id, - 'with_user_id': reply_user_id, - 'category': 'comment_reply', - 'content': linkify(strip_tags(reply_content)), - 'feed_id': "social:%s" % user_id, - 'story_feed_id': story_feed_id, - 'title': story_title, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": reply_user_id, + "category": "comment_reply", + "content": linkify(strip_tags(reply_content)), + "feed_id": "social:%s" % user_id, + "story_feed_id": story_feed_id, + "title": story_title, + "content_id": story_id, } if original_message: - params['content'] = original_message + params["content"] = original_message original = cls.objects.filter(**params).limit(1) if original: original = original[0] @@ -2982,55 +3260,69 @@ def new_comment_reply(cls, user_id, reply_user_id, reply_content, story_id, stor if not original_message: cls.objects.create(**params) - + cls.publish_update_to_subscribers(user_id) - + @classmethod def remove_comment_reply(cls, user_id, reply_user_id, reply_content, story_id, story_feed_id): params = { - 'user_id': user_id, - 'with_user_id': reply_user_id, - 'category': 'comment_reply', - 'content': linkify(strip_tags(reply_content)), - 'feed_id': "social:%s" % user_id, - 'story_feed_id': story_feed_id, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": reply_user_id, + "category": "comment_reply", + "content": linkify(strip_tags(reply_content)), + "feed_id": "social:%s" % user_id, + "story_feed_id": story_feed_id, + "content_id": story_id, } original = cls.objects.filter(**params) original.delete() - + cls.publish_update_to_subscribers(user_id) - + @classmethod - def new_comment_like(cls, liking_user_id, comment_user_id, story_id, story_feed_id, story_title, comments): - params = dict(user_id=comment_user_id, - with_user_id=liking_user_id, - category="comment_like", - feed_id="social:%s" % comment_user_id, - story_feed_id=story_feed_id, - content_id=story_id) + def new_comment_like( + cls, liking_user_id, comment_user_id, story_id, story_feed_id, story_title, comments + ): + params = dict( + user_id=comment_user_id, + with_user_id=liking_user_id, + category="comment_like", + feed_id="social:%s" % comment_user_id, + story_feed_id=story_feed_id, + content_id=story_id, + ) try: cls.objects.get(**params) except cls.DoesNotExist: params.update(dict(title=story_title, content=comments)) cls.objects.create(**params) - + cls.publish_update_to_subscribers(comment_user_id) @classmethod - def new_reply_reply(cls, user_id, comment_user_id, reply_user_id, reply_content, story_id, story_feed_id, story_title=None, original_message=None): + def new_reply_reply( + cls, + user_id, + comment_user_id, + reply_user_id, + reply_content, + story_id, + story_feed_id, + story_title=None, + original_message=None, + ): params = { - 'user_id': user_id, - 'with_user_id': reply_user_id, - 'category': 'reply_reply', - 'content': linkify(strip_tags(reply_content)), - 'feed_id': "social:%s" % comment_user_id, - 'story_feed_id': story_feed_id, - 'title': story_title, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": reply_user_id, + "category": "reply_reply", + "content": linkify(strip_tags(reply_content)), + "feed_id": "social:%s" % comment_user_id, + "story_feed_id": story_feed_id, + "title": story_title, + "content_id": story_id, } if original_message: - params['content'] = original_message + params["content"] = original_message original = cls.objects.filter(**params).limit(1) if original: original = original[0] @@ -3041,39 +3333,43 @@ def new_reply_reply(cls, user_id, comment_user_id, reply_user_id, reply_content, if not original_message: cls.objects.create(**params) - + cls.publish_update_to_subscribers(user_id) - + @classmethod - def remove_reply_reply(cls, user_id, comment_user_id, reply_user_id, reply_content, story_id, story_feed_id): + def remove_reply_reply( + cls, user_id, comment_user_id, reply_user_id, reply_content, story_id, story_feed_id + ): params = { - 'user_id': user_id, - 'with_user_id': reply_user_id, - 'category': 'reply_reply', - 'content': linkify(strip_tags(reply_content)), - 'feed_id': "social:%s" % comment_user_id, - 'story_feed_id': story_feed_id, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": reply_user_id, + "category": "reply_reply", + "content": linkify(strip_tags(reply_content)), + "feed_id": "social:%s" % comment_user_id, + "story_feed_id": story_feed_id, + "content_id": story_id, } original = cls.objects.filter(**params) original.delete() - + cls.publish_update_to_subscribers(user_id) - + @classmethod - def new_reshared_story(cls, user_id, reshare_user_id, comments, story_title, story_feed_id, story_id, original_comments=None): + def new_reshared_story( + cls, user_id, reshare_user_id, comments, story_title, story_feed_id, story_id, original_comments=None + ): params = { - 'user_id': user_id, - 'with_user_id': reshare_user_id, - 'category': 'story_reshare', - 'content': comments, - 'title': story_title, - 'feed_id': "social:%s" % reshare_user_id, - 'story_feed_id': story_feed_id, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": reshare_user_id, + "category": "story_reshare", + "content": comments, + "title": story_title, + "feed_id": "social:%s" % reshare_user_id, + "story_feed_id": story_feed_id, + "content_id": story_id, } if original_comments: - params['content'] = original_comments + params["content"] = original_comments original = cls.objects.filter(**params).limit(1) if original: interaction = original[0] @@ -3084,49 +3380,50 @@ def new_reshared_story(cls, user_id, reshare_user_id, comments, story_title, sto if not original_comments: cls.objects.create(**params) - + cls.publish_update_to_subscribers(user_id) + class MActivity(mongo.Document): - user_id = mongo.IntField() - date = mongo.DateTimeField(default=datetime.datetime.now) - category = mongo.StringField() - title = mongo.StringField() - content = mongo.StringField() + user_id = mongo.IntField() + date = mongo.DateTimeField(default=datetime.datetime.now) + category = mongo.StringField() + title = mongo.StringField() + content = mongo.StringField() with_user_id = mongo.IntField() - feed_id = mongo.DynamicField() - story_feed_id= mongo.IntField() - content_id = mongo.StringField() - + feed_id = mongo.DynamicField() + story_feed_id = mongo.IntField() + content_id = mongo.StringField() + meta = { - 'collection': 'activities', - 'indexes': [('user_id', '-date'), 'category', 'with_user_id'], - 'allow_inheritance': False, - 'ordering': ['-date'], + "collection": "activities", + "indexes": [("user_id", "-date"), "category", "with_user_id"], + "allow_inheritance": False, + "ordering": ["-date"], } - + def __str__(self): user = User.objects.get(pk=self.user_id) return "<%s> %s - %s" % (user.username, self.category, self.content and self.content[:20]) - + def canonical(self): story_hash = None if self.story_feed_id: story_hash = MStory.ensure_story_hash(self.content_id, story_feed_id=self.story_feed_id) return { - 'date': self.date, - 'category': self.category, - 'title': self.title, - 'content': self.content, - 'user_id': self.user_id, - 'with_user_id': self.with_user_id or self.user_id, - 'feed_id': self.feed_id or self.story_feed_id, - 'story_feed_id': self.story_feed_id or self.feed_id, - 'content_id': self.content_id, - 'story_hash': story_hash, + "date": self.date, + "category": self.category, + "title": self.title, + "content": self.content, + "user_id": self.user_id, + "with_user_id": self.with_user_id or self.user_id, + "feed_id": self.feed_id or self.story_feed_id, + "story_feed_id": self.story_feed_id or self.feed_id, + "content_id": self.content_id, + "story_hash": story_hash, } - + @classmethod def trim(cls, user_id, limit=100): user = User.objects.get(pk=user_id) @@ -3137,53 +3434,54 @@ def trim(cls, user_id, limit=100): activity_count = cls.objects.filter(user_id=user_id).count() logging.user(user, "~FBNot trimming activities, only ~SB%s~SN activities found" % activity_count) return - + logging.user(user, "~FBTrimming ~SB%s~SN activities..." % activity_count) for activity in activities: activity.delete() logging.user(user, "~FBDone trimming ~SB%s~SN activities" % activity_count) - + @classmethod def user(cls, user_id, page=1, limit=4, public=False, categories=None): user_profile = Profile.objects.get(user=user_id) dashboard_date = user_profile.dashboard_date or user_profile.last_seen_on page = max(1, page) limit = int(limit) - offset = (page-1) * limit - + offset = (page - 1) * limit + activities_db = cls.objects.filter(user_id=user_id) if categories: activities_db = activities_db.filter(category__in=categories) if public: - activities_db = activities_db.filter(category__nin=['star', 'feedsub', 'opml_import', 'opml_export']) - activities_db = activities_db[offset:offset+limit+1] - + activities_db = activities_db.filter( + category__nin=["star", "feedsub", "opml_import", "opml_export"] + ) + activities_db = activities_db[offset : offset + limit + 1] + has_next_page = len(activities_db) > limit - activities_db = activities_db[offset:offset+limit] + activities_db = activities_db[offset : offset + limit] with_user_ids = [a.with_user_id for a in activities_db if a.with_user_id] - social_profiles = dict((p.user_id, p) for p in MSocialProfile.objects.filter(user_id__in=with_user_ids)) + social_profiles = dict( + (p.user_id, p) for p in MSocialProfile.objects.filter(user_id__in=with_user_ids) + ) activities = [] for activity_db in activities_db: activity = activity_db.canonical() - activity['date'] = activity_db.date - activity['time_since'] = relative_timesince(activity_db.date) + activity["date"] = activity_db.date + activity["time_since"] = relative_timesince(activity_db.date) social_profile = social_profiles.get(activity_db.with_user_id) if social_profile: - activity['photo_url'] = social_profile.profile_photo_url - activity['is_new'] = activity_db.date > dashboard_date - activity['with_user'] = social_profiles.get(activity_db.with_user_id or activity_db.user_id) + activity["photo_url"] = social_profile.profile_photo_url + activity["is_new"] = activity_db.date > dashboard_date + activity["with_user"] = social_profiles.get(activity_db.with_user_id or activity_db.user_id) activities.append(activity) - + return activities, has_next_page - + @classmethod def new_starred_story(cls, user_id, story_title, story_feed_id, story_id): - params = dict(user_id=user_id, - category='star', - story_feed_id=story_feed_id, - content_id=story_id) + params = dict(user_id=user_id, category="star", story_feed_id=story_feed_id, content_id=story_id) try: cls.objects.get(**params) except cls.DoesNotExist: @@ -3193,19 +3491,19 @@ def new_starred_story(cls, user_id, story_title, story_feed_id, story_id): @classmethod def remove_starred_story(cls, user_id, story_feed_id, story_id): params = { - 'user_id': user_id, - 'category': 'star', - 'story_feed_id': story_feed_id, - 'content_id': story_id, + "user_id": user_id, + "category": "star", + "story_feed_id": story_feed_id, + "content_id": story_id, } original = cls.objects.filter(**params) original.delete() - + @classmethod def new_feed_subscription(cls, user_id, feed_id, feed_title): params = { "user_id": user_id, - "category": 'feedsub', + "category": "feedsub", "feed_id": feed_id, } try: @@ -3214,7 +3512,7 @@ def new_feed_subscription(cls, user_id, feed_id, feed_title): params.update(dict(content=feed_title)) cls.objects.create(**params) except cls.MultipleObjectsReturned: - dupes = cls.objects.filter(**params).order_by('-date') + dupes = cls.objects.filter(**params).order_by("-date") logging.debug(" ---> ~FRDeleting dupe feed subscription activities. %s found." % dupes.count()) for dupe in dupes[1:]: dupe.delete() @@ -3223,11 +3521,11 @@ def new_feed_subscription(cls, user_id, feed_id, feed_title): def new_opml_import(cls, user_id, count): if count <= 0: return - + params = { "user_id": user_id, - "category": 'opml_import', - 'content': f"You imported an OPML file with {count} sites" + "category": "opml_import", + "content": f"You imported an OPML file with {count} sites", } cls.objects.create(**params) @@ -3235,44 +3533,53 @@ def new_opml_import(cls, user_id, count): def new_opml_export(cls, user_id, count, automated=False): params = { "user_id": user_id, - "category": 'opml_export', - 'content': f"You exported an OPML backup of {count} subscriptions" + "category": "opml_export", + "content": f"You exported an OPML backup of {count} subscriptions", } if automated: - params['content'] = f"An automatic OPML backup of {count} subscriptions was emailed to you" + params["content"] = f"An automatic OPML backup of {count} subscriptions was emailed to you" cls.objects.create(**params) - + @classmethod def new_follow(cls, follower_user_id, followee_user_id): params = { - 'user_id': follower_user_id, - 'with_user_id': followee_user_id, - 'category': 'follow', + "user_id": follower_user_id, + "with_user_id": followee_user_id, + "category": "follow", } try: cls.objects.get(**params) except cls.DoesNotExist: cls.objects.create(**params) except cls.MultipleObjectsReturned: - dupes = cls.objects.filter(**params).order_by('-date') + dupes = cls.objects.filter(**params).order_by("-date") logging.debug(" ---> ~FRDeleting dupe follow activities. %s found." % dupes.count()) for dupe in dupes[1:]: dupe.delete() - + @classmethod - def new_comment_reply(cls, user_id, comment_user_id, reply_content, story_id, story_feed_id, story_title=None, original_message=None): + def new_comment_reply( + cls, + user_id, + comment_user_id, + reply_content, + story_id, + story_feed_id, + story_title=None, + original_message=None, + ): params = { - 'user_id': user_id, - 'with_user_id': comment_user_id, - 'category': 'comment_reply', - 'content': linkify(strip_tags(reply_content)), - 'feed_id': "social:%s" % comment_user_id, - 'story_feed_id': story_feed_id, - 'title': story_title, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": comment_user_id, + "category": "comment_reply", + "content": linkify(strip_tags(reply_content)), + "feed_id": "social:%s" % comment_user_id, + "story_feed_id": story_feed_id, + "title": story_title, + "content_id": story_id, } if original_message: - params['content'] = original_message + params["content"] = original_message original = cls.objects.filter(**params).limit(1) if original: original = original[0] @@ -3283,47 +3590,51 @@ def new_comment_reply(cls, user_id, comment_user_id, reply_content, story_id, st if not original_message: cls.objects.create(**params) - + @classmethod def remove_comment_reply(cls, user_id, comment_user_id, reply_content, story_id, story_feed_id): params = { - 'user_id': user_id, - 'with_user_id': comment_user_id, - 'category': 'comment_reply', - 'content': linkify(strip_tags(reply_content)), - 'feed_id': "social:%s" % comment_user_id, - 'story_feed_id': story_feed_id, - 'content_id': story_id, + "user_id": user_id, + "with_user_id": comment_user_id, + "category": "comment_reply", + "content": linkify(strip_tags(reply_content)), + "feed_id": "social:%s" % comment_user_id, + "story_feed_id": story_feed_id, + "content_id": story_id, } original = cls.objects.filter(**params) original.delete() - + @classmethod - def new_comment_like(cls, liking_user_id, comment_user_id, story_id, story_feed_id, story_title, comments): - params = dict(user_id=liking_user_id, - with_user_id=comment_user_id, - category="comment_like", - feed_id="social:%s" % comment_user_id, - story_feed_id=story_feed_id, - content_id=story_id) + def new_comment_like( + cls, liking_user_id, comment_user_id, story_id, story_feed_id, story_title, comments + ): + params = dict( + user_id=liking_user_id, + with_user_id=comment_user_id, + category="comment_like", + feed_id="social:%s" % comment_user_id, + story_feed_id=story_feed_id, + content_id=story_id, + ) try: cls.objects.get(**params) except cls.DoesNotExist: params.update(dict(title=story_title, content=comments)) cls.objects.create(**params) - + @classmethod - def new_shared_story(cls, user_id, source_user_id, story_title, comments, story_feed_id, story_id, share_date=None): + def new_shared_story( + cls, user_id, source_user_id, story_title, comments, story_feed_id, story_id, share_date=None + ): data = { "user_id": user_id, - "category": 'sharedstory', + "category": "sharedstory", "feed_id": "social:%s" % user_id, "story_feed_id": story_feed_id, "content_id": story_id, } - extradata = {'with_user_id': source_user_id, - 'title': story_title, - 'content': comments} + extradata = {"with_user_id": source_user_id, "title": story_title, "content": comments} try: a = cls.objects.get(**data) @@ -3351,20 +3662,22 @@ def new_shared_story(cls, user_id, source_user_id, story_title, comments, story_ @classmethod def remove_shared_story(cls, user_id, story_feed_id, story_id): - params = dict(user_id=user_id, - category='sharedstory', - feed_id="social:%s" % user_id, - story_feed_id=story_feed_id, - content_id=story_id) + params = dict( + user_id=user_id, + category="sharedstory", + feed_id="social:%s" % user_id, + story_feed_id=story_feed_id, + content_id=story_id, + ) try: a = cls.objects.get(**params) except cls.DoesNotExist: return except cls.MultipleObjectsReturned: a = cls.objects.filter(**params) - + a.delete() - + @classmethod def new_signup(cls, user_id): params = dict(user_id=user_id, with_user_id=user_id, category="signup") @@ -3375,17 +3688,17 @@ def new_signup(cls, user_id): class MFollowRequest(mongo.Document): - follower_user_id = mongo.IntField(unique_with='followee_user_id') - followee_user_id = mongo.IntField() - date = mongo.DateTimeField(default=datetime.datetime.now) - + follower_user_id = mongo.IntField(unique_with="followee_user_id") + followee_user_id = mongo.IntField() + date = mongo.DateTimeField(default=datetime.datetime.now) + meta = { - 'collection': 'follow_request', - 'indexes': ['follower_user_id', 'followee_user_id'], - 'ordering': ['-date'], - 'allow_inheritance': False, + "collection": "follow_request", + "indexes": ["follower_user_id", "followee_user_id"], + "ordering": ["-date"], + "allow_inheritance": False, } - + @classmethod def add(cls, follower_user_id, followee_user_id): params = dict(follower_user_id=follower_user_id, followee_user_id=followee_user_id) @@ -3393,9 +3706,7 @@ def add(cls, follower_user_id, followee_user_id): cls.objects.get(**params) except cls.DoesNotExist: cls.objects.create(**params) - + @classmethod def remove(cls, follower_user_id, followee_user_id): - cls.objects.filter(follower_user_id=follower_user_id, - followee_user_id=followee_user_id).delete() - + cls.objects.filter(follower_user_id=follower_user_id, followee_user_id=followee_user_id).delete() diff --git a/apps/social/tasks.py b/apps/social/tasks.py index d16f5c67a9..41eba160f2 100644 --- a/apps/social/tasks.py +++ b/apps/social/tasks.py @@ -1,7 +1,13 @@ from bson.objectid import ObjectId -from newsblur_web.celeryapp import app -from apps.social.models import MSharedStory, MSocialProfile, MSocialServices, MSocialSubscription from django.contrib.auth.models import User + +from apps.social.models import ( + MSharedStory, + MSocialProfile, + MSocialServices, + MSocialSubscription, +) +from newsblur_web.celeryapp import app from utils import log as logging @@ -12,52 +18,61 @@ def PostToService(shared_story_id, service): shared_story.post_to_service(service) except MSharedStory.DoesNotExist: logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service)) - + + @app.task() def EmailNewFollower(follower_user_id, followee_user_id): user_profile = MSocialProfile.get_user(followee_user_id) user_profile.send_email_for_new_follower(follower_user_id) - + + @app.task() def EmailFollowRequest(follower_user_id, followee_user_id): user_profile = MSocialProfile.get_user(followee_user_id) user_profile.send_email_for_follow_request(follower_user_id) - + + @app.task() def EmailFirstShare(user_id): user = User.objects.get(pk=user_id) user.profile.send_first_share_to_blurblog_email() - + + @app.task() def EmailCommentReplies(shared_story_id, reply_id): shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id)) shared_story.send_emails_for_new_reply(ObjectId(reply_id)) - + + @app.task() def EmailStoryReshares(shared_story_id): shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id)) shared_story.send_email_for_reshare() - + + @app.task() def SyncTwitterFriends(user_id): social_services = MSocialServices.objects.get(user_id=user_id) social_services.sync_twitter_friends() + @app.task() def SyncFacebookFriends(user_id): social_services = MSocialServices.objects.get(user_id=user_id) social_services.sync_facebook_friends() - + + @app.task(name="share-popular-stories") def SharePopularStories(): logging.debug(" ---> Sharing popular stories...") MSharedStory.share_popular_stories(interactive=False) - -@app.task(name='clean-social-spam') + + +@app.task(name="clean-social-spam") def CleanSocialSpam(): logging.debug(" ---> Finding social spammers...") MSharedStory.count_potential_spammers(destroy=True) - + @app.task() def UpdateRecalcForSubscription(subscription_user_id, shared_story_id): @@ -68,12 +83,12 @@ def UpdateRecalcForSubscription(subscription_user_id, shared_story_id): except MSharedStory.DoesNotExist: return - logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % ( - socialsubs.count(), - user.username - )) + logging.debug( + " ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" + % (socialsubs.count(), user.username) + ) for socialsub in socialsubs: socialsub.needs_unread_recalc = True socialsub.save() - + shared_story.publish_update_to_subscribers() diff --git a/apps/social/templatetags/social_tags.py b/apps/social/templatetags/social_tags.py index d07711381e..88a00b7d9b 100644 --- a/apps/social/templatetags/social_tags.py +++ b/apps/social/templatetags/social_tags.py @@ -1,66 +1,73 @@ from django import template from django.conf import settings + from apps.social.models import MSocialProfile register = template.Library() -@register.inclusion_tag('social/social_story.xhtml', takes_context=True) + +@register.inclusion_tag("social/social_story.xhtml", takes_context=True) def render_social_story(context, story, has_next_story=False): - user = context['user'] - user_social_profile = context['user_social_profile'] - + user = context["user"] + user_social_profile = context["user_social_profile"] + return { - 'story': story, - 'has_next_story': has_next_story, - 'user': user, - 'user_social_profile': user_social_profile, + "story": story, + "has_next_story": has_next_story, + "user": user, + "user_social_profile": user_social_profile, } -@register.inclusion_tag('social/story_share.xhtml', takes_context=True) + +@register.inclusion_tag("social/story_share.xhtml", takes_context=True) def render_story_share(context, story): - user = context['user'] + user = context["user"] return { - 'user': user, - 'story': story, + "user": user, + "story": story, } - -@register.inclusion_tag('social/story_comments.xhtml', takes_context=True) + + +@register.inclusion_tag("social/story_comments.xhtml", takes_context=True) def render_story_comments(context, story): - user = context['user'] - user_social_profile = context.get('user_social_profile') + user = context["user"] + user_social_profile = context.get("user_social_profile") MEDIA_URL = settings.MEDIA_URL if not user_social_profile and user.is_authenticated: user_social_profile = MSocialProfile.objects.get(user_id=user.pk) - + return { - 'user': user, - 'user_social_profile': user_social_profile, - 'story': story, - 'MEDIA_URL': MEDIA_URL, + "user": user, + "user_social_profile": user_social_profile, + "story": story, + "MEDIA_URL": MEDIA_URL, } -@register.inclusion_tag('social/story_comment.xhtml', takes_context=True) + +@register.inclusion_tag("social/story_comment.xhtml", takes_context=True) def render_story_comment(context, story, comment): - user = context['user'] + user = context["user"] MEDIA_URL = settings.MEDIA_URL - + return { - 'user': user, - 'story': story, - 'comment': comment, - 'MEDIA_URL': MEDIA_URL, + "user": user, + "story": story, + "comment": comment, + "MEDIA_URL": MEDIA_URL, } -@register.inclusion_tag('mail/email_story_comment.xhtml') + +@register.inclusion_tag("mail/email_story_comment.xhtml") def render_email_comment(comment): return { - 'comment': comment, + "comment": comment, } - -@register.inclusion_tag('social/avatars.xhtml') + + +@register.inclusion_tag("social/avatars.xhtml") def render_avatars(avatars): if not isinstance(avatars, list): avatars = [avatars] return { - 'users': avatars, + "users": avatars, } diff --git a/apps/social/urls.py b/apps/social/urls.py index 2b386b2179..90b498b31f 100644 --- a/apps/social/urls.py +++ b/apps/social/urls.py @@ -1,43 +1,70 @@ from django.conf.urls import url + from apps.social import views urlpatterns = [ - url(r'^river_stories/?$', views.load_river_blurblog, name='social-river-blurblog'), - url(r'^share_story/?$', views.mark_story_as_shared, name='mark-story-as-shared'), - url(r'^unshare_story/?$', views.mark_story_as_unshared, name='mark-story-as-unshared'), - url(r'^load_user_friends/?$', views.load_user_friends, name='load-user-friends'), - url(r'^load_follow_requests/?$', views.load_follow_requests, name='load-follow-requests'), - url(r'^profile/?$', views.profile, name='profile'), - url(r'^load_user_profile/?$', views.load_user_profile, name='load-user-profile'), - url(r'^save_user_profile/?$', views.save_user_profile, name='save-user-profile'), - url(r'^upload_avatar/?', views.upload_avatar, name='upload-avatar'), - url(r'^save_blurblog_settings/?$', views.save_blurblog_settings, name='save-blurblog-settings'), - url(r'^interactions/?$', views.load_interactions, name='social-interactions'), - url(r'^activities/?$', views.load_activities, name='social-activities'), - url(r'^follow/?$', views.follow, name='social-follow'), - url(r'^unfollow/?$', views.unfollow, name='social-unfollow'), - url(r'^approve_follower/?$', views.approve_follower, name='social-approve-follower'), - url(r'^ignore_follower/?$', views.ignore_follower, name='social-ignore-follower'), - url(r'^mute_user/?$', views.mute_user, name='social-mute-user'), - url(r'^unmute_user/?$', views.unmute_user, name='social-unmute-user'), - url(r'^feed_trainer', views.social_feed_trainer, name='social-feed-trainer'), - url(r'^public_comments/?$', views.story_public_comments, name='story-public-comments'), - url(r'^save_comment_reply/?$', views.save_comment_reply, name='social-save-comment-reply'), - url(r'^remove_comment_reply/?$', views.remove_comment_reply, name='social-remove-comment-reply'), - url(r'^find_friends/?$', views.find_friends, name='social-find-friends'), - url(r'^like_comment/?$', views.like_comment, name='social-like-comment'), - url(r'^remove_like_comment/?$', views.remove_like_comment, name='social-remove-like-comment'), + url(r"^river_stories/?$", views.load_river_blurblog, name="social-river-blurblog"), + url(r"^share_story/?$", views.mark_story_as_shared, name="mark-story-as-shared"), + url(r"^unshare_story/?$", views.mark_story_as_unshared, name="mark-story-as-unshared"), + url(r"^load_user_friends/?$", views.load_user_friends, name="load-user-friends"), + url(r"^load_follow_requests/?$", views.load_follow_requests, name="load-follow-requests"), + url(r"^profile/?$", views.profile, name="profile"), + url(r"^load_user_profile/?$", views.load_user_profile, name="load-user-profile"), + url(r"^save_user_profile/?$", views.save_user_profile, name="save-user-profile"), + url(r"^upload_avatar/?", views.upload_avatar, name="upload-avatar"), + url(r"^save_blurblog_settings/?$", views.save_blurblog_settings, name="save-blurblog-settings"), + url(r"^interactions/?$", views.load_interactions, name="social-interactions"), + url(r"^activities/?$", views.load_activities, name="social-activities"), + url(r"^follow/?$", views.follow, name="social-follow"), + url(r"^unfollow/?$", views.unfollow, name="social-unfollow"), + url(r"^approve_follower/?$", views.approve_follower, name="social-approve-follower"), + url(r"^ignore_follower/?$", views.ignore_follower, name="social-ignore-follower"), + url(r"^mute_user/?$", views.mute_user, name="social-mute-user"), + url(r"^unmute_user/?$", views.unmute_user, name="social-unmute-user"), + url(r"^feed_trainer", views.social_feed_trainer, name="social-feed-trainer"), + url(r"^public_comments/?$", views.story_public_comments, name="story-public-comments"), + url(r"^save_comment_reply/?$", views.save_comment_reply, name="social-save-comment-reply"), + url(r"^remove_comment_reply/?$", views.remove_comment_reply, name="social-remove-comment-reply"), + url(r"^find_friends/?$", views.find_friends, name="social-find-friends"), + url(r"^like_comment/?$", views.like_comment, name="social-like-comment"), + url(r"^remove_like_comment/?$", views.remove_like_comment, name="social-remove-like-comment"), # url(r'^like_reply/?$', views.like_reply, name='social-like-reply'), # url(r'^remove_like_reply/?$', views.remove_like_reply, name='social-remove-like-reply'), - url(r'^comment/(?P\w+)/reply/(?P\w+)/?$', views.comment_reply, name='social-comment-reply'), - url(r'^comment/(?P\w+)/?$', views.comment, name='social-comment'), - url(r'^rss/(?P\d+)/?$', views.shared_stories_rss_feed, name='shared-stories-rss-feed'), - url(r'^rss/(?P\d+)/(?P[-\w]+)?$', views.shared_stories_rss_feed, name='shared-stories-rss-feed'), - url(r'^stories/(?P\w+)/(?P[-\w]+)?/?$', views.load_social_stories, name='load-social-stories'), - url(r'^page/(?P\w+)/(?P[-\w]+)?/?$', views.load_social_page, name='load-social-page'), - url(r'^settings/(?P\w+)/(?P[-\w]+)?/?$', views.load_social_settings, name='load-social-settings'), - url(r'^statistics/(?P\w+)/(?P[-\w]+)/?$', views.load_social_statistics, name='load-social-statistics'), - url(r'^statistics/(?P\w+)/?$', views.load_social_statistics, name='load-social-statistics'), - url(r'^mute_story/(?P\w+)/(?P\w+)?$', views.mute_story, name='social-mute-story'), - url(r'^(?P[-\w]+)/?$', views.shared_stories_public, name='shared-stories-public'), + url( + r"^comment/(?P\w+)/reply/(?P\w+)/?$", + views.comment_reply, + name="social-comment-reply", + ), + url(r"^comment/(?P\w+)/?$", views.comment, name="social-comment"), + url(r"^rss/(?P\d+)/?$", views.shared_stories_rss_feed, name="shared-stories-rss-feed"), + url( + r"^rss/(?P\d+)/(?P[-\w]+)?$", + views.shared_stories_rss_feed, + name="shared-stories-rss-feed", + ), + url( + r"^stories/(?P\w+)/(?P[-\w]+)?/?$", + views.load_social_stories, + name="load-social-stories", + ), + url(r"^page/(?P\w+)/(?P[-\w]+)?/?$", views.load_social_page, name="load-social-page"), + url( + r"^settings/(?P\w+)/(?P[-\w]+)?/?$", + views.load_social_settings, + name="load-social-settings", + ), + url( + r"^statistics/(?P\w+)/(?P[-\w]+)/?$", + views.load_social_statistics, + name="load-social-statistics", + ), + url( + r"^statistics/(?P\w+)/?$", views.load_social_statistics, name="load-social-statistics" + ), + url( + r"^mute_story/(?P\w+)/(?P\w+)?$", + views.mute_story, + name="social-mute-story", + ), + url(r"^(?P[-\w]+)/?$", views.shared_stories_public, name="shared-stories-public"), ] diff --git a/apps/social/views.py b/apps/social/views.py index ab8797b35f..0e87864671 100644 --- a/apps/social/views.py +++ b/apps/social/views.py @@ -1,92 +1,133 @@ -import time import datetime import random import re +import time + from bson.objectid import ObjectId -from mongoengine.queryset import NotUniqueError -from django.shortcuts import get_object_or_404, render -from django.urls import reverse +from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site +from django.http import ( + Http404, + HttpResponse, + HttpResponseForbidden, + HttpResponseRedirect, +) +from django.shortcuts import get_object_or_404, render from django.template.loader import render_to_string -from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseForbidden -from django.conf import settings +from django.urls import reverse from django.utils import feedgenerator -from apps.rss_feeds.models import MStory, Feed, MStarredStory -from apps.social.models import MSharedStory, MSocialServices, MSocialProfile, MSocialSubscription, MCommentReply -from apps.social.models import MInteraction, MActivity, MFollowRequest -from apps.social.tasks import PostToService, EmailCommentReplies, EmailStoryReshares -from apps.social.tasks import UpdateRecalcForSubscription, EmailFirstShare -from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag -from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds, apply_classifier_authors, apply_classifier_tags -from apps.analyzer.models import get_classifiers_for_user, sort_classifiers_by_feed -from apps.reader.models import UserSubscription +from mongoengine.queryset import NotUniqueError + +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + apply_classifier_authors, + apply_classifier_feeds, + apply_classifier_tags, + apply_classifier_titles, + get_classifiers_for_user, + sort_classifiers_by_feed, +) from apps.profile.models import Profile +from apps.reader.models import UserSubscription +from apps.rss_feeds.models import Feed, MStarredStory, MStory +from apps.social.models import ( + MActivity, + MCommentReply, + MFollowRequest, + MInteraction, + MSharedStory, + MSocialProfile, + MSocialServices, + MSocialSubscription, +) +from apps.social.tasks import ( + EmailCommentReplies, + EmailFirstShare, + EmailStoryReshares, + PostToService, + UpdateRecalcForSubscription, +) +from utils import jennyholzer from utils import json_functions as json from utils import log as logging -from utils.user_functions import get_user, ajax_login_required -from utils.view_functions import render_to, is_true -from utils.view_functions import required_params -from utils.story_functions import format_story_link_date__short -from utils.story_functions import format_story_link_date__long -from utils.story_functions import strip_tags from utils.ratelimit import ratelimit -from utils import jennyholzer +from utils.story_functions import ( + format_story_link_date__long, + format_story_link_date__short, + strip_tags, +) +from utils.user_functions import ajax_login_required, get_user +from utils.view_functions import is_true, render_to, required_params from vendor.timezones.utilities import localtime_for_timezone @json.json_view def load_social_stories(request, user_id, username=None): - user = get_user(request) + user = get_user(request) social_user_id = int(user_id) - social_user = get_object_or_404(User, pk=social_user_id) - offset = int(request.GET.get('offset', 0)) - limit = int(request.GET.get('limit', 6)) - page = int(request.GET.get('page', 1)) - order = request.GET.get('order', 'newest') - read_filter = request.GET.get('read_filter', 'all') - query = request.GET.get('query', '').strip() - include_story_content = is_true(request.GET.get('include_story_content', True)) - stories = [] - message = None - - if page: offset = limit * (int(page) - 1) + social_user = get_object_or_404(User, pk=social_user_id) + offset = int(request.GET.get("offset", 0)) + limit = int(request.GET.get("limit", 6)) + page = int(request.GET.get("page", 1)) + order = request.GET.get("order", "newest") + read_filter = request.GET.get("read_filter", "all") + query = request.GET.get("query", "").strip() + include_story_content = is_true(request.GET.get("include_story_content", True)) + stories = [] + message = None + + if page: + offset = limit * (int(page) - 1) now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) - + social_profile = MSocialProfile.get_user(social_user.pk) try: socialsub = MSocialSubscription.objects.get(user_id=user.pk, subscription_user_id=social_user_id) except MSocialSubscription.DoesNotExist: socialsub = None - + if social_profile.private and not social_profile.is_followed_by_user(user.pk): - message = "%s has a private blurblog and you must be following them in order to read it." % social_profile.username + message = ( + "%s has a private blurblog and you must be following them in order to read it." + % social_profile.username + ) elif query: if user.profile.is_premium: stories = social_profile.find_stories(query, offset=offset, limit=limit) else: stories = [] message = "You must be a premium subscriber to search." - elif socialsub and (read_filter == 'unread' or order == 'oldest'): + elif socialsub and (read_filter == "unread" or order == "oldest"): cutoff_date = max(socialsub.mark_read_date, user.profile.unread_cutoff) - story_hashes = socialsub.get_stories(order=order, read_filter=read_filter, offset=offset, limit=limit, cutoff_date=cutoff_date) - story_date_order = "%sshared_date" % ('' if order == 'oldest' else '-') + story_hashes = socialsub.get_stories( + order=order, read_filter=read_filter, offset=offset, limit=limit, cutoff_date=cutoff_date + ) + story_date_order = "%sshared_date" % ("" if order == "oldest" else "-") if story_hashes: - mstories = MSharedStory.objects(user_id=social_user.pk, - story_hash__in=story_hashes).order_by(story_date_order) - for story in mstories: story.extract_image_urls() + mstories = MSharedStory.objects(user_id=social_user.pk, story_hash__in=story_hashes).order_by( + story_date_order + ) + for story in mstories: + story.extract_image_urls() stories = Feed.format_stories(mstories) else: - mstories = MSharedStory.objects(user_id=social_user.pk).order_by('-shared_date')[offset:offset+limit] - for story in mstories: story.extract_image_urls() + mstories = MSharedStory.objects(user_id=social_user.pk).order_by("-shared_date")[ + offset : offset + limit + ] + for story in mstories: + story.extract_image_urls() stories = Feed.format_stories(mstories) - if not stories or False: # False is to force a recount even if 0 stories + if not stories or False: # False is to force a recount even if 0 stories return dict(stories=[], message=message) - + stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk, check_all=True) - story_feed_ids = list(set(s['story_feed_id'] for s in stories)) + story_feed_ids = list(set(s["story_feed_id"] for s in stories)) usersubs = UserSubscription.objects.filter(user__pk=user.pk, feed__pk__in=story_feed_ids) usersubs_map = dict((sub.feed_id, sub) for sub in usersubs) unsub_feed_ids = list(set(story_feed_ids).difference(set(usersubs_map.keys()))) @@ -95,115 +136,140 @@ def load_social_stories(request, user_id, username=None): date_delta = user.profile.unread_cutoff if socialsub and date_delta < socialsub.mark_read_date: date_delta = socialsub.mark_read_date - + # Get intelligence classifier for user - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, social_user_id=social_user_id)) + classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, social_user_id=social_user_id)) classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, social_user_id=social_user_id)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, social_user_id=social_user_id)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, social_user_id=social_user_id)) + classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, social_user_id=social_user_id)) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, social_user_id=social_user_id)) # Merge with feed specific classifiers - classifier_feeds = classifier_feeds + list(MClassifierFeed.objects(user_id=user.pk, feed_id__in=story_feed_ids)) - classifier_authors = classifier_authors + list(MClassifierAuthor.objects(user_id=user.pk, feed_id__in=story_feed_ids)) - classifier_titles = classifier_titles + list(MClassifierTitle.objects(user_id=user.pk, feed_id__in=story_feed_ids)) - classifier_tags = classifier_tags + list(MClassifierTag.objects(user_id=user.pk, feed_id__in=story_feed_ids)) + classifier_feeds = classifier_feeds + list( + MClassifierFeed.objects(user_id=user.pk, feed_id__in=story_feed_ids) + ) + classifier_authors = classifier_authors + list( + MClassifierAuthor.objects(user_id=user.pk, feed_id__in=story_feed_ids) + ) + classifier_titles = classifier_titles + list( + MClassifierTitle.objects(user_id=user.pk, feed_id__in=story_feed_ids) + ) + classifier_tags = classifier_tags + list( + MClassifierTag.objects(user_id=user.pk, feed_id__in=story_feed_ids) + ) unread_story_hashes = [] - if (read_filter == 'all' or query) and socialsub: - unread_story_hashes = socialsub.get_stories(read_filter='unread', limit=500, cutoff_date=user.profile.unread_cutoff) - story_hashes = [story['story_hash'] for story in stories] - - starred_stories = MStarredStory.objects(user_id=user.pk, - story_hash__in=story_hashes)\ - .only('story_hash', 'starred_date', 'user_tags') - shared_stories = MSharedStory.objects(user_id=user.pk, - story_hash__in=story_hashes)\ - .hint([('story_hash', 1)])\ - .only('story_hash', 'shared_date', 'comments') - starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date, - user_tags=story.user_tags)) - for story in starred_stories]) - shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, - comments=story.comments)) - for story in shared_stories]) - + if (read_filter == "all" or query) and socialsub: + unread_story_hashes = socialsub.get_stories( + read_filter="unread", limit=500, cutoff_date=user.profile.unread_cutoff + ) + story_hashes = [story["story_hash"] for story in stories] + + starred_stories = MStarredStory.objects(user_id=user.pk, story_hash__in=story_hashes).only( + "story_hash", "starred_date", "user_tags" + ) + shared_stories = ( + MSharedStory.objects(user_id=user.pk, story_hash__in=story_hashes) + .hint([("story_hash", 1)]) + .only("story_hash", "shared_date", "comments") + ) + starred_stories = dict( + [ + (story.story_hash, dict(starred_date=story.starred_date, user_tags=story.user_tags)) + for story in starred_stories + ] + ) + shared_stories = dict( + [ + (story.story_hash, dict(shared_date=story.shared_date, comments=story.comments)) + for story in shared_stories + ] + ) + nowtz = localtime_for_timezone(now, user.profile.timezone) for story in stories: if not include_story_content: - del story['story_content'] - story['social_user_id'] = social_user_id + del story["story_content"] + story["social_user_id"] = social_user_id # story_date = localtime_for_timezone(story['story_date'], user.profile.timezone) - shared_date = localtime_for_timezone(story['shared_date'], user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(shared_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(shared_date, nowtz) - - story['read_status'] = 1 - if story['story_date'] < user.profile.unread_cutoff: - story['read_status'] = 1 - elif (read_filter == 'all' or query) and socialsub: - story['read_status'] = 1 if story['story_hash'] not in unread_story_hashes else 0 - elif read_filter == 'unread' and socialsub: - story['read_status'] = 0 - - if story['story_hash'] in starred_stories: - story['starred'] = True - starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'], - user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, now) - story['user_tags'] = starred_stories[story['story_hash']]['user_tags'] - if story['story_hash'] in shared_stories: - story['shared'] = True - story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments']) - - story['intelligence'] = { - 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'], - social_user_ids=social_user_id), - 'author': apply_classifier_authors(classifier_authors, story), - 'tags': apply_classifier_tags(classifier_tags, story), - 'title': apply_classifier_titles(classifier_titles, story), + shared_date = localtime_for_timezone(story["shared_date"], user.profile.timezone) + story["short_parsed_date"] = format_story_link_date__short(shared_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(shared_date, nowtz) + + story["read_status"] = 1 + if story["story_date"] < user.profile.unread_cutoff: + story["read_status"] = 1 + elif (read_filter == "all" or query) and socialsub: + story["read_status"] = 1 if story["story_hash"] not in unread_story_hashes else 0 + elif read_filter == "unread" and socialsub: + story["read_status"] = 0 + + if story["story_hash"] in starred_stories: + story["starred"] = True + starred_date = localtime_for_timezone( + starred_stories[story["story_hash"]]["starred_date"], user.profile.timezone + ) + story["starred_date"] = format_story_link_date__long(starred_date, now) + story["user_tags"] = starred_stories[story["story_hash"]]["user_tags"] + if story["story_hash"] in shared_stories: + story["shared"] = True + story["shared_comments"] = strip_tags(shared_stories[story["story_hash"]]["comments"]) + + story["intelligence"] = { + "feed": apply_classifier_feeds( + classifier_feeds, story["story_feed_id"], social_user_ids=social_user_id + ), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } - - - classifiers = sort_classifiers_by_feed(user=user, feed_ids=story_feed_ids, - classifier_feeds=classifier_feeds, - classifier_authors=classifier_authors, - classifier_titles=classifier_titles, - classifier_tags=classifier_tags) + + classifiers = sort_classifiers_by_feed( + user=user, + feed_ids=story_feed_ids, + classifier_feeds=classifier_feeds, + classifier_authors=classifier_authors, + classifier_titles=classifier_titles, + classifier_tags=classifier_tags, + ) if socialsub: socialsub.feed_opens += 1 socialsub.needs_unread_recalc = True socialsub.save() - + search_log = "~SN~FG(~SB%s~SN)" % query if query else "" - logging.user(request, "~FYLoading ~FMshared stories~FY: ~SB%s%s %s" % ( - social_profile.title[:22], ('~SN/p%s' % page) if page > 1 else '', search_log)) + logging.user( + request, + "~FYLoading ~FMshared stories~FY: ~SB%s%s %s" + % (social_profile.title[:22], ("~SN/p%s" % page) if page > 1 else "", search_log), + ) return { - "stories": stories, - "user_profiles": user_profiles, - "feeds": unsub_feeds, + "stories": stories, + "user_profiles": user_profiles, + "feeds": unsub_feeds, "classifiers": classifiers, } - + + @json.json_view def load_river_blurblog(request): - limit = int(request.GET.get('limit', 10)) - start = time.time() - user = get_user(request) - social_user_ids = request.GET.getlist('social_user_ids') or request.GET.getlist('social_user_ids[]') - social_user_ids = [int(uid) for uid in social_user_ids if uid] + limit = int(request.GET.get("limit", 10)) + start = time.time() + user = get_user(request) + social_user_ids = request.GET.getlist("social_user_ids") or request.GET.getlist("social_user_ids[]") + social_user_ids = [int(uid) for uid in social_user_ids if uid] original_user_ids = list(social_user_ids) - page = int(request.GET.get('page', 1)) - order = request.GET.get('order', 'newest') - read_filter = request.GET.get('read_filter', 'unread') - relative_user_id = request.GET.get('relative_user_id', None) - global_feed = request.GET.get('global_feed', None) - on_dashboard = is_true(request.GET.get('dashboard', False)) - now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) + page = int(request.GET.get("page", 1)) + order = request.GET.get("order", "newest") + read_filter = request.GET.get("read_filter", "unread") + relative_user_id = request.GET.get("relative_user_id", None) + global_feed = request.GET.get("global_feed", None) + on_dashboard = is_true(request.GET.get("dashboard", False)) + now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone) if global_feed: - global_user = User.objects.get(username='popular') + global_user = User.objects.get(username="popular") relative_user_id = global_user.pk - + if not relative_user_id: relative_user_id = user.pk @@ -213,147 +279,172 @@ def load_river_blurblog(request): if not social_user_ids: social_user_ids = [s.subscription_user_id for s in socialsubs] - - offset = (page-1) * limit + + offset = (page - 1) * limit limit = page * limit - 1 - + story_hashes, story_dates, unread_feed_story_hashes = MSocialSubscription.feed_stories( - user.pk, social_user_ids, - offset=offset, limit=limit, - order=order, read_filter=read_filter, - relative_user_id=relative_user_id, - socialsubs=socialsubs, - cutoff_date=user.profile.unread_cutoff, - dashboard_global=on_dashboard and global_feed) + user.pk, + social_user_ids, + offset=offset, + limit=limit, + order=order, + read_filter=read_filter, + relative_user_id=relative_user_id, + socialsubs=socialsubs, + cutoff_date=user.profile.unread_cutoff, + dashboard_global=on_dashboard and global_feed, + ) mstories = MStory.find_by_story_hashes(story_hashes) story_hashes_to_dates = dict(list(zip(story_hashes, story_dates))) sorted_mstories = reversed(sorted(mstories, key=lambda x: int(story_hashes_to_dates[str(x.story_hash)]))) stories = Feed.format_stories(sorted_mstories) for s, story in enumerate(stories): - timestamp = story_hashes_to_dates[story['story_hash']] - story['story_date'] = datetime.datetime.fromtimestamp(timestamp) + timestamp = story_hashes_to_dates[story["story_hash"]] + story["story_date"] = datetime.datetime.fromtimestamp(timestamp) share_relative_user_id = relative_user_id if global_feed: share_relative_user_id = user.pk - - stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, - share_relative_user_id, - check_all=True) - story_feed_ids = list(set(s['story_feed_id'] for s in stories)) + stories, user_profiles = MSharedStory.stories_with_comments_and_profiles( + stories, share_relative_user_id, check_all=True + ) + + story_feed_ids = list(set(s["story_feed_id"] for s in stories)) usersubs = UserSubscription.objects.filter(user__pk=user.pk, feed__pk__in=story_feed_ids) usersubs_map = dict((sub.feed_id, sub) for sub in usersubs) unsub_feed_ids = list(set(story_feed_ids).difference(set(usersubs_map.keys()))) unsub_feeds = Feed.objects.filter(pk__in=unsub_feed_ids) unsub_feeds = [feed.canonical(include_favicon=False) for feed in unsub_feeds] - + if story_feed_ids: - story_hashes = [story['story_hash'] for story in stories] - starred_stories = MStarredStory.objects( - user_id=user.pk, - story_hash__in=story_hashes - ).only('story_hash', 'starred_date', 'user_tags') - starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date, - user_tags=story.user_tags)) - for story in starred_stories]) - shared_stories = MSharedStory.objects(user_id=user.pk, - story_hash__in=story_hashes)\ - .hint([('story_hash', 1)])\ - .only('story_hash', 'shared_date', 'comments') - shared_stories = dict([(story.story_hash, dict(shared_date=story.shared_date, - comments=story.comments)) - for story in shared_stories]) + story_hashes = [story["story_hash"] for story in stories] + starred_stories = MStarredStory.objects(user_id=user.pk, story_hash__in=story_hashes).only( + "story_hash", "starred_date", "user_tags" + ) + starred_stories = dict( + [ + (story.story_hash, dict(starred_date=story.starred_date, user_tags=story.user_tags)) + for story in starred_stories + ] + ) + shared_stories = ( + MSharedStory.objects(user_id=user.pk, story_hash__in=story_hashes) + .hint([("story_hash", 1)]) + .only("story_hash", "shared_date", "comments") + ) + shared_stories = dict( + [ + (story.story_hash, dict(shared_date=story.shared_date, comments=story.comments)) + for story in shared_stories + ] + ) else: starred_stories = {} shared_stories = {} - + # Intelligence classifiers for all feeds involved if story_feed_ids: - classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, - social_user_id__in=social_user_ids)) - classifier_feeds = classifier_feeds + list(MClassifierFeed.objects(user_id=user.pk, - feed_id__in=story_feed_ids)) - classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, - feed_id__in=story_feed_ids)) - classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, - feed_id__in=story_feed_ids)) - classifier_tags = list(MClassifierTag.objects(user_id=user.pk, - feed_id__in=story_feed_ids)) + classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk, social_user_id__in=social_user_ids)) + classifier_feeds = classifier_feeds + list( + MClassifierFeed.objects(user_id=user.pk, feed_id__in=story_feed_ids) + ) + classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk, feed_id__in=story_feed_ids)) + classifier_titles = list(MClassifierTitle.objects(user_id=user.pk, feed_id__in=story_feed_ids)) + classifier_tags = list(MClassifierTag.objects(user_id=user.pk, feed_id__in=story_feed_ids)) else: classifier_feeds = [] classifier_authors = [] classifier_titles = [] classifier_tags = [] - + # Just need to format stories nowtz = localtime_for_timezone(now, user.profile.timezone) for story in stories: - story['read_status'] = 0 - if story['story_hash'] not in unread_feed_story_hashes: - story['read_status'] = 1 - story_date = localtime_for_timezone(story['story_date'], user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(story_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(story_date, nowtz) - if story['story_hash'] in starred_stories: - story['starred'] = True - starred_date = localtime_for_timezone(starred_stories[story['story_hash']]['starred_date'], user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, now) - story['user_tags'] = starred_stories[story['story_hash']]['user_tags'] - story['intelligence'] = { - 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'], - social_user_ids=story['friend_user_ids']), - 'author': apply_classifier_authors(classifier_authors, story), - 'tags': apply_classifier_tags(classifier_tags, story), - 'title': apply_classifier_titles(classifier_titles, story), + story["read_status"] = 0 + if story["story_hash"] not in unread_feed_story_hashes: + story["read_status"] = 1 + story_date = localtime_for_timezone(story["story_date"], user.profile.timezone) + story["short_parsed_date"] = format_story_link_date__short(story_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(story_date, nowtz) + if story["story_hash"] in starred_stories: + story["starred"] = True + starred_date = localtime_for_timezone( + starred_stories[story["story_hash"]]["starred_date"], user.profile.timezone + ) + story["starred_date"] = format_story_link_date__long(starred_date, now) + story["user_tags"] = starred_stories[story["story_hash"]]["user_tags"] + story["intelligence"] = { + "feed": apply_classifier_feeds( + classifier_feeds, story["story_feed_id"], social_user_ids=story["friend_user_ids"] + ), + "author": apply_classifier_authors(classifier_authors, story), + "tags": apply_classifier_tags(classifier_tags, story), + "title": apply_classifier_titles(classifier_titles, story), } - if story['story_hash'] in shared_stories: - story['shared'] = True - shared_date = localtime_for_timezone(shared_stories[story['story_hash']]['shared_date'], - user.profile.timezone) - story['shared_date'] = format_story_link_date__long(shared_date, now) - story['shared_comments'] = strip_tags(shared_stories[story['story_hash']]['comments']) - if (shared_stories[story['story_hash']]['shared_date'] < user.profile.unread_cutoff or - story['story_hash'] not in unread_feed_story_hashes): - story['read_status'] = 1 - - classifiers = sort_classifiers_by_feed(user=user, feed_ids=story_feed_ids, - classifier_feeds=classifier_feeds, - classifier_authors=classifier_authors, - classifier_titles=classifier_titles, - classifier_tags=classifier_tags) + if story["story_hash"] in shared_stories: + story["shared"] = True + shared_date = localtime_for_timezone( + shared_stories[story["story_hash"]]["shared_date"], user.profile.timezone + ) + story["shared_date"] = format_story_link_date__long(shared_date, now) + story["shared_comments"] = strip_tags(shared_stories[story["story_hash"]]["comments"]) + if ( + shared_stories[story["story_hash"]]["shared_date"] < user.profile.unread_cutoff + or story["story_hash"] not in unread_feed_story_hashes + ): + story["read_status"] = 1 + + classifiers = sort_classifiers_by_feed( + user=user, + feed_ids=story_feed_ids, + classifier_feeds=classifier_feeds, + classifier_authors=classifier_authors, + classifier_titles=classifier_titles, + classifier_tags=classifier_tags, + ) diff = time.time() - start timediff = round(float(diff), 2) - logging.user(request, "~FY%sLoading ~FCriver ~FMblurblogs~FC stories~FY: ~SBp%s~SN (%s/%s " - "stories, ~SN%s/%s/%s feeds)" % - ("~FCAuto-" if on_dashboard else "", - page, len(stories), len(mstories), len(story_feed_ids), - len(social_user_ids), len(original_user_ids))) - - + logging.user( + request, + "~FY%sLoading ~FCriver ~FMblurblogs~FC stories~FY: ~SBp%s~SN (%s/%s " + "stories, ~SN%s/%s/%s feeds)" + % ( + "~FCAuto-" if on_dashboard else "", + page, + len(stories), + len(mstories), + len(story_feed_ids), + len(social_user_ids), + len(original_user_ids), + ), + ) + return { - "stories": stories, - "user_profiles": user_profiles, - "feeds": unsub_feeds, + "stories": stories, + "user_profiles": user_profiles, + "feeds": unsub_feeds, "classifiers": classifiers, "elapsed_time": timediff, } - + + def load_social_page(request, user_id, username=None, **kwargs): user = get_user(request.user) social_user_id = int(user_id) social_user = get_object_or_404(User, pk=social_user_id) - offset = int(request.GET.get('offset', 0)) - limit = int(request.GET.get('limit', 6)) + offset = int(request.GET.get("offset", 0)) + limit = int(request.GET.get("limit", 6)) try: - page = int(request.GET.get('page', 1)) + page = int(request.GET.get("page", 1)) except ValueError: page = 1 - format = request.GET.get('format', None) + format = request.GET.get("format", None) has_next_page = False - feed_id = kwargs.get('feed_id') or request.GET.get('feed_id') - if page: - offset = limit * (page-1) + feed_id = kwargs.get("feed_id") or request.GET.get("feed_id") + if page: + offset = limit * (page - 1) social_services = None user_social_profile = None user_social_services = None @@ -364,9 +455,9 @@ def load_social_page(request, user_id, username=None, **kwargs): user_social_services = MSocialServices.get_user(user.pk) user_following_social_profile = user_social_profile.is_following_user(social_user_id) social_profile = MSocialProfile.get_user(social_user_id) - - if username and '.dev' in username: - username = username.replace('.dev', '') + + if username and ".dev" in username: + username = username.replace(".dev", "") current_tab = "blurblogs" global_feed = False if username == "popular": @@ -374,39 +465,46 @@ def load_social_page(request, user_id, username=None, **kwargs): elif username == "popular.global": current_tab = "global" global_feed = True - - if social_profile.private and (not user.is_authenticated or - not social_profile.is_followed_by_user(user.pk)): + + if social_profile.private and ( + not user.is_authenticated or not social_profile.is_followed_by_user(user.pk) + ): stories = [] elif global_feed: - socialsubs = MSocialSubscription.objects.filter(user_id=relative_user_id) + socialsubs = MSocialSubscription.objects.filter(user_id=relative_user_id) social_user_ids = [s.subscription_user_id for s in socialsubs] - story_ids, story_dates, _ = MSocialSubscription.feed_stories(user.pk, social_user_ids, - offset=offset, limit=limit+1, - # order=order, read_filter=read_filter, - relative_user_id=relative_user_id, - cache=request.user.is_authenticated, - cutoff_date=user.profile.unread_cutoff) + story_ids, story_dates, _ = MSocialSubscription.feed_stories( + user.pk, + social_user_ids, + offset=offset, + limit=limit + 1, + # order=order, read_filter=read_filter, + relative_user_id=relative_user_id, + cache=request.user.is_authenticated, + cutoff_date=user.profile.unread_cutoff, + ) if len(story_ids) > limit: has_next_page = True story_ids = story_ids[:-1] mstories = MStory.find_by_story_hashes(story_ids) story_id_to_dates = dict(list(zip(story_ids, story_dates))) - def sort_stories_by_id(a, b): - return int(story_id_to_dates[str(b.story_hash)]) - int(story_id_to_dates[str(a.story_hash)]) - sorted_mstories = sorted(mstories, key=sort_stories_by_id) + + def sort_stories_by_id(story): + return int(story_id_to_dates[str(story.story_hash)]) + + sorted_mstories = sorted(mstories, key=sort_stories_by_id, reverse=True) stories = Feed.format_stories(sorted_mstories) for story in stories: - story['shared_date'] = story['story_date'] + story["shared_date"] = story["story_date"] else: params = dict(user_id=social_user.pk) if feed_id: - params['story_feed_id'] = feed_id - if 'story_db_id' in params: - params.pop('story_db_id') - mstories = MSharedStory.objects(**params).order_by('-shared_date')[offset:offset+limit+1] + params["story_feed_id"] = feed_id + if "story_db_id" in params: + params.pop("story_db_id") + mstories = MSharedStory.objects(**params).order_by("-shared_date")[offset : offset + limit + 1] stories = Feed.format_stories(mstories, include_permalinks=True) - + if len(stories) > limit: has_next_page = True stories = stories[:-1] @@ -419,181 +517,210 @@ def sort_stories_by_id(a, b): "social_user": social_user, "social_profile": social_profile, "user_social_services": user_social_services, - 'user_social_profile' : json.encode(user_social_profile and user_social_profile.page()), - 'user_following_social_profile': user_following_social_profile, + "user_social_profile": json.encode(user_social_profile and user_social_profile.page()), + "user_following_social_profile": user_following_social_profile, } - template = 'social/social_page.xhtml' + template = "social/social_page.xhtml" return render(request, template, params) - story_feed_ids = list(set(s['story_feed_id'] for s in stories)) + story_feed_ids = list(set(s["story_feed_id"] for s in stories)) feeds = Feed.objects.filter(pk__in=story_feed_ids) feeds = dict((feed.pk, feed.canonical(include_favicon=False)) for feed in feeds) for story in stories: - if story['story_feed_id'] in feeds: + if story["story_feed_id"] in feeds: # Feed could have been deleted. - story['feed'] = feeds[story['story_feed_id']] - shared_date = localtime_for_timezone(story['shared_date'], user.profile.timezone) - story['shared_date'] = shared_date - - stories, profiles = MSharedStory.stories_with_comments_and_profiles(stories, social_user.pk, - check_all=True) + story["feed"] = feeds[story["story_feed_id"]] + shared_date = localtime_for_timezone(story["shared_date"], user.profile.timezone) + story["shared_date"] = shared_date + + stories, profiles = MSharedStory.stories_with_comments_and_profiles( + stories, social_user.pk, check_all=True + ) if user.is_authenticated: for story in stories: - if user.pk in story['share_user_ids']: - story['shared_by_user'] = True - shared_story = MSharedStory.objects.hint([('story_hash', 1)])\ - .get(user_id=user.pk, - story_feed_id=story['story_feed_id'], - story_hash=story['story_hash']) - story['user_comments'] = shared_story.comments + if user.pk in story["share_user_ids"]: + story["shared_by_user"] = True + shared_story = MSharedStory.objects.hint([("story_hash", 1)]).get( + user_id=user.pk, story_feed_id=story["story_feed_id"], story_hash=story["story_hash"] + ) + story["user_comments"] = shared_story.comments stories = MSharedStory.attach_users_to_stories(stories, profiles) - + active_story = None - path = request.META['PATH_INFO'] - if '/story/' in path and format != 'html': + path = request.META["PATH_INFO"] + if "/story/" in path and format != "html": story_id = re.sub(r"^/story/.*?/(.*?)/?", "", path) - if not story_id or '/story' in story_id: - story_id = path.replace('/story/', '') + if not story_id or "/story" in story_id: + story_id = path.replace("/story/", "") social_services = MSocialServices.get_user(social_user.pk) - active_story_db = MSharedStory.objects.filter(user_id=social_user.pk, - story_hash=story_id)\ - .hint([('story_hash', 1)])\ - .limit(1) + active_story_db = ( + MSharedStory.objects.filter(user_id=social_user.pk, story_hash=story_id) + .hint([("story_hash", 1)]) + .limit(1) + ) if active_story_db: active_story_db = active_story_db[0] if user_social_profile.bb_permalink_direct: return HttpResponseRedirect(active_story_db.story_permalink) active_story = Feed.format_story(active_story_db) if active_story_db.image_count: - active_story['image_url'] = active_story_db.image_sizes[0]['src'] - active_story['tags'] = ', '.join(active_story_db.story_tags) - active_story['blurblog_permalink'] = active_story_db.blurblog_permalink() - active_story['iso8601'] = active_story_db.story_date.isoformat() - if active_story['story_feed_id']: - feed = Feed.get_by_id(active_story['story_feed_id']) + active_story["image_url"] = active_story_db.image_sizes[0]["src"] + active_story["tags"] = ", ".join(active_story_db.story_tags) + active_story["blurblog_permalink"] = active_story_db.blurblog_permalink() + active_story["iso8601"] = active_story_db.story_date.isoformat() + if active_story["story_feed_id"]: + feed = Feed.get_by_id(active_story["story_feed_id"]) if feed: - active_story['feed'] = feed.canonical() - + active_story["feed"] = feed.canonical() + params = { - 'social_user' : social_user, - 'stories' : stories, - 'user_social_profile' : user_social_profile, - 'user_social_profile_page' : json.encode(user_social_profile and user_social_profile.page()), - 'user_social_services' : user_social_services, - 'user_social_services_page' : json.encode(user_social_services and user_social_services.canonical()), - 'user_following_social_profile': user_following_social_profile, - 'social_profile': social_profile, - 'feeds' : feeds, - 'user_profile' : hasattr(user, 'profile') and user.profile, - 'has_next_page' : has_next_page, - 'holzer_truism' : random.choice(jennyholzer.TRUISMS), #if not has_next_page else None - 'facebook_app_id': settings.FACEBOOK_APP_ID, - 'active_story' : active_story, - 'current_tab' : current_tab, - 'social_services': social_services, + "social_user": social_user, + "stories": stories, + "user_social_profile": user_social_profile, + "user_social_profile_page": json.encode(user_social_profile and user_social_profile.page()), + "user_social_services": user_social_services, + "user_social_services_page": json.encode(user_social_services and user_social_services.canonical()), + "user_following_social_profile": user_following_social_profile, + "social_profile": social_profile, + "feeds": feeds, + "user_profile": hasattr(user, "profile") and user.profile, + "has_next_page": has_next_page, + "holzer_truism": random.choice(jennyholzer.TRUISMS), # if not has_next_page else None + "facebook_app_id": settings.FACEBOOK_APP_ID, + "active_story": active_story, + "current_tab": current_tab, + "social_services": social_services, } - logging.user(request, "~FYLoading ~FMsocial page~FY: ~SB%s%s ~FM%s/%s" % ( - social_profile.title[:22], ('~SN/p%s' % page) if page > 1 else '', - request.META.get('HTTP_USER_AGENT', "")[:40], - request.META.get('HTTP_X_FORWARDED_FOR', ""))) - if format == 'html': - template = 'social/social_stories.xhtml' + logging.user( + request, + "~FYLoading ~FMsocial page~FY: ~SB%s%s ~FM%s/%s" + % ( + social_profile.title[:22], + ("~SN/p%s" % page) if page > 1 else "", + request.META.get("HTTP_USER_AGENT", "")[:40], + request.META.get("HTTP_X_FORWARDED_FOR", ""), + ), + ) + if format == "html": + template = "social/social_stories.xhtml" else: - template = 'social/social_page.xhtml' - + template = "social/social_page.xhtml" + return render(request, template, params) -@required_params('story_id', feed_id=int, method="GET") + +@required_params("story_id", feed_id=int, method="GET") def story_public_comments(request): - format = request.GET.get('format', 'json') - relative_user_id = request.GET.get('user_id', None) - feed_id = int(request.GET.get('feed_id')) - story_id = request.GET.get('story_id') - + format = request.GET.get("format", "json") + relative_user_id = request.GET.get("user_id", None) + feed_id = int(request.GET.get("feed_id")) + story_id = request.GET.get("story_id") + if not relative_user_id: relative_user_id = get_user(request).pk - + story, _ = MStory.find_story(story_feed_id=feed_id, story_id=story_id) if not story: - return json.json_response(request, { - 'message': "Story not found.", - 'code': -1, - }) - + return json.json_response( + request, + { + "message": "Story not found.", + "code": -1, + }, + ) + story = Feed.format_story(story) - stories, profiles = MSharedStory.stories_with_comments_and_profiles([story], - relative_user_id, - check_all=True) - - if format == 'html': + stories, profiles = MSharedStory.stories_with_comments_and_profiles( + [story], relative_user_id, check_all=True + ) + + if format == "html": stories = MSharedStory.attach_users_to_stories(stories, profiles) - return render(request, 'social/story_comments.xhtml', { - 'story': stories[0], - }) + return render( + request, + "social/story_comments.xhtml", + { + "story": stories[0], + }, + ) else: - return json.json_response(request, { - 'comments': stories[0]['public_comments'], - 'user_profiles': profiles, - }) + return json.json_response( + request, + { + "comments": stories[0]["public_comments"], + "user_profiles": profiles, + }, + ) + @ajax_login_required def mark_story_as_shared(request): - code = 1 - feed_id = int(request.POST['feed_id']) - story_id = request.POST['story_id'] - comments = request.POST.get('comments', '') - source_user_id = request.POST.get('source_user_id') - relative_user_id = request.POST.get('relative_user_id') or request.user.pk - post_to_services = request.POST.getlist('post_to_services') or request.POST.getlist('post_to_services[]') - format = request.POST.get('format', 'json') + code = 1 + feed_id = int(request.POST["feed_id"]) + story_id = request.POST["story_id"] + comments = request.POST.get("comments", "") + source_user_id = request.POST.get("source_user_id") + relative_user_id = request.POST.get("relative_user_id") or request.user.pk + post_to_services = request.POST.getlist("post_to_services") or request.POST.getlist("post_to_services[]") + format = request.POST.get("format", "json") now = datetime.datetime.now() nowtz = localtime_for_timezone(now, request.user.profile.timezone) - + MSocialProfile.get_user(request.user.pk) - + story, original_story_found = MStory.find_story(feed_id, story_id) if not story: - return json.json_response(request, { - 'code': -1, - 'message': 'Could not find the original story and no copies could be found.' - }) - + return json.json_response( + request, + {"code": -1, "message": "Could not find the original story and no copies could be found."}, + ) + feed = Feed.get_by_id(feed_id) if feed and feed.is_newsletter: - return json.json_response(request, { - 'code': -1, - 'message': 'You cannot share newsletters. Somebody could unsubscribe you!' - }) - - if not request.user.profile.is_premium and MSharedStory.feed_quota(request.user.pk, story.story_hash, feed_id=feed_id): - return json.json_response(request, { - 'code': -1, - 'message': 'Only premium users can share multiple stories per day from the same site.' - }) - + return json.json_response( + request, {"code": -1, "message": "You cannot share newsletters. Somebody could unsubscribe you!"} + ) + + if not request.user.profile.is_premium and MSharedStory.feed_quota( + request.user.pk, story.story_hash, feed_id=feed_id + ): + return json.json_response( + request, + { + "code": -1, + "message": "Only premium users can share multiple stories per day from the same site.", + }, + ) + quota = 100 if not request.user.profile.is_premium: quota = 3 if MSharedStory.feed_quota(request.user.pk, story.story_hash, quota=quota): - logging.user(request, "~FRNOT ~FCSharing ~FM%s~FC, over quota: ~SB~FB%s" % (story.story_title[:20], comments[:30])) - message = 'You can only share up to %s stories per day.' % quota + logging.user( + request, + "~FRNOT ~FCSharing ~FM%s~FC, over quota: ~SB~FB%s" % (story.story_title[:20], comments[:30]), + ) + message = "You can only share up to %s stories per day." % quota if not request.user.profile.is_premium: - message = 'You can only share up to %s stories per day as a free user. Upgrade to premium to share more.' % quota - return json.json_response(request, { - 'code': -1, - 'message': message - }) - - shared_story = MSharedStory.objects.filter(user_id=request.user.pk, - story_feed_id=feed_id, - story_hash=story['story_hash'])\ - .hint([('story_hash', 1)])\ - .limit(1).first() + message = ( + "You can only share up to %s stories per day as a free user. Upgrade to premium to share more." + % quota + ) + return json.json_response(request, {"code": -1, "message": message}) + + shared_story = ( + MSharedStory.objects.filter( + user_id=request.user.pk, story_feed_id=feed_id, story_hash=story["story_hash"] + ) + .hint([("story_hash", 1)]) + .limit(1) + .first() + ) if not shared_story: story_db = { "story_guid": story.story_guid, @@ -601,7 +728,7 @@ def mark_story_as_shared(request): "story_permalink": story.story_permalink, "story_title": story.story_title, "story_feed_id": story.story_feed_id, - "story_content_z": getattr(story, 'story_latest_content_z', None) or story.story_content_z, + "story_content_z": getattr(story, "story_latest_content_z", None) or story.story_content_z, "story_author_name": story.story_author_name, "story_tags": story.story_tags, "story_date": story.story_date, @@ -613,160 +740,198 @@ def mark_story_as_shared(request): shared_story = MSharedStory.objects.create(**story_db) shared_story.publish_to_subscribers() except NotUniqueError: - shared_story = MSharedStory.objects.get(story_guid=story_db['story_guid'], - user_id=story_db['user_id']) + shared_story = MSharedStory.objects.get( + story_guid=story_db["story_guid"], user_id=story_db["user_id"] + ) except MSharedStory.DoesNotExist: - return json.json_response(request, { - 'code': -1, - 'message': 'Story already shared but then not shared. I don\'t really know. Did you submit this twice very quickly?' - }) + return json.json_response( + request, + { + "code": -1, + "message": "Story already shared but then not shared. I don't really know. Did you submit this twice very quickly?", + }, + ) if source_user_id: shared_story.set_source_user_id(int(source_user_id)) - UpdateRecalcForSubscription.delay(subscription_user_id=request.user.pk, - shared_story_id=str(shared_story.id)) + UpdateRecalcForSubscription.delay( + subscription_user_id=request.user.pk, shared_story_id=str(shared_story.id) + ) logging.user(request, "~FCSharing ~FM%s: ~SB~FB%s" % (story.story_title[:20], comments[:30])) else: shared_story.comments = comments shared_story.has_comments = bool(comments) shared_story.save() - logging.user(request, "~FCUpdating shared story ~FM%s: ~SB~FB%s" % ( - story.story_title[:20], comments[:30])) - + logging.user( + request, "~FCUpdating shared story ~FM%s: ~SB~FB%s" % (story.story_title[:20], comments[:30]) + ) + if original_story_found: story.count_comments() - + story = Feed.format_story(story) check_all = not original_story_found - stories, profiles = MSharedStory.stories_with_comments_and_profiles([story], relative_user_id, - check_all=check_all) + stories, profiles = MSharedStory.stories_with_comments_and_profiles( + [story], relative_user_id, check_all=check_all + ) story = stories[0] - starred_stories = MStarredStory.objects(user_id=request.user.pk, - story_feed_id=story['story_feed_id'], - story_hash=story['story_hash'])\ - .only('story_hash', 'starred_date', 'user_tags').limit(1) + starred_stories = ( + MStarredStory.objects( + user_id=request.user.pk, story_feed_id=story["story_feed_id"], story_hash=story["story_hash"] + ) + .only("story_hash", "starred_date", "user_tags") + .limit(1) + ) if starred_stories: - story['user_tags'] = starred_stories[0]['user_tags'] - story['starred'] = True - starred_date = localtime_for_timezone(starred_stories[0]['starred_date'], - request.user.profile.timezone) - story['starred_date'] = format_story_link_date__long(starred_date, now) - story['shared_comments'] = strip_tags(shared_story['comments'] or "") - story['shared_by_user'] = True - story['shared'] = True - shared_date = localtime_for_timezone(shared_story['shared_date'], request.user.profile.timezone) - story['short_parsed_date'] = format_story_link_date__short(shared_date, nowtz) - story['long_parsed_date'] = format_story_link_date__long(shared_date, nowtz) - + story["user_tags"] = starred_stories[0]["user_tags"] + story["starred"] = True + starred_date = localtime_for_timezone( + starred_stories[0]["starred_date"], request.user.profile.timezone + ) + story["starred_date"] = format_story_link_date__long(starred_date, now) + story["shared_comments"] = strip_tags(shared_story["comments"] or "") + story["shared_by_user"] = True + story["shared"] = True + shared_date = localtime_for_timezone(shared_story["shared_date"], request.user.profile.timezone) + story["short_parsed_date"] = format_story_link_date__short(shared_date, nowtz) + story["long_parsed_date"] = format_story_link_date__long(shared_date, nowtz) + if post_to_services: for service in post_to_services: if service not in shared_story.posted_to_services: PostToService.delay(shared_story_id=str(shared_story.id), service=service) - + if shared_story.source_user_id and shared_story.comments: - EmailStoryReshares.apply_async(kwargs=dict(shared_story_id=str(shared_story.id)), - countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS) - + EmailStoryReshares.apply_async( + kwargs=dict(shared_story_id=str(shared_story.id)), + countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS, + ) + EmailFirstShare.apply_async(kwargs=dict(user_id=request.user.pk)) - - if format == 'html': + if format == "html": stories = MSharedStory.attach_users_to_stories(stories, profiles) - return render(request, 'social/social_story.xhtml', { - 'story': story, - }) + return render( + request, + "social/social_story.xhtml", + { + "story": story, + }, + ) else: - return json.json_response(request, { - 'code': code, - 'story': story, - 'user_profiles': profiles, - }) + return json.json_response( + request, + { + "code": code, + "story": story, + "user_profiles": profiles, + }, + ) + @ajax_login_required def mark_story_as_unshared(request): - feed_id = int(request.POST['feed_id']) - story_id = request.POST['story_id'] - relative_user_id = request.POST.get('relative_user_id') or request.user.pk - format = request.POST.get('format', 'json') + feed_id = int(request.POST["feed_id"]) + story_id = request.POST["story_id"] + relative_user_id = request.POST.get("relative_user_id") or request.user.pk + format = request.POST.get("format", "json") original_story_found = True - - story, original_story_found = MStory.find_story(story_feed_id=feed_id, - story_id=story_id) - - shared_story = MSharedStory.objects(user_id=request.user.pk, - story_feed_id=feed_id, - story_hash=story['story_hash']).limit(1).first() + + story, original_story_found = MStory.find_story(story_feed_id=feed_id, story_id=story_id) + + shared_story = ( + MSharedStory.objects(user_id=request.user.pk, story_feed_id=feed_id, story_hash=story["story_hash"]) + .limit(1) + .first() + ) if not shared_story: - return json.json_response(request, {'code': -1, 'message': 'Shared story not found.'}) - + return json.json_response(request, {"code": -1, "message": "Shared story not found."}) + shared_story.unshare_story() - + if original_story_found: story.count_comments() else: story = shared_story - + story = Feed.format_story(story) - stories, profiles = MSharedStory.stories_with_comments_and_profiles([story], - relative_user_id, - check_all=True) + stories, profiles = MSharedStory.stories_with_comments_and_profiles( + [story], relative_user_id, check_all=True + ) - if format == 'html': + if format == "html": stories = MSharedStory.attach_users_to_stories(stories, profiles) - return render(request, 'social/social_story.xhtml', { - 'story': stories[0], - }) + return render( + request, + "social/social_story.xhtml", + { + "story": stories[0], + }, + ) else: - return json.json_response(request, { - 'code': 1, - 'message': "Story unshared.", - 'story': stories[0], - 'user_profiles': profiles, - }) - + return json.json_response( + request, + { + "code": 1, + "message": "Story unshared.", + "story": stories[0], + "user_profiles": profiles, + }, + ) + + @ajax_login_required def save_comment_reply(request): - code = 1 - feed_id = int(request.POST['story_feed_id']) - story_id = request.POST['story_id'] - comment_user_id = request.POST['comment_user_id'] - reply_comments = request.POST.get('reply_comments') - reply_id = request.POST.get('reply_id') - format = request.POST.get('format', 'json') + code = 1 + feed_id = int(request.POST["story_feed_id"]) + story_id = request.POST["story_id"] + comment_user_id = request.POST["comment_user_id"] + reply_comments = request.POST.get("reply_comments") + reply_id = request.POST.get("reply_id") + format = request.POST.get("format", "json") original_message = None - + if not reply_comments: - return json.json_response(request, { - 'code': -1, - 'message': 'Reply comments cannot be empty.', - }) - + return json.json_response( + request, + { + "code": -1, + "message": "Reply comments cannot be empty.", + }, + ) + commenter_profile = MSocialProfile.get_user(comment_user_id) if commenter_profile.protected and not commenter_profile.is_followed_by_user(request.user.pk): - return json.json_response(request, { - 'code': -1, - 'message': 'You must be following %s to reply to them.' % (commenter_profile.user.username if commenter_profile.user else "[deleted]"), - }) - + return json.json_response( + request, + { + "code": -1, + "message": "You must be following %s to reply to them." + % (commenter_profile.user.username if commenter_profile.user else "[deleted]"), + }, + ) + try: - shared_story = MSharedStory.objects.get(user_id=comment_user_id, - story_feed_id=feed_id, - story_guid=story_id) + shared_story = MSharedStory.objects.get( + user_id=comment_user_id, story_feed_id=feed_id, story_guid=story_id + ) except MSharedStory.DoesNotExist: - return json.json_response(request, { - 'code': -1, - 'message': 'Shared story cannot be found.', - }) - + return json.json_response( + request, + { + "code": -1, + "message": "Shared story cannot be found.", + }, + ) + reply = MCommentReply() reply.user_id = request.user.pk reply.publish_date = datetime.datetime.now() reply.comments = reply_comments - + if reply_id: replies = [] for story_reply in shared_story.replies: - if (story_reply.user_id == reply.user_id and - story_reply.reply_id == ObjectId(reply_id)): + if story_reply.user_id == reply.user_id and story_reply.reply_id == ObjectId(reply_id): reply.publish_date = story_reply.publish_date reply.reply_id = story_reply.reply_id original_message = story_reply.comments @@ -774,80 +939,96 @@ def save_comment_reply(request): else: replies.append(story_reply) shared_story.replies = replies - logging.user(request, "~FCUpdating comment reply in ~FM%s: ~SB~FB%s~FM" % ( - shared_story.story_title[:20], reply_comments[:30])) + logging.user( + request, + "~FCUpdating comment reply in ~FM%s: ~SB~FB%s~FM" + % (shared_story.story_title[:20], reply_comments[:30]), + ) else: reply.reply_id = ObjectId() - logging.user(request, "~FCReplying to comment in: ~FM%s: ~SB~FB%s~FM" % ( - shared_story.story_title[:20], reply_comments[:30])) + logging.user( + request, + "~FCReplying to comment in: ~FM%s: ~SB~FB%s~FM" + % (shared_story.story_title[:20], reply_comments[:30]), + ) shared_story.replies.append(reply) shared_story.save() - + comment, profiles = shared_story.comment_with_author_and_profiles() - + # Interaction for every other replier and original commenter - MActivity.new_comment_reply(user_id=request.user.pk, - comment_user_id=comment['user_id'], - reply_content=reply_comments, - original_message=original_message, - story_id=story_id, - story_feed_id=feed_id, - story_title=shared_story.story_title) - if comment['user_id'] != request.user.pk: - MInteraction.new_comment_reply(user_id=comment['user_id'], - reply_user_id=request.user.pk, - reply_content=reply_comments, - original_message=original_message, - story_id=story_id, - story_feed_id=feed_id, - story_title=shared_story.story_title) - - reply_user_ids = list(r['user_id'] for r in comment['replies']) - for user_id in set(reply_user_ids).difference([comment['user_id']]): + MActivity.new_comment_reply( + user_id=request.user.pk, + comment_user_id=comment["user_id"], + reply_content=reply_comments, + original_message=original_message, + story_id=story_id, + story_feed_id=feed_id, + story_title=shared_story.story_title, + ) + if comment["user_id"] != request.user.pk: + MInteraction.new_comment_reply( + user_id=comment["user_id"], + reply_user_id=request.user.pk, + reply_content=reply_comments, + original_message=original_message, + story_id=story_id, + story_feed_id=feed_id, + story_title=shared_story.story_title, + ) + + reply_user_ids = list(r["user_id"] for r in comment["replies"]) + for user_id in set(reply_user_ids).difference([comment["user_id"]]): if request.user.pk != user_id: - MInteraction.new_reply_reply(user_id=user_id, - comment_user_id=comment['user_id'], - reply_user_id=request.user.pk, - reply_content=reply_comments, - original_message=original_message, - story_id=story_id, - story_feed_id=feed_id, - story_title=shared_story.story_title) - - EmailCommentReplies.apply_async(kwargs=dict(shared_story_id=str(shared_story.id), - reply_id=str(reply.reply_id)), - countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS) - - if format == 'html': + MInteraction.new_reply_reply( + user_id=user_id, + comment_user_id=comment["user_id"], + reply_user_id=request.user.pk, + reply_content=reply_comments, + original_message=original_message, + story_id=story_id, + story_feed_id=feed_id, + story_title=shared_story.story_title, + ) + + EmailCommentReplies.apply_async( + kwargs=dict(shared_story_id=str(shared_story.id), reply_id=str(reply.reply_id)), + countdown=settings.SECONDS_TO_DELAY_CELERY_EMAILS, + ) + + if format == "html": comment = MSharedStory.attach_users_to_comment(comment, profiles) - return render(request, 'social/story_comment.xhtml', { - 'comment': comment, - }) + return render( + request, + "social/story_comment.xhtml", + { + "comment": comment, + }, + ) else: - return json.json_response(request, { - 'code': code, - 'comment': comment, - 'reply_id': reply.reply_id, - 'user_profiles': profiles - }) + return json.json_response( + request, {"code": code, "comment": comment, "reply_id": reply.reply_id, "user_profiles": profiles} + ) + @ajax_login_required def remove_comment_reply(request): - code = 1 - feed_id = int(request.POST['story_feed_id']) - story_id = request.POST['story_id'] - comment_user_id = request.POST['comment_user_id'] - reply_id = request.POST.get('reply_id') - format = request.POST.get('format', 'json') + code = 1 + feed_id = int(request.POST["story_feed_id"]) + story_id = request.POST["story_id"] + comment_user_id = request.POST["comment_user_id"] + reply_id = request.POST.get("reply_id") + format = request.POST.get("format", "json") original_message = None - - shared_story = MSharedStory.objects.get(user_id=comment_user_id, - story_feed_id=feed_id, - story_guid=story_id) + + shared_story = MSharedStory.objects.get( + user_id=comment_user_id, story_feed_id=feed_id, story_guid=story_id + ) replies = [] for story_reply in shared_story.replies: - if ((story_reply.user_id == request.user.pk or request.user.is_staff) and - story_reply.reply_id == ObjectId(reply_id)): + if ( + story_reply.user_id == request.user.pk or request.user.is_staff + ) and story_reply.reply_id == ObjectId(reply_id): original_message = story_reply.comments # Skip reply else: @@ -855,53 +1036,64 @@ def remove_comment_reply(request): shared_story.replies = replies shared_story.save() - logging.user(request, "~FCRemoving comment reply in ~FM%s: ~SB~FB%s~FM" % ( - shared_story.story_title[:20], original_message and original_message[:30])) - + logging.user( + request, + "~FCRemoving comment reply in ~FM%s: ~SB~FB%s~FM" + % (shared_story.story_title[:20], original_message and original_message[:30]), + ) + comment, profiles = shared_story.comment_with_author_and_profiles() # Interaction for every other replier and original commenter - MActivity.remove_comment_reply(user_id=request.user.pk, - comment_user_id=comment['user_id'], - reply_content=original_message, - story_id=story_id, - story_feed_id=feed_id) - MInteraction.remove_comment_reply(user_id=comment['user_id'], - reply_user_id=request.user.pk, - reply_content=original_message, - story_id=story_id, - story_feed_id=feed_id) - - reply_user_ids = [reply['user_id'] for reply in comment['replies']] - for user_id in set(reply_user_ids).difference([comment['user_id']]): + MActivity.remove_comment_reply( + user_id=request.user.pk, + comment_user_id=comment["user_id"], + reply_content=original_message, + story_id=story_id, + story_feed_id=feed_id, + ) + MInteraction.remove_comment_reply( + user_id=comment["user_id"], + reply_user_id=request.user.pk, + reply_content=original_message, + story_id=story_id, + story_feed_id=feed_id, + ) + + reply_user_ids = [reply["user_id"] for reply in comment["replies"]] + for user_id in set(reply_user_ids).difference([comment["user_id"]]): if request.user.pk != user_id: - MInteraction.remove_reply_reply(user_id=user_id, - comment_user_id=comment['user_id'], - reply_user_id=request.user.pk, - reply_content=original_message, - story_id=story_id, - story_feed_id=feed_id) - - if format == 'html': + MInteraction.remove_reply_reply( + user_id=user_id, + comment_user_id=comment["user_id"], + reply_user_id=request.user.pk, + reply_content=original_message, + story_id=story_id, + story_feed_id=feed_id, + ) + + if format == "html": comment = MSharedStory.attach_users_to_comment(comment, profiles) - return render(request, 'social/story_comment.xhtml', { - 'comment': comment, - }) + return render( + request, + "social/story_comment.xhtml", + { + "comment": comment, + }, + ) else: - return json.json_response(request, { - 'code': code, - 'comment': comment, - 'user_profiles': profiles - }) - -@render_to('social/mute_story.xhtml') + return json.json_response(request, {"code": code, "comment": comment, "user_profiles": profiles}) + + +@render_to("social/mute_story.xhtml") def mute_story(request, secret_token, shared_story_id): user_profile = Profile.objects.get(secret_token=secret_token) shared_story = MSharedStory.objects.get(id=shared_story_id) shared_story.mute_for_user(user_profile.user_id) - + return {} - + + def shared_stories_public(request, username): try: user = User.objects.get(username=username) @@ -909,50 +1101,59 @@ def shared_stories_public(request, username): raise Http404 shared_stories = MSharedStory.objects.filter(user_id=user.pk) - + return HttpResponse("There are %s stories shared by %s." % (shared_stories.count(), username)) - + + @json.json_view def profile(request): user = get_user(request.user) - user_id = int(request.GET.get('user_id', user.pk)) - categories = request.GET.getlist('category') or request.GET.getlist('category[]') - include_activities_html = request.GET.get('include_activities_html', None) + user_id = int(request.GET.get("user_id", user.pk)) + categories = request.GET.getlist("category") or request.GET.getlist("category[]") + include_activities_html = request.GET.get("include_activities_html", None) user_profile = MSocialProfile.get_user(user_id) user_profile.count_follows() - + activities = [] if not user_profile.private or user_profile.is_followed_by_user(user.pk): activities, _ = MActivity.user(user_id, page=1, public=True, categories=categories) user_profile = user_profile.canonical(include_follows=True, common_follows_with_user=user.pk) - profile_ids = set(user_profile['followers_youknow'] + user_profile['followers_everybody'] + - user_profile['following_youknow'] + user_profile['following_everybody']) + profile_ids = set( + user_profile["followers_youknow"] + + user_profile["followers_everybody"] + + user_profile["following_youknow"] + + user_profile["following_everybody"] + ) profiles = MSocialProfile.profiles(profile_ids) - logging.user(request, "~BB~FRLoading social profile: %s" % user_profile['username']) - + logging.user(request, "~BB~FRLoading social profile: %s" % user_profile["username"]) + payload = { - 'user_profile': user_profile, - 'followers_youknow': user_profile['followers_youknow'], - 'followers_everybody': user_profile['followers_everybody'], - 'following_youknow': user_profile['following_youknow'], - 'following_everybody': user_profile['following_everybody'], - 'requested_follow': user_profile['requested_follow'], - 'profiles': dict([(p.user_id, p.canonical(compact=True)) for p in profiles]), - 'activities': activities, + "user_profile": user_profile, + "followers_youknow": user_profile["followers_youknow"], + "followers_everybody": user_profile["followers_everybody"], + "following_youknow": user_profile["following_youknow"], + "following_everybody": user_profile["following_everybody"], + "requested_follow": user_profile["requested_follow"], + "profiles": dict([(p.user_id, p.canonical(compact=True)) for p in profiles]), + "activities": activities, } if include_activities_html: - payload['activities_html'] = render_to_string('reader/activities_module.xhtml', { - 'activities': activities, - 'username': user_profile['username'], - 'public': True, - }) - + payload["activities_html"] = render_to_string( + "reader/activities_module.xhtml", + { + "activities": activities, + "username": user_profile["username"], + "public": True, + }, + ) + return payload + @ajax_login_required @json.json_view def load_user_profile(request): @@ -961,43 +1162,44 @@ def load_user_profile(request): social_services = MSocialServices.objects.get(user_id=request.user.pk) except MSocialServices.DoesNotExist: social_services = MSocialServices.objects.create(user_id=request.user.pk) - + logging.user(request, "~BB~FRLoading social profile and blurblog settings") - + return { - 'services': social_services, - 'user_profile': social_profile.canonical(include_follows=True, include_settings=True), + "services": social_services, + "user_profile": social_profile.canonical(include_follows=True, include_settings=True), } - + + @ajax_login_required @json.json_view def save_user_profile(request): data = request.POST - website = data['website'] - - if website and not website.startswith('http'): - website = 'http://' + website - + website = data["website"] + + if website and not website.startswith("http"): + website = "http://" + website + profile = MSocialProfile.get_user(request.user.pk) - profile.location = data['location'] - profile.bio = data['bio'] + profile.location = data["location"] + profile.bio = data["bio"] profile.website = website - profile.protected = is_true(data.get('protected', False)) - profile.private = is_true(data.get('private', False)) + profile.protected = is_true(data.get("protected", False)) + profile.private = is_true(data.get("private", False)) profile.save() social_services = MSocialServices.get_user(user_id=request.user.pk) - profile = social_services.set_photo(data['photo_service']) - + profile = social_services.set_photo(data["photo_service"]) + logging.user(request, "~BB~FRSaving social profile") - + return dict(code=1, user_profile=profile.canonical(include_follows=True)) @ajax_login_required @json.json_view def upload_avatar(request): - photo = request.FILES['photo'] + photo = request.FILES["photo"] profile = MSocialProfile.get_user(request.user.pk) social_services = MSocialServices.objects.get(user_id=request.user.pk) @@ -1005,7 +1207,7 @@ def upload_avatar(request): image_url = social_services.save_uploaded_photo(photo) if image_url: - profile = social_services.set_photo('upload') + profile = social_services.set_photo("upload") return { "code": 1 if image_url else -1, @@ -1014,22 +1216,24 @@ def upload_avatar(request): "user_profile": profile.canonical(include_follows=True), } + @ajax_login_required @json.json_view def save_blurblog_settings(request): data = request.POST profile = MSocialProfile.get_user(request.user.pk) - profile.custom_css = strip_tags(data.get('custom_css', None)) - profile.custom_bgcolor = strip_tags(data.get('custom_bgcolor', None)) - profile.blurblog_title = strip_tags(data.get('blurblog_title', None)) - profile.bb_permalink_direct = is_true(data.get('bb_permalink_direct', False)) + profile.custom_css = strip_tags(data.get("custom_css", None)) + profile.custom_bgcolor = strip_tags(data.get("custom_bgcolor", None)) + profile.blurblog_title = strip_tags(data.get("blurblog_title", None)) + profile.bb_permalink_direct = is_true(data.get("bb_permalink_direct", False)) profile.save() logging.user(request, "~BB~FRSaving blurblog settings") - + return dict(code=1, user_profile=profile.canonical(include_follows=True, include_settings=True)) + @json.json_view def load_follow_requests(request): user = get_user(request.user) @@ -1039,53 +1243,57 @@ def load_follow_requests(request): request_profiles = [p.canonical(include_following_user=user.pk) for p in request_profiles] if len(request_profiles): - logging.user(request, "~BB~FRLoading Follow Requests (%s requests)" % ( - len(request_profiles), - )) + logging.user(request, "~BB~FRLoading Follow Requests (%s requests)" % (len(request_profiles),)) return { - 'request_profiles': request_profiles, + "request_profiles": request_profiles, } + @ratelimit(minutes=1, requests=100) @json.json_view def load_user_friends(request): user = get_user(request.user) - social_profile = MSocialProfile.get_user(user_id=user.pk) - social_services = MSocialServices.get_user(user_id=user.pk) + social_profile = MSocialProfile.get_user(user_id=user.pk) + social_services = MSocialServices.get_user(user_id=user.pk) following_profiles = MSocialProfile.profiles(social_profile.following_user_ids) - follower_profiles = MSocialProfile.profiles(social_profile.follower_user_ids) - recommended_users = social_profile.recommended_users() + follower_profiles = MSocialProfile.profiles(social_profile.follower_user_ids) + recommended_users = social_profile.recommended_users() following_profiles = [p.canonical(include_following_user=user.pk) for p in following_profiles] - follower_profiles = [p.canonical(include_following_user=user.pk) for p in follower_profiles] - - logging.user(request, "~BB~FRLoading Friends (%s following, %s followers)" % ( - social_profile.following_count, - social_profile.follower_count, - )) + follower_profiles = [p.canonical(include_following_user=user.pk) for p in follower_profiles] + + logging.user( + request, + "~BB~FRLoading Friends (%s following, %s followers)" + % ( + social_profile.following_count, + social_profile.follower_count, + ), + ) return { - 'services': social_services, - 'autofollow': social_services.autofollow, - 'user_profile': social_profile.canonical(include_follows=True), - 'following_profiles': following_profiles, - 'follower_profiles': follower_profiles, - 'recommended_users': recommended_users, + "services": social_services, + "autofollow": social_services.autofollow, + "user_profile": social_profile.canonical(include_follows=True), + "following_profiles": following_profiles, + "follower_profiles": follower_profiles, + "recommended_users": recommended_users, } + @ajax_login_required @json.json_view def follow(request): profile = MSocialProfile.get_user(request.user.pk) - user_id = request.POST['user_id'] + user_id = request.POST["user_id"] try: follow_user_id = int(user_id) except ValueError: try: - follow_user_id = int(user_id.replace('social:', '')) + follow_user_id = int(user_id.replace("social:", "")) follow_profile = MSocialProfile.get_user(follow_user_id) except (ValueError, MSocialProfile.DoesNotExist): - follow_username = user_id.replace('social:', '') + follow_username = user_id.replace("social:", "") try: follow_profile = MSocialProfile.objects.get(username=follow_username) except MSocialProfile.DoesNotExist: @@ -1094,54 +1302,55 @@ def follow(request): profile.follow_user(follow_user_id) follow_profile = MSocialProfile.get_user(follow_user_id) - + social_params = { - 'user_id': request.user.pk, - 'subscription_user_id': follow_user_id, - 'include_favicon': True, - 'update_counts': True, + "user_id": request.user.pk, + "subscription_user_id": follow_user_id, + "include_favicon": True, + "update_counts": True, } follow_subscription = MSocialSubscription.feeds(calculate_all_scores=True, **social_params) - + if follow_profile.user: if follow_profile.protected: logging.user(request, "~BB~FR~SBRequested~SN follow from: ~SB%s" % follow_profile.user.username) else: logging.user(request, "~BB~FRFollowing: ~SB%s" % follow_profile.user.username) - + return { - "user_profile": profile.canonical(include_follows=True), + "user_profile": profile.canonical(include_follows=True), "follow_profile": follow_profile.canonical(common_follows_with_user=request.user.pk), "follow_subscription": follow_subscription, } - + + @ajax_login_required @json.json_view def unfollow(request): profile = MSocialProfile.get_user(request.user.pk) - user_id = request.POST['user_id'] + user_id = request.POST["user_id"] try: unfollow_user_id = int(user_id) except ValueError: try: - unfollow_user_id = int(user_id.replace('social:', '')) + unfollow_user_id = int(user_id.replace("social:", "")) unfollow_profile = MSocialProfile.get_user(unfollow_user_id) except (ValueError, MSocialProfile.DoesNotExist): - unfollow_username = user_id.replace('social:', '') + unfollow_username = user_id.replace("social:", "") try: unfollow_profile = MSocialProfile.objects.get(username=unfollow_username) except MSocialProfile.DoesNotExist: raise Http404 unfollow_user_id = unfollow_profile.user_id - + profile.unfollow_user(unfollow_user_id) unfollow_profile = MSocialProfile.get_user(unfollow_user_id) - + logging.user(request, "~BB~FRUnfollowing: ~SB%s" % unfollow_profile.username) - + return { - 'user_profile': profile.canonical(include_follows=True), - 'unfollow_profile': unfollow_profile.canonical(common_follows_with_user=request.user.pk), + "user_profile": profile.canonical(include_follows=True), + "unfollow_profile": unfollow_profile.canonical(common_follows_with_user=request.user.pk), } @@ -1149,80 +1358,84 @@ def unfollow(request): @json.json_view def approve_follower(request): profile = MSocialProfile.get_user(request.user.pk) - user_id = int(request.POST['user_id']) + user_id = int(request.POST["user_id"]) follower_profile = MSocialProfile.get_user(user_id) code = -1 - + logging.user(request, "~BB~FRApproving follow: ~SB%s" % follower_profile.username) - + if user_id in profile.requested_follow_user_ids: follower_profile.follow_user(request.user.pk, force=True) code = 1 - - return {'code': code} + + return {"code": code} + @ajax_login_required @json.json_view def ignore_follower(request): profile = MSocialProfile.get_user(request.user.pk) - user_id = int(request.POST['user_id']) + user_id = int(request.POST["user_id"]) follower_profile = MSocialProfile.get_user(user_id) code = -1 - + logging.user(request, "~BB~FR~SK~SBNOT~SN approving follow: ~SB%s" % follower_profile.username) - + if user_id in profile.requested_follow_user_ids: follower_profile.unfollow_user(request.user.pk) code = 1 - - return {'code': code} + + return {"code": code} + @ajax_login_required -@required_params('user_id', method="POST") +@required_params("user_id", method="POST") @json.json_view def mute_user(request): profile = MSocialProfile.get_user(request.user.pk) - muting_user_id = int(request.POST['user_id']) + muting_user_id = int(request.POST["user_id"]) social_profile = MSocialProfile.get_user(request.user.pk) muting_profile = MSocialProfile.get_user(muting_user_id) code = 1 - + logging.user(request, "~FMMuting user ~SB%s" % muting_profile.username) - + social_profile.mute_user(muting_user_id) - + return { - 'code': code, - 'user_profile': social_profile.canonical(), + "code": code, + "user_profile": social_profile.canonical(), } + @ajax_login_required -@required_params('user_id', method="POST") +@required_params("user_id", method="POST") @json.json_view def unmute_user(request): profile = MSocialProfile.get_user(request.user.pk) - muting_user_id = int(request.POST['user_id']) + muting_user_id = int(request.POST["user_id"]) muting_profile = MSocialProfile.get_user(muting_user_id) code = 1 - + logging.user(request, "~FM~SBUn-~SN~FMMuting user ~SB%s" % muting_profile.username) - + profile.unmute_user(muting_user_id) - + return { - 'code': code, - 'user_profile': profile.canonical(), + "code": code, + "user_profile": profile.canonical(), } -@required_params('query', method="GET") + +@required_params("query", method="GET") @json.json_view def find_friends(request): - query = request.GET['query'] - limit = int(request.GET.get('limit', 3)) + query = request.GET["query"] + limit = int(request.GET.get("limit", 3)) profiles = [] - - if '@' in query: - results = re.search(r'[\w\.-]+@[\w\.-]+', query) + + if "@" in query: + results = re.search(r"[\w\.-]+@[\w\.-]+", query) if results: email = results.group(0) profiles = MSocialProfile.objects.filter(email__iexact=email)[:limit] @@ -1238,110 +1451,141 @@ def find_friends(request): profiles = MSocialProfile.objects.filter(blurblog_title__icontains=query)[:limit] if not profiles: profiles = MSocialProfile.objects.filter(location__icontains=query)[:limit] - + profiles = [p.canonical(include_following_user=request.user.pk) for p in profiles] - profiles = sorted(profiles, key=lambda p: -1 * p['shared_stories_count']) + profiles = sorted(profiles, key=lambda p: -1 * p["shared_stories_count"]) return dict(profiles=profiles) + @ajax_login_required def like_comment(request): - code = 1 - feed_id = int(request.POST['story_feed_id']) - story_id = request.POST['story_id'] - comment_user_id = int(request.POST['comment_user_id']) - format = request.POST.get('format', 'json') - + code = 1 + feed_id = int(request.POST["story_feed_id"]) + story_id = request.POST["story_id"] + comment_user_id = int(request.POST["comment_user_id"]) + format = request.POST.get("format", "json") + if comment_user_id == request.user.pk: - return json.json_response(request, {'code': -1, 'message': 'You cannot favorite your own shared story comment.'}) + return json.json_response( + request, {"code": -1, "message": "You cannot favorite your own shared story comment."} + ) try: - shared_story = MSharedStory.objects.get(user_id=comment_user_id, - story_feed_id=feed_id, - story_guid=story_id) + shared_story = MSharedStory.objects.get( + user_id=comment_user_id, story_feed_id=feed_id, story_guid=story_id + ) except MSharedStory.DoesNotExist: - return json.json_response(request, {'code': -1, 'message': 'The shared comment cannot be found.'}) - + return json.json_response(request, {"code": -1, "message": "The shared comment cannot be found."}) + shared_story.add_liking_user(request.user.pk) comment, profiles = shared_story.comment_with_author_and_profiles() comment_user = User.objects.get(pk=shared_story.user_id) - logging.user(request, "~BB~FMLiking comment by ~SB%s~SN: %s" % ( - comment_user.username, - shared_story.comments[:30], - )) - - MActivity.new_comment_like(liking_user_id=request.user.pk, - comment_user_id=comment['user_id'], - story_id=story_id, - story_feed_id=feed_id, - story_title=shared_story.story_title, - comments=shared_story.comments) - MInteraction.new_comment_like(liking_user_id=request.user.pk, - comment_user_id=comment['user_id'], - story_id=story_id, - story_feed_id=feed_id, - story_title=shared_story.story_title, - comments=shared_story.comments) - - if format == 'html': + logging.user( + request, + "~BB~FMLiking comment by ~SB%s~SN: %s" + % ( + comment_user.username, + shared_story.comments[:30], + ), + ) + + MActivity.new_comment_like( + liking_user_id=request.user.pk, + comment_user_id=comment["user_id"], + story_id=story_id, + story_feed_id=feed_id, + story_title=shared_story.story_title, + comments=shared_story.comments, + ) + MInteraction.new_comment_like( + liking_user_id=request.user.pk, + comment_user_id=comment["user_id"], + story_id=story_id, + story_feed_id=feed_id, + story_title=shared_story.story_title, + comments=shared_story.comments, + ) + + if format == "html": comment = MSharedStory.attach_users_to_comment(comment, profiles) - return render(request, 'social/story_comment.xhtml', { - 'comment': comment, - }) + return render( + request, + "social/story_comment.xhtml", + { + "comment": comment, + }, + ) else: - return json.json_response(request, { - 'code': code, - 'comment': comment, - 'user_profiles': profiles, - }) - + return json.json_response( + request, + { + "code": code, + "comment": comment, + "user_profiles": profiles, + }, + ) + + @ajax_login_required def remove_like_comment(request): - code = 1 - feed_id = int(request.POST['story_feed_id']) - story_id = request.POST['story_id'] - comment_user_id = request.POST['comment_user_id'] - format = request.POST.get('format', 'json') - - shared_story = MSharedStory.objects.get(user_id=comment_user_id, - story_feed_id=feed_id, - story_guid=story_id) + code = 1 + feed_id = int(request.POST["story_feed_id"]) + story_id = request.POST["story_id"] + comment_user_id = request.POST["comment_user_id"] + format = request.POST.get("format", "json") + + shared_story = MSharedStory.objects.get( + user_id=comment_user_id, story_feed_id=feed_id, story_guid=story_id + ) shared_story.remove_liking_user(request.user.pk) comment, profiles = shared_story.comment_with_author_and_profiles() comment_user = User.objects.get(pk=shared_story.user_id) - logging.user(request, "~BB~FMRemoving like on comment by ~SB%s~SN: %s" % ( - comment_user.username, - shared_story.comments[:30], - )) - - if format == 'html': + logging.user( + request, + "~BB~FMRemoving like on comment by ~SB%s~SN: %s" + % ( + comment_user.username, + shared_story.comments[:30], + ), + ) + + if format == "html": comment = MSharedStory.attach_users_to_comment(comment, profiles) - return render(request, 'social/story_comment.xhtml', { - 'comment': comment, - }) + return render( + request, + "social/story_comment.xhtml", + { + "comment": comment, + }, + ) else: - return json.json_response(request, { - 'code': code, - 'comment': comment, - 'user_profiles': profiles, - }) + return json.json_response( + request, + { + "code": code, + "comment": comment, + "user_profiles": profiles, + }, + ) + + def get_subdomain(request): - host = request.META.get('HTTP_HOST') + host = request.META.get("HTTP_HOST") if host.count(".") == 2: return host.split(".")[0] else: return None + def shared_stories_rss_feed_noid(request): - index = HttpResponseRedirect('http://%s%s' % ( - Site.objects.get_current().domain, - reverse('index'))) + index = HttpResponseRedirect("http://%s%s" % (Site.objects.get_current().domain, reverse("index"))) if get_subdomain(request): username = get_subdomain(request) try: - if '.' in username: - username = username.split('.')[0] + if "." in username: + username = username.split(".")[0] user = User.objects.get(username__iexact=username) except User.DoesNotExist: return index @@ -1349,6 +1593,7 @@ def shared_stories_rss_feed_noid(request): return index + @ratelimit(minutes=1, requests=5) def shared_stories_rss_feed(request, user_id, username=None): try: @@ -1357,81 +1602,89 @@ def shared_stories_rss_feed(request, user_id, username=None): raise Http404 limit = 25 - offset = request.GET.get('page', 0) * limit + offset = request.GET.get("page", 0) * limit username = username and username.lower() profile = MSocialProfile.get_user(user.pk) - params = {'username': profile.username_slug, 'user_id': user.pk} + params = {"username": profile.username_slug, "user_id": user.pk} if not username or profile.username_slug.lower() != username: - return HttpResponseRedirect(reverse('shared-stories-rss-feed', kwargs=params)) + return HttpResponseRedirect(reverse("shared-stories-rss-feed", kwargs=params)) social_profile = MSocialProfile.get_user(user_id) current_site = Site.objects.get_current() current_site = current_site and current_site.domain - + if social_profile.private: return HttpResponseForbidden() - + data = {} - data['title'] = social_profile.title - data['link'] = social_profile.blurblog_url - data['description'] = "Stories shared by %s on NewsBlur." % user.username - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur - %s' % settings.NEWSBLUR_URL - data['docs'] = None - data['author_name'] = user.username - data['feed_url'] = "http://%s%s" % ( + data["title"] = social_profile.title + data["link"] = social_profile.blurblog_url + data["description"] = "Stories shared by %s on NewsBlur." % user.username + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur - %s" % settings.NEWSBLUR_URL + data["docs"] = None + data["author_name"] = user.username + data["feed_url"] = "http://%s%s" % ( current_site, - reverse('shared-stories-rss-feed', kwargs=params), + reverse("shared-stories-rss-feed", kwargs=params), ) rss = feedgenerator.Atom1Feed(**data) - shared_stories = MSharedStory.objects.filter(user_id=user.pk).order_by('-shared_date')[offset:offset+limit] + shared_stories = MSharedStory.objects.filter(user_id=user.pk).order_by("-shared_date")[ + offset : offset + limit + ] for shared_story in shared_stories: feed = Feed.get_by_id(shared_story.story_feed_id) - content = render_to_string('social/rss_story.xhtml', { - 'feed': feed, - 'user': user, - 'social_profile': social_profile, - 'shared_story': shared_story, - 'content': shared_story.story_content_str, - }) + content = render_to_string( + "social/rss_story.xhtml", + { + "feed": feed, + "user": user, + "social_profile": social_profile, + "shared_story": shared_story, + "content": shared_story.story_content_str, + }, + ) story_data = { - 'title': shared_story.story_title, - 'link': shared_story.story_permalink, - 'description': content, - 'author_name': shared_story.story_author_name, - 'categories': shared_story.story_tags, - 'unique_id': shared_story.story_permalink, - 'pubdate': shared_story.shared_date, + "title": shared_story.story_title, + "link": shared_story.story_permalink, + "description": content, + "author_name": shared_story.story_author_name, + "categories": shared_story.story_tags, + "unique_id": shared_story.story_permalink, + "pubdate": shared_story.shared_date, } rss.add_item(**story_data) - - logging.user(request, "~FBGenerating ~SB%s~SN's RSS feed: ~FM%s" % ( - user.username, - request.META.get('HTTP_USER_AGENT', "")[:24] - )) - return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml') - -@required_params('user_id', method="GET") + + logging.user( + request, + "~FBGenerating ~SB%s~SN's RSS feed: ~FM%s" + % (user.username, request.META.get("HTTP_USER_AGENT", "")[:24]), + ) + return HttpResponse(rss.writeString("utf-8"), content_type="application/rss+xml") + + +@required_params("user_id", method="GET") @json.json_view def social_feed_trainer(request): - social_user_id = request.GET['user_id'] + social_user_id = request.GET["user_id"] social_profile = MSocialProfile.get_user(social_user_id) social_user = get_object_or_404(User, pk=social_user_id) user = get_user(request) - + social_profile.count_stories() classifier = social_profile.canonical() - classifier['classifiers'] = get_classifiers_for_user(user, social_user_id=classifier['id']) - classifier['num_subscribers'] = social_profile.follower_count - classifier['feed_tags'] = [] - classifier['feed_authors'] = [] - - logging.user(user, "~FGLoading social trainer on ~SB%s: %s" % ( - social_user.username, social_profile.title)) - + classifier["classifiers"] = get_classifiers_for_user(user, social_user_id=classifier["id"]) + classifier["num_subscribers"] = social_profile.follower_count + classifier["feed_tags"] = [] + classifier["feed_authors"] = [] + + logging.user( + user, "~FGLoading social trainer on ~SB%s: %s" % (social_user.username, social_profile.title) + ) + return [classifier] - + @json.json_view def load_social_statistics(request, social_user_id, username=None): @@ -1439,96 +1692,101 @@ def load_social_statistics(request, social_user_id, username=None): social_profile = MSocialProfile.get_user(social_user_id) social_profile.save_feed_story_history_statistics() social_profile.save_classifier_counts() - + # Stories per month - average and month-by-month breakout - stats['average_stories_per_month'] = social_profile.average_stories_per_month - stats['story_count_history'] = social_profile.story_count_history - stats['story_hours_history'] = social_profile.story_hours_history - stats['story_days_history'] = social_profile.story_days_history - + stats["average_stories_per_month"] = social_profile.average_stories_per_month + stats["story_count_history"] = social_profile.story_count_history + stats["story_hours_history"] = social_profile.story_hours_history + stats["story_days_history"] = social_profile.story_days_history + # Subscribers - stats['subscriber_count'] = social_profile.follower_count - stats['num_subscribers'] = social_profile.follower_count - + stats["subscriber_count"] = social_profile.follower_count + stats["num_subscribers"] = social_profile.follower_count + # Classifier counts - stats['classifier_counts'] = social_profile.feed_classifier_counts - + stats["classifier_counts"] = social_profile.feed_classifier_counts + # Feeds - feed_ids = [c['feed_id'] for c in stats['classifier_counts'].get('feed', [])] - feeds = Feed.objects.filter(pk__in=feed_ids).only('feed_title') + feed_ids = [c["feed_id"] for c in stats["classifier_counts"].get("feed", [])] + feeds = Feed.objects.filter(pk__in=feed_ids).only("feed_title") titles = dict([(f.pk, f.feed_title) for f in feeds]) - for stat in stats['classifier_counts'].get('feed', []): - stat['feed_title'] = titles.get(stat['feed_id'], "") - - logging.user(request, "~FBStatistics social: ~SB%s ~FG(%s subs)" % ( - social_profile.user_id, social_profile.follower_count)) + for stat in stats["classifier_counts"].get("feed", []): + stat["feed_title"] = titles.get(stat["feed_id"], "") + + logging.user( + request, + "~FBStatistics social: ~SB%s ~FG(%s subs)" % (social_profile.user_id, social_profile.follower_count), + ) return stats + @json.json_view def load_social_settings(request, social_user_id, username=None): social_profile = MSocialProfile.get_user(social_user_id) - + return social_profile.canonical() + @ajax_login_required def load_interactions(request): - user_id = request.GET.get('user_id', None) - categories = request.GET.getlist('category') or request.GET.getlist('category[]') - if not user_id or 'null' in user_id: + user_id = request.GET.get("user_id", None) + categories = request.GET.getlist("category") or request.GET.getlist("category[]") + if not user_id or "null" in user_id: user_id = get_user(request).pk - page = max(1, int(request.GET.get('page', 1))) - limit = request.GET.get('limit') - interactions, has_next_page = MInteraction.user(user_id, page=page, limit=limit, - categories=categories) - format = request.GET.get('format', None) - - data = { - 'interactions': interactions, - 'page': page, - 'has_next_page': has_next_page - } - + page = max(1, int(request.GET.get("page", 1))) + limit = request.GET.get("limit") + interactions, has_next_page = MInteraction.user(user_id, page=page, limit=limit, categories=categories) + format = request.GET.get("format", None) + + data = {"interactions": interactions, "page": page, "has_next_page": has_next_page} + logging.user(request, "~FBLoading interactions ~SBp/%s" % page) - - if format == 'html': - return render(request, 'reader/interactions_module.xhtml', data) + + if format == "html": + return render(request, "reader/interactions_module.xhtml", data) else: return json.json_response(request, data) + @ajax_login_required def load_activities(request): - user_id = request.GET.get('user_id', None) - categories = request.GET.getlist('category') or request.GET.getlist('category[]') - if user_id and 'null' not in user_id: + user_id = request.GET.get("user_id", None) + categories = request.GET.getlist("category") or request.GET.getlist("category[]") + if user_id and "null" not in user_id: user_id = int(user_id) user = User.objects.get(pk=user_id) else: user = get_user(request) user_id = user.pk - + public = user_id != request.user.pk - page = max(1, int(request.GET.get('page', 1))) - limit = request.GET.get('limit', 4) - activities, has_next_page = MActivity.user(user_id, page=page, limit=limit, public=public, - categories=categories) - format = request.GET.get('format', None) - + page = max(1, int(request.GET.get("page", 1))) + limit = request.GET.get("limit", 4) + activities, has_next_page = MActivity.user( + user_id, page=page, limit=limit, public=public, categories=categories + ) + format = request.GET.get("format", None) + data = { - 'activities': activities, - 'page': page, - 'has_next_page': has_next_page, - 'username': (user.username if public else 'You'), + "activities": activities, + "page": page, + "has_next_page": has_next_page, + "username": (user.username if public else "You"), } - + logging.user(request, "~FBLoading activities ~SBp/%s" % page) - - if format == 'html': - return render(request, 'reader/activities_module.xhtml', data, - ) + + if format == "html": + return render( + request, + "reader/activities_module.xhtml", + data, + ) else: return json.json_response(request, data) + @json.json_view def comment(request, comment_id): try: @@ -1537,13 +1795,14 @@ def comment(request, comment_id): raise Http404 return shared_story.comments_with_author() + @json.json_view def comment_reply(request, comment_id, reply_id): try: shared_story = MSharedStory.objects.get(id=comment_id) except MSharedStory.DoesNotExist: raise Http404 - + for story_reply in shared_story.replies: if story_reply.reply_id == ObjectId(reply_id): return story_reply diff --git a/apps/static/tests.py b/apps/static/tests.py index 2247054b35..3748f41ba4 100644 --- a/apps/static/tests.py +++ b/apps/static/tests.py @@ -7,6 +7,7 @@ from django.test import TestCase + class SimpleTest(TestCase): def test_basic_addition(self): """ @@ -14,10 +15,12 @@ def test_basic_addition(self): """ self.failUnlessEqual(1 + 1, 2) -__test__ = {"doctest": """ + +__test__ = { + "doctest": """ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True -"""} - +""" +} diff --git a/apps/static/views.py b/apps/static/views.py index c5c98f05cc..9b2f04df42 100644 --- a/apps/static/views.py +++ b/apps/static/views.py @@ -1,109 +1,132 @@ import os -import yaml + import redis +import yaml from django.conf import settings from django.http import HttpResponse from django.shortcuts import render + from apps.rss_feeds.models import Feed, MStory from apps.search.models import SearchFeed from utils import log as logging + def about(request): - return render(request, 'static/about.xhtml') - + return render(request, "static/about.xhtml") + + def faq(request): - return render(request, 'static/faq.xhtml') - + return render(request, "static/faq.xhtml") + + def api(request): - filename = settings.TEMPLATES[0]['DIRS'][0] + '/static/api.yml' + filename = settings.TEMPLATES[0]["DIRS"][0] + "/static/api.yml" api_yml_file = open(filename).read() - data = yaml.load(api_yml_file) + data = yaml.load(api_yml_file) + + return render(request, "static/api.xhtml", {"data": data}) + - return render(request, 'static/api.xhtml', {'data': data}) - def press(request): - return render(request, 'static/press.xhtml') + return render(request, "static/press.xhtml") + def privacy(request): - return render(request, 'static/privacy.xhtml') + return render(request, "static/privacy.xhtml") + def tos(request): - return render(request, 'static/tos.xhtml') + return render(request, "static/tos.xhtml") + def webmanifest(request): - filename = settings.MEDIA_ROOT + '/extensions/edge/manifest.json' + filename = settings.MEDIA_ROOT + "/extensions/edge/manifest.json" manifest = open(filename).read() - - return HttpResponse(manifest, content_type='application/manifest+json') + + return HttpResponse(manifest, content_type="application/manifest+json") + def apple_app_site_assoc(request): - return render(request, 'static/apple_app_site_assoc.xhtml') - + return render(request, "static/apple_app_site_assoc.xhtml") + + def apple_developer_merchantid(request): - return render(request, 'static/apple_developer_merchantid.xhtml') + return render(request, "static/apple_developer_merchantid.xhtml") + def feedback(request): - return render(request, 'static/feedback.xhtml') + return render(request, "static/feedback.xhtml") + def firefox(request): - filename = settings.MEDIA_ROOT + '/extensions/firefox/manifest.json' + filename = settings.MEDIA_ROOT + "/extensions/firefox/manifest.json" manifest = open(filename).read() - - return HttpResponse(manifest, content_type='application/x-web-app-manifest+json') + + return HttpResponse(manifest, content_type="application/x-web-app-manifest+json") + def ios(request): - return render(request, 'static/ios.xhtml') - + return render(request, "static/ios.xhtml") + + def android(request): - return render(request, 'static/android.xhtml') - + return render(request, "static/android.xhtml") + + def ios_download(request): - return render(request, 'static/ios_download.xhtml') - + return render(request, "static/ios_download.xhtml") + + def ios_plist(request): - filename = os.path.join(settings.NEWSBLUR_DIR, 'clients/ios/NewsBlur.plist') + filename = os.path.join(settings.NEWSBLUR_DIR, "clients/ios/NewsBlur.plist") manifest = open(filename).read() - + logging.user(request, "~SK~FR~BBDownloading NewsBlur.plist...") - return HttpResponse(manifest, content_type='text/xml') - + return HttpResponse(manifest, content_type="text/xml") + + def ios_ipa(request): - filename = os.path.join(settings.NEWSBLUR_DIR, 'clients/ios/NewsBlur.ipa') + filename = os.path.join(settings.NEWSBLUR_DIR, "clients/ios/NewsBlur.ipa") manifest = open(filename).read() - + logging.user(request, "~SK~FR~BBDownloading NewsBlur.ipa...") - return HttpResponse(manifest, content_type='application/octet-stream') + return HttpResponse(manifest, content_type="application/octet-stream") + def haproxy_check(request): return HttpResponse("OK") + def postgres_check(request): - feed = Feed.objects.latest('pk').pk + feed = Feed.objects.latest("pk").pk if feed: return HttpResponse(unicode(feed)) assert False, "Cannot read from postgres database" + def mongo_check(request): stories = MStory.objects.count() if stories: return HttpResponse(unicode(stories)) assert False, "Cannot read from mongo database" + def elasticsearch_check(request): client = SearchFeed.ES() if client.indices.exists_index(SearchFeed.index_name()): return HttpResponse(SearchFeed.index_name()) assert False, "Cannot read from elasticsearch database" + def redis_check(request): - pool = request.GET['pool'] - if pool == 'main': + pool = request.GET["pool"] + if pool == "main": r = redis.Redis(connection_pool=settings.REDIS_POOL) - elif pool == 'story': + elif pool == "story": r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL) - elif pool == 'sessions': + elif pool == "sessions": r = redis.Redis(connection_pool=settings.REDIS_SESSION_POOL) - + key = r.randomkey() if key: return HttpResponse(unicode(key)) diff --git a/apps/statistics/management/commands/collect_feedback.py b/apps/statistics/management/commands/collect_feedback.py index 38f0e93785..e8d9d84493 100644 --- a/apps/statistics/management/commands/collect_feedback.py +++ b/apps/statistics/management/commands/collect_feedback.py @@ -1,7 +1,8 @@ from django.core.management.base import BaseCommand + from apps.statistics.models import MFeedback -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): - MFeedback.collect_feedback() \ No newline at end of file + MFeedback.collect_feedback() diff --git a/apps/statistics/management/commands/collect_stats.py b/apps/statistics/management/commands/collect_stats.py index eea39564e6..28e6e772bf 100644 --- a/apps/statistics/management/commands/collect_stats.py +++ b/apps/statistics/management/commands/collect_stats.py @@ -1,8 +1,8 @@ from django.core.management.base import BaseCommand + from apps.statistics.models import MStatistics -class Command(BaseCommand): +class Command(BaseCommand): def handle(self, *args, **options): MStatistics.collect_statistics() - \ No newline at end of file diff --git a/apps/statistics/models.py b/apps/statistics/models.py index 9d4e5fa5e6..05c7d41058 100644 --- a/apps/statistics/models.py +++ b/apps/statistics/models.py @@ -1,32 +1,37 @@ import datetime +import urllib.error +import urllib.parse +import urllib.request + +import dateutil import mongoengine as mongo -import urllib.request, urllib.error, urllib.parse import redis -import dateutil import requests from django.conf import settings -from apps.social.models import MSharedStory + from apps.profile.models import Profile +from apps.social.models import MSharedStory from apps.statistics.rstats import RStats, round_time -from utils.story_functions import relative_date -from utils import json_functions as json from utils import db_functions +from utils import json_functions as json from utils import log as logging +from utils.story_functions import relative_date + class MStatistics(mongo.Document): - key = mongo.StringField(unique=True) + key = mongo.StringField(unique=True) value = mongo.DynamicField() expiration_date = mongo.DateTimeField() - + meta = { - 'collection': 'statistics', - 'allow_inheritance': False, - 'indexes': ['key'], + "collection": "statistics", + "allow_inheritance": False, + "indexes": ["key"], } - + def __str__(self): return "%s: %s" % (self.key, self.value) - + @classmethod def get(cls, key, default=None, set_default=False, expiration_sec=None): obj = cls.objects.filter(key=key).first() @@ -53,25 +58,31 @@ def set(cls, key, value, expiration_sec=None): if expiration_sec: obj.expiration_date = datetime.datetime.now() + datetime.timedelta(seconds=expiration_sec) obj.save() - + @classmethod def all(cls): stats = cls.objects.all() values = dict([(stat.key, stat.value) for stat in stats]) for key, value in list(values.items()): - if key in ('avg_time_taken', 'sites_loaded', 'stories_shared'): + if key in ("avg_time_taken", "sites_loaded", "stories_shared"): values[key] = json.decode(value) - elif key in ('feeds_fetched', 'premium_users', 'standard_users', 'latest_sites_loaded', - 'max_sites_loaded', 'max_stories_shared'): + elif key in ( + "feeds_fetched", + "premium_users", + "standard_users", + "latest_sites_loaded", + "max_sites_loaded", + "max_stories_shared", + ): values[key] = int(value) - elif key in ('latest_avg_time_taken', 'max_avg_time_taken', 'last_1_min_time_taken'): + elif key in ("latest_avg_time_taken", "max_avg_time_taken", "last_1_min_time_taken"): values[key] = float(value) - - values['total_sites_loaded'] = sum(values['sites_loaded']) if 'sites_loaded' in values else 0 - values['total_stories_shared'] = sum(values['stories_shared']) if 'stories_shared' in values else 0 + + values["total_sites_loaded"] = sum(values["sites_loaded"]) if "sites_loaded" in values else 0 + values["total_stories_shared"] = sum(values["stories_shared"]) if "stories_shared" in values else 0 return values - + @classmethod def collect_statistics(cls): now = datetime.datetime.now() @@ -93,34 +104,38 @@ def collect_statistics(cls): cls.collect_statistics_feeds_fetched() # if settings.DEBUG: # print("Feeds Fetched: %s" % (datetime.datetime.now() - now)) - + @classmethod def collect_statistics_feeds_fetched(cls): - feeds_fetched = RStats.count('feed_fetch', hours=24) - cls.objects(key='feeds_fetched').update_one(upsert=True, - set__key='feeds_fetched', - set__value=feeds_fetched) - + feeds_fetched = RStats.count("feed_fetch", hours=24) + cls.objects(key="feeds_fetched").update_one( + upsert=True, set__key="feeds_fetched", set__value=feeds_fetched + ) + return feeds_fetched - + @classmethod def collect_statistics_premium_users(cls): last_day = datetime.datetime.now() - datetime.timedelta(hours=24) - + premium_users = Profile.objects.filter(last_seen_on__gte=last_day, is_premium=True).count() - cls.objects(key='premium_users').update_one(upsert=True, set__key='premium_users', set__value=premium_users) - + cls.objects(key="premium_users").update_one( + upsert=True, set__key="premium_users", set__value=premium_users + ) + return premium_users - + @classmethod def collect_statistics_standard_users(cls): last_day = datetime.datetime.now() - datetime.timedelta(hours=24) - + standard_users = Profile.objects.filter(last_seen_on__gte=last_day, is_premium=False).count() - cls.objects(key='standard_users').update_one(upsert=True, set__key='standard_users', set__value=standard_users) - + cls.objects(key="standard_users").update_one( + upsert=True, set__key="standard_users", set__value=standard_users + ) + return standard_users - + @classmethod def collect_statistics_sites_loaded(cls): now = round_time(datetime.datetime.now(), round_to=60) @@ -130,23 +145,23 @@ def collect_statistics_sites_loaded(cls): r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL) for hours_ago in range(24): - start_hours_ago = now - datetime.timedelta(hours=hours_ago+1) - + start_hours_ago = now - datetime.timedelta(hours=hours_ago + 1) + pipe = r.pipeline() for m in range(60): minute = start_hours_ago + datetime.timedelta(minutes=m) - key = "%s:%s" % (RStats.stats_type('page_load'), minute.strftime('%s')) + key = "%s:%s" % (RStats.stats_type("page_load"), minute.strftime("%s")) pipe.get("%s:s" % key) pipe.get("%s:a" % key) - + times = pipe.execute() - + counts = [int(c) for c in times[::2] if c] avgs = [float(a) for a in times[1::2] if a] - + if hours_ago == 0: last_1_min_time_taken = round(sum(avgs[:1]) / max(1, sum(counts[:1])), 2) - + if counts and avgs: count = max(1, sum(counts)) avg = round(sum(avgs) / count, 3) @@ -161,81 +176,81 @@ def collect_statistics_sites_loaded(cls): avg_time_taken.reverse() values = ( - ('sites_loaded', json.encode(sites_loaded)), - ('avg_time_taken', json.encode(avg_time_taken)), - ('latest_sites_loaded', sites_loaded[-1]), - ('latest_avg_time_taken', avg_time_taken[-1]), - ('max_sites_loaded', max(sites_loaded)), - ('max_avg_time_taken', max(1, max(avg_time_taken))), - ('last_1_min_time_taken', last_1_min_time_taken), + ("sites_loaded", json.encode(sites_loaded)), + ("avg_time_taken", json.encode(avg_time_taken)), + ("latest_sites_loaded", sites_loaded[-1]), + ("latest_avg_time_taken", avg_time_taken[-1]), + ("max_sites_loaded", max(sites_loaded)), + ("max_avg_time_taken", max(1, max(avg_time_taken))), + ("last_1_min_time_taken", last_1_min_time_taken), ) for key, value in values: cls.objects(key=key).update_one(upsert=True, set__key=key, set__value=value) - + @classmethod def collect_statistics_stories_shared(cls): now = datetime.datetime.now() stories_shared = [] - + for hour in range(24): start_hours_ago = now - datetime.timedelta(hours=hour) - end_hours_ago = now - datetime.timedelta(hours=hour+1) + end_hours_ago = now - datetime.timedelta(hours=hour + 1) shares = MSharedStory.objects.filter( - shared_date__lte=start_hours_ago, - shared_date__gte=end_hours_ago + shared_date__lte=start_hours_ago, shared_date__gte=end_hours_ago ).count() stories_shared.append(shares) stories_shared.reverse() - + values = ( - ('stories_shared', json.encode(stories_shared)), - ('latest_stories_shared', stories_shared[-1]), - ('max_stories_shared', max(stories_shared)), + ("stories_shared", json.encode(stories_shared)), + ("latest_stories_shared", stories_shared[-1]), + ("max_stories_shared", max(stories_shared)), ) for key, value in values: cls.objects(key=key).update_one(upsert=True, set__key=key, set__value=value) - + @classmethod def collect_statistics_for_db(cls, debug=False): lag = db_functions.mongo_max_replication_lag(settings.MONGODB) - cls.set('mongodb_replication_lag', lag) - + cls.set("mongodb_replication_lag", lag) + now = round_time(datetime.datetime.now(), round_to=60) r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL) db_times = {} latest_db_times = {} - for db in ['sql', - 'mongo', - 'redis', - 'redis_user', - 'redis_story', - 'redis_session', - 'redis_pubsub', - 'task_sql', - 'task_mongo', - 'task_redis', - 'task_redis_user', - 'task_redis_story', - 'task_redis_session', - 'task_redis_pubsub', - ]: + for db in [ + "sql", + "mongo", + "redis", + "redis_user", + "redis_story", + "redis_session", + "redis_pubsub", + "task_sql", + "task_mongo", + "task_redis", + "task_redis_user", + "task_redis_story", + "task_redis_session", + "task_redis_pubsub", + ]: db_times[db] = [] for hour in range(24): - start_hours_ago = now - datetime.timedelta(hours=hour+1) + start_hours_ago = now - datetime.timedelta(hours=hour + 1) pipe = r.pipeline() for m in range(60): minute = start_hours_ago + datetime.timedelta(minutes=m) - key = "DB:%s:%s" % (db, minute.strftime('%s')) + key = "DB:%s:%s" % (db, minute.strftime("%s")) if debug: print(" -> %s:c" % key) pipe.get("%s:c" % key) pipe.get("%s:t" % key) - + times = pipe.execute() - + counts = [int(c or 0) for c in times[::2]] avgs = [float(a or 0) for a in times[1::2]] if counts and avgs: @@ -244,7 +259,7 @@ def collect_statistics_for_db(cls, debug=False): else: count = 0 avg = 0 - + if hour == 0: latest_count = float(counts[-1]) if len(counts) else 0 latest_avg = float(avgs[-1]) if len(avgs) else 0 @@ -254,85 +269,91 @@ def collect_statistics_for_db(cls, debug=False): db_times[db].reverse() values = ( - ('avg_sql_times', json.encode(db_times['sql'])), - ('avg_mongo_times', json.encode(db_times['mongo'])), - ('avg_redis_times', json.encode(db_times['redis'])), - ('latest_sql_avg', latest_db_times['sql']), - ('latest_mongo_avg', latest_db_times['mongo']), - ('latest_redis_user_avg', latest_db_times['redis_user']), - ('latest_redis_story_avg', latest_db_times['redis_story']), - ('latest_redis_session_avg',latest_db_times['redis_session']), - ('latest_redis_pubsub_avg', latest_db_times['redis_pubsub']), - ('latest_task_sql_avg', latest_db_times['task_sql']), - ('latest_task_mongo_avg', latest_db_times['task_mongo']), - ('latest_task_redis_user_avg', latest_db_times['task_redis_user']), - ('latest_task_redis_story_avg', latest_db_times['task_redis_story']), - ('latest_task_redis_session_avg',latest_db_times['task_redis_session']), - ('latest_task_redis_pubsub_avg', latest_db_times['task_redis_pubsub']), + ("avg_sql_times", json.encode(db_times["sql"])), + ("avg_mongo_times", json.encode(db_times["mongo"])), + ("avg_redis_times", json.encode(db_times["redis"])), + ("latest_sql_avg", latest_db_times["sql"]), + ("latest_mongo_avg", latest_db_times["mongo"]), + ("latest_redis_user_avg", latest_db_times["redis_user"]), + ("latest_redis_story_avg", latest_db_times["redis_story"]), + ("latest_redis_session_avg", latest_db_times["redis_session"]), + ("latest_redis_pubsub_avg", latest_db_times["redis_pubsub"]), + ("latest_task_sql_avg", latest_db_times["task_sql"]), + ("latest_task_mongo_avg", latest_db_times["task_mongo"]), + ("latest_task_redis_user_avg", latest_db_times["task_redis_user"]), + ("latest_task_redis_story_avg", latest_db_times["task_redis_story"]), + ("latest_task_redis_session_avg", latest_db_times["task_redis_session"]), + ("latest_task_redis_pubsub_avg", latest_db_times["task_redis_pubsub"]), ) for key, value in values: cls.objects(key=key).update_one(upsert=True, set__key=key, set__value=value) class MFeedback(mongo.Document): - date = mongo.DateTimeField() + date = mongo.DateTimeField() date_short = mongo.StringField() subject = mongo.StringField() - url = mongo.StringField() - style = mongo.StringField() - order = mongo.IntField() - + url = mongo.StringField() + style = mongo.StringField() + order = mongo.IntField() + meta = { - 'collection': 'feedback', - 'allow_inheritance': False, - 'indexes': ['style'], - 'ordering': ['order'], + "collection": "feedback", + "allow_inheritance": False, + "indexes": ["style"], + "ordering": ["order"], } - + CATEGORIES = { - 5: 'idea', - 6: 'problem', - 7: 'praise', - 8: 'question', - 9: 'admin', - 10: 'updates', + 5: "idea", + 6: "problem", + 7: "praise", + 8: "question", + 9: "admin", + 10: "updates", } - + def __str__(self): return "%s: (%s) %s" % (self.style, self.date, self.subject) - + @classmethod def collect_feedback(cls): seen_posts = set() try: - data = requests.get('https://forum.newsblur.com/posts.json', timeout=3).content + data = requests.get("https://forum.newsblur.com/posts.json", timeout=3).content except (urllib.error.HTTPError, requests.exceptions.ConnectTimeout) as e: logging.debug(" ***> Failed to collect feedback: %s" % e) return - data = json.decode(data).get('latest_posts', "") + data = json.decode(data).get("latest_posts", "") if not len(data): print("No data!") return - + cls.objects.delete() post_count = 0 for post in data: - if post['topic_id'] in seen_posts: continue - seen_posts.add(post['topic_id']) + if post["topic_id"] in seen_posts: + continue + seen_posts.add(post["topic_id"]) feedback = {} - feedback['order'] = post_count + feedback["order"] = post_count post_count += 1 - feedback['date'] = dateutil.parser.parse(post['created_at']).replace(tzinfo=None) - feedback['date_short'] = relative_date(feedback['date']) - feedback['subject'] = post['topic_title'] - feedback['url'] = "https://forum.newsblur.com/t/%s/%s/%s" % (post['topic_slug'], post['topic_id'], post['post_number']) - feedback['style'] = cls.CATEGORIES[post['category_id']] + feedback["date"] = dateutil.parser.parse(post["created_at"]).replace(tzinfo=None) + feedback["date_short"] = relative_date(feedback["date"]) + feedback["subject"] = post["topic_title"] + feedback["url"] = "https://forum.newsblur.com/t/%s/%s/%s" % ( + post["topic_slug"], + post["topic_id"], + post["post_number"], + ) + feedback["style"] = cls.CATEGORIES[post["category_id"]] cls.objects.create(**feedback) # if settings.DEBUG: # print("%s: %s (%s)" % (feedback['style'], feedback['subject'], feedback['date_short'])) - if post_count >= 4: break - + if post_count >= 4: + break + @classmethod def all(cls): feedbacks = cls.objects.all()[:4] @@ -350,28 +371,31 @@ class MAnalyticsFetcher(mongo.Document): total = mongo.FloatField() server = mongo.StringField() feed_code = mongo.IntField() - + meta = { - 'db_alias': 'nbanalytics', - 'collection': 'feed_fetches', - 'allow_inheritance': False, - 'indexes': ['date', 'feed_id', 'server', 'feed_code'], - 'ordering': ['date'], + "db_alias": "nbanalytics", + "collection": "feed_fetches", + "allow_inheritance": False, + "indexes": ["date", "feed_id", "server", "feed_code"], + "ordering": ["date"], } - + def __str__(self): - return "%s: %.4s+%.4s+%.4s+%.4s = %.4ss" % (self.feed_id, self.feed_fetch, - self.feed_process, - self.page, - self.icon, - self.total) - + return "%s: %.4s+%.4s+%.4s+%.4s = %.4ss" % ( + self.feed_id, + self.feed_fetch, + self.feed_process, + self.page, + self.icon, + self.total, + ) + @classmethod - def add(cls, feed_id, feed_fetch, feed_process, - page, icon, total, feed_code): + def add(cls, feed_id, feed_fetch, feed_process, page, icon, total, feed_code): server_name = settings.SERVER_NAME - if 'app' in server_name: return - + if "app" in server_name: + return + if icon and page: icon -= page if page and feed_process: @@ -380,12 +404,18 @@ def add(cls, feed_id, feed_fetch, feed_process, page -= feed_fetch if feed_process and feed_fetch: feed_process -= feed_fetch - - cls.objects.create(feed_id=feed_id, feed_fetch=feed_fetch, - feed_process=feed_process, - page=page, icon=icon, total=total, - server=server_name, feed_code=feed_code) - + + cls.objects.create( + feed_id=feed_id, + feed_fetch=feed_fetch, + feed_process=feed_process, + page=page, + icon=icon, + total=total, + server=server_name, + feed_code=feed_code, + ) + @classmethod def calculate_stats(cls, stats): return cls.aggregate(**stats) @@ -395,24 +425,24 @@ class MAnalyticsLoader(mongo.Document): date = mongo.DateTimeField(default=datetime.datetime.now) page_load = mongo.FloatField() server = mongo.StringField() - + meta = { - 'db_alias': 'nbanalytics', - 'collection': 'page_loads', - 'allow_inheritance': False, - 'indexes': ['date', 'server'], - 'ordering': ['date'], + "db_alias": "nbanalytics", + "collection": "page_loads", + "allow_inheritance": False, + "indexes": ["date", "server"], + "ordering": ["date"], } - + def __str__(self): return "%s: %.4ss" % (self.server, self.page_load) - + @classmethod def add(cls, page_load): server_name = settings.SERVER_NAME cls.objects.create(page_load=page_load, server=server_name) - + @classmethod def calculate_stats(cls, stats): return cls.aggregate(**stats) diff --git a/apps/statistics/rstats.py b/apps/statistics/rstats.py index e25a61f167..cb1b165683 100644 --- a/apps/statistics/rstats.py +++ b/apps/statistics/rstats.py @@ -1,92 +1,92 @@ -import redis import datetime import re from collections import defaultdict + +import redis from django.conf import settings class RStats: - STATS_TYPE = { - 'page_load': 'PLT', - 'feed_fetch': 'FFH', + "page_load": "PLT", + "feed_fetch": "FFH", } - + @classmethod def stats_type(cls, name): return cls.STATS_TYPE[name] - + @classmethod def add(cls, name, duration=None): r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL) pipe = r.pipeline() minute = round_time(round_to=60) - key = "%s:%s" % (cls.stats_type(name), minute.strftime('%s')) + key = "%s:%s" % (cls.stats_type(name), minute.strftime("%s")) pipe.incr("%s:s" % key) if duration: pipe.incrbyfloat("%s:a" % key, duration) pipe.expireat("%s:a" % key, (minute + datetime.timedelta(days=2)).strftime("%s")) pipe.expireat("%s:s" % key, (minute + datetime.timedelta(days=2)).strftime("%s")) pipe.execute() - + @classmethod def clean_path(cls, path): if not path: return - - if path.startswith('/reader/feed/'): - path = '/reader/feed/' - elif path.startswith('/social/stories'): - path = '/social/stories/' - elif path.startswith('/reader/river_stories'): - path = '/reader/river_stories/' - elif path.startswith('/social/river_stories'): - path = '/social/river_stories/' - elif path.startswith('/reader/page/'): - path = '/reader/page/' - elif path.startswith('/api/check_share_on_site'): - path = '/api/check_share_on_site/' - + + if path.startswith("/reader/feed/"): + path = "/reader/feed/" + elif path.startswith("/social/stories"): + path = "/social/stories/" + elif path.startswith("/reader/river_stories"): + path = "/reader/river_stories/" + elif path.startswith("/social/river_stories"): + path = "/social/river_stories/" + elif path.startswith("/reader/page/"): + path = "/reader/page/" + elif path.startswith("/api/check_share_on_site"): + path = "/api/check_share_on_site/" + return path - + @classmethod def count(cls, name, hours=24): r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL) stats_type = cls.stats_type(name) now = datetime.datetime.now() pipe = r.pipeline() - for minutes_ago in range(60*hours): + for minutes_ago in range(60 * hours): dt_min_ago = now - datetime.timedelta(minutes=minutes_ago) minute = round_time(dt=dt_min_ago, round_to=60) - key = "%s:%s" % (stats_type, minute.strftime('%s')) + key = "%s:%s" % (stats_type, minute.strftime("%s")) pipe.get("%s:s" % key) values = pipe.execute() total = sum(int(v) for v in values if v) return total - + @classmethod def sample(cls, sample=1000, pool=None): if not pool: pool = settings.REDIS_STORY_HASH_POOL - r = redis.Redis(connection_pool=pool) - keys = set() - errors = set() - prefixes = defaultdict(set) - sizes = defaultdict(int) + r = redis.Redis(connection_pool=pool) + keys = set() + errors = set() + prefixes = defaultdict(set) + sizes = defaultdict(int) prefixes_ttls = defaultdict(lambda: defaultdict(int)) - prefix_re = re.compile(r"(\w+):(.*)") + prefix_re = re.compile(r"(\w+):(.*)") - p = r.pipeline() + p = r.pipeline() [p.randomkey() for _ in range(sample)] - keys = set(p.execute()) + keys = set(p.execute()) - p = r.pipeline() + p = r.pipeline() [p.ttl(key) for key in keys] - ttls = p.execute() + ttls = p.execute() + + dump = [r.execute_command("dump", key) for key in keys] - dump = [r.execute_command('dump', key) for key in keys] - for k, key in enumerate(keys): match = prefix_re.match(key) if not match or dump[k] is None: @@ -96,39 +96,49 @@ def sample(cls, sample=1000, pool=None): prefixes[prefix].add(rest) sizes[prefix] += len(dump[k]) ttl = ttls[k] - if ttl < 0: # Never expire - prefixes_ttls[prefix]['-'] += 1 + if ttl < 0: # Never expire + prefixes_ttls[prefix]["-"] += 1 elif ttl == 0: - prefixes_ttls[prefix]['X'] += 1 - elif ttl < 60*60: # 1 hour - prefixes_ttls[prefix]['1h'] += 1 - elif ttl < 60*60*24: - prefixes_ttls[prefix]['1d'] += 1 - elif ttl < 60*60*24*7: - prefixes_ttls[prefix]['1w'] += 1 - elif ttl < 60*60*24*14: - prefixes_ttls[prefix]['2w'] += 1 - elif ttl < 60*60*24*30: - prefixes_ttls[prefix]['4w'] += 1 + prefixes_ttls[prefix]["X"] += 1 + elif ttl < 60 * 60: # 1 hour + prefixes_ttls[prefix]["1h"] += 1 + elif ttl < 60 * 60 * 24: + prefixes_ttls[prefix]["1d"] += 1 + elif ttl < 60 * 60 * 24 * 7: + prefixes_ttls[prefix]["1w"] += 1 + elif ttl < 60 * 60 * 24 * 14: + prefixes_ttls[prefix]["2w"] += 1 + elif ttl < 60 * 60 * 24 * 30: + prefixes_ttls[prefix]["4w"] += 1 else: - prefixes_ttls[prefix]['4w+'] += 1 - + prefixes_ttls[prefix]["4w+"] += 1 + keys_count = len(keys) total_size = float(sum([k for k in sizes.values()])) print(" ---> %s total keys" % keys_count) for prefix, rest in prefixes.items(): total_expiring = sum([k for p, k in dict(prefixes_ttls[prefix]).items() if p != "-"]) - print(" ---> %s: (%s keys - %s space) %s keys (%s expiring: %s)" % (str(prefix, 100. * (len(rest) / float(keys_count)))[:4], str(100 * (sizes[prefix] / total_size))[:4], str(len(rest))[:4], total_expiring, dict(prefixes_ttls[prefix]))) + print( + " ---> %s: (%s keys - %s space) %s keys (%s expiring: %s)" + % ( + str(prefix, 100.0 * (len(rest) / float(keys_count)))[:4], + str(100 * (sizes[prefix] / total_size))[:4], + str(len(rest))[:4], + total_expiring, + dict(prefixes_ttls[prefix]), + ) + ) print(" ---> %s errors: %s" % (len(errors), errors)) + def round_time(dt=None, round_to=60): - """Round a datetime object to any time laps in seconds - dt : datetime.datetime object, default now. - round_to : Closest number of seconds to round to, default 1 minute. - Author: Thierry Husson 2012 - Use it as you want but don't blame me. - """ - if dt == None : dt = datetime.datetime.now() - seconds = (dt - dt.min).seconds - rounding = (seconds+round_to/2) // round_to * round_to - return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond) - + """Round a datetime object to any time laps in seconds + dt : datetime.datetime object, default now. + round_to : Closest number of seconds to round to, default 1 minute. + Author: Thierry Husson 2012 - Use it as you want but don't blame me. + """ + if dt == None: + dt = datetime.datetime.now() + seconds = (dt - dt.min).seconds + rounding = (seconds + round_to / 2) // round_to * round_to + return dt + datetime.timedelta(0, rounding - seconds, -dt.microsecond) diff --git a/apps/statistics/tasks.py b/apps/statistics/tasks.py index b05a5108b7..2a27dd551d 100644 --- a/apps/statistics/tasks.py +++ b/apps/statistics/tasks.py @@ -1,17 +1,15 @@ +from apps.statistics.models import MFeedback, MStatistics from newsblur_web.celeryapp import app -from apps.statistics.models import MStatistics -from apps.statistics.models import MFeedback from utils import log as logging - -@app.task(name='collect-stats') +@app.task(name="collect-stats") def CollectStats(): logging.debug(" ---> ~FBCollecting stats...") MStatistics.collect_statistics() - - -@app.task(name='collect-feedback') + + +@app.task(name="collect-feedback") def CollectFeedback(): logging.debug(" ---> ~FBCollecting feedback...") MFeedback.collect_feedback() diff --git a/apps/statistics/templatetags/statistics_tags.py b/apps/statistics/templatetags/statistics_tags.py index 70015a8429..362ec82a11 100644 --- a/apps/statistics/templatetags/statistics_tags.py +++ b/apps/statistics/templatetags/statistics_tags.py @@ -1,21 +1,25 @@ from django import template + from apps.statistics.models import MFeedback register = template.Library() -@register.inclusion_tag('statistics/render_statistics_graphs.xhtml') + +@register.inclusion_tag("statistics/render_statistics_graphs.xhtml") def render_statistics_graphs(statistics): return { - 'statistics': statistics, + "statistics": statistics, } - + + @register.filter def format_graph(n, max_value, height=30): if n == 0 or max_value == 0: return 1 - return max(1, height * (n/float(max_value))) - -@register.inclusion_tag('statistics/render_feedback_table.xhtml') + return max(1, height * (n / float(max_value))) + + +@register.inclusion_tag("statistics/render_feedback_table.xhtml") def render_feedback_table(): feedbacks = MFeedback.all() - return dict(feedbacks=feedbacks) \ No newline at end of file + return dict(feedbacks=feedbacks) diff --git a/apps/statistics/tests.py b/apps/statistics/tests.py index c7c4668e12..f51d798ffd 100644 --- a/apps/statistics/tests.py +++ b/apps/statistics/tests.py @@ -7,6 +7,7 @@ from django.test import TestCase + class SimpleTest(TestCase): def test_basic_addition(self): """ @@ -14,10 +15,12 @@ def test_basic_addition(self): """ self.assertEqual(1 + 1, 2) -__test__ = {"doctest": """ + +__test__ = { + "doctest": """ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True -"""} - +""" +} diff --git a/apps/statistics/urls.py b/apps/statistics/urls.py index ee2ede961d..8474204ac0 100644 --- a/apps/statistics/urls.py +++ b/apps/statistics/urls.py @@ -1,9 +1,10 @@ from django.conf.urls import * + from apps.statistics import views urlpatterns = [ - url(r'^dashboard_graphs', views.dashboard_graphs, name='statistics-graphs'), - url(r'^feedback_table', views.feedback_table, name='feedback-table'), - url(r'^revenue', views.revenue, name='revenue'), - url(r'^slow', views.slow, name='slow'), + url(r"^dashboard_graphs", views.dashboard_graphs, name="statistics-graphs"), + url(r"^feedback_table", views.feedback_table, name="feedback-table"), + url(r"^revenue", views.revenue, name="revenue"), + url(r"^slow", views.slow, name="slow"), ] diff --git a/apps/statistics/views.py b/apps/statistics/views.py index 8d769c07a2..0e79fb37de 100644 --- a/apps/statistics/views.py +++ b/apps/statistics/views.py @@ -1,64 +1,63 @@ import base64 -import pickle -import redis import datetime -from operator import countOf +import pickle from collections import defaultdict -from django.http import HttpResponse -from django.shortcuts import render -from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import AnonymousUser -from django.contrib.auth.models import User +from operator import countOf + +import redis from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import AnonymousUser, User +from django.http import HttpResponse, HttpResponseForbidden +from django.shortcuts import render from django.utils import feedgenerator -from django.http import HttpResponseForbidden -from apps.statistics.models import MStatistics, MFeedback -from apps.statistics.rstats import round_time + from apps.profile.models import PaymentHistory +from apps.statistics.models import MFeedback, MStatistics +from apps.statistics.rstats import round_time from utils import log as logging + def dashboard_graphs(request): statistics = MStatistics.all() - return render( - request, - 'statistics/render_statistics_graphs.xhtml', - {'statistics': statistics} - ) + return render(request, "statistics/render_statistics_graphs.xhtml", {"statistics": statistics}) + def feedback_table(request): feedbacks = MFeedback.all() - return render( - request, - 'statistics/render_feedback_table.xhtml', - {'feedbacks': feedbacks} - ) + return render(request, "statistics/render_feedback_table.xhtml", {"feedbacks": feedbacks}) + def revenue(request): data = {} - data['title'] = "NewsBlur Revenue" - data['link'] = "https://www.newsblur.com" - data['description'] = "Revenue" - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur Revenue Writer' - data['docs'] = None + data["title"] = "NewsBlur Revenue" + data["link"] = "https://www.newsblur.com" + data["description"] = "Revenue" + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur Revenue Writer" + data["docs"] = None rss = feedgenerator.Atom1Feed(**data) - + report = PaymentHistory.report() - content = "%s revenue: $%s
%s" % (datetime.datetime.now().strftime('%Y'), report['annual'], report['output'].replace('\n', '
')) - + content = "%s revenue: $%s
%s" % ( + datetime.datetime.now().strftime("%Y"), + report["annual"], + report["output"].replace("\n", "
"), + ) + story = { - 'title': "Daily snapshot: %s" % (datetime.datetime.now().strftime('%a %b %-d, %Y')), - 'link': 'https://www.newsblur.com', - 'description': content, - 'unique_id': datetime.datetime.now().strftime('%a %b %-d, %Y'), - 'pubdate': datetime.datetime.now(), + "title": "Daily snapshot: %s" % (datetime.datetime.now().strftime("%a %b %-d, %Y")), + "link": "https://www.newsblur.com", + "description": content, + "unique_id": datetime.datetime.now().strftime("%a %b %-d, %Y"), + "pubdate": datetime.datetime.now(), } rss.add_item(**story) - - logging.user(request, "~FBGenerating Revenue RSS feed: ~FM%s" % ( - request.META.get('HTTP_USER_AGENT', "")[:24] - )) - return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml') + + logging.user( + request, "~FBGenerating Revenue RSS feed: ~FM%s" % (request.META.get("HTTP_USER_AGENT", "")[:24]) + ) + return HttpResponse(rss.writeString("utf-8"), content_type="application/rss+xml") @login_required @@ -74,8 +73,8 @@ def slow(request): user_id_counts = {} path_counts = {} users = {} - - for minutes_ago in range(60*6): + + for minutes_ago in range(60 * 6): dt_ago = now - datetime.timedelta(minutes=minutes_ago) minute = round_time(dt_ago, round_to=60) dt_ago_str = minute.strftime("%a %b %-d, %Y %H:%M") @@ -83,7 +82,7 @@ def slow(request): minute_queries = r.lrange(name, 0, -1) for query_raw in minute_queries: query = pickle.loads(base64.b64decode(query_raw)) - user_id = query['user_id'] + user_id = query["user_id"] if dt_ago_str not in all_queries: all_queries[dt_ago_str] = [] if user_id in users: @@ -97,22 +96,26 @@ def slow(request): else: user = AnonymousUser() users[user_id] = user - query['user'] = user - query['datetime'] = minute + query["user"] = user + query["datetime"] = minute all_queries[dt_ago_str].append(query) if user_id not in user_id_counts: user_id_counts[user_id] = 0 user_id_counts[user_id] += 1 - if query['path'] not in path_counts: - path_counts[query['path']] = 0 - path_counts[query['path']] += 1 + if query["path"] not in path_counts: + path_counts[query["path"]] = 0 + path_counts[query["path"]] += 1 user_counts = [] for user_id, count in user_id_counts.items(): - user_counts.append({'user': users[user_id], 'count': count}) - - return render(request, 'statistics/slow.xhtml', { - 'all_queries': all_queries, - 'user_counts': user_counts, - 'path_counts': path_counts, - }) + user_counts.append({"user": users[user_id], "count": count}) + + return render( + request, + "statistics/slow.xhtml", + { + "all_queries": all_queries, + "user_counts": user_counts, + "path_counts": path_counts, + }, + ) diff --git a/archive/ansible/do_inventory.py b/archive/ansible/do_inventory.py index 3cfa63e33a..aba12c1076 100755 --- a/archive/ansible/do_inventory.py +++ b/archive/ansible/do_inventory.py @@ -121,7 +121,8 @@ # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function + __metaclass__ = type ###################################################################### @@ -130,10 +131,11 @@ import ast import os import re -import requests import sys from time import time +import requests + try: import ConfigParser except ImportError: @@ -145,21 +147,23 @@ class DoManager: def __init__(self, api_token): self.api_token = api_token - self.api_endpoint = 'https://api.digitalocean.com/v2' - self.headers = {'Authorization': 'Bearer {0}'.format(self.api_token), - 'Content-type': 'application/json'} + self.api_endpoint = "https://api.digitalocean.com/v2" + self.headers = { + "Authorization": "Bearer {0}".format(self.api_token), + "Content-type": "application/json", + } self.timeout = 60 def _url_builder(self, path): - if path[0] == '/': + if path[0] == "/": path = path[1:] - return '%s/%s' % (self.api_endpoint, path) + return "%s/%s" % (self.api_endpoint, path) - def send(self, url, method='GET', data=None): + def send(self, url, method="GET", data=None): url = self._url_builder(url) data = json.dumps(data) try: - if method == 'GET': + if method == "GET": resp_data = {} incomplete = True while incomplete: @@ -173,7 +177,7 @@ def send(self, url, method='GET', data=None): resp_data[key] = value try: - url = json_resp['links']['pages']['next'] + url = json_resp["links"]["pages"]["next"] except KeyError: incomplete = False @@ -182,54 +186,53 @@ def send(self, url, method='GET', data=None): return resp_data def all_active_droplets(self): - resp = self.send('droplets/') - return resp['droplets'] + resp = self.send("droplets/") + return resp["droplets"] def all_regions(self): - resp = self.send('regions/') - return resp['regions'] + resp = self.send("regions/") + return resp["regions"] - def all_images(self, filter_name='global'): - params = {'filter': filter_name} - resp = self.send('images/', data=params) - return resp['images'] + def all_images(self, filter_name="global"): + params = {"filter": filter_name} + resp = self.send("images/", data=params) + return resp["images"] def sizes(self): - resp = self.send('sizes/') - return resp['sizes'] + resp = self.send("sizes/") + return resp["sizes"] def all_ssh_keys(self): - resp = self.send('account/keys') - return resp['ssh_keys'] + resp = self.send("account/keys") + return resp["ssh_keys"] def all_domains(self): - resp = self.send('domains/') - return resp['domains'] + resp = self.send("domains/") + return resp["domains"] def show_droplet(self, droplet_id): - resp = self.send('droplets/%s' % droplet_id) - return resp['droplet'] + resp = self.send("droplets/%s" % droplet_id) + return resp["droplet"] def all_tags(self): - resp = self.send('tags') - return resp['tags'] + resp = self.send("tags") + return resp["tags"] class DigitalOceanInventory(object): - ########################################################################### # Main execution path ########################################################################### def __init__(self): - """Main execution path """ + """Main execution path""" # DigitalOceanInventory data self.data = {} # All DigitalOcean data self.inventory = {} # Ansible Inventory # Define defaults - self.cache_path = '.' + self.cache_path = "." self.cache_max_age = 0 self.use_private_network = False self.group_variables = {} @@ -240,9 +243,11 @@ def __init__(self): self.read_cli_args() # Verify credentials were set - if not hasattr(self, 'api_token'): - msg = 'Could not find values for DigitalOcean api_token. They must be specified via either ini file, ' \ - 'command line argument (--api-token), or environment variables (DO_API_TOKEN)\n' + if not hasattr(self, "api_token"): + msg = ( + "Could not find values for DigitalOcean api_token. They must be specified via either ini file, " + "command line argument (--api-token), or environment variables (DO_API_TOKEN)\n" + ) sys.stderr.write(msg) sys.exit(-1) @@ -259,40 +264,40 @@ def __init__(self): self.load_from_cache() if len(self.data) == 0: if self.args.force_cache: - sys.stderr.write('Cache is empty and --force-cache was specified\n') + sys.stderr.write("Cache is empty and --force-cache was specified\n") sys.exit(-1) self.manager = DoManager(self.api_token) # Pick the json_data to print based on the CLI command if self.args.droplets: - self.load_from_digital_ocean('droplets') - json_data = {'droplets': self.data['droplets']} + self.load_from_digital_ocean("droplets") + json_data = {"droplets": self.data["droplets"]} elif self.args.regions: - self.load_from_digital_ocean('regions') - json_data = {'regions': self.data['regions']} + self.load_from_digital_ocean("regions") + json_data = {"regions": self.data["regions"]} elif self.args.images: - self.load_from_digital_ocean('images') - json_data = {'images': self.data['images']} + self.load_from_digital_ocean("images") + json_data = {"images": self.data["images"]} elif self.args.sizes: - self.load_from_digital_ocean('sizes') - json_data = {'sizes': self.data['sizes']} + self.load_from_digital_ocean("sizes") + json_data = {"sizes": self.data["sizes"]} elif self.args.ssh_keys: - self.load_from_digital_ocean('ssh_keys') - json_data = {'ssh_keys': self.data['ssh_keys']} + self.load_from_digital_ocean("ssh_keys") + json_data = {"ssh_keys": self.data["ssh_keys"]} elif self.args.domains: - self.load_from_digital_ocean('domains') - json_data = {'domains': self.data['domains']} + self.load_from_digital_ocean("domains") + json_data = {"domains": self.data["domains"]} elif self.args.tags: - self.load_from_digital_ocean('tags') - json_data = {'tags': self.data['tags']} + self.load_from_digital_ocean("tags") + json_data = {"tags": self.data["tags"]} elif self.args.all: self.load_from_digital_ocean() json_data = self.data elif self.args.host: json_data = self.load_droplet_variables_for_host() - else: # '--list' this is last to make it default - self.load_from_digital_ocean('droplets') + else: # '--list' this is last to make it default + self.load_from_digital_ocean("droplets") self.build_inventory() json_data = self.inventory @@ -309,31 +314,31 @@ def __init__(self): ########################################################################### def read_settings(self): - """ Reads the settings from the digital_ocean.ini file """ + """Reads the settings from the digital_ocean.ini file""" config = ConfigParser.ConfigParser() - config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'digital_ocean.ini') + config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "digital_ocean.ini") config.read(config_path) # Credentials - if config.has_option('digital_ocean', 'api_token'): - self.api_token = config.get('digital_ocean', 'api_token') + if config.has_option("digital_ocean", "api_token"): + self.api_token = config.get("digital_ocean", "api_token") # Cache related - if config.has_option('digital_ocean', 'cache_path'): - self.cache_path = config.get('digital_ocean', 'cache_path') - if config.has_option('digital_ocean', 'cache_max_age'): - self.cache_max_age = config.getint('digital_ocean', 'cache_max_age') + if config.has_option("digital_ocean", "cache_path"): + self.cache_path = config.get("digital_ocean", "cache_path") + if config.has_option("digital_ocean", "cache_max_age"): + self.cache_max_age = config.getint("digital_ocean", "cache_max_age") # Private IP Address - if config.has_option('digital_ocean', 'use_private_network'): - self.use_private_network = config.getboolean('digital_ocean', 'use_private_network') + if config.has_option("digital_ocean", "use_private_network"): + self.use_private_network = config.getboolean("digital_ocean", "use_private_network") # Group variables - if config.has_option('digital_ocean', 'group_variables'): - self.group_variables = ast.literal_eval(config.get('digital_ocean', 'group_variables')) + if config.has_option("digital_ocean", "group_variables"): + self.group_variables = ast.literal_eval(config.get("digital_ocean", "group_variables")) def read_environment(self): - """ Reads the settings from environment variables """ + """Reads the settings from environment variables""" # Setup credentials if os.getenv("DO_API_TOKEN"): self.api_token = os.getenv("DO_API_TOKEN") @@ -341,31 +346,48 @@ def read_environment(self): self.api_token = os.getenv("DO_API_KEY") def read_cli_args(self): - """ Command line argument processing """ - parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on DigitalOcean credentials') - - parser.add_argument('--list', action='store_true', help='List all active Droplets as Ansible inventory (default: True)') - parser.add_argument('--host', action='store', help='Get all Ansible inventory variables about a specific Droplet') - - parser.add_argument('--all', action='store_true', help='List all DigitalOcean information as JSON') - parser.add_argument('--droplets', '-d', action='store_true', help='List Droplets as JSON') - parser.add_argument('--regions', action='store_true', help='List Regions as JSON') - parser.add_argument('--images', action='store_true', help='List Images as JSON') - parser.add_argument('--sizes', action='store_true', help='List Sizes as JSON') - parser.add_argument('--ssh-keys', action='store_true', help='List SSH keys as JSON') - parser.add_argument('--domains', action='store_true', help='List Domains as JSON') - parser.add_argument('--tags', action='store_true', help='List Tags as JSON') - - parser.add_argument('--pretty', '-p', action='store_true', help='Pretty-print results') - - parser.add_argument('--cache-path', action='store', help='Path to the cache files (default: .)') - parser.add_argument('--cache-max_age', action='store', help='Maximum age of the cached items (default: 0)') - parser.add_argument('--force-cache', action='store_true', default=False, help='Only use data from the cache') - parser.add_argument('--refresh-cache', '-r', action='store_true', default=False, - help='Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files)') - - parser.add_argument('--env', '-e', action='store_true', help='Display DO_API_TOKEN') - parser.add_argument('--api-token', '-a', action='store', help='DigitalOcean API Token') + """Command line argument processing""" + parser = argparse.ArgumentParser( + description="Produce an Ansible Inventory file based on DigitalOcean credentials" + ) + + parser.add_argument( + "--list", + action="store_true", + help="List all active Droplets as Ansible inventory (default: True)", + ) + parser.add_argument( + "--host", action="store", help="Get all Ansible inventory variables about a specific Droplet" + ) + + parser.add_argument("--all", action="store_true", help="List all DigitalOcean information as JSON") + parser.add_argument("--droplets", "-d", action="store_true", help="List Droplets as JSON") + parser.add_argument("--regions", action="store_true", help="List Regions as JSON") + parser.add_argument("--images", action="store_true", help="List Images as JSON") + parser.add_argument("--sizes", action="store_true", help="List Sizes as JSON") + parser.add_argument("--ssh-keys", action="store_true", help="List SSH keys as JSON") + parser.add_argument("--domains", action="store_true", help="List Domains as JSON") + parser.add_argument("--tags", action="store_true", help="List Tags as JSON") + + parser.add_argument("--pretty", "-p", action="store_true", help="Pretty-print results") + + parser.add_argument("--cache-path", action="store", help="Path to the cache files (default: .)") + parser.add_argument( + "--cache-max_age", action="store", help="Maximum age of the cached items (default: 0)" + ) + parser.add_argument( + "--force-cache", action="store_true", default=False, help="Only use data from the cache" + ) + parser.add_argument( + "--refresh-cache", + "-r", + action="store_true", + default=False, + help="Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files)", + ) + + parser.add_argument("--env", "-e", action="store_true", help="Display DO_API_TOKEN") + parser.add_argument("--api-token", "-a", action="store", help="DigitalOcean API Token") self.args = parser.parse_args() @@ -373,11 +395,17 @@ def read_cli_args(self): self.api_token = self.args.api_token # Make --list default if none of the other commands are specified - if (not self.args.droplets and not self.args.regions and - not self.args.images and not self.args.sizes and - not self.args.ssh_keys and not self.args.domains and - not self.args.tags and - not self.args.all and not self.args.host): + if ( + not self.args.droplets + and not self.args.regions + and not self.args.images + and not self.args.sizes + and not self.args.ssh_keys + and not self.args.domains + and not self.args.tags + and not self.args.all + and not self.args.host + ): self.args.list = True ########################################################################### @@ -385,117 +413,112 @@ def read_cli_args(self): ########################################################################### def load_from_digital_ocean(self, resource=None): - """Get JSON from DigitalOcean API """ + """Get JSON from DigitalOcean API""" if self.args.force_cache and os.path.isfile(self.cache_filename): return # We always get fresh droplets - if self.is_cache_valid() and not (resource == 'droplets' or resource is None): + if self.is_cache_valid() and not (resource == "droplets" or resource is None): return if self.args.refresh_cache: resource = None - if resource == 'droplets' or resource is None: - self.data['droplets'] = self.manager.all_active_droplets() + if resource == "droplets" or resource is None: + self.data["droplets"] = self.manager.all_active_droplets() self.cache_refreshed = True - if resource == 'regions' or resource is None: - self.data['regions'] = self.manager.all_regions() + if resource == "regions" or resource is None: + self.data["regions"] = self.manager.all_regions() self.cache_refreshed = True - if resource == 'images' or resource is None: - self.data['images'] = self.manager.all_images() + if resource == "images" or resource is None: + self.data["images"] = self.manager.all_images() self.cache_refreshed = True - if resource == 'sizes' or resource is None: - self.data['sizes'] = self.manager.sizes() + if resource == "sizes" or resource is None: + self.data["sizes"] = self.manager.sizes() self.cache_refreshed = True - if resource == 'ssh_keys' or resource is None: - self.data['ssh_keys'] = self.manager.all_ssh_keys() + if resource == "ssh_keys" or resource is None: + self.data["ssh_keys"] = self.manager.all_ssh_keys() self.cache_refreshed = True - if resource == 'domains' or resource is None: - self.data['domains'] = self.manager.all_domains() + if resource == "domains" or resource is None: + self.data["domains"] = self.manager.all_domains() self.cache_refreshed = True - if resource == 'tags' or resource is None: - self.data['tags'] = self.manager.all_tags() + if resource == "tags" or resource is None: + self.data["tags"] = self.manager.all_tags() self.cache_refreshed = True def add_inventory_group(self, key): - """ Method to create group dict """ - host_dict = {'hosts': [], 'vars': {}} + """Method to create group dict""" + host_dict = {"hosts": [], "vars": {}} self.inventory[key] = host_dict return def add_host(self, group, host): - """ Helper method to reduce host duplication """ + """Helper method to reduce host duplication""" if group not in self.inventory: self.add_inventory_group(group) - if host not in self.inventory[group]['hosts']: - self.inventory[group]['hosts'].append(host) + if host not in self.inventory[group]["hosts"]: + self.inventory[group]["hosts"].append(host) return def build_inventory(self): - """ Build Ansible inventory of droplets """ - self.inventory = { - 'all': { - 'hosts': [], - 'vars': self.group_variables - }, - '_meta': {'hostvars': {}} - } + """Build Ansible inventory of droplets""" + self.inventory = {"all": {"hosts": [], "vars": self.group_variables}, "_meta": {"hostvars": {}}} # add all droplets by id and name - for droplet in self.data['droplets']: - for net in droplet['networks']['v4']: - if net['type'] == 'public': - dest = net['ip_address'] + for droplet in self.data["droplets"]: + for net in droplet["networks"]["v4"]: + if net["type"] == "public": + dest = net["ip_address"] else: continue - self.inventory['all']['hosts'].append(dest) + self.inventory["all"]["hosts"].append(dest) - self.add_host(droplet['id'], dest) + self.add_host(droplet["id"], dest) - self.add_host(droplet['name'], dest) + self.add_host(droplet["name"], dest) - roledef = re.split(r"([0-9]+)", droplet['name'])[0] + roledef = re.split(r"([0-9]+)", droplet["name"])[0] self.add_host(roledef, dest) # groups that are always present - for group in ('digital_ocean', - 'region_' + droplet['region']['slug'], - 'image_' + str(droplet['image']['id']), - 'size_' + droplet['size']['slug'], - 'distro_' + DigitalOceanInventory.to_safe(droplet['image']['distribution']), - 'status_' + droplet['status']): + for group in ( + "digital_ocean", + "region_" + droplet["region"]["slug"], + "image_" + str(droplet["image"]["id"]), + "size_" + droplet["size"]["slug"], + "distro_" + DigitalOceanInventory.to_safe(droplet["image"]["distribution"]), + "status_" + droplet["status"], + ): # self.add_host(group, dest) pass # groups that are not always present - for group in (droplet['image']['slug'], - droplet['image']['name']): + for group in (droplet["image"]["slug"], droplet["image"]["name"]): if group: - image = 'image_' + DigitalOceanInventory.to_safe(group) + image = "image_" + DigitalOceanInventory.to_safe(group) # self.add_host(image, dest) - if droplet['tags']: - for tag in droplet['tags']: + if droplet["tags"]: + for tag in droplet["tags"]: self.add_host(tag, dest) # hostvars info = self.do_namespace(droplet) - self.inventory['_meta']['hostvars'][dest] = info + self.inventory["_meta"]["hostvars"][dest] = info def load_droplet_variables_for_host(self): - """ Generate a JSON response to a --host call """ + """Generate a JSON response to a --host call""" host = int(self.args.host) droplet = self.manager.show_droplet(host) info = self.do_namespace(droplet) - return {'droplet': info} + return {"droplet": info} ########################################################################### # Cache Management ########################################################################### def is_cache_valid(self): - """ Determines if the cache files have expired, or if it is still valid """ + """Determines if the cache files have expired, or if it is still valid""" if os.path.isfile(self.cache_filename): mod_time = os.path.getmtime(self.cache_filename) current_time = time() @@ -504,23 +527,23 @@ def is_cache_valid(self): return False def load_from_cache(self): - """ Reads the data from the cache file and assigns it to member variables as Python Objects """ + """Reads the data from the cache file and assigns it to member variables as Python Objects""" try: - with open(self.cache_filename, 'r') as cache: + with open(self.cache_filename, "r") as cache: json_data = cache.read() data = json.loads(json_data) except IOError: - data = {'data': {}, 'inventory': {}} + data = {"data": {}, "inventory": {}} - self.data = data['data'] - self.inventory = data['inventory'] + self.data = data["data"] + self.inventory = data["inventory"] def write_to_cache(self): - """ Writes data in JSON format to a file """ - data = {'data': self.data, 'inventory': self.inventory} + """Writes data in JSON format to a file""" + data = {"data": self.data, "inventory": self.inventory} json_data = json.dumps(data, indent=2) - with open(self.cache_filename, 'w') as cache: + with open(self.cache_filename, "w") as cache: cache.write(json_data) ########################################################################### @@ -528,15 +551,15 @@ def write_to_cache(self): ########################################################################### @staticmethod def to_safe(word): - """ Converts 'bad' characters in a string to underscores so they can be used as Ansible groups """ + """Converts 'bad' characters in a string to underscores so they can be used as Ansible groups""" return re.sub(r"[^A-Za-z0-9\-.]", "_", word) @staticmethod def do_namespace(data): - """ Returns a copy of the dictionary with all the keys put in a 'do_' namespace """ + """Returns a copy of the dictionary with all the keys put in a 'do_' namespace""" info = {} for k, v in data.items(): - info['do_' + k] = v + info["do_" + k] = v return info diff --git a/archive/fabfile.py b/archive/fabfile.py index 1c35d76f9d..8df446eb26 100644 --- a/archive/fabfile.py +++ b/archive/fabfile.py @@ -1,21 +1,33 @@ -from fabric.api import cd, lcd, env, local, parallel, serial -from fabric.api import put, run, settings, sudo, prefix -from fabric.operations import prompt -from fabric.contrib import django -from fabric.contrib import files -from fabric.state import connections +import os +import re +import sys +import time +from collections import defaultdict +from contextlib import contextmanager as _contextmanager +from pprint import pprint + +import yaml +from boto.ec2.connection import EC2Connection + # from fabric.colors import red, green, blue, cyan, magenta, white, yellow from boto.s3.connection import S3Connection from boto.s3.key import Key -from boto.ec2.connection import EC2Connection -import yaml -from pprint import pprint -from collections import defaultdict -from contextlib import contextmanager as _contextmanager -import os -import time -import sys -import re +from fabric.api import ( + cd, + env, + lcd, + local, + parallel, + prefix, + put, + run, + serial, + settings, + sudo, +) +from fabric.contrib import django, files +from fabric.operations import prompt +from fabric.state import connections # django.setup() @@ -25,7 +37,7 @@ print("Digital Ocean's API not loaded. Install python-digitalocean.") -django.settings_module('newsblur_web.settings') +django.settings_module("newsblur_web.settings") try: from django.conf import settings as django_settings except ImportError: @@ -37,10 +49,10 @@ # ============ env.NEWSBLUR_PATH = "/srv/newsblur" -env.SECRETS_PATH = "/srv/secrets-newsblur" -env.VENDOR_PATH = "/srv/code" -env.user = 'sclay' -env.key_filename = os.path.join(env.SECRETS_PATH, 'keys/newsblur.key') +env.SECRETS_PATH = "/srv/secrets-newsblur" +env.VENDOR_PATH = "/srv/code" +env.user = "sclay" +env.key_filename = os.path.join(env.SECRETS_PATH, "keys/newsblur.key") env.connection_attempts = 10 env.do_ip_to_hostname = {} env.colorize_errors = True @@ -50,7 +62,7 @@ # ========= try: - hosts_path = os.path.expanduser(os.path.join(env.SECRETS_PATH, 'configs/hosts.yml')) + hosts_path = os.path.expanduser(os.path.join(env.SECRETS_PATH, "configs/hosts.yml")) roles = yaml.load(open(hosts_path)) for role_name, hosts in list(roles.items()): if isinstance(hosts, dict): @@ -59,11 +71,12 @@ except: print(" ***> No role definitions found in %s. Using default roles." % hosts_path) env.roledefs = { - 'app' : ['app01.newsblur.com'], - 'db' : ['db01.newsblur.com'], - 'task' : ['task01.newsblur.com'], + "app": ["app01.newsblur.com"], + "db": ["db01.newsblur.com"], + "task": ["task01.newsblur.com"], } + def do_roledefs(split=False, debug=False): doapi = digitalocean.Manager(token=django_settings.DO_TOKEN_FABRIC) droplets = doapi.get_all_droplets() @@ -76,7 +89,7 @@ def do_roledefs(split=False, debug=False): if roledef not in hostnames: hostnames[roledef] = [] if droplet.ip_address not in hostnames[roledef]: - hostnames[roledef].append({'name': droplet.name, 'address': droplet.ip_address}) + hostnames[roledef].append({"name": droplet.name, "address": droplet.ip_address}) env.do_ip_to_hostname[droplet.ip_address] = droplet.name if droplet.ip_address not in env.roledefs[roledef]: env.roledefs[roledef].append(droplet.ip_address) @@ -85,6 +98,7 @@ def do_roledefs(split=False, debug=False): return hostnames return droplets + def list_do(): droplets = assign_digitalocean_roledefs(split=True) pprint(droplets) @@ -94,7 +108,7 @@ def list_do(): # for server in group: # if 'address' in server: # print(server['address']) - + doapi = digitalocean.Manager(token=django_settings.DO_TOKEN_FABRIC) droplets = doapi.get_all_droplets() sizes = doapi.get_all_sizes() @@ -103,31 +117,35 @@ def list_do(): total_cost = 0 for droplet in droplets: roledef = re.split(r"([0-9]+)", droplet.name)[0] - cost = droplet.size['price_monthly'] + cost = droplet.size["price_monthly"] role_costs[roledef] += cost total_cost += cost - + print("\n\n Costs:") pprint(dict(role_costs)) print(" ---> Total cost: $%s/month" % total_cost) - + + def host(*names): env.hosts = [] - env.doname = ','.join(names) + env.doname = ",".join(names) hostnames = assign_digitalocean_roledefs(split=True) for role, hosts in list(hostnames.items()): for host in hosts: - if isinstance(host, dict) and host['name'] in names: - env.hosts.append(host['address']) + if isinstance(host, dict) and host["name"] in names: + env.hosts.append(host["address"]) print(" ---> Using %s as hosts" % env.hosts) - + + # ================ # = Environments = # ================ + def server(): env.NEWSBLUR_PATH = "/srv/newsblur" - env.VENDOR_PATH = "/srv/code" + env.VENDOR_PATH = "/srv/code" + def assign_digitalocean_roledefs(split=False): server() @@ -136,66 +154,81 @@ def assign_digitalocean_roledefs(split=False): for roledef, hosts in list(env.roledefs.items()): if roledef not in droplets: droplets[roledef] = hosts - + return droplets + def app(): assign_digitalocean_roledefs() - env.roles = ['app'] + env.roles = ["app"] + def web(): assign_digitalocean_roledefs() - env.roles = ['app', 'push', 'work', 'search'] + env.roles = ["app", "push", "work", "search"] + def work(): assign_digitalocean_roledefs() - env.roles = ['work'] + env.roles = ["work"] + def www(): assign_digitalocean_roledefs() - env.roles = ['www'] + env.roles = ["www"] + def dev(): assign_digitalocean_roledefs() - env.roles = ['dev'] + env.roles = ["dev"] + def debug(): assign_digitalocean_roledefs() - env.roles = ['debug'] + env.roles = ["debug"] + def node(): assign_digitalocean_roledefs() - env.roles = ['node'] + env.roles = ["node"] + def push(): assign_digitalocean_roledefs() - env.roles = ['push'] + env.roles = ["push"] + def db(): assign_digitalocean_roledefs() - env.roles = ['db', 'search'] + env.roles = ["db", "search"] + def task(): assign_digitalocean_roledefs() - env.roles = ['task'] + env.roles = ["task"] + def ec2task(): ec2() - env.roles = ['ec2task'] + env.roles = ["ec2task"] + def ec2(): - env.user = 'ubuntu' - env.key_filename = ['/Users/sclay/.ec2/sclay.pem'] + env.user = "ubuntu" + env.key_filename = ["/Users/sclay/.ec2/sclay.pem"] assign_digitalocean_roledefs() + def all(): assign_digitalocean_roledefs() - env.roles = ['app', 'db', 'debug', 'node', 'push', 'work', 'www', 'search'] + env.roles = ["app", "db", "debug", "node", "push", "work", "www", "search"] + # ============= # = Bootstrap = # ============= + def setup_common(): setup_installs() change_shell() @@ -224,17 +257,19 @@ def setup_common(): setup_nginx() setup_munin() + def setup_all(): setup_common() setup_app(skip_common=True) setup_db(skip_common=True) setup_task(skip_common=True) + def setup_app_docker(skip_common=False): if not skip_common: setup_common() setup_app_firewall() - setup_motd('app') + setup_motd("app") change_shell() setup_user() @@ -248,13 +283,14 @@ def setup_app_docker(skip_common=False): setup_docker() done() - sudo('reboot') + sudo("reboot") + def setup_app(skip_common=False, node=False): if not skip_common: setup_common() setup_app_firewall() - setup_motd('app') + setup_motd("app") copy_app_settings() config_nginx() setup_gunicorn(supervisor=True) @@ -264,7 +300,8 @@ def setup_app(skip_common=False, node=False): config_monit_app() setup_usage_monitor() done() - sudo('reboot') + sudo("reboot") + def setup_app_image(): copy_app_settings() @@ -274,17 +311,19 @@ def setup_app_image(): pip() deploy_web() done() - sudo('reboot') + sudo("reboot") + def setup_node(): setup_node_app() config_node(full=True) - + + def setup_db(engine=None, skip_common=False, skip_benchmark=False): if not skip_common: setup_common() setup_db_firewall() - setup_motd('db') + setup_motd("db") copy_db_settings() if engine == "postgres": setup_postgres(standby=False) @@ -316,18 +355,20 @@ def setup_db(engine=None, skip_common=False, skip_benchmark=False): # if env.user == 'ubuntu': # setup_db_mdadm() + def setup_task(queue=None, skip_common=False): if not skip_common: setup_common() setup_task_firewall() - setup_motd('task') + setup_motd("task") copy_task_settings() enable_celery_supervisor(queue) setup_gunicorn(supervisor=False) config_monit_task() setup_usage_monitor() done() - sudo('reboot') + sudo("reboot") + def setup_task_image(): setup_installs() @@ -338,198 +379,229 @@ def setup_task_image(): pip() deploy(reload=True) done() - sudo('reboot') + sudo("reboot") + # ================== # = Setup - Docker = # ================== + def setup_docker(): packages = [ - 'build-essential', + "build-essential", ] - sudo('DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install %s' % ' '.join(packages)) + sudo( + 'DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install %s' + % " ".join(packages) + ) - sudo('apt install -fy docker docker-compose') - sudo('usermod -aG docker ${USER}') - sudo('su - ${USER}') + sudo("apt install -fy docker docker-compose") + sudo("usermod -aG docker ${USER}") + sudo("su - ${USER}") copy_certificates() - + + # ================== # = Setup - Common = # ================== + def done(): print("\n\n\n\n-----------------------------------------------------") - print("\n\n %s / %s IS SUCCESSFULLY BOOTSTRAPPED" % (env.get('doname') or env.host_string, env.host_string)) + print( + "\n\n %s / %s IS SUCCESSFULLY BOOTSTRAPPED" + % (env.get("doname") or env.host_string, env.host_string) + ) print("\n\n-----------------------------------------------------\n\n\n\n") + def setup_installs(): packages = [ - 'build-essential', - 'gcc', - 'scons', - 'libreadline-dev', - 'sysstat', - 'iotop', - 'git', - 'python2', - 'python2.7-dev', - 'locate', - 'software-properties-common', - 'libpcre3-dev', - 'libncurses5-dev', - 'libdbd-pg-perl', - 'libssl-dev', - 'libffi-dev', - 'libevent-dev', - 'make', - 'postgresql-common', - 'ssl-cert', - 'python-setuptools', - 'libyaml-0-2', - 'pgbouncer', - 'python-yaml', - 'python-numpy', - 'curl', - 'monit', - 'ufw', - 'libjpeg8', - 'libjpeg62-dev', - 'libfreetype6', - 'libfreetype6-dev', - 'libmysqlclient-dev', - 'libblas-dev', - 'liblapack-dev', - 'libatlas-base-dev', - 'gfortran', - 'libpq-dev', + "build-essential", + "gcc", + "scons", + "libreadline-dev", + "sysstat", + "iotop", + "git", + "python2", + "python2.7-dev", + "locate", + "software-properties-common", + "libpcre3-dev", + "libncurses5-dev", + "libdbd-pg-perl", + "libssl-dev", + "libffi-dev", + "libevent-dev", + "make", + "postgresql-common", + "ssl-cert", + "python-setuptools", + "libyaml-0-2", + "pgbouncer", + "python-yaml", + "python-numpy", + "curl", + "monit", + "ufw", + "libjpeg8", + "libjpeg62-dev", + "libfreetype6", + "libfreetype6-dev", + "libmysqlclient-dev", + "libblas-dev", + "liblapack-dev", + "libatlas-base-dev", + "gfortran", + "libpq-dev", ] # sudo("sed -i -e 's/archive.ubuntu.com\|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list") put("config/apt_sources.conf", "/etc/apt/sources.list", use_sudo=True) - run('sleep 10') # Dies on a lock, so just delay - sudo('apt-get -y update') - run('sleep 10') # Dies on a lock, so just delay - sudo('DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade') - run('sleep 10') # Dies on a lock, so just delay - sudo('DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install %s' % ' '.join(packages)) - + run("sleep 10") # Dies on a lock, so just delay + sudo("apt-get -y update") + run("sleep 10") # Dies on a lock, so just delay + sudo( + 'DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade' + ) + run("sleep 10") # Dies on a lock, so just delay + sudo( + 'DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install %s' + % " ".join(packages) + ) + with settings(warn_only=True): sudo("ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /usr/lib") sudo("ln -s /usr/lib/x86_64-linux-gnu/libfreetype.so /usr/lib") sudo("ln -s /usr/lib/x86_64-linux-gnu/libz.so /usr/lib") - + with settings(warn_only=True): - sudo('mkdir -p %s' % env.VENDOR_PATH) - sudo('chown %s.%s %s' % (env.user, env.user, env.VENDOR_PATH)) + sudo("mkdir -p %s" % env.VENDOR_PATH) + sudo("chown %s.%s %s" % (env.user, env.user, env.VENDOR_PATH)) + def change_shell(): - sudo('apt-get -fy install zsh') + sudo("apt-get -fy install zsh") with settings(warn_only=True): - run('git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh') - run('git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting') - sudo('chsh %s -s /bin/zsh' % env.user) + run("git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh") + run( + "git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting" + ) + sudo("chsh %s -s /bin/zsh" % env.user) + def setup_user(): # run('useradd -c "NewsBlur" -m newsblur -s /bin/zsh') # run('openssl rand -base64 8 | tee -a ~conesus/.password | passwd -stdin conesus') - run('mkdir -p ~/.ssh && chmod 700 ~/.ssh') - run('rm -fr ~/.ssh/id_dsa*') + run("mkdir -p ~/.ssh && chmod 700 ~/.ssh") + run("rm -fr ~/.ssh/id_dsa*") run('ssh-keygen -t dsa -f ~/.ssh/id_dsa -N ""') - run('touch ~/.ssh/authorized_keys') + run("touch ~/.ssh/authorized_keys") put("~/.ssh/id_dsa.pub", "authorized_keys") - run("echo \"\n\" >> ~sclay/.ssh/authorized_keys") - run('echo `cat authorized_keys` >> ~sclay/.ssh/authorized_keys') - run('rm authorized_keys') - -def copy_ssh_keys(username='sclay', private=False): - sudo('mkdir -p ~%s/.ssh' % username) - - put(os.path.join(env.SECRETS_PATH, 'keys/newsblur.key.pub'), 'local.key.pub') - sudo('mv local.key.pub ~%s/.ssh/id_rsa.pub' % username) + run('echo "\n" >> ~sclay/.ssh/authorized_keys') + run("echo `cat authorized_keys` >> ~sclay/.ssh/authorized_keys") + run("rm authorized_keys") + + +def copy_ssh_keys(username="sclay", private=False): + sudo("mkdir -p ~%s/.ssh" % username) + + put(os.path.join(env.SECRETS_PATH, "keys/newsblur.key.pub"), "local.key.pub") + sudo("mv local.key.pub ~%s/.ssh/id_rsa.pub" % username) if private: - put(os.path.join(env.SECRETS_PATH, 'keys/newsblur.key'), 'local.key') - sudo('mv local.key ~%s/.ssh/id_rsa' % username) - - sudo("echo \"\n\" >> ~%s/.ssh/authorized_keys" % username) + put(os.path.join(env.SECRETS_PATH, "keys/newsblur.key"), "local.key") + sudo("mv local.key ~%s/.ssh/id_rsa" % username) + + sudo('echo "\n" >> ~%s/.ssh/authorized_keys' % username) sudo("echo `cat ~%s/.ssh/id_rsa.pub` >> ~%s/.ssh/authorized_keys" % (username, username)) - sudo('chown -R %s.%s ~%s/.ssh' % (username, username, username)) - sudo('chmod 700 ~%s/.ssh' % username) - sudo('chmod 600 ~%s/.ssh/id_rsa*' % username) + sudo("chown -R %s.%s ~%s/.ssh" % (username, username, username)) + sudo("chmod 700 ~%s/.ssh" % username) + sudo("chmod 600 ~%s/.ssh/id_rsa*" % username) + def setup_repo(): - sudo('mkdir -p /srv') - sudo('chown -R %s.%s /srv' % (env.user, env.user)) + sudo("mkdir -p /srv") + sudo("chown -R %s.%s /srv" % (env.user, env.user)) with settings(warn_only=True): - run('git clone https://github.com/samuelclay/NewsBlur.git %s' % env.NEWSBLUR_PATH) + run("git clone https://github.com/samuelclay/NewsBlur.git %s" % env.NEWSBLUR_PATH) with settings(warn_only=True): - sudo('ln -sfn /srv/code /home/%s/code' % env.user) - sudo('ln -sfn /srv/newsblur /home/%s/newsblur' % env.user) + sudo("ln -sfn /srv/code /home/%s/code" % env.user) + sudo("ln -sfn /srv/newsblur /home/%s/newsblur" % env.user) + def setup_repo_local_settings(): with virtualenv(): - run('cp newsblur/local_settings.py.template newsblur/local_settings.py') - run('mkdir -p logs') - run('touch logs/newsblur.log') + run("cp newsblur/local_settings.py.template newsblur/local_settings.py") + run("mkdir -p logs") + run("touch logs/newsblur.log") + def setup_local_files(): - run('mkdir -p ~/.config/procps') + run("mkdir -p ~/.config/procps") put("config/toprc", "~/.config/procps/toprc") - run('rm -f ~/.toprc') + run("rm -f ~/.toprc") put("config/zshrc", "~/.zshrc") - put('config/gitconfig.txt', '~/.gitconfig') - put('config/ssh.conf', '~/.ssh/config') + put("config/gitconfig.txt", "~/.gitconfig") + put("config/ssh.conf", "~/.ssh/config") + def setup_psql_client(): - sudo('apt-get -y install postgresql-client') - sudo('mkdir -p /var/run/postgresql') + sudo("apt-get -y install postgresql-client") + sudo("mkdir -p /var/run/postgresql") with settings(warn_only=True): - sudo('chown postgres.postgres /var/run/postgresql') + sudo("chown postgres.postgres /var/run/postgresql") + def setup_libxml(): - sudo('apt-get -y install libxml2-dev libxslt1-dev python-lxml') + sudo("apt-get -y install libxml2-dev libxslt1-dev python-lxml") + def setup_libxml_code(): with cd(env.VENDOR_PATH): - run('git clone git://git.gnome.org/libxml2') - run('git clone git://git.gnome.org/libxslt') + run("git clone git://git.gnome.org/libxml2") + run("git clone git://git.gnome.org/libxslt") - with cd(os.path.join(env.VENDOR_PATH, 'libxml2')): - run('./configure && make && sudo make install') + with cd(os.path.join(env.VENDOR_PATH, "libxml2")): + run("./configure && make && sudo make install") + + with cd(os.path.join(env.VENDOR_PATH, "libxslt")): + run("./configure && make && sudo make install") - with cd(os.path.join(env.VENDOR_PATH, 'libxslt')): - run('./configure && make && sudo make install') def setup_psycopg(): - sudo('easy_install -U psycopg2') + sudo("easy_install -U psycopg2") + def setup_virtualenv(): - sudo('rm -fr ~/.cache') # Clean `sudo pip` - sudo('pip install --upgrade virtualenv') - sudo('pip install --upgrade virtualenvwrapper') + sudo("rm -fr ~/.cache") # Clean `sudo pip` + sudo("pip install --upgrade virtualenv") + sudo("pip install --upgrade virtualenvwrapper") setup_local_files() - with prefix('WORKON_HOME=%s' % os.path.join(env.NEWSBLUR_PATH, 'venv')): - with prefix('source /usr/local/bin/virtualenvwrapper.sh'): + with prefix("WORKON_HOME=%s" % os.path.join(env.NEWSBLUR_PATH, "venv")): + with prefix("source /usr/local/bin/virtualenvwrapper.sh"): with cd(env.NEWSBLUR_PATH): # sudo('rmvirtualenv newsblur') # sudo('rm -fr venv') with settings(warn_only=True): - run('mkvirtualenv newsblur') + run("mkvirtualenv newsblur") # run('echo "import sys; sys.setdefaultencoding(\'utf-8\')" | sudo tee venv/newsblur/lib/python2.7/sitecustomize.py') # run('echo "/srv/newsblur" | sudo tee venv/newsblur/lib/python2.7/site-packages/newsblur.pth') - + + @_contextmanager def virtualenv(): - with prefix('WORKON_HOME=%s' % os.path.join(env.NEWSBLUR_PATH, 'venv')): - with prefix('source /usr/local/bin/virtualenvwrapper.sh'): + with prefix("WORKON_HOME=%s" % os.path.join(env.NEWSBLUR_PATH, "venv")): + with prefix("source /usr/local/bin/virtualenvwrapper.sh"): with cd(env.NEWSBLUR_PATH): - with prefix('workon newsblur'): + with prefix("workon newsblur"): yield + def setup_pip(): with cd(env.VENDOR_PATH), settings(warn_only=True): - run('curl https://bootstrap.pypa.io/2.6/get-pip.py | sudo python2') + run("curl https://bootstrap.pypa.io/2.6/get-pip.py | sudo python2") # sudo('python2 get-pip.py') @@ -541,18 +613,19 @@ def pip(): with virtualenv(): if role == "task": with settings(warn_only=True): - sudo('fallocate -l 4G /swapfile') - sudo('chmod 600 /swapfile') - sudo('mkswap /swapfile') - sudo('swapon /swapfile') - sudo('chown %s.%s -R %s' % (env.user, env.user, os.path.join(env.NEWSBLUR_PATH, 'venv'))) + sudo("fallocate -l 4G /swapfile") + sudo("chmod 600 /swapfile") + sudo("mkswap /swapfile") + sudo("swapon /swapfile") + sudo("chown %s.%s -R %s" % (env.user, env.user, os.path.join(env.NEWSBLUR_PATH, "venv"))) # run('easy_install -U pip') # run('pip install --upgrade pip') # run('pip install --upgrade setuptools') - run('pip install -r requirements.txt') + run("pip install -r requirements.txt") if role == "task": with settings(warn_only=True): - sudo('swapoff /swapfile') + sudo("swapoff /swapfile") + def solo_pip(role): if role == "app": @@ -564,170 +637,195 @@ def solo_pip(role): copy_task_settings() pip() celery() - + + def setup_supervisor(): - sudo('apt-get update') - sudo('apt-get -y install supervisor') - put('config/supervisord.conf', '/etc/supervisor/supervisord.conf', use_sudo=True) - sudo('/etc/init.d/supervisor stop') - sudo('sleep 2') - sudo('ulimit -n 100000 && /etc/init.d/supervisor start') + sudo("apt-get update") + sudo("apt-get -y install supervisor") + put("config/supervisord.conf", "/etc/supervisor/supervisord.conf", use_sudo=True) + sudo("/etc/init.d/supervisor stop") + sudo("sleep 2") + sudo("ulimit -n 100000 && /etc/init.d/supervisor start") sudo("/usr/sbin/update-rc.d -f supervisor defaults") - sudo('systemctl enable supervisor') - sudo('systemctl start supervisor') + sudo("systemctl enable supervisor") + sudo("systemctl start supervisor") + @parallel def setup_hosts(): - put(os.path.join(env.SECRETS_PATH, 'configs/hosts'), '/etc/hosts', use_sudo=True) + put(os.path.join(env.SECRETS_PATH, "configs/hosts"), "/etc/hosts", use_sudo=True) sudo('echo "\n\n127.0.0.1 `hostname`" | sudo tee -a /etc/hosts') + def setup_pgbouncer(): - sudo('apt-get remove -y pgbouncer') - sudo('apt-get install -y libevent-dev pkg-config libc-ares2 libc-ares-dev') - PGBOUNCER_VERSION = '1.15.0' + sudo("apt-get remove -y pgbouncer") + sudo("apt-get install -y libevent-dev pkg-config libc-ares2 libc-ares-dev") + PGBOUNCER_VERSION = "1.15.0" with cd(env.VENDOR_PATH), settings(warn_only=True): - run('wget https://pgbouncer.github.io/downloads/files/%s/pgbouncer-%s.tar.gz' % (PGBOUNCER_VERSION, PGBOUNCER_VERSION)) - run('tar -xzf pgbouncer-%s.tar.gz' % PGBOUNCER_VERSION) - run('rm pgbouncer-%s.tar.gz' % PGBOUNCER_VERSION) - with cd('pgbouncer-%s' % PGBOUNCER_VERSION): - run('./configure --prefix=/usr/local') - run('make') - sudo('make install') - sudo('ln -s /usr/local/bin/pgbouncer /usr/sbin/pgbouncer') + run( + "wget https://pgbouncer.github.io/downloads/files/%s/pgbouncer-%s.tar.gz" + % (PGBOUNCER_VERSION, PGBOUNCER_VERSION) + ) + run("tar -xzf pgbouncer-%s.tar.gz" % PGBOUNCER_VERSION) + run("rm pgbouncer-%s.tar.gz" % PGBOUNCER_VERSION) + with cd("pgbouncer-%s" % PGBOUNCER_VERSION): + run("./configure --prefix=/usr/local") + run("make") + sudo("make install") + sudo("ln -s /usr/local/bin/pgbouncer /usr/sbin/pgbouncer") config_pgbouncer() - + + def config_pgbouncer(): - sudo('mkdir -p /etc/pgbouncer') - put('config/pgbouncer.conf', 'pgbouncer.conf') - sudo('mv pgbouncer.conf /etc/pgbouncer/pgbouncer.ini') - put(os.path.join(env.SECRETS_PATH, 'configs/pgbouncer_auth.conf'), 'userlist.txt') - sudo('mv userlist.txt /etc/pgbouncer/userlist.txt') + sudo("mkdir -p /etc/pgbouncer") + put("config/pgbouncer.conf", "pgbouncer.conf") + sudo("mv pgbouncer.conf /etc/pgbouncer/pgbouncer.ini") + put(os.path.join(env.SECRETS_PATH, "configs/pgbouncer_auth.conf"), "userlist.txt") + sudo("mv userlist.txt /etc/pgbouncer/userlist.txt") sudo('echo "START=1" | sudo tee /etc/default/pgbouncer') # sudo('su postgres -c "/etc/init.d/pgbouncer stop"', pty=False) with settings(warn_only=True): - sudo('/etc/init.d/pgbouncer stop') - sudo('pkill -9 pgbouncer -e') - run('sleep 2') - sudo('/etc/init.d/pgbouncer start', pty=False) + sudo("/etc/init.d/pgbouncer stop") + sudo("pkill -9 pgbouncer -e") + run("sleep 2") + sudo("/etc/init.d/pgbouncer start", pty=False) + @parallel def kill_pgbouncer(stop=False): # sudo('su postgres -c "/etc/init.d/pgbouncer stop"', pty=False) with settings(warn_only=True): - sudo('/etc/init.d/pgbouncer stop') - run('sleep 2') - sudo('rm /var/log/postgresql/pgbouncer.pid') + sudo("/etc/init.d/pgbouncer stop") + run("sleep 2") + sudo("rm /var/log/postgresql/pgbouncer.pid") with settings(warn_only=True): - sudo('pkill -9 pgbouncer') - run('sleep 2') + sudo("pkill -9 pgbouncer") + run("sleep 2") if not stop: - run('sudo /etc/init.d/pgbouncer start', pty=False) + run("sudo /etc/init.d/pgbouncer start", pty=False) + def config_monit_task(): - put('config/monit_task.conf', '/etc/monit/conf.d/celery.conf', use_sudo=True) + put("config/monit_task.conf", "/etc/monit/conf.d/celery.conf", use_sudo=True) sudo('echo "START=yes" | sudo tee /etc/default/monit') - sudo('/etc/init.d/monit restart') + sudo("/etc/init.d/monit restart") + def config_monit_node(): - put('config/monit_node.conf', '/etc/monit/conf.d/node.conf', use_sudo=True) + put("config/monit_node.conf", "/etc/monit/conf.d/node.conf", use_sudo=True) sudo('echo "START=yes" | sudo tee /etc/default/monit') - sudo('/etc/init.d/monit restart') + sudo("/etc/init.d/monit restart") + def config_monit_original(): - put('config/monit_original.conf', '/etc/monit/conf.d/node_original.conf', use_sudo=True) + put("config/monit_original.conf", "/etc/monit/conf.d/node_original.conf", use_sudo=True) sudo('echo "START=yes" | sudo tee /etc/default/monit') - sudo('/etc/init.d/monit restart') + sudo("/etc/init.d/monit restart") + def config_monit_app(): - put('config/monit_app.conf', '/etc/monit/conf.d/gunicorn.conf', use_sudo=True) + put("config/monit_app.conf", "/etc/monit/conf.d/gunicorn.conf", use_sudo=True) sudo('echo "START=yes" | sudo tee /etc/default/monit') - sudo('/etc/init.d/monit restart') + sudo("/etc/init.d/monit restart") + def config_monit_work(): - put('config/monit_work.conf', '/etc/monit/conf.d/work.conf', use_sudo=True) + put("config/monit_work.conf", "/etc/monit/conf.d/work.conf", use_sudo=True) sudo('echo "START=yes" | sudo tee /etc/default/monit') - sudo('/etc/init.d/monit restart') + sudo("/etc/init.d/monit restart") + def config_monit_redis(): - sudo('chown root.root /etc/init.d/redis') - sudo('chmod a+x /etc/init.d/redis') - put('config/monit_debug.sh', '/etc/monit/monit_debug.sh', use_sudo=True) - sudo('chmod a+x /etc/monit/monit_debug.sh') - put('config/monit_redis.conf', '/etc/monit/conf.d/redis.conf', use_sudo=True) + sudo("chown root.root /etc/init.d/redis") + sudo("chmod a+x /etc/init.d/redis") + put("config/monit_debug.sh", "/etc/monit/monit_debug.sh", use_sudo=True) + sudo("chmod a+x /etc/monit/monit_debug.sh") + put("config/monit_redis.conf", "/etc/monit/conf.d/redis.conf", use_sudo=True) sudo('echo "START=yes" | sudo tee /etc/default/monit') - sudo('/etc/init.d/monit restart') + sudo("/etc/init.d/monit restart") + def setup_mongoengine_repo(): with cd(env.VENDOR_PATH), settings(warn_only=True): - run('rm -fr mongoengine') - run('git clone https://github.com/MongoEngine/mongoengine.git') - sudo('rm -fr /usr/local/lib/python2.7/dist-packages/mongoengine') - sudo('rm -fr /usr/local/lib/python2.7/dist-packages/mongoengine-*') - sudo('ln -sfn %s /usr/local/lib/python2.7/dist-packages/mongoengine' % - os.path.join(env.VENDOR_PATH, 'mongoengine/mongoengine')) - with cd(os.path.join(env.VENDOR_PATH, 'mongoengine')), settings(warn_only=True): - run('git co v0.8.2') + run("rm -fr mongoengine") + run("git clone https://github.com/MongoEngine/mongoengine.git") + sudo("rm -fr /usr/local/lib/python2.7/dist-packages/mongoengine") + sudo("rm -fr /usr/local/lib/python2.7/dist-packages/mongoengine-*") + sudo( + "ln -sfn %s /usr/local/lib/python2.7/dist-packages/mongoengine" + % os.path.join(env.VENDOR_PATH, "mongoengine/mongoengine") + ) + with cd(os.path.join(env.VENDOR_PATH, "mongoengine")), settings(warn_only=True): + run("git co v0.8.2") + def clear_pymongo_repo(): - sudo('rm -fr /usr/local/lib/python2.7/dist-packages/pymongo*') - sudo('rm -fr /usr/local/lib/python2.7/dist-packages/bson*') - sudo('rm -fr /usr/local/lib/python2.7/dist-packages/gridfs*') - + sudo("rm -fr /usr/local/lib/python2.7/dist-packages/pymongo*") + sudo("rm -fr /usr/local/lib/python2.7/dist-packages/bson*") + sudo("rm -fr /usr/local/lib/python2.7/dist-packages/gridfs*") + + def setup_pymongo_repo(): with cd(env.VENDOR_PATH), settings(warn_only=True): - run('git clone git://github.com/mongodb/mongo-python-driver.git pymongo') + run("git clone git://github.com/mongodb/mongo-python-driver.git pymongo") # with cd(os.path.join(env.VENDOR_PATH, 'pymongo')): # sudo('python setup.py install') clear_pymongo_repo() - sudo('ln -sfn %s /usr/local/lib/python2.7/dist-packages/' % - os.path.join(env.VENDOR_PATH, 'pymongo/{pymongo,bson,gridfs}')) + sudo( + "ln -sfn %s /usr/local/lib/python2.7/dist-packages/" + % os.path.join(env.VENDOR_PATH, "pymongo/{pymongo,bson,gridfs}") + ) + def setup_forked_mongoengine(): - with cd(os.path.join(env.VENDOR_PATH, 'mongoengine')), settings(warn_only=True): - run('git remote add clay https://github.com/samuelclay/mongoengine.git') - run('git pull') - run('git fetch clay') - run('git checkout -b clay_master clay/master') + with cd(os.path.join(env.VENDOR_PATH, "mongoengine")), settings(warn_only=True): + run("git remote add clay https://github.com/samuelclay/mongoengine.git") + run("git pull") + run("git fetch clay") + run("git checkout -b clay_master clay/master") + def switch_forked_mongoengine(): - with cd(os.path.join(env.VENDOR_PATH, 'mongoengine')): - run('git co dev') - run('git pull %s dev --force' % env.user) + with cd(os.path.join(env.VENDOR_PATH, "mongoengine")): + run("git co dev") + run("git pull %s dev --force" % env.user) # run('git checkout .') # run('git checkout master') # run('get branch -D dev') # run('git checkout -b dev origin/dev') + def setup_logrotate(clear=True): if clear: - run('find /srv/newsblur/logs/*.log | xargs tee') + run("find /srv/newsblur/logs/*.log | xargs tee") with settings(warn_only=True): - sudo('find /var/log/mongodb/*.log | xargs tee') - put('config/logrotate.conf', '/etc/logrotate.d/newsblur', use_sudo=True) - put('config/logrotate.mongo.conf', '/etc/logrotate.d/mongodb', use_sudo=True) - put('config/logrotate.nginx.conf', '/etc/logrotate.d/nginx', use_sudo=True) - sudo('chown root.root /etc/logrotate.d/{newsblur,mongodb,nginx}') - sudo('chmod 644 /etc/logrotate.d/{newsblur,mongodb,nginx}') + sudo("find /var/log/mongodb/*.log | xargs tee") + put("config/logrotate.conf", "/etc/logrotate.d/newsblur", use_sudo=True) + put("config/logrotate.mongo.conf", "/etc/logrotate.d/mongodb", use_sudo=True) + put("config/logrotate.nginx.conf", "/etc/logrotate.d/nginx", use_sudo=True) + sudo("chown root.root /etc/logrotate.d/{newsblur,mongodb,nginx}") + sudo("chmod 644 /etc/logrotate.d/{newsblur,mongodb,nginx}") with settings(warn_only=True): - sudo('chown sclay.sclay /srv/newsblur/logs/*.log') - sudo('logrotate -f /etc/logrotate.d/newsblur') - sudo('logrotate -f /etc/logrotate.d/nginx') - sudo('logrotate -f /etc/logrotate.d/mongodb') + sudo("chown sclay.sclay /srv/newsblur/logs/*.log") + sudo("logrotate -f /etc/logrotate.d/newsblur") + sudo("logrotate -f /etc/logrotate.d/nginx") + sudo("logrotate -f /etc/logrotate.d/mongodb") + def setup_ulimit(): # Increase File Descriptor limits. - run('export FILEMAX=`sysctl -n fs.file-max`', pty=False) - sudo('mv /etc/security/limits.conf /etc/security/limits.conf.bak', pty=False) - sudo('touch /etc/security/limits.conf', pty=False) + run("export FILEMAX=`sysctl -n fs.file-max`", pty=False) + sudo("mv /etc/security/limits.conf /etc/security/limits.conf.bak", pty=False) + sudo("touch /etc/security/limits.conf", pty=False) run('echo "root soft nofile 100000\n" | sudo tee -a /etc/security/limits.conf', pty=False) run('echo "root hard nofile 100000\n" | sudo tee -a /etc/security/limits.conf', pty=False) run('echo "* soft nofile 100000\n" | sudo tee -a /etc/security/limits.conf', pty=False) run('echo "* hard nofile 100090\n" | sudo tee -a /etc/security/limits.conf', pty=False) run('echo "fs.file-max = 100000\n" | sudo tee -a /etc/sysctl.conf', pty=False) - sudo('sysctl -p') - sudo('ulimit -n 100000') + sudo("sysctl -p") + sudo("ulimit -n 100000") connections.connect(env.host_string) - + # run('touch /home/ubuntu/.bash_profile') # run('echo "ulimit -n $FILEMAX" >> /home/ubuntu/.bash_profile') @@ -736,67 +834,78 @@ def setup_ulimit(): # echo "net.ipv4.ip_local_port_range = 1024 65535" >> /etc/sysctl.conf # sudo chmod 644 /etc/sysctl.conf + def setup_do_monitoring(): - run('curl -sSL https://agent.digitalocean.com/install.sh | sh') - + run("curl -sSL https://agent.digitalocean.com/install.sh | sh") + + def setup_syncookies(): - sudo('echo 1 | sudo tee /proc/sys/net/ipv4/tcp_syncookies') - sudo('sudo /sbin/sysctl -w net.ipv4.tcp_syncookies=1') + sudo("echo 1 | sudo tee /proc/sys/net/ipv4/tcp_syncookies") + sudo("sudo /sbin/sysctl -w net.ipv4.tcp_syncookies=1") + def setup_sudoers(user=None): sudo('echo "%s ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/sclay' % (user or env.user)) - sudo('chmod 0440 /etc/sudoers.d/sclay') + sudo("chmod 0440 /etc/sudoers.d/sclay") + def setup_nginx(): - NGINX_VERSION = '1.19.5' + NGINX_VERSION = "1.19.5" with cd(env.VENDOR_PATH), settings(warn_only=True): sudo("groupadd nginx") sudo("useradd -g nginx -d /var/www/htdocs -s /bin/false nginx") - run('wget http://nginx.org/download/nginx-%s.tar.gz' % NGINX_VERSION) - run('tar -xzf nginx-%s.tar.gz' % NGINX_VERSION) - run('rm nginx-%s.tar.gz' % NGINX_VERSION) - with cd('nginx-%s' % NGINX_VERSION): - run('./configure --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_realip_module ') - run('make') - sudo('make install') + run("wget http://nginx.org/download/nginx-%s.tar.gz" % NGINX_VERSION) + run("tar -xzf nginx-%s.tar.gz" % NGINX_VERSION) + run("rm nginx-%s.tar.gz" % NGINX_VERSION) + with cd("nginx-%s" % NGINX_VERSION): + run( + "./configure --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_realip_module " + ) + run("make") + sudo("make install") config_nginx() + def config_nginx(): put("config/nginx.conf", "/usr/local/nginx/conf/nginx.conf", use_sudo=True) sudo("mkdir -p /usr/local/nginx/conf/sites-enabled") sudo("mkdir -p /var/log/nginx") put("config/nginx.newsblur.conf", "/usr/local/nginx/conf/sites-enabled/newsblur.conf", use_sudo=True) put("config/nginx-init", "/etc/init.d/nginx", use_sudo=True) - sudo('sed -i -e s/nginx_none/`cat /etc/hostname`/g /usr/local/nginx/conf/sites-enabled/newsblur.conf') + sudo("sed -i -e s/nginx_none/`cat /etc/hostname`/g /usr/local/nginx/conf/sites-enabled/newsblur.conf") sudo("chmod 0755 /etc/init.d/nginx") sudo("/usr/sbin/update-rc.d -f nginx defaults") sudo("/etc/init.d/nginx restart") copy_certificates() + # =============== # = Setup - App = # =============== + def setup_app_firewall(): - sudo('ufw default deny') - sudo('ufw allow ssh') # ssh - sudo('ufw allow 80') # http - sudo('ufw allow 8000') # gunicorn - sudo('ufw allow 8888') # socket.io - sudo('ufw allow 8889') # socket.io ssl - sudo('ufw allow 443') # https - sudo('ufw --force enable') + sudo("ufw default deny") + sudo("ufw allow ssh") # ssh + sudo("ufw allow 80") # http + sudo("ufw allow 8000") # gunicorn + sudo("ufw allow 8888") # socket.io + sudo("ufw allow 8889") # socket.io ssl + sudo("ufw allow 443") # https + sudo("ufw --force enable") + def remove_gunicorn(): with cd(env.VENDOR_PATH): - sudo('rm -fr gunicorn') - + sudo("rm -fr gunicorn") + + def setup_gunicorn(supervisor=True, restart=True): if supervisor: - put('config/supervisor_gunicorn.conf', '/etc/supervisor/conf.d/gunicorn.conf', use_sudo=True) - sudo('supervisorctl reread') + put("config/supervisor_gunicorn.conf", "/etc/supervisor/conf.d/gunicorn.conf", use_sudo=True) + sudo("supervisorctl reread") if restart: - sudo('supervisorctl update') + sudo("supervisorctl update") # with cd(env.VENDOR_PATH): # sudo('rm -fr gunicorn') # run('git clone git://github.com/benoitc/gunicorn.git') @@ -806,265 +915,304 @@ def setup_gunicorn(supervisor=True, restart=True): def update_gunicorn(): - with cd(os.path.join(env.VENDOR_PATH, 'gunicorn')): - run('git pull') - sudo('python setup.py develop') + with cd(os.path.join(env.VENDOR_PATH, "gunicorn")): + run("git pull") + sudo("python setup.py develop") + def setup_staging(): - run('git clone https://github.com/samuelclay/NewsBlur.git staging') - with cd('~/staging'): - run('cp ../newsblur/local_settings.py local_settings.py') - run('mkdir -p logs') - run('touch logs/newsblur.log') + run("git clone https://github.com/samuelclay/NewsBlur.git staging") + with cd("~/staging"): + run("cp ../newsblur/local_settings.py local_settings.py") + run("mkdir -p logs") + run("touch logs/newsblur.log") + def setup_node_app(): - sudo('curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -') - sudo('apt-get install -y nodejs') + sudo("curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -") + sudo("apt-get install -y nodejs") # run('curl -L https://npmjs.org/install.sh | sudo sh') # sudo('apt-get install npm') - sudo('sudo npm install -g npm') - sudo('npm install -g supervisor') - sudo('ufw allow 8888') - sudo('ufw allow 4040') + sudo("sudo npm install -g npm") + sudo("npm install -g supervisor") + sudo("ufw allow 8888") + sudo("ufw allow 4040") + def config_node(full=False): - sudo('rm -f /etc/supervisor/conf.d/gunicorn.conf') - sudo('rm -f /etc/supervisor/conf.d/node.conf') - put('config/supervisor_node_unread.conf', '/etc/supervisor/conf.d/node_unread.conf', use_sudo=True) - put('config/supervisor_node_unread_ssl.conf', '/etc/supervisor/conf.d/node_unread_ssl.conf', use_sudo=True) - put('config/supervisor_node_favicons.conf', '/etc/supervisor/conf.d/node_favicons.conf', use_sudo=True) - put('config/supervisor_node_text.conf', '/etc/supervisor/conf.d/node_text.conf', use_sudo=True) - + sudo("rm -f /etc/supervisor/conf.d/gunicorn.conf") + sudo("rm -f /etc/supervisor/conf.d/node.conf") + put("config/supervisor_node_unread.conf", "/etc/supervisor/conf.d/node_unread.conf", use_sudo=True) + put( + "config/supervisor_node_unread_ssl.conf", "/etc/supervisor/conf.d/node_unread_ssl.conf", use_sudo=True + ) + put("config/supervisor_node_favicons.conf", "/etc/supervisor/conf.d/node_favicons.conf", use_sudo=True) + put("config/supervisor_node_text.conf", "/etc/supervisor/conf.d/node_text.conf", use_sudo=True) + if full: run("rm -fr /srv/newsblur/node/node_modules") with cd(os.path.join(env.NEWSBLUR_PATH, "node")): run("npm install") - - sudo('supervisorctl reload') + + sudo("supervisorctl reload") + @parallel def copy_app_settings(): - run('rm -f %s/local_settings.py' % env.NEWSBLUR_PATH) - put(os.path.join(env.SECRETS_PATH, 'settings/app_settings.py'), - '%s/newsblur/local_settings.py' % env.NEWSBLUR_PATH) + run("rm -f %s/local_settings.py" % env.NEWSBLUR_PATH) + put( + os.path.join(env.SECRETS_PATH, "settings/app_settings.py"), + "%s/newsblur/local_settings.py" % env.NEWSBLUR_PATH, + ) run('echo "\nSERVER_NAME = \\\\"`hostname`\\\\"" >> %s/newsblur/local_settings.py' % env.NEWSBLUR_PATH) + def assemble_certificates(): - with lcd(os.path.join(env.SECRETS_PATH, 'certificates/comodo')): - local('pwd') - local('cat STAR_newsblur_com.crt EssentialSSLCA_2.crt ComodoUTNSGCCA.crt UTNAddTrustSGCCA.crt AddTrustExternalCARoot.crt > newsblur.com.crt') - + with lcd(os.path.join(env.SECRETS_PATH, "certificates/comodo")): + local("pwd") + local( + "cat STAR_newsblur_com.crt EssentialSSLCA_2.crt ComodoUTNSGCCA.crt UTNAddTrustSGCCA.crt AddTrustExternalCARoot.crt > newsblur.com.crt" + ) + + def copy_certificates(copy=False): - cert_path = os.path.join(env.NEWSBLUR_PATH, 'config/certificates') - run('mkdir -p %s' % cert_path) + cert_path = os.path.join(env.NEWSBLUR_PATH, "config/certificates") + run("mkdir -p %s" % cert_path) fullchain_path = "/etc/letsencrypt/live/newsblur.com/fullchain.pem" privkey_path = "/etc/letsencrypt/live/newsblur.com/privkey.pem" if copy: - sudo('mkdir -p %s' % os.path.dirname(fullchain_path)) - put(os.path.join(env.SECRETS_PATH, 'certificates/newsblur.com.pem'), fullchain_path, use_sudo=True) - put(os.path.join(env.SECRETS_PATH, 'certificates/newsblur.com.key'), privkey_path, use_sudo=True) - - run('ln -fs %s %s' % (fullchain_path, os.path.join(cert_path, 'newsblur.com.crt'))) - run('ln -fs %s %s' % (fullchain_path, os.path.join(cert_path, 'newsblur.com.pem'))) # For backwards compatibility with hard-coded nginx configs - run('ln -fs %s %s' % (privkey_path, os.path.join(cert_path, 'newsblur.com.key'))) - run('ln -fs %s %s' % (privkey_path, os.path.join(cert_path, 'newsblur.com.crt.key'))) # HAProxy - put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/dhparams.pem'), cert_path) - put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps_development.pem'), cert_path) + sudo("mkdir -p %s" % os.path.dirname(fullchain_path)) + put(os.path.join(env.SECRETS_PATH, "certificates/newsblur.com.pem"), fullchain_path, use_sudo=True) + put(os.path.join(env.SECRETS_PATH, "certificates/newsblur.com.key"), privkey_path, use_sudo=True) + + run("ln -fs %s %s" % (fullchain_path, os.path.join(cert_path, "newsblur.com.crt"))) + run( + "ln -fs %s %s" % (fullchain_path, os.path.join(cert_path, "newsblur.com.pem")) + ) # For backwards compatibility with hard-coded nginx configs + run("ln -fs %s %s" % (privkey_path, os.path.join(cert_path, "newsblur.com.key"))) + run("ln -fs %s %s" % (privkey_path, os.path.join(cert_path, "newsblur.com.crt.key"))) # HAProxy + put(os.path.join(env.SECRETS_PATH, "certificates/comodo/dhparams.pem"), cert_path) + put(os.path.join(env.SECRETS_PATH, "certificates/ios/aps_development.pem"), cert_path) # Export aps.cer from Apple issued certificate using Keychain Assistant # openssl x509 -in aps.cer -inform DER -outform PEM -out aps.pem - put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps.pem'), cert_path) + put(os.path.join(env.SECRETS_PATH, "certificates/ios/aps.pem"), cert_path) # Export aps.p12 from aps.cer using Keychain Assistant # openssl pkcs12 -in aps.p12 -out aps.p12.pem -nodes - put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps.p12.pem'), cert_path) - + put(os.path.join(env.SECRETS_PATH, "certificates/ios/aps.p12.pem"), cert_path) + + def setup_certbot(): - sudo('snap install --classic certbot') - sudo('snap set certbot trust-plugin-with-root=ok') - sudo('snap install certbot-dns-dnsimple') - sudo('ln -fs /snap/bin/certbot /usr/bin/certbot') - put(os.path.join(env.SECRETS_PATH, 'configs/certbot.conf'), - os.path.join(env.NEWSBLUR_PATH, 'certbot.conf')) - sudo('chmod 0600 %s' % os.path.join(env.NEWSBLUR_PATH, 'certbot.conf')) - sudo('certbot certonly -n --agree-tos ' - ' --dns-dnsimple --dns-dnsimple-credentials %s' - ' --email samuel@newsblur.com --domains newsblur.com ' - ' -d "*.newsblur.com" -d "popular.global.newsblur.com"' % - (os.path.join(env.NEWSBLUR_PATH, 'certbot.conf'))) - sudo('chmod 0755 /etc/letsencrypt/{live,archive}') - sudo('chmod 0755 /etc/letsencrypt/archive/newsblur.com/privkey1.pem') - + sudo("snap install --classic certbot") + sudo("snap set certbot trust-plugin-with-root=ok") + sudo("snap install certbot-dns-dnsimple") + sudo("ln -fs /snap/bin/certbot /usr/bin/certbot") + put( + os.path.join(env.SECRETS_PATH, "configs/certbot.conf"), + os.path.join(env.NEWSBLUR_PATH, "certbot.conf"), + ) + sudo("chmod 0600 %s" % os.path.join(env.NEWSBLUR_PATH, "certbot.conf")) + sudo( + "certbot certonly -n --agree-tos " + " --dns-dnsimple --dns-dnsimple-credentials %s" + " --email samuel@newsblur.com --domains newsblur.com " + ' -d "*.newsblur.com" -d "popular.global.newsblur.com"' + % (os.path.join(env.NEWSBLUR_PATH, "certbot.conf")) + ) + sudo("chmod 0755 /etc/letsencrypt/{live,archive}") + sudo("chmod 0755 /etc/letsencrypt/archive/newsblur.com/privkey1.pem") + + # def setup_certbot_old(): # sudo('add-apt-repository -y universe') # sudo('add-apt-repository -y ppa:certbot/certbot') # sudo('apt-get update') # sudo('apt-get install -y certbot') # sudo('apt-get install -y python3-certbot-dns-dnsimple') -# put(os.path.join(env.SECRETS_PATH, 'configs/certbot.conf'), +# put(os.path.join(env.SECRETS_PATH, 'configs/certbot.conf'), # os.path.join(env.NEWSBLUR_PATH, 'certbot.conf')) # sudo('chmod 0600 %s' % os.path.join(env.NEWSBLUR_PATH, 'certbot.conf')) # sudo('certbot certonly -n --agree-tos ' # ' --dns-dnsimple --dns-dnsimple-credentials %s' # ' --email samuel@newsblur.com --domains newsblur.com ' -# ' -d "*.newsblur.com" -d "global.popular.newsblur.com"' % +# ' -d "*.newsblur.com" -d "global.popular.newsblur.com"' % # (os.path.join(env.NEWSBLUR_PATH, 'certbot.conf'))) # sudo('chmod 0755 /etc/letsencrypt/{live,archive}') # sudo('chmod 0755 /etc/letsencrypt/archive/newsblur.com/privkey1.pem') - + + @parallel def maintenance_on(): role = role_for_host() - if role in ['work', 'search']: - sudo('supervisorctl stop all') + if role in ["work", "search"]: + sudo("supervisorctl stop all") else: - put('templates/maintenance_off.html', '%s/templates/maintenance_off.html' % env.NEWSBLUR_PATH) + put("templates/maintenance_off.html", "%s/templates/maintenance_off.html" % env.NEWSBLUR_PATH) with virtualenv(): - run('mv templates/maintenance_off.html templates/maintenance_on.html') + run("mv templates/maintenance_off.html templates/maintenance_on.html") + @parallel def maintenance_off(): role = role_for_host() - if role in ['work', 'search']: - sudo('supervisorctl start all') + if role in ["work", "search"]: + sudo("supervisorctl start all") else: with virtualenv(): - run('mv templates/maintenance_on.html templates/maintenance_off.html') - run('git checkout templates/maintenance_off.html') + run("mv templates/maintenance_on.html templates/maintenance_off.html") + run("git checkout templates/maintenance_off.html") + def setup_haproxy(debug=False): version = "2.3.3" - sudo('ufw allow 81') # nginx moved - sudo('ufw allow 1936') # haproxy stats + sudo("ufw allow 81") # nginx moved + sudo("ufw allow 1936") # haproxy stats # sudo('apt-get install -y haproxy') # sudo('apt-get remove -y haproxy') with cd(env.VENDOR_PATH): - run('wget http://www.haproxy.org/download/2.3/src/haproxy-%s.tar.gz' % version) - run('tar -xf haproxy-%s.tar.gz' % version) - with cd('haproxy-%s' % version): - run('make TARGET=linux-glibc USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1') - sudo('make install') - put('config/haproxy-init', '/etc/init.d/haproxy', use_sudo=True) - sudo('chmod u+x /etc/init.d/haproxy') - sudo('mkdir -p /etc/haproxy') + run("wget http://www.haproxy.org/download/2.3/src/haproxy-%s.tar.gz" % version) + run("tar -xf haproxy-%s.tar.gz" % version) + with cd("haproxy-%s" % version): + run("make TARGET=linux-glibc USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1") + sudo("make install") + put("config/haproxy-init", "/etc/init.d/haproxy", use_sudo=True) + sudo("chmod u+x /etc/init.d/haproxy") + sudo("mkdir -p /etc/haproxy") if debug: - put('config/debug_haproxy.conf', '/etc/haproxy/haproxy.cfg', use_sudo=True) + put("config/debug_haproxy.conf", "/etc/haproxy/haproxy.cfg", use_sudo=True) else: build_haproxy() - put(os.path.join(env.SECRETS_PATH, 'configs/haproxy.conf'), - '/etc/haproxy/haproxy.cfg', use_sudo=True) + put(os.path.join(env.SECRETS_PATH, "configs/haproxy.conf"), "/etc/haproxy/haproxy.cfg", use_sudo=True) sudo('echo "ENABLED=1" | sudo tee /etc/default/haproxy') cert_path = "%s/config/certificates" % env.NEWSBLUR_PATH - run('cat %s/newsblur.com.crt > %s/newsblur.pem' % (cert_path, cert_path)) - run('cat %s/newsblur.com.key >> %s/newsblur.pem' % (cert_path, cert_path)) - run('ln -s %s/newsblur.com.key %s/newsblur.pem.key' % (cert_path, cert_path)) - put('config/haproxy_rsyslog.conf', '/etc/rsyslog.d/49-haproxy.conf', use_sudo=True) + run("cat %s/newsblur.com.crt > %s/newsblur.pem" % (cert_path, cert_path)) + run("cat %s/newsblur.com.key >> %s/newsblur.pem" % (cert_path, cert_path)) + run("ln -s %s/newsblur.com.key %s/newsblur.pem.key" % (cert_path, cert_path)) + put("config/haproxy_rsyslog.conf", "/etc/rsyslog.d/49-haproxy.conf", use_sudo=True) # sudo('restart rsyslog') - sudo('update-rc.d -f haproxy defaults') + sudo("update-rc.d -f haproxy defaults") + + sudo("/etc/init.d/haproxy stop") + run("sleep 5") + sudo("/etc/init.d/haproxy start") - sudo('/etc/init.d/haproxy stop') - run('sleep 5') - sudo('/etc/init.d/haproxy start') def config_haproxy(debug=False): if debug: - put('config/debug_haproxy.conf', '/etc/haproxy/haproxy.cfg', use_sudo=True) + put("config/debug_haproxy.conf", "/etc/haproxy/haproxy.cfg", use_sudo=True) else: build_haproxy() - put(os.path.join(env.SECRETS_PATH, 'configs/haproxy.conf'), - '/etc/haproxy/haproxy.cfg', use_sudo=True) + put(os.path.join(env.SECRETS_PATH, "configs/haproxy.conf"), "/etc/haproxy/haproxy.cfg", use_sudo=True) - haproxy_check = run('haproxy -c -f /etc/haproxy/haproxy.cfg') + haproxy_check = run("haproxy -c -f /etc/haproxy/haproxy.cfg") if haproxy_check.return_code == 0: - sudo('/etc/init.d/haproxy reload') + sudo("/etc/init.d/haproxy reload") else: print(" !!!> Uh-oh, HAProxy config doesn't check out: %s" % haproxy_check.return_code) + def build_haproxy(): droplets = assign_digitalocean_roledefs(split=True) servers = defaultdict(list) - gunicorn_counts_servers = ['app22', 'app26'] - gunicorn_refresh_servers = ['app20', 'app21'] - maintenance_servers = ['app20'] - node_socket3_servers = ['node02', 'node03'] + gunicorn_counts_servers = ["app22", "app26"] + gunicorn_refresh_servers = ["app20", "app21"] + maintenance_servers = ["app20"] + node_socket3_servers = ["node02", "node03"] ignore_servers = [] - - for group_type in ['app', 'push', 'work', 'node_socket', 'node_socket3', 'node_favicon', 'node_text', 'www']: + + for group_type in [ + "app", + "push", + "work", + "node_socket", + "node_socket3", + "node_favicon", + "node_text", + "www", + ]: group_type_name = group_type - if 'node' in group_type: - group_type_name = 'node' + if "node" in group_type: + group_type_name = "node" for server in droplets[group_type_name]: - droplet_nums = re.findall(r'\d+', server['name']) - droplet_num = droplet_nums[0] if droplet_nums else '' + droplet_nums = re.findall(r"\d+", server["name"]) + droplet_num = droplet_nums[0] if droplet_nums else "" server_type = group_type port = 80 check_inter = 3000 - - if server['name'] in ignore_servers: - print(" ---> Ignoring %s" % server['name']) + + if server["name"] in ignore_servers: + print(" ---> Ignoring %s" % server["name"]) continue - if server['name'] in node_socket3_servers and group_type != 'node_socket3': + if server["name"] in node_socket3_servers and group_type != "node_socket3": continue - if server['name'] not in node_socket3_servers and group_type == 'node_socket3': + if server["name"] not in node_socket3_servers and group_type == "node_socket3": continue - if server_type == 'www': + if server_type == "www": port = 81 - if group_type == 'node_socket': + if group_type == "node_socket": port = 8888 - if group_type == 'node_socket3': + if group_type == "node_socket3": port = 8888 - if group_type == 'node_text': + if group_type == "node_text": port = 4040 - if group_type in ['app', 'push']: + if group_type in ["app", "push"]: port = 8000 - address = "%s:%s" % (server['address'], port) - - if server_type == 'app': - nginx_address = "%s:80" % (server['address']) - servers['nginx'].append(" server nginx%-15s %-22s check inter 3000ms" % (droplet_num, nginx_address)) - if server['name'] in maintenance_servers: - nginx_address = "%s:80" % (server['address']) - servers['maintenance'].append(" server nginx%-15s %-22s check inter 3000ms" % (droplet_num, nginx_address)) - - if server['name'] in gunicorn_counts_servers: - server_type = 'gunicorn_counts' + address = "%s:%s" % (server["address"], port) + + if server_type == "app": + nginx_address = "%s:80" % (server["address"]) + servers["nginx"].append( + " server nginx%-15s %-22s check inter 3000ms" % (droplet_num, nginx_address) + ) + if server["name"] in maintenance_servers: + nginx_address = "%s:80" % (server["address"]) + servers["maintenance"].append( + " server nginx%-15s %-22s check inter 3000ms" % (droplet_num, nginx_address) + ) + + if server["name"] in gunicorn_counts_servers: + server_type = "gunicorn_counts" check_inter = 15000 - elif server['name'] in gunicorn_refresh_servers: - server_type = 'gunicorn_refresh' + elif server["name"] in gunicorn_refresh_servers: + server_type = "gunicorn_refresh" check_inter = 30000 - + server_name = "%s%s" % (server_type, droplet_num) - servers[server_type].append(" server %-20s %-22s check inter %sms" % (server_name, address, check_inter)) - - h = open(os.path.join(env.NEWSBLUR_PATH, 'config/haproxy.conf.template'), 'r') + servers[server_type].append( + " server %-20s %-22s check inter %sms" % (server_name, address, check_inter) + ) + + h = open(os.path.join(env.NEWSBLUR_PATH, "config/haproxy.conf.template"), "r") haproxy_template = h.read() for sub, server_list in list(servers.items()): - sorted_servers = '\n'.join(sorted(server_list)) + sorted_servers = "\n".join(sorted(server_list)) haproxy_template = haproxy_template.replace("{{ %s }}" % sub, sorted_servers) - f = open(os.path.join(env.SECRETS_PATH, 'configs/haproxy.conf'), 'w') + f = open(os.path.join(env.SECRETS_PATH, "configs/haproxy.conf"), "w") f.write(haproxy_template) f.close() + def upgrade_django(role=None): if not role: role = role_for_host() with virtualenv(), settings(warn_only=True): - sudo('sudo dpkg --configure -a') + sudo("sudo dpkg --configure -a") setup_supervisor() pull() - run('git co django1.11') + run("git co django1.11") if role == "task": - sudo('supervisorctl stop celery') - run('./utils/kill_celery.sh') + sudo("supervisorctl stop celery") + run("./utils/kill_celery.sh") copy_task_settings() enable_celery_supervisor(update=False) elif role == "work": copy_app_settings() enable_celerybeat() elif role == "web" or role == "app": - sudo('supervisorctl stop gunicorn') - run('./utils/kill_gunicorn.sh') + sudo("supervisorctl stop gunicorn") + run("./utils/kill_gunicorn.sh") copy_app_settings() setup_gunicorn(restart=False) elif role == "node": @@ -1078,90 +1226,97 @@ def upgrade_django(role=None): # sudo('reboot') + def clean(): with virtualenv(), settings(warn_only=True): run('find . -name "*.pyc" -exec rm -f {} \;') - + + def downgrade_django(role=None): with virtualenv(), settings(warn_only=True): pull() - run('git co master') + run("git co master") pip() - run('pip uninstall -y django-paypal') + run("pip uninstall -y django-paypal") if role == "task": copy_task_settings() enable_celery_supervisor() else: copy_app_settings() deploy() - + + def vendorize_paypal(): with virtualenv(), settings(warn_only=True): - run('pip uninstall -y django-paypal') + run("pip uninstall -y django-paypal") + def upgrade_pil(): with virtualenv(): pull() - run('pip install --upgrade pillow') + run("pip install --upgrade pillow") # celery_stop() - sudo('apt-get remove -y python-imaging') - sudo('supervisorctl reload') + sudo("apt-get remove -y python-imaging") + sudo("supervisorctl reload") # kill() + def downgrade_pil(): with virtualenv(): - sudo('apt-get install -y python-imaging') - sudo('rm -fr /usr/local/lib/python2.7/dist-packages/Pillow*') + sudo("apt-get install -y python-imaging") + sudo("rm -fr /usr/local/lib/python2.7/dist-packages/Pillow*") pull() - sudo('supervisorctl reload') + sudo("supervisorctl reload") # kill() + def setup_db_monitor(): pull() with virtualenv(): - sudo('apt-get install -y libpq-dev python2.7-dev') - run('pip install -r flask/requirements.txt') - put('flask/supervisor_db_monitor.conf', '/etc/supervisor/conf.d/db_monitor.conf', use_sudo=True) - sudo('supervisorctl reread') - sudo('supervisorctl update') - + sudo("apt-get install -y libpq-dev python2.7-dev") + run("pip install -r flask/requirements.txt") + put("flask/supervisor_db_monitor.conf", "/etc/supervisor/conf.d/db_monitor.conf", use_sudo=True) + sudo("supervisorctl reread") + sudo("supervisorctl update") + + # ============== # = Setup - DB = # ============== + @parallel def setup_db_firewall(): ports = [ - 5432, # PostgreSQL + 5432, # PostgreSQL 27017, # MongoDB 28017, # MongoDB web 27019, # MongoDB config - 6379, # Redis + 6379, # Redis # 11211, # Memcached - 3060, # Node original page server - 9200, # Elasticsearch - 5000, # DB Monitor + 3060, # Node original page server + 9200, # Elasticsearch + 5000, # DB Monitor ] - sudo('ufw --force reset') - sudo('ufw default deny') - sudo('ufw allow ssh') - sudo('ufw allow 80') - sudo('ufw allow 443') + sudo("ufw --force reset") + sudo("ufw default deny") + sudo("ufw allow ssh") + sudo("ufw allow 80") + sudo("ufw allow 443") # DigitalOcean - for ip in set(env.roledefs['app'] + - env.roledefs['db'] + - env.roledefs['debug'] + - env.roledefs['task'] + - env.roledefs['work'] + - env.roledefs['push'] + - env.roledefs['www'] + - env.roledefs['search'] + - env.roledefs['node']): - sudo('ufw allow proto tcp from %s to any port %s' % ( - ip, - ','.join(map(str, ports)) - )) + for ip in set( + env.roledefs["app"] + + env.roledefs["db"] + + env.roledefs["debug"] + + env.roledefs["task"] + + env.roledefs["work"] + + env.roledefs["push"] + + env.roledefs["www"] + + env.roledefs["search"] + + env.roledefs["node"] + ): + sudo("ufw allow proto tcp from %s to any port %s" % (ip, ",".join(map(str, ports)))) # EC2 # for host in set(env.roledefs['ec2task']): @@ -1171,67 +1326,77 @@ def setup_db_firewall(): # ','.join(map(str, ports)) # )) - sudo('ufw --force enable') + sudo("ufw --force enable") + def setup_rabbitmq(): sudo('echo "deb http://www.rabbitmq.com/debian/ testing main" | sudo tee -a /etc/apt/sources.list') - run('wget http://www.rabbitmq.com/rabbitmq-signing-key-public.asc') - sudo('apt-key add rabbitmq-signing-key-public.asc') - run('rm rabbitmq-signing-key-public.asc') - sudo('apt-get update') - sudo('apt-get install -y rabbitmq-server') - sudo('rabbitmqctl add_user newsblur newsblur') - sudo('rabbitmqctl add_vhost newsblurvhost') + run("wget http://www.rabbitmq.com/rabbitmq-signing-key-public.asc") + sudo("apt-key add rabbitmq-signing-key-public.asc") + run("rm rabbitmq-signing-key-public.asc") + sudo("apt-get update") + sudo("apt-get install -y rabbitmq-server") + sudo("rabbitmqctl add_user newsblur newsblur") + sudo("rabbitmqctl add_vhost newsblurvhost") sudo('rabbitmqctl set_permissions -p newsblurvhost newsblur ".*" ".*" ".*"') + # def setup_memcached(): # sudo('apt-get -y install memcached') + def setup_postgres(standby=False): shmmax = 17818362112 hugepages = 9000 - sudo('echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee /etc/apt/sources.list.d/pgdg.list') - sudo('wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -') - sudo('apt update') - sudo('apt install -y postgresql-13') - put('config/postgresql-13.conf', '/etc/postgresql/13/main/postgresql.conf', use_sudo=True) - put('config/postgres_hba-13.conf', '/etc/postgresql/13/main/pg_hba.conf', use_sudo=True) - sudo('mkdir -p /var/lib/postgresql/13/archive') - sudo('chown -R postgres.postgres /etc/postgresql/13/main') - sudo('chown -R postgres.postgres /var/lib/postgresql/13/main') - sudo('chown -R postgres.postgres /var/lib/postgresql/13/archive') + sudo( + 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee /etc/apt/sources.list.d/pgdg.list' + ) + sudo("wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -") + sudo("apt update") + sudo("apt install -y postgresql-13") + put("config/postgresql-13.conf", "/etc/postgresql/13/main/postgresql.conf", use_sudo=True) + put("config/postgres_hba-13.conf", "/etc/postgresql/13/main/pg_hba.conf", use_sudo=True) + sudo("mkdir -p /var/lib/postgresql/13/archive") + sudo("chown -R postgres.postgres /etc/postgresql/13/main") + sudo("chown -R postgres.postgres /var/lib/postgresql/13/main") + sudo("chown -R postgres.postgres /var/lib/postgresql/13/archive") sudo('echo "%s" | sudo tee /proc/sys/kernel/shmmax' % shmmax) sudo('echo "\nkernel.shmmax = %s" | sudo tee -a /etc/sysctl.conf' % shmmax) sudo('echo "\nvm.nr_hugepages = %s\n" | sudo tee -a /etc/sysctl.conf' % hugepages) run('echo "ulimit -n 100000" > postgresql.defaults') - sudo('mv postgresql.defaults /etc/default/postgresql') - sudo('sysctl -p') - sudo('rm -f /lib/systemd/system/postgresql.service') # Ubuntu 16 has wrong default - sudo('systemctl daemon-reload') - sudo('systemctl enable postgresql') + sudo("mv postgresql.defaults /etc/default/postgresql") + sudo("sysctl -p") + sudo("rm -f /lib/systemd/system/postgresql.service") # Ubuntu 16 has wrong default + sudo("systemctl daemon-reload") + sudo("systemctl enable postgresql") if standby: - put('config/postgresql_recovery.conf', '/var/lib/postgresql/13/recovery.conf', use_sudo=True) - sudo('chown -R postgres.postgres /var/lib/postgresql/13/recovery.conf') + put("config/postgresql_recovery.conf", "/var/lib/postgresql/13/recovery.conf", use_sudo=True) + sudo("chown -R postgres.postgres /var/lib/postgresql/13/recovery.conf") + + sudo("/etc/init.d/postgresql stop") + sudo("/etc/init.d/postgresql start") - sudo('/etc/init.d/postgresql stop') - sudo('/etc/init.d/postgresql start') def config_postgres(standby=False): - put('config/postgresql-13.conf', '/etc/postgresql/13/main/postgresql.conf', use_sudo=True) - put('config/postgres_hba.conf', '/etc/postgresql/13/main/pg_hba.conf', use_sudo=True) - sudo('chown postgres.postgres /etc/postgresql/13/main/postgresql.conf') + put("config/postgresql-13.conf", "/etc/postgresql/13/main/postgresql.conf", use_sudo=True) + put("config/postgres_hba.conf", "/etc/postgresql/13/main/pg_hba.conf", use_sudo=True) + sudo("chown postgres.postgres /etc/postgresql/13/main/postgresql.conf") run('echo "ulimit -n 100000" > postgresql.defaults') - sudo('mv postgresql.defaults /etc/default/postgresql') - - sudo('/etc/init.d/postgresql reload 13') + sudo("mv postgresql.defaults /etc/default/postgresql") + + sudo("/etc/init.d/postgresql reload 13") + def upgrade_postgres(): - sudo('su postgres -c "/usr/lib/postgresql/10/bin/pg_upgrade -b /usr/lib/postgresql/9.4/bin -B /usr/lib/postgresql/10/bin -d /var/lib/postgresql/9.4/main -D /var/lib/postgresql/10/main"') - -def copy_postgres_to_standby(master='db01'): + sudo( + 'su postgres -c "/usr/lib/postgresql/10/bin/pg_upgrade -b /usr/lib/postgresql/9.4/bin -B /usr/lib/postgresql/10/bin -d /var/lib/postgresql/9.4/main -D /var/lib/postgresql/10/main"' + ) + + +def copy_postgres_to_standby(master="db01"): # http://www.rassoc.com/gregr/weblog/2013/02/16/zero-to-postgresql-streaming-replication-in-10-mins/ - + # Make sure you can ssh from master to slave and back with the postgres user account. # Need to give postgres accounts keys in authroized_keys. @@ -1240,259 +1405,319 @@ def copy_postgres_to_standby(master='db01'): # new: sudo su postgres; ssh db_pgsql # old: sudo su postgres; ssh new # old: sudo su postgres -c "psql -c \"SELECT pg_start_backup('label', true)\"" - sudo('systemctl stop postgresql') - sudo('mkdir -p /var/lib/postgresql/9.4/archive') - sudo('chown postgres.postgres /var/lib/postgresql/9.4/archive') + sudo("systemctl stop postgresql") + sudo("mkdir -p /var/lib/postgresql/9.4/archive") + sudo("chown postgres.postgres /var/lib/postgresql/9.4/archive") with settings(warn_only=True): - sudo('su postgres -c "rsync -Pav -e \'ssh -i ~postgres/.ssh/newsblur.key\' --stats --progress postgres@%s:/var/lib/postgresql/9.4/main /var/lib/postgresql/9.4/ --exclude postmaster.pid"' % master) - put('config/postgresql_recovery.conf', '/var/lib/postgresql/9.4/main/recovery.conf', use_sudo=True) - sudo('systemctl start postgresql') + sudo( + "su postgres -c \"rsync -Pav -e 'ssh -i ~postgres/.ssh/newsblur.key' --stats --progress postgres@%s:/var/lib/postgresql/9.4/main /var/lib/postgresql/9.4/ --exclude postmaster.pid\"" + % master + ) + put("config/postgresql_recovery.conf", "/var/lib/postgresql/9.4/main/recovery.conf", use_sudo=True) + sudo("systemctl start postgresql") # old: sudo su postgres -c "psql -c \"SELECT pg_stop_backup()\"" - + # Don't forget to add 'setup_postgres_backups' to new - + def disable_thp(): - put('config/disable_transparent_hugepages.sh', '/etc/init.d/disable-transparent-hugepages', use_sudo=True) - sudo('chmod 755 /etc/init.d/disable-transparent-hugepages') - sudo('update-rc.d disable-transparent-hugepages defaults') - + put("config/disable_transparent_hugepages.sh", "/etc/init.d/disable-transparent-hugepages", use_sudo=True) + sudo("chmod 755 /etc/init.d/disable-transparent-hugepages") + sudo("update-rc.d disable-transparent-hugepages defaults") + + def setup_mongo(): MONGODB_VERSION = "3.4.24" pull() disable_thp() - sudo('systemctl enable rc-local.service') # Enable rc.local - sudo('echo "#!/bin/sh -e\n\nif test -f /sys/kernel/mm/transparent_hugepage/enabled; then\n\ + sudo("systemctl enable rc-local.service") # Enable rc.local + sudo( + 'echo "#!/bin/sh -e\n\nif test -f /sys/kernel/mm/transparent_hugepage/enabled; then\n\ echo never > /sys/kernel/mm/transparent_hugepage/enabled\n\ fi\n\ if test -f /sys/kernel/mm/transparent_hugepage/defrag; then\n\ echo never > /sys/kernel/mm/transparent_hugepage/defrag\n\ fi\n\n\ - exit 0" | sudo tee /etc/rc.local') - sudo('curl -fsSL https://www.mongodb.org/static/pgp/server-3.4.asc | sudo apt-key add -') + exit 0" | sudo tee /etc/rc.local' + ) + sudo("curl -fsSL https://www.mongodb.org/static/pgp/server-3.4.asc | sudo apt-key add -") # sudo('echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list') # sudo('echo "\ndeb http://downloads-distro.mongodb.org/repo/debian-sysvinit dist 10gen" | sudo tee -a /etc/apt/sources.list') # sudo('echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list') - sudo('echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list') - sudo('apt-get update') - sudo('apt-get install -y mongodb-org=%s mongodb-org-server=%s mongodb-org-shell=%s mongodb-org-mongos=%s mongodb-org-tools=%s' % - (MONGODB_VERSION, MONGODB_VERSION, MONGODB_VERSION, MONGODB_VERSION, MONGODB_VERSION)) - put('config/mongodb.%s.conf' % ('prod' if env.user != 'ubuntu' else 'ec2'), - '/etc/mongodb.conf', use_sudo=True) - put('config/mongodb.service', '/etc/systemd/system/mongodb.service', use_sudo=True) + sudo( + 'echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list' + ) + sudo("apt-get update") + sudo( + "apt-get install -y mongodb-org=%s mongodb-org-server=%s mongodb-org-shell=%s mongodb-org-mongos=%s mongodb-org-tools=%s" + % (MONGODB_VERSION, MONGODB_VERSION, MONGODB_VERSION, MONGODB_VERSION, MONGODB_VERSION) + ) + put( + "config/mongodb.%s.conf" % ("prod" if env.user != "ubuntu" else "ec2"), + "/etc/mongodb.conf", + use_sudo=True, + ) + put("config/mongodb.service", "/etc/systemd/system/mongodb.service", use_sudo=True) run('echo "ulimit -n 100000" > mongodb.defaults') - sudo('mv mongodb.defaults /etc/default/mongod') - sudo('mkdir -p /var/log/mongodb') - sudo('chown mongodb /var/log/mongodb') - put('config/logrotate.mongo.conf', '/etc/logrotate.d/mongod', use_sudo=True) - sudo('systemctl enable mongodb') - + sudo("mv mongodb.defaults /etc/default/mongod") + sudo("mkdir -p /var/log/mongodb") + sudo("chown mongodb /var/log/mongodb") + put("config/logrotate.mongo.conf", "/etc/logrotate.d/mongod", use_sudo=True) + sudo("systemctl enable mongodb") + # Reclaim 5% disk space used for root logs. Set to 1%. with settings(warn_only=True): - sudo('tune2fs -m 1 /dev/vda1') + sudo("tune2fs -m 1 /dev/vda1") + def setup_mongo_configsvr(): - sudo('mkdir -p /var/lib/mongodb_configsvr') - sudo('chown mongodb.mongodb /var/lib/mongodb_configsvr') - put('config/mongodb.configsvr.conf', '/etc/mongodb.configsvr.conf', use_sudo=True) - put('config/mongodb.configsvr-init', '/etc/init.d/mongodb-configsvr', use_sudo=True) - sudo('chmod u+x /etc/init.d/mongodb-configsvr') + sudo("mkdir -p /var/lib/mongodb_configsvr") + sudo("chown mongodb.mongodb /var/lib/mongodb_configsvr") + put("config/mongodb.configsvr.conf", "/etc/mongodb.configsvr.conf", use_sudo=True) + put("config/mongodb.configsvr-init", "/etc/init.d/mongodb-configsvr", use_sudo=True) + sudo("chmod u+x /etc/init.d/mongodb-configsvr") run('echo "ulimit -n 100000" > mongodb_configsvr.defaults') - sudo('mv mongodb_configsvr.defaults /etc/default/mongodb_configsvr') - sudo('update-rc.d -f mongodb-configsvr defaults') - sudo('/etc/init.d/mongodb-configsvr start') + sudo("mv mongodb_configsvr.defaults /etc/default/mongodb_configsvr") + sudo("update-rc.d -f mongodb-configsvr defaults") + sudo("/etc/init.d/mongodb-configsvr start") + def setup_mongo_mongos(): - put('config/mongodb.mongos.conf', '/etc/mongodb.mongos.conf', use_sudo=True) - put('config/mongodb.mongos-init', '/etc/init.d/mongodb-mongos', use_sudo=True) - sudo('chmod u+x /etc/init.d/mongodb-mongos') + put("config/mongodb.mongos.conf", "/etc/mongodb.mongos.conf", use_sudo=True) + put("config/mongodb.mongos-init", "/etc/init.d/mongodb-mongos", use_sudo=True) + sudo("chmod u+x /etc/init.d/mongodb-mongos") run('echo "ulimit -n 100000" > mongodb_mongos.defaults') - sudo('mv mongodb_mongos.defaults /etc/default/mongodb_mongos') - sudo('update-rc.d -f mongodb-mongos defaults') - sudo('/etc/init.d/mongodb-mongos restart') + sudo("mv mongodb_mongos.defaults /etc/default/mongodb_mongos") + sudo("update-rc.d -f mongodb-mongos defaults") + sudo("/etc/init.d/mongodb-mongos restart") + def setup_mongo_mms(): pull() - sudo('rm -f /etc/supervisor/conf.d/mongomms.conf') - sudo('supervisorctl reread') - sudo('supervisorctl update') + sudo("rm -f /etc/supervisor/conf.d/mongomms.conf") + sudo("supervisorctl reread") + sudo("supervisorctl update") with cd(env.VENDOR_PATH): - sudo('apt-get remove -y mongodb-mms-monitoring-agent') - run('curl -OL https://mms.mongodb.com/download/agent/monitoring/mongodb-mms-monitoring-agent_2.2.0.70-1_amd64.deb') - sudo('dpkg -i mongodb-mms-monitoring-agent_2.2.0.70-1_amd64.deb') - run('rm mongodb-mms-monitoring-agent_2.2.0.70-1_amd64.deb') - put(os.path.join(env.SECRETS_PATH, 'settings/mongo_mms_config.txt'), - 'mongo_mms_config.txt') - sudo("echo \"\n\" | sudo tee -a /etc/mongodb-mms/monitoring-agent.config") - sudo('cat mongo_mms_config.txt | sudo tee -a /etc/mongodb-mms/monitoring-agent.config') - sudo('start mongodb-mms-monitoring-agent') + sudo("apt-get remove -y mongodb-mms-monitoring-agent") + run( + "curl -OL https://mms.mongodb.com/download/agent/monitoring/mongodb-mms-monitoring-agent_2.2.0.70-1_amd64.deb" + ) + sudo("dpkg -i mongodb-mms-monitoring-agent_2.2.0.70-1_amd64.deb") + run("rm mongodb-mms-monitoring-agent_2.2.0.70-1_amd64.deb") + put(os.path.join(env.SECRETS_PATH, "settings/mongo_mms_config.txt"), "mongo_mms_config.txt") + sudo('echo "\n" | sudo tee -a /etc/mongodb-mms/monitoring-agent.config') + sudo("cat mongo_mms_config.txt | sudo tee -a /etc/mongodb-mms/monitoring-agent.config") + sudo("start mongodb-mms-monitoring-agent") + def setup_redis(slave=False): - redis_version = '3.2.6' + redis_version = "3.2.6" with cd(env.VENDOR_PATH): - run('wget http://download.redis.io/releases/redis-%s.tar.gz' % redis_version) - run('tar -xzf redis-%s.tar.gz' % redis_version) - run('rm redis-%s.tar.gz' % redis_version) - with cd(os.path.join(env.VENDOR_PATH, 'redis-%s' % redis_version)): - sudo('make install') - put('config/redis-init', '/etc/init.d/redis', use_sudo=True) - sudo('chmod u+x /etc/init.d/redis') - put('config/redis.conf', '/etc/redis.conf', use_sudo=True) + run("wget http://download.redis.io/releases/redis-%s.tar.gz" % redis_version) + run("tar -xzf redis-%s.tar.gz" % redis_version) + run("rm redis-%s.tar.gz" % redis_version) + with cd(os.path.join(env.VENDOR_PATH, "redis-%s" % redis_version)): + sudo("make install") + put("config/redis-init", "/etc/init.d/redis", use_sudo=True) + sudo("chmod u+x /etc/init.d/redis") + put("config/redis.conf", "/etc/redis.conf", use_sudo=True) if slave: - put('config/redis_slave.conf', '/etc/redis_server.conf', use_sudo=True) + put("config/redis_slave.conf", "/etc/redis_server.conf", use_sudo=True) else: - put('config/redis_master.conf', '/etc/redis_server.conf', use_sudo=True) + put("config/redis_master.conf", "/etc/redis_server.conf", use_sudo=True) # sudo('chmod 666 /proc/sys/vm/overcommit_memory', pty=False) # run('echo "1" > /proc/sys/vm/overcommit_memory', pty=False) # sudo('chmod 644 /proc/sys/vm/overcommit_memory', pty=False) disable_thp() - sudo('systemctl enable rc-local.service') # Enable rc.local - sudo('echo "#!/bin/sh -e\n\nif test -f /sys/kernel/mm/transparent_hugepage/enabled; then\n\ + sudo("systemctl enable rc-local.service") # Enable rc.local + sudo( + 'echo "#!/bin/sh -e\n\nif test -f /sys/kernel/mm/transparent_hugepage/enabled; then\n\ echo never > /sys/kernel/mm/transparent_hugepage/enabled\n\ fi\n\ if test -f /sys/kernel/mm/transparent_hugepage/defrag; then\n\ echo never > /sys/kernel/mm/transparent_hugepage/defrag\n\ fi\n\n\ - exit 0" | sudo tee /etc/rc.local') + exit 0" | sudo tee /etc/rc.local' + ) sudo("echo 1 | sudo tee /proc/sys/vm/overcommit_memory") sudo('echo "vm.overcommit_memory = 1" | sudo tee -a /etc/sysctl.conf') sudo("sysctl vm.overcommit_memory=1") - put('config/redis_rclocal.txt', '/etc/rc.local', use_sudo=True) + put("config/redis_rclocal.txt", "/etc/rc.local", use_sudo=True) sudo("chown root.root /etc/rc.local") sudo("chmod a+x /etc/rc.local") sudo('echo "never" | sudo tee /sys/kernel/mm/transparent_hugepage/enabled') run('echo "\nnet.core.somaxconn=65535\n" | sudo tee -a /etc/sysctl.conf', pty=False) - sudo('mkdir -p /var/lib/redis') - sudo('update-rc.d redis defaults') - sudo('/etc/init.d/redis stop') - sudo('/etc/init.d/redis start') + sudo("mkdir -p /var/lib/redis") + sudo("update-rc.d redis defaults") + sudo("/etc/init.d/redis stop") + sudo("/etc/init.d/redis start") setup_syncookies() config_monit_redis() - + + def setup_munin(): - sudo('apt-get update') - sudo('apt-get install -y munin munin-node munin-plugins-extra spawn-fcgi') - put('config/munin.conf', '/etc/munin/munin.conf', use_sudo=True) # Only use on main munin - put('config/spawn_fcgi_munin_graph.conf', '/etc/init.d/spawn_fcgi_munin_graph', use_sudo=True) - put('config/spawn_fcgi_munin_html.conf', '/etc/init.d/spawn_fcgi_munin_html', use_sudo=True) - sudo('chmod u+x /etc/init.d/spawn_fcgi_munin_graph') - sudo('chmod u+x /etc/init.d/spawn_fcgi_munin_html') + sudo("apt-get update") + sudo("apt-get install -y munin munin-node munin-plugins-extra spawn-fcgi") + put("config/munin.conf", "/etc/munin/munin.conf", use_sudo=True) # Only use on main munin + put("config/spawn_fcgi_munin_graph.conf", "/etc/init.d/spawn_fcgi_munin_graph", use_sudo=True) + put("config/spawn_fcgi_munin_html.conf", "/etc/init.d/spawn_fcgi_munin_html", use_sudo=True) + sudo("chmod u+x /etc/init.d/spawn_fcgi_munin_graph") + sudo("chmod u+x /etc/init.d/spawn_fcgi_munin_html") with settings(warn_only=True): - sudo('chown nginx.www-data /var/log/munin/munin-cgi*') - sudo('chown nginx.www-data /usr/lib/cgi-bin/munin-cgi*') - sudo('chown nginx.www-data /usr/lib/munin/cgi/munin-cgi*') + sudo("chown nginx.www-data /var/log/munin/munin-cgi*") + sudo("chown nginx.www-data /usr/lib/cgi-bin/munin-cgi*") + sudo("chown nginx.www-data /usr/lib/munin/cgi/munin-cgi*") with settings(warn_only=True): - sudo('/etc/init.d/spawn_fcgi_munin_graph stop') - sudo('/etc/init.d/spawn_fcgi_munin_graph start') - sudo('update-rc.d spawn_fcgi_munin_graph defaults') - sudo('/etc/init.d/spawn_fcgi_munin_html stop') - sudo('/etc/init.d/spawn_fcgi_munin_html start') - sudo('update-rc.d spawn_fcgi_munin_html defaults') - sudo('/etc/init.d/munin-node stop') + sudo("/etc/init.d/spawn_fcgi_munin_graph stop") + sudo("/etc/init.d/spawn_fcgi_munin_graph start") + sudo("update-rc.d spawn_fcgi_munin_graph defaults") + sudo("/etc/init.d/spawn_fcgi_munin_html stop") + sudo("/etc/init.d/spawn_fcgi_munin_html start") + sudo("update-rc.d spawn_fcgi_munin_html defaults") + sudo("/etc/init.d/munin-node stop") time.sleep(2) - sudo('/etc/init.d/munin-node start') + sudo("/etc/init.d/munin-node start") with settings(warn_only=True): - sudo('chown nginx.www-data /var/log/munin/munin-cgi*') - sudo('chown nginx.www-data /usr/lib/cgi-bin/munin-cgi*') - sudo('chown nginx.www-data /usr/lib/munin/cgi/munin-cgi*') - sudo('chmod a+rw /var/log/munin/*') + sudo("chown nginx.www-data /var/log/munin/munin-cgi*") + sudo("chown nginx.www-data /usr/lib/cgi-bin/munin-cgi*") + sudo("chown nginx.www-data /usr/lib/munin/cgi/munin-cgi*") + sudo("chmod a+rw /var/log/munin/*") with settings(warn_only=True): - sudo('/etc/init.d/spawn_fcgi_munin_graph start') - sudo('/etc/init.d/spawn_fcgi_munin_html start') + sudo("/etc/init.d/spawn_fcgi_munin_graph start") + sudo("/etc/init.d/spawn_fcgi_munin_html start") + def copy_munin_data(from_server): - put(os.path.join(env.SECRETS_PATH, 'keys/newsblur.key'), '~/.ssh/newsblur.key') - put(os.path.join(env.SECRETS_PATH, 'keys/newsblur.key.pub'), '~/.ssh/newsblur.key.pub') - run('chmod 600 ~/.ssh/newsblur*') + put(os.path.join(env.SECRETS_PATH, "keys/newsblur.key"), "~/.ssh/newsblur.key") + put(os.path.join(env.SECRETS_PATH, "keys/newsblur.key.pub"), "~/.ssh/newsblur.key.pub") + run("chmod 600 ~/.ssh/newsblur*") # put("config/munin.nginx.conf", "/usr/local/nginx/conf/sites-enabled/munin.conf", use_sudo=True) - sudo('/etc/init.d/nginx reload') + sudo("/etc/init.d/nginx reload") - run("rsync -az -e \"ssh -i /home/sclay/.ssh/newsblur.key\" --stats --progress %s:/var/lib/munin/ /srv/munin" % from_server) - sudo('rm -fr /var/lib/bak-munin') + run( + 'rsync -az -e "ssh -i /home/sclay/.ssh/newsblur.key" --stats --progress %s:/var/lib/munin/ /srv/munin' + % from_server + ) + sudo("rm -fr /var/lib/bak-munin") sudo("mv /var/lib/munin /var/lib/bak-munin") sudo("mv /srv/munin /var/lib/") sudo("chown munin.munin -R /var/lib/munin") - run("sudo rsync -az -e \"ssh -i /home/sclay/.ssh/newsblur.key\" --stats --progress %s:/etc/munin/ /srv/munin-etc" % from_server) - sudo('rm -fr /etc/munin') + run( + 'sudo rsync -az -e "ssh -i /home/sclay/.ssh/newsblur.key" --stats --progress %s:/etc/munin/ /srv/munin-etc' + % from_server + ) + sudo("rm -fr /etc/munin") sudo("mv /srv/munin-etc /etc/munin") sudo("chown munin.munin -R /etc/munin") - run("sudo rsync -az -e \"ssh -i /home/sclay/.ssh/newsblur.key\" --stats --progress %s:/var/cache/munin/www/ /srv/munin-www" % from_server) - sudo('rm -fr /var/cache/munin/www') + run( + 'sudo rsync -az -e "ssh -i /home/sclay/.ssh/newsblur.key" --stats --progress %s:/var/cache/munin/www/ /srv/munin-www' + % from_server + ) + sudo("rm -fr /var/cache/munin/www") sudo("mv /srv/munin-www /var/cache/munin/www") sudo("chown munin.munin -R /var/cache/munin/www") sudo("/etc/init.d/munin restart") sudo("/etc/init.d/munin-node restart") - + def setup_db_munin(): - sudo('rm -f /etc/munin/plugins/mongo*') - sudo('rm -f /etc/munin/plugins/pg_*') - sudo('rm -f /etc/munin/plugins/redis_*') - sudo('cp -frs %s/config/munin/mongo* /etc/munin/plugins/' % env.NEWSBLUR_PATH) - sudo('cp -frs %s/config/munin/pg_* /etc/munin/plugins/' % env.NEWSBLUR_PATH) - sudo('cp -frs %s/config/munin/redis_* /etc/munin/plugins/' % env.NEWSBLUR_PATH) - sudo('/etc/init.d/munin-node stop') + sudo("rm -f /etc/munin/plugins/mongo*") + sudo("rm -f /etc/munin/plugins/pg_*") + sudo("rm -f /etc/munin/plugins/redis_*") + sudo("cp -frs %s/config/munin/mongo* /etc/munin/plugins/" % env.NEWSBLUR_PATH) + sudo("cp -frs %s/config/munin/pg_* /etc/munin/plugins/" % env.NEWSBLUR_PATH) + sudo("cp -frs %s/config/munin/redis_* /etc/munin/plugins/" % env.NEWSBLUR_PATH) + sudo("/etc/init.d/munin-node stop") time.sleep(2) - sudo('/etc/init.d/munin-node start') + sudo("/etc/init.d/munin-node start") def enable_celerybeat(): with virtualenv(): - run('mkdir -p data') - put('config/supervisor_celerybeat.conf', '/etc/supervisor/conf.d/celerybeat.conf', use_sudo=True) - put('config/supervisor_celeryd_work_queue.conf', '/etc/supervisor/conf.d/celeryd_work_queue.conf', use_sudo=True) - put('config/supervisor_celeryd_beat.conf', '/etc/supervisor/conf.d/celeryd_beat.conf', use_sudo=True) - put('config/supervisor_celeryd_beat_feeds.conf', '/etc/supervisor/conf.d/celeryd_beat_feeds.conf', use_sudo=True) - sudo('supervisorctl reread') - sudo('supervisorctl update') + run("mkdir -p data") + put("config/supervisor_celerybeat.conf", "/etc/supervisor/conf.d/celerybeat.conf", use_sudo=True) + put( + "config/supervisor_celeryd_work_queue.conf", + "/etc/supervisor/conf.d/celeryd_work_queue.conf", + use_sudo=True, + ) + put("config/supervisor_celeryd_beat.conf", "/etc/supervisor/conf.d/celeryd_beat.conf", use_sudo=True) + put( + "config/supervisor_celeryd_beat_feeds.conf", + "/etc/supervisor/conf.d/celeryd_beat_feeds.conf", + use_sudo=True, + ) + sudo("supervisorctl reread") + sudo("supervisorctl update") + def setup_db_mdadm(): - sudo('apt-get -y install xfsprogs mdadm') - sudo('yes | mdadm --create /dev/md0 --level=0 -c256 --raid-devices=4 /dev/xvdf /dev/xvdg /dev/xvdh /dev/xvdi') - sudo('mkfs.xfs /dev/md0') - sudo('mkdir -p /srv/db') - sudo('mount -t xfs -o rw,nobarrier,noatime,nodiratime /dev/md0 /srv/db') - sudo('mkdir -p /srv/db/mongodb') - sudo('chown mongodb.mongodb /srv/db/mongodb') + sudo("apt-get -y install xfsprogs mdadm") + sudo( + "yes | mdadm --create /dev/md0 --level=0 -c256 --raid-devices=4 /dev/xvdf /dev/xvdg /dev/xvdh /dev/xvdi" + ) + sudo("mkfs.xfs /dev/md0") + sudo("mkdir -p /srv/db") + sudo("mount -t xfs -o rw,nobarrier,noatime,nodiratime /dev/md0 /srv/db") + sudo("mkdir -p /srv/db/mongodb") + sudo("chown mongodb.mongodb /srv/db/mongodb") sudo("echo 'DEVICE /dev/xvdf /dev/xvdg /dev/xvdh /dev/xvdi' | sudo tee -a /etc/mdadm/mdadm.conf") sudo("mdadm --examine --scan | sudo tee -a /etc/mdadm/mdadm.conf") - sudo("echo '/dev/md0 /srv/db xfs rw,nobarrier,noatime,nodiratime,noauto 0 0' | sudo tee -a /etc/fstab") + sudo( + "echo '/dev/md0 /srv/db xfs rw,nobarrier,noatime,nodiratime,noauto 0 0' | sudo tee -a /etc/fstab" + ) sudo("sudo update-initramfs -u -v -k `uname -r`") + def setup_original_page_server(): setup_node_app() - sudo('mkdir -p /srv/originals') - sudo('chown %s.%s -R /srv/originals' % (env.user, env.user)) # We assume that the group is the same name as the user. It's common on linux + sudo("mkdir -p /srv/originals") + sudo( + "chown %s.%s -R /srv/originals" % (env.user, env.user) + ) # We assume that the group is the same name as the user. It's common on linux config_monit_original() - put('config/supervisor_node_original.conf', - '/etc/supervisor/conf.d/node_original.conf', use_sudo=True) - sudo('supervisorctl reread') - sudo('supervisorctl reload') + put("config/supervisor_node_original.conf", "/etc/supervisor/conf.d/node_original.conf", use_sudo=True) + sudo("supervisorctl reread") + sudo("supervisorctl reload") + def setup_elasticsearch(): ES_VERSION = "2.4.4" - sudo('add-apt-repository -y ppa:openjdk-r/ppa') - sudo('apt-get update') - sudo('apt-get install openjdk-7-jre -y') + sudo("add-apt-repository -y ppa:openjdk-r/ppa") + sudo("apt-get update") + sudo("apt-get install openjdk-7-jre -y") with cd(env.VENDOR_PATH): - run('mkdir -p elasticsearch-%s' % ES_VERSION) - with cd(os.path.join(env.VENDOR_PATH, 'elasticsearch-%s' % ES_VERSION)): + run("mkdir -p elasticsearch-%s" % ES_VERSION) + with cd(os.path.join(env.VENDOR_PATH, "elasticsearch-%s" % ES_VERSION)): # run('wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-%s.deb' % ES_VERSION) # For v5+ - run('wget http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-%s.deb' % ES_VERSION) # For v1-v2 - sudo('dpkg -i elasticsearch-%s.deb' % ES_VERSION) - if not files.exists('/usr/share/elasticsearch/plugins/head'): - sudo('/usr/share/elasticsearch/bin/plugin install mobz/elasticsearch-head') + run( + "wget http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-%s.deb" + % ES_VERSION + ) # For v1-v2 + sudo("dpkg -i elasticsearch-%s.deb" % ES_VERSION) + if not files.exists("/usr/share/elasticsearch/plugins/head"): + sudo("/usr/share/elasticsearch/bin/plugin install mobz/elasticsearch-head") + def setup_db_search(): - put('config/supervisor_celeryd_search_indexer.conf', '/etc/supervisor/conf.d/celeryd_search_indexer.conf', use_sudo=True) - put('config/supervisor_celeryd_search_indexer_tasker.conf', '/etc/supervisor/conf.d/celeryd_search_indexer_tasker.conf', use_sudo=True) - sudo('supervisorctl reread') - sudo('supervisorctl update') + put( + "config/supervisor_celeryd_search_indexer.conf", + "/etc/supervisor/conf.d/celeryd_search_indexer.conf", + use_sudo=True, + ) + put( + "config/supervisor_celeryd_search_indexer_tasker.conf", + "/etc/supervisor/conf.d/celeryd_search_indexer_tasker.conf", + use_sudo=True, + ) + sudo("supervisorctl reread") + sudo("supervisorctl update") + def setup_imageproxy(install_go=False): # sudo('apt-get update') @@ -1500,86 +1725,105 @@ def setup_imageproxy(install_go=False): if install_go: with cd(env.VENDOR_PATH): with settings(warn_only=True): - run('git clone https://github.com/willnorris/imageproxy.git') - run('wget https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz') - run('tar -xzf go1.13.3.linux-amd64.tar.gz') - run('rm go1.13.3.linux-amd64.tar.gz') - sudo('rm /usr/bin/go') - sudo('ln -s /srv/code/go/bin/go /usr/bin/go') - with cd(os.path.join(env.VENDOR_PATH, 'imageproxy')): - run('go get willnorris.com/go/imageproxy/cmd/imageproxy') - put(os.path.join(env.SECRETS_PATH, 'settings/imageproxy.key'), - '/etc/imageproxy.key', use_sudo=True) - put(os.path.join(env.NEWSBLUR_PATH, 'config/supervisor_imageproxy.conf'), '/etc/supervisor/conf.d/supervisor_imageproxy.conf', use_sudo=True) - sudo('supervisorctl reread') - sudo('supervisorctl update') - sudo('ufw allow 443') - sudo('ufw allow 80') - put(os.path.join(env.NEWSBLUR_PATH, 'config/nginx.imageproxy.conf'), "/usr/local/nginx/conf/sites-enabled/imageproxy.conf", use_sudo=True) + run("git clone https://github.com/willnorris/imageproxy.git") + run("wget https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz") + run("tar -xzf go1.13.3.linux-amd64.tar.gz") + run("rm go1.13.3.linux-amd64.tar.gz") + sudo("rm /usr/bin/go") + sudo("ln -s /srv/code/go/bin/go /usr/bin/go") + with cd(os.path.join(env.VENDOR_PATH, "imageproxy")): + run("go get willnorris.com/go/imageproxy/cmd/imageproxy") + put(os.path.join(env.SECRETS_PATH, "settings/imageproxy.key"), "/etc/imageproxy.key", use_sudo=True) + put( + os.path.join(env.NEWSBLUR_PATH, "config/supervisor_imageproxy.conf"), + "/etc/supervisor/conf.d/supervisor_imageproxy.conf", + use_sudo=True, + ) + sudo("supervisorctl reread") + sudo("supervisorctl update") + sudo("ufw allow 443") + sudo("ufw allow 80") + put( + os.path.join(env.NEWSBLUR_PATH, "config/nginx.imageproxy.conf"), + "/usr/local/nginx/conf/sites-enabled/imageproxy.conf", + use_sudo=True, + ) sudo("/etc/init.d/nginx restart") - - - + + @parallel def setup_usage_monitor(): - sudo('ln -fs %s/utils/monitor_disk_usage.py /etc/cron.daily/monitor_disk_usage' % env.NEWSBLUR_PATH) - sudo('/etc/cron.daily/monitor_disk_usage') - + sudo("ln -fs %s/utils/monitor_disk_usage.py /etc/cron.daily/monitor_disk_usage" % env.NEWSBLUR_PATH) + sudo("/etc/cron.daily/monitor_disk_usage") + + @parallel def setup_feeds_fetched_monitor(): - sudo('ln -fs %s/utils/monitor_task_fetches.py /etc/cron.hourly/monitor_task_fetches' % env.NEWSBLUR_PATH) - sudo('/etc/cron.hourly/monitor_task_fetches') - + sudo("ln -fs %s/utils/monitor_task_fetches.py /etc/cron.hourly/monitor_task_fetches" % env.NEWSBLUR_PATH) + sudo("/etc/cron.hourly/monitor_task_fetches") + + @parallel def setup_newsletter_monitor(): - sudo('ln -fs %s/utils/monitor_newsletter_delivery.py /etc/cron.hourly/monitor_newsletter_delivery' % env.NEWSBLUR_PATH) - sudo('/etc/cron.hourly/monitor_newsletter_delivery') - + sudo( + "ln -fs %s/utils/monitor_newsletter_delivery.py /etc/cron.hourly/monitor_newsletter_delivery" + % env.NEWSBLUR_PATH + ) + sudo("/etc/cron.hourly/monitor_newsletter_delivery") + + @parallel def setup_queue_monitor(): - sudo('ln -fs %s/utils/monitor_work_queue.py /etc/cron.hourly/monitor_work_queue' % env.NEWSBLUR_PATH) - sudo('/etc/cron.hourly/monitor_work_queue') - + sudo("ln -fs %s/utils/monitor_work_queue.py /etc/cron.hourly/monitor_work_queue" % env.NEWSBLUR_PATH) + sudo("/etc/cron.hourly/monitor_work_queue") + + @parallel def setup_redis_monitor(): - run('sleep 5') # Wait for redis to startup so the log file is there - sudo('ln -fs %s/utils/monitor_redis_bgsave.py /etc/cron.daily/monitor_redis_bgsave' % env.NEWSBLUR_PATH) + run("sleep 5") # Wait for redis to startup so the log file is there + sudo("ln -fs %s/utils/monitor_redis_bgsave.py /etc/cron.daily/monitor_redis_bgsave" % env.NEWSBLUR_PATH) with settings(warn_only=True): - sudo('/etc/cron.daily/monitor_redis_bgsave') - + sudo("/etc/cron.daily/monitor_redis_bgsave") + + # ================ # = Setup - Task = # ================ + def setup_task_firewall(): - sudo('ufw default deny') - sudo('ufw allow ssh') - sudo('ufw allow 80') - sudo('ufw --force enable') + sudo("ufw default deny") + sudo("ufw allow ssh") + sudo("ufw allow 80") + sudo("ufw --force enable") + + +def setup_motd(role="app"): + motd = "/etc/update-motd.d/22-newsblur-motd" + put("config/motd_%s.txt" % role, motd, use_sudo=True) + sudo("chown root.root %s" % motd) + sudo("chmod a+x %s" % motd) -def setup_motd(role='app'): - motd = '/etc/update-motd.d/22-newsblur-motd' - put('config/motd_%s.txt' % role, motd, use_sudo=True) - sudo('chown root.root %s' % motd) - sudo('chmod a+x %s' % motd) def enable_celery_supervisor(queue=None, update=True): if not queue: - put('config/supervisor_celeryd.conf', '/etc/supervisor/conf.d/celeryd.conf', use_sudo=True) + put("config/supervisor_celeryd.conf", "/etc/supervisor/conf.d/celeryd.conf", use_sudo=True) else: - put('config/supervisor_celeryd_%s.conf' % queue, '/etc/supervisor/conf.d/celeryd.conf', use_sudo=True) + put("config/supervisor_celeryd_%s.conf" % queue, "/etc/supervisor/conf.d/celeryd.conf", use_sudo=True) - sudo('supervisorctl reread') + sudo("supervisorctl reread") if update: - sudo('supervisorctl update') + sudo("supervisorctl update") + @parallel def copy_db_settings(): return copy_task_settings() - + + @parallel def copy_task_settings(): - server_hostname = run('hostname') + server_hostname = run("hostname") # if any([(n in server_hostname) for n in ['task', 'db', 'search', 'node', 'push']]): host = server_hostname # elif env.host: @@ -1588,31 +1832,38 @@ def copy_task_settings(): # host = env.host_string.split('.', 2)[0] with settings(warn_only=True): - run('rm -f %s/local_settings.py' % env.NEWSBLUR_PATH) - put(os.path.join(env.SECRETS_PATH, 'settings/task_settings.py'), - '%s/newsblur/local_settings.py' % env.NEWSBLUR_PATH) - run('echo "\nSERVER_NAME = \\\\"%s\\\\"" >> %s/newsblur/local_settings.py' % (host, env.NEWSBLUR_PATH)) + run("rm -f %s/local_settings.py" % env.NEWSBLUR_PATH) + put( + os.path.join(env.SECRETS_PATH, "settings/task_settings.py"), + "%s/newsblur/local_settings.py" % env.NEWSBLUR_PATH, + ) + run( + 'echo "\nSERVER_NAME = \\\\"%s\\\\"" >> %s/newsblur/local_settings.py' % (host, env.NEWSBLUR_PATH) + ) + @parallel def copy_spam(): - put(os.path.join(env.SECRETS_PATH, 'spam/spam.py'), '%s/apps/social/spam.py' % env.NEWSBLUR_PATH) - + put(os.path.join(env.SECRETS_PATH, "spam/spam.py"), "%s/apps/social/spam.py" % env.NEWSBLUR_PATH) + + # ========================= # = Setup - Digital Ocean = # ========================= DO_SIZES = { - '1': 's-1vcpu-1gb', - '2': 's-1vcpu-2gb', - '4': 's-2vcpu-4gb', - '8': 's-4vcpu-8gb', - '16': 's-6vcpu-16gb', - '32': 's-8vcpu-32gb', - '48': 's-12vcpu-48gb', - '64': 's-16vcpu-64gb', - '32c': 'c-16', + "1": "s-1vcpu-1gb", + "2": "s-1vcpu-2gb", + "4": "s-2vcpu-4gb", + "8": "s-4vcpu-8gb", + "16": "s-6vcpu-16gb", + "32": "s-8vcpu-32gb", + "48": "s-12vcpu-48gb", + "64": "s-16vcpu-64gb", + "32c": "c-16", } + def setup_do(name, size=1, image=None): instance_size = DO_SIZES[str(size)] doapi = digitalocean.Manager(token=django_settings.DO_TOKEN_FABRIC) @@ -1623,25 +1874,27 @@ def setup_do(name, size=1, image=None): image = "ubuntu-20-04-x64" else: images = dict((s.name, s.id) for s in doapi.get_all_images()) - if image == "task": + if image == "task": image = images["task-2018-02"] elif image == "app": image = images["app-2018-02"] else: images = dict((s.name, s.id) for s in doapi.get_all_images()) print(images) - + name = do_name(name) env.doname = name print("Creating droplet: %s" % name) - instance = digitalocean.Droplet(token=django_settings.DO_TOKEN_FABRIC, - name=name, - size_slug=instance_size, - image=image, - region='nyc1', - monitoring=True, - private_networking=True, - ssh_keys=ssh_key_ids) + instance = digitalocean.Droplet( + token=django_settings.DO_TOKEN_FABRIC, + name=name, + size_slug=instance_size, + image=image, + region="nyc1", + monitoring=True, + private_networking=True, + ssh_keys=ssh_key_ids, + ) instance.create() time.sleep(2) instance = digitalocean.Droplet.get_object(django_settings.DO_TOKEN_FABRIC, instance.id) @@ -1649,12 +1902,12 @@ def setup_do(name, size=1, image=None): i = 0 while True: - if instance.status == 'active': + if instance.status == "active": print("...booted: %s" % instance.ip_address) time.sleep(5) break - elif instance.status == 'new': - print(".", end=' ') + elif instance.status == "new": + print(".", end=" ") sys.stdout.flush() instance = digitalocean.Droplet.get_object(django_settings.DO_TOKEN_FABRIC, instance.id) i += 1 @@ -1669,6 +1922,7 @@ def setup_do(name, size=1, image=None): add_user_to_do() assign_digitalocean_roledefs() + def do_name(name): if re.search(r"[0-9]", name): print(" ---> Using %s as hostname" % name) @@ -1680,48 +1934,52 @@ def do_name(name): for i in range(1, 100): try_host = "%s%02d" % (name, i) if try_host not in existing_hosts: - print(" ---> %s hosts in %s (%s). %s is unused." % (len(existing_hosts), name, - ', '.join(existing_hosts), try_host)) + print( + " ---> %s hosts in %s (%s). %s is unused." + % (len(existing_hosts), name, ", ".join(existing_hosts), try_host) + ) return try_host - - + + def add_user_to_do(): env.user = "root" repo_user = "sclay" with settings(warn_only=True): - run('useradd -m %s' % (repo_user)) + run("useradd -m %s" % (repo_user)) setup_sudoers("%s" % (repo_user)) - run('mkdir -p ~%s/.ssh && chmod 700 ~%s/.ssh' % (repo_user, repo_user)) - run('rm -fr ~%s/.ssh/id_dsa*' % (repo_user)) + run("mkdir -p ~%s/.ssh && chmod 700 ~%s/.ssh" % (repo_user, repo_user)) + run("rm -fr ~%s/.ssh/id_dsa*" % (repo_user)) run('ssh-keygen -t dsa -f ~%s/.ssh/id_dsa -N ""' % (repo_user)) - run('touch ~%s/.ssh/authorized_keys' % (repo_user)) + run("touch ~%s/.ssh/authorized_keys" % (repo_user)) copy_ssh_keys() - run('chown %s.%s -R ~%s/.ssh' % (repo_user, repo_user, repo_user)) + run("chown %s.%s -R ~%s/.ssh" % (repo_user, repo_user, repo_user)) env.user = repo_user + # =============== # = Setup - EC2 = # =============== + def setup_ec2(): - AMI_NAME = 'ami-834cf1ea' # Ubuntu 64-bit 12.04 LTS + AMI_NAME = "ami-834cf1ea" # Ubuntu 64-bit 12.04 LTS # INSTANCE_TYPE = 'c1.medium' - INSTANCE_TYPE = 'c1.medium' + INSTANCE_TYPE = "c1.medium" conn = EC2Connection(django_settings.AWS_ACCESS_KEY_ID, django_settings.AWS_SECRET_ACCESS_KEY) - reservation = conn.run_instances(AMI_NAME, instance_type=INSTANCE_TYPE, - key_name=env.user, - security_groups=['db-mongo']) + reservation = conn.run_instances( + AMI_NAME, instance_type=INSTANCE_TYPE, key_name=env.user, security_groups=["db-mongo"] + ) instance = reservation.instances[0] print("Booting reservation: %s/%s (size: %s)" % (reservation, instance, INSTANCE_TYPE)) i = 0 while True: - if instance.state == 'pending': - print(".", end=' ') + if instance.state == "pending": + print(".", end=" ") sys.stdout.flush() instance.update() i += 1 time.sleep(i) - elif instance.state == 'running': + elif instance.state == "running": print("...booted: %s" % instance.public_dns_name) time.sleep(5) break @@ -1732,213 +1990,246 @@ def setup_ec2(): host = instance.public_dns_name env.host_string = host + # ========== # = Deploy = # ========== + @parallel def pull(master=False): with virtualenv(): - run('git pull') + run("git pull") if master: - run('git checkout master') - run('git pull') + run("git checkout master") + run("git pull") + def pre_deploy(): compress_assets(bundle=True) + @serial def post_deploy(): cleanup_assets() + def role_for_host(): for role, hosts in list(env.roledefs.items()): if env.host in hosts: return role + @parallel def deploy(fast=False, reload=False): role = role_for_host() - if role in ['work', 'search', 'debug']: + if role in ["work", "search", "debug"]: deploy_code(copy_assets=False, fast=fast, reload=True) else: deploy_code(copy_assets=False, fast=fast, reload=reload) + @parallel def deploy_web(fast=False): role = role_for_host() - if role in ['work', 'search']: + if role in ["work", "search"]: deploy_code(copy_assets=True, fast=fast, reload=True) else: deploy_code(copy_assets=True, fast=fast) + @parallel def deploy_rebuild(fast=False): deploy_code(copy_assets=True, fast=fast, rebuild=True) + @parallel def kill_gunicorn(): with virtualenv(): - sudo('pkill -9 -u %s -f gunicorn_django' % env.user) - + sudo("pkill -9 -u %s -f gunicorn_django" % env.user) + + @parallel def deploy_code(copy_assets=False, rebuild=False, fast=False, reload=False): with virtualenv(): - run('git pull') - run('mkdir -p static') + run("git pull") + run("mkdir -p static") if rebuild: - run('rm -fr static/*') + run("rm -fr static/*") if copy_assets: transfer_assets() - + with virtualenv(): with settings(warn_only=True): if reload: - sudo('supervisorctl reload') + sudo("supervisorctl reload") elif fast: kill_gunicorn() else: - sudo('kill -HUP `cat /srv/newsblur/logs/gunicorn.pid`') + sudo("kill -HUP `cat /srv/newsblur/logs/gunicorn.pid`") + @parallel def kill(): - sudo('supervisorctl reload') + sudo("supervisorctl reload") with settings(warn_only=True): - if env.user == 'ubuntu': - sudo('./utils/kill_gunicorn.sh') + if env.user == "ubuntu": + sudo("./utils/kill_gunicorn.sh") else: - run('./utils/kill_gunicorn.sh') + run("./utils/kill_gunicorn.sh") + @parallel def deploy_node(): pull() with virtualenv(): - run('sudo supervisorctl restart node_unread') - run('sudo supervisorctl restart node_unread_ssl') - run('sudo supervisorctl restart node_favicons') - run('sudo supervisorctl restart node_text') + run("sudo supervisorctl restart node_unread") + run("sudo supervisorctl restart node_unread_ssl") + run("sudo supervisorctl restart node_favicons") + run("sudo supervisorctl restart node_text") + def gunicorn_restart(): restart_gunicorn() + def restart_gunicorn(): with virtualenv(), settings(warn_only=True): - run('sudo supervisorctl restart gunicorn') + run("sudo supervisorctl restart gunicorn") + def gunicorn_stop(): with virtualenv(), settings(warn_only=True): - run('sudo supervisorctl stop gunicorn') + run("sudo supervisorctl stop gunicorn") + def staging(): - with cd('~/staging'): - run('git pull') - run('kill -HUP `cat logs/gunicorn.pid`') - run('curl -s http://dev.newsblur.com > /dev/null') - run('curl -s http://dev.newsblur.com/m/ > /dev/null') + with cd("~/staging"): + run("git pull") + run("kill -HUP `cat logs/gunicorn.pid`") + run("curl -s http://dev.newsblur.com > /dev/null") + run("curl -s http://dev.newsblur.com/m/ > /dev/null") + def staging_build(): - with cd('~/staging'): - run('git pull') - run('./manage.py migrate') - run('kill -HUP `cat logs/gunicorn.pid`') - run('curl -s http://dev.newsblur.com > /dev/null') - run('curl -s http://dev.newsblur.com/m/ > /dev/null') + with cd("~/staging"): + run("git pull") + run("./manage.py migrate") + run("kill -HUP `cat logs/gunicorn.pid`") + run("curl -s http://dev.newsblur.com > /dev/null") + run("curl -s http://dev.newsblur.com/m/ > /dev/null") + @parallel def celery(): celery_slow() + def celery_slow(): with virtualenv(): - run('git pull') + run("git pull") celery_stop() celery_start() + @parallel def celery_fast(): with virtualenv(): - run('git pull') + run("git pull") celery_reload() + @parallel def celery_stop(): with virtualenv(): - sudo('supervisorctl stop celery') + sudo("supervisorctl stop celery") with settings(warn_only=True): - if env.user == 'ubuntu': - sudo('./utils/kill_celery.sh') + if env.user == "ubuntu": + sudo("./utils/kill_celery.sh") else: - run('./utils/kill_celery.sh') + run("./utils/kill_celery.sh") + @parallel def celery_start(): with virtualenv(): - run('sudo supervisorctl start celery') - run('tail logs/newsblur.log') + run("sudo supervisorctl start celery") + run("tail logs/newsblur.log") + @parallel def celery_reload(): with virtualenv(): - run('sudo supervisorctl reload celery') - run('tail logs/newsblur.log') + run("sudo supervisorctl reload celery") + run("tail logs/newsblur.log") + def kill_celery(): with virtualenv(): with settings(warn_only=True): - if env.user == 'ubuntu': - sudo('./utils/kill_celery.sh') + if env.user == "ubuntu": + sudo("./utils/kill_celery.sh") else: - run('./utils/kill_celery.sh') + run("./utils/kill_celery.sh") + def compress_assets(bundle=False): - local('jammit -c newsblur/assets.yml --base-url https://www.newsblur.com --output static') - local('tar -czf static.tgz static/*') + local("jammit -c newsblur/assets.yml --base-url https://www.newsblur.com --output static") + local("tar -czf static.tgz static/*") tries_left = 5 while True: try: success = False with settings(warn_only=True): - local('PYTHONPATH=/srv/newsblur python utils/backups/s3.py set static.tgz') + local("PYTHONPATH=/srv/newsblur python utils/backups/s3.py set static.tgz") success = True if not success: raise Exception("Ack!") break except Exception as e: - print(" ***> %s. Trying %s more time%s..." % (e, tries_left, '' if tries_left == 1 else 's')) + print(" ***> %s. Trying %s more time%s..." % (e, tries_left, "" if tries_left == 1 else "s")) tries_left -= 1 - if tries_left <= 0: break + if tries_left <= 0: + break def transfer_assets(): # filename = "deploy_%s.tgz" % env.commit # Easy rollback? Eh, can just upload it again. # run('PYTHONPATH=/srv/newsblur python s3.py get deploy_%s.tgz' % filename) - run('PYTHONPATH=/srv/newsblur python utils/backups/s3.py get static.tgz') + run("PYTHONPATH=/srv/newsblur python utils/backups/s3.py get static.tgz") # run('mv %s static/static.tgz' % filename) - run('mv static.tgz static/static.tgz') - run('tar -xzf static/static.tgz') - run('rm -f static/static.tgz') + run("mv static.tgz static/static.tgz") + run("tar -xzf static/static.tgz") + run("rm -f static/static.tgz") + def cleanup_assets(): - local('rm -f static.tgz') + local("rm -f static.tgz") + # =========== # = Backups = # =========== + def setup_redis_backups(name=None): # crontab for redis backups, name is either none, story, sessions, pubsub - crontab = ("0 4 * * * /srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_redis%s.py" % - (("_%s"%name) if name else "")) + crontab = ( + "0 4 * * * /srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_redis%s.py" + % (("_%s" % name) if name else "") + ) run('(crontab -l ; echo "%s") | sort - | uniq - | crontab -' % crontab) - run('crontab -l') + run("crontab -l") + def setup_mongo_backups(): # crontab for mongo backups crontab = "0 4 * * * /srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_mongo.py" run('(crontab -l ; echo "%s") | sort - | uniq - | crontab -' % crontab) - run('crontab -l') - + run("crontab -l") + + def setup_postgres_backups(): # crontab for postgres backups crontab = """ @@ -1947,64 +2238,84 @@ def setup_postgres_backups(): 0 * * * * sudo find /var/lib/postgresql/13/archive -type f -mmin +180 -delete""" run('(crontab -l ; echo "%s") | sort - | uniq - | crontab -' % crontab) - run('crontab -l') - + run("crontab -l") + + def backup_redis(name=None): - run('/srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_redis%s.py' % (("_%s"%name) if name else "")) - + run( + "/srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_redis%s.py" + % (("_%s" % name) if name else "") + ) + + def backup_mongo(): - run('/srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_mongo.py') + run("/srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_mongo.py") + def backup_postgresql(): - run('/srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_psql.py') + run("/srv/newsblur/venv/newsblur3/bin/python /srv/newsblur/utils/backups/backup_psql.py") + # =============== # = Calibration = # =============== + def sync_time(): with settings(warn_only=True): sudo("/etc/init.d/ntp stop") sudo("ntpdate pool.ntp.org") sudo("/etc/init.d/ntp start") + def setup_time_calibration(): - sudo('apt-get -y install ntp') - put('config/ntpdate.cron', '%s/' % env.NEWSBLUR_PATH) - sudo('chown root.root %s/ntpdate.cron' % env.NEWSBLUR_PATH) - sudo('chmod 755 %s/ntpdate.cron' % env.NEWSBLUR_PATH) - sudo('mv %s/ntpdate.cron /etc/cron.hourly/ntpdate' % env.NEWSBLUR_PATH) + sudo("apt-get -y install ntp") + put("config/ntpdate.cron", "%s/" % env.NEWSBLUR_PATH) + sudo("chown root.root %s/ntpdate.cron" % env.NEWSBLUR_PATH) + sudo("chmod 755 %s/ntpdate.cron" % env.NEWSBLUR_PATH) + sudo("mv %s/ntpdate.cron /etc/cron.hourly/ntpdate" % env.NEWSBLUR_PATH) with settings(warn_only=True): - sudo('/etc/cron.hourly/ntpdate') + sudo("/etc/cron.hourly/ntpdate") + # ============== # = Tasks - DB = # ============== + def restore_postgres(port=5432, download=False): with virtualenv(): - backup_date = '2020-12-03-02-51' + backup_date = "2020-12-03-02-51" yes = prompt("Dropping and creating NewsBlur PGSQL db. Sure?") - if yes != 'y': + if yes != "y": return if download: - run('mkdir -p postgres') - run('PYTHONPATH=%s python utils/backups/s3.py get postgres/backup_postgresql_%s.sql.gz' % (env.NEWSBLUR_PATH, backup_date)) + run("mkdir -p postgres") + run( + "PYTHONPATH=%s python utils/backups/s3.py get postgres/backup_postgresql_%s.sql.gz" + % (env.NEWSBLUR_PATH, backup_date) + ) # sudo('su postgres -c "createuser -p %s -U newsblur"' % (port,)) - with settings(warn_only=True): + with settings(warn_only=True): # May not exist - run('dropdb newsblur -p %s -U newsblur' % (port,), pty=False) - run('sudo -u postgres createuser newsblur -s') + run("dropdb newsblur -p %s -U newsblur" % (port,), pty=False) + run("sudo -u postgres createuser newsblur -s") # May already exist - run('createdb newsblur -p %s -O newsblur -U newsblur' % (port,), pty=False) - run('pg_restore -U newsblur -p %s --role=newsblur --dbname=newsblur /srv/newsblur/postgres/backup_postgresql_%s.sql.gz' % (port, backup_date), pty=False) + run("createdb newsblur -p %s -O newsblur -U newsblur" % (port,), pty=False) + run( + "pg_restore -U newsblur -p %s --role=newsblur --dbname=newsblur /srv/newsblur/postgres/backup_postgresql_%s.sql.gz" + % (port, backup_date), + pty=False, + ) + def restore_mongo(download=False): - backup_date = '2020-11-11-04-00' + backup_date = "2020-11-11-04-00" if download: - run('PYTHONPATH=/srv/newsblur python utils/backups/s3.py get backup_mongo_%s.tgz' % (backup_date)) - run('tar -xf backup_mongo_%s.tgz' % backup_date) - run('mongorestore backup_mongo_%s' % backup_date) + run("PYTHONPATH=/srv/newsblur python utils/backups/s3.py get backup_mongo_%s.tgz" % (backup_date)) + run("tar -xf backup_mongo_%s.tgz" % backup_date) + run("mongorestore backup_mongo_%s" % backup_date) + # ====== # = S3 = @@ -2012,48 +2323,54 @@ def restore_mongo(download=False): if django_settings: try: - ACCESS_KEY = django_settings.S3_ACCESS_KEY - SECRET = django_settings.S3_SECRET + ACCESS_KEY = django_settings.S3_ACCESS_KEY + SECRET = django_settings.S3_SECRET BUCKET_NAME = django_settings.S3_BACKUP_BUCKET # Note that you need to create this bucket first except: print(" ---> You need to fix django's settings. Enter python and type `import settings`.") + def save_file_in_s3(filename): - conn = S3Connection(ACCESS_KEY, SECRET) + conn = S3Connection(ACCESS_KEY, SECRET) bucket = conn.get_bucket(BUCKET_NAME) - k = Key(bucket) - k.key = filename + k = Key(bucket) + k.key = filename k.set_contents_from_filename(filename) + def get_file_from_s3(filename): - conn = S3Connection(ACCESS_KEY, SECRET) + conn = S3Connection(ACCESS_KEY, SECRET) bucket = conn.get_bucket(BUCKET_NAME) - k = Key(bucket) - k.key = filename + k = Key(bucket) + k.key = filename k.get_contents_to_filename(filename) + def list_backup_in_s3(): - conn = S3Connection(ACCESS_KEY, SECRET) + conn = S3Connection(ACCESS_KEY, SECRET) bucket = conn.get_bucket(BUCKET_NAME) for i, key in enumerate(bucket.get_all_keys()): print("[%s] %s" % (i, key.name)) + def delete_all_backups(): - #FIXME: validate filename exists - conn = S3Connection(ACCESS_KEY, SECRET) + # FIXME: validate filename exists + conn = S3Connection(ACCESS_KEY, SECRET) bucket = conn.get_bucket(BUCKET_NAME) for i, key in enumerate(bucket.get_all_keys()): print("deleting %s" % (key.name)) key.delete() + def add_revsys_keys(): put("~/Downloads/revsys-keys.pub", "revsys_keys") - run('cat revsys_keys >> ~/.ssh/authorized_keys') - run('rm revsys_keys') + run("cat revsys_keys >> ~/.ssh/authorized_keys") + run("rm revsys_keys") + def upgrade_to_virtualenv(role=None): if not role: @@ -2065,31 +2382,32 @@ def upgrade_to_virtualenv(role=None): elif role == "app": gunicorn_stop() elif role == "node": - run('sudo supervisorctl stop node_unread') - run('sudo supervisorctl stop node_favicons') + run("sudo supervisorctl stop node_unread") + run("sudo supervisorctl stop node_favicons") elif role == "work": - sudo('/etc/init.d/supervisor stop') + sudo("/etc/init.d/supervisor stop") kill_pgbouncer(bounce=False) setup_installs() pip() if role == "task": enable_celery_supervisor(update=False) - sudo('reboot') + sudo("reboot") elif role == "app": setup_gunicorn(supervisor=True, restart=False) - sudo('reboot') + sudo("reboot") elif role == "node": deploy_node() elif role == "search": setup_db_search() elif role == "work": enable_celerybeat() - sudo('reboot') + sudo("reboot") + def benchmark(): - run('curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.deb.sh | sudo bash') - sudo('apt-get install -y sysbench') - run('sysbench cpu --cpu-max-prime=20000 run') - run('sysbench fileio --file-total-size=150G prepare') - run('sysbench fileio --file-total-size=150G --file-test-mode=rndrw --time=300 --max-requests=0 run') - run('sysbench fileio --file-total-size=150G cleanup') + run("curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.deb.sh | sudo bash") + sudo("apt-get install -y sysbench") + run("sysbench cpu --cpu-max-prime=20000 run") + run("sysbench fileio --file-total-size=150G prepare") + run("sysbench fileio --file-total-size=150G --file-test-mode=rndrw --time=300 --max-requests=0 run") + run("sysbench fileio --file-total-size=150G cleanup") diff --git a/archive/jammit.py b/archive/jammit.py index f1020547e4..573f924abe 100644 --- a/archive/jammit.py +++ b/archive/jammit.py @@ -1,5 +1,6 @@ import os from fnmatch import fnmatch + import yaml from django.conf import settings @@ -8,10 +9,10 @@ MHTML_START = "" + class JammitAssets: + ASSET_FILENAME = "assets.yml" - ASSET_FILENAME = 'assets.yml' - def __init__(self, assets_dir): """ Initializes the Jammit object by reading the assets.yml file and @@ -20,31 +21,31 @@ def __init__(self, assets_dir): """ self.assets_dir = assets_dir self.assets = self.read_assets() - + def read_assets(self): """ Read the assets from the YAML and store it as a lookup dictionary. """ filepath = os.path.join(self.assets_dir, self.ASSET_FILENAME) - with open(filepath, 'r') as yaml_file: + with open(filepath, "r") as yaml_file: return yaml.safe_load(yaml_file) - + def render_tags(self, asset_type, asset_package): """ Returns rendered ' % path - + def javascript_tag_compressed(self, asset_package, asset_type_ext): - filename = 'static/%s.%s' % (asset_package, asset_type_ext) + filename = "static/%s.%s" % (asset_package, asset_type_ext) asset_mtime = int(os.path.getmtime(filename)) - path = '%s?%s' % (filename, asset_mtime) + path = "%s?%s" % (filename, asset_mtime) return self.javascript_tag(path) - + def stylesheet_tag(self, path): return '' % path def stylesheet_tag_compressed(self, asset_package, asset_type_ext): - datauri_filename = 'static/%s-datauri.%s' % (asset_package, asset_type_ext) - original_filename = 'static/%s.%s' % (asset_package, asset_type_ext) + datauri_filename = "static/%s-datauri.%s" % (asset_package, asset_type_ext) + original_filename = "static/%s.%s" % (asset_package, asset_type_ext) asset_mtime = int(os.path.getmtime(datauri_filename)) - datauri_path = '%s?%s' % (datauri_filename, asset_mtime) - original_path = '%s?%s' % (original_filename, asset_mtime) - - return '\n'.join([ - DATA_URI_START, - self.stylesheet_tag(datauri_path), - DATA_URI_END, - MHTML_START, - self.stylesheet_tag(original_path), - MHTML_END, - ]) + datauri_path = "%s?%s" % (datauri_filename, asset_mtime) + original_path = "%s?%s" % (original_filename, asset_mtime) + + return "\n".join( + [ + DATA_URI_START, + self.stylesheet_tag(datauri_path), + DATA_URI_END, + MHTML_START, + self.stylesheet_tag(original_path), + MHTML_END, + ] + ) -class FileFinder: +class FileFinder: @classmethod def filefinder(cls, pattern): paths = [] - if '**' in pattern: - folder, wild, pattern = pattern.partition('/**/') + if "**" in pattern: + folder, wild, pattern = pattern.partition("/**/") for f in cls.recursive_find_files(folder, pattern): paths.append(f) else: diff --git a/archive/munin/aws_elb_latency b/archive/munin/aws_elb_latency index a1e8f6ee57..32b78683f5 100755 --- a/archive/munin/aws_elb_latency +++ b/archive/munin/aws_elb_latency @@ -9,6 +9,7 @@ from boto.ec2.cloudwatch import CloudWatchConnection from vendor.munin import MuninPlugin + class AWSCloudWatchELBLatencyPlugin(MuninPlugin): category = "AWS" args = "-l 0 --base 1000" diff --git a/archive/munin/aws_elb_requests b/archive/munin/aws_elb_requests index 9af76a7a88..b0233864c7 100755 --- a/archive/munin/aws_elb_requests +++ b/archive/munin/aws_elb_requests @@ -9,6 +9,7 @@ from boto.ec2.cloudwatch import CloudWatchConnection from vendor.munin import MuninPlugin + class AWSCloudWatchELBRequestsPlugin(MuninPlugin): category = "AWS" args = "-l 0 --base 1000" diff --git a/archive/munin/aws_sqs_queue_length_ b/archive/munin/aws_sqs_queue_length_ index 19219074d2..35eee48900 100755 --- a/archive/munin/aws_sqs_queue_length_ +++ b/archive/munin/aws_sqs_queue_length_ @@ -8,6 +8,7 @@ from boto.sqs.connection import SQSConnection from vendor.munin import MuninPlugin + class AWSSQSQueueLengthPlugin(MuninPlugin): category = "AWS" args = "-l 0 --base 1000" diff --git a/archive/munin/cassandra_cfcounts b/archive/munin/cassandra_cfcounts index 4332ab16df..cf4fbdb534 100755 --- a/archive/munin/cassandra_cfcounts +++ b/archive/munin/cassandra_cfcounts @@ -1,8 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python import os + from vendor.munin.cassandra import MuninCassandraPlugin + class CassandraCFCountsPlugin(MuninCassandraPlugin): title = "read/write rate" args = "--base 1000 -l 0" diff --git a/archive/munin/cassandra_key_cache_ratio b/archive/munin/cassandra_key_cache_ratio index dbaa964493..c62da4c5aa 100755 --- a/archive/munin/cassandra_key_cache_ratio +++ b/archive/munin/cassandra_key_cache_ratio @@ -1,8 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python import os + from vendor.munin.cassandra import MuninCassandraPlugin + class CassandraKeyCacheRatioPlugin(MuninCassandraPlugin): title = "key cache hit ratio" args = "--base 1000 -l 0" diff --git a/archive/munin/cassandra_latency b/archive/munin/cassandra_latency index 891a942942..af473ecac0 100755 --- a/archive/munin/cassandra_latency +++ b/archive/munin/cassandra_latency @@ -1,8 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python import os + from vendor.munin.cassandra import MuninCassandraPlugin + class CassandraLatencyPlugin(MuninCassandraPlugin): title = "read/write latency" args = "--base 1000 -l 0" diff --git a/archive/munin/cassandra_load b/archive/munin/cassandra_load index 5a7916a492..4cfa5bb735 100755 --- a/archive/munin/cassandra_load +++ b/archive/munin/cassandra_load @@ -1,8 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python import os + from vendor.munin.cassandra import MuninCassandraPlugin + class CassandraLoadPlugin(MuninCassandraPlugin): title = "load (data stored in node)" args = "--base 1024 -l 0" diff --git a/archive/munin/cassandra_pending b/archive/munin/cassandra_pending index 92fddb700b..0c7b71c9a2 100755 --- a/archive/munin/cassandra_pending +++ b/archive/munin/cassandra_pending @@ -1,8 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python import os + from vendor.munin.cassandra import MuninCassandraPlugin + class CassandraPendingPlugin(MuninCassandraPlugin): title = "thread pool pending tasks" args = "--base 1000 -l 0" diff --git a/archive/munin/ddwrt_wl_rate b/archive/munin/ddwrt_wl_rate index 16e0e162c5..5117359fb9 100755 --- a/archive/munin/ddwrt_wl_rate +++ b/archive/munin/ddwrt_wl_rate @@ -2,6 +2,7 @@ from vendor.munin.ddwrt import DDWrtPlugin + class DDWrtWirelessRate(DDWrtPlugin): title = "Wireless rate" args = "--base 1000 -l 0" diff --git a/archive/munin/ddwrt_wl_signal b/archive/munin/ddwrt_wl_signal index 4f317f6cee..d0ae1fbec2 100755 --- a/archive/munin/ddwrt_wl_signal +++ b/archive/munin/ddwrt_wl_signal @@ -2,6 +2,7 @@ from vendor.munin.ddwrt import DDWrtPlugin + class DDWrtWirelessSignalPlugin(DDWrtPlugin): title = "Wireless signal" args = "--base 1000 -l 0" diff --git a/archive/munin/gearman_connections b/archive/munin/gearman_connections index 207076c397..985d57b3b6 100755 --- a/archive/munin/gearman_connections +++ b/archive/munin/gearman_connections @@ -2,6 +2,7 @@ from vendor.munin.gearman import MuninGearmanPlugin + class MuninGearmanConnectionsPlugin(MuninGearmanPlugin): title = "Gearman Connections" args = "--base 1000" diff --git a/archive/munin/gearman_queues b/archive/munin/gearman_queues index e0a2a308a7..9941e99c11 100755 --- a/archive/munin/gearman_queues +++ b/archive/munin/gearman_queues @@ -1,8 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python import os + from vendor.munin.gearman import MuninGearmanPlugin + class MuninGearmanQueuesPlugin(MuninGearmanPlugin): title = "Gearman Queues" args = "--base 1000" diff --git a/archive/munin/hookbox b/archive/munin/hookbox index 4a136318e3..2ceb00eff5 100755 --- a/archive/munin/hookbox +++ b/archive/munin/hookbox @@ -1,14 +1,14 @@ #!/srv/newsblur/venv/newsblur3/bin/python -import os import json +import os import urllib + import urllib2 from vendor.munin import MuninPlugin - class HookboxPlugin(MuninPlugin): title = 'hookbox' args = "--base 1000" diff --git a/archive/munin/loadavg b/archive/munin/loadavg index f0b9da26f3..d8629d85d5 100755 --- a/archive/munin/loadavg +++ b/archive/munin/loadavg @@ -10,8 +10,10 @@ First tries reading from /proc/loadavg if it exists. Otherwise, execute """ import os + from vendor.munin import MuninPlugin + class LoadAVGPlugin(MuninPlugin): title = "Load average" args = "--base 1000 -l 0" @@ -35,7 +37,7 @@ class LoadAVGPlugin(MuninPlugin): if os.path.exists("/proc/loadavg"): loadavg = open("/proc/loadavg", "r").read().strip().split(' ') else: - from subprocess import Popen, PIPE + from subprocess import PIPE, Popen output = Popen(["uptime"], stdout=PIPE).communicate()[0] loadavg = output.rsplit(':', 1)[1].strip().split(' ')[:3] return dict(load=loadavg[1]) diff --git a/archive/munin/memcached_bytes b/archive/munin/memcached_bytes index 7b402253eb..75b01261f3 100755 --- a/archive/munin/memcached_bytes +++ b/archive/munin/memcached_bytes @@ -2,6 +2,7 @@ from vendor.munin.memcached import MuninMemcachedPlugin + class MuninMemcachedBytesPlugin(MuninMemcachedPlugin): title = "Memcached bytes read/written stats" args = "--base 1024" diff --git a/archive/munin/memcached_connections b/archive/munin/memcached_connections index 391a1299d9..45adb6d5d6 100755 --- a/archive/munin/memcached_connections +++ b/archive/munin/memcached_connections @@ -2,6 +2,7 @@ from vendor.munin.memcached import MuninMemcachedPlugin + class MuninMemcachedConnectionsPlugin(MuninMemcachedPlugin): title = "Memcached connections stats" args = "--base 1000" diff --git a/archive/munin/memcached_curr_items b/archive/munin/memcached_curr_items index 03b6e27592..9aeba345f5 100755 --- a/archive/munin/memcached_curr_items +++ b/archive/munin/memcached_curr_items @@ -2,6 +2,7 @@ from vendor.munin.memcached import MuninMemcachedPlugin + class MuninMemcachedCurrentItemsPlugin(MuninMemcachedPlugin): title = "Memcached current items stats" args = "--base 1000" diff --git a/archive/munin/memcached_items b/archive/munin/memcached_items index 5ac10c7d6b..f8fe4f7155 100755 --- a/archive/munin/memcached_items +++ b/archive/munin/memcached_items @@ -2,6 +2,7 @@ from vendor.munin.memcached import MuninMemcachedPlugin + class MuninMemcachedItemsPlugin(MuninMemcachedPlugin): title = "Memcached new items stats" args = "--base 1000" diff --git a/archive/munin/memcached_queries b/archive/munin/memcached_queries index 0a40b04245..8deca8e6fe 100755 --- a/archive/munin/memcached_queries +++ b/archive/munin/memcached_queries @@ -2,6 +2,7 @@ from vendor.munin.memcached import MuninMemcachedPlugin + class MuninMemcachedQueriesPlugin(MuninMemcachedPlugin): title = "Memcached query stats" args = "--base 1000" diff --git a/archive/munin/mongo_btree b/archive/munin/mongo_btree index 360c6f3701..1c79b630e3 100755 --- a/archive/munin/mongo_btree +++ b/archive/munin/mongo_btree @@ -2,9 +2,10 @@ ## GENERATED FILE - DO NOT EDIT -import urllib2 -import sys import os +import sys + +import urllib2 try: import json diff --git a/archive/munin/mongo_indexsize b/archive/munin/mongo_indexsize index 25baed35d5..9294639bc2 100644 --- a/archive/munin/mongo_indexsize +++ b/archive/munin/mongo_indexsize @@ -28,9 +28,10 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from pymongo import Connection import os +from pymongo import Connection + settings_host = os.environ.get("host", "127.0.0.1") settings_port = 27017 settings_db = 'newsblur' @@ -72,8 +73,8 @@ def doConfig(): if __name__ == "__main__": - from sys import argv from os import environ + from sys import argv # Could be done by a for loop # but i think if's are faster diff --git a/archive/munin/mongo_mem b/archive/munin/mongo_mem index 9853edc947..a7a31db13b 100755 --- a/archive/munin/mongo_mem +++ b/archive/munin/mongo_mem @@ -2,9 +2,10 @@ ## GENERATED FILE - DO NOT EDIT -import urllib2 -import sys import os +import sys + +import urllib2 try: import json diff --git a/archive/munin/mongo_ops b/archive/munin/mongo_ops index 14fdebb7fb..20bc91e58f 100755 --- a/archive/munin/mongo_ops +++ b/archive/munin/mongo_ops @@ -2,9 +2,10 @@ ## GENERATED FILE - DO NOT EDIT -import urllib2 -import sys import os +import sys + +import urllib2 try: import json diff --git a/archive/munin/mongodb_conn b/archive/munin/mongodb_conn index 233436e3a8..6e7d3672fc 100755 --- a/archive/munin/mongodb_conn +++ b/archive/munin/mongodb_conn @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBConnectionsPlugin(MuninMongoDBPlugin): args = "-l 0 --base 1000" vlabel = "count" diff --git a/archive/munin/mongodb_heap_usage b/archive/munin/mongodb_heap_usage index f19fb3d6aa..8eed336a52 100755 --- a/archive/munin/mongodb_heap_usage +++ b/archive/munin/mongodb_heap_usage @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBHeapUsagePlugin(MuninMongoDBPlugin): args = "-l 0 --base 1024" vlabel = "bytes" diff --git a/archive/munin/mongodb_objects_newsblur b/archive/munin/mongodb_objects_newsblur index dec54eb549..aeff0256f6 100755 --- a/archive/munin/mongodb_objects_newsblur +++ b/archive/munin/mongodb_objects_newsblur @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBObjectsPlugin(MuninMongoDBPlugin): args = "--base 1000" vlabel = "objects" diff --git a/archive/munin/mongodb_ops b/archive/munin/mongodb_ops index 203ed5aa7f..e62219955a 100755 --- a/archive/munin/mongodb_ops +++ b/archive/munin/mongodb_ops @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBOpsPlugin(MuninMongoDBPlugin): args = "-l 0 --base 1000" vlabel = "ops / ${graph_period}" diff --git a/archive/munin/mongodb_page_faults b/archive/munin/mongodb_page_faults index cddb5b55d7..f4c7d3eb84 100755 --- a/archive/munin/mongodb_page_faults +++ b/archive/munin/mongodb_page_faults @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBPageFaultsPlugin(MuninMongoDBPlugin): args = "-l 0 --base 1000" vlabel = "page faults / sec" diff --git a/archive/munin/mongodb_queues b/archive/munin/mongodb_queues index 6c96b57735..69b87b3d56 100755 --- a/archive/munin/mongodb_queues +++ b/archive/munin/mongodb_queues @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBQueuesPlugin(MuninMongoDBPlugin): args = "-l 0 --base 1000" vlabel = "count" diff --git a/archive/munin/mongodb_size_newsblur b/archive/munin/mongodb_size_newsblur index 09f9213e24..c38fbe7645 100755 --- a/archive/munin/mongodb_size_newsblur +++ b/archive/munin/mongodb_size_newsblur @@ -3,6 +3,7 @@ from vendor.munin.mongodb import MuninMongoDBPlugin + class MongoDBSizePlugin(MuninMongoDBPlugin): args = "-l 0 --base 1024" vlabel = "bytes" diff --git a/archive/munin/munin/__init__.py b/archive/munin/munin/__init__.py index 67fa6a55c5..84b9e21c9f 100755 --- a/archive/munin/munin/__init__.py +++ b/archive/munin/munin/__init__.py @@ -1,11 +1,11 @@ - __version__ = "1.4" import os -import sys import socket +import sys from decimal import Decimal + class MuninPlugin(object): title = "" args = None @@ -15,10 +15,10 @@ class MuninPlugin(object): fields = [] def __init__(self): - if 'GRAPH_TITLE' in os.environ: - self.title = os.environ['GRAPH_TITLE'] - if 'GRAPH_CATEGORY' in os.environ: - self.category = os.environ['GRAPH_CATEGORY'] + if "GRAPH_TITLE" in os.environ: + self.title = os.environ["GRAPH_TITLE"] + if "GRAPH_CATEGORY" in os.environ: + self.category = os.environ["GRAPH_CATEGORY"] super(MuninPlugin, self).__init__() def autoconf(self): @@ -26,18 +26,18 @@ def autoconf(self): def config(self): conf = [] - for k in ('title', 'category', 'args', 'vlabel', 'info', 'scale', 'order'): + for k in ("title", "category", "args", "vlabel", "info", "scale", "order"): v = getattr(self, k, None) if v is not None: if isinstance(v, bool): v = v and "yes" or "no" elif isinstance(v, (tuple, list)): v = " ".join(v) - conf.append('graph_%s %s' % (k, v)) + conf.append("graph_%s %s" % (k, v)) for field_name, field_args in self.fields: for arg_name, arg_value in field_args.items(): - conf.append('%s.%s %s' % (field_name, arg_name, arg_value)) + conf.append("%s.%s %s" % (field_name, arg_name, arg_value)) print("\n".join(conf)) @@ -45,7 +45,7 @@ def suggest(self): sys.exit(1) def run(self): - cmd = ((len(sys.argv) > 1) and sys.argv[1] or None) or "execute" + cmd = ((len(sys.argv) > 1) and sys.argv[1] or None) or "execute" if cmd == "execute": values = self.execute() if values: @@ -67,11 +67,12 @@ def run(self): self.suggest() sys.exit(0) + class MuninClient(object): def __init__(self, host, port=4949): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) - self.sock.recv(4096) # welcome, TODO: receive all + self.sock.recv(4096) # welcome, TODO: receive all def _command(self, cmd, term): self.sock.send("%s\n" % cmd) @@ -81,15 +82,15 @@ def _command(self, cmd, term): return buf.split(term)[0] def list(self): - return self._command('list', '\n').split(' ') + return self._command("list", "\n").split(" ") def fetch(self, service): data = self._command("fetch %s" % service, ".\n") - if data.startswith('#'): + if data.startswith("#"): raise Exception(data[2:]) values = {} - for line in data.split('\n'): + for line in data.split("\n"): if line: - k, v = line.split(' ', 1) - values[k.split('.')[0]] = Decimal(v) + k, v = line.split(" ", 1) + values[k.split(".")[0]] = Decimal(v) return values diff --git a/archive/munin/munin/cassandra.py b/archive/munin/munin/cassandra.py index f5a75405c4..9c9429b730 100755 --- a/archive/munin/munin/cassandra.py +++ b/archive/munin/munin/cassandra.py @@ -2,12 +2,13 @@ import re import socket import time -from subprocess import Popen, PIPE +from subprocess import PIPE, Popen from vendor.munin import MuninPlugin space_re = re.compile(r"\s+") + class MuninCassandraPlugin(MuninPlugin): category = "Cassandra" @@ -15,7 +16,7 @@ def __init__(self, *args, **kwargs): super(MuninCassandraPlugin, self).__init__(*args, **kwargs) self.nodetool_path = os.environ["NODETOOL_PATH"] self.host = socket.gethostname() - self.keyspaces = [x for x in os.environ.get('CASSANDRA_KEYSPACE', '').split(',') if x] + self.keyspaces = [x for x in os.environ.get("CASSANDRA_KEYSPACE", "").split(",") if x] def execute_nodetool(self, cmd): p = Popen([self.nodetool_path, "-host", self.host, cmd], stdout=PIPE) @@ -23,22 +24,22 @@ def execute_nodetool(self, cmd): return output def parse_cfstats(self, text): - text = text.strip().split('\n') + text = text.strip().split("\n") cfstats = {} cf = None for line in text: line = line.strip() - if not line or line.startswith('-'): + if not line or line.startswith("-"): continue - name, value = line.strip().split(': ', 1) + name, value = line.strip().split(": ", 1) if name == "Keyspace": - ks = {'cf': {}} + ks = {"cf": {}} cf = None cfstats[value] = ks elif name == "Column Family": cf = {} - ks['cf'][value] = cf + ks["cf"][value] = cf elif cf is None: ks[name] = value else: @@ -50,30 +51,30 @@ def cfstats(self): def cinfo(self): text = self.execute_nodetool("info") - lines = text.strip().split('\n') + lines = text.strip().split("\n") token = lines[0] info = {} for l in lines[1:]: - name, value = l.split(':') + name, value = l.split(":") info[name.strip()] = value.strip() - l_num, l_units = info['Load'].split(' ', 1) + l_num, l_units = info["Load"].split(" ", 1) l_num = float(l_num) if l_units == "KB": scale = 1024 elif l_units == "MB": - scale = 1024*1024 + scale = 1024 * 1024 elif l_units == "GB": - scale = 1024*1024*1024 + scale = 1024 * 1024 * 1024 elif l_units == "TB": - scale = 1024*1024*1024*1024 - info['Load'] = int(l_num * scale) - info['token'] = token + scale = 1024 * 1024 * 1024 * 1024 + info["Load"] = int(l_num * scale) + info["token"] = token return info def tpstats(self): out = self.execute_nodetool("tpstats") tpstats = {} - for line in out.strip().split('\n')[1:]: + for line in out.strip().split("\n")[1:]: name, active, pending, completed = space_re.split(line) tpstats[name] = dict(active=int(active), pending=int(pending), completed=int(completed)) return tpstats diff --git a/archive/munin/munin/ddwrt.py b/archive/munin/munin/ddwrt.py index b053e14ea7..bfd5baf730 100755 --- a/archive/munin/munin/ddwrt.py +++ b/archive/munin/munin/ddwrt.py @@ -1,23 +1,21 @@ - # https://192.168.1.10/Info.live.htm import os import re import urllib.request + from vendor.munin import MuninPlugin + class DDWrtPlugin(MuninPlugin): category = "Wireless" def __init__(self): super(DDWrtPlugin, self).__init__() - self.root_url = os.environ.get('DDWRT_URL') or "http://192.168.1.1" + self.root_url = os.environ.get("DDWRT_URL") or "http://192.168.1.1" self.url = self.root_url + "/Info.live.htm" def get_info(self): res = urllib.request.urlopen(self.url) text = res.read() - return dict( - x[1:-1].split('::') - for x in text.split('\n') - ) + return dict(x[1:-1].split("::") for x in text.split("\n")) diff --git a/archive/munin/munin/gearman.py b/archive/munin/munin/gearman.py index cf5a1a86e8..95854eb19b 100755 --- a/archive/munin/munin/gearman.py +++ b/archive/munin/munin/gearman.py @@ -3,18 +3,20 @@ import os import re import socket + from vendor.munin import MuninPlugin -worker_re = re.compile(r'^(?P\d+) (?P[\d\.]+) (?P[^\s]+) :\s?(?P.*)$') +worker_re = re.compile(r"^(?P\d+) (?P[\d\.]+) (?P[^\s]+) :\s?(?P.*)$") + class MuninGearmanPlugin(MuninPlugin): category = "Gearman" def __init__(self): super(MuninGearmanPlugin, self).__init__() - addr = os.environ.get('GM_SERVER') or "127.0.0.1" - port = int(addr.split(':')[-1]) if ':' in addr else 4730 - host = addr.split(':')[0] + addr = os.environ.get("GM_SERVER") or "127.0.0.1" + port = int(addr.split(":")[-1]) if ":" in addr else 4730 + host = addr.split(":")[0] self.addr = (host, port) self._sock = None @@ -36,12 +38,12 @@ def get_workers(self): buf += sock.recv(8192) info = [] - for l in buf.split('\n'): - if l.strip() == '.': + for l in buf.split("\n"): + if l.strip() == ".": break m = worker_re.match(l) i = m.groupdict() - i['abilities'] = [x for x in i['abilities'].split(' ') if x] + i["abilities"] = [x for x in i["abilities"].split(" ") if x] info.append(i) return info @@ -53,14 +55,14 @@ def get_status(self): buf += sock.recv(8192) info = {} - for l in buf.split('\n'): + for l in buf.split("\n"): l = l.strip() - if l == '.': + if l == ".": break - counts = l.split('\t') + counts = l.split("\t") info[counts[0]] = dict( - total = int(counts[1]), - running = int(counts[2]), - workers = int(counts[3]), + total=int(counts[1]), + running=int(counts[2]), + workers=int(counts[3]), ) return info diff --git a/archive/munin/munin/memcached.py b/archive/munin/munin/memcached.py index 0663344576..e1de0f04f5 100755 --- a/archive/munin/munin/memcached.py +++ b/archive/munin/munin/memcached.py @@ -2,8 +2,10 @@ import os import socket + from vendor.munin import MuninPlugin + class MuninMemcachedPlugin(MuninPlugin): category = "Memcached" @@ -15,16 +17,16 @@ def autoconf(self): return True def get_stats(self): - host = os.environ.get('MEMCACHED_HOST') or '127.0.0.1' - port = int(os.environ.get('MEMCACHED_PORT') or '11211') + host = os.environ.get("MEMCACHED_HOST") or "127.0.0.1" + port = int(os.environ.get("MEMCACHED_PORT") or "11211") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s.send("stats\n") buf = "" - while 'END\r\n' not in buf: + while "END\r\n" not in buf: buf += s.recv(1024) - stats = (x.split(' ', 2) for x in buf.split('\r\n')) - stats = dict((x[1], x[2]) for x in stats if x[0] == 'STAT') + stats = (x.split(" ", 2) for x in buf.split("\r\n")) + stats = dict((x[1], x[2]) for x in stats if x[0] == "STAT") s.close() return stats diff --git a/archive/munin/munin/mongodb.py b/archive/munin/munin/mongodb.py index 4011040529..adffb13ffc 100755 --- a/archive/munin/munin/mongodb.py +++ b/archive/munin/munin/mongodb.py @@ -2,8 +2,10 @@ import os import sys + from vendor.munin import MuninPlugin + class MuninMongoDBPlugin(MuninPlugin): dbname_in_args = False category = "MongoDB" @@ -13,13 +15,13 @@ def __init__(self): self.dbname = None if self.dbname_in_args: - self.dbname = sys.argv[0].rsplit('_', 1)[-1] + self.dbname = sys.argv[0].rsplit("_", 1)[-1] if not self.dbname: - self.dbname = os.environ.get('MONGODB_DATABASE') + self.dbname = os.environ.get("MONGODB_DATABASE") - host = os.environ.get('MONGODB_SERVER') or 'localhost' - if ':' in host: - host, port = host.split(':') + host = os.environ.get("MONGODB_SERVER") or "localhost" + if ":" in host: + host, port = host.split(":") port = int(port) else: port = 27017 @@ -27,14 +29,15 @@ def __init__(self): @property def connection(self): - if not hasattr(self, '_connection'): + if not hasattr(self, "_connection"): import pymongo + self._connection = pymongo.MongoClient(self.server[0], self.server[1]) return self._connection @property def db(self): - if not hasattr(self, '_db'): + if not hasattr(self, "_db"): self._db = getattr(self.connection, self.dbname) return self._db diff --git a/archive/munin/munin/mysql.py b/archive/munin/munin/mysql.py index 119de734dd..03d01ba3cc 100755 --- a/archive/munin/munin/mysql.py +++ b/archive/munin/munin/mysql.py @@ -1,7 +1,11 @@ -import os, sys, re +import os +import re +import sys from configparser import SafeConfigParser + from vendor.munin import MuninPlugin + class MuninMySQLPlugin(MuninPlugin): dbname_in_args = False category = "MySQL" @@ -9,12 +13,15 @@ class MuninMySQLPlugin(MuninPlugin): def __init__(self): super(MuninMySQLPlugin, self).__init__() - self.dbname = ((sys.argv[0].rsplit('_', 1)[-1] if self.dbname_in_args else None) - or os.environ.get('DATABASE') or self.default_table) + self.dbname = ( + (sys.argv[0].rsplit("_", 1)[-1] if self.dbname_in_args else None) + or os.environ.get("DATABASE") + or self.default_table + ) self.conninfo = dict( - user = "root", - host = "localhost", + user="root", + host="localhost", ) cnfpath = "" @@ -34,19 +41,25 @@ def __init__(self): for section in ["client", "munin"]: if not cnf.has_section(section): continue - for connkey, opt in [("user", "user"), ("passwd", "password"), ("host", "host"), ("port", "port")]: + for connkey, opt in [ + ("user", "user"), + ("passwd", "password"), + ("host", "host"), + ("port", "port"), + ]: if cnf.has_option(section, opt): self.conninfo[connkey] = cnf.get(section, opt) - for k in ('user', 'passwd', 'host', 'port'): + for k in ("user", "passwd", "host", "port"): # Use lowercase because that's what the existing mysql plugins do v = os.environ.get(k) if v: self.conninfo[k] = v def connection(self): - if not hasattr(self, '_connection'): + if not hasattr(self, "_connection"): import MySQLdb + self._connection = MySQLdb.connect(**self.conninfo) return self._connection diff --git a/archive/munin/munin/nginx.py b/archive/munin/munin/nginx.py index 383e3af127..b8918627a8 100755 --- a/archive/munin/munin/nginx.py +++ b/archive/munin/munin/nginx.py @@ -3,8 +3,10 @@ import os import re import urllib.request + from vendor.munin import MuninPlugin + class MuninNginxPlugin(MuninPlugin): category = "Nginx" @@ -12,11 +14,12 @@ class MuninNginxPlugin(MuninPlugin): r"Active connections:\s+(?P\d+)\s+" r"server accepts handled requests\s+" r"(?P\d+)\s+(?P\d+)\s+(?P\d+)\s+" - r"Reading: (?P\d+) Writing: (?P\d+) Waiting: (?P\d+)") + r"Reading: (?P\d+) Writing: (?P\d+) Waiting: (?P\d+)" + ) def __init__(self): super(MuninNginxPlugin, self).__init__() - self.url = os.environ.get('NX_STATUS_URL') or "http://localhost/nginx_status" + self.url = os.environ.get("NX_STATUS_URL") or "http://localhost/nginx_status" def autoconf(self): return bool(self.get_status()) diff --git a/archive/munin/munin/pgbouncer.py b/archive/munin/munin/pgbouncer.py index d8f3dd96ff..a41d274922 100755 --- a/archive/munin/munin/pgbouncer.py +++ b/archive/munin/munin/pgbouncer.py @@ -1,6 +1,8 @@ import sys + from vendor.munin.postgres import MuninPostgresPlugin + class MuninPgBouncerPlugin(MuninPostgresPlugin): dbname_in_args = False default_table = "pgbouncer" @@ -8,11 +10,12 @@ class MuninPgBouncerPlugin(MuninPostgresPlugin): def __init__(self, *args, **kwargs): super(MuninPgBouncerPlugin, self).__init__(*args, **kwargs) - self.dbwatched = sys.argv[0].rsplit('_', 1)[-1] + self.dbwatched = sys.argv[0].rsplit("_", 1)[-1] def connection(self): - if not hasattr(self, '_connection'): + if not hasattr(self, "_connection"): import psycopg2 + self._connection = psycopg2.connect(self.dsn) self._connection.set_isolation_level(0) return self._connection @@ -25,9 +28,8 @@ def execute(self): totals = dict.fromkeys((field[0] for field in self.fields), 0) for row in cursor: row_dict = dict(zip(columns, row)) - if row_dict['database'] in (self.dbwatched, self.dbwatched + '\x00'): + if row_dict["database"] in (self.dbwatched, self.dbwatched + "\x00"): for field in self.fields: totals[field[0]] += row_dict[field[0]] return dict((field[0], totals[field[0]]) for field in self.fields) - diff --git a/archive/munin/munin/postgres.py b/archive/munin/munin/postgres.py index 541c25bae7..1920796187 100755 --- a/archive/munin/munin/postgres.py +++ b/archive/munin/munin/postgres.py @@ -1,7 +1,9 @@ +import os +import sys -import os, sys from vendor.munin import MuninPlugin + class MuninPostgresPlugin(MuninPlugin): dbname_in_args = False category = "PostgreSQL" @@ -10,18 +12,22 @@ class MuninPostgresPlugin(MuninPlugin): def __init__(self): super(MuninPostgresPlugin, self).__init__() - self.dbname = ((sys.argv[0].rsplit('_', 1)[-1] if self.dbname_in_args else None) - or os.environ.get('PGDATABASE') or self.default_table) + self.dbname = ( + (sys.argv[0].rsplit("_", 1)[-1] if self.dbname_in_args else None) + or os.environ.get("PGDATABASE") + or self.default_table + ) dsn = ["dbname='%s'" % self.dbname] - for k in ('user', 'password', 'host', 'port'): - v = os.environ.get('DB%s' % k.upper()) + for k in ("user", "password", "host", "port"): + v = os.environ.get("DB%s" % k.upper()) if v: dsn.append("db%s='%s'" % (k, v)) - self.dsn = ' '.join(dsn) + self.dsn = " ".join(dsn) def connection(self): - if not hasattr(self, '_connection'): + if not hasattr(self, "_connection"): import psycopg2 + self._connection = psycopg2.connect(self.dsn) return self._connection @@ -32,13 +38,14 @@ def autoconf(self): return bool(self.connection()) def tables(self): - if not hasattr(self, '_tables'): + if not hasattr(self, "_tables"): c = self.cursor() c.execute( "SELECT c.relname FROM pg_catalog.pg_class c" " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace" " WHERE c.relkind IN ('r','')" " AND n.nspname NOT IN ('pg_catalog', 'pg_toast')" - " AND pg_catalog.pg_table_is_visible(c.oid)") + " AND pg_catalog.pg_table_is_visible(c.oid)" + ) self._tables = [r[0] for r in c.fetchall()] return self._tables diff --git a/archive/munin/munin/redis.py b/archive/munin/munin/redis.py index a569adc06c..24b953c077 100755 --- a/archive/munin/munin/redis.py +++ b/archive/munin/munin/redis.py @@ -2,8 +2,10 @@ import os import socket + from vendor.munin import MuninPlugin + class MuninRedisPlugin(MuninPlugin): category = "Redis" @@ -15,9 +17,9 @@ def autoconf(self): return True def get_info(self): - host = os.environ.get('REDIS_HOST') or '127.0.0.1' - port = int(os.environ.get('REDIS_PORT') or '6379') - if host.startswith('/'): + host = os.environ.get("REDIS_HOST") or "127.0.0.1" + port = int(os.environ.get("REDIS_PORT") or "6379") + if host.startswith("/"): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(host) else: @@ -25,9 +27,9 @@ def get_info(self): s.connect((host, port)) s.send("*1\r\n$4\r\ninfo\r\n") buf = "" - while '\r\n' not in buf: + while "\r\n" not in buf: buf += s.recv(1024) - l, buf = buf.split('\r\n', 1) + l, buf = buf.split("\r\n", 1) if l[0] != "$": s.close() raise Exception("Protocol error") @@ -35,7 +37,7 @@ def get_info(self): if remaining > 0: buf += s.recv(remaining) s.close() - return dict(x.split(':', 1) for x in buf.split('\r\n') if ':' in x) + return dict(x.split(":", 1) for x in buf.split("\r\n") if ":" in x) def execute(self): stats = self.get_info() diff --git a/archive/munin/munin/riak.py b/archive/munin/munin/riak.py index 7d30e48ca4..3b3fa51f03 100755 --- a/archive/munin/munin/riak.py +++ b/archive/munin/munin/riak.py @@ -4,20 +4,23 @@ import json except ImportError: import simplejson as json + import os import sys import urllib.request + from vendor.munin import MuninPlugin + class MuninRiakPlugin(MuninPlugin): category = "Riak" def __init__(self): super(MuninRiakPlugin, self).__init__() - host = os.environ.get('RIAK_HOST') or 'localhost' - if ':' in host: - host, port = host.split(':') + host = os.environ.get("RIAK_HOST") or "localhost" + if ":" in host: + host, port = host.split(":") port = int(port) else: port = 8098 diff --git a/archive/munin/mysql_dbrows_ b/archive/munin/mysql_dbrows_ index b5f2bdc5cd..812620d890 100755 --- a/archive/munin/mysql_dbrows_ +++ b/archive/munin/mysql_dbrows_ @@ -3,6 +3,7 @@ from vendor.munin.mysql import MuninMySQLPlugin + class MuninMySQLDBRowsPlugin(MuninMySQLPlugin): dbname_in_args = True args = "-l 0 --base 1000" diff --git a/archive/munin/mysql_dbsize_ b/archive/munin/mysql_dbsize_ index 1c30b701ba..a4b7dd9db6 100755 --- a/archive/munin/mysql_dbsize_ +++ b/archive/munin/mysql_dbsize_ @@ -3,6 +3,7 @@ from vendor.munin.mysql import MuninMySQLPlugin + class MuninMySQLDBSizePlugin(MuninMySQLPlugin): dbname_in_args = True args = "-l 0 --base 1024" diff --git a/archive/munin/nginx_connections b/archive/munin/nginx_connections index ca7eabe3d4..c4632cc5c6 100755 --- a/archive/munin/nginx_connections +++ b/archive/munin/nginx_connections @@ -3,8 +3,10 @@ import os import re import urllib + from vendor.munin.nginx import MuninNginxPlugin + class MuninNginxConnectionsPlugin(MuninNginxPlugin): title = "Nginx Connections" args = "--base 1000" diff --git a/archive/munin/nginx_requests b/archive/munin/nginx_requests index 1ef06280da..ce846359ce 100755 --- a/archive/munin/nginx_requests +++ b/archive/munin/nginx_requests @@ -3,8 +3,10 @@ import os import re import urllib + from vendor.munin.nginx import MuninNginxPlugin + class MuninNginxRequestsPlugin(MuninNginxPlugin): title = "Nginx Requests" args = "--base 1000" diff --git a/archive/munin/path_size b/archive/munin/path_size index bfa258e954..128f780d98 100755 --- a/archive/munin/path_size +++ b/archive/munin/path_size @@ -2,8 +2,10 @@ import os import subprocess + from vendor.munin import MuninPlugin + class PathSizePlugin(MuninPlugin): args = "--base 1024 -l 0" vlabel = "bytes" diff --git a/archive/munin/pgbouncer_pools_cl_ b/archive/munin/pgbouncer_pools_cl_ index 249929b586..12aa7ba012 100755 --- a/archive/munin/pgbouncer_pools_cl_ +++ b/archive/munin/pgbouncer_pools_cl_ @@ -3,6 +3,7 @@ from vendor.munin.pgbouncer import MuninPgBouncerPlugin + class MuninPgBouncerPoolsClientPlugin(MuninPgBouncerPlugin): command = "SHOW POOLS" vlabel = "Connections" diff --git a/archive/munin/pgbouncer_pools_sv_ b/archive/munin/pgbouncer_pools_sv_ index 09c6095c1d..c794a11e69 100755 --- a/archive/munin/pgbouncer_pools_sv_ +++ b/archive/munin/pgbouncer_pools_sv_ @@ -3,6 +3,7 @@ from vendor.munin.pgbouncer import MuninPgBouncerPlugin + class MuninPgBouncerPoolsServerPlugin(MuninPgBouncerPlugin): command = "SHOW POOLS" vlabel = "Connections" diff --git a/archive/munin/pgbouncer_stats_avg_bytes_ b/archive/munin/pgbouncer_stats_avg_bytes_ index dbaaafd98a..039105f919 100755 --- a/archive/munin/pgbouncer_stats_avg_bytes_ +++ b/archive/munin/pgbouncer_stats_avg_bytes_ @@ -3,6 +3,7 @@ from vendor.munin.pgbouncer import MuninPgBouncerPlugin + class MuninPgBouncerStatsBytesServerPlugin(MuninPgBouncerPlugin): command = "SHOW STATS" vlabel = "Bytes" diff --git a/archive/munin/pgbouncer_stats_avg_query_ b/archive/munin/pgbouncer_stats_avg_query_ index f913dc9c54..3124a4e214 100755 --- a/archive/munin/pgbouncer_stats_avg_query_ +++ b/archive/munin/pgbouncer_stats_avg_query_ @@ -3,6 +3,7 @@ from vendor.munin.pgbouncer import MuninPgBouncerPlugin + class MuninPgBouncerStatsQueryServerPlugin(MuninPgBouncerPlugin): command = "SHOW STATS" vlabel = "Microseconds" diff --git a/archive/munin/pgbouncer_stats_avg_req_ b/archive/munin/pgbouncer_stats_avg_req_ index 929e45589f..90da415e55 100755 --- a/archive/munin/pgbouncer_stats_avg_req_ +++ b/archive/munin/pgbouncer_stats_avg_req_ @@ -3,6 +3,7 @@ from vendor.munin.pgbouncer import MuninPgBouncerPlugin + class MuninPgBouncerStatsRequestsServerPlugin(MuninPgBouncerPlugin): command = "SHOW STATS" vlabel = "Requests" diff --git a/archive/munin/postgres_block_read_ b/archive/munin/postgres_block_read_ index 55c673890d..57d8d3e319 100755 --- a/archive/munin/postgres_block_read_ +++ b/archive/munin/postgres_block_read_ @@ -21,6 +21,7 @@ for a (short) description. from vendor.munin.postgres import MuninPostgresPlugin + class MuninPostgresBlockReadPlugin(MuninPostgresPlugin): dbname_in_args = True args = "--base 1000" diff --git a/archive/munin/postgres_commits_ b/archive/munin/postgres_commits_ index 39baf9b0bb..c88b411394 100755 --- a/archive/munin/postgres_commits_ +++ b/archive/munin/postgres_commits_ @@ -24,6 +24,7 @@ Find out more at from vendor.munin.postgres import MuninPostgresPlugin + class MuninPostgresCommitsPlugin(MuninPostgresPlugin): dbname_in_args = True args = "--base 1000" diff --git a/archive/munin/postgres_connections b/archive/munin/postgres_connections index 8f5a49291f..12641150a2 100755 --- a/archive/munin/postgres_connections +++ b/archive/munin/postgres_connections @@ -2,6 +2,7 @@ from vendor.munin.postgres import MuninPostgresPlugin + class MuninPostgresConnectionsPlugin(MuninPostgresPlugin): dbname_in_args = False title = "Postgres active connections" diff --git a/archive/munin/postgres_locks b/archive/munin/postgres_locks index 80b8e42b4e..45763a5053 100755 --- a/archive/munin/postgres_locks +++ b/archive/munin/postgres_locks @@ -9,6 +9,7 @@ Show postgres lock statistics. from vendor.munin.postgres import MuninPostgresPlugin + class MuninPostgresLocksPlugin(MuninPostgresPlugin): dbname_in_args = False title = "Postgres locks" diff --git a/archive/munin/postgres_queries_ b/archive/munin/postgres_queries_ index 5cd2be6f8d..fbf09519f9 100755 --- a/archive/munin/postgres_queries_ +++ b/archive/munin/postgres_queries_ @@ -16,6 +16,7 @@ Find out more at from vendor.munin.postgres import MuninPostgresPlugin + class MuninPostgresQueriesPlugin(MuninPostgresPlugin): dbname_in_args = True args = "--base 1000" diff --git a/archive/munin/postgres_space_ b/archive/munin/postgres_space_ index d3ad6a001f..4d19605647 100755 --- a/archive/munin/postgres_space_ +++ b/archive/munin/postgres_space_ @@ -9,6 +9,7 @@ Plugin to monitor PostgreSQL disk usage. from vendor.munin.postgres import MuninPostgresPlugin + class MuninPostgresSpacePlugin(MuninPostgresPlugin): dbname_in_args = True args = "-l 0 --base 1024" diff --git a/archive/munin/postgres_table_sizes b/archive/munin/postgres_table_sizes index c174d52c34..6ea4651291 100755 --- a/archive/munin/postgres_table_sizes +++ b/archive/munin/postgres_table_sizes @@ -5,6 +5,7 @@ from vendor.munin.postgres import MuninPostgresPlugin + class PostgresTableSizes(MuninPostgresPlugin): vlabel = "Table Size" title = "Table Sizes" diff --git a/archive/munin/redis_active_connections b/archive/munin/redis_active_connections index ada659f489..5bd1d51a88 100755 --- a/archive/munin/redis_active_connections +++ b/archive/munin/redis_active_connections @@ -2,6 +2,7 @@ from vendor.munin.redis import MuninRedisPlugin + class MuninRedisActiveConnectionsPlugin(MuninRedisPlugin): title = "Redis active connections" args = "--base 1000" diff --git a/archive/munin/redis_commands b/archive/munin/redis_commands index 0e86805d99..7ba967d448 100755 --- a/archive/munin/redis_commands +++ b/archive/munin/redis_commands @@ -2,6 +2,7 @@ from vendor.munin.redis import MuninRedisPlugin + class MuninRedisCommandsPlugin(MuninRedisPlugin): title = "Redis commands" args = "--base 1000" diff --git a/archive/munin/redis_connects b/archive/munin/redis_connects index f56519a903..a51c9db77b 100755 --- a/archive/munin/redis_connects +++ b/archive/munin/redis_connects @@ -2,6 +2,7 @@ from vendor.munin.redis import MuninRedisPlugin + class MuninRedisTotalConnectionsPlugin(MuninRedisPlugin): title = "Redis connects" args = "--base 1000" diff --git a/archive/munin/redis_size b/archive/munin/redis_size index dcdf309b97..dc87aeaf31 100755 --- a/archive/munin/redis_size +++ b/archive/munin/redis_size @@ -1,6 +1,10 @@ #!/srv/newsblur/venv/newsblur3/bin/python +import os + +import redis + from utils.munin.base import MuninGraph -import os, redis + class NBMuninGraph(MuninGraph): diff --git a/archive/munin/redis_used_memory b/archive/munin/redis_used_memory index 86bb32ba40..3b5d6cb639 100755 --- a/archive/munin/redis_used_memory +++ b/archive/munin/redis_used_memory @@ -2,6 +2,7 @@ from vendor.munin.redis import MuninRedisPlugin + class MuninRedisUsedMemoryPlugin(MuninRedisPlugin): title = "Redis used memory" args = "--base 1024" diff --git a/archive/munin/request_time b/archive/munin/request_time index f8c7a07144..edabb1baa3 100755 --- a/archive/munin/request_time +++ b/archive/munin/request_time @@ -2,10 +2,12 @@ import os from time import time -from urllib2 import urlopen, Request + +from urllib2 import Request, urlopen from vendor.munin import MuninPlugin + class MuninRequestTimePlugin(MuninPlugin): title = "Request Time" args = "--base 1000" diff --git a/archive/munin/riak_ops b/archive/munin/riak_ops index 372d6270f7..8470f37d66 100755 --- a/archive/munin/riak_ops +++ b/archive/munin/riak_ops @@ -3,6 +3,7 @@ from vendor.munin.riak import MuninRiakPlugin + class RiakOpsPlugin(MuninRiakPlugin): args = "-l 0 --base 1000" vlabel = "ops/sec" diff --git a/archive/munin/tc_size b/archive/munin/tc_size index bb3f49cefb..e21b6a1253 100755 --- a/archive/munin/tc_size +++ b/archive/munin/tc_size @@ -5,6 +5,7 @@ import subprocess from vendor.munin import MuninPlugin + class MuninTokyoCabinetSizePlugin(MuninPlugin): title = "Size of Tokyo Cabinet database" args = "--base 1024" diff --git a/clients/ios/Classes/ActivityModule.m b/clients/ios/Classes/ActivityModule.m index 59637f53f9..5656e03de0 100644 --- a/clients/ios/Classes/ActivityModule.m +++ b/clients/ios/Classes/ActivityModule.m @@ -154,7 +154,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger activitiesCount = [appDelegate.userActivitiesArray count]; int minimumHeight; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { minimumHeight = MINIMUM_ACTIVITY_HEIGHT_IPAD; } else { minimumHeight = MINIMUM_ACTIVITY_HEIGHT_IPHONE; @@ -165,7 +165,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa } id activityCell; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { activityCell = [[ActivityCell alloc] init]; } else { activityCell = [[SmallActivityCell alloc] init]; @@ -185,7 +185,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N ActivityCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ActivityCell"]; if (cell == nil) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { cell = [[ActivityCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ActivityCell"]; @@ -304,7 +304,7 @@ - (UITableViewCell *)makeLoadingCell { UIImage *img = [UIImage imageNamed:@"fleuron.png"]; UIImageView *fleuron = [[UIImageView alloc] initWithImage:img]; int height; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { height = MINIMUM_ACTIVITY_HEIGHT_IPAD; } else { height = MINIMUM_ACTIVITY_HEIGHT_IPHONE; diff --git a/clients/ios/Classes/AddSiteViewController.h b/clients/ios/Classes/AddSiteViewController.h index 5023bf4b98..2e7c3d24a9 100644 --- a/clients/ios/Classes/AddSiteViewController.h +++ b/clients/ios/Classes/AddSiteViewController.h @@ -7,15 +7,10 @@ // #import -#import "NewsBlurAppDelegate.h" #import "NewsBlur-Swift.h" -@class NewsBlurAppDelegate; - @interface AddSiteViewController : BaseViewController - { - NewsBlurAppDelegate *appDelegate; -} + - (void)reload; - (IBAction)addSite; diff --git a/clients/ios/Classes/AddSiteViewController.m b/clients/ios/Classes/AddSiteViewController.m index b8517af6e8..7ffb83bf8b 100644 --- a/clients/ios/Classes/AddSiteViewController.m +++ b/clients/ios/Classes/AddSiteViewController.m @@ -8,7 +8,6 @@ #import "AddSiteViewController.h" #import "AddSiteAutocompleteCell.h" -#import "NewsBlurAppDelegate.h" #import "MenuViewController.h" #import "SBJson4.h" #import "NewsBlur-Swift.h" @@ -93,7 +92,7 @@ - (void)viewWillAppear:(BOOL)animated { //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } else if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { // return YES; @@ -130,7 +129,7 @@ - (CGSize)preferredContentSize { } - (IBAction)doCancelButton { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [appDelegate hidePopover]; } else { [appDelegate hidePopoverAnimated:YES]; @@ -272,7 +271,7 @@ - (IBAction)addSite { [self.errorLabel setText:[responseObject valueForKey:@"message"]]; [self.errorLabel setHidden:NO]; } else { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self->appDelegate hidePopover]; } else { [self->appDelegate hidePopoverAnimated:YES]; diff --git a/clients/ios/Classes/AuthorizeServicesViewController.m b/clients/ios/Classes/AuthorizeServicesViewController.m index 2d3e2b9db4..70d0f418ef 100644 --- a/clients/ios/Classes/AuthorizeServicesViewController.m +++ b/clients/ios/Classes/AuthorizeServicesViewController.m @@ -53,7 +53,7 @@ - (void)viewWillAppear:(BOOL)animated { [self.webView loadRequest:requestObj]; }]; - if (self.fromStory && [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (self.fromStory && !appDelegate.isPhone) { UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle: @"Cancel" style: UIBarButtonItemStylePlain @@ -75,6 +75,7 @@ - (void)doCancelButton { } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + BOOL isPhone = appDelegate.isPhone; NSURLRequest *request = navigationAction.request; NSString *URLString = [[request URL] absoluteString]; NSLog(@"URL STRING IS %@", URLString); @@ -86,7 +87,7 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati if (self.fromStory) { [self.appDelegate refreshUserProfile:^{ - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!isPhone) { [self.appDelegate.shareNavigationController viewWillAppear:YES]; [self.appDelegate.modalNavigationController dismissViewControllerAnimated:YES completion:nil]; } else { diff --git a/clients/ios/Classes/AuxSceneDelegate.swift b/clients/ios/Classes/AuxSceneDelegate.swift new file mode 100644 index 0000000000..a1fea1c296 --- /dev/null +++ b/clients/ios/Classes/AuxSceneDelegate.swift @@ -0,0 +1,70 @@ +// +// AuxSceneDelegate.swift +// NewsBlur +// +// Created by David Sinclair on 2024-05-30. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import UIKit + +/// Scene delegate for auxiliary windows. Currently only used on macOS. +class AuxSceneDelegate: UIResponder, UIWindowSceneDelegate { + let appDelegate: NewsBlurAppDelegate = .shared + + var window: UIWindow? +#if targetEnvironment(macCatalyst) + var toolbar = NSToolbar(identifier: "aux") + var toolbarDelegate = ToolbarDelegate() +#endif + + /// Open a new window with an `OriginalStoryViewController` for the given URL. + @objc(openWindowForURL:customTitle:) class func openWindow(for url: URL, customTitle: String) { + let activity = NSUserActivity(activityType: "aux") + + activity.userInfo = ["url" : url, "title" : customTitle] + + if #available(iOS 17.0, *) { + let request = UISceneSessionActivationRequest(userActivity: activity) + + UIApplication.shared.activateSceneSession(for: request) { error in + print("Error activating scene: \(error)") + } + } else { + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil) { error in + print("Error activating scene: \(error)") + } + } + } + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { +#if targetEnvironment(macCatalyst) + guard let windowScene = scene as? UIWindowScene, //let titlebar = windowScene.titlebar, + let userInfo = connectionOptions.userActivities.first?.userInfo else { + return + } + + let url = userInfo["url"] as? URL + let title = userInfo["title"] as? String + + let controller = OriginalStoryViewController() + + windowScene.title = "Loading…" + window?.rootViewController = controller + + appDelegate.activeOriginalStoryURL = url + + controller.customPageTitle = title + _ = controller.view + controller.loadInitialStory() + + //TODO: 🚧 perhaps make a toolbar for this window +// toolbar.delegate = toolbarDelegate +// toolbar.displayMode = .iconOnly +// +// titlebar.toolbar = toolbar +// titlebar.toolbarStyle = .automatic + +#endif + } +} diff --git a/clients/ios/Classes/BaseViewController.h b/clients/ios/Classes/BaseViewController.h index 7666afb74a..68c87a463c 100644 --- a/clients/ios/Classes/BaseViewController.h +++ b/clients/ios/Classes/BaseViewController.h @@ -1,9 +1,23 @@ #import #import "MBProgressHUD.h" +@class NewsBlurAppDelegate; + @interface BaseViewController : UIViewController { + NewsBlurAppDelegate *appDelegate; } +@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; + +@property (nonatomic, readonly) BOOL isPhone; +@property (nonatomic, readonly) BOOL isMac; +@property (nonatomic, readonly) BOOL isVision; +@property (nonatomic, readonly) BOOL isPortrait; +@property (nonatomic, readonly) BOOL isCompactWidth; +@property (nonatomic, readonly) BOOL isGrid; +@property (nonatomic, readonly) BOOL isFeedShown; +@property (nonatomic, readonly) BOOL isStoryShown; + - (void)informError:(id)error; - (void)informError:(id)error statusCode:(NSInteger)statusCode; - (void)informMessage:(NSString *)message; @@ -13,6 +27,7 @@ - (void)addKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle wantPriority:(BOOL)wantPriority; - (void)addCancelKeyCommandWithAction:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle; +- (void)systemAppearanceDidChange:(BOOL)isDark; - (void)updateTheme; - (void)tableView:(UITableView *)tableView redisplayCellAtIndexPath:(NSIndexPath *)indexPath; @@ -25,5 +40,55 @@ - (void)collectionView:(UICollectionView *)collectionView selectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition; - (void)collectionView:(UICollectionView *)collectionView deselectItemAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated; +- (IBAction)newSite:(id)sender; +- (IBAction)reloadFeeds:(id)sender; +- (IBAction)showMuteSites:(id)sender; +- (IBAction)showOrganizeSites:(id)sender; +- (IBAction)showWidgetSites:(id)sender; +- (IBAction)showNotifications:(id)sender; +- (IBAction)showFindFriends:(id)sender; +- (IBAction)showPremium:(id)sender; +- (IBAction)showSupportForum:(id)sender; +- (IBAction)showLogout:(id)sender; + +- (IBAction)findInFeeds:(id)sender; +- (IBAction)findInFeedDetail:(id)sender; + +- (IBAction)chooseColumns:(id)sender; +- (IBAction)chooseLayout:(id)sender; +- (IBAction)chooseTitle:(id)sender; +- (IBAction)choosePreview:(id)sender; +- (IBAction)chooseGridColumns:(id)sender; +- (IBAction)chooseGridHeight:(id)sender; +- (IBAction)chooseFontSize:(id)sender; +- (IBAction)chooseSpacing:(id)sender; +- (IBAction)chooseTheme:(id)sender; + +- (IBAction)moveSite:(id)sender; +- (IBAction)openRenameSite:(id)sender; +- (IBAction)muteSite:(id)sender; +- (IBAction)deleteSite:(id)sender; +- (IBAction)openTrainSite:(id)sender; +- (IBAction)openNotifications:(id)sender; +- (IBAction)openStatistics:(id)sender; +- (IBAction)instaFetchFeed:(id)sender; +- (IBAction)doMarkAllRead:(id)sender; +- (IBAction)openMarkReadMenu:(id)sender; +- (IBAction)openSettingsMenu:(id)sender; +- (IBAction)nextSite:(id)sender; +- (IBAction)previousSite:(id)sender; +- (IBAction)nextFolder:(id)sender; +- (IBAction)previousFolder:(id)sender; +- (IBAction)openAllStories:(id)sender; + +- (IBAction)showSendTo:(id)sender; +- (IBAction)showTrain:(id)sender; +- (IBAction)showShare:(id)sender; +- (IBAction)nextUnreadStory:(id)sender; +- (IBAction)nextStory:(id)sender; +- (IBAction)previousStory:(id)sender; +- (IBAction)toggleTextStory:(id)sender; +- (IBAction)openInBrowser:(id)sender; + @end diff --git a/clients/ios/Classes/BaseViewController.m b/clients/ios/Classes/BaseViewController.m index f026a48210..47a48f8fee 100644 --- a/clients/ios/Classes/BaseViewController.m +++ b/clients/ios/Classes/BaseViewController.m @@ -4,17 +4,33 @@ @implementation BaseViewController +@synthesize appDelegate; + #pragma mark - #pragma mark HTTP requests - (instancetype)init { if (self = [super init]) { - + self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; } return self; } +- (void)awakeFromNib { + [super awakeFromNib]; + + self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; +} + +- (BOOL)becomeFirstResponder { + BOOL success = [super becomeFirstResponder]; + + NSLog(@"%@ becomeFirstResponder: %@", self, success ? @"yes" : @"no"); // log + + return success; +} + #pragma mark - #pragma mark View methods @@ -37,7 +53,7 @@ - (void)informError:(id)error details:(NSString *)details statusCode:(NSInteger) return [self informError:@"The server barfed!"]; } else { errorMessage = [error localizedDescription]; - if ([error code] == 4 && + if ([error code] == 4 && [errorMessage rangeOfString:@"cancelled"].location != NSNotFound) { return; } @@ -45,8 +61,8 @@ - (void)informError:(id)error details:(NSString *)details statusCode:(NSInteger) [MBProgressHUD hideHUDForView:self.view animated:YES]; MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - [HUD setCustomView:[[UIImageView alloc] - initWithImage:[UIImage imageNamed:@"warning.gif"]]]; + [HUD setCustomView:[[UIImageView alloc] + initWithImage:[UIImage imageNamed:@"warning.gif"]]]; [HUD setMode:MBProgressHUDModeCustomView]; if (details) { [HUD setDetailsLabelText:details]; @@ -54,19 +70,19 @@ - (void)informError:(id)error details:(NSString *)details statusCode:(NSInteger) HUD.labelText = errorMessage; [HUD hide:YES afterDelay:(details ? 3 : 1)]; -// UIAlertView* alertView = [[UIAlertView alloc] -// initWithTitle:@"Error" -// message:localizedDescription delegate:nil -// cancelButtonTitle:@"OK" -// otherButtonTitles:nil]; -// [alertView show]; -// [alertView release]; + // UIAlertView* alertView = [[UIAlertView alloc] + // initWithTitle:@"Error" + // message:localizedDescription delegate:nil + // cancelButtonTitle:@"OK" + // otherButtonTitles:nil]; + // [alertView show]; + // [alertView release]; } - (void)informMessage:(NSString *)message { [MBProgressHUD hideHUDForView:self.view animated:YES]; MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; - HUD.mode = MBProgressHUDModeText; + HUD.mode = MBProgressHUDModeText; HUD.labelText = message; [HUD hide:YES afterDelay:.75]; } @@ -78,8 +94,14 @@ - (void)informLoadingMessage:(NSString *)message { [HUD hide:YES afterDelay:2]; } +- (void)systemAppearanceDidChange:(BOOL)isDark { + [[ThemeManager themeManager] systemAppearanceDidChange:isDark]; +} + - (void)updateTheme { // Subclasses should override this, calling super, to update their nav bar, table, etc + + appDelegate.splitViewController.view.backgroundColor = UIColorFromLightDarkRGB(0x555555, 0x777777); } - (void)tableView:(UITableView *)tableView redisplayCellAtIndexPath:(NSIndexPath *)indexPath { @@ -144,7 +166,7 @@ - (void)addCancelKeyCommandWithAction:(SEL)action discoverabilityTitle:(NSString #pragma mark UIViewController - (void) viewDidLoad { - [super viewDidLoad]; + [super viewDidLoad]; BOOL isDark = [NewsBlurAppDelegate sharedAppDelegate].window.windowScene.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; @@ -166,7 +188,7 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { BOOL isDark = [NewsBlurAppDelegate sharedAppDelegate].window.windowScene.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; - [[ThemeManager themeManager] systemAppearanceDidChange:isDark]; + [self systemAppearanceDidChange:isDark]; } - (UIStatusBarStyle)preferredStatusBarStyle { @@ -177,4 +199,463 @@ - (UIStatusBarStyle)preferredStatusBarStyle { return UIStatusBarStyleLightContent; } +- (BOOL)isPhone { + return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone; +} + +- (BOOL)isMac { + return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomMac; +} + +- (BOOL)isVision { + if (@available(iOS 17.0, *)) { + return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomVision; + } else { + return NO; + } +} + +- (BOOL)isPortrait { + UIWindow *window = [NewsBlurAppDelegate sharedAppDelegate].window; + UIInterfaceOrientation orientation = window.windowScene.interfaceOrientation; + if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { + return YES; + } else { + return NO; + } +} + +- (BOOL)isCompactWidth { + UIWindow *window = [NewsBlurAppDelegate sharedAppDelegate].window; + UITraitCollection *traits = window.windowScene.traitCollection; + + return traits.horizontalSizeClass == UIUserInterfaceSizeClassCompact; + //return self.compactWidth > 0.0; +} + +- (BOOL)isGrid { + return self.appDelegate.detailViewController.storyTitlesInGrid; +} + +- (BOOL)isFeedShown { + return appDelegate.storiesCollection.activeFeed != nil || appDelegate.storiesCollection.activeFolder != nil; +} + +- (BOOL)isStoryShown { + return !appDelegate.storyPagesViewController.currentPage.view.isHidden && appDelegate.storyPagesViewController.currentPage.noStoryMessage.isHidden; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + if (action == @selector(chooseLayout:) || action == @selector(findInFeedDetail:)) { + return self.isFeedShown; + } else if (action == @selector(chooseTitle:) || action == @selector(choosePreview:)) { + return self.isFeedShown && !self.isGrid; + } else if (action == @selector(chooseGridColumns:) || action == @selector(chooseGridHeight:)) { + return self.isFeedShown && self.isGrid; + } else if (action == @selector(openTrainSite) || + action == @selector(openTrainSite:) || + action == @selector(openNotifications:) || + action == @selector(openStatistics:) || + action == @selector(moveSite:) || + action == @selector(openRenameSite:) || + action == @selector(deleteSite:)) { + return self.isFeedShown && appDelegate.storiesCollection.isCustomFolderOrFeed; + } else if (action == @selector(muteSite) || + action == @selector(muteSite:)) { + return self.isFeedShown && !appDelegate.storiesCollection.isRiverView; + } else if (action == @selector(instaFetchFeed:) || + action == @selector(doMarkAllRead:)) { + return self.isFeedShown; + } else if (action == @selector(showSendTo:) || + action == @selector(showTrain:) || + action == @selector(showShare:) || + action == @selector(nextUnreadStory:) || + action == @selector(nextStory:) || + action == @selector(previousStory:) || + action == @selector(toggleTextStory:) || + action == @selector(openInBrowser:)) { + return self.isStoryShown; + } else { + return [super canPerformAction:action withSender:sender]; + } +} + +- (void)validateCommand:(UICommand *)command { + [super validateCommand:command]; + + if (command.action == @selector(chooseColumns:)) { + command.state = [command.propertyList isEqualToString:appDelegate.detailViewController.behaviorString]; + } else if (command.action == @selector(chooseLayout:)) { + NSString *value = self.appDelegate.storiesCollection.activeStoryTitlesPosition; + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(chooseIntelligence:)) { + NSInteger intelligence = [[NSUserDefaults standardUserDefaults] integerForKey:@"selectedIntelligence"]; + NSString *value = [NSString stringWithFormat:@"%@", @(intelligence + 1)]; + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(toggleSidebar:)) { + UISplitViewController *splitViewController = self.appDelegate.splitViewController; + if (splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModeTwoBesideSecondary) { + command.title = @"Show Sidebar"; + } else { + command.title = @"Hide Sidebar"; + } + } else if (command.action == @selector(chooseTitle:)) { + NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:@"story_list_preview_text_size"]; + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(choosePreview:)) { + NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:@"story_list_preview_images_size"]; + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(chooseGridColumns:)) { + NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:@"grid_columns"]; + if (value == nil) { + value = @"auto"; + } + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(chooseGridHeight:)) { + NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:@"grid_height"]; + if (value == nil) { + value = @"medium"; + } + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(chooseFontSize:)) { + NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:@"feed_list_font_size"]; + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(chooseSpacing:)) { + NSString *value = [[NSUserDefaults standardUserDefaults] objectForKey:@"feed_list_spacing"]; + command.state = [command.propertyList isEqualToString:value]; + } else if (command.action == @selector(chooseTheme:)) { + command.state = [command.propertyList isEqualToString:ThemeManager.themeManager.theme]; + } else if (command.action == @selector(openRenameSite:)) { + if (appDelegate.storiesCollection.isRiverOrSocial) { + command.title = @"Rename Folder…"; + } else { + command.title = @"Rename Site…"; + } + } else if (command.action == @selector(deleteSite:)) { + if (appDelegate.storiesCollection.isRiverOrSocial) { + command.title = @"Delete Folder…"; + } else { + command.title = @"Delete Site…"; + } + } else if (command.action == @selector(toggleStorySaved:)) { + BOOL isRead = [[self.appDelegate.activeStory objectForKey:@"starred"] boolValue]; + if (isRead) { + command.title = @"Unsave This Story"; + } else { + command.title = @"Save This Story"; + } + } else if (command.action == @selector(toggleStoryUnread:)) { + BOOL isRead = [[self.appDelegate.activeStory objectForKey:@"read_status"] boolValue]; + if (isRead) { + command.title = @"Mark as Unread"; + } else { + command.title = @"Mark as Read"; + } + } +} + +#pragma mark - +#pragma mark File menu + +- (IBAction)newSite:(id)sender { + [appDelegate.feedsViewController tapAddSite:nil]; +} + +- (IBAction)reloadFeeds:(id)sender { + [appDelegate reloadFeedsView:NO]; +} + +- (IBAction)showMuteSites:(id)sender { + [self.appDelegate showMuteSites]; +} + +- (IBAction)showOrganizeSites:(id)sender { + [self.appDelegate showOrganizeSites]; +} + +- (IBAction)showWidgetSites:(id)sender { + [self.appDelegate showWidgetSites]; +} + +- (IBAction)showNotifications:(id)sender { + [self.appDelegate openNotificationsWithFeed:nil]; +} + +- (IBAction)showFindFriends:(id)sender { + [self.appDelegate showFindFriends]; +} + +- (IBAction)showPremium:(id)sender { + [self.appDelegate showPremiumDialog]; +} + +- (IBAction)showSupportForum:(id)sender { + NSURL *url = [NSURL URLWithString:@"https://forum.newsblur.com"]; + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; +} + +- (IBAction)showLogout:(id)sender { + [self.appDelegate confirmLogout]; +} + +#pragma mark - +#pragma mark Edit menu + +- (IBAction)findInFeeds:(id)sender { + [self.appDelegate showColumn:UISplitViewControllerColumnPrimary debugInfo:@"findInFeeds"]; + [self.appDelegate.feedsViewController.searchBar becomeFirstResponder]; +} + +- (IBAction)findInFeedDetail:(id)sender { + [self.appDelegate showColumn:UISplitViewControllerColumnSupplementary debugInfo:@"findInFeedDetail"]; + [self.appDelegate.feedDetailViewController.searchBar becomeFirstResponder]; +} + +#pragma mark - +#pragma mark View menu + +- (IBAction)chooseColumns:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"split_behavior"]; + + [UIView animateWithDuration:0.5 animations:^{ + [self.appDelegate updateSplitBehavior:YES]; + }]; + + [self.appDelegate.detailViewController updateLayoutWithReload:NO fetchFeeds:YES]; +} + +- (IBAction)chooseLayout:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + NSString *key = self.appDelegate.storiesCollection.storyTitlesPositionKey; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:key]; + + [self.appDelegate.detailViewController updateLayoutWithReload:YES fetchFeeds:YES]; +} + +- (IBAction)chooseIntelligence:(id)sender { + UICommand *command = sender; + NSInteger index = [command.propertyList integerValue]; + + [self.appDelegate.feedsViewController.intelligenceControl setSelectedSegmentIndex:index]; + [self.appDelegate.feedsViewController selectIntelligence]; +} + +- (IBAction)chooseTitle:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"story_list_preview_text_size"]; + + [self.appDelegate resizePreviewSize]; +} + +- (IBAction)choosePreview:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"story_list_preview_images_size"]; + + [self.appDelegate resizePreviewSize]; +} + +- (IBAction)chooseGridColumns:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"grid_columns"]; + + [self.appDelegate.detailViewController updateLayoutWithReload:YES fetchFeeds:YES]; +} + +- (IBAction)chooseGridHeight:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"grid_height"]; + + [self.appDelegate.detailViewController updateLayoutWithReload:YES fetchFeeds:YES]; +} + +- (IBAction)chooseFontSize:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"feed_list_font_size"]; + + [self.appDelegate resizeFontSize]; +} + +- (IBAction)chooseSpacing:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [[NSUserDefaults standardUserDefaults] setObject:string forKey:@"feed_list_spacing"]; + + [self.appDelegate.feedsViewController reloadFeedTitlesTable]; + [self.appDelegate.feedDetailViewController reloadWithSizing]; +} + +- (IBAction)chooseTheme:(id)sender { + UICommand *command = sender; + NSString *string = command.propertyList; + + [ThemeManager themeManager].theme = string; +} + +- (IBAction)toggleSidebar:(id)sender{ + UISplitViewController *splitViewController = self.appDelegate.splitViewController; + + [UIView animateWithDuration:0.2 animations:^{ + NSLog(@"toggleSidebar: displayMode: %@; preferredDisplayMode: %@; UISplitViewControllerDisplayModeSecondaryOnly: %@; UISplitViewControllerDisplayModeTwoBesideSecondary: %@, UISplitViewControllerDisplayModeOneBesideSecondary: %@; ", @(splitViewController.displayMode), @(splitViewController.preferredDisplayMode), @(UISplitViewControllerDisplayModeSecondaryOnly), @(UISplitViewControllerDisplayModeTwoBesideSecondary), @(UISplitViewControllerDisplayModeOneBesideSecondary)); // log + + if (splitViewController.splitBehavior == UISplitViewControllerSplitBehaviorOverlay) { + splitViewController.preferredDisplayMode = (splitViewController.displayMode != UISplitViewControllerDisplayModeTwoOverSecondary ? UISplitViewControllerDisplayModeTwoOverSecondary : UISplitViewControllerDisplayModeOneOverSecondary); + } else if (splitViewController.splitBehavior == UISplitViewControllerSplitBehaviorDisplace) { + if (splitViewController.preferredDisplayMode == UISplitViewControllerDisplayModeTwoDisplaceSecondary && + splitViewController.displayMode == UISplitViewControllerDisplayModeSecondaryOnly) { + splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeTwoDisplaceSecondary; + }); + } else { + splitViewController.preferredDisplayMode = (splitViewController.displayMode != UISplitViewControllerDisplayModeTwoDisplaceSecondary ? UISplitViewControllerDisplayModeTwoDisplaceSecondary : UISplitViewControllerDisplayModeOneBesideSecondary); + } + } else { + if (splitViewController.preferredDisplayMode == UISplitViewControllerDisplayModeTwoBesideSecondary && + splitViewController.displayMode == UISplitViewControllerDisplayModeSecondaryOnly) { + splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeOneBesideSecondary; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeTwoBesideSecondary; + }); + } else { + splitViewController.preferredDisplayMode = (splitViewController.displayMode != UISplitViewControllerDisplayModeTwoBesideSecondary ? UISplitViewControllerDisplayModeTwoBesideSecondary : UISplitViewControllerDisplayModeOneBesideSecondary); + } + } + }]; +} + +#pragma mark - +#pragma mark Site menu + +- (IBAction)moveSite:(id)sender { + [self.appDelegate.feedDetailViewController openMoveView:self.appDelegate.navigationController]; +} + +- (IBAction)openRenameSite:(id)sender { + [self.appDelegate.feedDetailViewController openRenameSite]; +} + +- (IBAction)muteSite:(id)sender { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Are you sure you wish to mute %@?", self.appDelegate.storiesCollection.activeTitle] message:nil preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle: @"Mute Site" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { + [alertController dismissViewControllerAnimated:YES completion:nil]; + [self.appDelegate.feedDetailViewController muteSite]; + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel handler:nil]]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (IBAction)deleteSite:(id)sender { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Are you sure you wish to delete %@?", self.appDelegate.storiesCollection.activeTitle] message:nil preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle: @"Delete Site" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { + [alertController dismissViewControllerAnimated:YES completion:nil]; + [self.appDelegate.feedDetailViewController deleteSite]; + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel handler:nil]]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (IBAction)openTrainSite:(id)sender { + [self.appDelegate.feedDetailViewController openTrainSite]; +} + +- (IBAction)openNotifications:(id)sender { + [self.appDelegate.feedDetailViewController openNotifications:sender]; +} + +- (IBAction)openStatistics:(id)sender { + [self.appDelegate.feedDetailViewController openStatistics:sender]; +} + +- (IBAction)instaFetchFeed:(id)sender { + [self.appDelegate.feedDetailViewController instafetchFeed]; +} + +- (IBAction)doMarkAllRead:(id)sender { + [self.appDelegate.feedDetailViewController doMarkAllRead:sender]; +} + +// These two are needed for the toolbar in Grid view. +- (IBAction)openMarkReadMenu:(id)sender { + [self.appDelegate.feedDetailViewController doOpenMarkReadMenu:sender]; +} + +- (IBAction)openSettingsMenu:(id)sender { + [self.appDelegate.feedDetailViewController doOpenSettingsMenu:sender]; +} + +- (IBAction)nextSite:(id)sender { + [self.appDelegate.feedsViewController selectNextFeed:sender]; +} + +- (IBAction)previousSite:(id)sender { + [self.appDelegate.feedsViewController selectPreviousFeed:sender]; +} + +- (IBAction)nextFolder:(id)sender { + [self.appDelegate.feedsViewController selectNextFolder:sender]; +} + +- (IBAction)previousFolder:(id)sender { + [self.appDelegate.feedsViewController selectPreviousFolder:sender]; +} + +- (IBAction)openAllStories:(id)sender { + [self.appDelegate.feedsViewController selectEverything:sender]; +} + +#pragma mark - +#pragma mark Story menu + +- (IBAction)showSendTo:(id)sender { + [appDelegate showSendTo:self sender:sender]; +} + +- (IBAction)showTrain:(id)sender { + [self.appDelegate openTrainStory:self.appDelegate.storyPagesViewController.fontSettingsButton]; +} + +- (IBAction)showShare:(id)sender { + [self.appDelegate.storyPagesViewController.currentPage openShareDialog]; +} + +- (IBAction)nextUnreadStory:(id)sender { + [self.appDelegate.storyPagesViewController doNextUnreadStory:sender]; +} + +- (IBAction)nextStory:(id)sender { + [self.appDelegate.storyPagesViewController changeToNextPage:sender]; +} + +- (IBAction)previousStory:(id)sender { + [self.appDelegate.storyPagesViewController changeToPreviousPage:sender]; +} + +- (IBAction)toggleTextStory:(id)sender { + [self.appDelegate.storyPagesViewController toggleTextView:sender]; +} + +- (IBAction)openInBrowser:(id)sender { + [self.appDelegate.storyPagesViewController showOriginalSubview:sender]; +} + @end diff --git a/clients/ios/Classes/DetailViewController.swift b/clients/ios/Classes/DetailViewController.swift index 8a92af4a5a..2fbb1fd619 100644 --- a/clients/ios/Classes/DetailViewController.swift +++ b/clients/ios/Classes/DetailViewController.swift @@ -10,11 +10,6 @@ import UIKit /// Manages the detail column of the split view, with the feed detail and/or the story pages. class DetailViewController: BaseViewController { - /// Returns the shared app delegate. - var appDelegate: NewsBlurAppDelegate { - return NewsBlurAppDelegate.shared() - } - /// Preference keys. enum Key { /// Style of the feed detail list layout. @@ -28,6 +23,9 @@ class DetailViewController: BaseViewController { /// Position of the divider between the views when in vertical orientation. Only used for `.top` and `.bottom` layouts. static let verticalPosition = "story_titles_divider_vertical" + + /// Width of the feeds view, i.e. the primary split column. + static let feedsWidth = "split_primary_width" } /// Preference values. @@ -173,7 +171,7 @@ class DetailViewController: BaseViewController { /// How the split controller behaves. var behavior: Behavior { - switch UserDefaults.standard.string(forKey: Key.behavior) { + switch behaviorString { case BehaviorValue.tile: return .tile case BehaviorValue.displace: @@ -185,20 +183,15 @@ class DetailViewController: BaseViewController { } } - /// Returns `true` if the device is an iPhone, otherwise `false`. - @objc var isPhone: Bool { - return UIDevice.current.userInterfaceIdiom == .phone - } - - /// Returns `true` if the window is in portrait orientation, otherwise `false`. - @objc var isPortraitOrientation: Bool { - return view.window?.windowScene?.interfaceOrientation.isPortrait ?? false + /// The split controller behavior as a raw string. + @objc var behaviorString: String { + return UserDefaults.standard.string(forKey: Key.behavior) ?? BehaviorValue.auto } /// Position of the divider between the views. var dividerPosition: CGFloat { get { - let key = isPortraitOrientation ? Key.verticalPosition : Key.horizontalPosition + let key = isPortrait ? Key.verticalPosition : Key.horizontalPosition let value = CGFloat(UserDefaults.standard.float(forKey: key)) if value == 0 { @@ -212,12 +205,32 @@ class DetailViewController: BaseViewController { return } - let key = isPortraitOrientation ? Key.verticalPosition : Key.horizontalPosition + let key = isPortrait ? Key.verticalPosition : Key.horizontalPosition UserDefaults.standard.set(Float(newValue), forKey: key) } } + /// Width of the feeds view, i.e. the primary split column. + var feedsWidth: CGFloat { + get { + let value = CGFloat(UserDefaults.standard.float(forKey: Key.feedsWidth)) + + if value == 0 { + return 320 + } else { + return value + } + } + set { + guard newValue != feedsWidth else { + return + } + + UserDefaults.standard.set(Float(newValue), forKey: Key.feedsWidth) + } + } + /// Top container view. @IBOutlet weak var topContainerView: UIView! @@ -395,6 +408,16 @@ class DetailViewController: BaseViewController { } } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let width = splitViewController?.primaryColumnWidth ?? 320 + + if width != feedsWidth { + feedsWidth = width + } + } + private func adjustTopConstraint() { guard let scene = view.window?.windowScene else { return @@ -453,6 +476,13 @@ private extension DetailViewController { func checkViewControllers() { let isTop = layout == .top +#if targetEnvironment(macCatalyst) + splitViewController?.primaryBackgroundStyle = .sidebar + splitViewController?.minimumPrimaryColumnWidth = 250 + splitViewController?.maximumPrimaryColumnWidth = 700 + splitViewController?.preferredPrimaryColumnWidth = feedsWidth +#endif + if layout != .grid || isPhone { storyPagesViewController = listStoryPagesViewController _ = storyPagesViewController?.view diff --git a/clients/ios/Classes/Feed.swift b/clients/ios/Classes/Feed.swift new file mode 100644 index 0000000000..57ef2fc5ec --- /dev/null +++ b/clients/ios/Classes/Feed.swift @@ -0,0 +1,212 @@ +// +// Feed.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-04. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import Foundation + +// The Feed, Story, and StoryCache classes could be quite useful going forward; Rather than calling getStory() to get the dictionary, could have a variation that returns a Story instance. Could fetch from the cache if available, or make and cache one from the dictionary. Would need to remove it from the cache when changing anything about a story. Could perhaps make the cache part of StoriesCollection. + +/// A dictionary with the most broad key and value types, common in ObjC code. +typealias AnyDictionary = [AnyHashable : Any] + +/// A feed, wrapping the dictionary representation. +class Feed: Identifiable { + let id: String + var name = "" + var subscribers = 0 + + var dictionary = AnyDictionary() + + var isRiverOrSocial = false + + var colorBarLeft: UIColor? + var colorBarRight: UIColor? + + lazy var image: UIImage? = { + guard let appDelegate = NewsBlurAppDelegate.shared else { + return nil + } + + if let image = appDelegate.getFavicon(id) { + return Utilities.roundCorneredImage(image, radius: 4, convertTo: CGSizeMake(16, 16)) + } else { + return nil + } + }() + + var classifiers: AnyDictionary? { + guard let appDelegate = NewsBlurAppDelegate.shared else { + return nil + } + + return appDelegate.storiesCollection.activeClassifiers[id] as? AnyDictionary + } + + func classifiers(for kind: String) -> AnyDictionary? { + return classifiers?[kind] as? AnyDictionary + } + + enum Score: Int { + case none = 0 + case like = 1 + case dislike = -1 + + var imageName: String { + switch self { + case .none: + return "hand.thumbsup" + case .like: + return "hand.thumbsup.fill" + case .dislike: + return "hand.thumbsdown.fill" + } + } + } + + struct Training: Identifiable { + let name: String + let count: Int + let score: Score + + var id: String { + return name + } + } + + lazy var titles: [Training] = { + guard let appDelegate = NewsBlurAppDelegate.shared, + let classifierTitles = self.classifiers(for: "titles") else { + return [] + } + + let userTitles = classifierTitles.map { Training(name: $0.key as! String, count: 0, score: Score(rawValue: $0.value as? Int ?? 0) ?? .none) } + + return userTitles.sorted() + }() + + lazy var authors: [Training] = { + guard let appDelegate = NewsBlurAppDelegate.shared, + let classifierAuthors = self.classifiers(for: "authors"), + let activeAuthors = appDelegate.storiesCollection.activePopularAuthors as? [[AnyHashable]] else { + return [] + } + + var userAuthors = [Training]() + + for (someName, someScore) in classifierAuthors { + if let name = someName as? String, let score = someScore as? Int, !activeAuthors.contains(where: { $0[0] == someName }) { + userAuthors.append(Training(name: name, count: 0, score: Score(rawValue: score) ?? .none)) + } + } + + let otherAuthors: [Training] = activeAuthors.map { Training(name: $0[0] as! String, count: $0[1] as! Int, score: Score(rawValue: classifierAuthors[$0[0] as! String] as? Int ?? 0) ?? .none) } + + return userAuthors.sorted() + otherAuthors + }() + + lazy var tags: [Training] = { + guard let appDelegate = NewsBlurAppDelegate.shared, + let classifierTags = self.classifiers(for: "tags"), + let activeTags = appDelegate.storiesCollection.activePopularTags as? [[AnyHashable]] else { + return [] + } + + var userTags = [Training]() + + for (someName, someScore) in classifierTags { + if let name = someName as? String, let score = someScore as? Int, !activeTags.contains(where: { $0[0] == someName }) { + userTags.append(Training(name: name, count: 0, score: Score(rawValue: score) ?? .none)) + } + } + + let otherTags: [Training] = activeTags.map { Training(name: $0[0] as! String, count: $0[1] as! Int, score: Score(rawValue: classifierTags[$0[0] as! String] as? Int ?? 0) ?? .none) } + + return userTags.sorted() + otherTags + }() + + init(id: String) { + self.id = id + + guard let appDelegate = NewsBlurAppDelegate.shared else { + return + } + + var feed: [String : Any]? = appDelegate.dictActiveFeeds[id] as? [String : Any] + + if feed == nil { + feed = appDelegate.dictFeeds[id] as? [String : Any] + } + + guard let feed else { + return + } + + dictionary = feed + + load() + } + + init(dictionary: AnyDictionary) { + id = "\(dictionary["id"] ?? "")" + + self.dictionary = dictionary + + load() + } + + private func load() { + guard let appDelegate = NewsBlurAppDelegate.shared, let storiesCollection = appDelegate.storiesCollection else { + return + } + + name = dictionary["feed_title"] as? String ?? "" + subscribers = dictionary["num_subscribers"] as? Int ?? 0 + + colorBarLeft = color(for: "favicon_fade", from: dictionary, default: "707070") + colorBarRight = color(for: "favicon_color", from: dictionary, default: "505050") + + isRiverOrSocial = storiesCollection.isRiverOrSocial + } + + func color(for key: String, from feed: AnyDictionary, default defaultHex: String) -> UIColor { + let hex = feed[key] as? String ?? defaultHex + let scanner = Scanner(string: hex) + var color: Int64 = 0 + scanner.scanHexInt64(&color) + let value = Int(color) + + return ThemeManager.shared.fixedColor(fromRGB: value) ?? UIColor.gray + } +} + +extension Feed: Equatable { + static func == (lhs: Feed, rhs: Feed) -> Bool { + return lhs.id == rhs.id + } +} + +extension Feed: CustomDebugStringConvertible { + var debugDescription: String { + return "Feed \"\(name)\" (\(id))" + } +} + +extension Feed.Training: Hashable { + static func == (lhs: Feed.Training, rhs: Feed.Training) -> Bool { + return lhs.name == rhs.name + } + + func hash(into hasher: inout Hasher) { + hasher.combine(name) + } +} + +extension Feed.Training: Comparable { + static func < (lhs: Feed.Training, rhs: Feed.Training) -> Bool { + return lhs.name < rhs.name + } +} diff --git a/clients/ios/Classes/FeedChooserTitleView.m b/clients/ios/Classes/FeedChooserTitleView.m index 106548bbb8..8f090aab5a 100644 --- a/clients/ios/Classes/FeedChooserTitleView.m +++ b/clients/ios/Classes/FeedChooserTitleView.m @@ -79,7 +79,7 @@ - (void)drawRect:(CGRect)rect { UIImage *folderImage = [UIImage imageNamed:@"folder-open"]; CGFloat folderImageViewX = 10.0; - if ([[UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPad) { + if (((NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate]).isPhone) { folderImageViewX = 7.0; } diff --git a/clients/ios/Classes/FeedChooserViewController.h b/clients/ios/Classes/FeedChooserViewController.h index 6c94bf1a42..71d6abb7fb 100644 --- a/clients/ios/Classes/FeedChooserViewController.h +++ b/clients/ios/Classes/FeedChooserViewController.h @@ -18,9 +18,7 @@ typedef NS_ENUM(NSUInteger, FeedChooserOperation) }; -@interface FeedChooserViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; -} +@interface FeedChooserViewController : BaseViewController @property (weak) IBOutlet UITableView *tableView; diff --git a/clients/ios/Classes/FeedChooserViewController.m b/clients/ios/Classes/FeedChooserViewController.m index 21ac5443e4..76bcdaf177 100644 --- a/clients/ios/Classes/FeedChooserViewController.m +++ b/clients/ios/Classes/FeedChooserViewController.m @@ -30,7 +30,6 @@ @interface FeedChooserViewController () @property (nonatomic) FeedChooserSort sort; @property (nonatomic) BOOL ascending; @property (nonatomic) BOOL flat; -@property (nonatomic, readonly) NewsBlurAppDelegate *appDelegate; @property (nonatomic, strong) NSUserDefaults *groupDefaults; @property (nonatomic, readonly) NSArray *widgetFeeds; @@ -45,8 +44,6 @@ - (void)dealloc { - (void)viewDidLoad { [super viewDidLoad]; - appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - if (self.operation == FeedChooserOperationWidgetSites) { self.groupDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.newsblur.NewsBlur-Group"]; } @@ -830,6 +827,36 @@ - (NSInteger)tableView:(UITableView *)theTableView sectionForSectionIndexTitle:( return indexIndex; } +#if TARGET_OS_MACCATALYST +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { + NSArray *selectedRows = [tableView indexPathsForSelectedRows]; + if ([selectedRows containsObject:indexPath]) { + [tableView deselectRowAtIndexPath:indexPath animated:false]; + return nil; + } + + return indexPath; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath { + NSArray *selectedRows = [tableView indexPathsForSelectedRows]; + if ([selectedRows containsObject:indexPath]) { + return nil; + } + + return indexPath; +} + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { + NSArray *selectedRows = [tableView indexPathsForSelectedRows]; + for (NSIndexPath *index in selectedRows) { + [[tableView cellForRowAtIndexPath:index] setHighlighted:YES]; + } + + return YES; +} +#endif + - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (self.operation == FeedChooserOperationWidgetSites) { [self deselectRowsOutsideSection:indexPath.section]; diff --git a/clients/ios/Classes/FeedDetailCardView.swift b/clients/ios/Classes/FeedDetailCardView.swift index edad9fc990..1a1720060f 100644 --- a/clients/ios/Classes/FeedDetailCardView.swift +++ b/clients/ios/Classes/FeedDetailCardView.swift @@ -75,6 +75,7 @@ struct CardView: View { } Button { + cache.appDelegate.activeStory = story.dictionary cache.appDelegate.feedDetailViewController.markFeedsRead(fromTimestamp: story.timestamp, andOlder: false) cache.appDelegate.feedDetailViewController.reload() } label: { @@ -82,12 +83,15 @@ struct CardView: View { } Button { + cache.appDelegate.activeStory = story.dictionary cache.appDelegate.feedDetailViewController.markFeedsRead(fromTimestamp: story.timestamp, andOlder: true) cache.appDelegate.feedDetailViewController.reload() } label: { Label("Mark older stories read", image: "mark-read") } + Divider() + Button { cache.appDelegate.storiesCollection.toggleStorySaved(story.dictionary) cache.appDelegate.feedDetailViewController.reload() @@ -96,13 +100,15 @@ struct CardView: View { } Button { + cache.appDelegate.activeStory = story.dictionary cache.appDelegate.showSend(to: cache.appDelegate.feedDetailViewController, sender: cache.appDelegate.feedDetailViewController.view) } label: { Label("Send this story to…", image: "email") } Button { - cache.appDelegate.openTrainStory(nil) + cache.appDelegate.activeStory = story.dictionary + cache.appDelegate.openTrainStory(cache.appDelegate.feedDetailViewController.view) } label: { Label("Train this story", image: "train") } @@ -168,14 +174,14 @@ struct CardContentView: View { var body: some View { VStack(alignment: .leading) { - if story.isRiverOrSocial, let feedImage { + if let feed = story.feed, feed.isRiverOrSocial, let feedImage = feed.image { HStack { Image(uiImage: feedImage) .resizable() .frame(width: 16, height: 16) .padding(.leading, cache.settings.spacing == .compact ? 20 : 24) - Text(story.feedName) + Text(feed.name) .font(font(named: "WhitneySSm-Medium", size: 12)) .lineLimit(1) .foregroundColor(feedColor) @@ -239,14 +245,6 @@ struct CardContentView: View { } } - var feedImage: UIImage? { - if let image = cache.appDelegate.getFavicon(story.feedID) { - return Utilities.roundCorneredImage(image, radius: 4, convertTo: CGSizeMake(16, 16)) - } else { - return nil - } - } - var unreadImage: UIImage? { guard story.isReadAvailable else { return nil @@ -304,7 +302,7 @@ struct CardFeedBarView: View { var body: some View { GeometryReader { geometry in - if let color = story.feedColorBarLeft { + if let feed = story.feed, let color = feed.colorBarLeft { Path { path in path.move(to: CGPoint(x: 0, y: 0)) path.addLine(to: CGPoint(x: 0, y: geometry.size.height)) @@ -312,7 +310,7 @@ struct CardFeedBarView: View { .stroke(Color(color), lineWidth: 4) } - if let color = story.feedColorBarRight { + if let feed = story.feed, let color = feed.colorBarRight { Path { path in path.move(to: CGPoint(x: 4, y: 0)) path.addLine(to: CGPoint(x: 4, y: geometry.size.height)) diff --git a/clients/ios/Classes/FeedDetailGridView.swift b/clients/ios/Classes/FeedDetailGridView.swift index 8b802e1cae..a7020b6a6e 100644 --- a/clients/ios/Classes/FeedDetailGridView.swift +++ b/clients/ios/Classes/FeedDetailGridView.swift @@ -136,6 +136,7 @@ struct FeedDetailGridView: View { } } .modify({ view in +#if !targetEnvironment(macCatalyst) if #available(iOS 15.0, *) { view.refreshable { if cache.canPullToRefresh { @@ -143,6 +144,7 @@ struct FeedDetailGridView: View { } } } +#endif }) } .background(Color.themed([0xE0E0E0, 0xFFF8CA, 0x363636, 0x101010])) diff --git a/clients/ios/Classes/FeedDetailObjCViewController.h b/clients/ios/Classes/FeedDetailObjCViewController.h index 8434d29949..836efca9ab 100644 --- a/clients/ios/Classes/FeedDetailObjCViewController.h +++ b/clients/ios/Classes/FeedDetailObjCViewController.h @@ -14,7 +14,6 @@ #import "MCSwipeTableViewCell.h" #import "FeedDetailTableCell.h" -@class NewsBlurAppDelegate; @class MCSwipeTableViewCell; @interface FeedDetailObjCViewController : BaseViewController @@ -23,8 +22,6 @@ MCSwipeTableViewCellDelegate, UIGestureRecognizerDelegate, UISearchBarDelegate, UITableViewDragDelegate> { - NewsBlurAppDelegate *appDelegate; - BOOL pageFetching; BOOL pageFinished; BOOL finishedAnimatingIn; @@ -39,7 +36,6 @@ NBNotifier *notifier; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic, strong) IBOutlet UITableView *storyTitlesTable; @property (nonatomic) IBOutlet UIBarButtonItem * feedMarkReadButton; @property (nonatomic) IBOutlet UIBarButtonItem * feedsBarButton; @@ -50,7 +46,9 @@ @property (nonatomic) IBOutlet UIBarButtonItem * titleImageBarButton; @property (nonatomic, retain) NBNotifier *notifier; @property (nonatomic, retain) StoriesCollection *storiesCollection; +#if !TARGET_OS_MACCATALYST @property (nonatomic) UIRefreshControl *refreshControl; +#endif @property (nonatomic) UISearchBar *searchBar; @property (nonatomic) IBOutlet UIView *messageView; @property (nonatomic) IBOutlet UILabel *messageLabel; @@ -112,16 +110,19 @@ - (void)loadStoryAtRow:(NSInteger)row; - (void)redrawUnreadStory; - (IBAction)doOpenMarkReadMenu:(id)sender; +- (IBAction)doMarkAllRead:(id)sender; - (IBAction)doOpenSettingsMenu:(id)sender; - (void)deleteSite; - (void)deleteFolder; -- (void)muteSite; -- (void)openTrainSite; +- (IBAction)muteSite; +- (IBAction)openTrainSite; +- (IBAction)openNotifications:(id)sender; - (void)openNotificationsWithFeed:(NSString *)feedId; -- (void)openRenameSite; +- (IBAction)openStatistics:(id)sender; +- (IBAction)openRenameSite; - (void)showUserProfile; - (void)changeActiveFeedDetailRow; -- (void)instafetchFeed; +- (IBAction)instafetchFeed; - (void)changeActiveStoryTitleCellLayout; - (void)didSelectItemAtIndexPath:(NSIndexPath *)indexPath; - (void)loadFaviconsFromActiveFeed; @@ -132,4 +133,7 @@ - (void)failedMarkAsUnsaved:(NSDictionary *)params; - (void)failedMarkAsUnread:(NSDictionary *)params; +- (void)confirmDeleteSite:(UINavigationController *)menuNavigationController; +- (void)openMoveView:(UINavigationController *)menuNavigationController; + @end diff --git a/clients/ios/Classes/FeedDetailObjCViewController.m b/clients/ios/Classes/FeedDetailObjCViewController.m index 3793d14cff..dbe281c6de 100644 --- a/clients/ios/Classes/FeedDetailObjCViewController.m +++ b/clients/ios/Classes/FeedDetailObjCViewController.m @@ -69,7 +69,6 @@ @implementation FeedDetailObjCViewController @synthesize separatorBarButton; @synthesize titleImageBarButton; @synthesize spacerBarButton, spacer2BarButton; -@synthesize appDelegate; @synthesize pageFetching; @synthesize pageFinished; @synthesize finishedAnimatingIn; @@ -92,8 +91,6 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferredContentSizeChanged:) name:UIContentSizeCategoryDidChangeNotification @@ -106,7 +103,7 @@ - (void)viewDidLoad { if (@available(iOS 15.0, *)) { self.storyTitlesTable.allowsFocus = NO; } - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { self.storyTitlesTable.dragDelegate = self; self.storyTitlesTable.dragInteractionEnabled = YES; } @@ -119,10 +116,12 @@ - (void)viewDidLoad { initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; spacer2BarButton.width = 0; +#if !TARGET_OS_MACCATALYST self.refreshControl = [UIRefreshControl new]; self.refreshControl.tintColor = UIColorFromLightDarkRGB(0x0, 0xffffff); self.refreshControl.backgroundColor = UIColorFromRGB(0xE3E6E0); [self.refreshControl addTarget:self action:@selector(refresh:) forControlEvents:UIControlEventValueChanged]; +#endif self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.storyTitlesTable.frame), 44.)]; @@ -150,11 +149,11 @@ - (void)viewDidLoad { self.feedsBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Sites" style:UIBarButtonItemStylePlain target:self action:@selector(doShowFeeds:)]; self.feedsBarButton.accessibilityLabel = @"Show Sites"; - UIImage *settingsImage = [Utilities imageNamed:@"settings" sized:30]; + UIImage *settingsImage = [Utilities imageNamed:@"settings" sized:self.isMac ? 24 : 30]; settingsBarButton = [UIBarButtonItem barItemWithImage:settingsImage target:self action:@selector(doOpenSettingsMenu:)]; settingsBarButton.accessibilityLabel = @"Settings"; - UIImage *markreadImage = [Utilities imageNamed:@"mark-read" sized:30]; + UIImage *markreadImage = [Utilities imageNamed:@"mark-read" sized:self.isMac ? 24 : 30]; feedMarkReadButton = [UIBarButtonItem barItemWithImage:markreadImage target:self action:@selector(doOpenMarkReadMenu:)]; feedMarkReadButton.accessibilityLabel = @"Mark all as read"; @@ -166,16 +165,19 @@ - (void)viewDidLoad { [view addGestureRecognizer:markReadLongPress]; titleImageBarButton = [UIBarButtonItem alloc]; - + +#if TARGET_OS_MACCATALYST + if (@available(macCatalyst 16.0, *)) { + settingsBarButton.hidden = YES; + feedMarkReadButton.hidden = YES; + } +#else UILongPressGestureRecognizer *tableLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleTableLongPress:)]; tableLongPress.minimumPressDuration = 1.0; tableLongPress.delegate = self; [self.storyTitlesTable addGestureRecognizer:tableLongPress]; -#if TARGET_OS_MACCATALYST - // CATALYST: support double-click; doing the following breaks clicking on rows in Catalyst. -#else UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:nil]; doubleTapGesture.numberOfTapsRequired = 2; @@ -406,6 +408,11 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id storiesCollection.storyLocationsCount) { + NSLog(@"âš ï¸ row %@ is greater than the story locations count: %@", @(indexPath.row), @(storiesCollection.storyLocationsCount)); // log + continue; + } + [self reloadIndexPath:indexPath withRowAnimation:UITableViewRowAnimationNone]; break; } @@ -1300,6 +1314,13 @@ - (void)finishedLoadingFeed:(NSDictionary *)results feedPage:(NSInteger)feedPage NSLog(@"finishedLoadingFeed: %@", receivedFeedId); // log +#if TARGET_OS_MACCATALYST + if (@available(macCatalyst 16.0, *)) { + settingsBarButton.hidden = NO; + feedMarkReadButton.hidden = NO; + } +#endif + self.pageFinished = NO; [self renderStories:confirmedNewStories]; @@ -1440,7 +1461,7 @@ - (void)testForTryFeed { NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; NSString *feedOpening = [preferences stringForKey:@"feed_opening"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && feedOpening == nil) { + if (!self.isPhone && feedOpening == nil) { feedOpening = @"story"; } @@ -1493,7 +1514,7 @@ - (void)testForTryFeed { // NSIndexPath *indexPath = [NSIndexPath indexPathForRow:locationOfStoryId inSection:0]; NSIndexPath *indexPath = [self indexPathForStoryLocation:locationOfStoryId]; - if (self.isLegacyTable && self.storyTitlesTable.window != nil) { + if (self.isLegacyTable && self.storyTitlesTable.window != nil && indexPath.row < [self.storyTitlesTable numberOfRowsInSection:0]) { [self tableView:self.storyTitlesTable selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle]; @@ -1621,6 +1642,35 @@ - (UITableViewCell *)makeLoadingCell { toItem:fleuron attribute:NSLayoutAttributeBottom multiplier:1.0 constant:height/2]]; + } else if (!self.isMarkReadOnScroll) { + UIButton *markReadButton = [UIButton buttonWithType:UIButtonTypeCustom]; + + markReadButton.titleLabel.font = [UIFont systemFontOfSize:14]; + [markReadButton setTitle:@" Mark All Stories as Read " forState:UIControlStateNormal]; + + [markReadButton addTarget:self action:@selector(doMarkAllRead:) forControlEvents:UIControlEventTouchUpInside]; + + markReadButton.tintColor = UIColor.whiteColor; + markReadButton.backgroundColor = UIColorFromFixedRGB(0x939EAF); + markReadButton.layer.cornerRadius = 10; + + [markReadButton sizeToFit]; + + markReadButton.translatesAutoresizingMaskIntoConstraints = NO; + + [cell.contentView addSubview:markReadButton]; + [cell.contentView addConstraint:[NSLayoutConstraint constraintWithItem:markReadButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:cell.contentView + attribute:NSLayoutAttributeCenterX + multiplier:1.0 constant:0]]; + [cell.contentView addConstraint:[NSLayoutConstraint constraintWithItem:markReadButton + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:fleuron + attribute:NSLayoutAttributeBottom + multiplier:1.0 constant:height/2]]; } return cell; @@ -2380,6 +2430,21 @@ - (void)markReadShowMenu:(MarkReadShowMenu)showMenu sender:(id)sender { visibleUnreadCount = 0; } +#if TARGET_OS_MACCATALYST + UINavigationController *feedDetailNavController = appDelegate.feedDetailViewController.navigationController; + UIView *sourceView = feedDetailNavController.view; + CGRect sourceRect = CGRectMake(120, 0, 20, 20); + + if (appDelegate.splitViewController.isFeedListHidden) { + sourceRect = CGRectMake(192, 0, 20, 20); + } + + [self.appDelegate showMarkReadMenuWithFeedIds:feedIds collectionTitle:collectionTitle visibleUnreadCount:visibleUnreadCount sourceView:sourceView sourceRect:sourceRect completionHandler:^(BOOL marked){ + if (marked) { + pop(); + } + }]; +#else UIBarButtonItem *barButton = self.feedMarkReadButton; if (sender && [sender isKindOfClass:[UIBarButtonItem class]]) barButton = sender; @@ -2388,6 +2453,7 @@ - (void)markReadShowMenu:(MarkReadShowMenu)showMenu sender:(id)sender { pop(); } }]; +#endif } - (IBAction)doOpenMarkReadMenu:(id)sender { @@ -2406,11 +2472,6 @@ - (BOOL)isRiver { appDelegate.storiesCollection.isReadView; } -- (BOOL)isInfrequent { - return appDelegate.storiesCollection.isRiverView && - [appDelegate.storiesCollection.activeFolder isEqualToString:@"infrequent"]; -} - - (IBAction)doShowFeeds:(id)sender { [self.appDelegate showColumn:UISplitViewControllerColumnPrimary debugInfo:@"showFeeds"]; } @@ -2425,8 +2486,8 @@ - (IBAction)doOpenSettingsMenu:(id)sender { MenuViewController *viewController = [MenuViewController new]; __weak MenuViewController *weakViewController = viewController; - BOOL everything = [appDelegate.storiesCollection.activeFolder isEqualToString:@"everything"]; - BOOL infrequent = [self isInfrequent]; + BOOL everything = appDelegate.storiesCollection.isEverything; + BOOL infrequent = appDelegate.storiesCollection.isInfrequent; BOOL river = [self isRiver]; BOOL read = appDelegate.storiesCollection.isReadView; BOOL widget = appDelegate.storiesCollection.isWidgetView; @@ -2608,7 +2669,19 @@ - (IBAction)doOpenSettingsMenu:(id)sender { UINavigationController *navController = self.navigationController ?: appDelegate.storyPagesViewController.navigationController; +#if TARGET_OS_MACCATALYST + UINavigationController *feedDetailNavController = appDelegate.feedDetailViewController.navigationController; + UIView *sourceView = feedDetailNavController.view; + CGRect sourceRect = CGRectMake(152, 0, 20, 20); + + if (appDelegate.splitViewController.isFeedListHidden) { + sourceRect = CGRectMake(224, 0, 20, 20); + } + + [viewController showFromNavigationController:navController barButtonItem:nil sourceView:sourceView sourceRect:sourceRect permittedArrowDirections:UIPopoverArrowDirectionDown]; +#else [viewController showFromNavigationController:navController barButtonItem:self.settingsBarButton]; +#endif } - (NSString *)feedIdForSearch { @@ -2806,7 +2879,7 @@ - (void)deleteFolder { }]; } -- (void)muteSite { +- (IBAction)muteSite { [MBProgressHUD hideHUDForView:self.view animated:YES]; MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; HUD.labelText = @"Muting..."; @@ -2959,7 +3032,7 @@ - (void)openMoveView:(UINavigationController *)menuNavigationController { [menuNavigationController showViewController:viewController sender:self]; } -- (void)openTrainSite { +- (IBAction)openTrainSite { [appDelegate openTrainSite]; } @@ -2969,15 +3042,27 @@ - (void)toggleHiddenStories { [self reload]; } +- (IBAction)openNotifications:(id)sender { + NSString *feedIdStr = storiesCollection.activeFeedIdStr; + + [appDelegate openNotificationsWithFeed:feedIdStr]; +} + - (void)openNotificationsWithFeed:(NSString *)feedId { [appDelegate openNotificationsWithFeed:feedId]; } +- (IBAction)openStatistics:(id)sender { + NSString *feedIdStr = storiesCollection.activeFeedIdStr; + + [appDelegate openStatisticsWithFeed:feedIdStr sender:settingsBarButton]; +} + - (void)openStatisticsWithFeed:(NSString *)feedId { [appDelegate openStatisticsWithFeed:feedId sender:settingsBarButton]; } -- (void)openRenameSite { +- (IBAction)openRenameSite { NSString *title = [NSString stringWithFormat:@"Rename \"%@\"", appDelegate.storiesCollection.isRiverView ? [appDelegate extractFolderName:appDelegate.storiesCollection.activeFolder] : [appDelegate.storiesCollection.activeFeed objectForKey:@"feed_title"]]; NSString *subtitle = (appDelegate.storiesCollection.isRiverView ? @@ -3068,8 +3153,10 @@ - (void)updateTheme { self.navigationItem.titleView = [appDelegate makeFeedTitle:storiesCollection.activeFeed]; } +#if !TARGET_OS_MACCATALYST self.refreshControl.tintColor = UIColorFromLightDarkRGB(0x0, 0xffffff); self.refreshControl.backgroundColor = UIColorFromRGB(0xE3E6E0); +#endif self.searchBar.backgroundColor = UIColorFromRGB(0xE3E6E0); self.searchBar.tintColor = UIColorFromRGB(0xffffff); @@ -3093,6 +3180,16 @@ - (void)updateTheme { [self reload]; } +//- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { +// NSLog(@"canPerformAction: %@ withSender: %@", NSStringFromSelector(action), sender); // log +// +// if (action == @selector(deleteSite:)) { +// return NO; +// } +// +// return YES; +//} + #pragma mark - #pragma mark Story Actions - save @@ -3127,7 +3224,7 @@ - (void)failedMarkAsUnread:(NSDictionary *)params { // called when the user taps refresh button -- (void)instafetchFeed { +- (IBAction)instafetchFeed { NSString *urlString = [NSString stringWithFormat:@"%@/reader/refresh_feed/%@", self.appDelegate.url, @@ -3152,12 +3249,16 @@ - (void)instafetchFeed { } } +- (IBAction)deleteSite:(id)sender { + //TODO +} + #pragma mark - #pragma mark PullToRefresh - (BOOL)canPullToRefresh { BOOL river = appDelegate.storiesCollection.isRiverView; - BOOL infrequent = [self isInfrequent]; + BOOL infrequent = appDelegate.storiesCollection.isInfrequent; BOOL read = appDelegate.storiesCollection.isReadView; BOOL widget = appDelegate.storiesCollection.isWidgetView; BOOL saved = appDelegate.storiesCollection.isSavedView; @@ -3165,6 +3266,7 @@ - (BOOL)canPullToRefresh { return appDelegate.storiesCollection.activeFeed != nil && !river && !infrequent && !saved && !read && !widget; } +#if !TARGET_OS_MACCATALYST - (void)refresh:(UIRefreshControl *)refreshControl { if (self.canPullToRefresh) { self.inPullToRefresh_ = YES; @@ -3173,10 +3275,13 @@ - (void)refresh:(UIRefreshControl *)refreshControl { [self finishRefresh]; } } +#endif - (void)finishRefresh { self.inPullToRefresh_ = NO; +#if !TARGET_OS_MACCATALYST [self.refreshControl endRefreshing]; +#endif } #pragma mark - diff --git a/clients/ios/Classes/FeedDetailViewController.swift b/clients/ios/Classes/FeedDetailViewController.swift index 6c0904b7a6..9e564e6cbc 100644 --- a/clients/ios/Classes/FeedDetailViewController.swift +++ b/clients/ios/Classes/FeedDetailViewController.swift @@ -29,10 +29,6 @@ class FeedDetailViewController: FeedDetailObjCViewController { case loading } - var isGrid: Bool { - return appDelegate.detailViewController.layout == .grid - } - var wasGrid: Bool { return appDelegate.detailViewController.wasGrid } @@ -138,7 +134,15 @@ class FeedDetailViewController: FeedDetailObjCViewController { @objc var suppressMarkAsRead = false + var scrollingDate = Date.distantPast + func deferredReload(story: Story? = nil) { + if let story { + print("🪿 queuing deferred reload for \(story)") + } else { + print("🪿 queuing deferred reload") + } + reloadWorkItem?.cancel() if let story { @@ -153,6 +157,16 @@ class FeedDetailViewController: FeedDetailObjCViewController { } if pendingStories.isEmpty { + print("🪿 starting deferred reload") + + let secondsSinceScroll = -scrollingDate.timeIntervalSinceNow + + if secondsSinceScroll < 0.5 { + print("🪿 too soon to reload; \(secondsSinceScroll) seconds since scroll") + deferredReload(story: story) + return + } + configureDataSource() } else { for story in pendingStories.values { @@ -202,6 +216,55 @@ extension FeedDetailViewController { reloadTable() } } + +#if targetEnvironment(macCatalyst) + override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + let location = storyLocation(for: indexPath) + + guard location < storiesCollection.storyLocationsCount else { + return nil + } + + let storyIndex = storiesCollection.index(fromLocation: location) + let story = Story(index: storyIndex) + + appDelegate.activeStory = story.dictionary + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in + let read = UIAction(title: story.isRead ? "Mark as unread" : "Mark as read", image: UIImage(named: "mark-read")) { action in + self.appDelegate.storiesCollection.toggleStoryUnread(story.dictionary) + self.reload() + } + + let newer = UIAction(title: "Mark newer stories read", image: UIImage(named: "mark-read")) { action in + self.markFeedsRead(fromTimestamp: story.timestamp, andOlder: false) + self.reload() + } + + let older = UIAction(title: "Mark older stories read", image: UIImage(named: "mark-read")) { action in + self.markFeedsRead(fromTimestamp: story.timestamp, andOlder: true) + self.reload() + } + + let saved = UIAction(title: story.isSaved ? "Unsave this story" : "Save this story", image: UIImage(named: "saved-stories")) { action in + self.appDelegate.storiesCollection.toggleStorySaved(story.dictionary) + self.reload() + } + + let send = UIAction(title: "Send this story to…", image: UIImage(named: "email")) { action in + self.appDelegate.showSend(to: self, sender: self.view) + } + + let train = UIAction(title: "Train this story", image: UIImage(named: "train")) { action in + self.appDelegate.openTrainStory(self.view) + } + + let submenu = UIMenu(title: "", options: .displayInline, children: [saved, send, train]) + + return UIMenu(title: "", children: [read, newer, older, submenu]) + } + } +#endif } extension FeedDetailViewController: FeedDetailInteraction { @@ -232,12 +295,18 @@ extension FeedDetailViewController: FeedDetailInteraction { let cacheCount = storyCache.before.count + storyCache.after.count if cacheCount > 0, story.index >= cacheCount - 5 { + let debug = Date() + if storiesCollection.isRiverView, storiesCollection.activeFolder != nil { fetchRiverPage(storiesCollection.feedPage + 1, withCallback: nil) } else { fetchFeedDetail(storiesCollection.feedPage + 1, withCallback: nil) } + + print("📠Fetching next page took \(-debug.timeIntervalSinceNow) seconds") } + + scrollingDate = Date() } func tapped(story: Story) { diff --git a/clients/ios/Classes/FeedTableCell.m b/clients/ios/Classes/FeedTableCell.m index 88c0ab7a63..526af3f3ed 100644 --- a/clients/ios/Classes/FeedTableCell.m +++ b/clients/ios/Classes/FeedTableCell.m @@ -174,11 +174,18 @@ - (void)drawRect:(CGRect)r { BOOL isHighlighted = cell.highlighted || cell.selected; UIColor *backgroundColor; +#if TARGET_OS_MACCATALYST + backgroundColor = cell.isSocial ? UIColorFromRGB(0xD8E3DB) : + cell.isSearch ? UIColorFromRGB(0xDBDFE6) : + cell.isSaved ? UIColorFromRGB(0xDFDCD6) : + UIColor.clearColor; +#else backgroundColor = cell.isSocial ? UIColorFromRGB(0xD8E3DB) : cell.isSearch ? UIColorFromRGB(0xDBDFE6) : cell.isSaved ? UIColorFromRGB(0xDFDCD6) : UIColorFromRGB(0xF7F8F5); - +#endif + // [backgroundColor set]; self.backgroundColor = backgroundColor; cell.backgroundColor = backgroundColor; @@ -219,7 +226,7 @@ - (void)drawRect:(CGRect)r { paragraphStyle.alignment = NSTextAlignmentLeft; CGSize faviconSize; if (cell.isSocial) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!cell.appDelegate.isPhone) { faviconSize = CGSizeMake(28, 28); UIImage *feedIcon = [Utilities roundCorneredImage:cell.feedFavicon radius:4 convertToSize:faviconSize]; [feedIcon drawInRect:CGRectMake(9.0, CGRectGetMidY(r)-faviconSize.height/2, faviconSize.width, faviconSize.height)]; @@ -239,7 +246,7 @@ - (void)drawRect:(CGRect)r { } else { faviconSize = CGSizeMake(16, 16); UIImage *feedIcon = [Utilities roundCorneredImage:cell.feedFavicon radius:4 convertToSize:faviconSize]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!cell.appDelegate.isPhone) { [feedIcon drawInRect:CGRectMake(12.0, CGRectGetMidY(r)-faviconSize.height/2, faviconSize.width, faviconSize.height)]; [cell.feedTitle drawInRect:CGRectMake(36.0, titleOffsetY, r.size.width - ([cell.unreadCount offsetWidth] + 36) - 10, font.pointSize*1.4) withAttributes:@{NSFontAttributeName: font, diff --git a/clients/ios/Classes/FeedsObjCViewController.h b/clients/ios/Classes/FeedsObjCViewController.h index 7dda6f8b7e..8c1747b111 100644 --- a/clients/ios/Classes/FeedsObjCViewController.h +++ b/clients/ios/Classes/FeedsObjCViewController.h @@ -21,8 +21,6 @@ static enum { NewsBlurTopSectionAllStories = 1 } NewsBlurTopSection; -@class NewsBlurAppDelegate; - @interface FeedsObjCViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; - NSMutableDictionary * activeFeedLocations; NSMutableDictionary *stillVisibleFeeds; NSMutableDictionary *visibleFolders; @@ -53,15 +49,23 @@ UIGestureRecognizerDelegate, UISearchBarDelegate> { NBNotifier *notifier; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UIView *innerView; @property (nonatomic) IBOutlet UITableView *feedTitlesTable; +@property (nonatomic) IBOutlet NSLayoutConstraint *feedTitlesTopConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *feedTitlesLeadingConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *feedTitlesTrailingConstraint; @property (nonatomic) IBOutlet UIToolbar *feedViewToolbar; @property (nonatomic) IBOutlet UISlider * feedScoreSlider; @property (nonatomic) IBOutlet UIBarButtonItem * homeButton; @property (nonatomic) IBOutlet UIBarButtonItem * addBarButton; @property (nonatomic) IBOutlet UIBarButtonItem * settingsBarButton; +@property (nonatomic) UIButton *activityButton; @property (nonatomic) IBOutlet UIBarButtonItem * activitiesButton; +#if TARGET_OS_MACCATALYST +@property (nonatomic) IBOutlet UIBarButtonItem * spacerBarButton; +@property (nonatomic) IBOutlet UIBarButtonItem * userBarButton; +#endif +@property (nonatomic) IBOutlet UIView *userInfoView; @property (nonatomic) IBOutlet UIButton *userAvatarButton; @property (nonatomic) IBOutlet UILabel *neutralCount; @property (nonatomic) IBOutlet UILabel *positiveCount; @@ -74,7 +78,9 @@ UIGestureRecognizerDelegate, UISearchBarDelegate> { @property (nonatomic, readwrite) BOOL viewShowingAllFeeds; @property (nonatomic, readwrite) BOOL interactiveFeedDetailTransition; @property (nonatomic, readwrite) BOOL isOffline; +#if !TARGET_OS_MACCATALYST @property (nonatomic) UIRefreshControl *refreshControl; +#endif @property (nonatomic) UISearchBar *searchBar; @property (nonatomic, strong) NSArray *searchFeedIds; @property (nonatomic) NSCache *imageCache; @@ -94,7 +100,14 @@ UIGestureRecognizerDelegate, UISearchBarDelegate> { - (void)didSelectSectionHeader:(UIButton *)button; - (void)didSelectSectionHeaderWithTag:(NSInteger)tag; - (void)selectNextFolderOrFeed; + - (IBAction)selectIntelligence; +- (void)selectEverything:(id)sender; +- (void)selectNextFeed:(id)sender; +- (void)selectPreviousFeed:(id)sender; +- (void)selectNextFolder:(id)sender; +- (void)selectPreviousFolder:(id)sender; + - (void)markFeedRead:(NSString *)feedId cutoffDays:(NSInteger)days; - (void)markFeedsRead:(NSArray *)feedIds cutoffDays:(NSInteger)days; - (void)markEverythingReadWithDays:(NSInteger)days; diff --git a/clients/ios/Classes/FeedsObjCViewController.m b/clients/ios/Classes/FeedsObjCViewController.m index f8e720b347..d7dfc93dcc 100644 --- a/clients/ios/Classes/FeedsObjCViewController.m +++ b/clients/ios/Classes/FeedsObjCViewController.m @@ -57,7 +57,6 @@ @interface FeedsObjCViewController () @implementation FeedsObjCViewController -@synthesize appDelegate; @synthesize feedTitlesTable; @synthesize feedViewToolbar; @synthesize feedScoreSlider; @@ -113,12 +112,14 @@ - (void)viewDidLoad { self.rowHeights = [NSMutableDictionary dictionary]; self.folderTitleViews = [NSMutableDictionary dictionary]; +#if !TARGET_OS_MACCATALYST self.refreshControl = [UIRefreshControl new]; self.refreshControl.tintColor = UIColorFromLightDarkRGB(0x0, 0xffffff); self.refreshControl.backgroundColor = UIColorFromRGB(0xE3E6E0); [self.refreshControl addTarget:self action:@selector(refresh:) forControlEvents:UIControlEventValueChanged]; self.feedTitlesTable.refreshControl = self.refreshControl; self.feedViewToolbar.translatesAutoresizingMaskIntoConstraints = NO; +#endif self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.feedTitlesTable.frame), 44.)]; @@ -129,7 +130,16 @@ - (void)viewDidLoad { self.searchBar.nb_searchField.textColor = UIColorFromRGB(0x0); [self.searchBar setSearchBarStyle:UISearchBarStyleMinimal]; [self.searchBar setAutocapitalizationType:UITextAutocapitalizationTypeNone]; +#if TARGET_OS_MACCATALYST + // Workaround for Catalyst bug. + self.searchBar.frame = CGRectMake(10, 0, CGRectGetWidth(self.feedTitlesTable.frame) - 20, 44.); + self.searchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; + UIView *searchContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.feedTitlesTable.frame), 44.)]; + [searchContainerView addSubview:self.searchBar]; + self.feedTitlesTable.tableHeaderView = searchContainerView; +#else self.feedTitlesTable.tableHeaderView = self.searchBar; +#endif userLabelFont = [UIFont fontWithName:@"WhitneySSm-Medium" size:15.0]; @@ -169,7 +179,17 @@ - (void)viewDidLoad { [[UIBarButtonItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName: UIColorFromFixedRGB(0x4C4D4A)} forState:UIControlStateHighlighted]; +#if TARGET_OS_MACCATALYST + self.innerView.backgroundColor = UIColor.clearColor; + + if (ThemeManager.themeManager.isLikeSystem) { + self.view.backgroundColor = UIColor.clearColor; + } else { + self.view.backgroundColor = UIColorFromRGB(0xf4f4f4); + } +#else self.view.backgroundColor = UIColorFromRGB(0xf4f4f4); +#endif self.navigationController.navigationBar.tintColor = UIColorFromRGB(0x8F918B); self.navigationController.navigationBar.translucent = NO; UIInterfaceOrientation orientation = self.view.window.windowScene.interfaceOrientation; @@ -199,6 +219,12 @@ - (void)viewDidLoad { self.feedTitlesTable.translatesAutoresizingMaskIntoConstraints = NO; self.feedTitlesTable.estimatedRowHeight = 0; +#if TARGET_OS_MACCATALYST + // Workaround for Catalyst bug. + self.feedTitlesLeadingConstraint.constant = -10; + self.feedTitlesTrailingConstraint.constant = -10; +#endif + if (@available(iOS 15.0, *)) { self.feedTitlesTable.sectionHeaderTopPadding = 0; } @@ -228,7 +254,7 @@ - (void)viewWillAppear:(BOOL)animated { [self resetRowHeights]; -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && +// if (!self.isPhone && // !self.interactiveFeedDetailTransition) { // // [appDelegate.masterContainerViewController transitionFromFeedDetail]; @@ -236,6 +262,16 @@ - (void)viewWillAppear:(BOOL)animated { // NSLog(@"Feed List timing 0: %f", [NSDate timeIntervalSinceReferenceDate] - start); [super viewWillAppear:animated]; +#if TARGET_OS_MACCATALYST + UINavigationController *navController = self.navigationController; + UITitlebar *titlebar = navController.navigationBar.window.windowScene.titlebar; + + titlebar.titleVisibility = UITitlebarTitleVisibilityHidden; + + [self.navigationController setNavigationBarHidden:YES animated:animated]; + [self.navigationController setToolbarHidden:YES animated:animated]; +#endif + NSUserDefaults *userPreferences = [NSUserDefaults standardUserDefaults]; NSInteger intelligenceLevel = [userPreferences integerForKey:@"selectedIntelligence"]; @@ -425,7 +461,7 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)builder { + +} + #pragma mark - #pragma mark State Restoration @@ -686,18 +726,19 @@ - (void)finishLoadingFeedListWithDict:(NSDictionary *)results finished:(BOOL)fin // [settingsBarButton setCustomView:settingsButton]; UIImage *activityImage = [Utilities templateImageNamed:@"dialog-notifications" sized:32]; - NBBarButtonItem *activityButton = [NBBarButtonItem buttonWithType:UIButtonTypeCustom]; - activityButton.accessibilityLabel = @"Activities"; - [activityButton setImage:activityImage forState:UIControlStateNormal]; - activityButton.tintColor = UIColorFromRGB(0x8F918B); - [activityButton setImageEdgeInsets:UIEdgeInsetsMake(4, 0, 4, 0)]; - [activityButton addTarget:self + [self.activityButton removeFromSuperview]; + self.activityButton = [NBBarButtonItem buttonWithType:UIButtonTypeCustom]; + self.activityButton.accessibilityLabel = @"Activities"; + [self.activityButton setImage:activityImage forState:UIControlStateNormal]; + self.activityButton.tintColor = UIColorFromRGB(0x8F918B); + [self.activityButton setImageEdgeInsets:UIEdgeInsetsMake(4, 0, 4, 0)]; + [self.activityButton addTarget:self action:@selector(showInteractionsPopover:) forControlEvents:UIControlEventTouchUpInside]; activitiesButton = [[UIBarButtonItem alloc] - initWithCustomView:activityButton]; + initWithCustomView:self.activityButton]; activitiesButton.width = 32; -// activityButton.backgroundColor = UIColor.redColor; +// self.activityButton.backgroundColor = UIColor.redColor; self.navigationItem.rightBarButtonItem = activitiesButton; NSMutableDictionary *sortedFolders = [[NSMutableDictionary alloc] init]; @@ -901,7 +942,7 @@ - (void)finishLoadingFeedListWithDict:(NSDictionary *)results finished:(BOOL)fin [self refreshHeaderCounts]; [appDelegate checkForFeedNotifications]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && finished) { + if (!self.isPhone && finished) { [self cacheFeedRowLocations]; } @@ -1035,7 +1076,11 @@ - (void)showUserProfile { appDelegate.activeUserProfileId = [NSString stringWithFormat:@"%@", [appDelegate.dictSocialProfile objectForKey:@"user_id"]]; appDelegate.activeUserProfileName = [NSString stringWithFormat:@"%@", [appDelegate.dictSocialProfile objectForKey:@"username"]]; // appDelegate.activeUserProfileName = @"You"; - [appDelegate showUserProfileModal:self.navigationItem.titleView]; +#if TARGET_OS_MACCATALYST + [appDelegate showUserProfileModal:self.userAvatarButton]; +#else + [appDelegate showUserProfileModal:self.navigationItem.titleView]; +#endif } - (IBAction)tapAddSite:(id)sender { @@ -1056,9 +1101,11 @@ - (IBAction)showSettingsPopover:(id)sender { MenuViewController *viewController = [MenuViewController new]; - [viewController addTitle:@"Preferences" iconName:@"dialog-preferences" iconColor:UIColorFromRGB(0xDF8566) selectionShouldDismiss:YES handler:^{ - [self.appDelegate showPreferences]; - }]; + if (!self.isMac) { + [viewController addTitle:@"Preferences" iconName:@"dialog-preferences" iconColor:UIColorFromRGB(0xDF8566) selectionShouldDismiss:YES handler:^{ + [self.appDelegate showPreferences]; + }]; + } [viewController addTitle:@"Mute Sites" iconName:@"menu_icn_mute.png" selectionShouldDismiss:YES handler:^{ [self.appDelegate showMuteSites]; @@ -1275,14 +1322,21 @@ - (void)resizeFontSize { [appDelegate.feedDetailViewController reloadWithSizing]; } +- (void)systemAppearanceDidChange:(BOOL)isDark { + [super systemAppearanceDidChange:isDark]; + +#if TARGET_OS_MACCATALYST + if (ThemeManager.themeManager.isLikeSystem) { + self.view.backgroundColor = UIColor.clearColor; + } else { + self.view.backgroundColor = UIColorFromRGB(0xf4f4f4); + } +#endif +} + - (void)updateTheme { [super updateTheme]; - // CATALYST: This prematurely dismisses the login view controller; is it really appropriate? -// if (![self.presentedViewController isKindOfClass:[UINavigationController class]] || (((UINavigationController *)self.presentedViewController).topViewController != (UIViewController *)self.appDelegate.fontSettingsViewController && ![((UINavigationController *)self.presentedViewController).topViewController conformsToProtocol:@protocol(IASKViewController)])) { -// [self.presentedViewController dismissViewControllerAnimated:YES completion:nil]; -// } - [self.appDelegate hidePopoverAnimated:YES]; UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] initWithIdiom:[[UIDevice currentDevice] userInterfaceIdiom]]; @@ -1299,16 +1353,24 @@ - (void)updateTheme { self.feedViewToolbar.barTintColor = [UINavigationBar appearance].barTintColor; self.addBarButton.tintColor = UIColorFromRGB(0x8F918B); self.settingsBarButton.tintColor = UIColorFromRGB(0x8F918B); +#if TARGET_OS_MACCATALYST + if (ThemeManager.themeManager.isLikeSystem) { + self.view.backgroundColor = UIColor.clearColor; + } else { + self.view.backgroundColor = UIColorFromRGB(0xf4f4f4); + } +#else self.refreshControl.tintColor = UIColorFromLightDarkRGB(0x0, 0xffffff); self.refreshControl.backgroundColor = UIColorFromRGB(0xE3E6E0); self.view.backgroundColor = UIColorFromRGB(0xf4f4f4); +#endif [[ThemeManager themeManager] updateSegmentedControl:self.intelligenceControl]; NBBarButtonItem *barButton = self.addBarButton.customView; [barButton setImage:[[ThemeManager themeManager] themedImage:[UIImage imageNamed:@"nav_icn_add.png"]] forState:UIControlStateNormal]; - self.settingsBarButton.image = [Utilities imageNamed:@"settings" sized:30]; + self.settingsBarButton.image = [Utilities imageNamed:@"settings" sized:self.isMac ? 24 : 30]; [self layoutHeaderCounts:0]; [self refreshHeaderCounts]; @@ -1327,6 +1389,7 @@ - (void)updateTheme { } self.feedTitlesTable.backgroundColor = UIColorFromRGB(0xf4f4f4); + [self reloadFeedTitlesTable]; [self resetupGestures]; @@ -1682,7 +1745,7 @@ - (CGFloat)tableView:(UITableView *)tableView - (CGFloat)calculateHeightForRowAtIndexPath:(NSIndexPath *)indexPath { if (appDelegate.hasNoSites) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { return kBlurblogTableViewRowHeight; } else { return kPhoneBlurblogTableViewRowHeight; @@ -1726,13 +1789,13 @@ - (CGFloat)calculateHeightForRowAtIndexPath:(NSIndexPath *)indexPath { if ([folderName isEqualToString:@"river_blurblogs"] || [folderName isEqualToString:@"river_global"]) { // blurblogs - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { height = kBlurblogTableViewRowHeight; } else { height = kPhoneBlurblogTableViewRowHeight; } } else { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { height = kTableViewRowHeight; } else { height = kPhoneTableViewRowHeight; @@ -2386,6 +2449,10 @@ - (IBAction)selectIntelligence { hud.mode = MBProgressHUDModeText; hud.removeFromSuperViewOnHide = YES; + if (!self.appDelegate.isPhone) { + hud.xOffset = 50; + } + NSIndexPath *topRow; if ([[self.feedTitlesTable indexPathsForVisibleRows] count]) { topRow = [[self.feedTitlesTable indexPathsForVisibleRows] objectAtIndex:0]; @@ -2452,7 +2519,7 @@ - (IBAction)selectIntelligence { [hud hide:YES afterDelay:0.5]; [self showExplainerOnEmptyFeedlist]; -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // FeedDetailViewController *storiesModule = self.appDelegate.dashboardViewController.storiesModule; // // storiesModule.storiesCollection.feedPage = 0; @@ -2681,15 +2748,19 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText #pragma mark - #pragma mark PullToRefresh +#if !TARGET_OS_MACCATALYST - (void)refresh:(UIRefreshControl *)refreshControl { self.inPullToRefresh_ = YES; [appDelegate reloadFeedsView:NO]; [appDelegate donateRefresh]; } +#endif - (void)finishRefresh { self.inPullToRefresh_ = NO; +#if !TARGET_OS_MACCATALYST [self.refreshControl endRefreshing]; +#endif } - (void)refreshFeedList { @@ -2814,6 +2885,11 @@ - (void)finishRefreshingFeedList:(NSDictionary *)results feedId:(NSString *)feed }); } +//- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { +// NSLog(@"canPerformAction: %@ withSender: %@", NSStringFromSelector(action, sender); // log +// return YES; +//} + - (void)resetToolbar { // self.navigationItem.leftBarButtonItem = nil; self.navigationItem.titleView = nil; @@ -2821,6 +2897,16 @@ - (void)resetToolbar { } - (void)layoutHeaderCounts:(UIInterfaceOrientation)orientation { +#if TARGET_OS_MACCATALYST + int xOffset = 60; + int yOffset = 10; + + [self.userInfoView removeFromSuperview]; + + self.userInfoView = [[UIView alloc] + initWithFrame:CGRectMake(0, 0, self.innerView.bounds.size.width, 50)]; + self.userInfoView.backgroundColor = UIColorFromLightSepiaMediumDarkRGB(0xE0E0E0, 0xFFF8CA, 0x4F4F4F, 0x292B2C); +#else if (!orientation) { orientation = self.view.window.windowScene.interfaceOrientation; } @@ -2831,22 +2917,33 @@ - (void)layoutHeaderCounts:(UIInterfaceOrientation)orientation { isShort = YES; } + int xOffset = 50; int yOffset = isShort ? 0 : 6; - UIView *userInfoView = [[UIView alloc] - initWithFrame:CGRectMake(0, 0, - self.navigationController.navigationBar.frame.size.width, - self.navigationController.navigationBar.frame.size.height)]; + + self.userInfoView = [[UIView alloc] + initWithFrame:CGRectMake(0, 0, + self.navigationController.navigationBar.frame.size.width, + self.navigationController.navigationBar.frame.size.height)]; +#endif + // adding user avatar to left NSURL *imageURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@", [appDelegate.dictSocialProfile objectForKey:@"large_photo_url"]]]; userAvatarButton = [UIButton systemButtonWithImage:[UIImage imageNamed:@"user"] - target:self action:@selector((showUserProfile))]; + target:self action:@selector(showUserProfile)]; userAvatarButton.pointerInteractionEnabled = YES; userAvatarButton.accessibilityLabel = @"User info"; +#if TARGET_OS_MACCATALYST + userAvatarButton.accessibilityHint = @"Double-click for information about your account."; + CGRect frame = userAvatarButton.frame; + userAvatarButton.frame = frame; +#else userAvatarButton.accessibilityHint = @"Double-tap for information about your account."; UIEdgeInsets insets = UIEdgeInsetsMake(0, -10, 10, 0); userAvatarButton.contentEdgeInsets = insets; +#endif +// userAvatarButton.backgroundColor = UIColor.blueColor; NSMutableURLRequest *avatarRequest = [NSMutableURLRequest requestWithURL:imageURL]; [avatarRequest addValue:@"image/*" forHTTPHeaderField:@"Accept"]; @@ -2857,49 +2954,61 @@ - (void)layoutHeaderCounts:(UIInterfaceOrientation)orientation { typeof(weakSelf) __strong strongSelf = weakSelf; image = [Utilities roundCorneredImage:image radius:6 convertToSize:CGSizeMake(38, 38)]; image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; - [(UIButton *)strongSelf.userAvatarButton setImage:image forState:UIControlStateNormal]; - + UIButton *button = strongSelf.userAvatarButton; + [button setImage:image forState:UIControlStateNormal]; } failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nonnull response, NSError * _Nonnull error) { NSLog(@"Could not fetch user avatar: %@", error); }]; - [userInfoView addSubview:userAvatarButton]; + [self.userInfoView addSubview:userAvatarButton]; - userLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, yOffset, userInfoView.frame.size.width, 16)]; + userLabel = [[UILabel alloc] initWithFrame:CGRectMake(xOffset, yOffset, self.userInfoView.frame.size.width, 16)]; userLabel.text = appDelegate.activeUsername; userLabel.font = userLabelFont; userLabel.textColor = UIColorFromRGB(0x404040); userLabel.backgroundColor = [UIColor clearColor]; userLabel.accessibilityLabel = [NSString stringWithFormat:@"Logged in as %@", appDelegate.activeUsername]; [userLabel sizeToFit]; - [userInfoView addSubview:userLabel]; + [self.userInfoView addSubview:userLabel]; [appDelegate.folderCountCache removeObjectForKey:@"everything"]; yellowIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"g_icn_unread"]]; - [userInfoView addSubview:yellowIcon]; + [self.userInfoView addSubview:yellowIcon]; yellowIcon.hidden = YES; neutralCount = [[UILabel alloc] init]; neutralCount.font = [UIFont fontWithName:@"WhitneySSm-Book" size:12]; neutralCount.textColor = UIColorFromRGB(0x707070); neutralCount.backgroundColor = [UIColor clearColor]; - [userInfoView addSubview:neutralCount]; + [self.userInfoView addSubview:neutralCount]; greenIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"g_icn_focus"]]; - [userInfoView addSubview:greenIcon]; + [self.userInfoView addSubview:greenIcon]; greenIcon.hidden = YES; positiveCount = [[UILabel alloc] init]; positiveCount.font = [UIFont fontWithName:@"WhitneySSm-Book" size:12]; positiveCount.textColor = UIColorFromRGB(0x707070); positiveCount.backgroundColor = [UIColor clearColor]; - [userInfoView addSubview:positiveCount]; + [self.userInfoView addSubview:positiveCount]; - [userInfoView sizeToFit]; +// self.userInfoView.backgroundColor = UIColor.blueColor; -// userInfoView.backgroundColor = UIColor.blueColor; +#if TARGET_OS_MACCATALYST + self.activityButton.frame = CGRectMake(self.innerView.bounds.size.width - 36, 10, 32, 32); - self.navigationItem.titleView = userInfoView; + [self.userInfoView addSubview:self.activityButton]; + + [self.innerView addSubview:self.userInfoView]; + + self.activityButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + self.userInfoView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + + self.feedTitlesTopConstraint.constant = 50; +#else + [self.userInfoView sizeToFit]; + self.navigationItem.titleView = self.userInfoView; +#endif } - (void)refreshHeaderCounts { @@ -2908,6 +3017,12 @@ - (void)refreshHeaderCounts { return; } +#if TARGET_OS_MACCATALYST + int yOffset = 2; +#else + int yOffset = 0; +#endif + userAvatarButton.hidden = NO; [appDelegate.folderCountCache removeObjectForKey:@"everything"]; @@ -2924,13 +3039,13 @@ - (void)refreshHeaderCounts { yellowIcon.frame = CGRectMake(CGRectGetMinX(userLabel.frame), CGRectGetMaxY(userLabel.frame) + 4, 8, 8); neutralCount.frame = CGRectMake(CGRectGetMaxX(yellowIcon.frame) + 2, - CGRectGetMinY(yellowIcon.frame) - 2, 100, 16); + CGRectGetMinY(yellowIcon.frame) - 2 - yOffset, 100, 16); [neutralCount sizeToFit]; greenIcon.frame = CGRectMake(CGRectGetMaxX(neutralCount.frame) + 8, CGRectGetMinY(yellowIcon.frame), 8, 8); positiveCount.frame = CGRectMake(CGRectGetMaxX(greenIcon.frame) + 2, - CGRectGetMinY(greenIcon.frame) - 2, 100, 16); + CGRectGetMinY(greenIcon.frame) - 2 - yOffset, 100, 16); [positiveCount sizeToFit]; yellowIcon.hidden = NO; diff --git a/clients/ios/Classes/FirstTimeUserAddFriendsViewController.h b/clients/ios/Classes/FirstTimeUserAddFriendsViewController.h index af9a037262..6f67eca198 100644 --- a/clients/ios/Classes/FirstTimeUserAddFriendsViewController.h +++ b/clients/ios/Classes/FirstTimeUserAddFriendsViewController.h @@ -11,11 +11,8 @@ #import "NewsBlurAppDelegate.h" #import "NewsBlur-Swift.h" -@interface FirstTimeUserAddFriendsViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; -} +@interface FirstTimeUserAddFriendsViewController : BaseViewController -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UIBarButtonItem *nextButton; @property (weak, nonatomic) IBOutlet UIButton *facebookButton; @property (weak, nonatomic) IBOutlet UIButton *twitterButton; diff --git a/clients/ios/Classes/FirstTimeUserAddFriendsViewController.m b/clients/ios/Classes/FirstTimeUserAddFriendsViewController.m index 72124a4626..63c44141ec 100644 --- a/clients/ios/Classes/FirstTimeUserAddFriendsViewController.m +++ b/clients/ios/Classes/FirstTimeUserAddFriendsViewController.m @@ -16,7 +16,6 @@ @interface FirstTimeUserAddFriendsViewController () @implementation FirstTimeUserAddFriendsViewController -@synthesize appDelegate; @synthesize nextButton; @synthesize facebookButton; @synthesize twitterButton; @@ -36,8 +35,6 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - UIBarButtonItem *next = [[UIBarButtonItem alloc] initWithTitle:@"Skip this step" style:UIBarButtonItemStyleDone target:self action:@selector(tapNextButton)]; self.nextButton = next; self.navigationItem.rightBarButtonItem = next; @@ -53,7 +50,7 @@ - (void)viewWillAppear:(BOOL)animated { //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } else if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { // return YES; diff --git a/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.h b/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.h index 8643876558..e7b09d347c 100644 --- a/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.h +++ b/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.h @@ -9,11 +9,8 @@ #import #import "NewsBlurAppDelegate.h" -@interface FirstTimeUserAddNewsBlurViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; -} +@interface FirstTimeUserAddNewsBlurViewController : BaseViewController -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UIBarButtonItem *nextButton; @property (strong, nonatomic) IBOutlet UILabel *instructionsLabel; diff --git a/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.m b/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.m index 308622701c..0e7be044ee 100644 --- a/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.m +++ b/clients/ios/Classes/FirstTimeUserAddNewsBlurViewController.m @@ -11,7 +11,6 @@ @implementation FirstTimeUserAddNewsBlurViewController -@synthesize appDelegate; @synthesize nextButton; @synthesize instructionsLabel; @@ -27,8 +26,6 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - UIBarButtonItem *next = [[UIBarButtonItem alloc] initWithTitle:@"Start reading" style:UIBarButtonItemStyleDone target:self action:@selector(tapNextButton)]; self.nextButton = next; self.navigationItem.rightBarButtonItem = next; @@ -51,7 +48,7 @@ - (void)viewDidAppear:(BOOL)animated { //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } else if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { // return YES; diff --git a/clients/ios/Classes/FirstTimeUserAddSitesViewController.h b/clients/ios/Classes/FirstTimeUserAddSitesViewController.h index ef936b3b94..be255d8269 100644 --- a/clients/ios/Classes/FirstTimeUserAddSitesViewController.h +++ b/clients/ios/Classes/FirstTimeUserAddSitesViewController.h @@ -10,11 +10,8 @@ #import "NewsBlurAppDelegate.h" @interface FirstTimeUserAddSitesViewController : BaseViewController - { - NewsBlurAppDelegate *appDelegate; -} + -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UIButton *googleReaderButton; @property (nonatomic) IBOutlet UIView *googleReaderButtonWrapper; @property (nonatomic) IBOutlet UIBarButtonItem *nextButton; diff --git a/clients/ios/Classes/FirstTimeUserAddSitesViewController.m b/clients/ios/Classes/FirstTimeUserAddSitesViewController.m index b37b658b43..b449c72c44 100644 --- a/clients/ios/Classes/FirstTimeUserAddSitesViewController.m +++ b/clients/ios/Classes/FirstTimeUserAddSitesViewController.m @@ -24,7 +24,6 @@ @interface FirstTimeUserAddSitesViewController() @implementation FirstTimeUserAddSitesViewController -@synthesize appDelegate; @synthesize googleReaderButton; @synthesize nextButton; @synthesize activityIndicator; @@ -50,8 +49,6 @@ - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - UIBarButtonItem *next = [[UIBarButtonItem alloc] initWithTitle:@"Next step" style:UIBarButtonItemStyleDone target:self action:@selector(tapNextButton)]; self.nextButton = next; self.nextButton.enabled = YES; @@ -89,7 +86,7 @@ - (void)viewWillAppear:(BOOL)animated { //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } else if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { // return YES; diff --git a/clients/ios/Classes/FirstTimeUserViewController.m b/clients/ios/Classes/FirstTimeUserViewController.m index f40444c8e5..6dbc872a0f 100644 --- a/clients/ios/Classes/FirstTimeUserViewController.m +++ b/clients/ios/Classes/FirstTimeUserViewController.m @@ -98,7 +98,7 @@ - (void)viewDidDisappear:(BOOL)animated { //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } else if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { // return YES; diff --git a/clients/ios/Classes/FolderTitleView.m b/clients/ios/Classes/FolderTitleView.m index f20c40b3de..a33be9291d 100644 --- a/clients/ios/Classes/FolderTitleView.m +++ b/clients/ios/Classes/FolderTitleView.m @@ -213,7 +213,7 @@ - (void) drawRect:(CGRect)rect { if (section == NewsBlurTopSectionInfrequentSiteStories) { folderImage = [UIImage imageNamed:@"ak-icon-infrequent.png"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 7; @@ -221,7 +221,7 @@ - (void) drawRect:(CGRect)rect { allowLongPress = YES; } else if (section == NewsBlurTopSectionAllStories) { folderImage = [UIImage imageNamed:@"all-stories"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 7; @@ -229,42 +229,42 @@ - (void) drawRect:(CGRect)rect { allowLongPress = NO; } else if ([folderName isEqual:@"river_global"]) { folderImage = [UIImage imageNamed:@"global-shares"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 8; } } else if ([folderName isEqual:@"river_blurblogs"]) { folderImage = [UIImage imageNamed:@"all-shares"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 8; } } else if ([folderName isEqual:@"saved_searches"]) { folderImage = [UIImage imageNamed:@"search"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 7; } } else if ([folderName isEqual:@"saved_stories"]) { folderImage = [UIImage imageNamed:@"saved-stories"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 7; } } else if ([folderName isEqual:@"read_stories"]) { folderImage = [UIImage imageNamed:@"indicator-unread"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 7; } } else if ([folderName isEqual:@"widget_stories"]) { folderImage = [UIImage imageNamed:@"g_icn_folder_widget.png"]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { folderImageViewX = 10; } else { folderImageViewX = 7; @@ -275,7 +275,7 @@ - (void) drawRect:(CGRect)rect { } else { folderImage = [UIImage imageNamed:@"folder-open"]; } - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { } else { folderImageViewX = 7; } diff --git a/clients/ios/Classes/FontSettingsViewController.m b/clients/ios/Classes/FontSettingsViewController.m index 6608cfb5df..0c9663f6f0 100644 --- a/clients/ios/Classes/FontSettingsViewController.m +++ b/clients/ios/Classes/FontSettingsViewController.m @@ -439,7 +439,9 @@ - (UITableViewCell *)makeFontSizeTableCell { [self.fontSizeSegment setTitle:@"M" forSegmentAtIndex:2]; [self.fontSizeSegment setTitle:@"L" forSegmentAtIndex:3]; [self.fontSizeSegment setTitle:@"XL" forSegmentAtIndex:4]; +#if !TARGET_OS_MACCATALYST self.fontSizeSegment.backgroundColor = UIColorFromRGB(0xeeeeee); +#endif [self.fontSizeSegment setTitleTextAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"WhitneySSm-Medium" size:12.0f]} forState:UIControlStateNormal]; [self.fontSizeSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:0]; [self.fontSizeSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:1]; @@ -467,7 +469,9 @@ - (UITableViewCell *)makeLineSpacingTableCell { [self.lineSpacingSegment setImage:[UIImage imageNamed:@"line_spacing_m"] forSegmentAtIndex:2]; [self.lineSpacingSegment setImage:[UIImage imageNamed:@"line_spacing_l"] forSegmentAtIndex:3]; [self.lineSpacingSegment setImage:[UIImage imageNamed:@"line_spacing_xl"] forSegmentAtIndex:4]; +#if !TARGET_OS_MACCATALYST self.lineSpacingSegment.backgroundColor = UIColorFromRGB(0xeeeeee); +#endif [[ThemeManager themeManager] updateSegmentedControl:self.lineSpacingSegment]; @@ -486,7 +490,9 @@ - (UITableViewCell *)makeFullScreenTableCell { self.fullscreenSegment.frame = CGRectMake(8, 7, cell.frame.size.width - 8*2, kMenuOptionHeight - 7*2); [self.fullscreenSegment setTitle:@"Full Screen" forSegmentAtIndex:0]; [self.fullscreenSegment setTitle:@"Toolbar" forSegmentAtIndex:1]; +#if !TARGET_OS_MACCATALYST self.fullscreenSegment.backgroundColor = UIColorFromRGB(0xeeeeee); +#endif [self.fullscreenSegment setTitleTextAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"WhitneySSm-Medium" size:12.0f]} forState:UIControlStateNormal]; [self.fullscreenSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:0]; [self.fullscreenSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:1]; @@ -508,7 +514,9 @@ - (UITableViewCell *)makeAutoscrollTableCell { self.autoscrollSegment.frame = CGRectMake(8, 7, cell.frame.size.width - 8*2, kMenuOptionHeight - 7*2); [self.autoscrollSegment setTitle:@"Manual scroll" forSegmentAtIndex:0]; [self.autoscrollSegment setTitle:@"Auto scroll" forSegmentAtIndex:1]; +#if !TARGET_OS_MACCATALYST self.autoscrollSegment.backgroundColor = UIColorFromRGB(0xeeeeee); +#endif [self.autoscrollSegment setTitleTextAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"WhitneySSm-Medium" size:12.0f]} forState:UIControlStateNormal]; [self.autoscrollSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:0]; [self.autoscrollSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:1]; @@ -530,7 +538,9 @@ - (UITableViewCell *)makeScrollOrientationTableCell { self.scrollOrientationSegment.frame = CGRectMake(8, 7, cell.frame.size.width - 8*2, kMenuOptionHeight - 7*2); [self.scrollOrientationSegment setTitle:@"â© Horizontal" forSegmentAtIndex:0]; [self.scrollOrientationSegment setTitle:@"⬠Vertical" forSegmentAtIndex:1]; +#if !TARGET_OS_MACCATALYST self.scrollOrientationSegment.backgroundColor = UIColorFromRGB(0xeeeeee); +#endif [self.scrollOrientationSegment setTitleTextAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"WhitneySSm-Medium" size:12.0f]} forState:UIControlStateNormal]; [self.scrollOrientationSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:0]; [self.scrollOrientationSegment setContentOffset:CGSizeMake(0, 1) forSegmentAtIndex:1]; @@ -566,7 +576,9 @@ - (UITableViewCell *)makeThemeTableCell { [self.themeSegment setDividerImage:blankImage forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; self.themeSegment.tintColor = [UIColor clearColor]; +#if !TARGET_OS_MACCATALYST self.themeSegment.backgroundColor = [UIColor clearColor]; +#endif [[ThemeManager themeManager] updateThemeSegmentedControl:self.themeSegment]; @@ -580,7 +592,14 @@ - (UIImage *)themeImageWithName:(NSString *)name selected:(BOOL)selected { name = [name stringByAppendingString:@"-sel"]; } - return [[UIImage imageNamed:name] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + UIImage *image = [[UIImage imageNamed:name] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomMac) { + image = [Utilities imageWithImage:image convertToSize:CGSizeMake(20.0, 20.0)]; + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + } + + return image; } @end diff --git a/clients/ios/Classes/FriendsListViewController.h b/clients/ios/Classes/FriendsListViewController.h index 1c335e7100..f46ac98918 100644 --- a/clients/ios/Classes/FriendsListViewController.h +++ b/clients/ios/Classes/FriendsListViewController.h @@ -13,7 +13,6 @@ @class NewsBlurAppDelegate; @interface FriendsListViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; UISearchBar *friendSearchBar; UITableView *friendsTable; NSArray *suggestedUserProfiles; @@ -21,7 +20,6 @@ NSArray *userProfileIds; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UISearchBar *friendSearchBar; @property (nonatomic) IBOutlet UITableView *friendsTable; diff --git a/clients/ios/Classes/FriendsListViewController.m b/clients/ios/Classes/FriendsListViewController.m index b18f1c5ba7..a98bd000f2 100644 --- a/clients/ios/Classes/FriendsListViewController.m +++ b/clients/ios/Classes/FriendsListViewController.m @@ -27,7 +27,6 @@ @interface FriendsListViewController() @implementation FriendsListViewController -@synthesize appDelegate; @synthesize friendSearchBar; @synthesize friendsTable; @synthesize suggestedUserProfiles; @@ -45,8 +44,6 @@ - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - self.navigationItem.title = @"Find Friends"; UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle: @"Done" style: UIBarButtonItemStylePlain @@ -156,7 +153,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSIntege // if (self.inSearch_){ // return 0; // } else { -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad){ +// if (!self.isPhone){ // return 28; // }else{ // return 21; @@ -168,7 +165,7 @@ - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { int headerLabelHeight, folderImageViewY; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { headerLabelHeight = 28; folderImageViewY = 3; } else { @@ -280,7 +277,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N // add a NO FRIENDS TO SUGGEST message on either the first or second row depending on iphone/ipad int row = 0; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { row = 1; } diff --git a/clients/ios/Classes/InteractionsModule.m b/clients/ios/Classes/InteractionsModule.m index b279074db7..4f1ba0dca3 100644 --- a/clients/ios/Classes/InteractionsModule.m +++ b/clients/ios/Classes/InteractionsModule.m @@ -154,7 +154,7 @@ -(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger userInteractions = [appDelegate.userInteractionsArray count]; int minimumHeight; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { minimumHeight = MINIMUM_INTERACTION_HEIGHT_IPAD; } else { minimumHeight = MINIMUM_INTERACTION_HEIGHT_IPHONE; @@ -165,7 +165,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa } InteractionCell *interactionCell; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { interactionCell = [[InteractionCell alloc] init]; } else { interactionCell = [[SmallInteractionCell alloc] init]; @@ -190,7 +190,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N InteractionCell *cell = [tableView dequeueReusableCellWithIdentifier:@"InteractionCell"]; if (cell == nil) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { cell = [[InteractionCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"InteractionCell"]; } else { cell = [[SmallInteractionCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"InteractionCell"]; @@ -276,7 +276,7 @@ - (UITableViewCell *)makeLoadingCell { UIImageView *fleuron = [[UIImageView alloc] initWithImage:img]; int height; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { height = MINIMUM_INTERACTION_HEIGHT_IPAD; } else { height = MINIMUM_INTERACTION_HEIGHT_IPHONE; diff --git a/clients/ios/Classes/LoginViewController.h b/clients/ios/Classes/LoginViewController.h index 3094f7563d..c19a52b628 100644 --- a/clients/ios/Classes/LoginViewController.h +++ b/clients/ios/Classes/LoginViewController.h @@ -12,8 +12,6 @@ #define LANDSCAPE_MARGIN 128 @interface LoginViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; - BOOL isOnSignUpScreen; UITextField *usernameInput; UITextField *passwordInput; @@ -46,8 +44,6 @@ - (void)animateLoop; -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; - @property (nonatomic) IBOutlet UITextField *usernameInput; @property (nonatomic) IBOutlet UITextField *passwordInput; @property (nonatomic) IBOutlet UITextField *emailInput; diff --git a/clients/ios/Classes/LoginViewController.m b/clients/ios/Classes/LoginViewController.m index a6cd9fe7ce..b9e0d56ede 100644 --- a/clients/ios/Classes/LoginViewController.m +++ b/clients/ios/Classes/LoginViewController.m @@ -12,7 +12,6 @@ @implementation LoginViewController -@synthesize appDelegate; @synthesize usernameInput; @synthesize passwordInput; @synthesize emailInput; @@ -44,8 +43,6 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil } - (void)viewDidLoad { - self.appDelegate = NewsBlurAppDelegate.sharedAppDelegate; - self.usernameInput.borderStyle = UITextBorderStyleRoundedRect; self.passwordInput.borderStyle = UITextBorderStyleRoundedRect; self.emailInput.borderStyle = UITextBorderStyleRoundedRect; @@ -71,7 +68,7 @@ - (CGFloat)xForWidth:(CGFloat)width { } - (void)rearrangeViews { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { CGSize viewSize = self.view.bounds.size; CGFloat viewWidth = viewSize.width; CGFloat yOffset = 0; @@ -98,7 +95,7 @@ - (void)viewWillAppear:(BOOL)animated { //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } // return NO; @@ -108,7 +105,7 @@ - (void)viewDidAppear:(BOOL)animated { [MBProgressHUD hideHUDForView:self.view animated:YES]; [super viewDidAppear:animated]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self updateControls]; [self rearrangeViews]; } @@ -141,7 +138,7 @@ - (void)showError:(NSString *)error { self.errorLabel.hidden = !hasError; self.forgotPasswordButton.hidden = !hasError; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { self.loginOptionalLabel.hidden = hasError; } } @@ -166,7 +163,7 @@ - (IBAction)findLoginFrom1Password:(id)sender { - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { if(textField == usernameInput) { [passwordInput becomeFirstResponder]; } else if (textField == passwordInput) { @@ -244,7 +241,7 @@ - (void)registerAccount { setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [params setObject:[signUpUsernameInput text] forKey:@"username"]; [params setObject:[signUpPasswordInput text] forKey:@"password"]; } else { diff --git a/clients/ios/Classes/MenuViewController.h b/clients/ios/Classes/MenuViewController.h index 0d323e355a..3eed9c9f94 100644 --- a/clients/ios/Classes/MenuViewController.h +++ b/clients/ios/Classes/MenuViewController.h @@ -7,11 +7,12 @@ // #import +#import "BaseViewController.h" typedef void (^MenuItemHandler)(void); typedef void (^MenuItemSegmentedHandler)(NSUInteger selectedIndex); -@interface MenuViewController : UIViewController +@interface MenuViewController : BaseViewController @property (weak) IBOutlet UITableView *menuTableView; @@ -29,5 +30,6 @@ typedef void (^MenuItemSegmentedHandler)(NSUInteger selectedIndex); - (void)showFromNavigationController:(UINavigationController *)navigationController barButtonItem:(UIBarButtonItem *)barButtonItem; - (void)showFromNavigationController:(UINavigationController *)navigationController barButtonItem:(UIBarButtonItem *)barButtonItem permittedArrowDirections:(UIPopoverArrowDirection)permittedArrowDirections; +- (void)showFromNavigationController:(UINavigationController *)navigationController barButtonItem:(UIBarButtonItem *)barButtonItem sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect permittedArrowDirections:(UIPopoverArrowDirection)permittedArrowDirections; @end diff --git a/clients/ios/Classes/MenuViewController.m b/clients/ios/Classes/MenuViewController.m index c2f84a227d..c794858b5e 100644 --- a/clients/ios/Classes/MenuViewController.m +++ b/clients/ios/Classes/MenuViewController.m @@ -157,7 +157,14 @@ - (UIImage *)themeImageWithName:(NSString *)name selected:(BOOL)selected { name = [name stringByAppendingString:@"-sel"]; } - return [[UIImage imageNamed:name] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + UIImage *image = [[UIImage imageNamed:name] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomMac) { + image = [Utilities imageWithImage:image convertToSize:CGSizeMake(20.0, 20.0)]; + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + } + + return image; } - (UITableViewCell *)makeThemeSegmentedTableCell { @@ -197,7 +204,9 @@ - (UITableViewCell *)makeThemeSegmentedTableCell { [segmentedControl setDividerImage:blankImage forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; segmentedControl.tintColor = [UIColor clearColor]; +#if !TARGET_OS_MACCATALYST segmentedControl.backgroundColor = [UIColor clearColor]; +#endif segmentedControl.selectedSegmentIndex = valueIndex; @@ -243,7 +252,9 @@ - (UITableViewCell *)makeSegmentedTableCellForItem:(NSDictionary *)item forRow:( segmentedControl.apportionsSegmentWidthsByContent = YES; segmentedControl.selectedSegmentIndex = [item[MenuSegmentIndex] integerValue]; segmentedControl.tag = row; +#if !TARGET_OS_MACCATALYST segmentedControl.backgroundColor = UIColorFromRGB(0xeeeeee); +#endif [segmentedControl setTitleTextAttributes:@{NSFontAttributeName : [UIFont fontWithName:@"WhitneySSm-Medium" size:12.0]} forState:UIControlStateNormal]; [segmentedControl addTarget:self action:@selector(segmentedValueChanged:) forControlEvents:UIControlEventValueChanged]; @@ -277,6 +288,10 @@ - (void)showFromNavigationController:(UINavigationController *)navigationControl } - (void)showFromNavigationController:(UINavigationController *)navigationController barButtonItem:(UIBarButtonItem *)barButtonItem permittedArrowDirections:(UIPopoverArrowDirection)permittedArrowDirections { + [self showFromNavigationController:navigationController barButtonItem:barButtonItem sourceView:nil sourceRect:CGRectZero permittedArrowDirections:permittedArrowDirections]; +} + +- (void)showFromNavigationController:(UINavigationController *)navigationController barButtonItem:(UIBarButtonItem *)barButtonItem sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect permittedArrowDirections:(UIPopoverArrowDirection)permittedArrowDirections { UIViewController *presentedViewController = navigationController.presentedViewController; if (presentedViewController && presentedViewController.presentationController.presentationStyle == UIModalPresentationPopover) { [presentedViewController dismissViewControllerAnimated:YES completion:nil]; @@ -293,6 +308,8 @@ - (void)showFromNavigationController:(UINavigationController *)navigationControl popoverPresentationController.backgroundColor = UIColorFromRGB(NEWSBLUR_WHITE_COLOR); popoverPresentationController.permittedArrowDirections = permittedArrowDirections; popoverPresentationController.barButtonItem = barButtonItem; + popoverPresentationController.sourceView = sourceView; + popoverPresentationController.sourceRect = sourceRect; [navigationController presentViewController:embeddedNavController animated:YES completion:nil]; } diff --git a/clients/ios/Classes/MoveSiteViewController.h b/clients/ios/Classes/MoveSiteViewController.h index f8fb7e6be3..d2d5460a05 100644 --- a/clients/ios/Classes/MoveSiteViewController.h +++ b/clients/ios/Classes/MoveSiteViewController.h @@ -9,16 +9,12 @@ #import #import "NewsBlurAppDelegate.h" -@class NewsBlurAppDelegate; - @interface FolderTextField : UITextField @end @interface MoveSiteViewController : BaseViewController - { - NewsBlurAppDelegate *appDelegate; -} + - (void)reload; - (IBAction)moveSite; @@ -27,7 +23,6 @@ - (IBAction)doMoveButton; - (NSArray *)pickerFolders; -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UITextField *fromFolderInput; @property (nonatomic) IBOutlet FolderTextField *toFolderInput; @property (nonatomic) IBOutlet UILabel *titleLabel; diff --git a/clients/ios/Classes/MoveSiteViewController.m b/clients/ios/Classes/MoveSiteViewController.m index 34190e24ab..eac5548437 100644 --- a/clients/ios/Classes/MoveSiteViewController.m +++ b/clients/ios/Classes/MoveSiteViewController.m @@ -7,13 +7,11 @@ // #import "MoveSiteViewController.h" -#import "NewsBlurAppDelegate.h" #import "StringHelper.h" #import "StoriesCollection.h" @implementation MoveSiteViewController -@synthesize appDelegate; @synthesize toFolderInput; @synthesize fromFolderInput; @synthesize titleLabel; @@ -34,8 +32,6 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil } - (void)viewDidLoad { - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - UIImageView *folderImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"folder-open"]]; folderImage.frame = CGRectMake(0, 0, 24, 16); [folderImage setContentMode:UIViewContentModeRight]; @@ -54,14 +50,12 @@ - (void)viewDidLoad { frame.size.height += 20; self.navBar.frame = frame; - appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - [super viewDidLoad]; } //- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // // Return YES for supported orientations -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // return YES; // } else if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) { // return YES; diff --git a/clients/ios/Classes/NewsBlurAppDelegate.h b/clients/ios/Classes/NewsBlurAppDelegate.h index f526de3db1..089acf9004 100644 --- a/clients/ios/Classes/NewsBlurAppDelegate.h +++ b/clients/ios/Classes/NewsBlurAppDelegate.h @@ -287,7 +287,6 @@ SFSafariViewControllerDelegate> { @property (nonatomic, readwrite) BOOL hasQueuedReadStories; @property (nonatomic, readwrite) BOOL hasQueuedSavedStories; @property (nonatomic, readonly) BOOL showingSafariViewController; -@property (nonatomic, readonly) BOOL isCompactWidth; //@property (nonatomic) CGFloat compactWidth; @property (nonatomic, strong) BGAppRefreshTask *backgroundAppRefreshTask; @@ -296,6 +295,9 @@ SFSafariViewControllerDelegate> { - (void)registerDefaultsFromSettingsBundle; - (void)finishBackground; +- (void)prepareViewControllers; + +- (BOOL)openURL:(NSURL *)url; - (void)showFirstTimeUser; - (void)showLogin; @@ -395,7 +397,6 @@ SFSafariViewControllerDelegate> { - (BOOL)isSavedStoriesIntelligenceMode; - (NSArray *)allFeedIds; - (NSArray *)feedIdsForFolderTitle:(NSString *)folderTitle; -- (BOOL)isPortrait; - (void)confirmLogout; - (void)showConnectToService:(NSString *)serviceName; - (void)showAlert:(UIAlertController *)alert withViewController:(UIViewController *)vc; @@ -439,6 +440,7 @@ SFSafariViewControllerDelegate> { - (void)renameFolder:(NSString *)newTitle; - (void)showMarkReadMenuWithFeedIds:(NSArray *)feedIds collectionTitle:(NSString *)collectionTitle visibleUnreadCount:(NSInteger)visibleUnreadCount barButtonItem:(UIBarButtonItem *)barButtonItem completionHandler:(void (^)(BOOL marked))completionHandler; +- (void)showMarkReadMenuWithFeedIds:(NSArray *)feedIds collectionTitle:(NSString *)collectionTitle visibleUnreadCount:(NSInteger)visibleUnreadCount sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect completionHandler:(void (^)(BOOL marked))completionHandler; - (void)showMarkReadMenuWithFeedIds:(NSArray *)feedIds collectionTitle:(NSString *)collectionTitle sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect completionHandler:(void (^)(BOOL marked))completionHandler; - (void)showMarkOlderNewerReadMenuWithStoriesCollection:(StoriesCollection *)olderNewerCollection story:(NSDictionary *)olderNewerStory sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect extraItems:(NSArray *)extraItems completionHandler:(void (^)(BOOL marked))completionHandler; @@ -446,6 +448,7 @@ SFSafariViewControllerDelegate> { - (void)showPopoverWithViewController:(UIViewController *)viewController contentSize:(CGSize)contentSize barButtonItem:(UIBarButtonItem *)barButtonItem; - (void)showPopoverWithViewController:(UIViewController *)viewController contentSize:(CGSize)contentSize sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect; - (void)showPopoverWithViewController:(UIViewController *)viewController contentSize:(CGSize)contentSize sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect permittedArrowDirections:(UIPopoverArrowDirection)permittedArrowDirections; + - (void)hidePopoverAnimated:(BOOL)animated completion:(void (^)(void))completion; - (BOOL)hidePopoverAnimated:(BOOL)animated; - (void)hidePopover; diff --git a/clients/ios/Classes/NewsBlurAppDelegate.m b/clients/ios/Classes/NewsBlurAppDelegate.m index 1aa42bf05b..301dabf68b 100644 --- a/clients/ios/Classes/NewsBlurAppDelegate.m +++ b/clients/ios/Classes/NewsBlurAppDelegate.m @@ -192,7 +192,7 @@ @implementation NewsBlurAppDelegate @synthesize remainingUncachedImagesCount; + (instancetype)sharedAppDelegate { - return (NewsBlurAppDelegate *)[UIApplication sharedApplication].delegate; + return (NewsBlurAppDelegate *)[UIApplication sharedApplication].delegate; } + (instancetype)shared { @@ -202,25 +202,10 @@ + (instancetype)shared { - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self registerDefaultsFromSettingsBundle]; - // CATALYST: this is now handled by the storyboard. -// self.navigationController.delegate = self; -// self.navigationController.viewControllers = [NSArray arrayWithObject:self.feedsViewController]; self.storiesCollection = [StoriesCollection new]; -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// self.window.rootViewController = self.masterContainerViewController; -// } else { -// self.window.rootViewController = self.navigationController; -// } - - [self prepareViewControllers]; - [self clearNetworkManager]; - [window makeKeyAndVisible]; - - [[ThemeManager themeManager] prepareForWindow:self.window]; - [self createDatabaseConnection]; cachedFavicons = [[PINCache alloc] initWithName:@"NBFavicons"]; @@ -234,23 +219,19 @@ - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions: NBURLCache *urlCache = [[NBURLCache alloc] init]; [NSURLCache setSharedURLCache:urlCache]; // Uncomment below line to test image caching -// [[NSURLCache sharedURLCache] removeAllCachedResponses]; + // [[NSURLCache sharedURLCache] removeAllCachedResponses]; - [feedsViewController view]; - [feedsViewController loadOfflineFeeds:NO]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void) { [self setupReachability]; self.cacheImagesOperationQueue = [NSOperationQueue new]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { self.cacheImagesOperationQueue.maxConcurrentOperationCount = 2; } else { self.cacheImagesOperationQueue.maxConcurrentOperationCount = 1; } }); -// [self showFirstTimeUser]; - return YES; } @@ -272,12 +253,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( if (![appOpening isEqualToString:@"feeds"]) { self.pendingFolder = appOpening; -// [self loadRiverFeedDetailView:self.feedDetailViewController withFolder:appOpening]; + // [self loadRiverFeedDetailView:self.feedDetailViewController withFolder:appOpening]; } [self registerBackgroundTask]; - return YES; + return YES; } - (void)applicationDidBecomeActive:(UIApplication *)application { @@ -303,6 +284,10 @@ - (void)applicationWillResignActive:(UIApplication *)application { - (void)applicationWillTerminate:(UIApplication *)application { [self.feedsViewController refreshHeaderCounts]; + +#if TARGET_OS_MACCATALYST + [SceneDelegate closeAuxWindows]; +#endif } - (void)applicationDidEnterBackground:(UIApplication *)application { @@ -323,31 +308,31 @@ - (BOOL)application:(UIApplication *)application shouldRestoreSecureApplicationS // state restoration disabled; uses other options now return NO; -// NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; -// NSString *option = [preferences stringForKey:@"restore_state"]; -// -// if ([option isEqualToString:@"never"]) { -// return NO; -// } else if ([option isEqualToString:@"always"]) { -// return YES; -// } -// -// NSTimeInterval daysInterval = 60 * 60; -// NSTimeInterval limitInterval = option.doubleValue * daysInterval; -// NSInteger version = [coder decodeIntegerForKey:@"version"]; -// NSDate *lastSavedDate = [coder decodeObjectOfClass:[NSDate class] forKey:@"last_saved_state_date"]; -// -// if (limitInterval == 0) { -// limitInterval = 24 * daysInterval; -// } -// -// if (version > CURRENT_STATE_VERSION || lastSavedDate == nil) { -// return NO; -// } -// -// NSTimeInterval savedInterval = -[lastSavedDate timeIntervalSinceNow]; -// -// return savedInterval < limitInterval; + // NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults]; + // NSString *option = [preferences stringForKey:@"restore_state"]; + // + // if ([option isEqualToString:@"never"]) { + // return NO; + // } else if ([option isEqualToString:@"always"]) { + // return YES; + // } + // + // NSTimeInterval daysInterval = 60 * 60; + // NSTimeInterval limitInterval = option.doubleValue * daysInterval; + // NSInteger version = [coder decodeIntegerForKey:@"version"]; + // NSDate *lastSavedDate = [coder decodeObjectOfClass:[NSDate class] forKey:@"last_saved_state_date"]; + // + // if (limitInterval == 0) { + // limitInterval = 24 * daysInterval; + // } + // + // if (version > CURRENT_STATE_VERSION || lastSavedDate == nil) { + // return NO; + // } + // + // NSTimeInterval savedInterval = -[lastSavedDate timeIntervalSinceNow]; + // + // return savedInterval < limitInterval; } - (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder { @@ -380,6 +365,20 @@ - (void)application:(UIApplication *)application didDecodeRestorableStateWithCod // All done; could do any cleanup here } +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { + if ([options.userActivities.anyObject.activityType isEqualToString:@"aux"]) { + UISceneConfiguration *configuration = [UISceneConfiguration configurationWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; + configuration.delegateClass = [AuxSceneDelegate class]; + configuration.storyboard = [UIStoryboard storyboardWithName:@"AuxInterface" bundle:[NSBundle mainBundle]]; + return configuration; + } else { + UISceneConfiguration *configuration = [UISceneConfiguration configurationWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; + configuration.delegateClass = [SceneDelegate class]; + configuration.storyboard = [UIStoryboard storyboardWithName:@"MainInterface" bundle:[NSBundle mainBundle]]; + return configuration; + } +} + - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> *restorableObjects))restorationHandler { [self handleUserActivity:userActivity]; @@ -477,11 +476,26 @@ - (void)registerDefaultsFromSettingsBundle { return; } - NSString *name = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad ? @"Root~ipad.plist" : @"Root.plist"; - NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:name]]; - NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"]; + if (self.isMac) { + [self registerDefaultsFromSettingsBundle:settingsBundle withPlistName:@"Root.plist"]; + [self registerDefaultsFromSettingsBundle:settingsBundle withPlistName:@"StoryList.plist"]; + [self registerDefaultsFromSettingsBundle:settingsBundle withPlistName:@"Appearance.plist"]; + [self registerDefaultsFromSettingsBundle:settingsBundle withPlistName:@"Advanced.plist"]; + } else if (self.isPhone) { + [self registerDefaultsFromSettingsBundle:settingsBundle withPlistName:@"Root.plist"]; + } else { + [self registerDefaultsFromSettingsBundle:settingsBundle withPlistName:@"Root~ipad.plist"]; + } + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + [[NSUserDefaults standardUserDefaults] setObject:version forKey:@"version"]; +} + +- (void)registerDefaultsFromSettingsBundle:(NSString *)settingsPath withPlistName:(NSString *)name { + NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsPath stringByAppendingPathComponent:name]]; + NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"]; NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]]; + for(NSDictionary *prefSpecification in preferences) { NSString *key = [prefSpecification objectForKey:@"Key"]; if (key && [[prefSpecification allKeys] containsObject:@"DefaultValue"]) { @@ -490,9 +504,6 @@ - (void)registerDefaultsFromSettingsBundle { } [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister]; - - NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; - [[NSUserDefaults standardUserDefaults] setObject:version forKey:@"version"]; } - (void)registerForRemoteNotifications { @@ -500,15 +511,15 @@ - (void)registerForRemoteNotifications { center.delegate = self; [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){ if(!error){ - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] registerForRemoteNotifications]; }); } }]; -// UNNotificationAction *viewAction = [UNNotificationAction actionWithIdentifier:@"VIEW_STORY_IDENTIFIER" -// title:@"View story" -// options:UNNotificationActionOptionForeground]; + // UNNotificationAction *viewAction = [UNNotificationAction actionWithIdentifier:@"VIEW_STORY_IDENTIFIER" + // title:@"View story" + // options:UNNotificationActionOptionForeground]; UNNotificationAction *readAction = [UNNotificationAction actionWithIdentifier:@"MARK_READ_IDENTIFIER" title:@"Mark read" options:UNNotificationActionOptionNone]; @@ -516,8 +527,8 @@ - (void)registerForRemoteNotifications { title:@"Save story" options:UNNotificationActionOptionNone]; UNNotificationAction *dismissAction = [UNNotificationAction actionWithIdentifier:@"DISMISS_IDENTIFIER" - title:@"Dismiss" - options:UNNotificationActionOptionDestructive]; + title:@"Dismiss" + options:UNNotificationActionOptionDestructive]; UNNotificationCategory *storyCategory = [UNNotificationCategory categoryWithIdentifier:@"STORY_CATEGORY" actions:@[readAction, starAction, dismissAction] intentIdentifiers:@[] @@ -530,7 +541,7 @@ - (void)registerForBadgeNotifications { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){ - + }]; } @@ -610,6 +621,10 @@ -(void)application:(UIApplication *)application didRegisterForRemoteNotification } - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + return [self openURL:url]; +} + +- (BOOL)openURL:(NSURL *)url { if (self.activeUsername && [url.scheme isEqualToString:@"newsblurwidget"]) { NSMutableDictionary *query = [NSMutableDictionary dictionary]; @@ -654,11 +669,13 @@ - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDiction } - (void)didReceiveMemoryWarning { - // Releases the view if it doesn't have a superview. + // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; - // Release any cached data, images, etc that aren't in use. +#if !TARGET_OS_MACCATALYST + // Release any cached data, images, etc that aren't in use. [cachedStoryImages removeAllObjects]; +#endif } - (void)setupReachability { @@ -681,13 +698,13 @@ - (void)setupReachability { - (void)reachabilityChanged:(id)something { NSLog(@"Reachability changed: %@", something); -// Reachability* reach = [Reachability reachabilityWithHostname:self.host]; - -// if (reach.isReachable && feedsViewController.isOffline) { -// [feedsViewController loadOfflineFeeds:NO]; -//// } else { -//// [feedsViewController loadOfflineFeeds:NO]; -// } + // Reachability* reach = [Reachability reachabilityWithHostname:self.host]; + + // if (reach.isReachable && feedsViewController.isOffline) { + // [feedsViewController loadOfflineFeeds:NO]; + //// } else { + //// [feedsViewController loadOfflineFeeds:NO]; + // } } - (NSString *)url { @@ -751,56 +768,56 @@ - (NSDictionary *)getUser:(NSInteger)userId { - (void)showUserProfileModal:(id)sender { [self hidePopoverAnimated:NO]; UserProfileViewController *newUserProfile = [[UserProfileViewController alloc] init]; - self.userProfileViewController = newUserProfile; + self.userProfileViewController = newUserProfile; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self.userProfileViewController]; self.userProfileNavigationController = navController; self.userProfileNavigationController.navigationBar.translucent = NO; - + // adding Done button UIBarButtonItem *donebutton = [[UIBarButtonItem alloc] - initWithTitle:@"Close" - style:UIBarButtonItemStyleDone - target:self + initWithTitle:@"Close" + style:UIBarButtonItemStyleDone + target:self action:@selector(hideUserProfileModal)]; newUserProfile.navigationItem.rightBarButtonItem = donebutton; newUserProfile.navigationItem.title = self.activeUserProfileName; newUserProfile.navigationItem.backBarButtonItem.title = self.activeUserProfileName; [newUserProfile getUserProfile]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self showPopoverWithViewController:self.userProfileNavigationController contentSize:CGSizeMake(320, 454) sender:sender]; } else { [self.feedsNavigationController presentViewController:navController animated:YES completion:nil]; } - + } - (void)pushUserProfile { UserProfileViewController *userProfileView = [[UserProfileViewController alloc] init]; - - + + // adding Done button UIBarButtonItem *donebutton = [[UIBarButtonItem alloc] - initWithTitle:@"Close" - style:UIBarButtonItemStyleDone - target:self + initWithTitle:@"Close" + style:UIBarButtonItemStyleDone + target:self action:@selector(hideUserProfileModal)]; userProfileView.navigationItem.rightBarButtonItem = donebutton; userProfileView.navigationItem.title = self.activeUserProfileName; userProfileView.navigationItem.backBarButtonItem.title = self.activeUserProfileName; - [userProfileView getUserProfile]; + [userProfileView getUserProfile]; if (self.modalNavigationController.view.window == nil) { [self.userProfileNavigationController showViewController:userProfileView sender:self]; } else { [self.modalNavigationController showViewController:userProfileView sender:self]; }; - + } - (void)hideUserProfileModal { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self hidePopover]; } else { [self.feedsNavigationController dismissViewControllerAnimated:YES completion:nil]; @@ -832,7 +849,11 @@ - (void)popToRootWithCompletion:(void (^)(void))completion { - (void)showColumn:(UISplitViewControllerColumn)column debugInfo:(NSString *)debugInfo { NSLog(@"âš ï¸ show column for %@: split view controller: %@ split nav: %@; split controllers: %@; detail controller: %@; detail nav: %@; detail nav controllers: %@", debugInfo, self.splitViewController, self.splitViewController.navigationController, self.splitViewController.viewControllers, self.detailViewController, self.detailViewController.navigationController, self.detailViewController.navigationController.viewControllers); // log - [self.splitViewController showColumn:column]; + if (self.splitViewController.displayMode != UISplitViewControllerDisplayModeSecondaryOnly && (self.splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModeTwoBesideSecondary || + self.splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModeTwoDisplaceSecondary || + self.splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModeTwoOverSecondary)) { + [self.splitViewController showColumn:column]; + } NSLog(@"...shown"); // log } @@ -843,7 +864,7 @@ - (void)showPremiumDialog { initWithRootViewController:self.premiumViewController]; } self.premiumNavigationController.navigationBar.translucent = NO; - + [self.splitViewController dismissViewControllerAnimated:NO completion:nil]; premiumNavigationController.modalPresentationStyle = UIModalPresentationFormSheet; [self.splitViewController presentViewController:premiumNavigationController animated:YES completion:nil]; @@ -897,13 +918,18 @@ - (void)addSplitControlToMenuController:(MenuViewController *)menuViewController } - (void)showPreferences { + if (self.isMac) { + // [[UIApplication sharedApplication] sendAction:@selector(orderFrontPreferencesPanel:) to:nil from:nil forEvent:nil]; + return; + } + if (!preferencesViewController) { preferencesViewController = [[IASKAppSettingsViewController alloc] init]; [[ThemeManager themeManager] addThemeGestureRecognizerToView:self.preferencesViewController.view]; } [self hidePopover]; - + preferencesViewController.delegate = self.feedsViewController; preferencesViewController.showDoneButton = YES; preferencesViewController.showCreditsFooter = NO; @@ -918,7 +944,7 @@ - (void)showPreferences { self.modalNavigationController = navController; self.modalNavigationController.navigationBar.translucent = NO; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { self.modalNavigationController.modalPresentationStyle = UIModalPresentationFormSheet; } @@ -1040,10 +1066,10 @@ - (void)showSendTo:(UIViewController *)vc sender:(id)sender NSString *maybeFeedTitle = feedTitle ? [NSString stringWithFormat:@" via %@", feedTitle] : @""; text = [NSString stringWithFormat:@"



%@%@
%@", [url absoluteString], title, maybeFeedTitle, text]; } - + NBActivityItemSource *activityItemSource = [[NBActivityItemSource alloc] initWithUrl:url authorName:authorName text:text title:title feedTitle:feedTitle]; NSArray *activityItems = @[activityItemSource, url]; - + NSMutableArray *appActivities = [[NSMutableArray alloc] init]; if (url) [appActivities addObject:[[TUSafariActivity alloc] init]]; if (url) [appActivities addObject:[[ARChromeActivity alloc] @@ -1094,8 +1120,8 @@ - (void)showSendTo:(UIViewController *)vc sender:(id)sender [storyHUD hide:YES afterDelay:1]; } }]; - - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + + if (!self.isPhone) { BOOL fromPopover = [self hidePopoverAnimated:NO]; [self.splitViewController presentViewController:activityViewController animated:!fromPopover completion:nil]; activityViewController.modalPresentationStyle = UIModalPresentationPopover; @@ -1130,25 +1156,25 @@ - (void)showSendTo:(UIViewController *)vc sender:(id)sender } - (void)showShareView:(NSString *)type - setUserId:(NSString *)userId - setUsername:(NSString *)username - setReplyId:(NSString *)replyId { + setUserId:(NSString *)userId + setUsername:(NSString *)username + setReplyId:(NSString *)replyId { [self.shareViewController setCommentType:type]; -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// [self.masterContainerViewController transitionToShareView]; -// [self.shareViewController setSiteInfo:type setUserId:userId setUsername:username setReplyId:replyId]; -// } else { - if (self.shareNavigationController == nil) { - UINavigationController *shareNav = [[UINavigationController alloc] - initWithRootViewController:self.shareViewController]; - self.shareNavigationController = shareNav; - self.shareNavigationController.navigationBar.translucent = NO; - } - [self.feedsNavigationController presentViewController:self.shareNavigationController animated:YES completion:^{ - [self.shareViewController setSiteInfo:type setUserId:userId setUsername:username setReplyId:replyId]; - }]; -// } + // if (!self.isPhone) { + // [self.masterContainerViewController transitionToShareView]; + // [self.shareViewController setSiteInfo:type setUserId:userId setUsername:username setReplyId:replyId]; + // } else { + if (self.shareNavigationController == nil) { + UINavigationController *shareNav = [[UINavigationController alloc] + initWithRootViewController:self.shareViewController]; + self.shareNavigationController = shareNav; + self.shareNavigationController.navigationBar.translucent = NO; + } + [self.feedsNavigationController presentViewController:self.shareNavigationController animated:YES completion:^{ + [self.shareViewController setSiteInfo:type setUserId:userId setUsername:username setReplyId:replyId]; + }]; + // } } - (void)hideShareView:(BOOL)resetComment { @@ -1156,11 +1182,11 @@ - (void)hideShareView:(BOOL)resetComment { self.shareViewController.commentField.text = @""; self.shareViewController.currentType = nil; } - -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// [self.masterContainerViewController transitionFromShareView]; -// [self.storyPagesViewController becomeFirstResponder]; -// } else + + // if (!self.isPhone) { + // [self.masterContainerViewController transitionFromShareView]; + // [self.storyPagesViewController becomeFirstResponder]; + // } else if (!self.showingSafariViewController) { [self.feedsNavigationController dismissViewControllerAnimated:YES completion:nil]; [self.shareViewController.commentField resignFirstResponder]; @@ -1175,6 +1201,7 @@ - (void)resetShareComments { #pragma mark View Management - (void)prepareViewControllers { + self.appDelegate = self; self.splitViewController = (SplitViewController *)self.window.rootViewController; NSArray *splitChildren = self.splitViewController.viewControllers; @@ -1212,6 +1239,13 @@ - (void)prepareViewControllers { self.firstTimeUserAddNewsBlurViewController = [FirstTimeUserAddNewsBlurViewController new]; [self updateSplitBehavior:NO]; + + [window makeKeyAndVisible]; + + [[ThemeManager themeManager] prepareForWindow:self.window]; + + [feedsViewController view]; + [feedsViewController loadOfflineFeeds:NO]; } - (StoryPagesViewController *)storyPagesViewController { @@ -1259,7 +1293,7 @@ - (void)showLogin { } - (void)showFirstTimeUser { -// [self.feedsViewController changeToAllMode]; + // [self.feedsViewController changeToAllMode]; UINavigationController *ftux = [[UINavigationController alloc] initWithRootViewController:self.firstTimeUserViewController]; @@ -1292,21 +1326,21 @@ - (void)openTrainSite { // Needs a delay because the menu will close the popover. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self - openTrainSiteWithFeedLoaded:YES - from:self.feedDetailViewController.settingsBarButton]; - }); + [self + openTrainSiteWithFeedLoaded:YES + from:self.feedDetailViewController.settingsBarButton]; + }); } - (void)openTrainSiteWithFeedLoaded:(BOOL)feedLoaded from:(id)sender { UINavigationController *navController = self.feedsNavigationController; - trainerViewController.feedTrainer = YES; - trainerViewController.storyTrainer = NO; - trainerViewController.feedLoaded = feedLoaded; + trainerViewController.isStoryTrainer = NO; + trainerViewController.isFeedLoaded = feedLoaded; + [trainerViewController reload]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// trainerViewController.modalPresentationStyle=UIModalPresentationFormSheet; -// [navController presentViewController:trainerViewController animated:YES completion:nil]; + if (!self.isPhone) { + // trainerViewController.modalPresentationStyle=UIModalPresentationFormSheet; + // [navController presentViewController:trainerViewController animated:YES completion:nil]; [self showPopoverWithViewController:self.trainerViewController contentSize:CGSizeMake(500, 630) sender:sender]; } else { if (self.trainNavigationController == nil) { @@ -1320,10 +1354,11 @@ - (void)openTrainSiteWithFeedLoaded:(BOOL)feedLoaded from:(id)sender { - (void)openTrainStory:(id)sender { UINavigationController *navController = self.feedsNavigationController; - trainerViewController.feedTrainer = NO; - trainerViewController.storyTrainer = YES; - trainerViewController.feedLoaded = YES; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + trainerViewController.isStoryTrainer = YES; + trainerViewController.isFeedLoaded = YES; + [trainerViewController reload]; + + if (!self.isPhone) { [self showPopoverWithViewController:self.trainerViewController contentSize:CGSizeMake(500, 630) sender:sender]; } else { if (self.trainNavigationController == nil) { @@ -1345,8 +1380,9 @@ - (void)openNotificationsWithFeed:(NSString *)feedId { - (void)openNotificationsWithFeed:(NSString *)feedId sender:(id)sender { UINavigationController *navController = self.feedsNavigationController; + self.notificationsViewController.feedId = feedId; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self showPopoverWithViewController:self.notificationsViewController contentSize:CGSizeMake(420, 382) sender:sender]; } else { if (self.notificationsNavigationController == nil) { @@ -1354,7 +1390,6 @@ - (void)openNotificationsWithFeed:(NSString *)feedId sender:(id)sender { initWithRootViewController:self.notificationsViewController]; } self.notificationsNavigationController.navigationBar.translucent = NO; - self.notificationsViewController.feedId = feedId; [navController presentViewController:self.notificationsNavigationController animated:YES completion:nil]; } } @@ -1435,7 +1470,7 @@ - (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:( } - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { - [self.feedsNavigationController.topViewController becomeFirstResponder]; +// [self.feedsNavigationController.topViewController becomeFirstResponder]; } #pragma mark - Network @@ -1456,13 +1491,17 @@ - (void)clearNetworkManager { networkManager.responseSerializer = [AFJSONResponseSerializer serializer]; [networkManager.requestSerializer setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; - NSString *currentiPhoneVersion = [[[NSBundle mainBundle] infoDictionary] - objectForKey:@"CFBundleVersion"]; + NSString *currentVersion = [[[NSBundle mainBundle] infoDictionary] + objectForKey:@"CFBundleVersion"]; NSString *UA; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { - UA = [NSString stringWithFormat:@"NewsBlur iPad App v%@", currentiPhoneVersion]; + if (self.isMac) { + UA = [NSString stringWithFormat:@"NewsBlur Mac App v%@", currentVersion]; + } else if (self.isVision) { + UA = [NSString stringWithFormat:@"NewsBlur Vision App v%@", currentVersion]; + } else if (self.isPhone) { + UA = [NSString stringWithFormat:@"NewsBlur iPhone App v%@", currentVersion]; } else { - UA = [NSString stringWithFormat:@"NewsBlur iPhone App v%@", currentiPhoneVersion]; + UA = [NSString stringWithFormat:@"NewsBlur iPad App v%@", currentVersion]; } [networkManager.requestSerializer setValue:UA forHTTPHeaderField:@"User-Agent"]; } @@ -1556,10 +1595,10 @@ - (void)POST:(NSString *)urlString } - (void)POST:(NSString *)urlString - parameters:(id)parameters - target:(id)target - success:(SEL)success - failure:(SEL)failure { + parameters:(id)parameters + target:(id)target + success:(SEL)success + failure:(SEL)failure { [self POST:urlString parameters:parameters success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [self safelyInvokeTarget:target withSelector:success passingObject:responseObject]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { @@ -1634,7 +1673,7 @@ - (void)loadFeedDetailView { - (void)loadFeedDetailView:(BOOL)transition { self.inFeedDetail = YES; popoverHasFeedView = YES; - + [feedDetailViewController resetFeedDetail]; feedDetailViewController.storiesCollection = storiesCollection; @@ -1701,24 +1740,24 @@ - (void)loadFeed:(NSString *)feedId [self reloadFeedsView:NO]; -// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// [self loadFeedDetailView]; -// } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { -// // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self showFeedsListAnimated:NO]; -// // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; -// [self hidePopoverAnimated:NO completion:^{ -// if (self.feedsNavigationController.presentedViewController) { -// [self.feedsNavigationController dismissViewControllerAnimated:NO completion:^{ -// [self loadFeedDetailView]; -// }]; -// } else { -// [self loadFeedDetailView]; -// } -// }]; -// } -// }); + // dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + // if (!self.isPhone) { + // [self loadFeedDetailView]; + // } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { + // // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self showFeedsListAnimated:NO]; + // // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self hidePopoverAnimated:NO completion:^{ + // if (self.feedsNavigationController.presentedViewController) { + // [self.feedsNavigationController dismissViewControllerAnimated:NO completion:^{ + // [self loadFeedDetailView]; + // }]; + // } else { + // [self loadFeedDetailView]; + // } + // }]; + // } + // }); } - (void)loadTryFeedDetailView:(NSString *)feedId @@ -1731,7 +1770,7 @@ - (void)loadTryFeedDetailView:(NSString *)feedId if (social) { storiesCollection.isSocialView = YES; self.inFindingStoryMode = YES; - + if (feed == nil) { feed = user; self.isTryFeedView = YES; @@ -1740,21 +1779,21 @@ - (void)loadTryFeedDetailView:(NSString *)feedId if (feed == nil) { feed = user; self.isTryFeedView = YES; - + } storiesCollection.isSocialView = NO; -// [self setInFindingStoryMode:NO]; + // [self setInFindingStoryMode:NO]; } - + self.tryFeedStoryId = contentId; storiesCollection.activeFeed = feed; storiesCollection.activeFolder = nil; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self loadFeedDetailView]; } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { -// [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; [self showFeedsListAnimated:NO]; [self hidePopoverAnimated:YES completion:^{ if (self.feedsNavigationController.presentedViewController) { @@ -1771,7 +1810,7 @@ - (void)loadTryFeedDetailView:(NSString *)feedId - (void)backgroundLoadNotificationStory { if (self.inFindingStoryMode) { if ([storiesCollection.activeFolder isEqualToString:@"widget_stories"]) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self.feedsViewController selectWidgetStories]; } else { [self loadRiverFeedDetailView:self.feedDetailViewController withFolder:self.widgetFolder]; @@ -1784,7 +1823,7 @@ - (void)backgroundLoadNotificationStory { } } else if (self.tryFeedFeedId && !self.isTryFeedView) { [self loadFeed:self.tryFeedFeedId withStory:self.tryFeedStoryId animated:NO]; - } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && !self.isCompactWidth && self.storiesCollection == nil) { + } else if (!self.isPhone && !self.isCompactWidth && self.storiesCollection == nil) { [self loadRiverFeedDetailView:self.feedDetailViewController withFolder:storiesCollection.activeFolder]; } else if (self.pendingFolder != nil) { [self loadRiverFeedDetailView:self.feedDetailViewController withFolder:self.pendingFolder]; @@ -1807,13 +1846,13 @@ - (NSString *)widgetFolder { - (void)loadStarredDetailViewWithStory:(NSString *)contentId showFindingStory:(BOOL)showHUD { if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { -// [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; [self showFeedsListAnimated:NO]; [self.feedsNavigationController dismissViewControllerAnimated:YES completion:nil]; [self hidePopoverAnimated:NO]; } - + self.inFindingStoryMode = YES; [storiesCollection reset]; storiesCollection.isRiverView = YES; @@ -1824,7 +1863,7 @@ - (void)loadStarredDetailViewWithStory:(NSString *)contentId [self loadRiverFeedDetailView:feedDetailViewController withFolder:@"saved_stories"]; if (showHUD) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self.storyPagesViewController showShareHUD:@"Finding story..."]; } else { MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.feedDetailViewController.view animated:YES]; @@ -1891,27 +1930,13 @@ - (NSArray *)feedIdsForFolderTitle:(NSString *)folderTitle { } } -- (BOOL)isPortrait { - UIInterfaceOrientation orientation = self.window.windowScene.interfaceOrientation; - if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { - return YES; - } else { - return NO; - } -} - -- (BOOL)isCompactWidth { - return self.window.windowScene.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact; - //return self.compactWidth > 0.0; -} - - (void)confirmLogout { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Positive?" message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle: @"Logout" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [alertController dismissViewControllerAnimated:YES completion:nil]; NSLog(@"Logging out..."); NSString *urlString = [NSString stringWithFormat:@"%@/reader/logout?api=1", - self.url]; + self.url]; [self GET:urlString parameters:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [MBProgressHUD hideHUDForView:self.view animated:YES]; [self showLogin]; @@ -1941,7 +1966,7 @@ - (void)showConnectToService:(NSString *)serviceName { self.modalNavigationController.modalPresentationStyle = UIModalPresentationFormSheet; self.modalNavigationController.navigationBar.translucent = NO; [self.splitViewController presentViewController:modalNavigationController - animated:YES completion:nil]; + animated:YES completion:nil]; } - (void)showAlert:(UIAlertController *)alert withViewController:(UIViewController *)vc { @@ -1962,7 +1987,7 @@ - (void)refreshUserProfile:(void(^)(void))callback { } - (void)refreshFeedCount:(id)feedId { -// [feedsViewController fadeFeed:feedId]; + // [feedsViewController fadeFeed:feedId]; [feedsViewController redrawFeedCounts:feedId]; [feedsViewController refreshHeaderCounts]; } @@ -1980,9 +2005,9 @@ - (void)loadRiverFeedDetailView:(FeedDetailViewController *)feedDetailView withF if (feedDetailView == feedDetailViewController) { feedDetailView.storiesCollection = storiesCollection; } - + [feedDetailView.storiesCollection reset]; - + if ([folder isEqualToString:@"river_global"]) { feedDetailView.storiesCollection.isSocialRiverView = YES; feedDetailView.storiesCollection.isRiverView = YES; @@ -2106,8 +2131,8 @@ - (void)loadRiverFeedDetailView:(FeedDetailViewController *)feedDetailView withF - (void)openDashboardRiverForStory:(NSString *)contentId showFindingStory:(BOOL)showHUD { if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { -// [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; [self showFeedsListAnimated:NO]; [self.feedsNavigationController dismissViewControllerAnimated:YES completion:nil]; [self hidePopoverAnimated:NO]; @@ -2123,7 +2148,7 @@ - (void)openDashboardRiverForStory:(NSString *)contentId [self loadRiverFeedDetailView:feedDetailViewController withFolder:@"river_dashboard"]; if (showHUD) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self.storyPagesViewController showShareHUD:@"Finding story..."]; } else { MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.feedDetailViewController.view animated:YES]; @@ -2156,9 +2181,9 @@ - (void)recalculateIntelligenceScores:(id)feedId { [newFeedStories addObject:story]; continue; } - + NSMutableDictionary *newStory = [story mutableCopy]; - + // If the story is visible, mark it as sticky so it doesn't go away on page loads. NSInteger score = [NewsBlurAppDelegate computeStoryScore:[story objectForKey:@"intelligence"]]; if (score >= self.selectedIntelligence) { @@ -2220,10 +2245,10 @@ - (void)changeActiveFeedDetailRow { } - (void)loadStoryDetailView { -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone || self.isCompactWidth) { -// [self showDetailViewController:detailViewController sender:self]; -// feedsNavigationController.navigationItem.hidesBackButton = YES; -// } + // if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone || self.isCompactWidth) { + // [self showDetailViewController:detailViewController sender:self]; + // feedsNavigationController.navigationItem.hidesBackButton = YES; + // } self.inFindingStoryMode = NO; self.findingStoryStartDate = nil; @@ -2236,7 +2261,7 @@ - (void)loadStoryDetailView { [self.detailViewController checkLayout]; } - BOOL animated = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && + BOOL animated = (!self.isPhone && !self.tryFeedCategory); [self.storyPagesViewController view]; [self.storyPagesViewController.view setNeedsLayout]; @@ -2252,7 +2277,7 @@ - (void)loadStoryDetailView { [self deferredChangePage:params]; } } - + [MBProgressHUD hideHUDForView:self.storyPagesViewController.view animated:YES]; } @@ -2295,9 +2320,11 @@ - (void)showOriginalStory:(NSURL *)url sender:(id)sender { } NSString *storyBrowser = [preferences stringForKey:@"story_browser"]; - if ([storyBrowser isEqualToString:@"safari"]) { + + if ([storyBrowser isEqualToString:@"system"] || [storyBrowser isEqualToString:@"safari"]) { + // There is no way to force opening in Safari if the default browser on macOS is not Safari. [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; -// [[UIApplication sharedApplication] openURL:url]; + // [[UIApplication sharedApplication] openURL:url]; return; } else if ([storyBrowser isEqualToString:@"chrome"] && [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"googlechrome-x-callback://"]]) { @@ -2316,8 +2343,8 @@ - (void)showOriginalStory:(NSURL *)url sender:(id)sender { return; } else if ([storyBrowser isEqualToString:@"opera_mini"] && [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"opera-http://"]]) { - - + + NSString *operaURL; NSRange prefix = [[url absoluteString] rangeOfString: @"http"]; if (NSNotFound != prefix.location) { @@ -2325,7 +2352,7 @@ - (void)showOriginalStory:(NSURL *)url sender:(id)sender { stringByReplacingCharactersInRange: prefix withString: @"opera-http"]; } - + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:operaURL] options:@{} completionHandler:nil]; return; } else if ([storyBrowser isEqualToString:@"firefox"]) { @@ -2338,8 +2365,8 @@ - (void)showOriginalStory:(NSURL *)url sender:(id)sender { if (NSNotFound != prefix.location) { edgeURL = [[url absoluteString] - stringByReplacingCharactersInRange: prefix - withString: @"microsoft-edge-http"]; + stringByReplacingCharactersInRange: prefix + withString: @"microsoft-edge-http"]; } [[UIApplication sharedApplication] openURL:[NSURL URLWithString:edgeURL] options:@{} completionHandler:nil]; @@ -2357,6 +2384,10 @@ - (void)showOriginalStory:(NSURL *)url sender:(id)sender { } - (void)showInAppBrowser:(NSURL *)url withCustomTitle:(NSString *)customTitle fromSender:(id)sender { +#if TARGET_OS_MACCATALYST +// [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + [AuxSceneDelegate openWindowForURL:url customTitle:customTitle]; +#else if (!originalStoryViewController) { originalStoryViewController = [[OriginalStoryViewController alloc] init]; } @@ -2364,7 +2395,7 @@ - (void)showInAppBrowser:(NSURL *)url withCustomTitle:(NSString *)customTitle fr self.activeOriginalStoryURL = url; originalStoryViewController.customPageTitle = customTitle; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { if ([sender isKindOfClass:[UIBarButtonItem class]]) { [originalStoryViewController view]; // Force viewDidLoad [originalStoryViewController loadInitialStory]; @@ -2389,9 +2420,13 @@ - (void)showInAppBrowser:(NSURL *)url withCustomTitle:(NSString *)customTitle fr [originalStoryViewController loadInitialStory]; [feedsNavigationController showViewController:originalStoryViewController sender:self]; } +#endif } - (void)showSafariViewControllerWithURL:(NSURL *)url useReader:(BOOL)useReader { +#if TARGET_OS_MACCATALYST + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; +#else SFSafariViewControllerConfiguration *config = [SFSafariViewControllerConfiguration new]; config.entersReaderIfAvailable = useReader; @@ -2404,7 +2439,9 @@ - (void)showSafariViewControllerWithURL:(NSURL *)url useReader:(BOOL)useReader { self.safariViewController = [[SFSafariViewController alloc] initWithURL:url configuration:config]; self.safariViewController.delegate = self; [self.storyPagesViewController setNavigationBarHidden:NO]; + [feedsNavigationController presentViewController:self.safariViewController animated:YES completion:nil]; +#endif } - (BOOL)showingSafariViewController { @@ -2419,9 +2456,9 @@ - (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { } - (void)deferredSafariCleanup { -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// self.navigationController.view.frame = CGRectMake(self.navigationController.view.frame.origin.x, self.navigationController.view.frame.origin.y, self.isPortrait ? 270.0 : 370.0, self.navigationController.view.frame.size.height); -// } + // if (!self.isPhone) { + // self.navigationController.view.frame = CGRectMake(self.navigationController.view.frame.origin.x, self.navigationController.view.frame.origin.y, self.isPortrait ? 270.0 : 370.0, self.navigationController.view.frame.size.height); + // } [self.storyPagesViewController reorientPages]; } @@ -2453,8 +2490,8 @@ - (UINavigationController *)fontSettingsNavigationController { } - (void)closeOriginalStory { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { -// [self.masterContainerViewController transitionFromOriginalView]; + if (!self.isPhone) { + // [self.masterContainerViewController transitionFromOriginalView]; } else { if ([[feedsNavigationController viewControllers] containsObject:originalStoryViewController]) { [feedsNavigationController popToViewController:self.storyPagesViewController animated:YES]; @@ -2480,7 +2517,7 @@ - (void)showFeedsListAnimated:(BOOL)animated { // //- (void)setActiveStory:(NSDictionary *)newActiveStory { // NSLog(@"🪿 setActiveStory: %@ -> %@", activeStory[@"story_title"], newActiveStory[@"story_title"]); // log -// +// // activeStory = newActiveStory; //} @@ -2489,23 +2526,23 @@ - (void)showFeedsListAnimated:(BOOL)animated { - (void)handleUserActivity:(NSUserActivity *)activity { if ([activity.activityType isEqualToString:@"com.newsblur.refresh"]) { -// [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; [self showFeedsListAnimated:NO]; [self.feedsViewController refreshFeedList]; } else if ([activity.activityType isEqualToString:@"com.newsblur.gotoFolder"]) { NSString *folder = activity.userInfo[@"folder"]; -// [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; [self showFeedsListAnimated:NO]; [self loadRiverFeedDetailView:self.feedDetailViewController withFolder:folder]; } else if ([activity.activityType isEqualToString:@"com.newsblur.gotoFeed"]) { NSString *folder = activity.userInfo[@"folder"]; NSString *feedID = activity.userInfo[@"feedID"]; -// [self.feedsNavigationController popToRootViewControllerAnimated:NO]; -// [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; + // [self.feedsNavigationController popToRootViewControllerAnimated:NO]; + // [self.splitViewController showColumn:UISplitViewControllerColumnPrimary]; [self showFeedsListAnimated:NO]; if (folder != nil) { @@ -2648,7 +2685,7 @@ - (void)toggleFeedTextView:(id)feedId { #pragma mark - Unread Counts -- (void)populateDictUnreadCounts { +- (void)populateDictUnreadCounts { [self.database inDatabase:^(FMDatabase *db) { FMResultSet *cursor = [db executeQuery:@"SELECT * FROM unread_counts"]; @@ -2664,7 +2701,7 @@ - (void)populateDictUnreadCounts { - (NSInteger)unreadCount { if (storiesCollection.isRiverView || storiesCollection.isSocialRiverView) { return [self unreadCountForFolder:nil]; - } else { + } else { return [self unreadCountForFeed:nil]; } } @@ -2683,17 +2720,17 @@ - (NSInteger)allUnreadCount { NSDictionary *feed = [self.dictUnreadCounts objectForKey:key]; total += [[feed objectForKey:@"ps"] intValue]; total += [[feed objectForKey:@"nt"] intValue]; -// NSLog(@"feed title and number is %@ %i", [feed objectForKey:@"feed_title"], ([[feed objectForKey:@"ps"] intValue] + [[feed objectForKey:@"nt"] intValue])); -// NSLog(@"total is %i", total); + // NSLog(@"feed title and number is %@ %i", [feed objectForKey:@"feed_title"], ([[feed objectForKey:@"ps"] intValue] + [[feed objectForKey:@"nt"] intValue])); + // NSLog(@"total is %i", total); } - + return total; } - (NSInteger)unreadCountForFeed:(NSString *)feedId { NSInteger total = 0; NSDictionary *feed; - + if (feedId) { NSString *feedIdStr = [NSString stringWithFormat:@"%@",feedId]; if ([feedIdStr containsString:@"social:"]) { @@ -2701,7 +2738,7 @@ - (NSInteger)unreadCountForFeed:(NSString *)feedId { } else { feed = [self.dictUnreadCounts objectForKey:feedIdStr]; } - + } else { NSString *feedIdStr = [NSString stringWithFormat:@"%@", [storiesCollection.activeFeed objectForKey:@"id"]]; feed = [self.dictUnreadCounts objectForKey:feedIdStr]; @@ -2748,7 +2785,7 @@ - (NSInteger)unreadCountForFolder:(NSString *)folderName { } else { folder = [self.dictFolders objectForKey:folderName]; } - + for (id feedId in folder) { total += [self unreadCountForFeed:feedId]; } @@ -2799,7 +2836,7 @@ - (UnreadCounts *)splitUnreadCountForFolder:(NSString *)folderName { [counts addCounts:[self splitUnreadCountForFeed:feedId]]; } } else if ([folderName isEqual:@"river_global"] || - (!folderName && [storiesCollection.activeFolder isEqual:@"river_global"])) { + (!folderName && [storiesCollection.activeFolder isEqual:@"river_global"])) { // Nothing for global } else if ([folderName isEqual:@"everything"] || [folderName isEqual:@"infrequent"] || @@ -2837,7 +2874,7 @@ - (UnreadCounts *)splitUnreadCountForFolder:(NSString *)folderName { [self.folderCountCache setObject:[NSNumber numberWithInt:counts.ps] forKey:[NSString stringWithFormat:@"%@-ps", folderName]]; [self.folderCountCache setObject:[NSNumber numberWithInt:counts.nt] forKey:[NSString stringWithFormat:@"%@-nt", folderName]]; [self.folderCountCache setObject:[NSNumber numberWithInt:counts.ng] forKey:[NSString stringWithFormat:@"%@-ng", folderName]]; - + return counts; } @@ -2886,7 +2923,7 @@ - (NSDictionary *)markVisibleStoriesRead { NSMutableArray *stories = [feedsStories objectForKey:feedIdStr]; [stories addObject:[story objectForKey:@"story_hash"]]; [storiesCollection markStoryRead:story feed:feed]; - } + } return feedsStories; } @@ -2898,7 +2935,7 @@ - (void)markActiveFolderAllRead { for (NSString *folderName in self.dictFoldersArray) { for (id feedId in [self.dictFolders objectForKey:folderName]) { [self markFeedAllRead:feedId]; - } + } } } else { for (id feedId in [self.dictFolders objectForKey:storiesCollection.activeFolder]) { @@ -3038,7 +3075,7 @@ - (void)markStoryAsStarred:(NSString *)storyHash withCallback:(void(^)(void))cal - (void)markStoriesRead:(NSDictionary *)stories inFeeds:(NSArray *)feeds cutoffTimestamp:(NSInteger)cutoff { // Must be offline and marking all as read, so load all stories. - + if (stories && [[stories allKeys] count]) { [self queueReadStories:stories]; } @@ -3157,7 +3194,7 @@ - (NSInteger)adjustSavedStoryCount:(NSString *)tagName direction:(NSInteger)dire if (!newTag) { newTag = [@{@"ps": [NSNumber numberWithInt:0], @"feed_title": tagName - } mutableCopy]; + } mutableCopy]; } NSInteger newCount = [[newTag objectForKey:@"ps"] integerValue] + direction; [newTag setObject:[NSNumber numberWithInteger:newCount] forKey:@"ps"]; @@ -3218,11 +3255,11 @@ - (NSArray *)updateStarredStoryCounts:(NSDictionary *)results { [savedStories addObject:savedTagId]; [savedStoryDict setObject:savedTag forKey:savedTagId]; [self.dictUnreadCounts setObject:@{@"ps": [userTag objectForKey:@"count"], - @"nt": [NSNumber numberWithInt:0], - @"ng": [NSNumber numberWithInt:0]} - forKey:savedTagId]; + @"nt": [NSNumber numberWithInt:0], + @"ng": [NSNumber numberWithInt:0]} + forKey:savedTagId]; } - + self.dictSavedStoryTags = savedStoryDict; self.dictSavedStoryFeedCounts = savedStoryFeedCounts; @@ -3275,6 +3312,10 @@ - (void)showMarkReadMenuWithFeedIds:(NSArray *)feedIds collectionTitle:(NSString [self showMarkReadMenuWithFeedIds:feedIds collectionTitle:collectionTitle visibleUnreadCount:0 olderNewerCollection:nil olderNewerStory:nil barButtonItem:nil sourceView:sourceView sourceRect:sourceRect extraItems:nil completionHandler:completionHandler]; } +- (void)showMarkReadMenuWithFeedIds:(NSArray *)feedIds collectionTitle:(NSString *)collectionTitle visibleUnreadCount:(NSInteger)visibleUnreadCount sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect completionHandler:(void (^)(BOOL marked))completionHandler { + [self showMarkReadMenuWithFeedIds:feedIds collectionTitle:collectionTitle visibleUnreadCount:visibleUnreadCount olderNewerCollection:nil olderNewerStory:nil barButtonItem:nil sourceView:sourceView sourceRect:sourceRect extraItems:nil completionHandler:completionHandler]; +} + - (void)showMarkOlderNewerReadMenuWithStoriesCollection:(StoriesCollection *)olderNewerCollection story:(NSDictionary *)olderNewerStory sourceView:(UIView *)sourceView sourceRect:(CGRect)sourceRect extraItems:(NSArray *)extraItems completionHandler:(void (^)(BOOL marked))completionHandler { [self showMarkReadMenuWithFeedIds:nil collectionTitle:nil visibleUnreadCount:0 olderNewerCollection:storiesCollection olderNewerStory:olderNewerStory barButtonItem:nil sourceView:sourceView sourceRect:sourceRect extraItems:extraItems completionHandler:completionHandler]; } @@ -3302,7 +3343,7 @@ - (void)showPopoverWithViewController:(UIViewController *)viewController content UITableViewCell *cell = (UITableViewCell *)sender; [self showPopoverWithViewController:viewController contentSize:contentSize sourceView:cell sourceRect:cell.bounds]; - } else if ([sender class] == [UIBarButtonItem class]) { + } else if ([sender class] == [UIBarButtonItem class] || [sender class] == [UIButton class]) { [self showPopoverWithViewController:viewController contentSize:contentSize barButtonItem:sender]; } else if ([sender class] == [UIView class]) { [self showPopoverWithViewController:viewController contentSize:contentSize sourceView:sender sourceRect:[sender frame]]; @@ -3345,6 +3386,24 @@ - (void)showPopoverWithViewController:(UIViewController *)viewController content popoverPresentationController.backgroundColor = UIColorFromRGB(NEWSBLUR_WHITE_COLOR); popoverPresentationController.permittedArrowDirections = permittedArrowDirections; +#if TARGET_OS_MACCATALYST + if (barButtonItem && barButtonItem == appDelegate.feedDetailViewController.settingsBarButton) { + UINavigationController *feedDetailNavController = appDelegate.feedDetailViewController.navigationController; + barButtonItem = nil; + sourceView = feedDetailNavController.view; + if (appDelegate.splitViewController.isFeedListHidden) { + sourceRect = CGRectMake(224, 0, 20, 20); + } else { + sourceRect = CGRectMake(152, 0, 20, 20); + } + } else if (barButtonItem && barButtonItem == appDelegate.storyPagesViewController.fontSettingsButton) { + UINavigationController *storiesNavController = appDelegate.storyPagesViewController.navigationController; + barButtonItem = nil; + sourceView = storiesNavController.view; + sourceRect = CGRectMake(storiesNavController.view.frame.size.width - 59, 0, 20, 20); + } +#endif + if (barButtonItem) { popoverPresentationController.barButtonItem = barButtonItem; } else { @@ -3355,7 +3414,7 @@ - (void)showPopoverWithViewController:(UIViewController *)viewController content [self.navigationControllerForPopover presentViewController:viewController animated:YES completion:^{ popoverPresentationController.passthroughViews = nil; // NSLog(@"%@ canBecomeFirstResponder? %d", viewController, viewController.canBecomeFirstResponder); - [viewController becomeFirstResponder]; +// [viewController becomeFirstResponder]; }]; } @@ -3388,7 +3447,11 @@ - (void)hidePopover { } - (UINavigationController *)navigationControllerForPopover { +#if TARGET_OS_MACCATALYST + return self.storyPagesViewController.navigationController ?: self.feedsNavigationController; +#else return self.feedsNavigationController; +#endif } #pragma mark - @@ -3873,7 +3936,7 @@ - (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId { [feedClassifiers setObject:authors forKey:@"authors"]; [storiesCollection.activeClassifiers setObject:feedClassifiers forKey:feedId]; [self.storyPagesViewController refreshHeaders]; - [self.trainerViewController refresh]; + [self.trainerViewController reload]; NSString *urlString = [NSString stringWithFormat:@"%@/classifier/save", self.url]; @@ -3918,7 +3981,7 @@ - (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId { [feedClassifiers setObject:tags forKey:@"tags"]; [storiesCollection.activeClassifiers setObject:feedClassifiers forKey:feedId]; [self.storyPagesViewController refreshHeaders]; - [self.trainerViewController refresh]; + [self.trainerViewController reload]; NSString *urlString = [NSString stringWithFormat:@"%@/classifier/save", self.url]; @@ -3967,7 +4030,7 @@ - (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score: [feedClassifiers setObject:titles forKey:@"titles"]; [storiesCollection.activeClassifiers setObject:feedClassifiers forKey:feedId]; [self.storyPagesViewController refreshHeaders]; - [self.trainerViewController refresh]; + [self.trainerViewController reload]; NSString *urlString = [NSString stringWithFormat:@"%@/classifier/save", self.url]; @@ -4010,7 +4073,7 @@ - (void)toggleFeedClassifier:(NSString *)feedId { [feedClassifiers setObject:feeds forKey:@"feeds"]; [storiesCollection.activeClassifiers setObject:feedClassifiers forKey:feedId]; [self.storyPagesViewController refreshHeaders]; - [self.trainerViewController refresh]; + [self.trainerViewController reload]; NSString *urlString = [NSString stringWithFormat:@"%@/classifier/save", self.url]; diff --git a/clients/ios/Classes/NotificationsViewController.h b/clients/ios/Classes/NotificationsViewController.h index 4a33d991cf..b0f59c8ea4 100644 --- a/clients/ios/Classes/NotificationsViewController.h +++ b/clients/ios/Classes/NotificationsViewController.h @@ -13,11 +13,9 @@ @class NewsBlurAppDelegate; @interface NotificationsViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; NSArray *notificationFeedIds; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UITableView *notificationsTable; @property (nonatomic) NSString *feedId; diff --git a/clients/ios/Classes/NotificationsViewController.m b/clients/ios/Classes/NotificationsViewController.m index 709581ce21..eb041e3402 100644 --- a/clients/ios/Classes/NotificationsViewController.m +++ b/clients/ios/Classes/NotificationsViewController.m @@ -16,14 +16,11 @@ @interface NotificationsViewController () @implementation NotificationsViewController @synthesize notificationsTable; -@synthesize appDelegate; @synthesize feedId; - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - self.navigationItem.title = @"Notifications"; UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle: @"Done" style: UIBarButtonItemStylePlain @@ -31,9 +28,6 @@ - (void)viewDidLoad { action: @selector(doCancelButton)]; [self.navigationItem setRightBarButtonItem:cancelButton]; - // Do any additional setup after loading the view from its nib. - self.appDelegate = (NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate]; - notificationsTable = [[UITableView alloc] init]; notificationsTable.delegate = self; notificationsTable.dataSource = self; @@ -85,7 +79,7 @@ - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { int headerLabelHeight, folderImageViewY; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { headerLabelHeight = 36; folderImageViewY = 8; } else { diff --git a/clients/ios/Classes/OriginalStoryViewController.h b/clients/ios/Classes/OriginalStoryViewController.h index 4798472a6c..214de52cd8 100644 --- a/clients/ios/Classes/OriginalStoryViewController.h +++ b/clients/ios/Classes/OriginalStoryViewController.h @@ -10,13 +10,10 @@ #import "BaseViewController.h" #import -@class NewsBlurAppDelegate; - @interface OriginalStoryViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; NSString *activeUrl; NSMutableArray *visitedUrls; WKWebView *webView; @@ -27,7 +24,6 @@ UIGestureRecognizerDelegate> { BOOL finishedLoading; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet WKWebView *webView; //@property (strong, nonatomic) SloppySwiper *swiper; @property (nonatomic) UIProgressView *progressView; diff --git a/clients/ios/Classes/OriginalStoryViewController.m b/clients/ios/Classes/OriginalStoryViewController.m index 8e31db3da5..744cb2f10b 100644 --- a/clients/ios/Classes/OriginalStoryViewController.m +++ b/clients/ios/Classes/OriginalStoryViewController.m @@ -17,7 +17,6 @@ @implementation OriginalStoryViewController -@synthesize appDelegate; @synthesize webView; //@synthesize swiper; @synthesize progressView; @@ -25,14 +24,12 @@ @implementation OriginalStoryViewController - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - self.view.layer.masksToBounds = NO; self.view.layer.shadowRadius = 5; self.view.layer.shadowOpacity = 0.5; self.view.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.view.bounds].CGPath; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { closeButton = [UIBarButtonItem barItemWithImage:[UIImage imageNamed:@"ios7_back_button"] target:self action:@selector(closeOriginalView)]; @@ -70,7 +67,7 @@ - (void)viewDidLoad { // UIGestureRecognizer *themeGesture = [[ThemeManager themeManager] addThemeGestureRecognizerToView:self.webView]; // [self.webView.scrollView.panGestureRecognizer requireGestureRecognizerToFail:themeGesture]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; gesture.delegate = self; @@ -215,7 +212,7 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer { center.y); self.view.center = center; [recognizer setTranslation:CGPointZero inView:self.view]; -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // [appDelegate.masterContainerViewController interactiveTransitionFromOriginalView:percentage]; // } else { // @@ -231,7 +228,7 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer { [self transitionToFeedDetail:recognizer]; } else { // NSLog(@"Original velocity: %f (at %.2f%%)", velocity, percentage*100); -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // [appDelegate.masterContainerViewController transitionToOriginalView:NO]; // } else { // @@ -241,7 +238,7 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer { } - (void)transitionToFeedDetail:(UIGestureRecognizer *)recognizer { -// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +// if (!self.isPhone) { // [appDelegate.masterContainerViewController transitionFromOriginalView]; // } else { // @@ -365,7 +362,7 @@ - (nullable WKWebView *)webView:(WKWebView *)aWebView createWebViewWithConfigura - (void)updateTitle:(WKWebView*)aWebView { - if (self.customPageTitle != nil) { + if (self.customPageTitle.length > 0) { titleView.text = self.customPageTitle; } else { NSString *pageTitleValue = webView.title; @@ -373,6 +370,10 @@ - (void)updateTitle:(WKWebView*)aWebView } [titleView sizeToFit]; + +#if TARGET_OS_MACCATALYST + self.view.window.windowScene.title = titleView.text; +#endif } - (IBAction)loadAddress:(id)sender { diff --git a/clients/ios/Classes/PremiumViewController.h b/clients/ios/Classes/PremiumViewController.h index cce68780ee..9f1a8dede2 100644 --- a/clients/ios/Classes/PremiumViewController.h +++ b/clients/ios/Classes/PremiumViewController.h @@ -14,7 +14,6 @@ @interface PremiumViewController : BaseViewController -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UITableView *premiumTable; diff --git a/clients/ios/Classes/PremiumViewController.m b/clients/ios/Classes/PremiumViewController.m index 0aa2db547e..2a821e5c93 100644 --- a/clients/ios/Classes/PremiumViewController.m +++ b/clients/ios/Classes/PremiumViewController.m @@ -24,8 +24,6 @@ @implementation PremiumViewController - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle: @"Done" style: UIBarButtonItemStylePlain target: self diff --git a/clients/ios/Classes/SceneDelegate.swift b/clients/ios/Classes/SceneDelegate.swift new file mode 100644 index 0000000000..9d771aa059 --- /dev/null +++ b/clients/ios/Classes/SceneDelegate.swift @@ -0,0 +1,66 @@ +// +// SceneDelegate.swift +// NewsBlur +// +// Created by David Sinclair on 2023-11-15. +// Copyright © 2023 NewsBlur. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + let appDelegate: NewsBlurAppDelegate = .shared + + var window: UIWindow? +#if targetEnvironment(macCatalyst) + var toolbar = NSToolbar(identifier: "main") + var toolbarDelegate = ToolbarDelegate() +#endif + + @objc(closeAuxWindows) class func closeAuxWindows() { + for window in UIApplication.shared.windows { + if window.windowScene?.delegate is AuxSceneDelegate, let session = window.windowScene?.session { + window.isHidden = true + UIApplication.shared.requestSceneSessionDestruction(session, options: .none) + } + } + } + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + if appDelegate.window != nil { + DispatchQueue.main.async { + self.window?.isHidden = true + UIApplication.shared.requestSceneSessionDestruction(session, options: .none) + } + return + } + + appDelegate.window = window + +#if targetEnvironment(macCatalyst) + guard let windowScene = scene as? UIWindowScene, let titlebar = windowScene.titlebar else { + return + } + + if #available(macCatalyst 16.0, *) { + windowScene.windowingBehaviors?.isClosable = false + } + + toolbar.delegate = toolbarDelegate + toolbar.displayMode = .iconOnly + + titlebar.toolbar = toolbar + titlebar.toolbarStyle = .automatic +#endif + + appDelegate.prepareViewControllers() + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { + return + } + + appDelegate.open(url) + } +} diff --git a/clients/ios/Classes/ShareViewController.h b/clients/ios/Classes/ShareViewController.h index 38df0a61a0..7d4cf67d6e 100644 --- a/clients/ios/Classes/ShareViewController.h +++ b/clients/ios/Classes/ShareViewController.h @@ -15,7 +15,6 @@ } @property (nonatomic) IBOutlet UITextView *commentField; -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UIButton *facebookButton; @property (nonatomic) IBOutlet UIButton *twitterButton; @property (nonatomic) IBOutlet UIBarButtonItem *submitButton; diff --git a/clients/ios/Classes/ShareViewController.m b/clients/ios/Classes/ShareViewController.m index eded2e0f66..6661edc770 100644 --- a/clients/ios/Classes/ShareViewController.m +++ b/clients/ios/Classes/ShareViewController.m @@ -21,7 +21,6 @@ @implementation ShareViewController @synthesize twitterButton; @synthesize submitButton; @synthesize commentField; -@synthesize appDelegate; @synthesize activeReplyId; @synthesize activeCommentId; @synthesize activeStoryId; @@ -38,9 +37,7 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil } - (void)viewDidLoad { - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - - [[NSNotificationCenter defaultCenter] + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onTextChange:) name:UITextViewTextDidChangeNotification @@ -202,7 +199,7 @@ - (void)adjustCommentField:(CGSize)kbSize { self.storyTitle.frame = CGRectMake(20, 8, v.width - 20*2, 24); stOffset = self.storyTitle.frame.origin.y + self.storyTitle.frame.size.height; stHeight = self.storyTitle.frame.size.height; - } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + } else if (!self.isPhone) { k = 0; } NSLog(@"Share type: %@", self.currentType); diff --git a/clients/ios/Classes/SmallActivityCell.m b/clients/ios/Classes/SmallActivityCell.m index c2ffa2f69c..0e4126d675 100644 --- a/clients/ios/Classes/SmallActivityCell.m +++ b/clients/ios/Classes/SmallActivityCell.m @@ -9,6 +9,7 @@ #import "SmallActivityCell.h" #import "UIImageView+AFNetworking.h" #import +#import "NewsBlurAppDelegate.h" @implementation SmallActivityCell @@ -62,7 +63,7 @@ - (void)layoutSubviews { labelFrame.size.height = contentRect.size.height; self.activityLabel.frame = labelFrame; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!((NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate]).isPhone) { self.activityLabel.backgroundColor = UIColorFromRGB(0xd7dadf); } else { self.activityLabel.backgroundColor = UIColorFromRGB(0xf6f6f6); diff --git a/clients/ios/Classes/SmallInteractionCell.m b/clients/ios/Classes/SmallInteractionCell.m index 6d346fe82e..23a7d15fd1 100644 --- a/clients/ios/Classes/SmallInteractionCell.m +++ b/clients/ios/Classes/SmallInteractionCell.m @@ -9,6 +9,7 @@ #import "SmallInteractionCell.h" #import "UIImageView+AFNetworking.h" #import +#import "NewsBlurAppDelegate.h" @implementation SmallInteractionCell @@ -57,8 +58,8 @@ - (void)layoutSubviews { labelFrame.size.width = contentRect.size.width - leftMargin - avatarSize - leftMargin - rightMargin - 20; labelFrame.size.height = contentRect.size.height; self.interactionLabel.frame = labelFrame; - - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + + if (!((NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate]).isPhone) { self.interactionLabel.backgroundColor = UIColorFromRGB(0xd7dadf); } else { self.interactionLabel.backgroundColor = UIColorFromRGB(0xf6f6f6); diff --git a/clients/ios/Classes/SplitViewController.swift b/clients/ios/Classes/SplitViewController.swift index 096496ffe6..28279c62d4 100644 --- a/clients/ios/Classes/SplitViewController.swift +++ b/clients/ios/Classes/SplitViewController.swift @@ -10,6 +10,10 @@ import UIKit /// Subclass of `UISplitViewController` to enable customizations. class SplitViewController: UISplitViewController { + @objc var isFeedListHidden: Bool { + return [.oneBesideSecondary, .oneOverSecondary, .secondaryOnly].contains(displayMode) + } + /// Update the theme of the split view controller. @objc func updateTheme() { @@ -24,4 +28,10 @@ class SplitViewController: UISplitViewController { override var childForStatusBarStyle: UIViewController? { return nil } + + // Can do menu validation here. +// override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { +// print("canPerformAction: \(action) with \(sender ?? "nil")") +// return true +// } } diff --git a/clients/ios/Classes/StoriesCollection.h b/clients/ios/Classes/StoriesCollection.h index c8e3389994..c56cb75d43 100644 --- a/clients/ios/Classes/StoriesCollection.h +++ b/clients/ios/Classes/StoriesCollection.h @@ -59,10 +59,15 @@ @property (nonatomic, readwrite) BOOL transferredFromDashboard; @property (nonatomic, readwrite) BOOL showHiddenStories; @property (nonatomic, readwrite) BOOL inSearch; +@property (nonatomic, readonly) BOOL isEverything; +@property (nonatomic, readonly) BOOL isInfrequent; @property (nonatomic, readonly) BOOL isRiverOrSocial; +@property (nonatomic, readonly) BOOL isCustomFolder; +@property (nonatomic, readonly) BOOL isCustomFolderOrFeed; @property (nonatomic) NSString *searchQuery; @property (nonatomic) NSString *savedSearchQuery; +@property (nonatomic, readonly) NSString *activeFeedIdStr; @property (nonatomic, readonly) NSString *activeOrder; @property (nonatomic, readonly) NSString *activeReadFilter; @property (nonatomic, readonly) NSString *activeStoryTitlesPosition; diff --git a/clients/ios/Classes/StoriesCollection.m b/clients/ios/Classes/StoriesCollection.m index aeac005ecb..9b5b11ab35 100644 --- a/clients/ios/Classes/StoriesCollection.m +++ b/clients/ios/Classes/StoriesCollection.m @@ -97,10 +97,26 @@ - (void)transferStoriesFromCollection:(StoriesCollection *)fromCollection { self.savedSearchQuery = fromCollection.savedSearchQuery; } +- (BOOL)isEverything { + return [activeFolder isEqualToString:@"everything"]; +} + +- (BOOL)isInfrequent { + return [activeFolder isEqualToString:@"infrequent"]; +} + - (BOOL)isRiverOrSocial { return self.isRiverView || self.isSavedView || self.isReadView || self.isWidgetView || self.isSocialView || self.isSocialRiverView; } +- (BOOL)isCustomFolder { + return self.isRiverView && !self.isEverything && !self.isInfrequent && !self.isSavedView && !self.isReadView && !self.isSocialView && !self.isWidgetView; +} + +- (BOOL)isCustomFolderOrFeed { + return !self.isRiverView || self.isCustomFolder; +} + #pragma mark - Story Traversal - (BOOL)isStoryUnread:(NSDictionary *)story { @@ -232,6 +248,10 @@ - (NSInteger)indexFromLocation:(NSInteger)location { return [[activeFeedStoryLocations objectAtIndex:location] intValue]; } +- (NSString *)activeFeedIdStr { + return [NSString stringWithFormat:@"%@", [activeFeed objectForKey:@"id"]]; +} + - (NSString *)activeOrder { NSUserDefaults *userPreferences = [NSUserDefaults standardUserDefaults]; NSString *orderPrefDefault = [userPreferences stringForKey:@"default_order"]; diff --git a/clients/ios/Classes/Story.swift b/clients/ios/Classes/Story.swift index 83b4f8b3e8..62b819d28e 100644 --- a/clients/ios/Classes/Story.swift +++ b/clients/ios/Classes/Story.swift @@ -8,17 +8,17 @@ import Foundation -// The Story and StoryCache classes could be quite useful going forward; Rather than calling getStory() to get the dictionary, could have a variation that returns a Story instance. Could fetch from the cache if available, or make and cache one from the dictionary. Would need to remove it from the cache when changing anything about a story. Could perhaps make the cache part of StoriesCollection. +// The Feed, Story, and StoryCache classes could be quite useful going forward; Rather than calling getStory() to get the dictionary, could have a variation that returns a Story instance. Could fetch from the cache if available, or make and cache one from the dictionary. Would need to remove it from the cache when changing anything about a story. Could perhaps make the cache part of StoriesCollection. /// A story, wrapping the dictionary representation. class Story: Identifiable { let id = UUID() let index: Int - var dictionary = [String : Any]() + var dictionary = AnyDictionary() + + var feed: Feed? - var feedID = "" - var feedName = "" var title = "" var content = "" var dateString = "" @@ -35,9 +35,34 @@ class Story: Identifiable { return author.isEmpty ? dateString : "\(dateString) · \(author)" } - var isRiverOrSocial = true - var feedColorBarLeft: UIColor? - var feedColorBarRight: UIColor? + var titles: [Feed.Training] { + guard let classifiers = feed?.classifiers(for: "titles") else { + return [] + } + + let lowercasedTitle = title.lowercased() + let keys = classifiers.keys.compactMap { $0 as? String } + let words = keys.filter { lowercasedTitle.contains($0.lowercased()) } + let sorted = words.sorted() + + return sorted.map { Feed.Training(name: $0, count: 0, score: Feed.Score(rawValue: classifiers[$0] as? Int ?? 0) ?? .none) } + } + + var authors: [Feed.Training] { + guard let classifiers = feed?.classifiers(for: "authors") else { + return [] + } + + return [Feed.Training(name: author, count: 0, score: Feed.Score(rawValue: classifiers[author] as? Int ?? 0) ?? .none)] + } + + var tags: [Feed.Training] { + guard let tags = dictionary["story_tags"] as? [String], let classifiers = feed?.classifiers(for: "tags") else { + return [] + } + + return tags.map { Feed.Training(name: $0, count: 0, score: Feed.Score(rawValue: classifiers[$0] as? Int ?? 0) ?? .none) } + } var isSelected: Bool { return index == NewsBlurAppDelegate.shared!.storiesCollection.locationOfActiveStory() @@ -79,24 +104,13 @@ class Story: Identifiable { dictionary = story - if let id = dictionary["story_feed_id"] { - feedID = appDelegate.feedIdWithoutSearchQuery("\(id)") - } - - var feed: [String : Any]? - - if storiesCollection.isRiverOrSocial { - feed = appDelegate.dictActiveFeeds[feedID] as? [String : Any] - } - - if feed == nil { - feed = appDelegate.dictFeeds[feedID] as? [String : Any] - } - - if let feed { - feedName = feed["feed_title"] as? String ?? "" - feedColorBarLeft = color(for: "favicon_fade", from: feed, default: "707070") - feedColorBarRight = color(for: "favicon_color", from: feed, default: "505050") + if let dictID = dictionary["story_feed_id"], let id = appDelegate.feedIdWithoutSearchQuery("\(dictID)") { + if let cachedFeed = StoryCache.feeds[id] { + feed = cachedFeed + } else { + feed = Feed(id: id) + StoryCache.feeds[id] = feed + } } title = (string(for: "story_title") as NSString).decodingHTMLEntities() @@ -114,17 +128,6 @@ class Story: Identifiable { isRead = !storiesCollection .isStoryUnread(dictionary) isReadAvailable = storiesCollection.activeFolder != "saved_stories" - isRiverOrSocial = storiesCollection.isRiverOrSocial - } - - func color(for key: String, from feed: [String : Any], default defaultHex: String) -> UIColor { - let hex = feed[key] as? String ?? defaultHex - let scanner = Scanner(string: hex) - var color: Int64 = 0 - scanner.scanHexInt64(&color) - let value = Int(color) - - return ThemeManager.shared.fixedColor(fromRGB: value) ?? UIColor.gray } } @@ -136,235 +139,6 @@ extension Story: Equatable { extension Story: CustomDebugStringConvertible { var debugDescription: String { - return "Story #\(index) \"\(title)\" in \(feedName)" - } -} - -/// A cache of stories for the feed detail grid view. -class StoryCache: ObservableObject { - let appDelegate = NewsBlurAppDelegate.shared! - - let settings = StorySettings() - - var isDarkTheme: Bool { - return ThemeManager.shared.isDarkTheme - } - - var isGrid: Bool { - return appDelegate.detailViewController.layout == .grid - } - - var isPhone: Bool { - return appDelegate.detailViewController.isPhone - } - - var canPullToRefresh: Bool { - return appDelegate.feedDetailViewController.canPullToRefresh - } - - @Published var before = [Story]() - @Published var selected: Story? - @Published var after = [Story]() - - var all: [Story] { - if let selected { - return before + [selected] + after - } else { - return before + after - } - } - - func story(with index: Int) -> Story? { - return all.first(where: { $0.index == index } ) - } - - func reload() { - let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount) - var beforeSelection = [Int]() - var selectedIndex = -999 - var afterSelection = [Int]() - - if storyCount > 0 { - selectedIndex = appDelegate.storiesCollection.locationOfActiveStory() - - if selectedIndex < 0 { - beforeSelection = Array(0..= 0 ? Story(index: selectedIndex) : nil - after = afterSelection.map { Story(index: $0) } - - print("🪿 Reload: \(before.count) before, \(selected == nil ? "none" : selected!.debugTitle) selected, \(after.count) after") - - -// -// #warning("hack") -// -// print("🪿 ... count: \(storyCount), index: \(selectedIndex)") -// print("🪿 ... before: \(before)") -// print("🪿 ... selection: \(selected == nil ? "none" : selected!.debugTitle)") -// print("🪿 ... after: \(after)") - - - - } - - func reload(story: Story) { - if story == selected { - selected = Story(index: story.index) - } else if let index = before.firstIndex(of: story) { - before[index] = Story(index: story.index) - } else if let index = after.firstIndex(of: story) { - after[index] = Story(index: story.index) - } - } -} - -class StorySettings { - let defaults = UserDefaults.standard - - enum Content: String, RawRepresentable { - case title - case short - case medium - case long - - static let titleLimit = 6 - - static let contentLimit = 10 - - var limit: Int { - switch self { - case .title: - return 6 - case .short: - return 2 - case .medium: - return 4 - case .long: - return 6 - } - } - } - - var content: Content { - if let string = defaults.string(forKey: "story_list_preview_text_size"), let value = Content(rawValue: string) { - return value - } else { - return .short - } - } - - enum Preview: String, RawRepresentable { - case none - case smallLeft = "small_left" - case largeLeft = "large_left" - case largeRight = "large_right" - case smallRight = "small_right" - - var isLeft: Bool { - return [.smallLeft, .largeLeft].contains(self) - } - - var isSmall: Bool { - return [.smallLeft, .smallRight].contains(self) - } - } - - var preview: Preview { - if let string = defaults.string(forKey: "story_list_preview_images_size"), let value = Preview(rawValue: string) { - return value - } else { - return .smallRight - } - } - - enum FontSize: String, RawRepresentable { - case xs - case small - case medium - case large - case xl - - var offset: CGFloat { - switch self { - case .xs: - return -2 - case .small: - return -1 - case .medium: - return 0 - case .large: - return 1 - case .xl: - return 2 - } - } - } - - var fontSize: FontSize { - if let string = defaults.string(forKey: "feed_list_font_size"), let value = FontSize(rawValue: string) { - return value - } else { - return .medium - } - } - - enum Spacing: String, RawRepresentable { - case compact - case comfortable - } - - var spacing: Spacing { - if let string = defaults.string(forKey: "feed_list_spacing"), let value = Spacing(rawValue: string) { - return value - } else { - return .comfortable - } - } - - var gridColumns: Int { - guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else { - if NewsBlurAppDelegate.shared.isCompactWidth { - return 1 - } else if NewsBlurAppDelegate.shared.isPortrait() { - return 2 - } else { - return 4 - } - } - - if NewsBlurAppDelegate.shared.isPortrait(), columns > 3 { - return 3 - } - - return columns - } - - var gridHeight: CGFloat { - guard let pref = UserDefaults.standard.string(forKey: "grid_height") else { - return 400 - } - - switch pref { - case "xs": - return 250 - case "short": - return 300 - case "tall": - return 500 - case "xl": - return 600 - default: - return 400 - } + return "Story #\(index) \"\(title)\" in \(feed?.name ?? "")" } } diff --git a/clients/ios/Classes/StoryCache.swift b/clients/ios/Classes/StoryCache.swift new file mode 100644 index 0000000000..9b30139f86 --- /dev/null +++ b/clients/ios/Classes/StoryCache.swift @@ -0,0 +1,114 @@ +// +// StoryCache.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-04. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import Foundation + +// The Feed, Story, and StoryCache classes could be quite useful going forward; Rather than calling getStory() to get the dictionary, could have a variation that returns a Story instance. Could fetch from the cache if available, or make and cache one from the dictionary. Would need to remove it from the cache when changing anything about a story. Could perhaps make the cache part of StoriesCollection. + +/// A cache of stories for the feed detail grid view. +class StoryCache: ObservableObject { + let appDelegate = NewsBlurAppDelegate.shared! + + let settings = StorySettings() + + var isDarkTheme: Bool { + return ThemeManager.shared.isDarkTheme + } + + var isGrid: Bool { + return appDelegate.detailViewController.layout == .grid + } + + var isPhone: Bool { + return appDelegate.detailViewController.isPhone + } + + var canPullToRefresh: Bool { + return appDelegate.feedDetailViewController.canPullToRefresh + } + + @Published var before = [Story]() + @Published var selected: Story? + @Published var after = [Story]() + + var all: [Story] { + if let selected { + return before + [selected] + after + } else { + return before + after + } + } + + func story(with index: Int) -> Story? { + return all.first(where: { $0.index == index } ) + } + + static var feeds = [String : Feed]() + + var currentFeed: Feed? + + func reload() { + let debug = Date() + let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount) + var beforeSelection = [Int]() + var selectedIndex = -999 + var afterSelection = [Int]() + + if storyCount > 0 { + selectedIndex = appDelegate.storiesCollection.locationOfActiveStory() + + if selectedIndex < 0 { + beforeSelection = Array(0..= 0 ? Story(index: selectedIndex) : nil + after = afterSelection.map { Story(index: $0) } + + print("🪿 Reload: \(before.count) before, \(selected == nil ? "none" : selected!.debugTitle) selected, \(after.count) after, took \(-debug.timeIntervalSinceNow) seconds") + + + // + // #warning("hack") + // + // print("🪿 ... count: \(storyCount), index: \(selectedIndex)") + // print("🪿 ... before: \(before)") + // print("🪿 ... selection: \(selected == nil ? "none" : selected!.debugTitle)") + // print("🪿 ... after: \(after)") + + + + } + + func reload(story: Story) { + if story == selected { + selected = Story(index: story.index) + } else if let index = before.firstIndex(of: story) { + before[index] = Story(index: story.index) + } else if let index = after.firstIndex(of: story) { + after[index] = Story(index: story.index) + } + } +} diff --git a/clients/ios/Classes/StoryDetailObjCViewController.h b/clients/ios/Classes/StoryDetailObjCViewController.h index 948d3dfbd5..28937ae5ac 100644 --- a/clients/ios/Classes/StoryDetailObjCViewController.h +++ b/clients/ios/Classes/StoryDetailObjCViewController.h @@ -11,13 +11,9 @@ #import "BaseViewController.h" @import WebKit; -@class NewsBlurAppDelegate; - @interface StoryDetailObjCViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; - NSString *activeStoryId; NSMutableDictionary *activeStory; UIView *innerView; @@ -34,7 +30,6 @@ UIActionSheetDelegate, WKNavigationDelegate> { UIInterfaceOrientation _orientation; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) NSString *activeStoryId; @property (nonatomic, readwrite) NSMutableDictionary *activeStory; @property (nonatomic) IBOutlet UIView *innerView; diff --git a/clients/ios/Classes/StoryDetailObjCViewController.m b/clients/ios/Classes/StoryDetailObjCViewController.m index e81789ec73..03819a2583 100644 --- a/clients/ios/Classes/StoryDetailObjCViewController.m +++ b/clients/ios/Classes/StoryDetailObjCViewController.m @@ -24,8 +24,8 @@ #import "JNWThrottledBlock.h" #import "NewsBlur-Swift.h" -#define iPadPro12 ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && ([UIScreen mainScreen].bounds.size.height == 1366 || [UIScreen mainScreen].bounds.size.width == 1366)) -#define iPadPro10 ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && ([UIScreen mainScreen].bounds.size.height == 1112 || [UIScreen mainScreen].bounds.size.width == 1112)) +#define iPadPro12 (!self.isPhone && ([UIScreen mainScreen].bounds.size.height == 1366 || [UIScreen mainScreen].bounds.size.width == 1366)) +#define iPadPro10 (!self.isPhone && ([UIScreen mainScreen].bounds.size.height == 1112 || [UIScreen mainScreen].bounds.size.width == 1112)) @interface StoryDetailObjCViewController () @@ -35,7 +35,6 @@ @interface StoryDetailObjCViewController () @implementation StoryDetailObjCViewController -@synthesize appDelegate; @synthesize activeStoryId; @synthesize activeStory; @synthesize innerView; @@ -71,8 +70,6 @@ - (NSString *)description { - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; AVAudioSession *audioSession = [AVAudioSession sharedInstance]; @@ -99,7 +96,7 @@ - (void)viewDidLoad { [self.webView.scrollView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { self.webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } @@ -314,6 +311,11 @@ - (void)viewWillDisappear:(BOOL)animated { - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; +#if TARGET_OS_MACCATALYST + [self.navigationController setNavigationBarHidden:YES animated:animated]; + [self.navigationController setToolbarHidden:YES animated:animated]; +#endif + if (!self.isPhoneOrCompact) { [appDelegate.feedDetailViewController.view endEditing:YES]; } @@ -401,9 +403,13 @@ - (void)loadHTMLString:(NSString *)html { static NSURL *baseURL; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ +#if TARGET_OS_MACCATALYST + baseURL = [NSBundle mainBundle].resourceURL; +#else baseURL = [NSBundle mainBundle].bundleURL; +#endif }); - + [self.webView loadHTMLString:html baseURL:baseURL]; } @@ -480,7 +486,7 @@ - (void)drawStory:(BOOL)force withOrientation:(UIInterfaceOrientation)orientatio #if TARGET_OS_MACCATALYST // CATALYST: probably will want to add custom CSS for Macs. - contentWidthClass = @"NB-ipad-wide NB-ipad-pro-12-wide NB-width-768"; + contentWidthClass = @"NB-mac NB-ipad-pro-12-wide"; #else if (UIInterfaceOrientationIsLandscape(orientation) && !self.isPhoneOrCompact) { if (iPadPro12) { @@ -503,14 +509,15 @@ - (void)drawStory:(BOOL)force withOrientation:(UIInterfaceOrientation)orientatio } else { contentWidthClass = @"NB-iphone"; } +#endif contentWidthClass = [NSString stringWithFormat:@"%@ NB-width-%d", contentWidthClass, (int)floorf(CGRectGetWidth(self.view.frame))]; -#endif - if (appDelegate.feedsViewController.isOffline) { + // if (appDelegate.feedsViewController.isOffline) { NSString *storyHash = [self.activeStory objectForKey:@"story_hash"]; NSArray *imageUrls = [appDelegate.activeCachedImages objectForKey:storyHash]; + // NSLog(@"📚 imageUrls: %@", imageUrls); if (imageUrls) { NSString *storyImagesDirectory = [appDelegate.documentsURL.path stringByAppendingPathComponent:@"story_images"]; @@ -524,7 +531,7 @@ - (void)drawStory:(BOOL)force withOrientation:(UIInterfaceOrientation)orientatio withString:cachedUrl.absoluteString]; } } - } + // } NSString *feedIdStr = [NSString stringWithFormat:@"%@", [self.activeStory @@ -614,7 +621,7 @@ - (void)drawStory:(BOOL)force withOrientation:(UIInterfaceOrientation)orientatio NSString *htmlTopAndBottom = [htmlTop stringByAppendingString:htmlBottom]; -// NSLog(@"\n\n\n\nStory html (%@):\n\n\n%@\n\n\n", self.activeStory[@"story_title"], htmlContent); + // NSLog(@"\n\n\n\nStory html (%@):\n\n\n%@\n\n\n", self.activeStory[@"story_title"], htmlContent); self.hasStory = NO; self.fullStoryHTML = htmlContent; @@ -1399,9 +1406,11 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N int bottomPosition = webpageHeight - topPosition - viewportHeight; BOOL singlePage = webpageHeight - 200 <= viewportHeight; BOOL atBottom = bottomPosition < 150; - BOOL pullingDown = topPosition < 0; BOOL atTop = topPosition < 50; +#if !TARGET_OS_MACCATALYST + BOOL pullingDown = topPosition < 0; BOOL nearTop = topPosition < 100; +#endif if (!hasScrolled && topPosition != 0) { hasScrolled = YES; @@ -1417,6 +1426,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } } +#if !TARGET_OS_MACCATALYST if (!isNavBarHidden && self.canHideNavigationBar && !nearTop) { [appDelegate.storyPagesViewController setNavigationBarHidden:YES]; } @@ -1424,6 +1434,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N if (isNavBarHidden && pullingDown) { [appDelegate.storyPagesViewController setNavigationBarHidden:NO]; } +#endif if (!atTop && !atBottom && !singlePage) { BOOL traversalVisible = appDelegate.storyPagesViewController.traverseView.alpha > 0; @@ -1878,6 +1889,12 @@ - (void)webViewNotifyLoaded { [self scrollToLastPosition:YES]; } +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { + NSLog(@"Web content process did terminate: %@", webView); // log + + [self drawStory]; +} + - (void)checkTryFeedStory { // see if it's a tryfeed for animation if (!self.webView.hidden && @@ -2241,9 +2258,11 @@ - (void)fetchImage:(NSURL *)url copy:(BOOL)copy save:(BOOL)save { } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { - if ([self respondsToSelector:action]) - return self.noStoryMessage.hidden; - return [super canPerformAction:action withSender:sender]; + if ([self respondsToSelector:action]) { + return [super canPerformAction:action withSender:sender] && self.noStoryMessage.hidden; + } else { + return [super canPerformAction:action withSender:sender]; + } } # pragma mark - @@ -2409,7 +2428,7 @@ - (void)changeWebViewWidth { #if TARGET_OS_MACCATALYST // CATALYST: probably will want to add custom CSS for Macs. - contentWidthClass = @"NB-ipad-wide NB-ipad-pro-12-wide NB-width-768"; + contentWidthClass = @"NB-mac NB-ipad-pro-12-wide"; #else UIInterfaceOrientation orientation = self.view.window.windowScene.interfaceOrientation; @@ -2434,10 +2453,10 @@ - (void)changeWebViewWidth { } else { contentWidthClass = @"NB-iphone"; } +#endif contentWidthClass = [NSString stringWithFormat:@"%@ NB-width-%d", contentWidthClass, (int)floorf(CGRectGetWidth(webView.scrollView.bounds))]; -#endif NSString *alternateViewClass = @""; if (!self.isPhoneOrCompact) { diff --git a/clients/ios/Classes/StoryPagesObjCViewController.h b/clients/ios/Classes/StoryPagesObjCViewController.h index 307a47ac03..0fa2063146 100644 --- a/clients/ios/Classes/StoryPagesObjCViewController.h +++ b/clients/ios/Classes/StoryPagesObjCViewController.h @@ -16,7 +16,6 @@ @interface StoryPagesObjCViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; THCircularProgressView *circularProgressView; UIButton *buttonPrevious; UIButton *buttonNext; @@ -37,7 +36,6 @@ CGFloat scrollPct; } -@property (nonatomic, strong) NewsBlurAppDelegate *appDelegate; @property (nonatomic) StoryDetailViewController *currentPage; @property (nonatomic) StoryDetailViewController *nextPage; @property (nonatomic) StoryDetailViewController *previousPage; @@ -153,9 +151,14 @@ - (IBAction)openSendToDialog:(id)sender; - (IBAction)doNextUnreadStory:(id)sender; - (IBAction)doPreviousStory:(id)sender; +- (void)changeToNextPage:(id)sender; +- (void)changeToPreviousPage:(id)sender; - (IBAction)tapProgressBar:(id)sender; - (IBAction)toggleTextView:(id)sender; +- (IBAction)toggleStorySaved:(id)sender; +- (IBAction)toggleStoryUnread:(id)sender; + - (void)finishMarkAsSaved:(NSDictionary *)params; - (BOOL)failedMarkAsSaved:(NSDictionary *)params; - (void)finishMarkAsUnsaved:(NSDictionary *)params; diff --git a/clients/ios/Classes/StoryPagesObjCViewController.m b/clients/ios/Classes/StoryPagesObjCViewController.m index bd7e2a5771..772be9bb12 100644 --- a/clients/ios/Classes/StoryPagesObjCViewController.m +++ b/clients/ios/Classes/StoryPagesObjCViewController.m @@ -36,7 +36,6 @@ @interface StoryPagesObjCViewController () @implementation StoryPagesObjCViewController -@synthesize appDelegate; @synthesize currentPage, nextPage, previousPage; @synthesize circularProgressView; @synthesize separatorBarButton; @@ -76,7 +75,6 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)viewDidLoad { [super viewDidLoad]; - appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; currentPage = [[StoryDetailViewController alloc] initWithNibName:@"StoryDetailViewController" bundle:nil]; @@ -109,7 +107,11 @@ - (void)viewDidLoad { [self.scrollView setAlwaysBounceHorizontal:self.isHorizontal]; [self.scrollView setAlwaysBounceVertical:!self.isHorizontal]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (@available(iOS 17.0, *)) { + self.scrollView.allowsKeyboardScrolling = NO; + } + + if (!self.isPhone) { self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } @@ -172,13 +174,13 @@ - (void)viewDidLoad { [separatorBarButton setEnabled:NO]; separatorBarButton.isAccessibilityElement = NO; - UIImage *settingsImage = [Utilities imageNamed:@"settings" sized:30]; + UIImage *settingsImage = [Utilities imageNamed:@"settings" sized:self.isMac ? 24 : 30]; fontSettingsButton = [UIBarButtonItem barItemWithImage:settingsImage target:self action:@selector(toggleFontSize:)]; fontSettingsButton.accessibilityLabel = @"Story settings"; - UIImage *markreadImage = [UIImage imageNamed:@"original_button.png"]; + UIImage *markreadImage = [Utilities imageNamed:@"original_button.png" sized:self.isMac ? 24 : 30]; originalStoryButton = [UIBarButtonItem barItemWithImage:markreadImage target:self action:@selector(showOriginalSubview:)]; @@ -251,6 +253,11 @@ - (void)viewDidLoad { - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; +#if TARGET_OS_MACCATALYST + [self.navigationController setNavigationBarHidden:YES animated:animated]; + [self.navigationController setToolbarHidden:YES animated:animated]; +#endif + [self updateTheme]; [self updateAutoscrollButtons]; @@ -374,6 +381,10 @@ - (void)viewDidLayoutSubviews { self.scrollView.frame = CGRectMake(frame.origin.x, frame.origin.y, floor(frame.size.width), floor(frame.size.height)); } + if (self.scrollView.subviews.lastObject != self.currentPage.view) { + [self.scrollView bringSubviewToFront:self.currentPage.view]; + } + [super viewDidLayoutSubviews]; } @@ -390,7 +401,10 @@ - (void)viewWillDisappear:(BOOL)animated { previousPage.view.hidden = YES; appDelegate.detailViewController.parentNavigationController.interactivePopGestureRecognizer.enabled = YES; + +#if !TARGET_OS_MACCATALYST [appDelegate.detailViewController.parentNavigationController setNavigationBarHidden:NO animated:YES]; +#endif self.autoscrollActive = NO; } @@ -484,7 +498,7 @@ - (void)setNavigationBarHidden:(BOOL)hide { } - (void)setNavigationBarHidden:(BOOL)hide alsoTraverse:(BOOL)alsoTraverse { - if (self.navigationController == nil || self.navigationController.navigationBarHidden == hide || self.currentlyTogglingNavigationBar) { + if (appDelegate.isMac || self.navigationController == nil || self.navigationController.navigationBarHidden == hide || self.currentlyTogglingNavigationBar) { return; } @@ -662,7 +676,7 @@ - (void)reorientPages { [MBProgressHUD hideHUDForView:self.view animated:YES]; [self hideNotifier]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [currentPage realignScroll]; } } @@ -771,7 +785,7 @@ - (void)restorePage { if (pageIndex >= 0) { [self changePage:pageIndex animated:NO]; - } else if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + } else if (!self.isPhone) { // If the story can't be found, don't show anything; uncomment this to instead show the first unread story: // [self doNextUnreadStory:nil]; } else { @@ -1080,9 +1094,12 @@ - (void)changePage:(NSInteger)pageIndex animated:(BOOL)animated { } self.scrollingToPage = pageIndex; - [self.currentPage hideNoStoryMessage]; - [self.nextPage hideNoStoryMessage]; - [self.previousPage hideNoStoryMessage]; + + if (pageIndex >= 0) { + [self.currentPage hideNoStoryMessage]; + [self.nextPage hideNoStoryMessage]; + [self.previousPage hideNoStoryMessage]; + } // Check if already on the selected page if (self.isHorizontal ? offset.x == frame.origin.x : offset.y == frame.origin.y) { @@ -1225,7 +1242,11 @@ - (void)updatePageWithActiveStory:(NSInteger)location updateFeedDetail:(BOOL)upd [appDelegate.storiesCollection pushReadStory:[appDelegate.activeStory objectForKey:@"story_hash"]]; - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { +#if TARGET_OS_MACCATALYST + self.appDelegate.detailViewController.navigationItem.leftBarButtonItems = @[[[UIBarButtonItem alloc] initWithCustomView:[UIView new]]]; +#endif + + if (!self.isPhone) { if (appDelegate.detailViewController.storyTitlesOnLeft) { appDelegate.detailViewController.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects: originalStoryButton, @@ -1327,6 +1348,13 @@ - (void)setTextButton:(StoryDetailViewController *)storyViewController { fontSettingsButton.enabled = YES; originalStoryButton.enabled = YES; + +#if TARGET_OS_MACCATALYST + if (@available(macCatalyst 16.0, *)) { + fontSettingsButton.hidden = NO; + originalStoryButton.hidden = NO; + } +#endif } else { [buttonText setEnabled:NO]; [buttonText setAlpha:.4]; @@ -1335,6 +1363,13 @@ - (void)setTextButton:(StoryDetailViewController *)storyViewController { fontSettingsButton.enabled = NO; originalStoryButton.enabled = NO; + +#if TARGET_OS_MACCATALYST + if (@available(macCatalyst 16.0, *)) { + fontSettingsButton.hidden = YES; + originalStoryButton.hidden = YES; + } +#endif } [buttonSend setBackgroundImage:[[ThemeManager themeManager] themedImage:[UIImage imageNamed:@"traverse_send.png"]] @@ -1455,11 +1490,11 @@ - (IBAction)toggleTextView:(id)sender { // [self.appDelegate.feedDetailViewController changedStoryHeight:currentPage.webView.scrollView.contentSize.height]; } -- (void)toggleStorySaved:(id)sender { +- (IBAction)toggleStorySaved:(id)sender { [appDelegate.storiesCollection toggleStorySaved]; } -- (void)toggleStoryUnread:(id)sender { +- (IBAction)toggleStoryUnread:(id)sender { [appDelegate.storiesCollection toggleStoryUnread]; [appDelegate.feedDetailViewController reload]; // XXX only if successful? } @@ -1482,12 +1517,26 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { #pragma mark - #pragma mark Styles +//- (BOOL)validateToolbarItem:(NSToolbarItem *)item { +// if item.itemIdentifier == +// return !self.currentPage.view.isHidden; +//} - (IBAction)toggleFontSize:(id)sender { UINavigationController *fontSettingsNavigationController = appDelegate.fontSettingsNavigationController; [fontSettingsNavigationController popToRootViewControllerAnimated:NO]; +// [appDelegate showPopoverWithViewController:fontSettingsNavigationController contentSize:CGSizeZero sourceNavigationController:self.navigationController barButtonItem:self.fontSettingsButton sourceView:nil sourceRect:CGRectZero permittedArrowDirections:UIPopoverArrowDirectionAny]; + +#if TARGET_OS_MACCATALYST + UINavigationController *storiesNavController = appDelegate.storyPagesViewController.navigationController; + UIView *sourceView = storiesNavController.view; + CGRect sourceRect = CGRectMake(storiesNavController.view.frame.size.width - 59, 0, 20, 20); + + [appDelegate showPopoverWithViewController:fontSettingsNavigationController contentSize:CGSizeZero sourceView:sourceView sourceRect:sourceRect]; +#else [appDelegate showPopoverWithViewController:fontSettingsNavigationController contentSize:CGSizeZero barButtonItem:self.fontSettingsButton]; +#endif } - (void)setFontStyle:(NSString *)fontStyle { diff --git a/clients/ios/Classes/StoryPagesViewController.swift b/clients/ios/Classes/StoryPagesViewController.swift index c56882b24f..f6196e51fb 100644 --- a/clients/ios/Classes/StoryPagesViewController.swift +++ b/clients/ios/Classes/StoryPagesViewController.swift @@ -20,6 +20,16 @@ class StoryPagesViewController: StoryPagesObjCViewController { /// Reload the widget timeline. @objc func reloadWidget() { - WidgetCenter.shared.reloadTimelines(ofKind: "Latest") + WidgetCenter.shared.reloadAllTimelines() } + +#if targetEnvironment(macCatalyst) + @objc func validateToolbarItem(_ item: NSToolbarItem) -> Bool { + if [.storyPagesSettings, .storyPagesBrowser].contains(item.itemIdentifier) { + return self.isStoryShown + } else { + return true + } + } +#endif } diff --git a/clients/ios/Classes/StorySettings.swift b/clients/ios/Classes/StorySettings.swift new file mode 100644 index 0000000000..f318b18100 --- /dev/null +++ b/clients/ios/Classes/StorySettings.swift @@ -0,0 +1,150 @@ +// +// StorySettings.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-04. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import Foundation + +class StorySettings { + let defaults = UserDefaults.standard + + enum Content: String, RawRepresentable { + case title + case short + case medium + case long + + static let titleLimit = 6 + + static let contentLimit = 10 + + var limit: Int { + switch self { + case .title: + return 6 + case .short: + return 2 + case .medium: + return 4 + case .long: + return 6 + } + } + } + + var content: Content { + if let string = defaults.string(forKey: "story_list_preview_text_size"), let value = Content(rawValue: string) { + return value + } else { + return .short + } + } + + enum Preview: String, RawRepresentable { + case none + case smallLeft = "small_left" + case largeLeft = "large_left" + case largeRight = "large_right" + case smallRight = "small_right" + + var isLeft: Bool { + return [.smallLeft, .largeLeft].contains(self) + } + + var isSmall: Bool { + return [.smallLeft, .smallRight].contains(self) + } + } + + var preview: Preview { + if let string = defaults.string(forKey: "story_list_preview_images_size"), let value = Preview(rawValue: string) { + return value + } else { + return .smallRight + } + } + + enum FontSize: String, RawRepresentable { + case xs + case small + case medium + case large + case xl + + var offset: CGFloat { + switch self { + case .xs: + return -2 + case .small: + return -1 + case .medium: + return 0 + case .large: + return 1 + case .xl: + return 2 + } + } + } + + var fontSize: FontSize { + if let string = defaults.string(forKey: "feed_list_font_size"), let value = FontSize(rawValue: string) { + return value + } else { + return .medium + } + } + + enum Spacing: String, RawRepresentable { + case compact + case comfortable + } + + var spacing: Spacing { + if let string = defaults.string(forKey: "feed_list_spacing"), let value = Spacing(rawValue: string) { + return value + } else { + return .comfortable + } + } + + var gridColumns: Int { + guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else { + if NewsBlurAppDelegate.shared.isCompactWidth { + return 1 + } else if NewsBlurAppDelegate.shared.isPortrait || NewsBlurAppDelegate.shared.isPhone { + return 2 + } else { + return 4 + } + } + + if NewsBlurAppDelegate.shared.isPortrait, columns > 3 { + return 3 + } + + return columns + } + + var gridHeight: CGFloat { + guard let pref = UserDefaults.standard.string(forKey: "grid_height") else { + return 400 + } + + switch pref { + case "xs": + return 250 + case "short": + return 300 + case "tall": + return 500 + case "xl": + return 600 + default: + return 400 + } + } +} diff --git a/clients/ios/Classes/SwiftUIUtilities.swift b/clients/ios/Classes/SwiftUIUtilities.swift index 6ef9e39830..c191dbc954 100644 --- a/clients/ios/Classes/SwiftUIUtilities.swift +++ b/clients/ios/Classes/SwiftUIUtilities.swift @@ -38,6 +38,16 @@ extension View { } } +extension Text { + func colored(_ color: Color) -> Text { + if #available(iOS 17.0, *) { + self.foregroundStyle(color) + } else { + self.foregroundColor(color) + } + } +} + struct RoundedCorner: Shape { var radius: CGFloat = .infinity var corners: UIRectCorner = .allCorners @@ -131,3 +141,70 @@ struct OffsetObservingScrollView: View { .coordinateSpace(name: coordinateSpaceName) } } + +struct WrappingHStack: View where Model: Hashable, V: View { + typealias ViewGenerator = (Model) -> V + + var models: [Model] + var horizontalSpacing: CGFloat = 2 + var verticalSpacing: CGFloat = 0 + var viewGenerator: ViewGenerator + + @State private var totalHeight + = CGFloat.zero // << variant for ScrollView/List + // = CGFloat.infinity // << variant for VStack + + var body: some View { + VStack { + GeometryReader { geometry in + self.generateContent(in: geometry) + } + } + .frame(height: totalHeight)// << variant for ScrollView/List + //.frame(maxHeight: totalHeight) // << variant for VStack + } + + private func generateContent(in geometry: GeometryProxy) -> some View { + var width = CGFloat.zero + var height = CGFloat.zero + + return ZStack(alignment: .topLeading) { + ForEach(self.models, id: \.self) { models in + viewGenerator(models) + .padding(.horizontal, horizontalSpacing) + .padding(.vertical, verticalSpacing) + .alignmentGuide(.leading, computeValue: { dimension in + if (abs(width - dimension.width) > geometry.size.width) + { + width = 0 + height -= dimension.height + } + let result = width + if models == self.models.last! { + width = 0 //last item + } else { + width -= dimension.width + } + return result + }) + .alignmentGuide(.top, computeValue: {dimension in + let result = height + if models == self.models.last! { + height = 0 // last item + } + return result + }) + } + }.background(viewHeightReader($totalHeight)) + } + + private func viewHeightReader(_ binding: Binding) -> some View { + return GeometryReader { geometry -> Color in + let rect = geometry.frame(in: .local) + DispatchQueue.main.async { + binding.wrappedValue = rect.size.height + } + return .clear + } + } +} diff --git a/clients/ios/Classes/ThemeManager.h b/clients/ios/Classes/ThemeManager.h index 907585bc5a..801b64dd98 100644 --- a/clients/ios/Classes/ThemeManager.h +++ b/clients/ios/Classes/ThemeManager.h @@ -31,6 +31,8 @@ extern NSString * const ThemeStyleDark; @property (nonatomic, readonly) NSString *themeDisplayName; @property (nonatomic, readonly) NSString *themeCSSSuffix; @property (nonatomic, readonly) BOOL isDarkTheme; +@property (nonatomic, readonly) BOOL isSystemDark; +@property (nonatomic, readonly) BOOL isLikeSystem; + (instancetype)themeManager; diff --git a/clients/ios/Classes/ThemeManager.m b/clients/ios/Classes/ThemeManager.m index fd97518d0d..a890f863c4 100644 --- a/clients/ios/Classes/ThemeManager.m +++ b/clients/ios/Classes/ThemeManager.m @@ -136,6 +136,14 @@ - (BOOL)isDarkTheme { return [theme isEqualToString:ThemeStyleDark] || [theme isEqualToString:ThemeStyleMedium]; } +- (BOOL)isSystemDark { + return self.appDelegate.window.windowScene.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; +} + +- (BOOL)isLikeSystem { + return self.isDarkTheme == self.isSystemDark; +} + - (BOOL)isValidTheme:(NSString *)theme { return [theme isEqualToString:ThemeStyleLight] || [theme isEqualToString:ThemeStyleSepia] || [theme isEqualToString:ThemeStyleMedium] || [theme isEqualToString:ThemeStyleDark]; } @@ -266,7 +274,9 @@ - (void)updateTextAttributesForSegmentedControl:(UISegmentedControl *)segmentedC - (void)updateSegmentedControl:(UISegmentedControl *)segmentedControl { segmentedControl.tintColor = UIColorFromRGB(0x8F918B); +#if !TARGET_OS_MACCATALYST segmentedControl.backgroundColor = UIColorFromLightDarkRGB(0xe7e6e7, 0x303030); +#endif segmentedControl.selectedSegmentTintColor = UIColorFromLightDarkRGB(0xffffff, 0x6f6f75); [self updateTextAttributesForSegmentedControl:segmentedControl forState:UIControlStateNormal foregroundColor:UIColorFromLightDarkRGB(0x909090, 0xaaaaaa)]; @@ -275,7 +285,9 @@ - (void)updateSegmentedControl:(UISegmentedControl *)segmentedControl { - (void)updateThemeSegmentedControl:(UISegmentedControl *)segmentedControl { segmentedControl.tintColor = [UIColor clearColor]; +#if !TARGET_OS_MACCATALYST segmentedControl.backgroundColor = [UIColor clearColor]; +#endif segmentedControl.selectedSegmentTintColor = [UIColor clearColor]; } @@ -440,9 +452,7 @@ - (void)handleThemeGesture:(UIPanGestureRecognizer *)recognizer { } - (void)updateForSystemAppearance { - BOOL isDark = self.appDelegate.window.windowScene.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; - - [self systemAppearanceDidChange:isDark]; + [self systemAppearanceDidChange:self.isSystemDark]; } - (void)systemAppearanceDidChange:(BOOL)isDark { diff --git a/clients/ios/Classes/ToolbarDelegate.swift b/clients/ios/Classes/ToolbarDelegate.swift new file mode 100644 index 0000000000..b720abcba1 --- /dev/null +++ b/clients/ios/Classes/ToolbarDelegate.swift @@ -0,0 +1,97 @@ +// +// ToolbarDelegate.swift +// NewsBlur +// +// Created by David Sinclair on 2024-01-05. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import UIKit + +#if targetEnvironment(macCatalyst) +class ToolbarDelegate: NSObject { +} + +extension NSToolbarItem.Identifier { + static let reloadFeeds = NSToolbarItem.Identifier("com.newsblur.reloadFeeds") + static let feedDetailUnread = NSToolbarItem.Identifier("com.newsblur.feedDetailUnread") + static let feedDetailSettings = NSToolbarItem.Identifier("com.newsblur.feedDetailSettings") + static let storyPagesSettings = NSToolbarItem.Identifier("com.newsblur.storyPagesSettings") + static let storyPagesBrowser = NSToolbarItem.Identifier("com.newsblur.storyPagesBrowser") +} + +extension ToolbarDelegate: NSToolbarDelegate { + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + let identifiers: [NSToolbarItem.Identifier] = [ + .toggleSidebar, + .space, + .reloadFeeds, + .space, + .feedDetailUnread, + .feedDetailSettings, + .flexibleSpace, + .storyPagesSettings, + .storyPagesBrowser + ] + return identifiers + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return toolbarDefaultItemIdentifiers(toolbar) + } + + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + switch itemIdentifier { + case .reloadFeeds: + return makeToolbarItem(itemIdentifier, + image: UIImage(systemName: "arrow.clockwise"), + label: "Reload Sites", + action: #selector(BaseViewController.reloadFeeds(_:))) + + case .feedDetailUnread: + return makeToolbarItem(itemIdentifier, + image: Utilities.imageNamed("mark-read", sized: 24), + label: "Mark as Read", + action: #selector(BaseViewController.openMarkReadMenu(_:))) + + case .feedDetailSettings: + return makeToolbarItem(itemIdentifier, + image: Utilities.imageNamed("settings", sized: 24), + label: "Site Settings", + action: #selector(BaseViewController.openSettingsMenu(_:))) + + case .storyPagesSettings: + return makeToolbarItem(itemIdentifier, + image: Utilities.imageNamed("settings", sized: 24), + label: "Story Settings", + action: #selector(StoryPagesViewController.toggleFontSize(_:))) + + case .storyPagesBrowser: + return makeToolbarItem(itemIdentifier, + image: Utilities.imageNamed("original_button.png", sized: 24), + label: "Show Original Story", + action: #selector(StoryPagesViewController.showOriginalSubview(_:))) + + default: + return nil + } + } + + func makeToolbarItem(_ identifier: NSToolbarItem.Identifier, + image: UIImage?, + label: String, + action: Selector, + target: AnyObject? = nil) -> NSToolbarItem { + let item = NSToolbarItem(itemIdentifier: identifier) + + item.image = image + item.label = label + item.action = action + item.target = target + + return item + } +} +#endif diff --git a/clients/ios/Classes/TrainerCapsule.swift b/clients/ios/Classes/TrainerCapsule.swift new file mode 100644 index 0000000000..e8f3eb02e9 --- /dev/null +++ b/clients/ios/Classes/TrainerCapsule.swift @@ -0,0 +1,67 @@ +// +// TrainerCapsule.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-02. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import SwiftUI + +struct TrainerCapsule: View { + var score: Feed.Score + + var header: String + + var image: UIImage? + + var value: String + + var count: Int = 0 + + var body: some View { + HStack { + HStack { + Image(systemName: score.imageName) + .foregroundColor(.white) + + content + } + .padding([.top, .bottom], 5) + .padding([.leading, .trailing], 10) + .background(score == .like ? Color(red: 0, green: 0.5, blue: 0.0) : score == .dislike ? Color.red : Color(white: ThemeManager.shared.isSystemDark ? 0.35 : 0.6)) + .clipShape(Capsule()) + + if count > 0 { + Text("x \(count)") + .colored(.gray) + .padding([.trailing], 10) + } + } + } + + var content: Text { + Text("\(Text("\(header):").colored(.init(white: 0.85))) \(imageText)\(value)") + .colored(.white) + } + + var imageText: Text { + if let image { + Text(Image(uiImage: image)).baselineOffset(-3) + Text(" ") + } else { + Text("") + } + } +} + +#Preview { + TrainerCapsule(score: .none, header: "Tag", value: "None Example") +} + +#Preview { + TrainerCapsule(score: .like, header: "Tag", value: "Liked Example") +} + +#Preview { + TrainerCapsule(score: .dislike, header: "Tag", value: "Disliked Example") +} diff --git a/clients/ios/Classes/TrainerView.swift b/clients/ios/Classes/TrainerView.swift new file mode 100644 index 0000000000..5615e0a6ac --- /dev/null +++ b/clients/ios/Classes/TrainerView.swift @@ -0,0 +1,218 @@ +// +// TrainerView.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-02. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import SwiftUI + +/// A protocol of interaction between the trainer view and the enclosing view controller. +protocol TrainerInteraction { + var isStoryTrainer: Bool { get set } +} + +struct TrainerView: View { + var interaction: TrainerInteraction + + @ObservedObject var cache: StoryCache + + let columns = [GridItem(.adaptive(minimum: 50))] + + var body: some View { + VStack(alignment: .leading) { + Text("What do you 👠\(Text("like").colored(.green)) and 👎 \(Text("dislike").colored(.red)) about this \(feedOrStoryLowercase)?") + .font(font(named: "WhitneySSm-Medium", size: 16)) + .padding() + + List { + Section(content: { + VStack(alignment: .leading) { + if interaction.isStoryTrainer { + Text("Choose one or more words from the title:") + .font(font(named: "WhitneySSm-Medium", size: 12)) + .padding([.top], 10) + + WrappingHStack(models: titleWords, horizontalSpacing: 1) { word in + Button(action: { + if addingTitle.isEmpty { + addingTitle = word + } else { + addingTitle.append(" \(word)") + } + }, label: { + TrainerWord(word: word) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding([.top, .bottom], 5) + } + + if !addingTitle.isEmpty { + HStack { + Button(action: { + cache.appDelegate.toggleTitleClassifier(addingTitle, feedId: feed?.id, score: 0) + addingTitle = "" + }, label: { + TrainerCapsule(score: .none, header: "Title", value: addingTitle) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding([.top, .bottom], 5) + + Button { + addingTitle = "" + } label: { + Image(systemName: "xmark.circle.fill") + .imageScale(.large) + .foregroundColor(.gray) + } + } + } + } + + WrappingHStack(models: titles) { title in + Button(action: { + cache.appDelegate.toggleTitleClassifier(title.name, feedId: feed?.id, score: 0) + }, label: { + TrainerCapsule(score: title.score, header: "Title", value: title.name, count: title.count) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding([.top, .bottom], 5) + } + } + }, header: { + header(story: "Story Title", feed: "Titles & Phrases") + }) + + Section(content: { + WrappingHStack(models: authors) { author in + Button(action: { + cache.appDelegate.toggleAuthorClassifier(author.name, feedId: feed?.id) + }, label: { + TrainerCapsule(score: author.score, header: "Author", value: author.name, count: author.count) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding([.top, .bottom], 5) + } + }, header: { + header(story: "Story Author", feed: "Authors") + }) + + Section(content: { + WrappingHStack(models: tags) { tag in + Button(action: { + cache.appDelegate.toggleTagClassifier(tag.name, feedId: feed?.id) + }, label: { + TrainerCapsule(score: tag.score, header: "Tag", value: tag.name, count: tag.count) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding([.top, .bottom], 5) + } + }, header: { + header(story: "Story Categories & Tags", feed: "Categories & Tags") + }) + + Section(content: { + HStack { + if let feed = feed { + Button(action: { + cache.appDelegate.toggleFeedClassifier(feed.id) + }, label: { + TrainerCapsule(score: score(key: "feeds", value: feed.id), header: "Site", image: feed.image, value: feed.name) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding([.top, .bottom], 5) + } + } + }, header: { + header(feed: "Everything by This Publisher") + }) + } + .font(font(named: "WhitneySSm-Medium", size: 12)) + } + .onAppear { + addingTitle = "" + } + } + + func font(named: String, size: CGFloat) -> Font { + return Font.custom(named, size: size + cache.settings.fontSize.offset, relativeTo: .caption) + } + + func reload() { + cache.reload() + addingTitle = "" + } + + var feedOrStoryLowercase: String { + return interaction.isStoryTrainer ? "story" : "site" + } + + @ViewBuilder + func header(story: String? = nil, feed: String) -> some View { + if let story { + Text(interaction.isStoryTrainer ? story : feed) + .font(font(named: "WhitneySSm-Medium", size: 16)) + } else { + Text(feed) + .font(font(named: "WhitneySSm-Medium", size: 16)) + } + } + + func score(key: String, value: String) -> Feed.Score { + guard let classifiers = feed?.classifiers(for: key), + let score = classifiers[value] as? Int else { + return .none + } + + if score > 0 { + return .like + } else if score < 0 { + return .dislike + } else { + return .none + } + } + + var titleWords: [String] { + if interaction.isStoryTrainer, let story = cache.selected { + return story.title.components(separatedBy: .whitespaces) + } else { + return [] + } + } + + @State private var addingTitle = "" + + var feed: Feed? { + return cache.currentFeed ?? cache.selected?.feed + } + + var titles: [Feed.Training] { + if interaction.isStoryTrainer { + return cache.selected?.titles ?? [] + } else { + return feed?.titles ?? [] + } + } + + var authors: [Feed.Training] { + if interaction.isStoryTrainer { + return cache.selected?.authors ?? [] + } else { + return feed?.authors ?? [] + } + } + + var tags: [Feed.Training] { + if interaction.isStoryTrainer { + return cache.selected?.tags ?? [] + } else { + return feed?.tags ?? [] + } + } +} + +//#Preview { +// TrainerViewController() +//} diff --git a/clients/ios/Classes/TrainerViewController.h b/clients/ios/Classes/TrainerViewController.h index 75cd5eb59c..7e5b425897 100644 --- a/clients/ios/Classes/TrainerViewController.h +++ b/clients/ios/Classes/TrainerViewController.h @@ -6,6 +6,10 @@ // Copyright (c) 2012 NewsBlur. All rights reserved. // + +#warning This code is obsolete, and will be removed once the SwiftUI implementation is complete. + + #import #import "BaseViewController.h" #import "NewsBlurAppDelegate.h" @@ -21,9 +25,7 @@ @end -@interface TrainerViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; - +@interface OldTrainerViewController : BaseViewController { IBOutlet UIBarButtonItem * closeButton; TrainerWebView *webView; IBOutlet UINavigationBar *navBar; @@ -33,7 +35,6 @@ BOOL storyTrainer; } -@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate; @property (nonatomic) IBOutlet UIBarButtonItem *closeButton; @property (nonatomic) IBOutlet TrainerWebView *webView; @property (nonatomic) IBOutlet UINavigationBar *navBar; diff --git a/clients/ios/Classes/TrainerViewController.m b/clients/ios/Classes/TrainerViewController.m index ce7c7b9d5c..819a865722 100644 --- a/clients/ios/Classes/TrainerViewController.m +++ b/clients/ios/Classes/TrainerViewController.m @@ -6,18 +6,21 @@ // Copyright (c) 2012 NewsBlur. All rights reserved. // + +#warning This code is obsolete, and will be removed once the SwiftUI implementation is complete. + + #import "TrainerViewController.h" #import "StringHelper.h" #import "Utilities.h" #import "AFNetworking.h" #import "StoriesCollection.h" -@implementation TrainerViewController +@implementation OldTrainerViewController @synthesize closeButton; @synthesize webView; @synthesize navBar; -@synthesize appDelegate; @synthesize feedTrainer; @synthesize storyTrainer; @synthesize feedLoaded; @@ -35,8 +38,6 @@ - (void)viewDidLoad { [super viewDidLoad]; - self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - UIBarButtonItem *done = [[UIBarButtonItem alloc] initWithTitle:@"Done Training" style:UIBarButtonItemStyleDone @@ -99,7 +100,7 @@ - (void)viewWillAppear:(BOOL)animated { [self informError:@"Could not load trainer"]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^() { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!self.isPhone) { [self.appDelegate hidePopover]; } else { [self.appDelegate.feedsNavigationController dismissViewControllerAnimated:YES completion:nil]; @@ -162,6 +163,9 @@ - (NSString *)makeTrainerHTML { int contentWidth = self.view.frame.size.width; NSString *contentWidthClass; +#if TARGET_OS_MACCATALYST + contentWidthClass = @"NB-mac"; +#else if (contentWidth > 700) { contentWidthClass = @"NB-ipad-wide"; } else if (contentWidth > 480) { @@ -169,6 +173,7 @@ - (NSString *)makeTrainerHTML { } else { contentWidthClass = @"NB-iphone"; } +#endif // set up layout values based on iPad/iPhone NSString *headerString = [NSString stringWithFormat:@ @@ -542,7 +547,7 @@ - (NSString *)makeClassifier:(NSString *)classifierName withType:(NSString *)cla - (IBAction)doCloseDialog:(id)sender { [appDelegate hidePopover]; - [appDelegate.trainerViewController dismissViewControllerAnimated:YES completion:nil]; +// [appDelegate.trainerViewController dismissViewControllerAnimated:YES completion:nil]; } - (void)changeTitle:(id)sender score:(int)score { @@ -603,12 +608,12 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { - (void)focusTitle:(id)sender { NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - [appDelegate.trainerViewController changeTitle:sender score:1]; +// [appDelegate.trainerViewController changeTitle:sender score:1]; } - (void)hideTitle:(id)sender { NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate]; - [appDelegate.trainerViewController changeTitle:sender score:-1]; +// [appDelegate.trainerViewController changeTitle:sender score:-1]; } // Work around iOS 9 issue where menu doesn't appear the first time diff --git a/clients/ios/Classes/TrainerViewController.swift b/clients/ios/Classes/TrainerViewController.swift new file mode 100644 index 0000000000..df551e099b --- /dev/null +++ b/clients/ios/Classes/TrainerViewController.swift @@ -0,0 +1,58 @@ +// +// TrainerViewController.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-01. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import SwiftUI + +@objc class TrainerViewController: BaseViewController { + @objc var isStoryTrainer = false + + @objc var isFeedLoaded = false + + lazy var hostingController = makeHostingController() + + var trainerView: TrainerView { + return hostingController.rootView + } + + var storyCache: StoryCache { + return appDelegate.feedDetailViewController.storyCache + } + + private func makeHostingController() -> UIHostingController { + let trainerView = TrainerView(interaction: self, cache: storyCache) + let trainerController = UIHostingController(rootView: trainerView) + trainerController.view.translatesAutoresizingMaskIntoConstraints = false + + return trainerController + } + + override func viewDidLoad() { + super.viewDidLoad() + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + +// changedLayout() + } + + @objc func reload() { + trainerView.reload() + } +} + +extension TrainerViewController: TrainerInteraction { + //TODO: 🚧 +} diff --git a/clients/ios/Classes/TrainerWord.swift b/clients/ios/Classes/TrainerWord.swift new file mode 100644 index 0000000000..b5309a0e78 --- /dev/null +++ b/clients/ios/Classes/TrainerWord.swift @@ -0,0 +1,28 @@ +// +// TrainerWord.swift +// NewsBlur +// +// Created by David Sinclair on 2024-04-03. +// Copyright © 2024 NewsBlur. All rights reserved. +// + +import SwiftUI + +struct TrainerWord: View { + var word: String + + var body: some View { + HStack { + Text(word) + .colored(Color(white: ThemeManager.shared.isSystemDark ? 0.8 : 0.1)) + .padding([.top, .bottom], 1) + .padding([.leading, .trailing], 1) + .background(Color(white: ThemeManager.shared.isSystemDark ? 0.35 : 0.95)) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } + } +} + +#Preview { + TrainerWord(word: "Example") +} diff --git a/clients/ios/Classes/UnreadCountView.m b/clients/ios/Classes/UnreadCountView.m index 5d6c9c6946..abcab54181 100644 --- a/clients/ios/Classes/UnreadCountView.m +++ b/clients/ios/Classes/UnreadCountView.m @@ -54,7 +54,7 @@ - (void)drawInRect:(CGRect)r ps:(NSInteger)ps nt:(NSInteger)nt listType:(NBFeedL CGRect rr; if (listType == NBFeedListSocial) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { rr = CGRectMake(rect.size.width + rect.origin.x - psOffset, CGRectGetMidY(r)-COUNT_HEIGHT/2, psWidth, COUNT_HEIGHT); } else { rr = CGRectMake(rect.size.width + rect.origin.x - psOffset, CGRectGetMidY(r)-COUNT_HEIGHT/2, psWidth, COUNT_HEIGHT); @@ -98,7 +98,7 @@ - (void)drawInRect:(CGRect)r ps:(NSInteger)ps nt:(NSInteger)nt listType:(NBFeedL if (nt > 0 && appDelegate.selectedIntelligence <= 0) { CGRect rr; if (listType == NBFeedListSocial) { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + if (!appDelegate.isPhone) { rr = CGRectMake(rect.size.width + rect.origin.x - psWidth - psPadding - ntOffset, CGRectGetMidY(r)-COUNT_HEIGHT/2, ntWidth, COUNT_HEIGHT); } else { rr = CGRectMake(rect.size.width + rect.origin.x - psWidth - psPadding - ntOffset, CGRectGetMidY(r)-COUNT_HEIGHT/2, ntWidth, COUNT_HEIGHT); diff --git a/clients/ios/Classes/UserProfileViewController.h b/clients/ios/Classes/UserProfileViewController.h index b8240b36e0..d54f3c3094 100644 --- a/clients/ios/Classes/UserProfileViewController.h +++ b/clients/ios/Classes/UserProfileViewController.h @@ -10,13 +10,10 @@ #import "NewsBlurAppDelegate.h" #import "NewsBlur-Swift.h" -@class NewsBlurAppDelegate; @class ProfileBadge; @interface UserProfileViewController : BaseViewController { - NewsBlurAppDelegate *appDelegate; - UILabel *followingCount; UILabel *followersCount; ProfileBadge *profileBadge; @@ -26,7 +23,6 @@ NSDictionary *userProfile; } -@property (nonatomic) NewsBlurAppDelegate *appDelegate; @property (nonatomic) ProfileBadge *profileBadge; @property (nonatomic) UITableView *profileTable; @property (nonatomic) NSArray *activitiesArray; diff --git a/clients/ios/Classes/UserProfileViewController.m b/clients/ios/Classes/UserProfileViewController.m index 2f56db4902..bbcb818418 100644 --- a/clients/ios/Classes/UserProfileViewController.m +++ b/clients/ios/Classes/UserProfileViewController.m @@ -17,7 +17,6 @@ @implementation UserProfileViewController -@synthesize appDelegate; @synthesize profileBadge; @synthesize profileTable; @synthesize activitiesArray; @@ -40,9 +39,7 @@ - (void)dealloc { - (void)viewDidLoad { [super viewDidLoad]; - // Do any additional setup after loading the view from its nib. - self.appDelegate = (NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate]; - + UITableView *profiles = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) style:UITableViewStyleGrouped]; self.profileTable = profiles; self.profileTable.dataSource = self; @@ -89,7 +86,6 @@ - (void)getUserProfile { // self.view.frame = self.view.bounds; self.preferredContentSize = CGSizeMake(320, 454); - self.appDelegate = (NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate]; [MBProgressHUD hideHUDForView:self.view animated:YES]; MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; HUD.labelText = @"Profiling..."; diff --git a/clients/ios/NewsBlur.xcodeproj/project.pbxproj b/clients/ios/NewsBlur.xcodeproj/project.pbxproj index 6ddff2abe2..5dc4adc86c 100755 --- a/clients/ios/NewsBlur.xcodeproj/project.pbxproj +++ b/clients/ios/NewsBlur.xcodeproj/project.pbxproj @@ -17,7 +17,15 @@ 170E3CD124F8A664009CE819 /* SplitViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170E3CD024F8A664009CE819 /* SplitViewDelegate.swift */; }; 170E3CD324F8A89B009CE819 /* HorizontalPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170E3CD224F8A89B009CE819 /* HorizontalPageViewController.swift */; }; 170E3CD724F8AB0D009CE819 /* FeedDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170E3CD624F8AB0D009CE819 /* FeedDetailViewController.swift */; }; + 17150E1E2B05775A004D5309 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17150E1D2B05775A004D5309 /* SceneDelegate.swift */; }; + 17150E1F2B05775A004D5309 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17150E1D2B05775A004D5309 /* SceneDelegate.swift */; }; 1715D02B2166B3F900227731 /* PremiumManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1715D02A2166B3F900227731 /* PremiumManager.m */; }; + 17179E292BD6F86C006B18D5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 17179E282BD6F86C006B18D5 /* PrivacyInfo.xcprivacy */; }; + 17179E2A2BD6F86D006B18D5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 17179E282BD6F86C006B18D5 /* PrivacyInfo.xcprivacy */; }; + 171904B52BBC8D4E004CCC96 /* TrainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171904B42BBC8D4E004CCC96 /* TrainerView.swift */; }; + 171904B62BBC8D4E004CCC96 /* TrainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171904B42BBC8D4E004CCC96 /* TrainerView.swift */; }; + 171904B82BBCA712004CCC96 /* TrainerCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171904B72BBCA712004CCC96 /* TrainerCapsule.swift */; }; + 171904B92BBCA712004CCC96 /* TrainerCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171904B72BBCA712004CCC96 /* TrainerCapsule.swift */; }; 171B6FFD25C4C7C8008638A9 /* StoryPagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171B6FFC25C4C7C8008638A9 /* StoryPagesViewController.swift */; }; 1721C9D12497F91A00B0EDC4 /* mute_gray.png in Resources */ = {isa = PBXBuildFile; fileRef = 1721C9D02497F91900B0EDC4 /* mute_gray.png */; }; 1723388B26BE43EB00610784 /* WidgetLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1723388A26BE43EB00610784 /* WidgetLoader.swift */; }; @@ -503,7 +511,7 @@ 1757920A2930605500490924 /* g_icn_textview_black.png in Resources */ = {isa = PBXBuildFile; fileRef = FF83FF0F1FB54691008DAC0F /* g_icn_textview_black.png */; }; 1757920B2930605500490924 /* logo_58.png in Resources */ = {isa = PBXBuildFile; fileRef = FF322234185BC1AA004078AA /* logo_58.png */; }; 1757920C2930605500490924 /* logo_144.png in Resources */ = {isa = PBXBuildFile; fileRef = FFC486A619CA40B700F4758F /* logo_144.png */; }; - 1757920D2930605500490924 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4C717750BDD00BF59D3 /* Settings.bundle */; }; + 1757920D2930605500490924 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4C717750BDD00BF59D3 /* Settings.bundle */; platformFilter = ios; }; 1757920E2930605500490924 /* menu_icn_preferences.png in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4C917750D2C00BF59D3 /* menu_icn_preferences.png */; }; 1757920F2930605500490924 /* menu_icn_preferences@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4CA17750D2C00BF59D3 /* menu_icn_preferences@2x.png */; }; 175792102930605500490924 /* checkmark.png in Resources */ = {isa = PBXBuildFile; fileRef = FF855B541794A53A0098D48A /* checkmark.png */; }; @@ -708,6 +716,8 @@ 175792DA2930605500490924 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78095E3E128EF35400230C8E /* CFNetwork.framework */; }; 175792E72930611C00490924 /* LaunchScreenDev.xib in Resources */ = {isa = PBXBuildFile; fileRef = 175792E62930611B00490924 /* LaunchScreenDev.xib */; }; 175792E92930617600490924 /* logo_newsblur_512-dev.png in Resources */ = {isa = PBXBuildFile; fileRef = 175792E82930617600490924 /* logo_newsblur_512-dev.png */; }; + 175DC6AF2BBB87D200B3708F /* TrainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175DC6AE2BBB87D200B3708F /* TrainerViewController.swift */; }; + 175DC6B02BBB87D200B3708F /* TrainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175DC6AE2BBB87D200B3708F /* TrainerViewController.swift */; }; 175FAC4C23AB34EB002AC38C /* menu_icn_widget.png in Resources */ = {isa = PBXBuildFile; fileRef = 175FAC4A23AB34EB002AC38C /* menu_icn_widget.png */; }; 175FAC4D23AB34EB002AC38C /* menu_icn_widget@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 175FAC4B23AB34EB002AC38C /* menu_icn_widget@2x.png */; }; 176129601C630AEB00702FE4 /* mute_feed_off.png in Resources */ = {isa = PBXBuildFile; fileRef = 1761295C1C630AEB00702FE4 /* mute_feed_off.png */; }; @@ -716,6 +726,20 @@ 176129631C630AEB00702FE4 /* mute_feed_on@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1761295F1C630AEB00702FE4 /* mute_feed_on@2x.png */; }; 1763E2A123B1BCC900BA080C /* WidgetFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1763E2A023B1BCC900BA080C /* WidgetFeed.swift */; }; 1763E2A323B1CEB600BA080C /* WidgetBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1763E2A223B1CEB600BA080C /* WidgetBarView.swift */; }; + 17654E332B02C08700F61B2B /* WidgetLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1723388A26BE43EB00610784 /* WidgetLoader.swift */; }; + 17654E342B02C08700F61B2B /* WidgetCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1723389026BF7CFE00610784 /* WidgetCache.swift */; }; + 17654E352B02C08700F61B2B /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173CB31326BCE94700BA872A /* WidgetExtension.swift */; }; + 17654E362B02C08700F61B2B /* WidgetStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1723388C26BE440400610784 /* WidgetStory.swift */; }; + 17654E372B02C08700F61B2B /* WidgetBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1723389326C3775A00610784 /* WidgetBarView.swift */; }; + 17654E382B02C08700F61B2B /* WidgetDebugTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17997C5727A8FDD100483E69 /* WidgetDebugTimer.swift */; }; + 17654E392B02C08700F61B2B /* WidgetFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1723388D26BE440400610784 /* WidgetFeed.swift */; }; + 17654E3A2B02C08700F61B2B /* WidgetStoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1791C21426C4C7BC00D815AA /* WidgetStoryView.swift */; }; + 17654E3C2B02C08700F61B2B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 173CB31026BCE94700BA872A /* SwiftUI.framework */; }; + 17654E3D2B02C08700F61B2B /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 173CB30E26BCE94700BA872A /* WidgetKit.framework */; }; + 17654E3F2B02C08700F61B2B /* WhitneySSm-Medium-Bas.otf in Resources */ = {isa = PBXBuildFile; fileRef = FF3A3E051BFBBAC600ADC01A /* WhitneySSm-Medium-Bas.otf */; }; + 17654E402B02C08700F61B2B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173CB31526BCE94A00BA872A /* Assets.xcassets */; }; + 17654E412B02C08700F61B2B /* WhitneySSm-Book-Bas.otf in Resources */ = {isa = PBXBuildFile; fileRef = FF3A3E011BFBBAC600ADC01A /* WhitneySSm-Book-Bas.otf */; }; + 17654E472B02C0A700F61B2B /* NewsBlur Alpha Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 17654E452B02C08700F61B2B /* NewsBlur Alpha Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 176A5C7A24F8BD1B009E8DF9 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176A5C7924F8BD1B009E8DF9 /* DetailViewController.swift */; }; 17731A9D23DFAD3D00759A7D /* ImportExportPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17731A9C23DFAD3D00759A7D /* ImportExportPreferences.swift */; }; 177551D5238E228A00E27818 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 177551D4238E228A00E27818 /* NotificationCenter.framework */; platformFilter = ios; }; @@ -737,7 +761,13 @@ 1788939D249332E6004CBA4E /* g_icn_search.png in Resources */ = {isa = PBXBuildFile; fileRef = 1788939C249332E6004CBA4E /* g_icn_search.png */; }; 1791C21526C4C7BC00D815AA /* WidgetStoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1791C21426C4C7BC00D815AA /* WidgetStoryView.swift */; }; 17997C5827A8FDD100483E69 /* WidgetDebugTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17997C5727A8FDD100483E69 /* WidgetDebugTimer.swift */; }; + 179A88022B48E64A00916CF4 /* ToolbarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179A88012B48E64900916CF4 /* ToolbarDelegate.swift */; }; + 179A88032B48E64A00916CF4 /* ToolbarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179A88012B48E64900916CF4 /* ToolbarDelegate.swift */; }; 179DD9CF23DFDD51007BFD21 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 179DD9CE23DFDD51007BFD21 /* CloudKit.framework */; }; + 17A0518A2C095B20000994E9 /* AuxSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A051892C095B20000994E9 /* AuxSceneDelegate.swift */; }; + 17A0518B2C095B20000994E9 /* AuxSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A051892C095B20000994E9 /* AuxSceneDelegate.swift */; }; + 17A0518D2C095E78000994E9 /* AuxInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17A0518C2C095E78000994E9 /* AuxInterface.storyboard */; }; + 17A0518E2C095E78000994E9 /* AuxInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17A0518C2C095E78000994E9 /* AuxInterface.storyboard */; }; 17A396D924F86A8F0023C9E2 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17A396D824F86A8F0023C9E2 /* MainInterface.storyboard */; }; 17A92A3C289B7C6B00AB0A78 /* saved-stories@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17A92A3B289B7C6B00AB0A78 /* saved-stories@2x.png */; }; 17AACFE122279A3C00DE6EA4 /* autoscroll_resume@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17AACFD722279A3900DE6EA4 /* autoscroll_resume@2x.png */; }; @@ -754,6 +784,14 @@ 17B14BDD23E24B4E00CF8D2C /* menu_icn_statistics@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17B14BDB23E24B4E00CF8D2C /* menu_icn_statistics@2x.png */; }; 17B33D1827D97282009108AD /* g_icn_folder_widget.png in Resources */ = {isa = PBXBuildFile; fileRef = 17B33D1627D97281009108AD /* g_icn_folder_widget.png */; }; 17B33D1927D97282009108AD /* g_icn_folder_widget@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17B33D1727D97282009108AD /* g_icn_folder_widget@2x.png */; }; + 17BC56A72BBE4A5600A30C41 /* TrainerWord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56A62BBE4A5600A30C41 /* TrainerWord.swift */; }; + 17BC56A82BBE4A5600A30C41 /* TrainerWord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56A62BBE4A5600A30C41 /* TrainerWord.swift */; }; + 17BC56AA2BBF6BC000A30C41 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56A92BBF6BC000A30C41 /* Feed.swift */; }; + 17BC56AB2BBF6BC000A30C41 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56A92BBF6BC000A30C41 /* Feed.swift */; }; + 17BC56AD2BBF6C0000A30C41 /* StoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56AC2BBF6C0000A30C41 /* StoryCache.swift */; }; + 17BC56AE2BBF6C0000A30C41 /* StoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56AC2BBF6C0000A30C41 /* StoryCache.swift */; }; + 17BC56B02BBF6C2200A30C41 /* StorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56AF2BBF6C2200A30C41 /* StorySettings.swift */; }; + 17BC56B12BBF6C2200A30C41 /* StorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BC56AF2BBF6C2200A30C41 /* StorySettings.swift */; }; 17BD3BA52271102500F615EC /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17BD3BA42271102500F615EC /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 17BD3BA72271122800F615EC /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17BD3BA62271122800F615EC /* CoreSpotlight.framework */; }; 17BD3BA92271125400F615EC /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17BD3BA82271125400F615EC /* CoreServices.framework */; }; @@ -830,6 +868,10 @@ 17EB505D1BE4411E0021358B /* choose_font@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17EB505B1BE4411E0021358B /* choose_font@2x.png */; }; 17EB50601BE46A900021358B /* FontListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 17EB505F1BE46A900021358B /* FontListViewController.m */; }; 17EB50621BE46BB00021358B /* FontListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 17EB50611BE46BB00021358B /* FontListViewController.xib */; }; + 17EE11C82B27FA0C00E7C0CC /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 17EE11C72B27FA0C00E7C0CC /* Settings.bundle */; platformFilter = maccatalyst; }; + 17EE11C92B27FA0C00E7C0CC /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 17EE11C72B27FA0C00E7C0CC /* Settings.bundle */; platformFilter = maccatalyst; }; + 17EE11CE2B28011D00E7C0CC /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17EE11CD2B28011D00E7C0CC /* Credits.rtf */; }; + 17EE11CF2B28011D00E7C0CC /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 17EE11CD2B28011D00E7C0CC /* Credits.rtf */; }; 17F156711BDABBF60092EBFD /* safari_shadow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 17F156701BDABBF60092EBFD /* safari_shadow@2x.png */; }; 17F363F2238E417300D5379D /* WidgetExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F363EF238E417300D5379D /* WidgetExtensionViewController.swift */; }; 17F39EAA264754CD004B46D1 /* image_preview_large_left.png in Resources */ = {isa = PBXBuildFile; fileRef = 17F39EA6264754CC004B46D1 /* image_preview_large_left.png */; }; @@ -1320,7 +1362,7 @@ FFEA5AEC19D340BC00ED87A0 /* logo_newsblur_512.png in Resources */ = {isa = PBXBuildFile; fileRef = FFEA5AEB19D340BC00ED87A0 /* logo_newsblur_512.png */; }; FFECD019172B105800D45A62 /* UIActivitySafari.png in Resources */ = {isa = PBXBuildFile; fileRef = FFECD017172B105800D45A62 /* UIActivitySafari.png */; }; FFECD01A172B105800D45A62 /* UIActivitySafari@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FFECD018172B105800D45A62 /* UIActivitySafari@2x.png */; }; - FFF1E4C817750BDD00BF59D3 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4C717750BDD00BF59D3 /* Settings.bundle */; }; + FFF1E4C817750BDD00BF59D3 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4C717750BDD00BF59D3 /* Settings.bundle */; platformFilter = ios; }; FFF1E4CB17750D2C00BF59D3 /* menu_icn_preferences.png in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4C917750D2C00BF59D3 /* menu_icn_preferences.png */; }; FFF1E4CC17750D2C00BF59D3 /* menu_icn_preferences@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = FFF1E4CA17750D2C00BF59D3 /* menu_icn_preferences@2x.png */; }; FFF8B3AF1F847505001AB95E /* NBDashboardNavigationBar.m in Sources */ = {isa = PBXBuildFile; fileRef = FFF8B3AE1F847505001AB95E /* NBDashboardNavigationBar.m */; }; @@ -1342,6 +1384,13 @@ remoteGlobalIDString = 1749390F1C251BFE003D98AA; remoteInfo = "Share Extension"; }; + 17654E482B02C0A800F61B2B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 17654E312B02C08700F61B2B; + remoteInfo = "NewsBlur Alpha Widget"; + }; 177551DD238E228A00E27818 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -1359,6 +1408,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 17654E4A2B02C0A800F61B2B /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 17654E472B02C0A700F61B2B /* NewsBlur Alpha Widget.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; FF8A94A31DE3BB77000A4C31 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1397,8 +1457,12 @@ 170E3CD024F8A664009CE819 /* SplitViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewDelegate.swift; sourceTree = ""; }; 170E3CD224F8A89B009CE819 /* HorizontalPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalPageViewController.swift; sourceTree = ""; }; 170E3CD624F8AB0D009CE819 /* FeedDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDetailViewController.swift; sourceTree = ""; }; + 17150E1D2B05775A004D5309 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 1715D0292166B3F900227731 /* PremiumManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PremiumManager.h; sourceTree = ""; }; 1715D02A2166B3F900227731 /* PremiumManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PremiumManager.m; sourceTree = ""; }; + 17179E282BD6F86C006B18D5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 171904B42BBC8D4E004CCC96 /* TrainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainerView.swift; sourceTree = ""; }; + 171904B72BBCA712004CCC96 /* TrainerCapsule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainerCapsule.swift; sourceTree = ""; }; 171B6FFC25C4C7C8008638A9 /* StoryPagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryPagesViewController.swift; sourceTree = ""; }; 1721C9D02497F91900B0EDC4 /* mute_gray.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mute_gray.png; sourceTree = ""; }; 1723388A26BE43EB00610784 /* WidgetLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLoader.swift; sourceTree = ""; }; @@ -1465,6 +1529,7 @@ 175792E42930605500490924 /* NB Alpha.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NB Alpha.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 175792E62930611B00490924 /* LaunchScreenDev.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LaunchScreenDev.xib; sourceTree = ""; }; 175792E82930617600490924 /* logo_newsblur_512-dev.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "logo_newsblur_512-dev.png"; sourceTree = ""; }; + 175DC6AE2BBB87D200B3708F /* TrainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainerViewController.swift; sourceTree = ""; }; 175FAC4A23AB34EB002AC38C /* menu_icn_widget.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_icn_widget.png; sourceTree = ""; }; 175FAC4B23AB34EB002AC38C /* menu_icn_widget@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_icn_widget@2x.png"; sourceTree = ""; }; 1761295C1C630AEB00702FE4 /* mute_feed_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mute_feed_off.png; sourceTree = ""; }; @@ -1473,6 +1538,7 @@ 1761295F1C630AEB00702FE4 /* mute_feed_on@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mute_feed_on@2x.png"; sourceTree = ""; }; 1763E2A023B1BCC900BA080C /* WidgetFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetFeed.swift; sourceTree = ""; }; 1763E2A223B1CEB600BA080C /* WidgetBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBarView.swift; sourceTree = ""; }; + 17654E452B02C08700F61B2B /* NewsBlur Alpha Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NewsBlur Alpha Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 176A5C7924F8BD1B009E8DF9 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 17731A9C23DFAD3D00759A7D /* ImportExportPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportPreferences.swift; sourceTree = ""; }; 177551D3238E228A00E27818 /* Old NewsBlur Latest.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Old NewsBlur Latest.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1494,8 +1560,11 @@ 1788939C249332E6004CBA4E /* g_icn_search.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_search.png; sourceTree = ""; }; 1791C21426C4C7BC00D815AA /* WidgetStoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetStoryView.swift; sourceTree = ""; }; 17997C5727A8FDD100483E69 /* WidgetDebugTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDebugTimer.swift; sourceTree = ""; }; + 179A88012B48E64900916CF4 /* ToolbarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarDelegate.swift; sourceTree = ""; }; 179DD9CC23DFD20E007BFD21 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BridgingHeader.h; path = "Other Sources/BridgingHeader.h"; sourceTree = ""; }; 179DD9CE23DFDD51007BFD21 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; + 17A051892C095B20000994E9 /* AuxSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxSceneDelegate.swift; sourceTree = ""; }; + 17A0518C2C095E78000994E9 /* AuxInterface.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = AuxInterface.storyboard; sourceTree = ""; }; 17A396D824F86A8F0023C9E2 /* MainInterface.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MainInterface.storyboard; sourceTree = ""; }; 17A92A3B289B7C6B00AB0A78 /* saved-stories@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "saved-stories@2x.png"; sourceTree = ""; }; 17AACFD722279A3900DE6EA4 /* autoscroll_resume@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "autoscroll_resume@2x.png"; sourceTree = ""; }; @@ -1513,6 +1582,10 @@ 17B33D1627D97281009108AD /* g_icn_folder_widget.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_folder_widget.png; sourceTree = ""; }; 17B33D1727D97282009108AD /* g_icn_folder_widget@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "g_icn_folder_widget@2x.png"; sourceTree = ""; }; 17B96BE624304F72009A8EED /* Story Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Story Notification Service Extension.entitlements"; sourceTree = ""; }; + 17BC56A62BBE4A5600A30C41 /* TrainerWord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainerWord.swift; sourceTree = ""; }; + 17BC56A92BBF6BC000A30C41 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; + 17BC56AC2BBF6C0000A30C41 /* StoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryCache.swift; sourceTree = ""; }; + 17BC56AF2BBF6C2200A30C41 /* StorySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorySettings.swift; sourceTree = ""; }; 17BD3BA42271102500F615EC /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; 17BD3BA62271122800F615EC /* CoreSpotlight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSpotlight.framework; path = System/Library/Frameworks/CoreSpotlight.framework; sourceTree = SDKROOT; }; 17BD3BA82271125400F615EC /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; @@ -1593,6 +1666,8 @@ 17EB505E1BE46A900021358B /* FontListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FontListViewController.h; sourceTree = ""; }; 17EB505F1BE46A900021358B /* FontListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FontListViewController.m; sourceTree = ""; }; 17EB50611BE46BB00021358B /* FontListViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = FontListViewController.xib; path = Classes/FontListViewController.xib; sourceTree = SOURCE_ROOT; }; + 17EE11C72B27FA0C00E7C0CC /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Settings.bundle; path = mac/Settings.bundle; sourceTree = ""; }; + 17EE11CD2B28011D00E7C0CC /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; name = Credits.rtf; path = mac/Credits.rtf; sourceTree = ""; }; 17F156701BDABBF60092EBFD /* safari_shadow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "safari_shadow@2x.png"; sourceTree = ""; }; 17F363EF238E417300D5379D /* WidgetExtensionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetExtensionViewController.swift; sourceTree = ""; }; 17F39EA6264754CC004B46D1 /* image_preview_large_left.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = image_preview_large_left.png; sourceTree = ""; }; @@ -1766,7 +1841,7 @@ 784B50EA127E3F68008F90EA /* LoginViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = ""; }; 784B50EB127E3F68008F90EA /* LoginViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = ""; }; 788EF355127E5BC80088EDC5 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; - 8D1107310486CEB800E47090 /* NewsBlur-iPhone-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "NewsBlur-iPhone-Info.plist"; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = ""; }; E160F0551C9DAC2C00CB96DF /* UIViewController+HidePopover.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+HidePopover.h"; path = "Other Sources/UIViewController+HidePopover.h"; sourceTree = ""; }; E160F0561C9DAC2C00CB96DF /* UIViewController+HidePopover.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+HidePopover.m"; path = "Other Sources/UIViewController+HidePopover.m"; sourceTree = ""; }; E1C44B09200147ED002128AD /* StoryTitleAttributedString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StoryTitleAttributedString.h; sourceTree = ""; }; @@ -2071,7 +2146,7 @@ FF8A949A1DE3BB77000A4C31 /* NotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationService.m; sourceTree = ""; }; FF8A949C1DE3BB77000A4C31 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FF8AFE561CAC73C9005D9B40 /* unread_blue@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "unread_blue@3x.png"; sourceTree = ""; }; - FF8C49921BBC9D140010D894 /* NewsBlur.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = NewsBlur.entitlements; path = NewsBlur/NewsBlur.entitlements; sourceTree = ""; }; + FF8C49921BBC9D140010D894 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; FF8D1EA51BAA304E00725D8A /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = ""; }; FF8D1EA61BAA304E00725D8A /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = ""; }; FF8D1EBD1BAA311000725D8A /* SBJson4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBJson4.h; sourceTree = ""; }; @@ -2313,6 +2388,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 17654E3B2B02C08700F61B2B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 17654E3C2B02C08700F61B2B /* SwiftUI.framework in Frameworks */, + 17654E3D2B02C08700F61B2B /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 177551D0238E228A00E27818 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2513,6 +2597,7 @@ 177551D3238E228A00E27818 /* Old NewsBlur Latest.appex */, 173CB30D26BCE94700BA872A /* NewsBlur Widget.appex */, 175792E42930605500490924 /* NB Alpha.app */, + 17654E452B02C08700F61B2B /* NewsBlur Alpha Widget.appex */, ); name = Products; sourceTree = ""; @@ -2531,8 +2616,6 @@ 173CB31226BCE94700BA872A /* Widget Extension */, 29B97323FDCFA39411CA2CEA /* Frameworks */, 19C28FACFE9D520D11CA2CBB /* Products */, - FF8C49921BBC9D140010D894 /* NewsBlur.entitlements */, - 8D1107310486CEB800E47090 /* NewsBlur-iPhone-Info.plist */, ); name = CustomTemplate; sourceTree = ""; @@ -2636,14 +2719,20 @@ isa = PBXGroup; children = ( 17A396D824F86A8F0023C9E2 /* MainInterface.storyboard */, + 17A0518C2C095E78000994E9 /* AuxInterface.storyboard */, FF191E4E18A323F400473252 /* Images.xcassets */, + 17EE11CD2B28011D00E7C0CC /* Credits.rtf */, 430C4BBE15D7208000B9F63B /* FTUX */, 431B857815A132C500DCE497 /* js */, 431B857715A132BE00DCE497 /* css */, 1753696F1BE535CF00904D00 /* fonts */, 431B857615A132B600DCE497 /* Images */, FFF1E4C717750BDD00BF59D3 /* Settings.bundle */, + 17EE11C72B27FA0C00E7C0CC /* Settings.bundle */, E1D123FD1C66753D00434F40 /* Localizable.stringsdict */, + FF8C49921BBC9D140010D894 /* App.entitlements */, + 8D1107310486CEB800E47090 /* Info.plist */, + 17179E282BD6F86C006B18D5 /* PrivacyInfo.xcprivacy */, ); path = Resources; sourceTree = ""; @@ -2769,6 +2858,10 @@ 17EB505F1BE46A900021358B /* FontListViewController.m */, 78095EC6128F30B500230C8E /* OriginalStoryViewController.h */, 78095EC7128F30B500230C8E /* OriginalStoryViewController.m */, + 175DC6AE2BBB87D200B3708F /* TrainerViewController.swift */, + 171904B42BBC8D4E004CCC96 /* TrainerView.swift */, + 171904B72BBCA712004CCC96 /* TrainerCapsule.swift */, + 17BC56A62BBE4A5600A30C41 /* TrainerWord.swift */, FF67D3B0168924C40057A7DA /* TrainerViewController.h */, FF67D3B1168924C40057A7DA /* TrainerViewController.m */, FF6282131A11613900271FDB /* UserTagsViewController.h */, @@ -3230,7 +3323,10 @@ 43D8189F15B9404D00733444 /* Models */ = { isa = PBXGroup; children = ( + 17BC56A92BBF6BC000A30C41 /* Feed.swift */, 172ECBF6298B1239006371BC /* Story.swift */, + 17BC56AC2BBF6C0000A30C41 /* StoryCache.swift */, + 17BC56AF2BBF6C2200A30C41 /* StorySettings.swift */, ); name = Models; sourceTree = ""; @@ -3274,6 +3370,9 @@ 175792E62930611B00490924 /* LaunchScreenDev.xib */, 1D3623240D0F684500981E51 /* NewsBlurAppDelegate.h */, 1D3623250D0F684500981E51 /* NewsBlurAppDelegate.m */, + 17150E1D2B05775A004D5309 /* SceneDelegate.swift */, + 17A051892C095B20000994E9 /* AuxSceneDelegate.swift */, + 179A88012B48E64900916CF4 /* ToolbarDelegate.swift */, FFD1D72F1459B63500E46F89 /* BaseViewController.h */, FFD1D7301459B63500E46F89 /* BaseViewController.m */, 17C074941C14C46B00CFCDB7 /* ThemeManager.h */, @@ -3696,10 +3795,12 @@ 175790622930605500490924 /* Resources */, 175792252930605500490924 /* Sources */, 175792C12930605500490924 /* Frameworks */, + 17654E4A2B02C0A800F61B2B /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 17654E492B02C0A800F61B2B /* PBXTargetDependency */, ); name = "NewsBlur Alpha"; packageProductDependencies = ( @@ -3708,6 +3809,23 @@ productReference = 175792E42930605500490924 /* NB Alpha.app */; productType = "com.apple.product-type.application"; }; + 17654E312B02C08700F61B2B /* NewsBlur Alpha Widget */ = { + isa = PBXNativeTarget; + buildConfigurationList = 17654E422B02C08700F61B2B /* Build configuration list for PBXNativeTarget "NewsBlur Alpha Widget" */; + buildPhases = ( + 17654E322B02C08700F61B2B /* Sources */, + 17654E3B2B02C08700F61B2B /* Frameworks */, + 17654E3E2B02C08700F61B2B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "NewsBlur Alpha Widget"; + productName = WidgetExtension; + productReference = 17654E452B02C08700F61B2B /* NewsBlur Alpha Widget.appex */; + productType = "com.apple.product-type.app-extension"; + }; 177551D2238E228A00E27818 /* Old Widget Extension */ = { isa = PBXNativeTarget; buildConfigurationList = 177551E2238E228A00E27818 /* Build configuration list for PBXNativeTarget "Old Widget Extension" */; @@ -3773,8 +3891,9 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1540; ORGANIZATIONNAME = NewsBlur; TargetAttributes = { 173CB30C26BCE94700BA872A = { @@ -3864,6 +3983,7 @@ FF8A94961DE3BB77000A4C31 /* Story Notification Service Extension */, 177551D2238E228A00E27818 /* Old Widget Extension */, 173CB30C26BCE94700BA872A /* NewsBlur Widget */, + 17654E312B02C08700F61B2B /* NewsBlur Alpha Widget */, ); }; /* End PBXProject section */ @@ -3895,10 +4015,13 @@ buildActionMask = 2147483647; files = ( 175790632930605500490924 /* ChronicleSSm-Book.otf in Resources */, + 1757920D2930605500490924 /* Settings.bundle in Resources */, 175790642930605500490924 /* WhitneySSm-Book-Bas.otf in Resources */, 175790652930605500490924 /* ChronicleSSm-BookItalic.otf in Resources */, 175790662930605500490924 /* WhitneySSm-Medium-Bas.otf in Resources */, + 17A0518E2C095E78000994E9 /* AuxInterface.storyboard in Resources */, 175790672930605500490924 /* icons8-stack-of-paper-100.png in Resources */, + 17EE11C92B27FA0C00E7C0CC /* Settings.bundle in Resources */, 175790682930605500490924 /* WhitneySSm-MediumItalic-Bas.otf in Resources */, 175790692930605500490924 /* WhitneySSm-BookItalic-Bas.otf in Resources */, 1757906A2930605500490924 /* barbutton_sort_desc@3x.png in Resources */, @@ -4106,6 +4229,7 @@ 175791342930605500490924 /* g_icn_greensun@2x.png in Resources */, 175791352930605500490924 /* ak-icon-infrequent.png in Resources */, 175791362930605500490924 /* nav_icn_add.png in Resources */, + 17EE11CF2B28011D00E7C0CC /* Credits.rtf in Resources */, 175791372930605500490924 /* icons8-pyramids-100.png in Resources */, 175791382930605500490924 /* ak-icon-global.png in Resources */, 175791392930605500490924 /* content_preview_large@2x.png in Resources */, @@ -4284,6 +4408,7 @@ 175791E42930605500490924 /* train@2x.png in Resources */, 175791E52930605500490924 /* logo_40.png in Resources */, 175791E62930605500490924 /* menu_icn_share.png in Resources */, + 17179E2A2BD6F86D006B18D5 /* PrivacyInfo.xcprivacy in Resources */, 175791E72930605500490924 /* autoscroll_pause.png in Resources */, 175791E82930605500490924 /* safari@3x.png in Resources */, 175791E92930605500490924 /* g_icn_eating.png in Resources */, @@ -4322,7 +4447,6 @@ 1757920A2930605500490924 /* g_icn_textview_black.png in Resources */, 1757920B2930605500490924 /* logo_58.png in Resources */, 1757920C2930605500490924 /* logo_144.png in Resources */, - 1757920D2930605500490924 /* Settings.bundle in Resources */, 1757920E2930605500490924 /* menu_icn_preferences.png in Resources */, 1757920F2930605500490924 /* menu_icn_preferences@2x.png in Resources */, 175792102930605500490924 /* checkmark.png in Resources */, @@ -4349,6 +4473,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 17654E3E2B02C08700F61B2B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 17654E3F2B02C08700F61B2B /* WhitneySSm-Medium-Bas.otf in Resources */, + 17654E402B02C08700F61B2B /* Assets.xcassets in Resources */, + 17654E412B02C08700F61B2B /* WhitneySSm-Book-Bas.otf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 177551D1238E228A00E27818 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4410,6 +4544,7 @@ 433323B9158901A40025064D /* fountain_pen@2x.png in Resources */, 433323BB158901C10025064D /* login_background.png in Resources */, FF83FF151FB54693008DAC0F /* g_icn_lightning.png in Resources */, + 17EE11CE2B28011D00E7C0CC /* Credits.rtf in Resources */, 17BE5A761C5DDA8C0075F92C /* barbutton_sort_desc@2x.png in Resources */, 433323BE1589022C0025064D /* user.png in Resources */, 433323BF1589022C0025064D /* user@2x.png in Resources */, @@ -4615,6 +4750,7 @@ 17EB505D1BE4411E0021358B /* choose_font@2x.png in Resources */, FFC5F30C16E2D2C2007AC72C /* story_share_appnet_active@2x.png in Resources */, FFC5F30D16E2D2C2007AC72C /* story_share_appnet.png in Resources */, + 17EE11C82B27FA0C00E7C0CC /* Settings.bundle in Resources */, FFC5F30E16E2D2C2007AC72C /* story_share_appnet@2x.png in Resources */, 1740C6A11C1110BA005EA453 /* theme_color_medium-sel@2x.png in Resources */, 17876BA01C9911D40055DD15 /* g_icn_folder_sm.png in Resources */, @@ -4661,6 +4797,7 @@ FF22FE6E16E554540046165A /* barbutton_refresh.png in Resources */, FF22FE6F16E554540046165A /* barbutton_refresh@2x.png in Resources */, FF22FE7216E554FD0046165A /* barbutton_sendto.png in Resources */, + 17A0518D2C095E78000994E9 /* AuxInterface.storyboard in Resources */, FFB9BE4D17F4B65B00FE0A36 /* logo_120@2x.png in Resources */, FF03AFFF19F881380063002A /* ARChromeActivity@2x.png in Resources */, FF83FF1B1FB54693008DAC0F /* g_icn_privacy.png in Resources */, @@ -4689,6 +4826,7 @@ FF03AFE419F87A770063002A /* g_icn_folder_read.png in Resources */, FFDD845E16E8871A000AA0A2 /* menu_icn_fetch_subscribers.png in Resources */, FF5ACC211DE5ED7500FBD044 /* menu_icn_notifications.png in Resources */, + 17179E292BD6F86C006B18D5 /* PrivacyInfo.xcprivacy in Resources */, FFDD845F16E8871A000AA0A2 /* menu_icn_fetch_subscribers@2x.png in Resources */, FFDD846016E8871A000AA0A2 /* menu_icn_fetch.png in Resources */, FFDD846116E8871A000AA0A2 /* menu_icn_fetch@2x.png in Resources */, @@ -4911,6 +5049,7 @@ 175792542930605500490924 /* IASKPSTextFieldSpecifierViewCell.m in Sources */, 175792552930605500490924 /* SBJson4StreamWriter.m in Sources */, 175792562930605500490924 /* FontSettingsViewController.m in Sources */, + 17BC56B12BBF6C2200A30C41 /* StorySettings.swift in Sources */, 175792572930605500490924 /* IASKAppSettingsViewController.m in Sources */, 175792582930605500490924 /* MarkReadMenuViewController.m in Sources */, 175792592930605500490924 /* SSWAnimator.m in Sources */, @@ -4932,10 +5071,14 @@ 175792692930605500490924 /* main.m in Sources */, 1757926A2930605500490924 /* MBProgressHUD.m in Sources */, 1757926B2930605500490924 /* NSString+HTML.m in Sources */, + 171904B92BBCA712004CCC96 /* TrainerCapsule.swift in Sources */, 1757926C2930605500490924 /* IASKSettingsReader.m in Sources */, 1757926D2930605500490924 /* UserTagsViewController.m in Sources */, 1757926E2930605500490924 /* StringHelper.m in Sources */, 1757926F2930605500490924 /* TransparentToolbar.m in Sources */, + 179A88032B48E64A00916CF4 /* ToolbarDelegate.swift in Sources */, + 175DC6B02BBB87D200B3708F /* TrainerViewController.swift in Sources */, + 171904B62BBC8D4E004CCC96 /* TrainerView.swift in Sources */, 175792702930605500490924 /* THCircularProgressView.m in Sources */, 175792712930605500490924 /* IASKSpecifier.m in Sources */, 175792722930605500490924 /* UIView+ViewController.m in Sources */, @@ -4970,6 +5113,7 @@ 1757928C2930605500490924 /* PINMemoryCache.m in Sources */, 1757928D2930605500490924 /* SiteCell.m in Sources */, 1757928E2930605500490924 /* SloppySwiper.m in Sources */, + 17BC56A82BBE4A5600A30C41 /* TrainerWord.swift in Sources */, 1757928F2930605500490924 /* UIViewController+HidePopover.m in Sources */, 175792902930605500490924 /* FolderTitleView.m in Sources */, 175792912930605500490924 /* HorizontalPageDelegate.swift in Sources */, @@ -5008,16 +5152,20 @@ 175792B02930605500490924 /* FMResultSet.m in Sources */, 175792B12930605500490924 /* NBNotifier.m in Sources */, 175792B22930605500490924 /* TUSafariActivity.m in Sources */, + 17BC56AE2BBF6C0000A30C41 /* StoryCache.swift in Sources */, 175792B32930605500490924 /* PremiumViewController.m in Sources */, 175792B42930605500490924 /* NBLoadingCell.m in Sources */, 175792B52930605500490924 /* IASKTextViewCell.m in Sources */, 175792B62930605500490924 /* StoriesCollection.m in Sources */, 175792B72930605500490924 /* IASKTextView.m in Sources */, + 17A0518B2C095B20000994E9 /* AuxSceneDelegate.swift in Sources */, 175792B82930605500490924 /* OfflineSyncUnreads.m in Sources */, 175792B92930605500490924 /* IASKPSSliderSpecifierViewCell.m in Sources */, + 17BC56AB2BBF6BC000A30C41 /* Feed.swift in Sources */, 175792BA2930605500490924 /* OfflineFetchStories.m in Sources */, 175792BB2930605500490924 /* OfflineFetchText.m in Sources */, 175792BC2930605500490924 /* OfflineFetchImages.m in Sources */, + 17150E1F2B05775A004D5309 /* SceneDelegate.swift in Sources */, 175792BD2930605500490924 /* SplitViewDelegate.swift in Sources */, 175792BE2930605500490924 /* Reachability.m in Sources */, 175792BF2930605500490924 /* NBSwipeableCell.m in Sources */, @@ -5025,6 +5173,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 17654E322B02C08700F61B2B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 17654E332B02C08700F61B2B /* WidgetLoader.swift in Sources */, + 17654E342B02C08700F61B2B /* WidgetCache.swift in Sources */, + 17654E352B02C08700F61B2B /* WidgetExtension.swift in Sources */, + 17654E362B02C08700F61B2B /* WidgetStory.swift in Sources */, + 17654E372B02C08700F61B2B /* WidgetBarView.swift in Sources */, + 17654E382B02C08700F61B2B /* WidgetDebugTimer.swift in Sources */, + 17654E392B02C08700F61B2B /* WidgetFeed.swift in Sources */, + 17654E3A2B02C08700F61B2B /* WidgetStoryView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 177551CF238E228A00E27818 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -5095,6 +5258,7 @@ FF34FD6B1E9D93CB0062F8ED /* IASKPSTextFieldSpecifierViewCell.m in Sources */, FF8D1ED01BAA311000725D8A /* SBJson4StreamWriter.m in Sources */, 43763AD1158F90B100B3DBE2 /* FontSettingsViewController.m in Sources */, + 17BC56B02BBF6C2200A30C41 /* StorySettings.swift in Sources */, FF34FD601E9D93CB0062F8ED /* IASKAppSettingsViewController.m in Sources */, 17CBD3BF1BF66B6C003FCCAE /* MarkReadMenuViewController.m in Sources */, FFA045B519CA49D700618DC4 /* SSWAnimator.m in Sources */, @@ -5116,10 +5280,14 @@ 43A4C3DC15B00966008787B5 /* main.m in Sources */, 43A4C3DD15B00966008787B5 /* MBProgressHUD.m in Sources */, 43A4C3E115B00966008787B5 /* NSString+HTML.m in Sources */, + 171904B82BBCA712004CCC96 /* TrainerCapsule.swift in Sources */, FF34FD641E9D93CB0062F8ED /* IASKSettingsReader.m in Sources */, FF6282151A11613900271FDB /* UserTagsViewController.m in Sources */, 43A4C3E315B00966008787B5 /* StringHelper.m in Sources */, 43A4C3E415B00966008787B5 /* TransparentToolbar.m in Sources */, + 179A88022B48E64A00916CF4 /* ToolbarDelegate.swift in Sources */, + 175DC6AF2BBB87D200B3708F /* TrainerViewController.swift in Sources */, + 171904B52BBC8D4E004CCC96 /* TrainerView.swift in Sources */, FFD6604C1BACA45D006E4B8D /* THCircularProgressView.m in Sources */, FF34FD681E9D93CB0062F8ED /* IASKSpecifier.m in Sources */, FFA0484419CA73B700618DC4 /* UIView+ViewController.m in Sources */, @@ -5154,6 +5322,7 @@ FF2924E71E932D2900FCFA63 /* PINMemoryCache.m in Sources */, 43CE0F5F15DADB7F00608ED8 /* SiteCell.m in Sources */, FFA045B419CA49D700618DC4 /* SloppySwiper.m in Sources */, + 17BC56A72BBE4A5600A30C41 /* TrainerWord.swift in Sources */, E160F0571C9DAC2C00CB96DF /* UIViewController+HidePopover.m in Sources */, FFDE35CC161B8F870034BFDE /* FolderTitleView.m in Sources */, 172AD264251D901D000BB264 /* HorizontalPageDelegate.swift in Sources */, @@ -5192,16 +5361,20 @@ FF753CD3175858FC00344EC9 /* FMResultSet.m in Sources */, FF6618C8176184560039913B /* NBNotifier.m in Sources */, FF03AFF319F87F2E0063002A /* TUSafariActivity.m in Sources */, + 17BC56AD2BBF6C0000A30C41 /* StoryCache.swift in Sources */, FF83FF051FB52565008DAC0F /* PremiumViewController.m in Sources */, FF11045F176950F900502C29 /* NBLoadingCell.m in Sources */, FF34FD701E9D93CB0062F8ED /* IASKTextViewCell.m in Sources */, FFAD89C218AC45A100D68567 /* StoriesCollection.m in Sources */, FF34FD6F1E9D93CB0062F8ED /* IASKTextView.m in Sources */, + 17A0518A2C095B20000994E9 /* AuxSceneDelegate.swift in Sources */, FF855B5B1794B0670098D48A /* OfflineSyncUnreads.m in Sources */, FF34FD6A1E9D93CB0062F8ED /* IASKPSSliderSpecifierViewCell.m in Sources */, + 17BC56AA2BBF6BC000A30C41 /* Feed.swift in Sources */, FF855B5E1794B0760098D48A /* OfflineFetchStories.m in Sources */, 17362ADD23639B4E00A0FCCC /* OfflineFetchText.m in Sources */, FF855B611794B0830098D48A /* OfflineFetchImages.m in Sources */, + 17150E1E2B05775A004D5309 /* SceneDelegate.swift in Sources */, 170E3CD124F8A664009CE819 /* SplitViewDelegate.swift in Sources */, FF8D1EA71BAA304E00725D8A /* Reachability.m in Sources */, FFCDD90117F65A71000C6483 /* NBSwipeableCell.m in Sources */, @@ -5231,6 +5404,11 @@ target = 1749390F1C251BFE003D98AA /* Share Extension */; targetProxy = 174939191C251BFE003D98AA /* PBXContainerItemProxy */; }; + 17654E492B02C0A800F61B2B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 17654E312B02C08700F61B2B /* NewsBlur Alpha Widget */; + targetProxy = 17654E482B02C0A800F61B2B /* PBXContainerItemProxy */; + }; 177551DE238E228A00E27818 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; @@ -5327,6 +5505,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = "Widget Extension/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5337,8 +5516,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; @@ -5371,6 +5549,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = "Widget Extension/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5381,8 +5560,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.newsblur.NewsBlur.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; @@ -5438,8 +5616,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Debug; @@ -5486,8 +5663,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; TARGETED_DEVICE_FAMILY = "1,2,6"; VALIDATE_PRODUCT = YES; }; @@ -5499,11 +5675,10 @@ ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_ENTITLEMENTS = NewsBlur/NewsBlur.entitlements; + CODE_SIGN_ENTITLEMENTS = Resources/App.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 152; DEVELOPMENT_TEAM = HR7P97SD72; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -5516,7 +5691,7 @@ GCC_THUMB_SUPPORT = NO; GCC_VERSION = ""; HEADER_SEARCH_PATHS = ""; - INFOPLIST_FILE = "NewsBlur-iPhone-Info.plist"; + INFOPLIST_FILE = Resources/Info.plist; LAUNCH_SCREEN_NAME = LaunchScreenDev; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( @@ -5535,9 +5710,9 @@ PRODUCT_NAME = "NB Alpha"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Other Sources/BridgingHeader.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "NewsBlur-Swift.h"; TARGETED_DEVICE_FAMILY = "1,2,6"; @@ -5551,11 +5726,10 @@ ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_ENTITLEMENTS = NewsBlur/NewsBlur.entitlements; + CODE_SIGN_ENTITLEMENTS = Resources/App.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 152; DEVELOPMENT_TEAM = HR7P97SD72; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -5567,7 +5741,7 @@ GCC_THUMB_SUPPORT = NO; GCC_VERSION = ""; HEADER_SEARCH_PATHS = ""; - INFOPLIST_FILE = "NewsBlur-iPhone-Info.plist"; + INFOPLIST_FILE = Resources/Info.plist; LAUNCH_SCREEN_NAME = LaunchScreenDev; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( @@ -5586,7 +5760,8 @@ PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Other Sources/BridgingHeader.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "NewsBlur-Swift.h"; TARGETED_DEVICE_FAMILY = "1,2,6"; @@ -5594,6 +5769,101 @@ }; name = Release; }; + 17654E432B02C08700F61B2B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Widget Extension/WidgetExtension.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = HR7P97SD72; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "Widget Extension/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NB-Alpha.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,6"; + }; + name = Debug; + }; + 17654E442B02C08700F61B2B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Widget Extension/WidgetExtension.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HR7P97SD72; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "Widget Extension/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.newsblur.NB-Alpha.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,6"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 177551E0238E228A00E27818 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5694,11 +5964,11 @@ ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_ENTITLEMENTS = NewsBlur/NewsBlur.entitlements; + CODE_SIGN_ENTITLEMENTS = Resources/App.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 152; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_TEAM = HR7P97SD72; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -5711,7 +5981,7 @@ GCC_THUMB_SUPPORT = NO; GCC_VERSION = ""; HEADER_SEARCH_PATHS = ""; - INFOPLIST_FILE = "NewsBlur-iPhone-Info.plist"; + INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -5728,9 +5998,9 @@ PRODUCT_NAME = NewsBlur; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Other Sources/BridgingHeader.h"; TARGETED_DEVICE_FAMILY = "1,2,6"; "WARNING_CFLAGS[arch=*]" = "-Wall"; @@ -5744,11 +6014,11 @@ ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_ENTITLEMENTS = NewsBlur/NewsBlur.entitlements; + CODE_SIGN_ENTITLEMENTS = Resources/App.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 152; + CURRENT_PROJECT_VERSION = 256; DEVELOPMENT_TEAM = HR7P97SD72; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -5760,7 +6030,7 @@ GCC_THUMB_SUPPORT = NO; GCC_VERSION = ""; HEADER_SEARCH_PATHS = ""; - INFOPLIST_FILE = "NewsBlur-iPhone-Info.plist"; + INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -5777,7 +6047,8 @@ PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Other Sources/BridgingHeader.h"; TARGETED_DEVICE_FAMILY = "1,2,6"; VALIDATE_PRODUCT = YES; @@ -5788,6 +6059,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; CLANG_ENABLE_MODULES = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -5810,10 +6082,11 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 151; + CURRENT_PROJECT_VERSION = 154; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = "compiler-default"; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -5826,13 +6099,12 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LAUNCH_SCREEN_NAME = LaunchScreen; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 13.0; + MARKETING_VERSION = 13.1.1; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "-ObjC"; PROVISIONING_PROFILE = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; - STRIP_INSTALLED_PRODUCT = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5843,6 +6115,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; CLANG_ENABLE_MODULES = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -5865,9 +6138,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 151; + CURRENT_PROJECT_VERSION = 154; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = "compiler-default"; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -5880,13 +6154,12 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LAUNCH_SCREEN_NAME = LaunchScreen; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 13.0; + MARKETING_VERSION = 13.1.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; OTHER_LDFLAGS = "-ObjC"; PROVISIONING_PROFILE = ""; SDKROOT = iphoneos; - STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -5932,8 +6205,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Debug; @@ -5970,8 +6242,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; + SUPPORTS_MACCATALYST = YES; TARGETED_DEVICE_FAMILY = "1,2,6"; VALIDATE_PRODUCT = YES; }; @@ -6007,6 +6278,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 17654E422B02C08700F61B2B /* Build configuration list for PBXNativeTarget "NewsBlur Alpha Widget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 17654E432B02C08700F61B2B /* Debug */, + 17654E442B02C08700F61B2B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 177551E2238E228A00E27818 /* Build configuration list for PBXNativeTarget "Old Widget Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Alpha Widget Extension.xcscheme b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Alpha Widget Extension.xcscheme new file mode 100644 index 0000000000..ee3aa01b69 --- /dev/null +++ b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/Alpha Widget Extension.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur Alpha.xcscheme b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur Alpha.xcscheme index f335651082..f541869de3 100644 --- a/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur Alpha.xcscheme +++ b/clients/ios/NewsBlur.xcodeproj/xcshareddata/xcschemes/NewsBlur Alpha.xcscheme @@ -1,6 +1,6 @@ -#if TARGET_OS_IOS +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import diff --git a/clients/ios/Other Sources/AFNetworking/UIRefreshControl+AFNetworking.m b/clients/ios/Other Sources/AFNetworking/UIRefreshControl+AFNetworking.m index cd46916a10..4c5efaacfe 100755 --- a/clients/ios/Other Sources/AFNetworking/UIRefreshControl+AFNetworking.m +++ b/clients/ios/Other Sources/AFNetworking/UIRefreshControl+AFNetworking.m @@ -23,7 +23,7 @@ #import "UIRefreshControl+AFNetworking.h" #import -#if TARGET_OS_IOS +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST #import "AFURLSessionManager.h" diff --git a/clients/ios/Other Sources/BridgingHeader.h b/clients/ios/Other Sources/BridgingHeader.h index ca30b83223..1bb179a32d 100644 --- a/clients/ios/Other Sources/BridgingHeader.h +++ b/clients/ios/Other Sources/BridgingHeader.h @@ -11,6 +11,7 @@ #import #import "NSString+HTML.h" +#import "Utilities.h" #import "NewsBlurAppDelegate.h" #import "ThemeManager.h" #import "StoriesCollection.h" diff --git a/clients/ios/Other Sources/InAppSettingsKit/Controllers/IASKAppSettingsViewController.m b/clients/ios/Other Sources/InAppSettingsKit/Controllers/IASKAppSettingsViewController.m index 0e49853a88..9ffc120fab 100755 --- a/clients/ios/Other Sources/InAppSettingsKit/Controllers/IASKAppSettingsViewController.m +++ b/clients/ios/Other Sources/InAppSettingsKit/Controllers/IASKAppSettingsViewController.m @@ -123,7 +123,7 @@ - (void)createSelections { - (BOOL)isPad { BOOL isPad = NO; #if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 30200) - isPad = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad; + isPad = [[UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPhone; #endif return isPad; } diff --git a/clients/ios/Other Sources/InAppSettingsKit/Models/IASKSettingsReader.m b/clients/ios/Other Sources/InAppSettingsKit/Models/IASKSettingsReader.m index 789bd9348e..2a511f7f93 100755 --- a/clients/ios/Other Sources/InAppSettingsKit/Models/IASKSettingsReader.m +++ b/clients/ios/Other Sources/InAppSettingsKit/Models/IASKSettingsReader.m @@ -279,7 +279,7 @@ - (NSString *)platformSuffixForInterfaceIdiom:(UIUserInterfaceIdiom) interfaceId switch (interfaceIdiom) { case UIUserInterfaceIdiomPad: return @"~ipad"; case UIUserInterfaceIdiomPhone: return @"~iphone"; - default: return @"~iphone"; + default: return @"~ipad"; } } diff --git a/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m b/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m index 3988ffb41e..c5840dae60 100644 --- a/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m +++ b/clients/ios/Other Sources/OnePasswordExtension/OnePasswordExtension.m @@ -466,7 +466,7 @@ - (void)processExtensionItem:(nullable NSExtensionItem *)extensionItem completio } - (UIActivityViewController *)activityViewControllerForItem:(nonnull NSDictionary *)item viewController:(nonnull UIViewController*)viewController sender:(nullable id)sender typeIdentifier:(nonnull NSString *)typeIdentifier { - NSAssert(NO == ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && sender == nil), @"sender must not be nil on iPad."); + NSAssert(NO == ([[UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPhone && sender == nil), @"sender must not be nil on iPad."); NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem:item typeIdentifier:typeIdentifier]; diff --git a/clients/ios/Resources-iPhone/TrainerViewController.xib b/clients/ios/Resources-iPhone/TrainerViewController.xib index 2dcced6c96..1030ffc963 100644 --- a/clients/ios/Resources-iPhone/TrainerViewController.xib +++ b/clients/ios/Resources-iPhone/TrainerViewController.xib @@ -1,43 +1,31 @@ - + - + - - - - - - - - - - - - - - - - - - + + + + + + diff --git a/clients/ios/NewsBlur/NewsBlur.entitlements b/clients/ios/Resources/App.entitlements similarity index 100% rename from clients/ios/NewsBlur/NewsBlur.entitlements rename to clients/ios/Resources/App.entitlements diff --git a/clients/ios/Resources/AuxInterface.storyboard b/clients/ios/Resources/AuxInterface.storyboard new file mode 100644 index 0000000000..8bdf9f5b4b --- /dev/null +++ b/clients/ios/Resources/AuxInterface.storyboard @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/ios/NewsBlur-iPhone-Info.plist b/clients/ios/Resources/Info.plist similarity index 92% rename from clients/ios/NewsBlur-iPhone-Info.plist rename to clients/ios/Resources/Info.plist index 0436b3cb3c..9163dbffa7 100644 --- a/clients/ios/NewsBlur-iPhone-Info.plist +++ b/clients/ios/Resources/Info.plist @@ -148,6 +148,25 @@ ChronicleSSm-MediumItalic.otf ChronicleSSm-BookItalic.otf + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + MainInterface + + + + UIApplicationShortcutItems @@ -189,8 +208,9 @@ UIBackgroundModes - fetch audio + fetch + processing UILaunchStoryboardName $(LAUNCH_SCREEN_NAME) diff --git a/clients/ios/Resources/MainInterface.storyboard b/clients/ios/Resources/MainInterface.storyboard index 6361ca2c57..91922efd08 100644 --- a/clients/ios/Resources/MainInterface.storyboard +++ b/clients/ios/Resources/MainInterface.storyboard @@ -1,11 +1,12 @@ - + - + + @@ -14,39 +15,39 @@ - + - + - + - + - + - + - + @@ -226,7 +227,10 @@ + + + @@ -239,6 +243,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -292,7 +895,7 @@ - + @@ -405,7 +1008,7 @@ - + @@ -420,7 +1023,7 @@ - + diff --git a/clients/ios/Resources/PrivacyInfo.xcprivacy b/clients/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..729c0df808 --- /dev/null +++ b/clients/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,18 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + + 1C8F.1 + + + + + diff --git a/clients/ios/Resources/mac/Credits.rtf b/clients/ios/Resources/mac/Credits.rtf new file mode 100644 index 0000000000..bd43258cf3 --- /dev/null +++ b/clients/ios/Resources/mac/Credits.rtf @@ -0,0 +1,13 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2758 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\margl1440\margr1440\vieww28800\viewh21020\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 +{\field{\*\fldinst{HYPERLINK "https://www.newsblur.com/"}}{\fldrslt +\f0\fs22 \cf0 newsblur.com}} +\f0\fs22 \ +\ +{\field{\*\fldinst{HYPERLINK "https://newsblur.com/privacy"}}{\fldrslt Privacy Policy}} \'95 {\field{\*\fldinst{HYPERLINK "https://newsblur.com/tos"}}{\fldrslt Terms of Service}}\ +\ +Copyright NewsBlur, Inc.} \ No newline at end of file diff --git a/clients/ios/Resources/mac/Settings.bundle/Advanced.plist b/clients/ios/Resources/mac/Settings.bundle/Advanced.plist new file mode 100644 index 0000000000..c2387232d9 --- /dev/null +++ b/clients/ios/Resources/mac/Settings.bundle/Advanced.plist @@ -0,0 +1,111 @@ + + + + + PreferenceSpecifiers + + + Title + Offline stories + Type + PSGroupSpecifier + + + Type + PSToggleSwitchSpecifier + Title + Download stories + Key + offline_allowed + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Download text + Key + offline_text_download + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Download images + Key + offline_image_download + DefaultValue + + + + FooterText + More stories take more disk space, but otherwise storing more stories has no noticable effect on performance. + Type + PSMultiValueSpecifier + Title + Store + Titles + + 100 stories + 500 stories + 1,000 stories + 2,000 stories + 5,000 stories + 10,000 stories + + DefaultValue + 1000 + Values + + 100 + 500 + 1000 + 2000 + 5000 + 10000 + + Key + offline_store_limit + + + Type + IASKButtonSpecifier + Title + Delete offline stories... + Key + offline_cache_empty_stories + + + Title + Custom Domain + FooterText + Leave blank to use the NewsBlur site, or enter the URL of your self-hosted NewsBlur installation, e.g. "https://www.domain.com". Takes effect next time app is opened. + Type + PSGroupSpecifier + + + Type + PSTextFieldSpecifier + KeyboardType + URL + AutocorrectionType + No + DefaultValue + + AutocapitalizationType + None + Title + URL + Key + custom_domain + + + StringsTable + Root + Icon + AdvancedTemplate + + diff --git a/clients/ios/Resources/mac/Settings.bundle/AdvancedTemplate.png b/clients/ios/Resources/mac/Settings.bundle/AdvancedTemplate.png new file mode 100644 index 0000000000..7cff794d5f Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/AdvancedTemplate.png differ diff --git a/clients/ios/Resources/mac/Settings.bundle/Appearance.plist b/clients/ios/Resources/mac/Settings.bundle/Appearance.plist new file mode 100644 index 0000000000..a63078363a --- /dev/null +++ b/clients/ios/Resources/mac/Settings.bundle/Appearance.plist @@ -0,0 +1,149 @@ + + + + + PreferenceSpecifiers + + + Title + Text Size + Type + PSGroupSpecifier + + + Type + PSToggleSwitchSpecifier + Title + Use system size + Key + use_system_font_size + DefaultValue + + + + Type + PSMultiValueSpecifier + Title + Feed and story list + Titles + + Extra small + Small + Medium + Large + Extra Large + + DefaultValue + medium + Values + + xs + small + medium + large + xl + + Key + feed_list_font_size + + + Type + PSMultiValueSpecifier + Title + Story detail + Titles + + Extra small + Small + Medium + Large + Extra Large + + DefaultValue + medium + Values + + xs + small + medium + large + xl + + Key + story_font_size + + + Title + Theme + Type + PSGroupSpecifier + + + Type + PSToggleSwitchSpecifier + Title + Follow system appearance + Key + theme_follow_system + DefaultValue + + + + Type + PSMultiValueSpecifier + Title + Current theme + Titles + + Light + Sepia + Medium + Dark + + DefaultValue + light + Values + + light + sepia + medium + dark + + Key + theme_style + + + Title + App badge + Type + PSGroupSpecifier + + + Type + PSMultiValueSpecifier + Title + Show unread count + Titles + + Off + Unread + Focus + Focus only + + DefaultValue + off + Values + + off + unread + focus + + Key + app_unread_badge + + + StringsTable + Root + Icon + AppearanceTemplate + + diff --git a/clients/ios/Resources/mac/Settings.bundle/AppearanceTemplate.png b/clients/ios/Resources/mac/Settings.bundle/AppearanceTemplate.png new file mode 100644 index 0000000000..6e78a2a52b Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/AppearanceTemplate.png differ diff --git a/clients/ios/Resources/mac/Settings.bundle/Note, this is the Mac app.txt b/clients/ios/Resources/mac/Settings.bundle/Note, this is the Mac app.txt new file mode 100644 index 0000000000..93499bddd0 --- /dev/null +++ b/clients/ios/Resources/mac/Settings.bundle/Note, this is the Mac app.txt @@ -0,0 +1 @@ +This is the settings for the Mac edition of the app. diff --git a/clients/ios/Resources/mac/Settings.bundle/Root.plist b/clients/ios/Resources/mac/Settings.bundle/Root.plist new file mode 100644 index 0000000000..dc02f5db00 --- /dev/null +++ b/clients/ios/Resources/mac/Settings.bundle/Root.plist @@ -0,0 +1,181 @@ + + + + + PreferenceSpecifiers + + + Type + PSChildPaneSpecifier + Title + Story List + File + StoryList + + + Type + PSChildPaneSpecifier + Title + Appearance + File + Appearance + + + Type + PSChildPaneSpecifier + Title + Advanced + File + Advanced + + + Title + Feed list + Type + PSGroupSpecifier + + + Type + PSMultiValueSpecifier + Title + Feed list order + Titles + + Alphabetical + Most used then alphabetical + + DefaultValue + title + Values + + title + usage + + Key + feed_list_sort_order + + + Type + PSToggleSwitchSpecifier + Title + Show feeds after being read + Key + show_feeds_after_being_read + DefaultValue + + + + Type + PSMultiValueSpecifier + Title + When opening app + WantUpdate + + Titles + + Show feed list + Open All Stories + + DefaultValue + everything + Values + + feeds + everything + + Key + app_opening + + + Title + Reading Stories + Type + PSGroupSpecifier + + + Type + PSToggleSwitchSpecifier + Title + Scroll horizontally + Key + scroll_stories_horizontally + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Show public comments + Key + show_public_comments + DefaultValue + + + + Type + PSMultiValueSpecifier + Title + Default browser + Titles + + System Default + Chrome + Opera Mini + Firefox + Edge + Brave + In-app browser + + DefaultValue + system + Values + + system + chrome + opera_mini + firefox + edge + brave + inapp + + Key + story_browser + + + FooterText + This setting only applies after the app is restarted. + Type + PSMultiValueSpecifier + Title + Play videos + Titles + + Inline + Full-screen + + DefaultValue + inline + Values + + inline + fullscreen + + Key + video_playback + + + Type + PSToggleSwitchSpecifier + Title + Show autoscroll + DefaultValue + + Key + story_autoscroll + + + StringsTable + Root + + diff --git a/clients/ios/Resources/mac/Settings.bundle/StoryList.plist b/clients/ios/Resources/mac/Settings.bundle/StoryList.plist new file mode 100644 index 0000000000..da498cfbce --- /dev/null +++ b/clients/ios/Resources/mac/Settings.bundle/StoryList.plist @@ -0,0 +1,321 @@ + + + + + PreferenceSpecifiers + + + Type + PSMultiValueSpecifier + Title + Story order + Titles + + Newest first + Oldest first + + DefaultValue + newest + Values + + newest + oldest + + Key + default_order + + + Type + PSMultiValueSpecifier + Title + Stories in a folder + Titles + + All stories + Unread only + + DefaultValue + unread + Values + + all + unread + + Key + default_folder_read_filter + + + Type + PSMultiValueSpecifier + Title + Stories in a site + Titles + + All stories + Unread only + + DefaultValue + all + Values + + all + unread + + Key + default_feed_read_filter + + + FooterText + The mark read options are always available via a long press on the mark read button in the stories list. + Type + PSMultiValueSpecifier + Title + Confirm mark read + Titles + + On folders and sites + On folders only + Never + + DefaultValue + folders + Values + + all + folders + never + + Key + default_confirm_read_filter + + + Type + PSMultiValueSpecifier + Title + After mark read + Titles + + Open the next site/folder + Stay on the feeds list + + DefaultValue + next + Values + + next + stay + + Key + after_mark_read + + + Type + PSMultiValueSpecifier + Title + When opening a site + Titles + + Open first story + Show stories + + DefaultValue + story + Values + + story + list + + Key + feed_opening + + + FooterText + You can change this setting in the Infrequent Site Stories view. + Type + PSMultiValueSpecifier + Title + Infrequent stories per month + Titles + + < 5 stories/month + < 15 stories/month + < 30 stories/month + < 60 stories/month + < 90 stories/month + + DefaultValue + 30 + Values + + 5 + 15 + 30 + 60 + 90 + + Key + infrequent_stories_per_month + + + Type + PSToggleSwitchSpecifier + Title + Show Infrequent Site Stories + DefaultValue + YES + Key + show_infrequent_site_stories + + + Type + PSToggleSwitchSpecifier + Title + Show Global Shared Stories + DefaultValue + YES + Key + show_global_shared_stories + + + Type + PSToggleSwitchSpecifier + Title + Mark stories read on scroll + DefaultValue + YES + Key + default_scroll_read_filter + + + Type + PSToggleSwitchSpecifier + Title + Show override mark read on scroll + DefaultValue + YES + Key + override_scroll_read_filter + + + Type + PSMultiValueSpecifier + Title + Column layout + Titles + + Automatic + Three columns + Two columns + One column + + DefaultValue + tile + Values + + auto + tile + displace + overlay + + Key + split_behavior + + + Type + PSMultiValueSpecifier + Title + Story titles layout + Titles + + Titles on left + Titles on top + Titles on bottom + Titles in grid + + DefaultValue + titles_on_left + Values + + titles_on_left + titles_on_top + titles_on_bottom + titles_in_grid + + Key + story_titles_position + + + Type + PSMultiValueSpecifier + Title + List style + Titles + + Standard + Experimental + + DefaultValue + standard + Values + + standard + experimental + + Key + story_titles_style + + + Type + PSMultiValueSpecifier + Title + Preview descriptions + Values + + title + short + medium + long + + Titles + + Title only + Short title and content + Medium title and content + Long title and content + + Key + story_list_preview_text_size + DefaultValue + medium + + + Type + PSMultiValueSpecifier + Title + Preview images + Titles + + None + Small Left + Large Left + Large Right + Small Right + + Values + + none + small_left + large_left + large_right + small_right + + Key + story_list_preview_images_size + DefaultValue + small_right + + + Icon + StoryListTemplate + + diff --git a/clients/ios/Resources/mac/Settings.bundle/StoryListTemplate.png b/clients/ios/Resources/mac/Settings.bundle/StoryListTemplate.png new file mode 100644 index 0000000000..59720a5e6f Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/StoryListTemplate.png differ diff --git a/clients/ios/Resources/mac/Settings.bundle/en.lproj/Root.strings b/clients/ios/Resources/mac/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 0000000000..8cd87b9d6b Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/en.lproj/Root.strings differ diff --git a/clients/ios/Resources/mac/Settings.bundle/theme_dark.png b/clients/ios/Resources/mac/Settings.bundle/theme_dark.png new file mode 100644 index 0000000000..57dc143e3a Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/theme_dark.png differ diff --git a/clients/ios/Resources/mac/Settings.bundle/theme_dark@2x.png b/clients/ios/Resources/mac/Settings.bundle/theme_dark@2x.png new file mode 100644 index 0000000000..1b29b335d2 Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/theme_dark@2x.png differ diff --git a/clients/ios/Resources/mac/Settings.bundle/theme_light.png b/clients/ios/Resources/mac/Settings.bundle/theme_light.png new file mode 100644 index 0000000000..b1470784fb Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/theme_light.png differ diff --git a/clients/ios/Resources/mac/Settings.bundle/theme_light@2x.png b/clients/ios/Resources/mac/Settings.bundle/theme_light@2x.png new file mode 100644 index 0000000000..52598bbdb7 Binary files /dev/null and b/clients/ios/Resources/mac/Settings.bundle/theme_light@2x.png differ diff --git a/clients/ios/Resources/original_button.png b/clients/ios/Resources/original_button.png index 966429fa52..500f82120a 100644 Binary files a/clients/ios/Resources/original_button.png and b/clients/ios/Resources/original_button.png differ diff --git a/clients/ios/Resources/original_button@2x.png b/clients/ios/Resources/original_button@2x.png index 49bf0cc1e5..2e6e610ae7 100644 Binary files a/clients/ios/Resources/original_button@2x.png and b/clients/ios/Resources/original_button@2x.png differ diff --git a/clients/ios/Widget Extension/WidgetExtension.swift b/clients/ios/Widget Extension/WidgetExtension.swift index 88ba7c09f9..c04ff68aaa 100644 --- a/clients/ios/Widget Extension/WidgetExtension.swift +++ b/clients/ios/Widget Extension/WidgetExtension.swift @@ -85,6 +85,8 @@ struct SimpleEntry: TimelineEntry { struct WidgetEntryView : View { var entry: Provider.Entry + @Environment(\.widgetRenderingMode) var renderingMode + @Environment(\.widgetContentMargins) var margins @Environment(\.colorScheme) var colorScheme @Environment(\.widgetFamily) private var family @@ -94,14 +96,19 @@ struct WidgetEntryView : View { var body: some View { ZStack { - Color("WidgetBackground") - .ignoresSafeArea() +// switch renderingMode { +// case .accented: +// case .fullColor: +// case .vibrant: +// break +// } if let error = entry.cache.error { Link(destination: URL(string: "newsblurwidget://?error=\(error)")!) { Text(message(for: error)) .font(.headline) - .foregroundColor(.secondary) + .foregroundStyle(.secondary) + .containerBackground(.fill, for: .widget) } } else { VStack(alignment: .leading, spacing: 0, content: { @@ -112,9 +119,19 @@ struct WidgetEntryView : View { Divider() } }) - .widgetURL(URL(string: "newsblurwidget://open")) + .padding(.top, 5) + .padding(.bottom, 5) + .containerBackground(for: .widget) { + Color("WidgetBackground") + } + .widgetURL(URL(string: "newsblurwidget://open")) } } +// .environment(\.colorScheme, colorScheme) + .containerBackground(for: .widget) { + Color("WidgetBackground") +// .ignoresSafeArea() + } } func message(for error: WidgetCacheError) -> String { @@ -142,6 +159,7 @@ struct WidgetExtension: Widget { .configurationDisplayName("NewsBlur") .description("The latest stories from NewsBlur.") .supportedFamilies([.systemMedium, .systemLarge]) + .contentMarginsDisabled() } } diff --git a/clients/ios/static/storyDetailView.css b/clients/ios/static/storyDetailView.css index 9c04227a53..ff3c965e1e 100644 --- a/clients/ios/static/storyDetailView.css +++ b/clients/ios/static/storyDetailView.css @@ -87,6 +87,54 @@ line-height: 2.0em; } +/** + * Mac Style + */ + +.NB-mac .NB-header { + padding: 1em 30px; +} + +.NB-mac .NB-header .NB-header-inner { + margin: 0px 0px; +} + +.NB-mac .NB-story { + padding: 20px 30px; +} + +.NB-mac .NB-share-inner-wrapper { + margin: 0 30px; +} + +.NB-mac#story_pane .NB-story-comments-public-teaser, +.NB-mac#story_pane .NB-story-comments-public-header, +.NB-mac#story_pane .NB-story-comments-friends-header, +.NB-mac#story_pane .NB-story-comments-shares-teaser { + padding-left: 30px; + padding-right: 30px; +} + +.NB-mac#story_pane .NB-story-comment { + padding: 0 30px 2px 110px; +} + +.NB-mac#story_pane .NB-story-comment .NB-user-avatar, +.NB-mac#story_pane .NB-story-comment .NB-user-avatar.NB-story-comment-reshare { + left: 26px; +} +.NB-mac#story_pane .NB-story-comment .NB-story-comment-reshares .NB-user-avatar { + left: 45px; +} + +.NB-mac#story_pane .NB-story-comment .NB-button-wrapper { + margin-top: -5px; +} + +.NB-mac .NB-button.NB-share-button a { + font-size: 11px; +} + /** * iPad Wide Style */ @@ -590,6 +638,11 @@ div + p { width: calc(100% + 24px) !important; } +.NB-mac .NB-story .NB-large-image { + margin-left: -30px !important; + width: calc(100% + 60px) !important; +} + .NB-ipad-narrow .NB-story .NB-large-image { margin-left: -30px !important; width: calc(100% + 60px) !important; diff --git a/config/gunicorn_conf.py b/config/gunicorn_conf.py index f7b0234752..866639a646 100644 --- a/config/gunicorn_conf.py +++ b/config/gunicorn_conf.py @@ -3,7 +3,7 @@ import psutil -GIGS_OF_MEMORY = psutil.virtual_memory().total/1024/1024/1024. +GIGS_OF_MEMORY = psutil.virtual_memory().total / 1024 / 1024 / 1024.0 NUM_CPUS = psutil.cpu_count() bind = "0.0.0.0:8000" @@ -27,12 +27,12 @@ if workers > 16: workers = 16 -if os.environ.get('DOCKERBUILD', False): +if os.environ.get("DOCKERBUILD", False): workers = 2 -prom_folder = '/srv/newsblur/.prom_cache' +prom_folder = "/srv/newsblur/.prom_cache" os.makedirs(prom_folder, exist_ok=True) -os.environ['PROMETHEUS_MULTIPROC_DIR'] = prom_folder +os.environ["PROMETHEUS_MULTIPROC_DIR"] = prom_folder for filename in os.listdir(prom_folder): file_path = os.path.join(prom_folder, filename) try: @@ -41,7 +41,7 @@ elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: - print('Failed to delete %s. Reason: %s' % (file_path, e)) + print("Failed to delete %s. Reason: %s" % (file_path, e)) from prometheus_client import multiprocess diff --git a/config/pystartup.py b/config/pystartup.py index 949523ccc6..ab64160a21 100644 --- a/config/pystartup.py +++ b/config/pystartup.py @@ -16,23 +16,37 @@ historyPath = os.path.expanduser("~/.pyhistory") historyTmp = os.path.expanduser("~/.pyhisttmp.py") -endMarkerStr= "# # # histDUMP # # #" +endMarkerStr = "# # # histDUMP # # #" -saveMacro= "import readline; readline.write_history_file('"+historyTmp+"'); \ +saveMacro = ( + "import readline; readline.write_history_file('" + + historyTmp + + "'); \ print '####>>>>>>>>>>'; print ''.join(filter(lambda lineP: \ - not lineP.strip().endswith('"+endMarkerStr+"'), \ - open('"+historyTmp+"').readlines())[:])+'####<<<<<<<<<<'"+endMarkerStr + not lineP.strip().endswith('" + + endMarkerStr + + "'), \ + open('" + + historyTmp + + "').readlines())[:])+'####<<<<<<<<<<'" + + endMarkerStr +) + +readline.parse_and_bind("tab: complete") +readline.parse_and_bind('\C-w: "' + saveMacro + '"') -readline.parse_and_bind('tab: complete') -readline.parse_and_bind('\C-w: "'+saveMacro+'"') def save_history(historyPath=historyPath, endMarkerStr=endMarkerStr): import readline + readline.write_history_file(historyPath) # Now filter out those line containing the saveMacro - lines= filter(lambda lineP, endMarkerStr=endMarkerStr: - not lineP.strip().endswith(endMarkerStr), open(historyPath).readlines()) - open(historyPath, 'w+').write(''.join(lines)) + lines = filter( + lambda lineP, endMarkerStr=endMarkerStr: not lineP.strip().endswith(endMarkerStr), + open(historyPath).readlines(), + ) + open(historyPath, "w+").write("".join(lines)) + if os.path.exists(historyPath): readline.read_history_file(historyPath) @@ -40,4 +54,4 @@ def save_history(historyPath=historyPath, endMarkerStr=endMarkerStr): atexit.register(save_history) del os, atexit, readline, rlcompleter, save_history, historyPath -del historyTmp, endMarkerStr, saveMacro \ No newline at end of file +del historyTmp, endMarkerStr, saveMacro diff --git a/docker/haproxy/haproxy.consul.cfg.j2 b/docker/haproxy/haproxy.consul.cfg.j2 index d3b5b372f6..d95023ad25 100644 --- a/docker/haproxy/haproxy.consul.cfg.j2 +++ b/docker/haproxy/haproxy.consul.cfg.j2 @@ -213,7 +213,7 @@ backend postgres {% for host in groups.postgres %} server {{host}} {{host}}.node.nyc1.consul:5579 {% endfor %} - server hdb-postgres-secondary hdb-redis-secondary.node.nyc1.consul:5579 + # server hdb-postgres-secondary hdb-redis-secondary.node.nyc1.consul:5579 backend mongo option httpchk GET /db_check/mongo diff --git a/flask_metrics/flask_metrics_haproxy.py b/flask_metrics/flask_metrics_haproxy.py index 7576584eef..1065945895 100644 --- a/flask_metrics/flask_metrics_haproxy.py +++ b/flask_metrics/flask_metrics_haproxy.py @@ -1,9 +1,10 @@ -from flask import Flask, render_template, Response -from newsblur_web import settings -import sentry_sdk -from sentry_sdk.integrations.flask import FlaskIntegration import requests +import sentry_sdk +from flask import Flask, Response, render_template from requests.auth import HTTPBasicAuth +from sentry_sdk.integrations.flask import FlaskIntegration + +from newsblur_web import settings if settings.FLASK_SENTRY_DSN is not None: sentry_sdk.init( @@ -19,22 +20,23 @@ STATUS_MAPPING = { - "UNK": 0, # unknown - "INI": 1, # initializing + "UNK": 0, # unknown + "INI": 1, # initializing "SOCKERR": 2, # socket error - "L4OK": 3, # check passed on layer 4, no upper layers testing enabled - "L4TOUT": 4, # layer 1-4 timeout - "L4CON": 5, # layer 1-4 connection problem, for example "Connection refused" (tcp rst) or "No route to host" (icmp) - "L6OK": 6, # check passed on layer 6 - "L6TOUT": 7, # layer 6 (SSL) timeout - "L6RSP": 8, # layer 6 invalid response - protocol error - "L7OK": 9, # check passed on layer 7 - "L7OKC": 10, # check conditionally passed on layer 7, for example 404 with disable-on-404 + "L4OK": 3, # check passed on layer 4, no upper layers testing enabled + "L4TOUT": 4, # layer 1-4 timeout + "L4CON": 5, # layer 1-4 connection problem, for example "Connection refused" (tcp rst) or "No route to host" (icmp) + "L6OK": 6, # check passed on layer 6 + "L6TOUT": 7, # layer 6 (SSL) timeout + "L6RSP": 8, # layer 6 invalid response - protocol error + "L7OK": 9, # check passed on layer 7 + "L7OKC": 10, # check conditionally passed on layer 7, for example 404 with disable-on-404 "L7TOUT": 11, # layer 7 (HTTP/SMTP) timeout - "L7RSP": 12, # layer 7 invalid response - protocol error - "L7STS": 13, # layer 7 response error, for example HTTP 5xx + "L7RSP": 12, # layer 7 invalid response - protocol error + "L7STS": 13, # layer 7 response error, for example HTTP 5xx } + def format_state_data(label, data): formatted_data = {} for k, v in data.items(): @@ -42,37 +44,37 @@ def format_state_data(label, data): formatted_data[k] = f'{label}{{servername="{k}"}} {STATUS_MAPPING[v.strip()]}' return formatted_data + def fetch_states(): - res = requests.get('https://newsblur.com:1936/;csv', auth=HTTPBasicAuth('gimmiestats', 'StatsGiver')) + res = requests.get("https://newsblur.com:1936/;csv", auth=HTTPBasicAuth("gimmiestats", "StatsGiver")) - lines = res.content.decode('utf-8').split('\n') + lines = res.content.decode("utf-8").split("\n") header_line = lines[0].split(",") - check_status_index = header_line.index('check_status') - servername_index = header_line.index('svname') + check_status_index = header_line.index("check_status") + servername_index = header_line.index("svname") data = {} backends = [line.split(",") for line in lines[1:]] for backend_data in backends: - if len(backend_data) <= check_status_index: continue - if len(backend_data) <= servername_index: continue - if backend_data[servername_index] in ['FRONTEND', 'BACKEND']: continue + if len(backend_data) <= check_status_index: + continue + if len(backend_data) <= servername_index: + continue + if backend_data[servername_index] in ["FRONTEND", "BACKEND"]: + continue backend_status = backend_data[check_status_index].replace("*", "") data[backend_data[servername_index]] = backend_status - + return data @app.route("/state/") def haproxy_state(): backends = fetch_states() - + formatted_data = format_state_data("haproxy_state", backends) - context = { - 'chart_name': 'haproxy_state', - 'chart_type': 'gauge', - 'data': formatted_data - } - html_body = render_template('prometheus_data.html', **context) + context = {"chart_name": "haproxy_state", "chart_type": "gauge", "data": formatted_data} + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") diff --git a/flask_metrics/flask_metrics_mongo.py b/flask_metrics/flask_metrics_mongo.py index 4eee7a501f..ebbad8ade4 100644 --- a/flask_metrics/flask_metrics_mongo.py +++ b/flask_metrics/flask_metrics_mongo.py @@ -17,10 +17,13 @@ if settings.DOCKERBUILD: connection = pymongo.MongoClient(f"mongodb://{settings.MONGO_DB['host']}") else: - connection = pymongo.MongoClient(f"mongodb://{settings.MONGO_DB['username']}:{settings.MONGO_DB['password']}@{settings.SERVER_NAME}.node.consul/?authSource=admin") + connection = pymongo.MongoClient( + f"mongodb://{settings.MONGO_DB['username']}:{settings.MONGO_DB['password']}@{settings.SERVER_NAME}.node.consul/?authSource=admin" + ) MONGO_HOST = settings.SERVER_NAME + @app.route("/objects/") def objects(): try: @@ -31,44 +34,44 @@ def objects(): return Response(f"Operation failure: {e}", 500) except pymongo.errors.NotMasterError as e: return Response(f"NotMaster error: {e}", 500) - data = dict(objects=stats['objects']) + data = dict(objects=stats["objects"]) formatted_data = {} for k, v in data.items(): formatted_data[k] = f'mongo_objects{{db="{MONGO_HOST}"}} {v}' context = { "data": formatted_data, - "chart_name": 'objects', - "chart_type": 'gauge', + "chart_name": "objects", + "chart_type": "gauge", } - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/mongo-replset-lag/") def repl_set_lag(): def _get_oplog_length(): - oplog = connection.rs.command('printReplicationInfo') - last_op = oplog.find({}, {'ts': 1}).sort([('$natural', -1)]).limit(1)[0]['ts'].time - first_op = oplog.find({}, {'ts': 1}).sort([('$natural', 1)]).limit(1)[0]['ts'].time + oplog = connection.rs.command("printReplicationInfo") + last_op = oplog.find({}, {"ts": 1}).sort([("$natural", -1)]).limit(1)[0]["ts"].time + first_op = oplog.find({}, {"ts": 1}).sort([("$natural", 1)]).limit(1)[0]["ts"].time oplog_length = last_op - first_op return oplog_length def _get_max_replication_lag(): PRIMARY_STATE = 1 SECONDARY_STATE = 2 - status = connection.admin.command('replSetGetStatus') - members = status['members'] + status = connection.admin.command("replSetGetStatus") + members = status["members"] primary_optime = None oldest_secondary_optime = None for member in members: - member_state = member['state'] - optime = member['optime'] + member_state = member["state"] + optime = member["optime"] if member_state == PRIMARY_STATE: - primary_optime = optime['ts'].time + primary_optime = optime["ts"].time elif member_state == SECONDARY_STATE: - if not oldest_secondary_optime or optime['ts'].time < oldest_secondary_optime: - oldest_secondary_optime = optime['ts'].time + if not oldest_secondary_optime or optime["ts"].time < oldest_secondary_optime: + oldest_secondary_optime = optime["ts"].time if not primary_optime or not oldest_secondary_optime: raise Exception("Replica set is not healthy") @@ -86,7 +89,7 @@ def _get_max_replication_lag(): return Response(f"Operation failure: {e}", 500) except pymongo.errors.NotMasterError as e: return Response(f"NotMaster error: {e}", 500) - + formatted_data = {} for k, v in oplog_length.items(): formatted_data[k] = f'mongo_oplog{{type="length", db="{MONGO_HOST}"}} {v}' @@ -95,10 +98,10 @@ def _get_max_replication_lag(): context = { "data": formatted_data, - "chart_name": 'oplog_metrics', - "chart_type": 'gauge', + "chart_name": "oplog_metrics", + "chart_type": "gauge", } - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @@ -112,52 +115,49 @@ def size(): return Response(f"Operation failure: {e}", 500) except pymongo.errors.NotMasterError as e: return Response(f"NotMaster error: {e}", 500) - data = dict(size=stats['fsUsedSize']) + data = dict(size=stats["fsUsedSize"]) formatted_data = {} for k, v in data.items(): formatted_data[k] = f'mongo_db_size{{db="{MONGO_HOST}"}} {v}' context = { "data": formatted_data, - "chart_name": 'db_size_bytes', - "chart_type": 'gauge', + "chart_name": "db_size_bytes", + "chart_type": "gauge", } - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/ops/") def ops(): try: - status = connection.admin.command('serverStatus') + status = connection.admin.command("serverStatus") except pymongo.errors.ServerSelectionTimeoutError as e: return Response(f"Server selection timeout: {e}", 500) except pymongo.errors.OperationFailure as e: return Response(f"Operation failure: {e}", 500) except pymongo.errors.NotMasterError as e: return Response(f"NotMaster error: {e}", 500) - data = dict( - (q, status["opcounters"][q]) - for q in status['opcounters'].keys() - ) - + data = dict((q, status["opcounters"][q]) for q in status["opcounters"].keys()) + formatted_data = {} for k, v in data.items(): formatted_data[k] = f'mongo_ops{{type="{k}", db="{MONGO_HOST}"}} {v}' - + context = { "data": formatted_data, - "chart_name": 'ops', - "chart_type": 'gauge', + "chart_name": "ops", + "chart_type": "gauge", } - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/page-faults/") def page_faults(): try: - status = connection.admin.command('serverStatus') + status = connection.admin.command("serverStatus") except pymongo.errors.ServerSelectionTimeoutError as e: return Response(f"Server selection timeout: {e}", 500) except pymongo.errors.OperationFailure as e: @@ -165,7 +165,7 @@ def page_faults(): except pymongo.errors.NotMasterError as e: return Response(f"NotMaster error: {e}", 500) try: - value = status['extra_info']['page_faults'] + value = status["extra_info"]["page_faults"] except KeyError: value = "U" data = dict(page_faults=value) @@ -175,37 +175,34 @@ def page_faults(): context = { "data": formatted_data, - "chart_name": 'page_faults', - "chart_type": 'counter', + "chart_name": "page_faults", + "chart_type": "counter", } - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/page-queues/") def page_queues(): try: - status = connection.admin.command('serverStatus') + status = connection.admin.command("serverStatus") except pymongo.errors.ServerSelectionTimeoutError as e: return Response(f"Server selection timeout: {e}", 500) except pymongo.errors.OperationFailure as e: return Response(f"Operation failure: {e}", 500) except pymongo.errors.NotMasterError as e: return Response(f"NotMaster error: {e}", 500) - data = dict( - (q, status["globalLock"]["currentQueue"][q]) - for q in ("readers", "writers") - ) + data = dict((q, status["globalLock"]["currentQueue"][q]) for q in ("readers", "writers")) formatted_data = {} for k, v in data.items(): formatted_data[k] = f'mongo_page_queues{{type="{k}", db="{MONGO_HOST}"}} {v}' context = { "data": formatted_data, - "chart_name": 'queues', - "chart_type": 'gauge', + "chart_name": "queues", + "chart_type": "gauge", } - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") diff --git a/flask_metrics/flask_metrics_redis.py b/flask_metrics/flask_metrics_redis.py index 44bb4fb147..21322433f2 100644 --- a/flask_metrics/flask_metrics_redis.py +++ b/flask_metrics/flask_metrics_redis.py @@ -15,14 +15,14 @@ app = Flask(__name__) INSTANCES = { - 'db-redis-session': settings.REDIS_SESSIONS, - 'db-redis-story': settings.REDIS_STORY, - 'db-redis-pubsub': settings.REDIS_PUBSUB, - 'db-redis-user': settings.REDIS_USER, + "db-redis-session": settings.REDIS_SESSIONS, + "db-redis-story": settings.REDIS_STORY, + "db-redis-pubsub": settings.REDIS_PUBSUB, + "db-redis-user": settings.REDIS_USER, } -class RedisMetric(object): +class RedisMetric(object): def __init__(self, title, fields): self.title = title self.fields = fields @@ -36,17 +36,17 @@ def redis_servers_stats(self): if not settings.DOCKERBUILD and instance not in settings.SERVER_NAME: continue self.host = f"{settings.SERVER_NAME}.node.nyc1.consul" - if instance == 'db-redis-session': - self.port = redis_config.get('port', settings.REDIS_SESSION_PORT) - elif instance == 'db-redis-story': - self.port = redis_config.get('port', settings.REDIS_STORY_PORT) - elif instance == 'db-redis-pubsub': - self.port = redis_config.get('port', settings.REDIS_PUBSUB_PORT) - elif instance == 'db-redis-user': - self.port = redis_config.get('port', settings.REDIS_USER_PORT) + if instance == "db-redis-session": + self.port = redis_config.get("port", settings.REDIS_SESSION_PORT) + elif instance == "db-redis-story": + self.port = redis_config.get("port", settings.REDIS_STORY_PORT) + elif instance == "db-redis-pubsub": + self.port = redis_config.get("port", settings.REDIS_PUBSUB_PORT) + elif instance == "db-redis-user": + self.port = redis_config.get("port", settings.REDIS_USER_PORT) stats = self.get_info() yield instance, stats - + def execute(self): data = {} for instance, stats in self.redis_servers_stats(): @@ -61,136 +61,154 @@ def execute(self): return data def format_data(self, data): - label = self.fields[0][1]['label'] + label = self.fields[0][1]["label"] formatted_data = {} for k, v in data.items(): formatted_data[k] = f'{label}{{db="{k}"}} {v[self.fields[0][0]]}' return formatted_data - + def get_db_size_data(self): data = {} for instance, stats in self.redis_servers_stats(): - dbs = [stat for stat in stats.keys() if stat.startswith('db')] + dbs = [stat for stat in stats.keys() if stat.startswith("db")] for db in dbs: - data[f'{instance}-{db}'] = f'redis_size{{db="{db}"}} {stats[db]["keys"]}' + data[f"{instance}-{db}"] = f'redis_size{{db="{db}"}} {stats[db]["keys"]}' return data def get_context(self): - if self.fields[0][0] == 'size': + if self.fields[0][0] == "size": formatted_data = self.get_db_size_data() else: values = self.execute() formatted_data = self.format_data(values) context = { "data": formatted_data, - "chart_name": self.fields[0][1]['label'], - "chart_type": self.fields[0][1]['type'], + "chart_name": self.fields[0][1]["label"], + "chart_type": self.fields[0][1]["type"], } return context - + @property def response_body(self): context = self.get_context() - return render_template('prometheus_data.html', **context) + return render_template("prometheus_data.html", **context) @app.route("/active-connections/") def active_connections(): conf = { - 'title': "Redis active connections", - 'fields': ( - ('connected_clients', dict( - label="redis_active_connections", - type="gauge", - )), + "title": "Redis active connections", + "fields": ( + ( + "connected_clients", + dict( + label="redis_active_connections", + type="gauge", + ), + ), ), } redis_metric = RedisMetric(**conf) return Response(redis_metric.response_body, content_type="text/plain") + @app.route("/commands/") def commands(): conf = { - 'title': "Redis commands", - 'fields': ( - ('total_commands_processed', dict( - label="redis_commands", - type="gauge", - )), + "title": "Redis commands", + "fields": ( + ( + "total_commands_processed", + dict( + label="redis_commands", + type="gauge", + ), + ), ), } redis_metric = RedisMetric(**conf) context = redis_metric.get_context() - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/connects/") def connects(): conf = { - 'title': "Redis connections per second", - 'fields': ( - ('total_connections_received', dict( - label="redis_connects", - type="counter", - )), + "title": "Redis connections per second", + "fields": ( + ( + "total_connections_received", + dict( + label="redis_connects", + type="counter", + ), + ), ), } redis_metric = RedisMetric(**conf) context = redis_metric.get_context() - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/size/") def size(): - conf = { - 'title': "Redis DB size", - 'fields': ( - ('size', dict( - label="redis_size", - type="gauge", - )), - ) + "title": "Redis DB size", + "fields": ( + ( + "size", + dict( + label="redis_size", + type="gauge", + ), + ), + ), } redis_metric = RedisMetric(**conf) context = redis_metric.get_context() - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/memory/") def memory(): conf = { - 'title': "Redis Total Memory", - 'fields': ( - ('total_system_memory', dict( - label="redis_memory", - type="gauge", - )), + "title": "Redis Total Memory", + "fields": ( + ( + "total_system_memory", + dict( + label="redis_memory", + type="gauge", + ), + ), ), } redis_metric = RedisMetric(**conf) context = redis_metric.get_context() - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") @app.route("/used-memory/") def memory_used(): conf = { - 'title': "Redis Used Memory", - 'fields': ( - ('used_memory', dict( - label="redis_used_memory", - type="gauge", - )), + "title": "Redis Used Memory", + "fields": ( + ( + "used_memory", + dict( + label="redis_used_memory", + type="gauge", + ), + ), ), } redis_metric = RedisMetric(**conf) context = redis_metric.get_context() - html_body = render_template('prometheus_data.html', **context) + html_body = render_template("prometheus_data.html", **context) return Response(html_body, content_type="text/plain") diff --git a/flask_monitor/db_monitor.py b/flask_monitor/db_monitor.py index eb95dd44b3..b37396bb8a 100644 --- a/flask_monitor/db_monitor.py +++ b/flask_monitor/db_monitor.py @@ -22,17 +22,18 @@ PRIMARY_STATE = 1 SECONDARY_STATE = 2 + @app.route("/db_check/postgres") def db_check_postgres(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) connect_params = "dbname='%s' user='%s' password='%s' host='%s' port='%s'" % ( - settings.DATABASES['default']['NAME'], - settings.DATABASES['default']['USER'], - settings.DATABASES['default']['PASSWORD'], - f'{settings.SERVER_NAME}.node.nyc1.consul', - settings.DATABASES['default']['PORT'], + settings.DATABASES["default"]["NAME"], + settings.DATABASES["default"]["USER"], + settings.DATABASES["default"]["PASSWORD"], + f"{settings.SERVER_NAME}.node.nyc1.consul", + settings.DATABASES["default"]["PORT"], ) try: conn = psycopg2.connect(connect_params) @@ -45,28 +46,30 @@ def db_check_postgres(): rows = cur.fetchall() for row in rows: return str(row[0]) - + abort(Response("No rows found", 504)) + @app.route("/db_check/mysql") def db_check_mysql(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) connect_params = "dbname='%s' user='%s' password='%s' host='%s' port='%s'" % ( - settings.DATABASES['default']['NAME'], - settings.DATABASES['default']['USER'], - settings.DATABASES['default']['PASSWORD'], - settings.DATABASES['default']['HOST'], - settings.DATABASES['default']['PORT'], + settings.DATABASES["default"]["NAME"], + settings.DATABASES["default"]["USER"], + settings.DATABASES["default"]["PASSWORD"], + settings.DATABASES["default"]["HOST"], + settings.DATABASES["default"]["PORT"], ) try: - - conn = pymysql.connect(host='mysql', - port=settings.DATABASES['default']['PORT'], - user=settings.DATABASES['default']['USER'], - passwd=settings.DATABASES['default']['PASSWORD'], - db=settings.DATABASES['default']['NAME']) + conn = pymysql.connect( + host="mysql", + port=settings.DATABASES["default"]["PORT"], + user=settings.DATABASES["default"]["USER"], + passwd=settings.DATABASES["default"]["PASSWORD"], + db=settings.DATABASES["default"]["NAME"], + ) except: print(" ---> Mysql can't connect to the database: %s" % connect_params) abort(Response("Can't connect to mysql db", 503)) @@ -76,17 +79,20 @@ def db_check_mysql(): rows = cur.fetchall() for row in rows: return str(row[0]) - + abort(Response("No rows found", 504)) + @app.route("/db_check/mongo") def db_check_mongo(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) try: # The `mongo` hostname below is a reference to the newsblurnet docker network, where 172.18.0.0/16 is defined - client = pymongo.MongoClient(f"mongodb://{settings.MONGO_DB['username']}:{settings.MONGO_DB['password']}@{settings.SERVER_NAME}.node.nyc1.consul/?authSource=admin") + client = pymongo.MongoClient( + f"mongodb://{settings.MONGO_DB['username']}:{settings.MONGO_DB['password']}@{settings.SERVER_NAME}.node.nyc1.consul/?authSource=admin" + ) db = client.newsblur except: abort(Response("Can't connect to db", 503)) @@ -98,25 +104,25 @@ def db_check_mongo(): except pymongo.errors.ServerSelectionTimeoutError: abort(Response("Server selection timeout", 503)) except pymongo.errors.OperationFailure as e: - if 'Authentication failed' in str(e): + if "Authentication failed" in str(e): abort(Response("Auth failed", 506)) abort(Response("Operation Failure", 507)) - + if not stories: abort(Response("No stories", 510)) - - status = client.admin.command('replSetGetStatus') - members = status['members'] + + status = client.admin.command("replSetGetStatus") + members = status["members"] primary_optime = None oldest_secondary_optime = None for member in members: - member_state = member['state'] - optime = member['optime'] + member_state = member["state"] + optime = member["optime"] if member_state == PRIMARY_STATE: - primary_optime = optime['ts'].time + primary_optime = optime["ts"].time elif member_state == SECONDARY_STATE: - if not oldest_secondary_optime or optime['ts'].time < oldest_secondary_optime: - oldest_secondary_optime = optime['ts'].time + if not oldest_secondary_optime or optime["ts"].time < oldest_secondary_optime: + oldest_secondary_optime = optime["ts"].time if not primary_optime or not oldest_secondary_optime: abort(Response("No optime", 511)) @@ -126,43 +132,47 @@ def db_check_mongo(): return str(stories) + @app.route("/db_check/mongo_analytics") def db_check_mongo_analytics(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) try: - client = pymongo.MongoClient(f"mongodb://{settings.MONGO_ANALYTICS_DB['username']}:{settings.MONGO_ANALYTICS_DB['password']}@{settings.SERVER_NAME}.node.consul/?authSource=admin") + client = pymongo.MongoClient( + f"mongodb://{settings.MONGO_ANALYTICS_DB['username']}:{settings.MONGO_ANALYTICS_DB['password']}@{settings.SERVER_NAME}.node.consul/?authSource=admin" + ) db = client.nbanalytics except: abort(Response("Can't connect to db", 503)) - + try: fetches = db.feed_fetches.estimated_document_count() except (pymongo.errors.NotMasterError, pymongo.errors.ServerSelectionTimeoutError): abort(Response("Not Master / Server selection timeout", 504)) except pymongo.errors.OperationFailure as e: - if 'Authentication failed' in str(e): + if "Authentication failed" in str(e): abort(Response("Auth failed", 505)) abort(Response("Operation failure", 506)) - + if not fetches: abort(Response("No fetches in data", 510)) - + return str(fetches) + @app.route("/db_check/redis_user") def db_check_redis_user(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) - port = request.args.get('port', settings.REDIS_USER_PORT) + port = request.args.get("port", settings.REDIS_USER_PORT) try: - r = redis.Redis(f'{settings.SERVER_NAME}.node.nyc1.consul', port=port, db=0) + r = redis.Redis(f"{settings.SERVER_NAME}.node.nyc1.consul", port=port, db=0) except: abort(Response("Can't connect to db", 503)) - + try: randkey = r.randomkey() except: @@ -173,18 +183,19 @@ def db_check_redis_user(): else: abort(Response("Can't find a randomkey", 505)) + @app.route("/db_check/redis_story") -def db_check_redis_story(): - if request.args.get('consul') == '1': +def db_check_redis_story(): + if request.args.get("consul") == "1": return str(1) - port = request.args.get('port', settings.REDIS_STORY_PORT) - + port = request.args.get("port", settings.REDIS_STORY_PORT) + try: - r = redis.Redis(f'{settings.SERVER_NAME}.node.nyc1.consul', port=port, db=1) + r = redis.Redis(f"{settings.SERVER_NAME}.node.nyc1.consul", port=port, db=1) except: abort(Response("Can't connect to db", 503)) - + try: randkey = r.randomkey() except: @@ -195,18 +206,19 @@ def db_check_redis_story(): else: abort(Response("Can't find a randomkey", 505)) + @app.route("/db_check/redis_sessions") def db_check_redis_sessions(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) - port = request.args.get('port', settings.REDIS_SESSION_PORT) + port = request.args.get("port", settings.REDIS_SESSION_PORT) try: - r = redis.Redis(f'{settings.SERVER_NAME}.node.nyc1.consul', port=port, db=5) + r = redis.Redis(f"{settings.SERVER_NAME}.node.nyc1.consul", port=port, db=5) except: abort(Response("Can't connect to db", 503)) - + try: randkey = r.randomkey() except: @@ -217,18 +229,19 @@ def db_check_redis_sessions(): else: abort(Response("Can't find a randomkey", 505)) + @app.route("/db_check/redis_pubsub") def db_check_redis_pubsub(): - if request.args.get('consul') == '1': + if request.args.get("consul") == "1": return str(1) - port = request.args.get('port', settings.REDIS_PUBSUB_PORT) + port = request.args.get("port", settings.REDIS_PUBSUB_PORT) try: - r = redis.Redis(f'{settings.SERVER_NAME}.node.nyc1.consul', port=port, db=1) + r = redis.Redis(f"{settings.SERVER_NAME}.node.nyc1.consul", port=port, db=1) except: abort(Response("Can't connect to db", 503)) - + try: pubsub_numpat = r.pubsub_numpat() except: @@ -239,17 +252,18 @@ def db_check_redis_pubsub(): else: abort(Response("Can't find a pubsub_numpat", 505)) + @app.route("/db_check/elasticsearch") def db_check_elasticsearch(): try: conn = elasticsearch.Elasticsearch("elasticsearch") except: abort(Response("Can't connect to db", 503)) - - if request.args.get('consul') == '1': + + if request.args.get("consul") == "1": return str(1) - - if conn.indices.exists('feeds-index'): + + if conn.indices.exists("feeds-index"): return str("Index exists, but didn't try search") # query = pyes.query.TermQuery("title", "daring fireball") # results = conn.search(query=query, size=1, doc_types=['feeds-type'], sort="num_subscribers:desc") @@ -260,6 +274,7 @@ def db_check_elasticsearch(): else: abort(Response("Couldn't find feeds-index", 504)) + if __name__ == "__main__": print(" ---> Starting NewsBlur DB monitor flask server...") app.run(host="0.0.0.0", port=5579) diff --git a/manage.py b/manage.py index 8ff26d71aa..0261a9b88c 100755 --- a/manage.py +++ b/manage.py @@ -8,4 +8,3 @@ from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) - diff --git a/media/css/bookmarklet/bookmarklet.css b/media/css/bookmarklet/bookmarklet.css index 866c183a4b..1c9c002b76 100644 --- a/media/css/bookmarklet/bookmarklet.css +++ b/media/css/bookmarklet/bookmarklet.css @@ -5,10 +5,12 @@ #simplemodal-container { padding: 20px; } + .NB-modal { padding: 0; } -.NB-modal p, + +.NB-modal p, .NB-modal ol, .NB-modal ul, .NB-modal li, @@ -32,18 +34,22 @@ border-right: none !important; text-transform: none !important; } + .NB-modal ol { list-style: decimal outside none !important; margin-left: 24px !important; } + .NB-modal ul { list-style: disc outside none !important; margin-left: 24px !important; } + .NB-modal p:first-child { margin-top: 0; } -.NB-modal p, + +.NB-modal p, .NB-modal ol, .NB-modal ul, .NB-modal li, @@ -64,22 +70,27 @@ font-size: 24px !important; margin: 30px 0 24px !important; } + .NB-modal h2 { font-size: 18px !important; margin: 24px 0 18px !important; } + .NB-modal h3 { font-size: 15px !important; margin: 18px 0 12px !important; } + .NB-modal h4 { font-size: 12px !important; margin: 12px 0 12px !important; } + .NB-modal h5 { font-size: 11px !important; margin: 12px 0 12px !important; } + .NB-modal h6 { font-size: 10px !important; margin: 12px 0 12px !important; @@ -97,11 +108,12 @@ /* ====================== */ .NB-bookmarklet { - font-family: 'Lucida Sans', 'Lucida Grande', Verdana, Arial, Helvetica, sans-serif; - font-size: 12px; - text-align: left; - overflow: hidden; + font-family: 'Lucida Sans', 'Lucida Grande', Verdana, Arial, Helvetica, sans-serif; + font-size: 12px; + text-align: left; + overflow: hidden; } + .NB-bookmarklet .NB-bookmarklet-main { width: 514px; overflow: hidden; @@ -109,6 +121,7 @@ border-left: 2px solid #F0F0F0; margin: 24px 0 0; } + .NB-bookmarklet .NB-bookmarklet-side { padding: 0 12px; position: relative; @@ -130,6 +143,7 @@ font-weight: bold; overflow: hidden; } + .NB-bookmarklet .NB-subscribe-feed img { width: 16px; height: 16px; @@ -137,107 +151,125 @@ vertical-align: bottom; margin: 11px 6px 0 0; } + .NB-bookmarklet .NB-subscribe-feed-title { margin: 12px 0 0; } .NB-bookmarklet .NB-modal-information { - float: right; - margin-top: 10px; - text-transform: uppercase; - color: #909090; - font-size: 10px; + float: right; + margin-top: 10px; + text-transform: uppercase; + color: #909090; + font-size: 10px; } + .NB-bookmarklet .NB-bookmarklet-folder-container { - margin: 12px 0 0; + margin: 12px 0 0; } + .NB-bookmarklet .NB-bookmarklet-folder-container .NB-bookmarklet-folder-label { - float: left; - margin: 2px 4px 0 0; + float: left; + margin: 2px 4px 0 0; } + .NB-bookmarklet .NB-bookmarklet-folder-container .NB-folders { - font-size: 14px; + font-size: 14px; } + .NB-bookmarklet .NB-bookmarklet-folder-add-button { - float: left; - margin: 2px 4px 0 0; - cursor: pointer; - width: 16px; - height: 16px; + float: left; + margin: 2px 4px 0 0; + cursor: pointer; + width: 16px; + height: 16px; } + .NB-bookmarklet .NB-bookmarklet-new-folder-container { - display: none; - clear: both; - overflow: hidden; - padding-left: 24px; + display: none; + clear: both; + overflow: hidden; + padding-left: 24px; } + .NB-bookmarklet .NB-bookmarklet-folder-new { - float: left; - padding: 2px; - border: 1px solid #505050; - font-size: 11px; - line-height: 13px; - margin: 2px 0 0; - width: 156px; + float: left; + padding: 2px; + border: 1px solid #505050; + font-size: 11px; + line-height: 13px; + margin: 2px 0 0; + width: 156px; } + .NB-bookmarklet .NB-bookmarklet-folder-new-label { - margin: 8px 4px 0 0; - float: left; - height: 8px; - width: 8px; + margin: 8px 4px 0 0; + float: left; + height: 8px; + width: 8px; } + .NB-bookmarklet .NB-folders { - width: 174px; + width: 174px; } + .NB-bookmarklet .NB-modal-submit { - margin: 12px 0 8px; - padding: 0 0 6px; - clear: both; - overflow: hidden; + margin: 12px 0 8px; + padding: 0 0 6px; + clear: both; + overflow: hidden; } + .NB-bookmarklet .NB-modal-submit-button { - clear: both; - padding: 0 16px; - line-height: 24px; - text-align: center; - text-shadow: none; - font-weight: normal; + clear: both; + padding: 0 16px; + line-height: 24px; + text-align: center; + text-shadow: none; + font-weight: normal; } + .NB-bookmarklet .NB-modal-submit-button.NB-disabled:hover, .NB-bookmarklet .NB-modal-submit-button.NB-disabled:active { cursor: default; } + .NB-bookmarklet .NB-modal-submit-button:hover, .NB-bookmarklet .NB-modal-submit-button:active { font-weight: normal; } + .NB-bookmarklet .NB-modal-submit-button img { - margin: 4px 6px 0 0; - width: 16px; - height: 16px; - float: left; - vertical-align: top; + margin: 4px 6px 0 0; + width: 16px; + height: 16px; + float: left; + vertical-align: top; } + .NB-bookmarklet .NB-bookmarklet-button-subscribe { width: 210px; } + .NB-bookmarklet .NB-bookmarklet-error { - margin: 0; - padding: 0 12px 0 24px; - line-height: 16px; - font-weight: bold; - color: #701040; - position: relative; + margin: 0; + padding: 0 12px 0 24px; + line-height: 16px; + font-weight: bold; + color: #701040; + position: relative; } + .NB-bookmarklet .NB-bookmarklet-error img { - position: absolute; - left: 0; - top: 0; - width: 16px; - height: 16px; - vertical-align: top; - margin: -2px 6px 0 0; + position: absolute; + left: 0; + top: 0; + width: 16px; + height: 16px; + vertical-align: top; + margin: -2px 6px 0 0; } + .NB-bookmarklet .NB-error-invalid { padding: 12px 12px; font-size: 13px; @@ -246,6 +278,7 @@ background-color: #690F0F; border-radius: 8px; } + .NB-bookmarklet .NB-error-invalid a { color: #9FC6F5; text-decoration: underline; @@ -264,6 +297,7 @@ background-color: #F6F6F6; line-height: 24px; } + .NB-bookmarklet .NB-bookmarklet-page-content-wrapper { max-height: 340px; overflow-y: scroll; @@ -271,41 +305,48 @@ margin: 12px 0 0; background-color: white; } + .NB-bookmarklet .NB-bookmarklet-page-content { border-top: 1px solid #F0F0F0; border-bottom: 1px solid #F0F0F0; } + .NB-bookmarklet .NB-bookmarklet-page-comment { margin-top: 12px; -/* border-top: 1px solid #F0F0F0;*/ + /* border-top: 1px solid #F0F0F0;*/ padding: 12px 0 0 24px; position: relative; overflow: hidden; } + .NB-bookmarklet .NB-bookmarklet-comment-photo { margin: 0 12px 0 0; float: left; } + .NB-bookmarklet .NB-bookmarklet-comment-photo img { border-radius: 6px; width: 48px; height: 48px; } + .NB-bookmarklet .NB-bookmarklet-comment-input { float: left; } + .NB-bookmarklet .NB-bookmarklet-comment-input textarea { width: 160px; border: 1px solid #E0E0E0; padding: 6px; margin: 0; - font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif !important; + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif !important; color: #404040; font-size: 13px; height: 98px; background-color: white; display: block; } + .NB-bookmarklet .NB-bookmarklet-comment-submit, .NB-bookmarklet .NB-bookmarklet-save-button { clear: both; @@ -313,25 +354,31 @@ padding: 8px 0; line-height: 16px; } + .NB-bookmarklet .NB-bookmarklet-comment-submit { width: 160px; } + .NB-bookmarklet .NB-bookmarklet-save-button { margin-left: 0; width: 220px; } + .NB-bookmarklet .NB-bookmarklet-comment-submit .NB-bookmarklet-accept, .NB-bookmarklet .NB-bookmarklet-save-button .NB-bookmarklet-accept { padding: 0 12px; } + .NB-bookmarklet .NB-bookmarklet-comment-submit .NB-bookmarklet-accept img, .NB-bookmarklet .NB-bookmarklet-save-button .NB-bookmarklet-accept img { vertical-align: top; margin: 0; } + .NB-bookmarklet .NB-bookmarklet-comment-container { overflow: hidden; } + .NB-bookmarklet .NB-bookmarklet-comment-separator { float: left; width: 1px; @@ -339,20 +386,23 @@ background-color: #e0e0e0; margin: 0 12px 0 0; } + .NB-bookmarklet .NB-bookmarklet-user-tags select { width: 220px; height: 72px; margin: 0 12px 0 0; -} +} .NB-bookmarklet .NB-bookmarklet-add-tag { width: 220px; margin: 4px 12px 0 0; } + .NB-bookmarklet-submit-left, .NB-bookmarklet-submit-right { float: left; } + /* =============== */ /* = Side Layout = */ /* =============== */ @@ -363,9 +413,11 @@ left: 0; width: 100%; } + .NB-bookmarklet .NB-bookmarklet-side-subscribe { left: 100%; } + .NB-bookmarklet .NB-bookmarklet-side-loading { text-align: center; font-size: 14px; @@ -376,24 +428,37 @@ padding: 48px 0 0; height: 400px; } + .NB-bookmarklet .NB-subscribe-loader { margin: 0 auto; width: 16px; height: 16px; - -webkit-animation: -webkit-slow-spin 4s infinite linear; - -moz-animation-duration: 4s; - -moz-animation-name: -moz-slow-spin; - -moz-animation-iteration-count: infinite; - -moz-animation-timing-function: linear; + -webkit-animation: -webkit-slow-spin 4s infinite linear; + -moz-animation-duration: 4s; + -moz-animation-name: -moz-slow-spin; + -moz-animation-iteration-count: infinite; + -moz-animation-timing-function: linear; } + @-webkit-keyframes -webkit-slow-spin { - from {-webkit-transform: rotate(0deg)} - to {-webkit-transform: rotate(360deg)} + from { + -webkit-transform: rotate(0deg) + } + + to { + -webkit-transform: rotate(360deg) + } +} + +@-moz-keyframes -moz-slow-spin { + from { + -moz-transform: rotate(0deg) + } + + to { + -moz-transform: rotate(360deg) + } } -@-moz-keyframes -moz-slow-spin { - from {-moz-transform: rotate(0deg)} - to {-moz-transform: rotate(360deg)} -} .NB-bookmarklet .NB-subscribe-load-text { padding: 24px 24px; @@ -412,6 +477,7 @@ padding: 6px 6px 8px; background-color: #F5F5F5; } + .NB-bookmarklet .NB-bookmarklet-stories-title img { width: 16px; height: 16px; @@ -419,6 +485,7 @@ margin: 0 6px -2px 0; vertical-align: bottom; } + .NB-bookmarklet .NB-story { margin: 2px 6px 0 0px; padding: 6px 12px 4px 26px; @@ -431,14 +498,17 @@ text-transform: none; background-color: white; } + .NB-bookmarklet .NB-story:hover { cursor: pointer; background-color: #F2F5FE; } + .NB-bookmarklet .NB-story:active { cursor: pointer; background-color: #FEE5D6; } + .NB-bookmarklet .NB-story img { width: 16px; height: 16px; @@ -447,20 +517,24 @@ top: 6px; left: 4px; } + .NB-bookmarklet .NB-story-username { display: inline; font-weight: bold; } + .NB-bookmarklet .NB-story-title { display: inline; color: #405BA8; } + .NB-bookmarklet .NB-story-date { color: #A0A0A0; font-size: 10px; text-transform: uppercase; margin: 4px 0 0; } + .NB-bookmarklet .NB-story-comments { margin: 4px 0 0; font-size: 10px; @@ -478,9 +552,11 @@ float: none; width: auto; } + .NB-bookmarklet .NB-bookmarklet-comment-input textarea { width: auto; } + .NB-bookmarklet .NB-bookmarklet-side { width: 90%; } diff --git a/media/css/bookmarklet/reset.css b/media/css/bookmarklet/reset.css index a715b07334..c08432d5e6 100644 --- a/media/css/bookmarklet/reset.css +++ b/media/css/bookmarklet/reset.css @@ -22,8 +22,9 @@ Source: http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ .NB-modal :focus { outline: 0; } + .NB-modal { line-height: 1; color: black; background: white; -} \ No newline at end of file +} diff --git a/media/css/circular/sass/social_page.scss b/media/css/circular/sass/social_page.scss index c51877738b..e2ae1ed9ff 100644 --- a/media/css/circular/sass/social_page.scss +++ b/media/css/circular/sass/social_page.scss @@ -1383,4 +1383,4 @@ footer { .NB-login { display: none; } -} \ No newline at end of file +} diff --git a/media/css/controls/chosen.css b/media/css/controls/chosen.css index 150b65e95c..591f2b5d78 100755 --- a/media/css/controls/chosen.css +++ b/media/css/controls/chosen.css @@ -6,6 +6,7 @@ vertical-align: middle; zoom: 1; } + .chzn-container .chzn-drop { background: #fff; border: 1px solid #aaa; @@ -13,16 +14,16 @@ position: absolute; top: 100%; left: -9999px; - -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15); - -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15); - box-shadow : 0 4px 5px rgba(0,0,0,.15); + -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + box-shadow: 0 4px 5px rgba(0, 0, 0, .15); z-index: 1010; width: 100%; - -moz-box-sizing : border-box; - -ms-box-sizing : border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; -webkit-box-sizing: border-box; - -khtml-box-sizing : border-box; - box-sizing : border-box; + -khtml-box-sizing: border-box; + box-sizing: border-box; } .chzn-container.chzn-with-drop .chzn-drop { @@ -38,17 +39,17 @@ background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); background-image: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); background-image: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); - background-image: linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); + background-image: linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); -webkit-border-radius: 5px; - -moz-border-radius : 5px; - border-radius : 5px; - -moz-background-clip : padding; + -moz-border-radius: 5px; + border-radius: 5px; + -moz-background-clip: padding; -webkit-background-clip: padding-box; - background-clip : padding-box; + background-clip: padding-box; border: 1px solid #aaaaaa; - -webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); - -moz-box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); - box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + -webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0, 0, 0, 0.1); display: block; overflow: hidden; white-space: nowrap; @@ -59,9 +60,11 @@ color: #444444; text-decoration: none; } + .chzn-container-single .chzn-default { color: #999; } + .chzn-container-single .chzn-single span { margin-right: 26px; display: block; @@ -71,6 +74,7 @@ -ms-text-overflow: ellipsis; text-overflow: ellipsis; } + .chzn-container-single .chzn-single-with-deselect span { margin-right: 38px; } @@ -85,12 +89,15 @@ font-size: 1px; background: url('/media/embed/reader/chosen-sprite.png') -42px 1px no-repeat; } + .chzn-container-single .chzn-single abbr:hover { background-position: -42px -10px; } + .chzn-container-single.chzn-disabled .chzn-single abbr:hover { background-position: -42px -10px; } + .chzn-container-single .chzn-single div { position: absolute; right: 0; @@ -99,12 +106,14 @@ height: 100%; width: 18px; } + .chzn-container-single .chzn-single div b { background: url('/media/embed/reader/chosen-sprite.png') no-repeat 0px 2px; display: block; width: 100%; height: 100%; } + .chzn-container-single .chzn-search { padding: 3px 4px; position: relative; @@ -112,6 +121,7 @@ white-space: nowrap; z-index: 1010; } + .chzn-container-single .chzn-search input { background: #fff url('/media/embed/reader/chosen-sprite.png') no-repeat 100% -20px; background: url('/media/embed/reader/chosen-sprite.png') no-repeat 100% -20px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); @@ -126,25 +136,28 @@ font-family: sans-serif; font-size: 1em; width: 100%; - -moz-box-sizing : border-box; - -ms-box-sizing : border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; -webkit-box-sizing: border-box; - -khtml-box-sizing : border-box; - box-sizing : border-box; + -khtml-box-sizing: border-box; + box-sizing: border-box; } + .chzn-container-single .chzn-drop { margin-top: -1px; -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius : 0 0 4px 4px; - border-radius : 0 0 4px 4px; - -moz-background-clip : padding; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; + -moz-background-clip: padding; -webkit-background-clip: padding-box; - background-clip : padding-box; + background-clip: padding-box; } + .chzn-container-single-nosearch .chzn-search { position: absolute; left: -9999px; } + /* @end */ /* @group Multi Chosen */ @@ -164,21 +177,24 @@ height: 1%; position: relative; width: 100%; - -moz-box-sizing : border-box; - -ms-box-sizing : border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; -webkit-box-sizing: border-box; - -khtml-box-sizing : border-box; - box-sizing : border-box; + -khtml-box-sizing: border-box; + box-sizing: border-box; } + .chzn-container-multi .chzn-choices li { float: left; list-style: none; } + .chzn-container-multi .chzn-choices .search-field { white-space: nowrap; margin: 0; padding: 0; } + .chzn-container-multi .chzn-choices .search-field input { color: #666; background: transparent !important; @@ -190,28 +206,30 @@ margin: 1px 0; outline: 0; -webkit-box-shadow: none; - -moz-box-shadow : none; - box-shadow : none; + -moz-box-shadow: none; + box-shadow: none; } + .chzn-container-multi .chzn-choices .search-field .default { color: #999; } + .chzn-container-multi .chzn-choices .search-choice { -webkit-border-radius: 3px; - -moz-border-radius : 3px; - border-radius : 3px; - -moz-background-clip : padding; + -moz-border-radius: 3px; + border-radius: 3px; + -moz-background-clip: padding; -webkit-background-clip: padding-box; - background-clip : padding-box; + background-clip: padding-box; background-color: #e4e4e4; background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); - -moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); - box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); + -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0, 0, 0, 0.05); color: #333; border: 1px solid #aaaaaa; line-height: 13px; @@ -220,6 +238,7 @@ position: relative; cursor: default; } + .chzn-container-multi .chzn-choices .search-choice.search-choice-disabled { background-color: #e4e4e4; background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); @@ -232,9 +251,11 @@ border: 1px solid #cccccc; padding-right: 5px; } + .chzn-container-multi .chzn-choices .search-choice-focus { background: #d4d4d4; } + .chzn-container-multi .chzn-choices .search-choice .search-choice-close { display: block; position: absolute; @@ -245,12 +266,15 @@ font-size: 1px; background: url('/media/embed/reader/chosen-sprite.png') -42px 1px no-repeat; } + .chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { background-position: -42px -10px; } + .chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { background-position: -42px -10px; } + /* @end */ /* @group Results */ @@ -263,10 +287,12 @@ overflow-y: auto; -webkit-overflow-scrolling: touch; } + .chzn-container-multi .chzn-results { margin: 0; padding: 0; } + .chzn-container .chzn-results li { display: none; line-height: 15px; @@ -274,6 +300,7 @@ margin: 0; list-style: none; } + .chzn-container .chzn-results .active-result { cursor: pointer; display: list-item; @@ -284,9 +311,11 @@ cursor: default; display: list-item; } + .chzn-container .chzn-results .disabled-result em { background: transparent; } + .chzn-container .chzn-results .highlighted { background-color: #3875d7; background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc)); @@ -296,157 +325,233 @@ background-image: linear-gradient(#3875d7 20%, #2a62bc 90%); color: #fff; } + .chzn-container .chzn-results li em { background: #feffde; font-style: normal; } + .chzn-container .chzn-results .highlighted em { background: transparent; } + .chzn-container .chzn-results .no-results { background: #f4f4f4; display: list-item; } + .chzn-container .chzn-results .group-result { cursor: default; color: #999; font-weight: bold; } + .chzn-container .chzn-results .group-option { padding-left: 15px; } + .chzn-container-multi .chzn-drop .result-selected { color: #ccc; cursor: default; display: list-item; } + .chzn-container-multi .chzn-drop .result-selected em { background: transparent; } + .chzn-container .chzn-results-scroll { background: white; margin: 0 4px; position: absolute; text-align: center; - width: 321px; /* This should by dynamic with js */ + width: 321px; + /* This should by dynamic with js */ z-index: 1; } + .chzn-container .chzn-results-scroll span { display: inline-block; height: 17px; text-indent: -5000px; width: 9px; } + .chzn-container .chzn-results-scroll-down { bottom: 0; } + .chzn-container .chzn-results-scroll-down span { background: url('/media/embed/reader/chosen-sprite.png') no-repeat -4px -3px; } + .chzn-container .chzn-results-scroll-up span { background: url('/media/embed/reader/chosen-sprite.png') no-repeat -22px -3px; } + /* @end */ /* @group Active */ .chzn-container-active .chzn-single { - -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); - -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); - box-shadow : 0 0 5px rgba(0,0,0,.3); + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + -moz-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + box-shadow: 0 0 5px rgba(0, 0, 0, .3); border: 1px solid #5897fb; } + .chzn-container-active.chzn-with-drop .chzn-single { border: 1px solid #aaa; -webkit-box-shadow: 0 1px 0 #fff inset; - -moz-box-shadow : 0 1px 0 #fff inset; - box-shadow : 0 1px 0 #fff inset; + -moz-box-shadow: 0 1px 0 #fff inset; + box-shadow: 0 1px 0 #fff inset; background-color: #eee; background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff)); background-image: -webkit-linear-gradient(top, #eeeeee 20%, #ffffff 80%); background-image: -moz-linear-gradient(top, #eeeeee 20%, #ffffff 80%); background-image: -o-linear-gradient(top, #eeeeee 20%, #ffffff 80%); background-image: linear-gradient(#eeeeee 20%, #ffffff 80%); - -webkit-border-bottom-left-radius : 0; + -webkit-border-bottom-left-radius: 0; -webkit-border-bottom-right-radius: 0; - -moz-border-radius-bottomleft : 0; + -moz-border-radius-bottomleft: 0; -moz-border-radius-bottomright: 0; - border-bottom-left-radius : 0; + border-bottom-left-radius: 0; border-bottom-right-radius: 0; } + .chzn-container-active.chzn-with-drop .chzn-single div { background: transparent; border-left: none; } + .chzn-container-active.chzn-with-drop .chzn-single div b { background-position: -18px 2px; } + .chzn-container-active .chzn-choices { - -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); - -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); - box-shadow : 0 0 5px rgba(0,0,0,.3); + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + -moz-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + box-shadow: 0 0 5px rgba(0, 0, 0, .3); border: 1px solid #5897fb; } + .chzn-container-active .chzn-choices .search-field input { color: #111 !important; } + /* @end */ /* @group Disabled Support */ .chzn-disabled { cursor: default; - opacity:0.5 !important; + opacity: 0.5 !important; } + .chzn-disabled .chzn-single { cursor: default; } + .chzn-disabled .chzn-choices .search-choice .search-choice-close { cursor: default; } /* @group Right to Left */ -.chzn-rtl { text-align: right; } -.chzn-rtl .chzn-single { padding: 0 8px 0 0; overflow: visible; } -.chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; direction: rtl; } -.chzn-rtl .chzn-single-with-deselect span { margin-left: 38px; } +.chzn-rtl { + text-align: right; +} + +.chzn-rtl .chzn-single { + padding: 0 8px 0 0; + overflow: visible; +} + +.chzn-rtl .chzn-single span { + margin-left: 26px; + margin-right: 0; + direction: rtl; +} + +.chzn-rtl .chzn-single-with-deselect span { + margin-left: 38px; +} + +.chzn-rtl .chzn-single div { + left: 3px; + right: auto; +} -.chzn-rtl .chzn-single div { left: 3px; right: auto; } .chzn-rtl .chzn-single abbr { left: 26px; right: auto; } -.chzn-rtl .chzn-choices .search-field input { direction: rtl; } -.chzn-rtl .chzn-choices li { float: right; } -.chzn-rtl .chzn-choices .search-choice { padding: 3px 5px 3px 19px; margin: 3px 5px 3px 0; } -.chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 4px; right: auto; } -.chzn-rtl .chzn-container-single-nosearch .chzn-search { left: 9999px; } -.chzn-rtl .chzn-drop { left: 9999px; } -.chzn-rtl.chzn-container-single .chzn-results { margin: 0 0 4px 4px; padding: 0 4px 0 0; } -.chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 15px; } -.chzn-rtl.chzn-container-active.chzn-with-drop .chzn-single div { border-right: none; } + +.chzn-rtl .chzn-choices .search-field input { + direction: rtl; +} + +.chzn-rtl .chzn-choices li { + float: right; +} + +.chzn-rtl .chzn-choices .search-choice { + padding: 3px 5px 3px 19px; + margin: 3px 5px 3px 0; +} + +.chzn-rtl .chzn-choices .search-choice .search-choice-close { + left: 4px; + right: auto; +} + +.chzn-rtl .chzn-container-single-nosearch .chzn-search { + left: 9999px; +} + +.chzn-rtl .chzn-drop { + left: 9999px; +} + +.chzn-rtl.chzn-container-single .chzn-results { + margin: 0 0 4px 4px; + padding: 0 4px 0 0; +} + +.chzn-rtl .chzn-results .group-option { + padding-left: 0; + padding-right: 15px; +} + +.chzn-rtl.chzn-container-active.chzn-with-drop .chzn-single div { + border-right: none; +} + .chzn-rtl .chzn-search input { background: #fff url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px; background: url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); - background: url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background: url('/media/embed/reader/chosen-sprite.png') no-repeat -30px -20px, linear-gradient(#eeeeee 1%, #ffffff 15%); padding: 4px 5px 4px 20px; direction: rtl; } + .chzn-container-single.chzn-rtl .chzn-single div b { background-position: 6px 2px; } + .chzn-container-single.chzn-rtl.chzn-with-drop .chzn-single div b { background-position: -12px 2px; } + /* @end */ /* @group Retina compatibility */ -@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) { +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) { .chzn-rtl .chzn-search input, .chzn-container-single .chzn-single abbr, .chzn-container-single .chzn-single div b, .chzn-container-single .chzn-search input, .chzn-container-multi .chzn-choices .search-choice .search-choice-close, .chzn-container .chzn-results-scroll-down span, .chzn-container .chzn-results-scroll-up span { - background-image: url('/media/embed/reader/chosen-sprite@2x.png') !important; - background-repeat: no-repeat !important; - background-size: 52px 37px !important; + background-image: url('/media/embed/reader/chosen-sprite@2x.png') !important; + background-repeat: no-repeat !important; + background-size: 52px 37px !important; } } + /* @end */ diff --git a/media/css/controls/controls.css b/media/css/controls/controls.css index 95e59ced7e..2aa6ef356b 100644 --- a/media/css/controls/controls.css +++ b/media/css/controls/controls.css @@ -13,16 +13,17 @@ ul.segmented-control { } .segmented-control ::-moz-selection { - background: transparent; + background: transparent; } .segmented-control ::selection { - background: transparent; + background: transparent; } .segmented-control:hover { background-color: #e5e6e2; } + .segmented-control li { background: none; margin: 0 2px 0 0; @@ -32,19 +33,22 @@ ul.segmented-control { border-radius: 4px; color: #61635e; text-shadow: 0 1px 0 rgba(255, 255, 255, .4); - transition: 0.14s ease-in-out background; + transition: 0.14s ease-in-out background; flex: 1 1 0; white-space: nowrap; } + .segmented-control li:last-child { margin-right: 0; } + .segmented-control li:active:not(.NB-disabled), .segmented-control-active li:active { color: #303030; text-shadow: 0 1px 0 rgba(255, 255, 255, .3); } + .segmented-control li.NB-active, .segmented-control-active li { color: #131313; @@ -54,15 +58,18 @@ ul.segmented-control { .segmented-control:hover li.NB-active { background-color: #c5c6c2; } + .segmented-control li:hover:not(.NB-disabled):not(.NB-active) { background-color: #d5d6d2; } + .segmented-control li.NB-disabled { opacity: .9; color: rgba(0, 0, 0, .1); text-shadow: 0 1px 0 rgba(255, 255, 255, .3); cursor: default; } + .segmented-control li.NB-disabled .NB-task-image { opacity: .3; } @@ -71,7 +78,7 @@ ul.segmented-control { .segmented-control li:first-child, .segmented-control li.NB-first { margin-left: 0; - + -webkit-border-top-left-radius: 3px; -webkit-border-bottom-left-radius: 3px; -moz-border-radius-topleft: 3px; @@ -116,11 +123,13 @@ ul.segmented-control-vertical { color: #61635e; text-shadow: 0 1px 0 rgba(255, 255, 255, .4); - + } + .segmented-control-vertical li:last-child { margin-bottom: 0; } + .segmented-control-vertical li:active:not(.NB-disabled), .segmented-control-vertical-active li:active { color: #303030; @@ -140,8 +149,9 @@ ul.segmented-control-vertical { color: rgba(0, 0, 0, .1); text-shadow: 0 1px 0 rgba(255, 255, 255, .3); cursor: default; - + } + .segmented-control-vertical li.NB-disabled .NB-task-image { opacity: .3; } @@ -150,7 +160,7 @@ ul.segmented-control-vertical { .segmented-control-vertical li:first-child, .segmented-control-vertical li.NB-first { margin-left: 0; - + -webkit-border-top-left-radius: 3px; -webkit-border-top-right-radius: 3px; -moz-border-radius-topleft: 3px; @@ -162,7 +172,7 @@ ul.segmented-control-vertical { .segmented-control-vertical li:last-child, .segmented-control-vertical li.NB-last { border-bottom: none; - + -webkit-border-bottom-left-radius: 3px; -webkit-border-bottom-right-radius: 3px; -moz-border-radius-bottomleft: 3px; diff --git a/media/css/pages/payments.css b/media/css/pages/payments.css index 1bcbf11a11..6e256b79f3 100644 --- a/media/css/pages/payments.css +++ b/media/css/pages/payments.css @@ -3,35 +3,35 @@ /* ========== */ .NB-paypal-return { - margin: 176px 0 0; - background-color: #D3E7BA; - border-top: 1px solid #A0A0A0; - border-bottom: 1px solid #A0A0A0; - padding: 24px 0; - background-image: linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - background-image: -moz-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - background-image: -webkit-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - background-image: -ms-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - text-align: center; + margin: 176px 0 0; + background-color: #D3E7BA; + border-top: 1px solid #A0A0A0; + border-bottom: 1px solid #A0A0A0; + padding: 24px 0; + background-image: linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + background-image: -moz-linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + background-image: -webkit-linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + background-image: -ms-linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + text-align: center; } .NB-paypal-return .NB-paypal-return-title { - font-size: 36px; - margin: 0 0 12px; - color: #303030; - text-shadow: 1px 1px 0 rgba(255, 255, 255, .5); + font-size: 36px; + margin: 0 0 12px; + color: #303030; + text-shadow: 1px 1px 0 rgba(255, 255, 255, .5); } .NB-paypal-return .NB-paypal-return-subtitle { - font-size: 24px; - color: #324A15; - text-shadow: 1px 1px 0 rgba(255, 255, 255, .5); + font-size: 24px; + color: #324A15; + text-shadow: 1px 1px 0 rgba(255, 255, 255, .5); } .NB-paypal-return .NB-paypal-return-loading { - margin: 18px auto 0; - height: 16px; - width: 300px; + margin: 18px auto 0; + height: 16px; + width: 300px; } /* ========== */ @@ -39,16 +39,17 @@ /* ========== */ .NB-static-form-wrapper { - margin: 48px 0 18px; - background-color: #D3E7BA; - border-top: 1px solid #A0A0A0; - border-bottom: 1px solid #A0A0A0; - padding: 24px 0; - background-image: linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - background-image: -moz-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - background-image: -webkit-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); - background-image: -ms-linear-gradient(bottom, rgb(188,214,167) 0%, rgb(223,247,212) 100%); + margin: 48px 0 18px; + background-color: #D3E7BA; + border-top: 1px solid #A0A0A0; + border-bottom: 1px solid #A0A0A0; + padding: 24px 0; + background-image: linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + background-image: -moz-linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + background-image: -webkit-linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); + background-image: -ms-linear-gradient(bottom, rgb(188, 214, 167) 0%, rgb(223, 247, 212) 100%); } + .NB-static-form { margin: 0 auto; width: 360px; @@ -73,13 +74,15 @@ -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-static-oauth .NB-static-form .controls input, .NB-stripe-form.NB-static-form .controls input { width: 180px; } + .NB-static-form .NB-label-right { margin: 0 0 24px 206px; - width: 200px; + width: 200px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; @@ -89,14 +92,16 @@ .NB-static-form select.error { border-color: #830C0C; } + .NB-static-form button { width: 200px; margin: 12px 0 4px 150px; - - -moz-box-shadow:2px 2px 0 #A0B998; - -webkit-box-shadow:2px 2px 0 #A0B998; - box-shadow:2px 2px 0 #A0B998; + + -moz-box-shadow: 2px 2px 0 #A0B998; + -webkit-box-shadow: 2px 2px 0 #A0B998; + box-shadow: 2px 2px 0 #A0B998; } + .NB-static-form .helptext { display: none; } @@ -126,6 +131,7 @@ margin: 8px 0 0 150px; overflow: hidden; } + .NB-static-form .NB-creditcards img { float: left; margin-right: 4px; @@ -145,9 +151,10 @@ color: darkred; } -.NB-static-form #payment-form > div { +.NB-static-form #payment-form>div { clear: both; } + .NB-static-form p { overflow: hidden; } @@ -155,9 +162,11 @@ .NB-static-form #id_plan { margin: 0; } + .NB-static-form #id_plan label { margin-top: 0; } + .NB-static-form .NB-stripe-plan-choice { float: left; width: 200px; @@ -165,12 +174,14 @@ padding: 0 2px; font-weight: bold; } + .NB-static-form .NB-stripe-plan-choice label { width: auto; float: left; margin-top: 0; padding-top: 0; } + .NB-static-form .NB-stripe-plan-choice input { width: auto; margin-right: 4px; @@ -181,12 +192,14 @@ margin-left: 4px; color: #575857; } + .NB-static-form .payment-errors { margin: 8px 0 0 150px; color: #600000; display: block; font-weight: bold; } + .NB-static-form .payment-notice { margin: 8px 0 0 150px; color: #606060; @@ -200,10 +213,12 @@ width: 200px; text-transform: none; } + .NB-static-form .payextra-label input { width: auto; margin: 0 4px 0 0; } + .NB-static-form input[name=plan] { display: none; } diff --git a/media/css/pages/welcome.css b/media/css/pages/welcome.css index 0d8e167e79..aec9eb6b6a 100644 --- a/media/css/pages/welcome.css +++ b/media/css/pages/welcome.css @@ -4,9 +4,11 @@ width: 100%; position: absolute; } + .NB-welcome .NB-splash-info.NB-splash-top .NB-splash-title { display: none; } + .NB-welcome .NB-splash-info.NB-splash-top, .NB-welcome .NB-splash-info.NB-splash-bottom { height: 6px; @@ -17,6 +19,7 @@ background: none; background-color: rgba(0, 0, 0, .4); } + .NB-welcome .NB-splash-info.NB-splash-bottom { position: relative; height: auto; @@ -24,20 +27,24 @@ overflow: hidden; border-top: 1px solid rgba(0, 0, 0, .4); } + .NB-welcome .NB-splash-info .NB-splash-links { position: relative; left: 50%; width: auto; float: left; } + .NB-welcome .NB-splash-info .NB-splash-links li { position: relative; right: 50%; } + .NB-welcome .NB-splash-info .NB-splash-links a { color: rgba(255, 255, 255, .7); text-shadow: 0 1px 0 rgba(0, 0, 0, .2); } + .NB-welcome .NB-splash-info .NB-splash-links a:hover { color: #F4DD43; } @@ -52,6 +59,7 @@ position: relative; height: 100%; } + .NB-welcome .NB-inner-account { width: 960px; margin: 0 auto; @@ -64,25 +72,28 @@ transform: translate(-50%, 0); pointer-events: none; } + .NB-button { border: 1px solid #07360F; font-size: 12px; padding: 4px 12px; margin: 2px 4px 2px; - -moz-box-shadow:2px 2px 0 rgba(35, 35, 35, 0.4); - -webkit-box-shadow:2px 2px 0 rgba(35, 35, 35, 0.4); - box-shadow:2px 2px 0 rgba(35, 35, 35, 0.4); + -moz-box-shadow: 2px 2px 0 rgba(35, 35, 35, 0.4); + -webkit-box-shadow: 2px 2px 0 rgba(35, 35, 35, 0.4); + box-shadow: 2px 2px 0 rgba(35, 35, 35, 0.4); border-radius: 4px; -moz-border-radius: 4px; cursor: pointer; text-decoration: none; color: #404040; } + .NB-button:active { - -moz-box-shadow:1px 1px 0 rgba(35, 35, 35, 0.6); - -webkit-box-shadow:1px 1px 0 rgba(35, 35, 35, 0.6); - box-shadow:1px 1px 0 rgba(35, 35, 35, 0.6); + -moz-box-shadow: 1px 1px 0 rgba(35, 35, 35, 0.6); + -webkit-box-shadow: 1px 1px 0 rgba(35, 35, 35, 0.6); + box-shadow: 1px 1px 0 rgba(35, 35, 35, 0.6); } + .NB-welcome-container.NB-welcome-tryout { overflow-x: hidden; } @@ -94,22 +105,25 @@ .NB-welcome-header { position: relative; background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#3F5354), to(#1B2424)); - background: -moz-linear-gradient(center top , #3F5354 0%, #1B2424 100%) repeat scroll 0 0 transparent; + background: -moz-linear-gradient(center top, #3F5354 0%, #1B2424 100%) repeat scroll 0 0 transparent; height: 420px; color: white; box-shadow: 0 1px 2px rgba(10, 10, 10, 0.3); } + .NB-welcome-header .NB-background { background: url(/media/img/welcome/header_background.jpg) no-repeat center center; width: 100%; height: 100%; overflow: hidden; } + .NB-welcome-header .NB-welcome-header-logo { padding: 48px 0 0 0; text-align: center; width: 460px; } + .NB-welcome-header .NB-welcome-header-logo img { opacity: .9; -webkit-transition: all .15s ease-in-out; @@ -118,9 +132,11 @@ -ms-transition: all .15s ease-in-out; } + .NB-welcome-header .NB-welcome-header-logo img:hover { opacity: 1; } + .NB-welcome-header .NB-welcome-header-tagline { margin: 24px 0 0 0; font-size: 22px; @@ -128,6 +144,7 @@ text-align: center; width: 460px; } + .NB-welcome-header .NB-welcome-header-tagline b { padding: 2px 8px; background-color: rgba(205, 205, 205, 0.1); @@ -140,12 +157,14 @@ right: 0; bottom: 0; } + .NB-welcome-header-image img { z-index: 0; opacity: 0; - transition: bottom 1s ease-in-out, - opacity 1s ease-in-out; + transition: bottom 1s ease-in-out, + opacity 1s ease-in-out; } + .NB-welcome-header-image.NB-1 img { height: 300px; position: absolute; @@ -155,6 +174,7 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; } + .NB-welcome-header-image.NB-2 img { opacity: 0; height: 300px; @@ -166,6 +186,7 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; } + .NB-welcome-header-image.NB-3 img { opacity: 0; height: 300px; @@ -177,11 +198,13 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; } + .NB-welcome-header-image.NB-active img { z-index: 1; bottom: 0; opacity: 1; } + .NB-welcome-header-captions { text-align: center; margin: 12px auto 0; @@ -189,9 +212,10 @@ top: 0; right: 32px; overflow: hidden; - text-shadow: 0 1px 0 rgba(35,35,35,0.35); + text-shadow: 0 1px 0 rgba(35, 35, 35, 0.35); padding-left: 64px; } + .NB-welcome-header-caption { float: left; color: white; @@ -199,9 +223,11 @@ padding: 36px 12px 12px; text-transform: uppercase; } + .NB-welcome-header-caption.NB-welcome-header-caption-signin { color: #E8F64A; } + .NB-welcome-header-caption span { padding: 2px 8px; border-radius: 4px; @@ -211,12 +237,14 @@ -o-transition: all 1s ease-in-out; -ms-transition: all 1s ease-in-out; } + .NB-welcome-header-caption:hover span { -webkit-transition: all .25s ease-in-out; -moz-transition: all .25s ease-in-out; -o-transition: all .25s ease-in-out; -ms-transition: all .25s ease-in-out; } + .NB-welcome-header-caption.NB-active span { color: #FDC85B; background-color: rgba(235, 235, 235, 0.1); @@ -230,10 +258,12 @@ overflow: hidden; float: left; } + .NB-welcome-header-action { float: left; margin: 4px 0 16px; } + .NB-welcome-header-action-subtext { text-align: center; line-height: 24px; @@ -243,20 +273,25 @@ color: white; text-shadow: 0 1px 0 rgba(35, 35, 35, 0.4); } + .NB-welcome-header-actions img { vertical-align: top; width: 16px; height: 16px; } + .NB-welcome-header-actions .NB-welcome-header-action-arrow { display: none; } + .NB-welcome-header-actions .NB-active .NB-welcome-header-action-bolt { display: none; } + .NB-welcome-header-actions .NB-active .NB-welcome-header-action-arrow { display: block; } + .NB-welcome-header-actions .NB-button { float: left; font-size: 16px; @@ -268,11 +303,13 @@ background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#F5FFE2), to(#E8FACA)); background: -moz-linear-gradient(center top, #F5FFE2 0%, #E8FACA 100%); } + .NB-welcome-header-actions .NB-button:hover { background-color: #F4DD43; background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#F4DD43), to(#F1D526)); background: -moz-linear-gradient(center top, #F4DD43 0%, #F1D526 100%); } + .NB-welcome-header-actions .NB-button:active { text-shadow: 0 1px 0 rgba(35, 35, 35, 0.4); background-color: #E97326; @@ -295,11 +332,13 @@ border-top-left-radius: 8px; border-top-right-radius: 8px; background-color: rgba(245, 245, 245, 0.4); - padding: 36px 36px 12px; + padding: 36px 36px 12px; } + .NB-welcome-header-account.NB-active { bottom: 0px; } + .NB-welcome-header-account .NB-module-header-login { width: 142px; margin: 0 50px 0 0; @@ -345,10 +384,12 @@ text-align: center; width: 190px; } + .NB-welcome-header-account .NB-import-signup-text h3 { margin-top: 48px; color: #20843D; } + .NB-welcome-header-account .NB-import-signup-text p { color: #636363; } @@ -365,6 +406,7 @@ .NB-welcome-header-account .NB-signup-orline-reduced { margin: 0px auto 0px; } + .NB-welcome-header-account .NB-signup-orline .NB-signup-orline-or { padding: 0 4px; } @@ -377,6 +419,7 @@ font-size: 9px; line-height: 17px; } + .NB-welcome-header-account .NB-signup-hidden { display: none; } @@ -395,11 +438,11 @@ .NB-welcome-header-account input[type=text], .NB-welcome-header-account input[type=password] { border: 1px solid rgba(35, 35, 35, 0.5); - display:block; - font-size:13px; - margin:0 0 12px; - padding:5px; - width:134px; + display: block; + font-size: 13px; + margin: 0 0 12px; + padding: 5px; + width: 134px; } .NB-welcome-header-account input[type=text]:focus, @@ -409,7 +452,8 @@ .NB-welcome-header-account input[type=submit] { outline: none; - width: 146px; /* 174=input-width + 5*2=padding + 2=border */ + width: 146px; + /* 174=input-width + 5*2=padding + 2=border */ margin: 4px 5px 0 0; padding: 4px 10px 5px; font-size: 12px; @@ -482,11 +526,13 @@ object-fit: cover; box-shadow: 0 0 3px rgba(30, 30, 30, 0.3); } + .NB-welcome-features .NB-feature-caption { font-weight: bold; font-size: 22px; margin: 24px 0 0; } + .NB-welcome-features .NB-feature-text { color: #808080; margin: 8px 0 0; @@ -514,11 +560,13 @@ object-fit: contain; box-shadow: 0 0 3px rgba(30, 30, 30, 0.3); } + .NB-welcome-subfeatures .NB-feature-caption { font-weight: bold; font-size: 22px; margin: 0px 0 0; } + .NB-welcome-subfeatures .NB-feature-text { color: #808080; margin: 8px 0 0; @@ -535,14 +583,16 @@ clear: both; margin: 48px 0 0; padding: 36px 0; - + background-color: #FAFAFA; border-top: 1px solid #F0F0F0; border-bottom: 1px solid #F0F0F0; } + .NB-welcome-pricing p { line-height: 24px; } + .NB-welcome-pricing .NB-price { float: right; margin: -2px 0 0; @@ -552,11 +602,13 @@ line-height: 16px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); } + .NB-pricing { margin: 12px 0; width: 100%; font-size: 14px; } + .NB-pricing th, .NB-pricing td { padding: 10px 8px; @@ -565,25 +617,31 @@ width: 30%; vertical-align: top; } + .NB-pricing th { border-right: 1px solid #E4E4E4; } + .NB-pricing td { border-top: 1px solid #E4E4E4; border-right: 1px solid #E4E4E4; } + .NB-pricing td:first-child, .NB-pricing th:first-child { white-space: nowrap; width: 100px; } + .NB-pricing td:last-child, .NB-pricing th:last-child { border-right: none; } + .NB-pricing .NB-bold { font-weight: bold; } + .NB-pricing .NB-lyric { display: block; float: left; @@ -591,9 +649,11 @@ height: 54px; margin: 0 12px 0 0; } + .NB-pricing .NB-lyric-text { padding-right: 20%; } + /* ================== */ /* = Bottom Actions = */ /* ================== */ @@ -615,6 +675,7 @@ background-color: rgba(209, 211, 219, .4); width: 450px; } + .NB-welcome-bottomactions .NB-welcome-header-action-subtext { color: #363C53; text-shadow: 0 1px 0 white; @@ -629,34 +690,44 @@ clear: both; padding: 36px 0; } + .NB-welcome-activity .NB-module { width: 47%; } + .NB-welcome-activity .NB-module-features { float: right; } + .NB-welcome-activity .NB-module-stats { float: left; margin-right: 3%; } + .NB-welcome-activity .NB-module .NB-module-header { display: none; } + .NB-welcome-activity .NB-module .NB-module-stats-counts { overflow: hidden; } + .NB-welcome-activity .NB-module-stats .NB-module-content-header-hour { margin-top: 36px; } + .NB-welcome-activity .NB-module-stats-count-graph { margin-left: 4px; } + .NB-welcome-activity .NB-graph-bar { width: 4px; } + .NB-welcome-activity .NB-graph-value { width: 4px; } + .NB-welcome-activity .NB-module-stats-count { float: left; width: 33%; @@ -664,6 +735,7 @@ -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-welcome-activity .NB-module-stats-count-graph { width: auto; margin-left: 16px; @@ -686,30 +758,37 @@ border-top: 2px solid rgba(0, 0, 0, .1); } + .NB-welcome-footer .NB-welcome-footer-content { margin-bottom: 46px; } + .NB-welcome-footer .NB-footer-logo { vertical-align: text-bottom; position: relative; bottom: -4px; } + .NB-welcome-footer .NB-splash-link { color: #363C53; font-weight: bold; text-shadow: 0 1px 0 rgba(245, 245, 245, 0.7); } + .NB-welcome-footer .NB-splash-link:hover { color: #822216; } + .NB-welcome-footer .NB-footer-icons { float: right; } + .NB-welcome-footer .NB-footer-icons a { line-height: 0; margin: 0 12px 0 0; float: right; } + .NB-welcome-footer .NB-footer-icons a img { vertical-align: middle; width: 32px; @@ -722,6 +801,7 @@ -o-transition: all .25s ease-in-out; -ms-transition: all .25s ease-in-out; } + .NB-welcome-footer .NB-footer-icons a img:hover { opacity: 1; } diff --git a/media/css/reader/darkmode.css b/media/css/reader/darkmode.css index 1e358bd061..34cb0b7a8e 100644 --- a/media/css/reader/darkmode.css +++ b/media/css/reader/darkmode.css @@ -29,11 +29,12 @@ .NB-theme-transitioning ul, .NB-theme-transitioning li, body.NB-theme-transitioning { - transition: color 1s ease-out, - background-color 1s ease-out, - text-shadow 1s ease-out, - border-color 1s ease-out; + transition: color 1s ease-out, + background-color 1s ease-out, + text-shadow 1s ease-out, + border-color 1s ease-out; } + body.NB-dark { background-color: #191b1c; color: #c0c0c0; @@ -48,25 +49,29 @@ body.NB-dark { } .NB-dark .NB-overlay { - background: rgba(16,16,28,0.3); + background: rgba(16, 16, 28, 0.3); } .NB-dark hr { background-color: rgba(255, 255, 255, 0.4); } + .NB-dark .NB-welcome-pricing, .NB-dark .NB-welcome-bottomactions { background-color: #303335; border-top: 1px solid #222425; border-bottom: 1px solid #222425; } + .NB-dark .NB-pricing th { border-right: 1px solid #545a5d; } + .NB-dark .NB-pricing td { border-top: 1px solid #545a5d; border-right: 1px solid #545a5d; } + .NB-dark .NB-welcome-pricing .NB-price { background-color: #3B5370; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3); @@ -76,6 +81,7 @@ body.NB-dark { color: white; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + /* ============== */ /* = Scrollbars = */ /* ============== */ @@ -83,6 +89,7 @@ body.NB-dark { .NB-dark { scrollbar-color: #565859 #262829; } + .NB-dark ::-webkit-scrollbar { width: 12px; } @@ -98,14 +105,15 @@ body.NB-dark { } .NB-dark .ui-layout-resizer-west { - background-color: rgb(52,59,61); + background-color: rgb(52, 59, 61); border-left: 1px solid #303030; } -.NB-dark .right-pane .ui-layout-resizer-south, + +.NB-dark .right-pane .ui-layout-resizer-south, .NB-dark .right-pane .ui-layout-resizer-north { - background-color: rgb(52,59,61); + background-color: rgb(52, 59, 61); border-top: 1px solid #303030; - border-bottom: 1px solid #303030; + border-bottom: 1px solid #303030; } @@ -132,18 +140,23 @@ color: #a85b40; .NB-dark .NB-module-features .NB-module-feature .NB-module-feature-description { color: #c0c0c0; } + .NB-dark .NB-module-features .NB-module-feature.NB-module-feature-new td { background-color: #a28334; } + .NB-dark .NB-module-features .NB-module-feature.NB-module-feature-new .NB-module-feature-description { color: #e1e1e1; } + .NB-dark .NB-module-features .NB-module-feature.NB-module-feature-new .NB-module-feature-date { color: #d1d1d1; } + .NB-dark .NB-module-recommended .NB-recommended-added { color: #69ac4b; } + /* Divider color */ .NB-dark .NB-module.NB-module-features .NB-module-content-header, .NB-dark .NB-module-recommended .NB-recommended-description, @@ -184,6 +197,7 @@ color: #a85b40; .NB-dark .NB-module-account-subscription .NB-module-stats-count-description { color: #c0c0c0; } + /* "Logout", "Try" & "Add" button colors */ .NB-dark .NB-recommended-try, .NB-dark .NB-recommended-add, @@ -243,16 +257,17 @@ color: #a85b40; .NB-dark .NB-storytitles-title { color: #c0c0c0; } + .NB-dark .read .NB-storytitles-title { color: #797b7c; } -.NB-dark .NB-feeds-header-user-interactions img, +.NB-dark .NB-feeds-header-user-interactions img, .NB-dark .NB-feeds-header-collapse-sidebar img { opacity: 0.9; } -.NB-dark .NB-feeds-header-user-interactions:hover img, +.NB-dark .NB-feeds-header-user-interactions:hover img, .NB-dark .NB-feeds-header-collapse-sidebar:hover img { opacity: 1.0; } @@ -264,24 +279,33 @@ color: #a85b40; /* Manage menu on splash page & cogwheel pop-up */ .NB-dark .NB-menu-manage-container { - background-color: #151613; + background-color: #151613; } + .NB-dark .NB-menu-manage li.NB-menu-item { background-color: #303739; } + .NB-dark .NB-menu-manage-container, -.NB-dark .NB-menu-manage .NB-menu-manage-site-info, /* Preferences title background */ -.NB-dark .NB-menu-manage li.NB-menu-item, /* Icon background */ -.NB-dark .NB-menu-manage-title /* Title text background */ { +.NB-dark .NB-menu-manage .NB-menu-manage-site-info, +/* Preferences title background */ +.NB-dark .NB-menu-manage li.NB-menu-item, +/* Icon background */ +.NB-dark .NB-menu-manage-title + +/* Title text background */ + { box-shadow: none; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); border-color: #10120b; } -.NB-dark .NB-menu-manage li.NB-menu-item:hover:not(.NB-disabled):not(.NB-active), + +.NB-dark .NB-menu-manage li.NB-menu-item:hover:not(.NB-disabled):not(.NB-active), .NB-dark .NB-menu-manage li.NB-menu-item.NB-hover:not(.NB-disabled):not(.NB-active) { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); - background-color: #3d4649; + background-color: #3d4649; } + .NB-dark .NB-menu-manage li.NB-menu-item:hover .NB-menu-manage-subtitle { color: rgba(255, 255, 255, .4); } @@ -289,6 +313,7 @@ color: #a85b40; .NB-dark .NB-menu-manage li.NB-menu-item:hover:not(.NB-disabled) .NB-menu-manage-title { text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); } + /* Subtitle color for manage menu on cogwheel pop-up */ .NB-dark .NB-menu-manage-subtitle { color: gray; @@ -311,10 +336,12 @@ color: #a85b40; .NB-dark .NB-menu-manage li.NB-menu-separator-inverse { border-bottom: none; } -.NB-dark .NB-menu-manage .NB-menu-manage-feed-info, + +.NB-dark .NB-menu-manage .NB-menu-manage-feed-info, .NB-dark .NB-menu-manage .NB-menu-manage-site-info { background-color: #252623; } + .NB-dark .popover { box-shadow: none; } @@ -332,34 +359,41 @@ color: #a85b40; border-top-color: #999; border-top-color: #10120b; } + .NB-dark .popover.top-left .arrow:after, .NB-dark .popover.top-right .arrow:after, .NB-dark .popover.top .arrow:after { border-top-color: #303739; border-bottom-width: 2px; } + .NB-dark .popover.right .arrow { border-right-color: #999; border-right-color: #10120b; } + .NB-dark .popover.right .arrow:after { border-right-color: #303739; } + .NB-dark .popover.bottom .arrow, .NB-dark .popover.bottom-left .arrow, .NB-dark .popover.bottom-right .arrow { border-bottom-color: #10120b; } + .NB-dark .popover.bottom .arrow:after, .NB-dark .popover.bottom-left .arrow:after, .NB-dark .popover.bottom-right .arrow:after { border-bottom-color: #303739; border-top-width: 2px; } + .NB-dark .popover.left .arrow { border-left-color: #999; border-left-color: #10120b; } + .NB-dark .popover.left .arrow:after { border-left-color: #303739; } @@ -384,7 +418,7 @@ color: #a85b40; .NB-dark .NB-add input[type=text].ui-autocomplete-loading, .NB-dark textarea, .NB-dark textarea.NB-modal-email-comments, -.NB-dark .NB-add-form .NB-folders{ +.NB-dark .NB-add-form .NB-folders { color: #c0c0c0; background: none; background-color: #505050; @@ -427,7 +461,7 @@ color: #a85b40; } /* Input field suggestions window (4 of 6) */ -.NB-dark .NB-add-form .ui-autocomplete li a .NB-add-autocomplete-title { +.NB-dark .NB-add-form .ui-autocomplete li a .NB-add-autocomplete-title { color: #c0c0c0; } @@ -448,8 +482,8 @@ color: #a85b40; } /* Style Font family active */ -.NB-dark .segmented-control-vertical li.NB-active, -.NB-dark .segmented-control-vertical li.NB-active:active, +.NB-dark .segmented-control-vertical li.NB-active, +.NB-dark .segmented-control-vertical li.NB-active:active, .NB-dark .segmented-control-vertical-active li { color: #c0c0c0; background-color: #6D6D74; @@ -467,7 +501,7 @@ color: #a85b40; /* Background color */ .NB-dark #simplemodal-container { background-color: #2f3840; - box-shadow: none; + box-shadow: none; border: 2px solid #131617; } @@ -476,7 +510,7 @@ color: #a85b40; .NB-dark .NB-modal .NB-modal-subtitle, .NB-dark .NB-modal .NB-feed-title, .NB-dark .NB-modal .NB-modal-feed-heading, -.NB-dark .NB-modal-trainer .NB-trainer-points li b{ +.NB-dark .NB-modal-trainer .NB-trainer-points li b { color: #c0c0c0; text-shadow: none; } @@ -609,7 +643,7 @@ color: #a85b40; /* Import or upload sites: Container color */ .NB-dark .NB-modal-intro .NB-intro-module, -.NB-dark .NB-modal-intro .NB-intro-uptodate-follow{ +.NB-dark .NB-modal-intro .NB-intro-uptodate-follow { background-color: #303739; border: 1px solid #999; } @@ -669,45 +703,54 @@ color: #a85b40; .NB-dark .NB-feedchooser-info-counts { text-shadow: none; } + .NB-dark .NB-modal-feedchooser .NB-modal-subtitle b { color: #85bd67; } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-premium-plan { background-color: #303739; } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-info-reset { text-shadow: 0 1px 0 #181818; } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-premium-bullets { color: #A0A0A0; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-premium-bullets li { - border-top-color:rgba(0, 0, 0, .1); + border-top-color: rgba(0, 0, 0, .1); } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-premium-bullets li .NB-feedchooser-premium-bullet-image { filter: brightness(5); } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-dollar-value { text-shadow: 0 1px 0 #2b2a1e; } + .NB-dark .NB-modal-feedchooser .NB-selected .NB-feedchooser-dollar-month { color: #8293b3; } + .NB-dark .NB-modal-feedchooser .NB-selected .NB-feedchooser-dollar-year { color: #697192; } + .NB-dark .NB-modal-feedchooser .NB-modal-submit.NB-modal-submit-paypal { background-color: #3b3725; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.16, #3b3725), - color-stop(0.84, #3b392e) - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.16, #3b3725), + color-stop(0.84, #3b392e)); box-shadow: 0 2px 0 #383215; } + .NB-dark .NB-modal-feedchooser .NB-feedchooser-premium-already { background-color: rgb(48 88 83); color: #85bd67; @@ -736,7 +779,7 @@ color: #a85b40; } /* Site Statistics: Text color */ -.NB-dark .NB-modal-statistics .NB-statistics-stat .NB-statistics-label , +.NB-dark .NB-modal-statistics .NB-statistics-stat .NB-statistics-label, .NB-dark .NB-statistics-count, .NB-dark .NB-modal-statistics .NB-statistics-classifiers { color: #c0c0c0; @@ -746,12 +789,13 @@ color: #a85b40; /* Site Statistics: Dividers */ .NB-dark .NB-modal-statistics .NB-statistics-facet-title, .NB-dark .NB-modal-statistics .NB-statistics-facet { - border-bottom: none!important; + border-bottom: none !important; } .NB-dark .NB-modal-statistics .NB-statistics-stat { border-color: #404040; } + .NB-dark .NB-modal-statistics .NB-statistics-history-chart-hours-row { background-color: #252D30; } @@ -763,6 +807,7 @@ color: #a85b40; .NB-dark .NB-classifiers .NB-classifier label b { color: rgba(255, 255, 255, 0.4); } + .NB-dark .NB-classifiers .NB-classifier label span { color: #c0c0c0; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2) @@ -814,8 +859,8 @@ color: #a85b40; } /* Username color */ -.NB-dark .NB-splash-info .NB-splash-links a, -.NB-dark .NB-static a, +.NB-dark .NB-splash-info .NB-splash-links a, +.NB-dark .NB-static a, .NB-dark .NB-splash-link { color: #83B4E0; } @@ -829,14 +874,17 @@ color: #a85b40; background-color: #c18025; color: rgb(217, 217, 217); } + .NB-dark .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-praise { background-color: #D7B806; color: rgb(255, 255, 255); } + .NB-dark .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-idea { background-color: #5C9A1E; color: rgb(224, 224, 244); } + .NB-dark .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-question { background-color: #4981A9; color: rgb(224, 224, 244); @@ -855,7 +903,7 @@ color: #a85b40; .NB-dark .NB-splash-info.NB-splash-bottom, .NB-dark .left-south { color: #ddd; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); background: #393b3c; background-color: #393b3c; background-image: none; @@ -872,7 +920,7 @@ color: #a85b40; background: none; background-color: #565759; color: #c0c0c0; - text-shadow: 0 1px 0 rgba(0,0,0,.1); + text-shadow: 0 1px 0 rgba(0, 0, 0, .1); /*border: 1px solid #90928b;*/ } @@ -882,42 +930,51 @@ color: #a85b40; .NB-dark .segmented-control { background: none; } + .NB-dark .segmented-control li { color: #9A9B9C; - text-shadow: 0 1px 0 rgba(0,0,0,.1); + text-shadow: 0 1px 0 rgba(0, 0, 0, .1); } -.NB-dark .segmented-control li.NB-active, -.NB-dark .segmented-control li.NB-active:not(.NB-disabled), + +.NB-dark .segmented-control li.NB-active, +.NB-dark .segmented-control li.NB-active:not(.NB-disabled), .NB-dark .segmented-control li:active:not(.NB-disabled), .NB-dark .segmented-control-active li { background: none; background-color: #6c6c74; color: #FCFDFE; - text-shadow: 0 1px 0 rgba(0,0,0,0.1); + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); } + .NB-dark .segmented-control:hover { background-color: #494d54; } + .NB-dark .segmented-control:hover li.NB-active { background-color: #787881; } + .NB-dark .segmented-control li:hover:not(.NB-disabled):not(.NB-active) { background-color: #585b63; } + .NB-dark #story_taskbar { border-top: 1px solid #303030; } + .NB-dark .NB-feedbar .NB-taskbar .NB-task-story-previous { - border-right: 1px solid rgba(0,0,0,0.1); + border-right: 1px solid rgba(0, 0, 0, 0.1); } + .NB-dark .NB-feedbar .NB-taskbar .NB-task-story-next { - border-left: 1px solid rgba(0,0,0,0.1); + border-left: 1px solid rgba(0, 0, 0, 0.1); } + /* Error text on bottom bar */ .NB-dark #story_taskbar .NB-river-progress .NB-river-progress-text, .NB-dark #story_taskbar .NB-feed-error .NB-feed-error-text { color: #c0c0c0; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); } /* "Unread/Oldest" & "Style" border color */ @@ -931,6 +988,7 @@ color: #a85b40; border: 1px solid rgba(0, 0, 0, .3); background-color: #303739; } + .NB-dark .NB-dashboard-river .NB-dashboard-river-options { background-color: #565759; } @@ -947,8 +1005,7 @@ color: #a85b40; /* "Mark read" icon*/ .NB-dark .NB-feedbar .NB-feedbar-mark-feed-read .NB-icon { - background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuOWwzfk4AAAFuSURBVFhHzZXBSsNAEIYjQg+CCAWfonjqqYTcAj15zFP4AoIP0Kt4KHrvWSnised6yMnH6EusM2VWdsc/3STVTAsfZP+d3fnSJUnmnDspYGgJDC2BoSUwtASGlsDQEhhaAkNLsrqu3RDoxk2cnhAKh4Rk74gbP/5VMCQkci//4M5LmR0ZZQ+qZi9lIkTjhZ4XPiLrIaCmTTKfZVlewUX/Af3OqOmTkvBsWWZfByYPEjZpi8gs9V7CZno7vfipBQUH8Qs9eZ5fVlV1rnMPz9G6F72PEMkw0eKuFEVxTZt+ESskJTIraa551zJMNOhCIOMbRFIJmfWkmozC/Ty9jmw2n43pOpTxvHIjhq7f1FxUE0qE9BJK3b2A5uDRhsCwDQkpRFKGgWFbRKrpCQp5biPDHP3YJ94xzJJr9Lom/uTFKFKPupazLjIMDPtCAuF3aoFqUsDwGESqlwwDQ0tgaAkMLYGhJTC0BIaWwNAOl30DsVBFw8rjuF0AAAAASUVORK5CYII=) - no-repeat center center; + background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuOWwzfk4AAAFuSURBVFhHzZXBSsNAEIYjQg+CCAWfonjqqYTcAj15zFP4AoIP0Kt4KHrvWSnised6yMnH6EusM2VWdsc/3STVTAsfZP+d3fnSJUnmnDspYGgJDC2BoSUwtASGlsDQEhhaAkNLsrqu3RDoxk2cnhAKh4Rk74gbP/5VMCQkci//4M5LmR0ZZQ+qZi9lIkTjhZ4XPiLrIaCmTTKfZVlewUX/Af3OqOmTkvBsWWZfByYPEjZpi8gs9V7CZno7vfipBQUH8Qs9eZ5fVlV1rnMPz9G6F72PEMkw0eKuFEVxTZt+ESskJTIraa551zJMNOhCIOMbRFIJmfWkmozC/Ty9jmw2n43pOpTxvHIjhq7f1FxUE0qE9BJK3b2A5uDRhsCwDQkpRFKGgWFbRKrpCQp5biPDHP3YJ94xzJJr9Lom/uTFKFKPupazLjIMDPtCAuF3aoFqUsDwGESqlwwDQ0tgaAkMLYGhJTC0BIaWwNAOl30DsVBFw8rjuF0AAAAASUVORK5CYII=) no-repeat center center; background-size: 18px; width: 100%; height: 100%; @@ -963,7 +1020,7 @@ color: #a85b40; } .NB-dark .NB-feedbar .NB-feedbar-mark-feed-read-expand { - background: transparent url('/media/embed/icons/circular/nav_icn_plus.png') no-repeat 4px center; + background: transparent url('/media/embed/icons/circular/nav_icn_plus.png') no-repeat 4px center; background-size: 6px; } @@ -978,9 +1035,10 @@ color: #a85b40; background: none; background-color: #303739; color: #c0c0c0; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); box-shadow: none; } + .NB-dark .NB-searching input.NB-search-input[type="text"] { background-color: #505050; } @@ -1000,8 +1058,8 @@ text-shadow: none; } .NB-dark .NB-splash-blurred-logo { - background: transparent url('/media/embed/logo_newsblur_blur_dark.png') no-repeat center center; - background-size: contain; + background: transparent url('/media/embed/logo_newsblur_blur_dark.png') no-repeat center center; + background-size: contain; } /* ============= */ @@ -1015,10 +1073,12 @@ text-shadow: none; background: transparent url("/media/embed/reader/sun_loader_dark.svg") no-repeat 0 0; background-size: 52px; } + .NB-dark .NB-feeds-list-empty { color: rgba(152, 152, 152, 1); text-shadow: 0 1px 0 rgba(32, 32, 32, 0.4); } + .NB-dark .left-pane, .NB-dark .NB-feedlist { background-color: #434543; @@ -1032,9 +1092,9 @@ text-shadow: none; } /* Selected Feed */ -.NB-dark .NB-feedlist .feed.selected, -.NB-dark .NB-feedlist .feed.NB-selected, -.NB-dark .NB-feeds-header.NB-selected, +.NB-dark .NB-feedlist .feed.selected, +.NB-dark .NB-feedlist .feed.NB-selected, +.NB-dark .NB-feeds-header.NB-selected, .NB-dark .NB-feedlist .folder.NB-selected>.folder_title { background: none; background-color: #4d6d95; @@ -1060,14 +1120,14 @@ border-bottom-color: #303739; .NB-dark .NB-feedlists .NB-socialfeeds .feed .feed_title, .NB-dark .NB-feedlist .feed_title { color: #bdbdbd; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); } /* Feed folder text */ .NB-dark .NB-feedlist .folder_title, .NB-dark .NB-feedlist .folder.NB-selected>.folder_title { color: #bdbdbd; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); } /* "All Site Stories" text*/ @@ -1077,21 +1137,25 @@ border-bottom-color: #303739; border-top-color: #797D7D; border-bottom-color: #4C4D4E; } + .NB-dark .NB-feeds-header:hover:not(.NB-selected) { background: none; background-color: #5c5e60; border-top-color: #797D7D; - border-bottom-color: #4C4D4E; + border-bottom-color: #4C4D4E; } -.NB-dark .NB-feeds-header-read.NB-feeds-header, -.NB-dark .NB-feeds-header-searches.NB-feeds-header, -.NB-dark .NB-feeds-header-starred.NB-feeds-header, + +.NB-dark .NB-feeds-header-read.NB-feeds-header, +.NB-dark .NB-feeds-header-searches.NB-feeds-header, +.NB-dark .NB-feeds-header-starred.NB-feeds-header, .NB-dark .NB-searches-folder { /* border-bottom: 1px solid #333536; */ } + .NB-dark .NB-feeds-header-container { background-color: #434543; } + /* Unread feed count badge */ .NB-dark .unread_count_neutral { background-color: #7a7e77; @@ -1118,27 +1182,34 @@ border-bottom-color: #303739; border-top: 1px solid #333536; border-bottom: 1px solid #232526; } + .NB-dark .NB-story-title-grid.NB-story-title { border: none; } + .NB-dark .NB-story-title-grid.NB-story-title .NB-storytitles-grid-bottom { - background-color: #292b2c; + background-color: #292b2c; border-top-color: #303030; } + .NB-dark .NB-story-title:hover:not(.NB-selected), .NB-dark .NB-story-title:hover:not(.NB-selected) .NB-storytitles-grid-bottom { background-color: #232526; } + .NB-dark .NB-story-title.read:hover:not(.NB-selected), .NB-dark .NB-story-title.read:hover:not(.NB-selected) .NB-storytitles-grid-bottom { background-color: #292b2c; } + .NB-dark .NB-story-title.read .NB-storytitles-author { color: #696b6c; } + .NB-dark .read .NB-storytitles-content-preview { color: #595b5c; } + .NB-dark .NB-story-title.read .story_date { color: #696b6c; } @@ -1147,7 +1218,7 @@ border-bottom-color: #303739; .NB-dark .NB-feedbar .feed .feed_title, .NB-dark .NB-feedbar .folder_title_text { color: #c0c0c0; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); } /* Stories title text */ @@ -1183,10 +1254,19 @@ border-bottom-color: #303739; .NB-dark .NB-end-line { animation: dark-end-line-animation 1.7s ease infinite; } -@keyframes dark-end-line-animation { - 0%{background-color:#242529} - 38%{background-color:#273a55} - 100%{background-color:#242529} + +@keyframes dark-end-line-animation { + 0% { + background-color: #242529 + } + + 38% { + background-color: #273a55 + } + + 100% { + background-color: #242529 + } } /* Search header */ @@ -1201,15 +1281,17 @@ border-bottom-color: #303739; .NB-dark .NB-search-header .NB-search-header-save:hover, .NB-dark .NB-search-header .NB-search-header-save.NB-active { color: #c0c0c0; - background-color: rgb(43,49,51); + background-color: rgb(43, 49, 51); } .NB-dark .NB-story-content-wrapper .NB-story-content-expander { background-color: #191b1c; } + .NB-dark .NB-story-content-wrapper .NB-story-content-expander .NB-story-cutoff { background: transparent url('/media/embed/circular/module_cutoff_dark.png') repeat-x left bottom; } + /* .NB-dark .NB-feeds-header-river-container .NB-feeds-header { border-bottom: 1px solid #222d31; @@ -1242,15 +1324,18 @@ background-color: #5d8392; color: #c0c0c0; background-color: #191b1c; } + .NB-dark .NB-newsletter p, .NB-dark .NB-newsletter div, .NB-dark .NB-newsletter span, .NB-dark .NB-newsletter td { color: #c0c0c0 !important; } + .NB-dark .NB-newsletter td { background-color: #191b1c; } + .NB-dark .NB-newsletter h1, .NB-dark .NB-newsletter h2, .NB-dark .NB-newsletter h3, @@ -1318,47 +1403,58 @@ color: #fff; .NB-dark .NB-feed-story .NB-feed-story-header-info { background-color: #303739; } + .NB-dark .NB-feed-story.read .NB-feed-story-header-info { background-color: #202729; } + .NB-dark .NB-feed-story .NB-feed-story-header .NB-feed-story-date, .NB-dark .NB-feed-story .NB-feed-story-header .NB-feed-story-author-wrapper { color: #A2B3BF; } + .NB-dark .NB-feed-story.read .NB-feed-story-header .NB-feed-story-date, .NB-dark .NB-feed-story.read .NB-feed-story-header .NB-feed-story-author-wrapper { color: #636D74; } + .NB-dark .NB-feed-story .NB-feed-story-tag { border-color: rgba(255, 255, 255, 0.05) transparent rgba(0, 0, 0, 0.4); } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score-1 { /* Green */ background-color: #627c50; } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score-1:hover { /* Green, active -> Red */ background-color: #905051; } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score--1 { /* Red */ background-color: #905051; } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score--1:hover { /* Red, active -> [Light] Grey */ background-color: rgba(0, 0, 0, .1); text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score-now-0:hover { /* Grey, active */ text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score-now-1.NB-score-1:hover { /* Green, active */ background-color: #627c50; color: white; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + .NB-dark .NB-feed-story .NB-feed-story-tag.NB-score-now--1.NB-score--1:hover { /* Red, active */ background-color: #905051; @@ -1371,6 +1467,7 @@ color: #fff; .NB-dark .NB-feed-story a.NB-feed-story-title { color: #d0d0d0; } + .NB-dark .NB-feed-story.read a.NB-feed-story-title { color: #c0c0c0; } @@ -1390,22 +1487,23 @@ color: #fff; .NB-dark .NB-sideoption-save, .NB-dark .NB-sideoption-share { - border: 1px solid #303739; + border: 1px solid #303739; } /* Save story and share window. Tag/Comment title */ .NB-dark .NB-sideoption-save .NB-sideoption-save-title, -.NB-dark .NB-sideoption-share .NB-sideoption-share-title{ +.NB-dark .NB-sideoption-share .NB-sideoption-share-title { text-shadow: none; color: #c0c0c0; } .NB-dark ul.tagit { - border-color: #303739; + border-color: #303739; } + /* Tag button and "Add 1 story tag" button */ .NB-dark ul.tagit li.tagit-choice, -.NB-dark .NB-sideoption-save .NB-sideoption-save-populate{ +.NB-dark .NB-sideoption-save .NB-sideoption-save-populate { color: #c0c0c0; background-color: #303739; border-color: rgba(255, 255, 255, .1) transparent rgba(0, 0, 0, .1); @@ -1444,19 +1542,18 @@ color: #fff; color: #c0c0c0; } -.NB-dark .NB-sideoption-share-comments{ +.NB-dark .NB-sideoption-share-comments { color: #000; background: #777; border: 1px solid #90928b; } -.NB-dark .NB-sideoption:hover, -.NB-dark .NB-sideoption.NB-active, -.NB-dark .NB-story-starred -.NB-dark .NB-sideoption.NB-feed-story-save, +.NB-dark .NB-sideoption:hover, +.NB-dark .NB-sideoption.NB-active, +.NB-dark .NB-story-starred .NB-dark .NB-sideoption.NB-feed-story-save, .NB-dark .NB-story-shared .NB-sideoption.NB-feed-story-share { - background-color: #303739; + background-color: #303739; } /* Story comment teaser (Top of story) and story comment (bottom of story) color */ @@ -1464,7 +1561,7 @@ color: #fff; .NB-dark .NB-feed-story-comments { background-color: #222425; border-color: #18191A; - text-shadow: 0 1px 0 rgba(0,0,0,.8); + text-shadow: 0 1px 0 rgba(0, 0, 0, .8); } /* Story comment border color */ @@ -1490,7 +1587,7 @@ background: #191b1c; /* Story commenter username color */ .NB-dark .NB-story-comment .NB-story-comment-username { color: #83b4e0; - text-shadow: 0 -1px 0 rgba(0,0,0,.5); + text-shadow: 0 -1px 0 rgba(0, 0, 0, .5); } /* Comment reply input field color */ @@ -1503,6 +1600,7 @@ background: #191b1c; .NB-dark .NB-story-comment-reply-button-wrapper { background-color: #505050; } + .NB-dark .NB-story-comment-reply-button-wrapper, .NB-dark .NB-modal-submit-green { background-image: none; @@ -1518,6 +1616,7 @@ background: #191b1c; color: rgba(255, 255, 255, .6); text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + /* ================= */ /* = NewsBlur Blog = */ /* ================= */ @@ -1561,10 +1660,10 @@ background: #191b1c; .NB-dark .modal-header, .NB-dark .modal-footer, .NB-dark .modal-inner-container, -.NB-dark .modal .modal-body label{ +.NB-dark .modal .modal-body label { color: #c0c0c0; background-color: #303739; - border:none; + border: none; } /* Header */ @@ -1585,7 +1684,7 @@ background: #191b1c; /* Sorting titles ("Category", "Replies", "Activity") */ .NB-dark .topic-list .sortable:hover { color: #c0c0c0; - background-color: #0088cc ; + background-color: #0088cc; } /* Thread excerpt */ @@ -1641,9 +1740,9 @@ a.mention, a.mention-group { /* Link counter badge and code block */ .NB-dark .badge-notification.clicks, -.NB-dark p > code, -.NB-dark li > code, -.NB-dark pre > code { +.NB-dark p>code, +.NB-dark li>code, +.NB-dark pre>code { color: #c0c0c0; background: #303739; } @@ -1666,14 +1765,14 @@ a.mention, a.mention-group { } /* "Latest" button */ -.NB-dark .nav-pills>li.active>a, +.NB-dark .nav-pills>li.active>a, .NB-dark .nav-pills>li>a.active { color: #fff; } /* "Latest", "Top, "Categories" button hover */ .NB-dark .nav-pills li a:hover { - background:none; + background: none; background-color: #e45735; } @@ -1782,7 +1881,7 @@ a.mention, a.mention-group { } /* Hamburger menu dropdown menu category titles */ -.NB-dark .menu-panel ul.menu-links li a, +.NB-dark .menu-panel ul.menu-links li a, .NB-dark .menu-panel ul li.heading a { color: #c0c0c0; } @@ -1817,6 +1916,7 @@ a.mention, a.mention-group { .NB-dark .NB-history-fetch.NB-ok { color: #499433; } + .NB-dark .NB-history-fetch.NB-errorcode { color: #953524; } diff --git a/media/css/reader/modals.css b/media/css/reader/modals.css index 115c3a0720..77c810d860 100644 --- a/media/css/reader/modals.css +++ b/media/css/reader/modals.css @@ -1,4 +1,3 @@ - /* ================ */ /* = Modal Dialog = */ /* ================ */ @@ -54,12 +53,14 @@ .NB-modal.NB-signed-out .NB-fieldset.NB-anonymous-ok { opacity: 1; } + fieldset { border: 0; border-top: 1px solid #E6E6E6; padding: 0 12px; margin: 12px 0 0; } + fieldset legend { padding: 4px 16px; font-weight: bold; @@ -117,6 +118,7 @@ fieldset legend { float: left; margin: 0 12px 0 0; } + .NB-modal .NB-icon-dropdown { display: inline-block; padding: 0 6px; @@ -126,6 +128,7 @@ fieldset legend { background-size: 8px; cursor: pointer; } + .NB-modal h5, .NB-module h5, .NB-module-header { @@ -133,7 +136,7 @@ fieldset legend { padding: 14px 12px; display: flex; align-items: center; - + background-color: #EAECE5; border-radius: 3px; text-shadow: 0 1px 0 rgba(255, 255, 255, .5); @@ -144,10 +147,12 @@ fieldset legend { text-align: center; position: relative; } + .NB-module h5, .NB-splash-modules .NB-module-header { margin: 0 0 24px; } + .NB-modal h5 { border: none; border-radius: 4px 4px 0 0; @@ -159,8 +164,7 @@ fieldset legend { clear: both; } -.NB-modal .NB-modal-field h5 { -} +.NB-modal .NB-modal-field h5 {} .NB-modal .NB-modal-field input[type=text] { width: 446px; @@ -169,9 +173,9 @@ fieldset legend { padding: 2px; margin: 0px 4px 6px; border: 1px solid #606060; - -moz-box-shadow:2px 2px 0 rgba(50, 50, 50, 0.15); - -webkit-box-shadow:2px 2px 0 rgba(50, 50, 50, 0.15); - box-shadow:2px 2px 0 rgba(50, 50, 50, 0.15); + -moz-box-shadow: 2px 2px 0 rgba(50, 50, 50, 0.15); + -webkit-box-shadow: 2px 2px 0 rgba(50, 50, 50, 0.15); + box-shadow: 2px 2px 0 rgba(50, 50, 50, 0.15); } .NB-modal .NB-modal-field input[type=checkbox] { @@ -186,6 +190,7 @@ fieldset legend { color: #7E0418; padding: 0 0 12px 0; } + .NB-modal .NB-modal-title { font-weight: bold; color: #303030; @@ -193,18 +198,20 @@ fieldset legend { } .NB-modal .NB-modal-subtitle { - margin:12px 0 0; - padding:8px 12px; + margin: 12px 0 0; + padding: 8px 12px; font-size: 14px; position: relative; background-color: #F8F8F6; overflow: hidden; clear: both; } + .NB-modal .NB-modal-feed-title { margin: 0; float: left; } + .NB-modal .NB-modal-feed-subscribers { font-size: 12px; background-color: #909090; @@ -221,6 +228,7 @@ fieldset legend { text-shadow: none; float: left; } + .NB-modal .NB-modal-feed-heading { display: block; margin: 0 40px 0 23px; @@ -235,17 +243,17 @@ fieldset legend { } .NB-modal .NB-modal-subtitle-right { - float: right; - clear: both; - font-size: 11px; - text-transform: uppercase; - color: #d0d0d0; - text-shadow: 0 1px 0 rgba(255, 255, 255, .5); + float: right; + clear: both; + font-size: 11px; + text-transform: uppercase; + color: #d0d0d0; + text-shadow: 0 1px 0 rgba(255, 255, 255, .5); } .NB-modal .NB-modal-subtitle-right-count { - color: #A8A8A8; - padding-right: 4px; + color: #A8A8A8; + padding-right: 4px; } .NB-modal-submit-bottom { @@ -284,10 +292,12 @@ fieldset legend { font-weight: bold; color: #FCFCFC; } + .NB-modal-submit-green:hover { background-color: #216200; background-image: linear-gradient(to top, #1E5000, #216200); } + .NB-modal-submit-green:active { background-color: #1E5000; background-image: linear-gradient(to top, #1B3F00, #1E5000); @@ -299,10 +309,12 @@ fieldset legend { color: #E4E4E4; font-weight: bold; } + .NB-modal-submit-red:hover { background-color: #7D0D25; background-image: linear-gradient(to top, #68001B, #68001B); } + .NB-modal-submit-red:active { background-color: #68001B; background-image: linear-gradient(to top, #540015, #68001B); @@ -318,6 +330,7 @@ fieldset legend { background-image: linear-gradient(to top, #CBCAD3, #d5d4dB); color: #505050; } + .NB-modal-submit-back:hover, .NB-modal-submit-reset:hover, .NB-modal-submit-close:hover, @@ -325,6 +338,7 @@ fieldset legend { background-color: #CBCAD3; background-image: linear-gradient(to top, #BDBCC7, #CBCAD3); } + .NB-modal-submit-back:active, .NB-modal-submit-reset:active, .NB-modal-submit-close:active, @@ -332,6 +346,7 @@ fieldset legend { background-color: #BDBCC7; background-image: linear-gradient(to top, #ADACBB, #BDBCC7); } + .NB-modal-submit-save.NB-disabled, .NB-modal-submit-green.NB-disabled, .NB-modal-submit-button.NB-disabled, @@ -351,50 +366,41 @@ fieldset legend { .NB-modal-submit-close { background-color: #525255; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.16, #192716), - color-stop(0.84, #2E432B) - ); - background-image: -moz-linear-gradient( - center bottom, - #192716 16%, - #2E432B 84% - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.16, #192716), + color-stop(0.84, #2E432B)); + background-image: -moz-linear-gradient(center bottom, + #192716 16%, + #2E432B 84%); color: white; } + .NB-modal-submit-close:hover { background-color: #CBCAD3; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.16, #131D10), - color-stop(0.84, #192716) - ); - background-image: -moz-linear-gradient( - center bottom, - #131D10 16%, - #192716 84% - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.16, #131D10), + color-stop(0.84, #192716)); + background-image: -moz-linear-gradient(center bottom, + #131D10 16%, + #192716 84%); } + .NB-modal-submit-close:active { background-color: #BDBCC7; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.16, #0A0D09), - color-stop(0.84, #131D10) - ); - background-image: -moz-linear-gradient( - center bottom, - #0A0D09 16%, - #131D10 84% - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.16, #0A0D09), + color-stop(0.84, #131D10)); + background-image: -moz-linear-gradient(center bottom, + #0A0D09 16%, + #131D10 84%); } + .NB-modal-submit-close, .NB-modal-submit-grey { font-weight: bold; @@ -417,6 +423,7 @@ fieldset legend { margin: 24px 0 0; overflow: hidden; } + .NB-modal .NB-fieldset:first-child { margin-top: 0; } @@ -432,6 +439,7 @@ fieldset legend { float: right; margin: 3px 0 12px; } + .NB-modal .NB-modal-feed-chooser-container .NB-modal-feed-chooser { width: 250px; margin: 0 0 12px; @@ -463,9 +471,11 @@ fieldset legend { .NB-modal .NB-modal-tab:hover { background-color: #E4E5DF; } + .NB-modal .NB-modal-tab:first-child { border-top-left-radius: 2px; } + .NB-modal .NB-modal-tab:last-child { border-right: 1px solid #A0A0A0; border-top-right-radius: 2px; @@ -491,8 +501,9 @@ fieldset legend { font-size: 14px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; - box-sizing: border-box; + box-sizing: border-box; } + .NB-modal .NB-tab.NB-active { display: block; } diff --git a/media/css/reader/reader.css b/media/css/reader/reader.css index fb748dba96..917d737a0c 100644 --- a/media/css/reader/reader.css +++ b/media/css/reader/reader.css @@ -3,7 +3,8 @@ /* ========== */ body { - /*resets*/margin: 0; + /*resets*/ + margin: 0; padding: 0; border: 0; outline: 0; @@ -40,6 +41,7 @@ body { width: 100%; position: absolute; } + a, a:active, a:hover, a:visited, button { outline: none; } @@ -49,23 +51,26 @@ a img { } .NB-hidden { - display: none; + display: none; } .NB-group { clear: both; overflow: hidden; } + .NB-left { - float: left; + float: left; } + .NB-right { - float: right; + float: right; } + .NB-raquo { - font-size: 18px; - vertical-align: baseline; - line-height: 12px; + font-size: 18px; + vertical-align: baseline; + line-height: 12px; } hr { @@ -96,44 +101,57 @@ hr { .right-pane { display: none !important; } + .NB-show-reader .right-pane { display: block !important; } + .NB-splash { display: block; } + .NB-show-reader .NB-splash { display: none; } + #NB-splash { display: block; background-color: #F6F8F0; } + .NB-show-reader #NB-splash { display: none; } + #NB-splash-overlay { display: block; } + .NB-show-reader #NB-splash-overlay { display: none; } + .NB-splash-bottom { display: block; } + .NB-show-reader .NB-splash-bottom { display: none; } + .NB-show-reader .NB-feeds-header-collapse-sidebar { opacity: 1; cursor: pointer; } + .NB-show-reader .NB-feeds-header-collapse-sidebar:hover img { opacity: .9; } + .NB-show-reader .NB-welcome-footer { display: none; } + .NB-splash-heading { display: none; } @@ -151,6 +169,7 @@ hr { flex: 1; margin: 24px 24px 32px 0; } + .NB-dashboard-account { margin: 0 24px; display: flex; @@ -162,17 +181,21 @@ hr { flex: 1; margin: 0 24px 12px 0; } + .NB-account-right { flex: 1.2; margin: 0 0 14px 0; } + .NB-account { display: flex; flex-wrap: wrap; } + .NB-account .NB-module-header { flex-basis: 100%; } + .NB-account .NB-module-header-login { width: 142px; margin: 0 50px 0 0; @@ -217,10 +240,12 @@ hr { text-align: center; width: 190px; } + .NB-account .NB-import-signup-text h3 { margin-top: 48px; color: #20843D; } + .NB-account .NB-import-signup-text p { color: #636363; } @@ -237,6 +262,7 @@ hr { .NB-account .NB-signup-orline-reduced { margin: 0px auto 0px; } + .NB-account .NB-signup-orline .NB-signup-orline-or { padding: 0 4px; } @@ -250,6 +276,7 @@ hr { font-size: 9px; line-height: 17px; } + .NB-account .NB-signup-hidden { display: none; } @@ -267,12 +294,12 @@ hr { .NB-account input[type=text], .NB-account input[type=password] { - border:1px solid #D3D5DE; - display:block; - font-size:13px; - margin:0 0 12px; - padding:5px; - width:134px; + border: 1px solid #D3D5DE; + display: block; + font-size: 13px; + margin: 0 0 12px; + padding: 5px; + width: 134px; } .NB-account input[type=text]:focus, @@ -287,8 +314,8 @@ hr { .NB-account label, .NB-account .NB-account-label { margin: 0; - color:#A0B0C0; - font-size:12px; + color: #A0B0C0; + font-size: 12px; display: block; text-transform: uppercase; } @@ -432,90 +459,97 @@ hr { } .NB-feedlists .NB-socialfeeds .feed { - background-color: #d6e3da; - /* border-top-color: #E6ECE8; */ - /* border-bottom-color: #E6ECE8; */ + background-color: #d6e3da; + /* border-top-color: #E6ECE8; */ + /* border-bottom-color: #E6ECE8; */ } + .NB-feedlists .NB-socialfeeds { display: none; /* border-bottom: 1px solid #B7BBAA; */ background-color: #d6e3da; } + .NB-feedlists .NB-socialfeeds .feed .feed_title { text-shadow: 0 1px 0 rgba(250, 250, 250, .4); } + /* ============= */ /* = Feed List = */ /* ============= */ .NB-feeds-list-loader, .NB-feeds-list-error { - background: transparent url("/media/embed/reader/sun_loader_light.svg") no-repeat 0 0; - background-size: 52px; - color: rgba(0, 0, 0, .2); - font-size: 16px; - height: 51px; - left: 5%; - padding: 5px 0 0 62px; - position: absolute; - text-shadow: 0 1px 0 rgba(255, 255, 255, .4); - text-transform: uppercase; - top: 40%; - width: 125px; - z-index: 10; - cursor: default; + background: transparent url("/media/embed/reader/sun_loader_light.svg") no-repeat 0 0; + background-size: 52px; + color: rgba(0, 0, 0, .2); + font-size: 16px; + height: 51px; + left: 5%; + padding: 5px 0 0 62px; + position: absolute; + text-shadow: 0 1px 0 rgba(255, 255, 255, .4); + text-transform: uppercase; + top: 40%; + width: 125px; + z-index: 10; + cursor: default; } + .NB-feeds-list-error { display: none; background: transparent url("/media/embed/reader/warning.gif") no-repeat 16px 8px; - background-size: 32px; + background-size: 32px; } + .NB-button.NB-feeds-list-retry { margin: 14px 0; } + .NB-feeds-list-empty { - background: transparent url("/media/embed/reader/big_world.png") no-repeat center 0; - background-size: 64px; - color: rgba(0, 0, 0, .4); - filter: opacity(0.4); - font-size: 16px; - padding: 78px 16px 0; - margin: 48px 0; - text-shadow: 0 1px 0 rgba(255, 255, 255, .4); - top: 40%; - width: 100%; - z-index: 10; - cursor: default; - line-height: 20px; - font-weight: bold; - text-align: center; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + background: transparent url("/media/embed/reader/big_world.png") no-repeat center 0; + background-size: 64px; + color: rgba(0, 0, 0, .4); + filter: opacity(0.4); + font-size: 16px; + padding: 78px 16px 0; + margin: 48px 0; + text-shadow: 0 1px 0 rgba(255, 255, 255, .4); + top: 40%; + width: 100%; + z-index: 10; + cursor: default; + line-height: 20px; + font-weight: bold; + text-align: center; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + .NB-story-list-empty { - color: rgba(0, 0, 0, .4); - filter: opacity(0.4); - font-size: 16px; - padding: 256px 24px; - text-shadow: 0 1px 0 rgba(255, 255, 255, .4); - width: 100%; - z-index: 10; - cursor: default; - line-height: 20px; - font-weight: bold; - text-align: center; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + color: rgba(0, 0, 0, .4); + filter: opacity(0.4); + font-size: 16px; + padding: 256px 24px; + text-shadow: 0 1px 0 rgba(255, 255, 255, .4); + width: 100%; + z-index: 10; + cursor: default; + line-height: 20px; + font-weight: bold; + text-align: center; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .NB-story-list-empty .NB-world { - background: transparent url("/media/embed/reader/big_world.png") no-repeat center 0; - background-size: 64px; - width: 100%; - height: 64px; - margin: 24px 0; + background: transparent url("/media/embed/reader/big_world.png") no-repeat center 0; + background-size: 64px; + width: 100%; + height: 64px; + margin: 24px 0; } @@ -525,11 +559,11 @@ hr { } .NB-feedlists ::-moz-selection { - background: transparent; + background: transparent; } .NB-feedlists ::selection { - background: transparent; + background: transparent; } .NB-feedlist { @@ -543,23 +577,29 @@ hr { width: auto; height: auto; } + .NB-theme-feed-size-xs .NB-feedlist { font-size: 11px; } + .NB-theme-feed-size-m .NB-feedlist { font-size: 12px; } + .NB-theme-feed-size-l .NB-feedlist { font-size: 13px; } + .NB-theme-feed-size-xl .NB-feedlist { font-size: 14px; } + #feed_list { display: none; padding: 0; overflow: visible; } + .NB-feedlist li.folder { padding: 0px 0 0; margin: 0px 0 0; @@ -570,6 +610,7 @@ hr { padding: 0 0 0 25px; list-style: none; } + .NB-feedlist ul.folder.NB-root { padding-left: 0; } @@ -595,16 +636,20 @@ hr { left: 10px; top: 4px; } + .NB-theme-feed-size-m .NB-feedlist li.folder .NB-folder-icon { top: 5px; } + .NB-theme-feed-size-l .NB-feedlist li.folder .NB-folder-icon { top: 5px; } + .NB-theme-feed-size-xl .NB-feedlist li.folder .NB-folder-icon { top: 6px; } -.NB-feedlist li.folder.NB-folder-collapsed .NB-folder-icon{ + +.NB-feedlist li.folder.NB-folder-collapsed .NB-folder-icon { background: transparent url('/media/embed/icons/nouns/folder-closed.svg') no-repeat 0 0; background-size: 16px; } @@ -625,41 +670,45 @@ hr { } .NB-feedlist .feed.NB-feed-exception { -/* background-color: #F7EDC6;*/ - + /* background-color: #F7EDC6;*/ + } + .NB-feedlist .feed.NB-feed-exception .feed_title { - color: #A0A0A0; + color: #A0A0A0; } .NB-feedlist .feed.NB-feed-self-blurblog, .NB-feedlist-hide-read-feeds .NB-feedlist .feed.NB-feed-self-blurblog { display: block; } + .NB-intelligence-starred .NB-feedlist .feed.NB-feed-self-blurblog { display: none; } -.NB-feedlist .feed.NB-feed-unfetched { -} + +.NB-feedlist .feed.NB-feed-unfetched {} .NB-feedlist .feed.NB-feed-exception .feed_counts { - display: none; + display: none; } .NB-feedlist .feed .NB-feed-exception-icon { - background: url('/media/embed/icons/circular/exclamation.png') no-repeat 0 0; - background-size: 16px; - width: 16px; - height: 16px; - position: absolute; - right: 4px; - top: 2px; - display: none; + background: url('/media/embed/icons/circular/exclamation.png') no-repeat 0 0; + background-size: 16px; + width: 16px; + height: 16px; + position: absolute; + right: 4px; + top: 2px; + display: none; } + #feed_list .feed.NB-feed-exception .NB-feed-exception-icon, .NB-modal-organizer .feed.NB-feed-exception .NB-feed-exception-icon { display: block; } + .NB-feedlist .feed .NB-feed-highlight { width: 100%; height: 100%; @@ -670,43 +719,51 @@ hr { display: none; opacity: 0; } + .NB-feedlist .feed.NB-feed-unfetched:not(.NB-highlighted) .feed_counts { - display: none; + display: none; } + .NB-modal-feedchooser .NB-feedlist .feed.NB-feed-unfetched:not(.NB-highlighted) .feed_counts { display: block; -} +} + .NB-feedlist .feed.NB-feed-unfetched .feed_favicon { - opacity: .5; + opacity: .5; } + .NB-feedlist .feed.NB-feed-unfetched .feed_title { - color: #A0A0A0; + color: #A0A0A0; } + .NB-feedlist .feed .NB-feed-unfetched-icon { background: transparent url('/media/embed/reader/ring_spinner.svg') no-repeat 0 0; background-size: 16px; - opacity: .1; - width: 16px; - height: 16px; - position: absolute; - right: 4px; - top: 4px; - display: none; + opacity: .1; + width: 16px; + height: 16px; + position: absolute; + right: 4px; + top: 4px; + display: none; } + #feed_list .feed.NB-feed-unfetched .NB-feed-unfetched-icon { - display: block; + display: block; } .NB-feedlist .feed.NB-empty { - border: none; - margin: 0; - padding: 0; - height: 4px; - cursor: default; + border: none; + margin: 0; + padding: 0; + height: 4px; + cursor: default; } + .NB-feedlist.NB-feed-sorting .feed.NB-empty { -/* height: 16px;*/ + /* height: 16px;*/ } + .feed.NB-feed-sorting, .NB-feedlist.NB-feed-sorting { cursor: move !important; @@ -715,9 +772,11 @@ hr { .NB-feedlist .feed_id { display: none; } + img.feed_favicon { border-radius: 4px; } + .NB-feedlist img.feed_favicon { position: absolute; top: 4px; @@ -726,46 +785,56 @@ img.feed_favicon { height: 16px; transition: all 0.36s ease-out; } + .NB-density-compact .NB-feedlist img.feed_favicon { top: 1px; } + .NB-theme-feed-size-s .NB-feedlist img.feed_favicon { top: 4px; } + .NB-density-compact.NB-theme-feed-size-s .NB-feedlist img.feed_favicon { top: 4px; } + .NB-theme-feed-size-m .NB-feedlist img.feed_favicon { top: 6px; } + .NB-density-compact.NB-theme-feed-size-m .NB-feedlist img.feed_favicon { top: 4px; } + .NB-theme-feed-size-l .NB-feedlist img.feed_favicon { top: 7px; } + .NB-density-compact.NB-theme-feed-size-l .NB-feedlist img.feed_favicon { top: 5px; } + .NB-theme-feed-size-xl .NB-feedlist img.feed_favicon { top: 8px; } + .NB-density-compact.NB-theme-feed-size-xl .NB-feedlist img.feed_favicon { top: 6px; } + .NB-feedlist .feed_title { display: block; padding: 4px 40px 4px 26px; text-decoration: none; - color: rgba(0,0,0,.7); + color: rgba(0, 0, 0, .7); line-height: 18px; overflow: hidden; text-shadow: 0 1px 0 rgba(250, 250, 250, .4); - + text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; - -webkit-box-orient: vertical; + -webkit-box-orient: vertical; overflow: hidden; word-break: break-all; @@ -776,18 +845,23 @@ img.feed_favicon { padding-top: 2px; padding-bottom: 2px; } + .NB-theme-feed-size-xs .NB-feedlist .feed_title { line-height: 16px; } + .NB-theme-feed-size-s .NB-feedlist .feed_title { line-height: 18px; } + .NB-theme-feed-size-m .NB-feedlist .feed_title { line-height: 20px; } + .NB-theme-feed-size-l .NB-feedlist .feed_title { line-height: 22px; } + .NB-theme-feed-size-xl .NB-feedlist .feed_title { line-height: 24px; } @@ -803,6 +877,7 @@ img.feed_favicon { background: transparent url('/media/embed/icons/nouns/right.svg') no-repeat 4px 6px; background-size: 8px; } + .NB-feedlist .folder_title .NB-feedlist-manage-icon { background-position: 4px 6px; } @@ -812,23 +887,29 @@ img.feed_favicon { background: transparent url('/media/embed/icons/nouns/down.svg') no-repeat 4px 6px; background-size: 8px; } + .NB-feedlist .folder_title .NB-feedlist-manage-icon:hover { background-position: 4px 5px; } + .NB-feedlist .feed.NB-hover-inverse .NB-feedlist-manage-icon:hover, .NB-feedlist .folder.NB-hover-inverse .NB-feedlist-manage-icon:hover { background: transparent url('/media/embed/icons/nouns/up.svg') no-repeat 4px 8px; background-size: 8px; } + .NB-feedlist .folder.NB-hover-inverse .NB-feedlist-manage-icon:hover { background-position: 6px 8px; } + .NB-feedlist .feed.NB-hover-inverse .NB-feedlist-manage-icon:hover { background-position: 4px 8px; } + .NB-feedlist .feed.NB-toplevel.NB-hover-inverse .NB-feedlist-manage-icon:hover { - background-position: 5px 7px; + background-position: 5px 7px; } + .NB-feedlist .feed.NB-toplevel .NB-feedlist-manage-icon, .NB-feedlist .folder_title.NB-toplevel .NB-feedlist-manage-icon { left: 3px; @@ -836,15 +917,18 @@ img.feed_favicon { display: none; background-position: 6px 7px; } + .NB-feedlist .folder_title.NB-toplevel .NB-feedlist-manage-icon { left: 8px; background-position: 6px 8px; } + .NB-feedlist .feed:hover .NB-feedlist-manage-icon, .NB-feedlist .folder_title:hover .NB-feedlist-manage-icon { opacity: 1; display: block; } + .NB-feedlist .feed.NB-no-hover .NB-feedlist-manage-icon, .NB-feedlist .folder_title.NB-no-hover .NB-feedlist-manage-icon { display: none; @@ -867,15 +951,19 @@ img.feed_favicon { opacity: .6; display: none; } + .NB-feeds-header .NB-feedlist-collapse-icon { top: 4px; } + .NB-theme-feed-size-xs .NB-feedlist .folder .folder_title .NB-feedlist-collapse-icon { top: -1px; } + .NB-theme-feed-size-xl .NB-feedlist .folder .folder_title .NB-feedlist-collapse-icon { top: 1px; } + .NB-feedlist .folder.NB-folder-collapsed .folder_title .NB-feedlist-collapse-icon, .NB-feeds-header.NB-folder-collapsed .NB-feedlist-collapse-icon { background-image: url('/media/embed/icons/circular/folder_expand.png'); @@ -890,16 +978,17 @@ img.feed_favicon { .NB-feedlist .folder .folder_title:hover .NB-feedlist-collapse-icon, .NB-feeds-header:hover .NB-feedlist-collapse-icon { -/*.NB-feedlist .folder.NB-showing-menu > .folder_title .NB-feedlist-collapse-icon {*/ + /*.NB-feedlist .folder.NB-showing-menu > .folder_title .NB-feedlist-collapse-icon {*/ display: block; opacity: .6; } .NB-feedlist .folder .folder_title:hover .feed_counts_floater, .NB-feeds-header:hover .feed_counts_floater, -.NB-feedlist .folder.NB-showing-menu > .folder_title .feed_counts_floater { +.NB-feedlist .folder.NB-showing-menu>.folder_title .feed_counts_floater { margin-right: 24px; } + .NB-feedlist .folder .folder_title.NB-feedlist-folder-title-recently-collapsed:hover .feed_counts_floater, .NB-feeds-header.NB-feedlist-folder-title-recently-collapsed:hover .feed_counts_floater { display: block; @@ -910,16 +999,19 @@ img.feed_favicon { opacity: 1; } */ - + .NB-feedlist .feed.NB-toplevel:hover .feed_favicon { display: none; } + .NB-feedlist .feed.NB-toplevel.NB-no-hover .feed_favicon { display: block; } + .NB-feedlist .folder_title.NB-toplevel:hover { background: none; } + .NB-feedlist .folder_title.NB-toplevel.NB-no-hover { background: inherit; } @@ -930,29 +1022,35 @@ img.feed_favicon { top: 3px; transition: top 0.36s ease-out; } + .NB-density-compact .NB-feedlist .feed_counts { top: 1px; } + .NB-feedlist .NB-feedbar-mark-feed-read { display: none; } + .NB-feedlist .feed.selected, .NB-feedlist .feed.NB-selected, -.NB-feedlist .folder.NB-selected > .folder_title { +.NB-feedlist .folder.NB-selected>.folder_title { background-color: #FFFFD2; /* background-color: #c9e5fb; */ border-radius: 8px; } + .NB-feeds-header.NB-selected { background-color: #FFFFD2; } -.NB-feedlist .folder.NB-selected > .folder_title { + +.NB-feedlist .folder.NB-selected>.folder_title { text-shadow: 0 1px 0 rgba(255, 255, 255, .5); } + .NB-feedlist .feed.NB-feed-selector-selected { background-color: #7AC0FE; background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#A7D3FE), to(#7AC0FE)); - background: -moz-linear-gradient(center top , #A7D3FE 0%, #7AC0FE 100%); + background: -moz-linear-gradient(center top, #A7D3FE 0%, #7AC0FE 100%); border-top: 1px solid #789FC6; border-bottom: 1px solid #789FC6; } @@ -970,50 +1068,59 @@ img.feed_favicon { } .NB-feedlist-hide-read-feeds .NB-feedlist .feed { - display: none; + display: none; } + .NB-feedlist-hide-read-feeds .unread_view_starred .unread_starred { - display: block; + display: block; } + .NB-feedlist-hide-read-feeds .unread_view_positive .unread_positive { - display: block; + display: block; } + .NB-feedlist-hide-read-feeds .unread_view_neutral .unread_positive, .NB-feedlist-hide-read-feeds .unread_view_neutral .unread_neutral { - display: block; + display: block; } + .NB-feedlist-hide-read-feeds .unread_view_negative .unread_positive, .NB-feedlist-hide-read-feeds .unread_view_negative .unread_neutral, .NB-feedlist-hide-read-feeds .unread_view_negative .unread_negative { - display: block; + display: block; } .NB-feedlist-hide-read-feeds .NB-feedlist .feed.NB-empty { - display: block; + display: block; } .NB-feedlist-hide-read-feeds .unread_view_neutral .NB-feed-inactive, .NB-feedlist-hide-read-feeds .unread_view_positive .NB-feed-inactive { - display: none; + display: none; } + .NB-feedlist-hide-read-feeds .NB-feedlist .feed.selected { display: block; } + #feed_list.NB-feedlist.NB-selector-active .feed, .NB-sidebar .NB-socialfeeds-folder.NB-selector-active .feed { display: none; } + #feed_list.NB-feedlist.NB-selector-active .feed.NB-feed-selector-active, .NB-socialfeeds-folder.NB-selector-active .feed.NB-feed-selector-active { display: block; opacity: 1; } + .NB-feedlist.NB-selector-active .NB-folder-collapsed .folder, .NB-feedlist.NB-selector-active .NB-hidden, .NB-socialfeeds-folder.NB-selector-active { display: block !important; opacity: 1 !important; } + .NB-selector-active .NB-feeds-list-empty { display: none; } @@ -1034,50 +1141,60 @@ img.feed_favicon { border-radius: 4px; transition: all 0.36s ease-out; -/* border-top: 1px solid rgba(255, 255, 255, .4);*/ + /* border-top: 1px solid rgba(255, 255, 255, .4);*/ /* border-bottom: 1px solid rgba(0, 0, 0, .1); */ } + .NB-theme-feed-size-xs .NB-feedlist .unread_count { margin-top: 0px; padding-top: 2px; padding-bottom: 2px; } + .NB-theme-feed-size-s .NB-feedlist .unread_count { margin-top: 1px; } + .NB-theme-feed-size-l .NB-feedlist .unread_count { margin-top: 2px; padding-top: 3px; padding-bottom: 3px; } + .NB-theme-feed-size-xl .NB-feedlist .unread_count { margin-top: 3px; padding-top: 4px; padding-bottom: 2px; } + .NB-feeds-header .unread_count { line-height: 11px; } + .NB-theme-feed-size-xs .NB-feeds-header .unread_count { margin-top: 9px; padding-top: 3px; padding-bottom: 2px; } + .NB-theme-feed-size-s .NB-feeds-header .unread_count { margin-top: 9px; padding-top: 2px; padding-bottom: 2px; } + .NB-theme-feed-size-m .NB-feeds-header .unread_count { margin-top: 9px; padding-top: 2px; padding-bottom: 2px; } + .NB-theme-feed-size-l .NB-feeds-header .unread_count { margin-top: 6px; padding-top: 4px; padding-bottom: 3px; } + .NB-theme-feed-size-xl .NB-feeds-header .unread_count { margin-top: 7px; padding-top: 4px; @@ -1087,54 +1204,60 @@ img.feed_favicon { .folder_title .unread_count { line-height: 15px; } + .NB-theme-feed-size-xs .folder_title .unread_count { margin-top: -3px; padding-top: 2px; padding-bottom: 1px; } + .NB-theme-feed-size-s .folder_title .unread_count { margin-top: -2px; padding-top: 2px; padding-bottom: 1px; } + .NB-theme-feed-size-m .folder_title .unread_count { margin-top: -2px; padding-top: 2px; padding-bottom: 2px; } + .NB-theme-feed-size-l .folder_title .unread_count { margin-top: -2px; padding-top: 2px; padding-bottom: 2px; } + .NB-theme-feed-size-xl .folder_title .unread_count { margin-top: -2px; padding-top: 3px; padding-bottom: 3px; } + .unread_count_starred { background-color: #506B9A; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); border-bottom: 1px solid rgba(0, 0, 0, .2); -/* text-shadow: none;*/ + /* text-shadow: none;*/ } .unread_count_positive { background-color: #6EA74A; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); -/* text-shadow: none;*/ + /* text-shadow: none;*/ } .unread_count_neutral { background-color: #B3B6AD; -/* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ + /* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ text-shadow: none; } .unread_count_negative { background-color: #CC2A2E; text-shadow: none; -/* text-shadow: 0 1px 0 rgba(0, 0, 0, .3);*/ + /* text-shadow: 0 1px 0 rgba(0, 0, 0, .3);*/ } .unread_view_starred .unread_count { @@ -1157,6 +1280,7 @@ img.feed_favicon { .unread_view_starred .unread_starred .unread_count_starred { display: block; } + .unread_view_positive .unread_positive .unread_count_positive { display: block; } @@ -1175,6 +1299,7 @@ img.feed_favicon { .unread_view_starred .unread_starred { font-weight: bold; } + .unread_view_positive .unread_positive { font-weight: bold; } @@ -1206,128 +1331,138 @@ img.feed_favicon { .NB-starred-folder .unread_starred .unread_count_positive { display: block; } + .unread_view_neutral .NB-feed-inactive.unread_neutral .unread_count_neutral, .unread_view_neutral .NB-feed-inactive.unread_positive .unread_count_positive, .unread_view_positive .NB-feed-inactive.unread_positive .unread_count_positive { - display: none; + display: none; } + .NB-feed-inactive .NB-muted-icon { - display: block; - float: right; - width: 20px; - height: 20px; - margin: 1px 1px 0; - background-image: url(/media/img/reader/mute_grey.png); - background-repeat: no-repeat; - background-position: center center; - background-size: contain; - background-color: inherit; - border: none; + display: block; + float: right; + width: 20px; + height: 20px; + margin: 1px 1px 0; + background-image: url(/media/img/reader/mute_grey.png); + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + background-color: inherit; + border: none; } + .NB-modal-feedchooser .NB-muted-icon { - display: none; + display: none; } + /* ====================== */ /* = Feeds Progress Bar = */ /* ====================== */ #NB-progress { - height: 40px; - width: 100%; - background-color: #505050; - border-top: 1px solid #777D86; - border-bottom: 1px solid #101010; - color: #fff; - text-shadow: 0 1px 0 #202020; - text-align: center; - z-index: 1; + height: 40px; + width: 100%; + background-color: #505050; + border-top: 1px solid #777D86; + border-bottom: 1px solid #101010; + color: #fff; + text-shadow: 0 1px 0 #202020; + text-align: center; + z-index: 1; } .NB-progress-container { - border-top: 1px solid #E0E0E0; + border-top: 1px solid #E0E0E0; } #NB-progress .NB-progress-close { - width: 11px; - height: 11px; - float: right; - margin: 6px 4px 0 0; - cursor: pointer; - background: transparent url('/media/embed/reader/close.png') no-repeat 0 0; + width: 11px; + height: 11px; + float: right; + margin: 6px 4px 0 0; + cursor: pointer; + background: transparent url('/media/embed/reader/close.png') no-repeat 0 0; } #NB-progress .NB-progress-title { - padding: 5px 15px 0; - font-size: 11px; - text-transform: uppercase; - height: 14px; + padding: 5px 15px 0; + font-size: 11px; + text-transform: uppercase; + height: 14px; } + #NB-progress.NB-progress-error .NB-progress-title { height: auto; } #NB-progress .NB-progress-bar { - height:6px; - margin:6px 50px 0; + height: 6px; + margin: 6px 50px 0; } #NB-progress .NB-progress-link { display: none; margin: -2px 0 0; } + #NB-progress .NB-progress-link .NB-modal-submit-button { padding: 3px 0; font-size: 15px; width: 90%; margin: 0 auto; - -moz-box-shadow:2px 2px 0px #404040; - -webkit-box-shadow:2px 2px 0px #404040; - box-shadow:2px 2px 0px #404040; + -moz-box-shadow: 2px 2px 0px #404040; + -webkit-box-shadow: 2px 2px 0px #404040; + box-shadow: 2px 2px 0px #404040; border-radius: 4px; - border:1px solid #303030; + border: 1px solid #303030; } #NB-progress.NB-progress-error { - height: 80px; + height: 80px; } + #NB-progress.NB-progress-big { - height: 72px; + height: 72px; } + #NB-progress.NB-progress-error .NB-progress-title { - font-size: 18px; + font-size: 18px; } + #NB-progress.NB-progress-error .NB-progress-link { display: block; font-size: 18px; } + #NB-progress.NB-progress-error .NB-progress-bar { display: none; } #NB-progress.NB-progress-big .NB-progress-link { - margin: 6px 0 0; + margin: 6px 0 0; } #NB-progress.NB-progress-big .NB-progress-link a { - font-size: 20px; + font-size: 20px; } #NB-progress .NB-progress-counts { - float: left; - font-size: 10px; - color: #B0B0B0; - padding: 4px 0 0; - width: 50px; + float: left; + font-size: 10px; + color: #B0B0B0; + padding: 4px 0 0; + width: 50px; } #NB-progress .NB-progress-percentage { - float: right; - clear: both; - color: #B0B0B0; - font-size: 12px; - width: 50px; - padding: 1px 0 0; -} + float: right; + clear: both; + color: #B0B0B0; + font-size: 12px; + width: 50px; + padding: 1px 0 0; +} /* ================ */ /* = Story Titles = */ @@ -1336,7 +1471,7 @@ img.feed_favicon { .NB-feed-story-header-info ::-moz-selection { background: transparent; -} +} .NB-feed-story-header-info ::selection { background: transparent; @@ -1361,35 +1496,41 @@ img.feed_favicon { .NB-story-titles-header { font-weight: bold; font-size: 16px; - padding: 6px 24px 0px 8px; + padding: 6px 24px 0px 8px; position: relative; overflow: hidden; } + .NB-sidebar-closed .NB-story-titles-header { - margin-left: 36px; + margin-left: 36px; } + .NB-story-titles-expand-sidebar { - background: url(/media/img/reader/chevron_sq_right.png) no-repeat center center; - background-size: 16px; - width: 36px; - height: 36px; - position: absolute; - top: 0; - left: 0; - border-right: 1px solid rgba(0, 0, 0, 0.1); - opacity: .7; - display: none; + background: url(/media/img/reader/chevron_sq_right.png) no-repeat center center; + background-size: 16px; + width: 36px; + height: 36px; + position: absolute; + top: 0; + left: 0; + border-right: 1px solid rgba(0, 0, 0, 0.1); + opacity: .7; + display: none; } + .NB-sidebar-closed .NB-story-titles-expand-sidebar { - display: block; + display: block; } + .NB-story-titles-expand-sidebar:hover { - opacity: 0.9; - cursor: pointer; + opacity: 0.9; + cursor: pointer; } + .NB-story-titles-expand-sidebar:active { - opacity: 1.0; + opacity: 1.0; } + .NB-feedbar { border-bottom: 1px solid #dbdbda; background-color: #F3F3EE; @@ -1403,12 +1544,14 @@ img.feed_favicon { width: 16px; height: 16px; } + .NB-feedbar .feed.NB-feed-social .feed_favicon { border-radius: 3px; } + .NB-feedbar .feed .feed_title, .NB-feedbar .folder_title_text { -/* float: left;*/ + /* float: left;*/ display: block; margin-left: 35px; color: #40413E; @@ -1419,12 +1562,14 @@ img.feed_favicon { text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; - -webkit-box-orient: vertical; + -webkit-box-orient: vertical; overflow: hidden; } + .NB-feedbar .NB-feedbar-options-container { float: right; } + .NB-feedbar .NB-feedbar-options { cursor: pointer; float: right; @@ -1438,10 +1583,12 @@ img.feed_favicon { margin: 3px 0 0; white-space: nowrap; } + .NB-feedbar .NB-feedbar-options:hover, .NB-feedbar .NB-feedbar-options.NB-active { background-color: rgba(0, 0, 0, .1); } + .NB-feedbar .NB-feedbar-options .NB-icon { float: right; width: 16px; @@ -1459,14 +1606,15 @@ img.feed_favicon { cursor: pointer; padding: 0 0 0 38px; } + .NB-feedbar .NB-feedbar-statistics { background: transparent url('/media/embed/icons/nouns/dialog-statistics.svg') no-repeat 0 0; background-size: 16px; width: 16px; height: 16px; cursor: pointer; - margin:3px 0 0 -6px; - padding:0 24px 0 6px; + margin: 3px 0 0 -6px; + padding: 0 24px 0 6px; filter: hue-rotate(284deg) saturate(18); } @@ -1478,10 +1626,12 @@ img.feed_favicon { font-weight: bold; float: right; } + .NB-intelligence-starred .NB-feedbar .NB-feedbar-mark-feed-read-container, .NB-searching .NB-feedbar-options-container { display: none; } + .NB-feedbar .NB-feedbar-mark-feed-read, .NB-feedbar .NB-feedbar-mark-feed-read-expand, .NB-feedbar .NB-feedbar-mark-feed-read-time { @@ -1495,23 +1645,25 @@ img.feed_favicon { height: 14px; z-index: 1; margin: 3px 0 0 8px; - + background-color: #F0F1EC; background-image: -webkit-gradient(linear, left top, left bottom, from(#F0F1EC), to(#EBEDE7)); - background-image: -moz-linear-gradient(center top , #F0F1EC 0%, #EBEDE7 100%); + background-image: -moz-linear-gradient(center top, #F0F1EC 0%, #EBEDE7 100%); background-image: linear-gradient(top, #F0F1EC, #EBEDE7); } + .NB-feedbar .NB-feedbar-mark-feed-read .NB-icon { - background: transparent url('/media/embed/icons/nouns/mark-read.svg') no-repeat center center; + background: transparent url('/media/embed/icons/nouns/mark-read.svg') no-repeat center center; background-size: 18px; width: 100%; height: 100%; } + .NB-feedbar .NB-feedbar-mark-feed-read:hover, .NB-feedbar .NB-feedbar-mark-feed-read-time:hover { background-color: #D8D9D4; background-image: -webkit-gradient(linear, left top, left bottom, from(#D8D9D4), to(#D4D5D0)); - background-image: -moz-linear-gradient(center top , #D8D9D4 0%, #D4D5D0 100%); + background-image: -moz-linear-gradient(center top, #D8D9D4 0%, #D4D5D0 100%); background-image: linear-gradient(top, #D8D9D4, #D4D5D0); } @@ -1523,7 +1675,7 @@ img.feed_favicon { position: absolute; right: 20px; z-index: 0; - background: transparent url('/media/embed/icons/circular/nav_icn_plus.png') no-repeat 4px center; + background: transparent url('/media/embed/icons/circular/nav_icn_plus.png') no-repeat 4px center; background-size: 6px; width: 19px; } @@ -1551,45 +1703,50 @@ img.feed_favicon { background: transparent url('/media/embed/icons/nouns/right.svg') no-repeat 6px 7px; background-size: 8px; } + .NB-feedbar .NB-feedlist-manage-icon:hover { background: transparent url('/media/embed/icons/nouns/down.svg') no-repeat 6px 7px; background-size: 8px; } + .NB-feedbar .NB-folder .NB-feedlist-manage-icon { top: 7px; left: 8px; } + .NB-feedbar .NB-hover-inverse .NB-feedlist-manage-icon:hover { background: transparent url('/media/embed/icons/nouns/up.svg') no-repeat 4px 5px; background-size: 8px; } + .NB-feedbar .NB-folder.NB-hover-inverse .NB-feedlist-manage-icon:hover { background-position: 4px 7px; } .NB-feedbar:hover .NB-feedlist-manage-icon { - display: block; - opacity: 1; + display: block; + opacity: 1; } .NB-feedbar:hover .NB-no-hover .NB-feedlist-manage-icon { - display: none; - opacity: 0; + display: none; + opacity: 0; } .NB-feedbar:hover .feed_favicon, .NB-feedbar:hover .NB-folder-icon { - display: none; + display: none; } .NB-feedbar:hover .NB-no-hover .feed_favicon, .NB-feedbar:hover .NB-no-hover .NB-folder-icon { - display: block; + display: block; } .NB-feedbar .NB-folder-fake .NB-feedbar-mark-feed-read-container { display: none; } + .NB-feedbar .NB-folder-river .NB-feedbar-mark-feed-read-container { display: block; } @@ -1682,67 +1839,74 @@ img.feed_favicon { line-height: 14px; margin: 3px 12px 0; } + .NB-feedbar .NB-story-title-indicator:hover { background-color: rgba(0, 0, 0, .1); } + .NB-feedbar .NB-story-title-indicator .NB-story-title-indicator-count { float: left; } + .NB-feedbar .NB-story-title-indicator .NB-story-title-indicator-count .unread_count_starred { display: none; } + /*.NB-feedbar .NB-story-title-indicator.unread_threshold_negative { display: none; } -*/.NB-feedbar .NB-story-title-indicator .feed_counts_floater { - float: left; - padding: 2px 0 0; - text-align: center; +*/ +.NB-feedbar .NB-story-title-indicator .feed_counts_floater { + float: left; + padding: 2px 0 0; + text-align: center; } .NB-feedbar .NB-story-title-indicator .unread_count_positive.unread_count_full, .NB-feedbar .NB-story-title-indicator .unread_count_neutral.unread_count_full, .NB-feedbar .NB-story-title-indicator .unread_count_negative.unread_count_full { - display: none; - opacity: .3; + display: none; + opacity: .3; } .NB-feedbar .NB-story-title-indicator.unread_threshold_positive .unread_count_neutral.unread_count_full, .NB-feedbar .NB-story-title-indicator.unread_threshold_positive .unread_count_negative.unread_count_full, .NB-feedbar .NB-story-title-indicator.unread_threshold_neutral .unread_count_negative.unread_count_full { - display: block; + display: block; } .NB-feedbar .NB-story-title-indicator.unread_threshold_positive:hover .unread_count_neutral { - opacity: 1; + opacity: 1; } + .NB-feedbar .NB-story-title-indicator.unread_threshold_neutral:hover .unread_count_negative { - opacity: 1; + opacity: 1; } .NB-feedbar .NB-story-title-indicator .feed_counts_floater .unread_count { - padding: 1px 3px 1px; - margin: 0 4px 0 0; - line-height: 8px; + padding: 1px 3px 1px; + margin: 0 4px 0 0; + line-height: 8px; } .NB-intelligence-positive .NB-story-title.NB-story-neutral .NB-hidden-fade, .NB-intelligence-positive .NB-story-title.NB-story-negative .NB-hidden-fade, .NB-intelligence-neutral .NB-story-title.NB-story-negative .NB-hidden-fade { - opacity: .5; + opacity: .5; } .NB-feed-story-premium-only .NB-feed-story-premium-only-divider { - background: transparent url(/media/embed/reader/separator_small.png) no-repeat 50% 100%; - height: 20px; - width: 100%; - margin: 4px 0 0; + background: transparent url(/media/embed/reader/separator_small.png) no-repeat 50% 100%; + height: 20px; + width: 100%; + margin: 4px 0 0; } + .NB-feed-story-premium-only .NB-feed-story-premium-only-text { - text-align: center; - margin: 6px 0 8px 0; - color: #707070; - font-size: 11px; + text-align: center; + margin: 6px 0 8px 0; + color: #707070; + font-size: 11px; } /* ======================== */ @@ -1763,78 +1927,97 @@ img.feed_favicon { border-top: 1px solid #FFF; border-bottom: 1px solid #F9F8F4; } + .NB-theme-feed-size-xs .NB-story-title { font-size: 12px; line-height: 14px; } + .NB-theme-feed-size-m .NB-story-title { font-size: 14px; line-height: 18px; } + .NB-theme-feed-size-l .NB-story-title { font-size: 16px; line-height: 20px; } + .NB-theme-feed-size-xl .NB-story-title { font-size: 18px; line-height: 22px; } + .NB-story-title-magazine.NB-story-title, .NB-story-title-magazine.NB-story-title .NB-storytitles-magazine-bottom { transition: padding 0.36s ease-out, margin 0.36s ease-out, transform 0.12s ease-out, border-radius 0.12s ease-out; } + .NB-theme-feed-size-xs .NB-story-title-magazine.NB-story-title { font-size: 14px; line-height: 16px; padding-top: 10px; } + .NB-theme-feed-size-s .NB-story-title-magazine.NB-story-title { font-size: 15px; line-height: 17px; padding-top: 15px; } + .NB-theme-feed-size-m .NB-story-title-magazine.NB-story-title { font-size: 16px; line-height: 19px; padding-top: 20px; } + .NB-theme-feed-size-l .NB-story-title-magazine.NB-story-title { font-size: 18px; line-height: 22px; padding-top: 25px; } + .NB-theme-feed-size-xl .NB-story-title-magazine.NB-story-title { font-size: 20px; line-height: 24px; padding-top: 30px; } + .NB-density-compact .NB-story-title-magazine.NB-story-title { padding-top: 4px; } + .NB-density-compact .NB-story-title-magazine.NB-story-title.NB-selected { padding-top: 4px; } + .NB-density-compact .NB-story-title-magazine.NB-story-title.NB-selected .NB-story-feed { margin-bottom: 6px; } + .NB-story-pane-west .NB-story-title { padding-right: 10px; padding-left: 40px; } + .NB-view-river .NB-story-title { padding-left: 178px; } + .NB-view-river .NB-story-pane-west .NB-story-title { padding-left: 66px; } + .NB-image-preview-small-right .NB-story-title.NB-has-image, .NB-image-preview-large-right .NB-story-title.NB-has-image { padding-right: 242px; } + .NB-image-preview-small-right .NB-story-pane-west .NB-story-title.NB-has-image, .NB-image-preview-large-right .NB-story-pane-west .NB-story-title.NB-has-image { padding-right: 90px; } + /* Ragged right but not ragged left .NB-image-preview-small-left .NB-story-title.NB-has-image, .NB-image-preview-large-left .NB-story-title.NB-has-image */ @@ -1842,41 +2025,50 @@ img.feed_favicon { .NB-image-preview-large-left .NB-story-title { padding-left: 116px; } + .NB-story-title.NB-story-title-grid, .NB-view-river .NB-story-title.NB-story-title-grid { padding-left: 0; } + .NB-image-preview-small-left .NB-view-river .NB-story-title, .NB-image-preview-large-left .NB-view-river .NB-story-title { padding-left: 248px; } + .NB-image-preview-small-left .NB-view-river .NB-story-pane-west .NB-story-title, .NB-image-preview-large-left .NB-view-river .NB-story-pane-west .NB-story-title { padding-left: 146px; padding-right: 6px; } + .NB-image-preview-small-left .NB-view-river .NB-story-title.NB-story-title-grid, .NB-image-preview-large-left .NB-view-river .NB-story-title.NB-story-title-grid { padding-left: 0; } + .NB-image-preview-small-left .NB-story-title-magazine.NB-story-title, .NB-image-preview-large-left .NB-story-title-magazine.NB-story-title, .NB-view-river .NB-story-title-magazine.NB-story-title { padding-left: 400px; min-height: 200px; } + .NB-image-preview-none .NB-story-title-magazine.NB-story-title { padding-left: 56px; min-height: none; } + .NB-image-preview-large-right .NB-story-title-magazine.NB-story-title, .NB-image-preview-small-right .NB-story-title-magazine.NB-story-title { padding-left: 48px; padding-right: 400px; } + .NB-story-title-magazine.NB-story-title.NB-selected { min-height: 0; } + .NB-story-title .NB-storytitles-feed-border-inner, .NB-story-title .NB-storytitles-feed-border-outer { position: absolute; @@ -1886,14 +2078,17 @@ img.feed_favicon { top: 0; background-color: #505050; } + .NB-story-title.read .NB-storytitles-feed-border-inner, .NB-story-title.read .NB-storytitles-feed-border-outer { opacity: .06; } + .NB-story-title .NB-storytitles-feed-border-inner { left: 4px; background-color: #707070; } + .NB-story-title .NB-storytitles-sentiment { position: absolute; width: 24px; @@ -1907,15 +2102,19 @@ img.feed_favicon { .NB-theme-feed-size-xs .NB-storytitles-sentiment { top: 9px; } + .NB-theme-feed-size-s .NB-storytitles-sentiment { top: 10px; } + .NB-density-compact .NB-story-title .NB-storytitles-sentiment { top: 3px; } + .NB-story-pane-west .NB-story-title .NB-storytitles-sentiment { left: -31px; } + /* .NB-image-preview-small-left .NB-story-title .NB-storytitles-sentiment, .NB-image-preview-large-left .NB-story-title .NB-storytitles-sentiment { left: -31px; @@ -1958,6 +2157,7 @@ img.feed_favicon { /* margin: -15px 0 0 0; */ transition: padding 0.36s ease-out, margin 0.36s ease-out, top 0.36s ease-out, height 0.36s ease-out; } + .NB-image-preview-large-right .NB-story-title-list.NB-story-title .NB-storytitles-story-image, .NB-image-preview-large-left .NB-story-title-list.NB-story-title .NB-storytitles-story-image, .NB-image-preview-large-left .NB-story-title-split.NB-story-title .NB-storytitles-story-image, @@ -1965,27 +2165,34 @@ img.feed_favicon { margin: 0 0 0 0; top: 0; } + .NB-story-title-magazine.NB-story-title .NB-storytitles-story-image { background-size: 40%, contain; } + .NB-story-title.read .NB-storytitles-story-image { opacity: 0.6; } + .NB-image-preview-large-left .NB-story-title .NB-storytitles-story-image { right: inherit; left: -108px; top: 0; } + .NB-image-preview-large-left .NB-view-river .NB-story-title .NB-storytitles-story-image { left: -138px; } + .NB-image-preview-large-left .NB-view-river .NB-story-title-list.NB-story-title .NB-storytitles-story-image { left: -240px; } + .NB-image-preview-large-left .NB-story-title-magazine.NB-story-title .NB-storytitles-story-image { height: 100%; top: 0; } + .NB-story-title.NB-story-starred .NB-storytitles-star, .NB-story-title.read.NB-story-starred .NB-storytitles-star { width: 16px; @@ -1996,10 +2203,12 @@ img.feed_favicon { float: left; display: none; } + .NB-story-title.NB-story-starred .NB-storytitles-star, .NB-story-title.read.NB-story-starred .NB-storytitles-star { display: block; } + .NB-story-title.NB-story-shared .NB-storytitles-share, .NB-story-title.read.NB-story-shared .NB-storytitles-share { width: 16px; @@ -2010,19 +2219,20 @@ img.feed_favicon { float: left; display: none; } + .NB-story-title.NB-story-shared .NB-storytitles-share, .NB-story-title.read.NB-story-shared .NB-storytitles-share { display: block; } .NB-story-title.read .NB-storytitles-sentiment { - opacity: .15; + opacity: .15; } -.NB-story-title:hover .NB-storytitles-sentiment { +.NB-story-title:hover .NB-storytitles-sentiment { background: none; } - + .NB-story-title a.story_title { text-decoration: none; color: #272727; @@ -2037,6 +2247,7 @@ img.feed_favicon { word-break: break-word; transition: padding 0.36s ease-out, margin 0.36s ease-out, top 0.36s ease-out; } + .NB-image-preview-small-right .NB-story-title.NB-has-image a.story_title, .NB-image-preview-large-right .NB-story-title.NB-has-image a.story_title { padding-right: 12px; @@ -2046,35 +2257,42 @@ img.feed_favicon { padding-top: 4px; padding-bottom: 4px; } + .NB-story-title-list.NB-story-title a.story_title { padding-bottom: 12px; } + .NB-density-compact .NB-story-title-list.NB-story-title a.story_title { padding-bottom: 4px; } + .NB-story-title.read a.story_title { color: #969696; } + .NB-storytitles-title { overflow-wrap: break-word; word-break: break-word; } -.NB-view-river .NB-story-title a.story_title { -} +.NB-view-river .NB-story-title a.story_title {} .NB-story-title .NB-storytitles-author { font-size: 11px; } + .NB-theme-feed-size-xs .NB-story-title .NB-storytitles-author { font-size: 10px; } + .NB-theme-feed-size-s .NB-story-title .NB-storytitles-author { font-size: 11px; } + .NB-theme-feed-size-l .NB-story-title .NB-storytitles-author { font-size: 12px; } + .NB-theme-feed-size-xl .NB-story-title .NB-storytitles-author { font-size: 13px; } @@ -2082,13 +2300,14 @@ img.feed_favicon { .NB-story-title.read .NB-middot { color: #cac9c5; } + .NB-story-title.read .NB-storytitles-author { color: #cac9c5; font-weight: normal; } .NB-story-title.NB-selected .NB-storytitles-author { -/* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/ + /* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/ } .NB-storytitles-content-preview { @@ -2102,35 +2321,41 @@ img.feed_favicon { word-break: break-word; display: -webkit-box; -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - + -webkit-box-orient: vertical; + -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; display: -webkit-box; } + .NB-density-compact .NB-storytitles-content-preview { margin-bottom: 0; } + .NB-story-pane-west .NB-storytitles-content-preview { display: -webkit-box; -webkit-line-clamp: 4; - -webkit-box-orient: vertical; + -webkit-box-orient: vertical; overflow: hidden; line-height: 16px; height: auto; padding: 6px 0 0; } + .NB-density-compact .NB-story-pane-west .NB-storytitles-content-preview { padding-top: 0; margin-bottom: 4px; } + .NB-content-preview-small .NB-story-pane-west .NB-storytitles-content-preview { -webkit-line-clamp: 2; } + .NB-content-preview-medium .NB-story-pane-west .NB-storytitles-content-preview { -webkit-line-clamp: 4; } + .NB-content-preview-large .NB-story-pane-west .NB-storytitles-content-preview { -webkit-line-clamp: 6; } @@ -2138,18 +2363,23 @@ img.feed_favicon { .NB-content-preview-small .NB-storytitles-content-preview { -webkit-line-clamp: 1; } + .NB-content-preview-medium .NB-storytitles-content-preview { -webkit-line-clamp: 2; } + .NB-content-preview-large .NB-storytitles-content-preview { -webkit-line-clamp: 3; } + .NB-content-preview-small .NB-story-title-magazine .NB-storytitles-content-preview { -webkit-line-clamp: 8; } + .NB-content-preview-medium .NB-story-title-magazine .NB-storytitles-content-preview { -webkit-line-clamp: 12; } + .NB-content-preview-large .NB-story-title-magazine .NB-storytitles-content-preview { -webkit-line-clamp: 16; } @@ -2158,36 +2388,45 @@ img.feed_favicon { font-size: 11px; line-height: 13px; } + .NB-theme-feed-size-s .NB-storytitles-content-preview { line-height: 14px; } + .NB-theme-feed-size-m .NB-storytitles-content-preview { font-size: 12px; } + .NB-theme-feed-size-l .NB-storytitles-content-preview { font-size: 13px; line-height: 17px; padding-top: 10px; } + .NB-density-compact.NB-theme-feed-size-l .NB-storytitles-content-preview { padding-top: 0; } + .NB-theme-feed-size-xl .NB-storytitles-content-preview { font-size: 14px; line-height: 18px; padding-top: 12px; } + .NB-density-compact.NB-theme-feed-size-xl .NB-storytitles-content-preview { padding-top: 0; } + .NB-content-preview-title .NB-storytitles-content-preview { display: none; } + .NB-story-title-list.NB-selected .NB-storytitles-content-preview, .NB-story-title-magazine.NB-selected .NB-storytitles-content-preview, .NB-story-title-grid.NB-selected .NB-storytitles-content-preview { display: none; } + .NB-layout-split .NB-story-title-list.NB-selected .NB-storytitles-content-preview { display: -webkit-box; } @@ -2197,35 +2436,43 @@ img.feed_favicon { line-height: 15px; margin: 10px 0; } + .NB-theme-feed-size-s .NB-story-title-magazine .NB-storytitles-content-preview { font-size: 13px; line-height: 16px; margin: 15px 0; } + .NB-theme-feed-size-m .NB-story-title-magazine .NB-storytitles-content-preview { font-size: 14px; line-height: 18px; margin: 20px 0; } + .NB-theme-feed-size-l .NB-story-title-magazine .NB-storytitles-content-preview { font-size: 16px; line-height: 20px; margin: 25px 0; } + .NB-theme-feed-size-xl .NB-story-title-magazine .NB-storytitles-content-preview { font-size: 18px; line-height: 22px; margin: 30px 0; } + .NB-story-title-magazine .NB-storytitles-content-preview { transition: padding 0.36s ease-out, margin 0.36s ease-out; } + .NB-density-compact .NB-story-title-magazine .NB-storytitles-content-preview { margin: 12px 0; } + .read .NB-storytitles-content-preview { color: #b3b3b1; } + .NB-storytitles-shares { position: absolute; height: 16px; @@ -2233,10 +2480,12 @@ img.feed_favicon { top: 2px; max-width: 56px; } + .NB-story-pane-west .NB-storytitles-shares { top: inherit; bottom: 4px; } + .NB-storytitles-shares .NB-icon { float: right; width: 12px; @@ -2245,6 +2494,7 @@ img.feed_favicon { background: transparent url('/media/embed/icons/nouns/share.svg') no-repeat 0 0; background-size: 16px; } + .NB-storytitles-shares .NB-user-avatar { float: right; width: 12px; @@ -2264,6 +2514,7 @@ img.feed_favicon { position: absolute; transition: top 0.36s ease-out; } + .NB-story-title-magazine.NB-story-title .NB-story-feed { position: relative; top: auto; @@ -2271,19 +2522,24 @@ img.feed_favicon { width: 100%; margin: 0 0 14px 4px; } + .NB-density-compact .NB-story-title-magazine.NB-story-title .NB-story-feed { top: 0; } + .NB-story-pane-west .NB-story-title .NB-story-feed { left: -47px; top: 14px; } + .NB-density-compact .NB-story-title .NB-story-feed { top: 6px; } + .NB-view-river .NB-story-title .NB-story-feed { display: block; } + .NB-story-title .NB-story-feed .feed_favicon { position: absolute; top: 0; @@ -2293,9 +2549,11 @@ img.feed_favicon { height: 16px; width: 16px; } + .NB-story-title.read .feed_favicon { opacity: .4; } + .NB-story-title .NB-story-feed .feed_title { display: block; @@ -2311,26 +2569,31 @@ img.feed_favicon { text-overflow: ellipsis; white-space: nowrap; } + .NB-theme-feed-size-xs .NB-story-title .NB-story-feed .feed_title { font-size: 10px; height: 12px; } + .NB-theme-feed-size-m .NB-story-title .NB-story-feed .feed_title { font-size: 11px; height: 15px; } + .NB-theme-feed-size-l .NB-story-title .NB-story-feed .feed_title { - font-size: 12px; + font-size: 12px; height: 15px; } + .NB-theme-feed-size-xl .NB-story-title .NB-story-feed .feed_title { font-size: 12px; height: 15px; } .NB-story-title.NB-selected .NB-story-feed .feed_title { -/* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/ + /* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/ } + .NB-story-pane-west .NB-story-title .NB-story-feed .feed_title { display: none; } @@ -2346,16 +2609,20 @@ img.feed_favicon { background-size: 8px; z-index: 1; } + .NB-density-compact .NB-story-title .NB-story-manage-icon { top: 3px; } + .NB-story-title-magazine.NB-story-title .NB-story-manage-icon { left: -28px; } + .NB-story-title-grid.NB-story-title .NB-story-manage-icon { left: 0; top: 2px; } + .NB-story-title:hover .NB-story-manage-icon { display: block; } @@ -2365,6 +2632,7 @@ img.feed_favicon { background-size: 8px; cursor: pointer; } + .NB-story-title.NB-hover-inverse:hover .NB-story-manage-icon:hover { background: transparent url('/media/embed/icons/nouns/up.svg') no-repeat 13px 6px; background-size: 8px; @@ -2390,46 +2658,57 @@ img.feed_favicon { background: transparent url('/media/embed/icons/nouns/thumbs-up.svg') no-repeat 2px 3px; opacity: .25; } + .NB-story-like:hover { - opacity: 1; -} + opacity: 1; +} + .NB-story-title .story_date { color: #888785; font-weight: normal; font-size: 12px; } + .NB-story-title-list.NB-story-title .story_date { position: absolute; width: 140px; top: 14px; right: 0; } + .NB-density-compact.NB-story-title-list.NB-story-title .story_date { top: 6px; } + .NB-theme-feed-size-xs .story_date { font-size: 10px; line-height: 13px; } + .NB-theme-feed-size-s .story_date { font-size: 11px; line-height: 14px; } + .NB-theme-feed-size-m .story_date { font-size: 11px; line-height: 14px; } + .NB-theme-feed-size-l .story_date { font-size: 12px; line-height: 15px; } + .NB-theme-feed-size-xl .story_date { font-size: 13px; line-height: 16px; } + .NB-story-title.read .story_date { color: #cac9c5; } + .NB-story-pane-west .NB-story-title .story_date { position: static; margin: 4px 0 4px 0px; @@ -2439,17 +2718,21 @@ img.feed_favicon { font-weight: normal; clear: both; } + .NB-story-title-split-bottom { color: #888785; font-weight: normal; margin: 8px 0 0 0; } + .NB-density-compact .NB-story-title-split-bottom { margin-bottom: 0; } + .NB-story-title { transition: transform 0.12s ease-out, border-radius 0.12s ease-out, height 0.36s ease-out; } + .NB-story-title.NB-selected, .NB-interaction:hover:not(.NB-disabled) { color: #304080; @@ -2463,8 +2746,9 @@ img.feed_favicon { .NB-story-title:hover:not(.NB-selected) { background-color: #F7F7F6; -/* border-top: 1px solid transparent;*/ + /* border-top: 1px solid transparent;*/ } + .NB-story-title.read:hover:not(.NB-selected) { background-color: #FDFCFA; } @@ -2475,34 +2759,46 @@ img.feed_favicon { border-bottom: 1px solid #6EADF5; background-color: #D4E2F2; background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#D4E2F2), to(#C8DEF8)); - background: -moz-linear-gradient(center top , #D4E2F2 0%, #C8DEF8 100%); + background: -moz-linear-gradient(center top, #D4E2F2 0%, #C8DEF8 100%); } .NB-end-line { min-height: 76px; border-top: 1px solid #dbdbda; border-bottom: 1px solid #E1E6E0; -/* padding-left: 14px; /* offset for scrollbar on right */ + /* padding-left: 14px; /* offset for scrollbar on right */ background-color: #F8F8F8; background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#F8F8F8), to(#F1F6F0)); background: -moz-linear-gradient(center top, #F8F8F8 0%, #F1F6F0 100%); - background-image: no-repeat center center; + background-image: no-repeat center center; overflow: hidden; clear: both; } + .NB-load-line { background: none; animation: end-line-animation 1.7s ease infinite; } -@keyframes end-line-animation { - 0%{background-color:#E1EBFF} - 38%{background-color:#5C89C9} - 100%{background-color:#E1EBFF} + +@keyframes end-line-animation { + 0% { + background-color: #E1EBFF + } + + 38% { + background-color: #5C89C9 + } + + 100% { + background-color: #E1EBFF + } } + .NB-module-river .NB-end-line { border-top: none; border-bottom: none; } + .NB-end-line.NB-short { height: 8px; min-height: 8px; @@ -2523,15 +2819,16 @@ img.feed_favicon { .NB-module-river .NB-end-line { display: none; } + .NB-module-river .NB-end-line.NB-visible { display: block; } + /* ============================= */ /* = Story Detail in List View = */ /* ============================= */ -.NB-story-title-container .NB-feed-story { -} +.NB-story-title-container .NB-feed-story {} /* ============================= */ /* = Story Detail in Grid View = */ @@ -2540,23 +2837,25 @@ img.feed_favicon { .NB-layout-grid { background-color: #F7F8F5; } + .NB-layout-grid .NB-story-titles { display: grid; grid-gap: 2rem; padding: 2rem; } + .NB-layout-grid .NB-end-line { margin: 0 -2rem -2rem; - + grid-column-start: 1; grid-column-end: -1; } -.NB-layout-grid .NB-story-title-container { - -} + +.NB-layout-grid .NB-story-title-container {} + .NB-layout-grid .NB-story-title-container.NB-selected { margin: 0 -2rem; - + grid-column-start: 1; grid-column-end: -1; } @@ -2572,25 +2871,32 @@ img.feed_favicon { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-layout-grid.NB-grid-height-xs .NB-story-title-grid { height: 280px; } + .NB-layout-grid.NB-grid-height-s .NB-story-title-grid { height: 360px; } + .NB-layout-grid.NB-grid-height-m .NB-story-title-grid { height: 420px; } + .NB-layout-grid.NB-grid-height-l .NB-story-title-grid { height: 480px; } + .NB-layout-grid.NB-grid-height-xl .NB-story-title-grid { height: 540px; } + .NB-story-title-grid.NB-story-title-hide-preview, .NB-content-preview-title .NB-story-title-grid { height: 296px; } + .NB-image-preview-small-right .NB-layout-grid .NB-story-title.NB-selected.NB-has-image, .NB-image-preview-large-right .NB-layout-grid .NB-story-title.NB-selected.NB-has-image { padding-right: 242px; @@ -2602,36 +2908,40 @@ img.feed_favicon { grid-template-columns: none; grid-template-columns: repeat(3, 1fr); } + .NB-layout-grid.NB-grid-columns-1 .NB-story-titles, .NB-layout-grid.NB-extra-narrow-content .NB-story-titles, .NB-extra-narrow-content.NB-narrow-content .NB-story-titles { grid-template-columns: none; grid-template-columns: repeat(1, 1fr); } + .NB-layout-grid.NB-grid-columns-2 .NB-story-titles, .NB-narrow-content .NB-story-titles { grid-template-columns: none; grid-template-columns: repeat(2, 1fr); } + .NB-layout-grid.NB-grid-columns-4 .NB-story-titles, .NB-extra-wide-content .NB-story-titles { grid-template-columns: none; grid-template-columns: repeat(4, 1fr); } + .NB-view-river .NB-story-title-grid .NB-story-title-grid { padding-left: 0; border: none; - box-shadow: 0 1px 2px rgba(0,0,0,0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); border-radius: 3px; } + .NB-story-title-grid, .NB-story-title.NB-story-title-grid:hover { border: none; - box-shadow: 0 1px 2px rgba(0,0,0,0.2); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } -.NB-story-title-grid.NB-selected { -} +.NB-story-title-grid.NB-selected {} .NB-story-title-grid .NB-storytitles-content { position: relative; @@ -2639,11 +2949,13 @@ img.feed_favicon { height: 100%; z-index: 0; } + .NB-story-title-grid a.story_title { padding: 2px 0 0 0; margin: 0; position: relative; } + .NB-density-compact .NB-story-title-grid a.story_title { padding-top: 2px; padding-bottom: 2px; @@ -2655,15 +2967,19 @@ img.feed_favicon { padding: 0; word-break: break-word; } + .NB-theme-feed-size-xs .NB-story-title-grid .NB-storytitles-title { font-size: 14px; } + .NB-theme-feed-size-s .NB-story-title-grid .NB-storytitles-title { font-size: 15px; } + .NB-theme-feed-size-l .NB-story-title-grid .NB-storytitles-title { font-size: 17px; } + .NB-theme-feed-size-xl .NB-story-title-grid .NB-storytitles-title { font-size: 18px; } @@ -2672,9 +2988,11 @@ img.feed_favicon { margin: 12px 0 0 0; word-break: break-word; } + .NB-story-title-grid .NB-storytitles-story-image-container { margin-left: 8px; } + .NB-image-preview-small-left .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-large-left .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-small-right .NB-story-title-grid .NB-storytitles-story-image, @@ -2691,83 +3009,103 @@ img.feed_favicon { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-image-preview-small-left .NB-grid-height-xs .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-small-right .NB-grid-height-xs .NB-story-title-grid .NB-storytitles-story-image { height: 76px; } + .NB-image-preview-large-left .NB-grid-height-xs .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-large-right .NB-grid-height-xs .NB-story-title-grid .NB-storytitles-story-image { height: 106px; } + .NB-image-preview-small-left .NB-grid-height-s .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-small-right .NB-grid-height-s .NB-story-title-grid .NB-storytitles-story-image { height: 106px; } + .NB-image-preview-large-left .NB-grid-height-s .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-large-right .NB-grid-height-s .NB-story-title-grid .NB-storytitles-story-image { height: 146px; } + .NB-image-preview-small-left .NB-grid-height-m .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-small-right .NB-grid-height-m .NB-story-title-grid .NB-storytitles-story-image { height: 146px; } + .NB-image-preview-large-left .NB-grid-height-m .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-large-right .NB-grid-height-m .NB-story-title-grid .NB-storytitles-story-image { height: 202px; } + .NB-image-preview-small-left .NB-grid-height-l .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-small-right .NB-grid-height-l .NB-story-title-grid .NB-storytitles-story-image { height: 202px; } + .NB-image-preview-large-left .NB-grid-height-l .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-large-right .NB-grid-height-l .NB-story-title-grid .NB-storytitles-story-image { height: 242px; } + .NB-image-preview-small-left .NB-grid-height-xl .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-small-right .NB-grid-height-xl .NB-story-title-grid .NB-storytitles-story-image { height: 242px; } + .NB-image-preview-large-left .NB-grid-height-xl .NB-story-title-grid .NB-storytitles-story-image, .NB-image-preview-large-right .NB-grid-height-xl .NB-story-title-grid .NB-storytitles-story-image { height: 272px; } + .NB-layout-grid.NB-grid-height-xs.NB-grid-columns-3 .NB-story-title-grid .NB-storytitles-author, .NB-layout-grid.NB-grid-height-xs.NB-grid-columns-4 .NB-story-title-grid .NB-storytitles-author, .NB-layout-grid.NB-grid-height-s.NB-grid-columns-4 .NB-story-title-grid .NB-storytitles-author { display: none; } + .NB-story-title-grid.read .NB-storytitles-story-image { opacity: .5; } + .NB-story-title-grid.NB-selected .NB-storytitles-story-image { display: none; } + .NB-story-title-grid .NB-storytitles-feed-border-inner, .NB-story-title-grid .NB-storytitles-feed-border-outer { -/* height: 4px;*/ -/* width: 100%;*/ -/* position: static;*/ + /* height: 4px;*/ + /* width: 100%;*/ + /* position: static;*/ z-index: 1; } + .NB-story-title-grid.read .NB-storytitles-feed-border-inner, .NB-story-title-grid.read .NB-storytitles-feed-border-outer { opacity: .2; } + .NB-story-title-grid .NB-storytitles-content-preview { height: auto; -webkit-line-clamp: inherit; font-size: 12px; line-height: 1.44em; } + .NB-theme-feed-size-xs .NB-story-title-grid .NB-storytitles-content-preview { font-size: 10px; } + .NB-theme-feed-size-s .NB-story-title-grid .NB-storytitles-content-preview { font-size: 11px; } + .NB-theme-feed-size-l .NB-story-title-grid .NB-storytitles-content-preview { font-size: 13px; } + .NB-theme-feed-size-xl .NB-story-title-grid .NB-storytitles-content-preview { font-size: 14px; } @@ -2775,13 +3113,14 @@ img.feed_favicon { .NB-story-title-grid.read .NB-storytitles-content-preview { color: #D6D6DE; } + .NB-story-title .NB-storytitles-grid-bottom { position: absolute; bottom: 0; top: inherit; left: 0; background-color: #FDFCFA; - width: 100%; + width: 100%; padding: 4px 16px 6px 38px; color: #A6A3A7; font-weight: normal; @@ -2791,21 +3130,27 @@ img.feed_favicon { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-theme-feed-size-xs .NB-story-title .NB-storytitles-grid-bottom { font-size: 10px; } + .NB-theme-feed-size-s .NB-story-title .NB-storytitles-grid-bottom { font-size: 11px; } + .NB-theme-feed-size-m .NB-story-title .NB-storytitles-grid-bottom { font-size: 11px; } + .NB-theme-feed-size-l .NB-story-title .NB-storytitles-grid-bottom { font-size: 12px; } + .NB-theme-feed-size-xl .NB-story-title .NB-storytitles-grid-bottom { font-size: 13px; } + .NB-story-title-grid:not(.read):hover .NB-storytitles-grid-bottom { background-color: #F7F7F6; } @@ -2817,6 +3162,7 @@ img.feed_favicon { font-weight: normal; transition: padding 0.36s ease-out, margin 0.36s ease-out; } + .NB-density-compact .NB-storytitles-magazine-bottom { margin-bottom: 4px; } @@ -2824,23 +3170,29 @@ img.feed_favicon { .NB-theme-feed-size-xs .NB-story-title .NB-storytitles-magazine-bottom { font-size: 10px; } + .NB-theme-feed-size-s .NB-story-title .NB-storytitles-magazine-bottom { font-size: 11px; } + .NB-theme-feed-size-m .NB-story-title .NB-storytitles-magazine-bottom { font-size: 11px; } + .NB-theme-feed-size-l .NB-story-title .NB-storytitles-magazine-bottom { font-size: 12px; } + .NB-theme-feed-size-xl .NB-story-title .NB-storytitles-magazine-bottom { font-size: 13px; } + .NB-story-title.NB-story-title-grid .NB-storytitles-sentiment, .NB-story-title.NB-story-title-grid .NB-story-manage-icon { left: -32px; top: 2px; } + .NB-story-title-magazine .NB-story-feed, .NB-story-title-grid .NB-story-feed { position: static; @@ -2848,12 +3200,14 @@ img.feed_favicon { height: auto; overflow: hidden; } + .NB-story-title-magazine .NB-story-feed .feed_favicon, .NB-story-title-grid .NB-story-feed .feed_favicon { margin: 0 4px 0 0; float: left; position: static; } + .NB-story-title-magazine .NB-story-feed .feed_title, .NB-story-title-grid .NB-story-feed .feed_title { width: auto; @@ -2864,6 +3218,7 @@ img.feed_favicon { word-break: break-all; text-overflow: ellipsis; } + .NB-story-title-grid .feed_favicon, .NB-story-title-grid .feed_title { position: static; @@ -2966,10 +3321,12 @@ img.feed_favicon { overflow: auto !important; display: block; } + #story_pane .NB-text-view { left: 200%; top: 0; } + #story_pane .NB-story-view { left: 300%; top: 0; @@ -2987,17 +3344,17 @@ img.feed_favicon { } .NB-feed-story-view-floater { - position: absolute; - top: 0; - left: 0; - z-index: 2; - width: 100%; - display: none; - opacity: .9; + position: absolute; + top: 0; + left: 0; + z-index: 2; + width: 100%; + display: none; + opacity: .9; } .NB-view-river .NB-feed-story-view-floater { - display: block; + display: block; } @@ -3008,17 +3365,21 @@ img.feed_favicon { body { font-family: 'Whitney SSm A', 'Whitney SSm B', "Lucida Grande", Verdana, "Helvetica Neue", Helvetica, sans-serif; } + .NB-theme-feed-font-whitney { font-family: 'Whitney SSm A', 'Whitney SSm B', "Lucida Grande", Verdana, "Helvetica Neue", Helvetica, sans-serif; } + .NB-theme-feed-font-lucida { font-family: "Lucida Grande", Verdana, "Helvetica Neue", Helvetica, sans-serif; /* font-family: Verdana, "Helvetica Neue", Helvetica, sans-serif; */ } + .NB-theme-feed-font-gotham { font-family: 'Gotham Narrow A', 'Gotham Narrow B', "Helvetica Neue", Helvetica, sans-serif; /* font-family: "Helvetica Neue", Helvetica, sans-serif; */ } + .NB-theme-sans-serif #story_pane { font-family: "Helvetica Neue", "Helvetica", sans-serif; } @@ -3046,8 +3407,8 @@ body { .NB-theme-gotham .NB-feed-story-comments, .NB-theme-gotham .NB-feed-story-view-floater, .NB-theme-gotham .NB-feed-story-header { - font-family: 'Gotham Narrow A', 'Gotham Narrow B'; - font-weight: 400; + font-family: 'Gotham Narrow A', 'Gotham Narrow B'; + font-weight: 400; font-style: normal; } @@ -3055,8 +3416,8 @@ body { .NB-theme-sentinel .NB-feed-story-comments, .NB-theme-sentinel .NB-feed-story-view-floater, .NB-theme-sentinel .NB-feed-story-header { - font-family: 'Sentinel A', 'Sentinel B'; - font-weight: 400; + font-family: 'Sentinel A', 'Sentinel B'; + font-weight: 400; font-style: normal; } @@ -3064,8 +3425,8 @@ body { .NB-theme-whitney .NB-feed-story-comments, .NB-theme-whitney .NB-feed-story-view-floater, .NB-theme-whitney .NB-feed-story-header { - font-family: 'Whitney A', 'Whitney B'; - font-weight: 400; + font-family: 'Whitney A', 'Whitney B'; + font-weight: 400; font-style: normal; } @@ -3073,8 +3434,8 @@ body { .NB-theme-chronicle .NB-feed-story-comments, .NB-theme-chronicle .NB-feed-story-view-floater, .NB-theme-chronicle .NB-feed-story-header { - font-family: 'Chronicle Display A', 'Chronicle Display B'; - font-weight: 400; + font-family: 'Chronicle Display A', 'Chronicle Display B'; + font-weight: 400; font-style: normal; } @@ -3090,15 +3451,19 @@ body { .NB-line-spacing-xs .NB-story-content-container { line-height: 1.1em; } + .NB-line-spacing-s .NB-story-content-container { line-height: 1.3em; } + .NB-line-spacing-m .NB-story-content-container { line-height: 1.5em; } + .NB-line-spacing-l .NB-story-content-container { line-height: 1.8em; } + .NB-line-spacing-xl .NB-story-content-container { line-height: 2.0em; } @@ -3108,6 +3473,7 @@ body { display: none; height: 0; } + .NB-image-preview-small-left .NB-storytitles-story-image { width: 62px; height: calc(86% - 24px); @@ -3115,9 +3481,11 @@ body { top: 14px; border-radius: 6px; } + .NB-image-preview-small-left .NB-view-river .NB-storytitles-story-image { left: -126px; } + .NB-image-preview-small-right .NB-storytitles-story-image { width: 62px; height: calc(86% - 24px); @@ -3125,33 +3493,41 @@ body { top: 16px; border-radius: 6px; } + .NB-density-compact .NB-storytitles-story-image { top: 4px; height: calc(86% - 8px); } + .NB-image-preview-large-left .NB-storytitles-story-image, .NB-image-preview-large-right .NB-storytitles-story-image { height: 100%; } + .NB-density-compact .NB-image-preview-large-left .NB-storytitles-story-image, .NB-density-compact .NB-image-preview-large-right .NB-storytitles-story-image { height: calc(100% - 8px); } + .NB-image-preview-small-left .NB-story-title-list .NB-storytitles-story-image, .NB-image-preview-small-right .NB-story-title-list .NB-storytitles-story-image { /* top: 4px; */ } + .NB-image-preview-small-left .NB-story-title-list .NB-storytitles-story-image { left: -94px; } + .NB-image-preview-small-left .NB-view-river .NB-story-title-list .NB-storytitles-story-image { left: -228px; } + .NB-image-preview-small-left .NB-view-river .NB-story-title-list .NB-storytitles-story-image, .NB-image-preview-small-right .NB-view-river .NB-story-title-list .NB-storytitles-story-image { top: 8px; height: calc(100% - 16px); } + .NB-image-preview-small-left .NB-story-title-magazine .NB-storytitles-story-image-container, .NB-image-preview-large-left .NB-story-title-magazine .NB-storytitles-story-image-container { position: absolute; @@ -3163,6 +3539,7 @@ body { top: 0; left: 8px; } + .NB-image-preview-small-left .NB-story-title-magazine .NB-storytitles-story-image, .NB-image-preview-small-right .NB-story-title-magazine .NB-storytitles-story-image { top: auto; @@ -3174,6 +3551,7 @@ body { background-position: center; border-radius: 12px; } + .NB-image-preview-large-left .NB-story-title-magazine .NB-storytitles-story-image, .NB-image-preview-large-right .NB-story-title-magazine .NB-storytitles-story-image { top: auto; @@ -3185,6 +3563,7 @@ body { background-position: center; border-radius: 0; } + .NB-image-preview-small-right .NB-story-title-magazine .NB-storytitles-story-image-container, .NB-image-preview-large-right .NB-story-title-magazine .NB-storytitles-story-image-container { position: absolute; @@ -3198,99 +3577,123 @@ body { } .NB-theme-sans-serif.NB-theme-size-xs .NB-feed-story { - font-size: 11px; + font-size: 11px; } + .NB-theme-sans-serif.NB-theme-size-s .NB-feed-story { - font-size: 12px; + font-size: 12px; } + .NB-theme-sans-serif.NB-theme-size-m .NB-feed-story { - font-size: 13px; + font-size: 13px; } + .NB-theme-sans-serif.NB-theme-size-l .NB-feed-story { - font-size: 15px; + font-size: 15px; } + .NB-theme-sans-serif.NB-theme-size-xl .NB-feed-story { - font-size: 18px; + font-size: 18px; } .NB-theme-gotham.NB-theme-size-xs .NB-feed-story { - font-size: 13px; + font-size: 13px; } + .NB-theme-gotham.NB-theme-size-s .NB-feed-story { - font-size: 14px; + font-size: 14px; } + .NB-theme-gotham.NB-theme-size-m .NB-feed-story { - font-size: 15px; + font-size: 15px; } + .NB-theme-gotham.NB-theme-size-l .NB-feed-story { - font-size: 16px; + font-size: 16px; } + .NB-theme-gotham.NB-theme-size-xl .NB-feed-story { - font-size: 19px; + font-size: 19px; } .NB-theme-sentinel.NB-theme-size-xs .NB-feed-story { - font-size: 13px; + font-size: 13px; } + .NB-theme-sentinel.NB-theme-size-s .NB-feed-story { - font-size: 14px; + font-size: 14px; } + .NB-theme-sentinel.NB-theme-size-m .NB-feed-story { - font-size: 15px; + font-size: 15px; } + .NB-theme-sentinel.NB-theme-size-l .NB-feed-story { - font-size: 16px; + font-size: 16px; } + .NB-theme-sentinel.NB-theme-size-xl .NB-feed-story { - font-size: 19px; + font-size: 19px; } .NB-theme-whitney.NB-theme-size-xs .NB-feed-story { - font-size: 13px; + font-size: 13px; } + .NB-theme-whitney.NB-theme-size-s .NB-feed-story { - font-size: 14px; + font-size: 14px; } + .NB-theme-whitney.NB-theme-size-m .NB-feed-story { - font-size: 15px; + font-size: 15px; } + .NB-theme-whitney.NB-theme-size-l .NB-feed-story { - font-size: 16px; + font-size: 16px; } + .NB-theme-whitney.NB-theme-size-xl .NB-feed-story { - font-size: 19px; + font-size: 19px; } .NB-theme-chronicle.NB-theme-size-xs .NB-feed-story { - font-size: 13px; + font-size: 13px; } + .NB-theme-chronicle.NB-theme-size-s .NB-feed-story { - font-size: 14px; + font-size: 14px; } + .NB-theme-chronicle.NB-theme-size-m .NB-feed-story { - font-size: 15px; + font-size: 15px; } + .NB-theme-chronicle.NB-theme-size-l .NB-feed-story { - font-size: 16px; + font-size: 16px; } + .NB-theme-chronicle.NB-theme-size-xl .NB-feed-story { - font-size: 19px; + font-size: 19px; } .NB-theme-serif.NB-theme-size-xs .NB-feed-story { - font-size: 13px; + font-size: 13px; } + .NB-theme-serif.NB-theme-size-s .NB-feed-story { - font-size: 14px; + font-size: 14px; } + .NB-theme-serif.NB-theme-size-m .NB-feed-story { - font-size: 15px; + font-size: 15px; } + .NB-theme-serif.NB-theme-size-l .NB-feed-story { - font-size: 16px; + font-size: 16px; } + .NB-theme-serif.NB-theme-size-xl .NB-feed-story { - font-size: 19px; + font-size: 19px; } .NB-feed-stories { @@ -3305,6 +3708,7 @@ body { .NB-feed-story:first-child .NB-feed-story-header { padding-top: 0; } + #story_pane .NB-feed-stories pre { overflow-x: auto; max-width: 100%; @@ -3318,9 +3722,11 @@ body { position: relative; overflow: hidden; } + .NB-feed-story.NB-selected .NB-feed-story-header-info { border-left: 3px solid #EBAA74; } + .NB-narrow-content .NB-feed-story .NB-feed-story-header-info { padding-right: 28px; } @@ -3331,18 +3737,14 @@ body { .NB-feed-story-header-feed { background: #404040 url('/media/embed/reader/feed_view_feed_background.png') repeat-x 0 0; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.36, rgba(76, 76, 76, 250)), - color-stop(0.84, rgba(55, 55, 55, 250)) - ); - background-image: -moz-linear-gradient( - center bottom, - rgb(76,76,76) 36%, - rgb(55,55,55) 84% - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.36, rgba(76, 76, 76, 250)), + color-stop(0.84, rgba(55, 55, 55, 250))); + background-image: -moz-linear-gradient(center bottom, + rgb(76, 76, 76) 36%, + rgb(55, 55, 55) 84%); padding: 3px 236px 3px 28px; position: sticky; top: -1px; @@ -3353,9 +3755,11 @@ body { .NB-narrow-content .NB-feed-story-header-feed { padding-right: 28px; } + .NB-feed-story-header-feed.NB-feed-story-river-same-feed { z-index: 0; } + .NB-feed-story-header-feed.NB-floater { z-index: 0; } @@ -3366,6 +3770,7 @@ body { font-size: 13px; position: relative; } + .NB-inverse .NB-feed-story-feed { color: black; } @@ -3374,6 +3779,7 @@ body { margin: 0 auto; max-width: 700px; } + .NB-pref-story-position-right .NB-feed-story-feed { margin: 0 0 0 auto; width: 700px; @@ -3391,6 +3797,7 @@ body { cursor: pointer; font-weight: bold; } + .NB-feed-story-feed .NB-feed-story-header-title:hover { color: rgba(255, 255, 255, .8); } @@ -3431,26 +3838,31 @@ body { .NB-feed-story .NB-feed-story-content div { max-width: 100%; } + .NB-feed-story .NB-feed-story-content img { - max-width: 100% !important; + max-width: 100% !important; width: auto !important; height: auto; /* See http://www.newsblur.com/site/1031643/le-21me for width: auto, height: auto */ } + .NB-feed-story .NB-feed-story-content img.NB-medium-image { - border-radius: 4px; -} -.NB-feed-story .NB-feed-story-content img.NB-small-image { + border-radius: 4px; } + +.NB-feed-story .NB-feed-story-content img.NB-small-image {} + .NB-feed-story .NB-feed-story-content img.NB-large-image { - border-radius: 4px; + border-radius: 4px; } + .NB-feed-story .NB-feed-story-content img.NB-large-image.NB-large-image-widen { - max-width: max-content !important; + max-width: max-content !important; margin-left: -28px !important; width: calc(100% - 56px * -1) !important; /* border-radius: 0; */ } + .NB-feed-story .NB-feed-story-content img.NB-table-image.NB-large-image { margin: 0; width: 100% !important; @@ -3468,6 +3880,7 @@ body { .NB-feed-story { position: relative; } + .NB-feed-story .NB-feed-story-sentiment { position: absolute; top: 6px; @@ -3475,10 +3888,11 @@ body { width: 16px; height: 16px; } + .NB-feed-story.NB-river-story .NB-feed-story-sentiment { -/* display: none;*/ + /* display: none;*/ } - + .NB-feed-story.NB-story-starred .NB-feed-story-sentiment { background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 6px 0; background-size: 10px; @@ -3498,19 +3912,23 @@ body { background: transparent url('/media/embed/icons/nouns/indicator-hidden.svg') no-repeat 6px 0; background-size: 10px; } + .NB-feed-story.read .NB-feed-story-sentiment { opacity: .06; } + .NB-feed-story .NB-feed-story-header:hover .NB-feed-story-sentiment { - display: none; + display: none; } + .NB-feed-story .NB-feed-story-sentiment-animation { - display: none; + display: none; } + .NB-feed-story.read .NB-feed-story-sentiment.NB-feed-story-sentiment-animate.NB-animating { - opacity: 1; - z-index: 10; - display: block; + opacity: 1; + z-index: 10; + display: block; } .NB-feed-story .NB-feed-story-manage-icon { @@ -3523,6 +3941,7 @@ body { top: 2px; width: 28px; } + .NB-feed-story .NB-feed-story-header:hover .NB-feed-story-manage-icon { display: block; } @@ -3532,6 +3951,7 @@ body { background-size: 8px; cursor: pointer; } + .NB-feed-story.NB-hover-inverse .NB-feed-story-header:hover .NB-feed-story-manage-icon:hover { background: transparent url('/media/embed/icons/nouns/up.svg') no-repeat 13px 6px; background-size: 8px; @@ -3541,10 +3961,12 @@ body { position: relative; clear: both; } + .NB-pref-story-position-center .NB-feed-story .NB-feed-story-title-container { margin: 0 auto; max-width: 700px; } + .NB-pref-story-position-right .NB-feed-story .NB-feed-story-title-container { margin: 0 0 0 auto; max-width: 700px; @@ -3558,7 +3980,7 @@ body { font-weight: bold; font-size: 1.4em; line-height: 1.2em; -/* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/ + /* text-shadow: 1px 1px 0 rgba(255, 255, 255, .5);*/ } .NB-feed-story.read a.NB-feed-story-title { @@ -3576,63 +3998,78 @@ body { float: left; font-size: 12px; color: #757B6B; -/* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ + /* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ margin: 6px 8px 0 0; flex: none; align-self: start; } + .NB-theme-size-xs .NB-feed-story .NB-feed-story-author-wrapper { font-size: 10px; } + .NB-theme-size-s .NB-feed-story .NB-feed-story-author-wrapper { font-size: 11px; } + .NB-theme-size-l .NB-feed-story .NB-feed-story-author-wrapper { font-size: 13px; } + .NB-theme-size-xl .NB-feed-story .NB-feed-story-author-wrapper { font-size: 14px; } + .NB-feed-story .NB-feed-story-author-wrapper .NB-middot { color: #A0A0A0; padding-right: 4px; font-weight: normal; } + .NB-feed-story.read .NB-feed-story-author-wrapper { color: #959B8B; text-shadow: none; } + .NB-feed-story .NB-feed-story-author { cursor: pointer; flex: none; } + .NB-feed-story .NB-feed-story-author.NB-score-1 { color: #34912E; } + .NB-feed-story .NB-feed-story-author.NB-score--1 { color: #A90103; } + .NB-feed-story .NB-feed-story-author:hover { /* Gray, active -> [Light] Green */ color: #89AE6E; } + .NB-feed-story .NB-feed-story-author.NB-score-1:hover { /* Green, active -> [Light] Red */ color: #E35356; } + .NB-feed-story .NB-feed-story-author.NB-score--1:hover { /* Red, active -> [Light] Grey */ color: #B1B1B1; -/* color: #A7A399;*/ + /* color: #A7A399;*/ } + .NB-feed-story .NB-feed-story-author.NB-score-now-0:hover { /* Grey, active */ color: #808080; } + .NB-feed-story .NB-feed-story-author.NB-score-now-1.NB-score-1:hover { /* Green, active */ color: #34912E; } + .NB-feed-story .NB-feed-story-author.NB-score-now--1.NB-score--1:hover { /* Red, active */ color: #A90103; @@ -3642,6 +4079,7 @@ body { margin: 5px 0 6px; padding-left: 12px; } + .NB-feed-story .NB-feed-story-tags .NB-middot { color: #A0A0A0; font-size: 10px; @@ -3650,6 +4088,7 @@ body { margin-left: -12px; font-weight: normal; } + .NB-feed-story .NB-feed-story-tag { /* Grey */ display: inline-block; @@ -3657,7 +4096,7 @@ body { font-weight: normal; font-size: 12px; border-radius: 4px; - + padding: 1px 5px; margin: 0 4px 4px 0; line-height: 14px; @@ -3665,16 +4104,20 @@ body { color: #959B8B; border: 1px solid transparent; border-color: rgba(255, 255, 255, .3) transparent rgba(0, 0, 0, .1); -} +} + .NB-theme-size-xs .NB-feed-story .NB-feed-story-tag { font-size: 9px; } + .NB-theme-size-s .NB-feed-story .NB-feed-story-tag { font-size: 10px; } + .NB-theme-size-l .NB-feed-story .NB-feed-story-tag { font-size: 13px; } + .NB-theme-size-xl .NB-feed-story .NB-feed-story-tag { font-size: 14px; } @@ -3686,48 +4129,56 @@ body { .NB-feed-story.read .NB-feed-story-tag { color: #949181; } + .NB-feed-story .NB-feed-story-tag.NB-score-1 { /* Green */ background-color: #A3CA87; color: white; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + .NB-feed-story .NB-feed-story-tag.NB-score--1 { /* Red */ background-color: #D58586; color: white; text-shadow: 0 1px 0 rgba(0, 0, 0, .7); } + .NB-feed-story .NB-feed-story-tag:hover { /* Gray, active -> [Light] Green */ background-color: #9CB987; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); color: white; } + .NB-feed-story .NB-feed-story-tag.NB-score-1:hover { /* Green, active -> [Light] Red */ background-color: #E35356; color: white; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + .NB-feed-story .NB-feed-story-tag.NB-score--1:hover { /* Red, active -> [Light] Grey */ background-color: #E2E2E2; color: #A7A399; text-shadow: 0 1px 0 rgba(255, 255, 255, .5); } + .NB-feed-story .NB-feed-story-tag.NB-score-now-0:hover { /* Grey, active */ background-color: rgba(0, 0, 0, .1); color: #9D9A95; text-shadow: 0 1px 0 rgba(255, 255, 255, .5); } + .NB-feed-story .NB-feed-story-tag.NB-score-now-1.NB-score-1:hover { /* Green, active */ background-color: #34912E; color: white; text-shadow: 0 1px 0 rgba(0, 0, 0, .3); } + .NB-feed-story .NB-feed-story-tag.NB-score-now--1.NB-score--1:hover { /* Red, active */ background-color: #A90103; @@ -3738,52 +4189,63 @@ body { .NB-feed-story .NB-feed-story-title .NB-score-1 { color: #34912E; } + .NB-feed-story .NB-feed-story-title .NB-score--1 { color: #A90103; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-date { float: left; font-size: 12px; color: #757B6B; -/* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ + /* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ font-weight: normal; margin: 6px 8px 0 0; flex: none; align-self: start; } + .NB-theme-size-xs .NB-feed-story .NB-feed-story-date { font-size: 10px; } + .NB-theme-size-s .NB-feed-story .NB-feed-story-date { font-size: 11px; } + .NB-theme-size-l .NB-feed-story .NB-feed-story-date { font-size: 13px; } + .NB-theme-size-xl .NB-feed-story .NB-feed-story-date { font-size: 14px; } + .NB-feed-story.read .NB-feed-story-header .NB-feed-story-date { color: #959B8B; text-shadow: none; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-date-line { clear: left; display: flex; } + .NB-pref-story-position-center .NB-feed-story .NB-feed-story-header .NB-feed-story-date-line { margin: 0 auto; max-width: 700px; } + .NB-pref-story-position-right .NB-feed-story .NB-feed-story-header .NB-feed-story-date-line { margin: 0 0 0 auto; max-width: 700px; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-show-changes { float: left; font-size: 12px; color: #959B8B; -/* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ + /* text-shadow: 0 1px 0 rgba(255, 255, 255, .3);*/ font-weight: normal; margin: 6px 8px 0 0; padding-left: 24px; @@ -3793,22 +4255,28 @@ body { flex: none; align-self: start; } + .NB-theme-size-xs .NB-feed-story-header .NB-feed-story-show-changes { font-size: 10px; } + .NB-theme-size-s .NB-feed-story-header .NB-feed-story-show-changes { font-size: 11px; } + .NB-theme-size-l .NB-feed-story-header .NB-feed-story-show-changes { font-size: 13px; } + .NB-theme-size-xl .NB-feed-story-header .NB-feed-story-show-changes { font-size: 14px; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-show-changes .NB-middot { color: #A0A0A0; padding-left: 4px; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-show-changes:hover { color: #757B6B; } @@ -3823,6 +4291,7 @@ body { flex: none; align-self: flex-start; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-starred-date .NB-icon { background: transparent url('/media/embed/icons/nouns/saved-stories.svg') no-repeat 0 2px; background-size: 16px; @@ -3832,25 +4301,32 @@ body { color: #9B9D97; padding-left: 20px; } + .NB-feed-story .NB-feed-story-header .NB-feed-story-starred-date .NB-title { color: #9B9D97; } + .NB-theme-size-xs .NB-feed-story-header .NB-feed-story-starred-date { font-size: 10px; } + .NB-theme-size-s .NB-feed-story-header .NB-feed-story-starred-date { font-size: 11px; } + .NB-theme-size-l .NB-feed-story-header .NB-feed-story-starred-date { font-size: 13px; } + .NB-theme-size-xl .NB-feed-story-header .NB-feed-story-starred-date { font-size: 14px; } + .NB-pref-story-position-center .NB-feed-story-header .NB-feed-story-starred-date { margin: 6px auto 0; max-width: 700px; } + .NB-pref-story-position-right .NB-feed-story-header .NB-feed-story-starred-date { margin: 6px 0 0 auto; max-width: 700px; @@ -3861,30 +4337,38 @@ body { max-width: 700px; min-height: 12px; } + .NB-pref-story-position-center .NB-feed-story .NB-feed-story-content { margin: 0 auto; } + .NB-pref-story-position-right .NB-feed-story .NB-feed-story-content { margin: 0 0 0 auto; } + .NB-pref-story-position-stretch .NB-feed-story .NB-feed-story-content { max-width: none; } + .NB-feed-story .NB-narrow-content .NB-feed-story-content { margin-right: 28px; } + .NB-modal-preferences ins, .NB-feed-story-content ins { background-color: #BEE8BC; text-decoration: none; } + .NB-feed-story-content ins img { border: 3px solid #BEE8BC; } + .NB-modal-preferences del, .NB-feed-story-content del { background-color: #f5c3c3; } + .NB-feed-story-content del img { border: 3px solid #f5c3c3; } @@ -3893,9 +4377,11 @@ body { .NB-feed-story-content .NB-highlight { background-color: #FFEE8E; } + .NB-feed-story-content .NB-starred-story-selection-highlight-popover { background-color: #FEFFB5; } + /*.NB-feed-story-content ::selection {*/ /* background: rgba(185, 215, 251, .2);*/ /*}*/ @@ -3903,16 +4389,19 @@ body { .NB-unhighlight-selection { padding: 2px 8px; } + .NB-highlight-selection:hover, .NB-unhighlight-selection:hover { cursor: pointer; background-color: rgba(255, 255, 255, 0.2); } + .NB-highlight-selection:active, .NB-unhighlight-selection:active { color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.1); } + /* =========================== */ /* = Story content expansion = */ /* =========================== */ @@ -3923,28 +4412,35 @@ body { min-height: 192px; background-color: white; } + .NB-narrow-content .NB-story-content-container { min-height: 108px; } + .NB-story-content-wrapper { position: relative; clear: both; max-height: none; } + .NB-feed-story .NB-story-content-wrapper { padding: 0 236px 42px 28px; } + .NB-narrow-content .NB-feed-story .NB-story-content-wrapper { padding-right: 28px; } + .NB-story-content-wrapper.NB-story-content-wrapper-height-truncated { max-height: 460px; padding-bottom: 0; overflow: hidden; } + .NB-story-content-wrapper.NB-story-content-wrapper-height-fudged { max-height: none; } + .NB-story-content-expander { display: none; position: absolute; @@ -3958,17 +4454,21 @@ body { margin: 28px 0 0; padding: 0 28px 28px; } + .NB-story-content-expander .NB-story-content-expander-inner { position: relative; padding: 18px 25px 14px 0; height: 28px; } + .NB-story-content-expander:hover { color: #306187; } + .NB-story-content-expander:active { color: #722125; } + .NB-story-content-expander .NB-story-cutoff { position: absolute; top: -14px; @@ -3978,10 +4478,12 @@ body { z-index: 0; background: transparent url('/media/embed/circular/module_cutoff.png') repeat-x left bottom; } + .NB-story-content-expander .NB-story-content-expander-text, .NB-story-content-expander .NB-story-content-expander-pages { display: inline-block; } + .NB-story-content-expander .NB-story-content-expander-pages { line-height: 8px; padding: 0 12px; @@ -3989,6 +4491,7 @@ body { font-family: Helvetica, Arial; vertical-align: middle; } + @media screen and (max-width: 580px) { .NB-story-content-expander .NB-story-content-expander-pages { padding: 0 4px; @@ -4033,6 +4536,7 @@ body { left: 0; background-size: 25px 50px; } + .NB-text-view-premium-only img { width: 18px; height: 15px; @@ -4056,15 +4560,19 @@ body { background-color: white; position: relative; } + .NB-narrow-content .NB-feed-story-comments { padding-right: 28px; } + .NB-pref-story-position-center .NB-feed-story-comments { margin: 0 auto; } + .NB-pref-story-position-right .NB-feed-story-comments { margin: 0 0 0 auto; } + .NB-story-comment { border-bottom: 1px solid #EAECE8; position: relative; @@ -4072,6 +4580,7 @@ body { line-height: 18px; overflow: hidden; } + .NB-story-comment .NB-story-comment-author-avatar.NB-user-avatar, .NB-story-comment .NB-story-comment-reshares .NB-user-avatar { position: absolute; @@ -4079,49 +4588,60 @@ body { top: 6px; cursor: pointer; } + .NB-story-comment .NB-story-comment-author-avatar.NB-user-avatar.NB-story-comment-reshare { top: 22px; left: 6px; z-index: 1; } + .NB-story-comment .NB-story-comment-friend-share .NB-story-comment-author-avatar.NB-user-avatar.NB-story-comment-reshare { top: 10px; } + .NB-story-comment .NB-story-comment-author-avatar.NB-user-avatar img { border-radius: 6px; margin: 2px 0 0 1px; width: 38px; height: 38px; } + .NB-story-comment .NB-story-comment-author-avatar.NB-user-avatar.NB-story-comment-reshare img { height: 24px; width: 24px; } + .NB-story-comment .NB-story-comment-friend-share .NB-story-comment-author-avatar.NB-user-avatar img { height: 24px; - width: 24px; + width: 24px; margin-left: 6px; } + .NB-story-comment .NB-story-comment-friend-share .NB-story-comment-author-avatar.NB-user-avatar.NB-story-comment-reshare img { margin-left: 0px; } + .NB-story-comment .NB-story-comment-author-container { overflow: hidden; margin: 6px 0 0; } + .NB-story-comment .NB-story-comment-friend-share .NB-story-comment-author-container { margin-top: 10px; } + .NB-story-comment .NB-story-comment-reshares { position: absolute; top: 0; left: 8px; z-index: 0; } + .NB-story-comment .NB-story-comment-reshares .NB-user-avatar { top: 8px; left: 14px; } + .NB-story-comment .NB-story-comment-username { float: left; font-size: 11px; @@ -4131,17 +4651,20 @@ body { text-shadow: 0 -1px 0 rgba(255, 255, 255, .5); cursor: pointer; } + .NB-story-comment .NB-story-comment-date { font-size: 10px; color: #9D9D9D; float: left; margin-right: 4px; } + .NB-story-comment .NB-story-comment-likes { overflow: hidden; height: 14px; margin: 4px 2px 0; } + .NB-story-comment .NB-story-comment-like { float: left; width: 16px; @@ -4151,36 +4674,44 @@ body { margin-top: -1px; padding: 1px 6px 1px 2px; } + .NB-story-comment .NB-story-comment-like:hover, .NB-story-comment .NB-story-comment-like.NB-active { cursor: pointer; background: transparent url('/media/embed/icons/circular/g_icn_fav_active.png') no-repeat center 0; background-size: 14px; } + .NB-story-comment .NB-story-comment-like:active { cursor: pointer; } + .NB-story-comment .NB-story-comment-likes-users { display: inline-block; } + .NB-story-comment .NB-story-comment-likes-users .NB-story-share-profile .NB-user-avatar { width: auto; height: auto; } + .NB-story-comment .NB-story-comment-likes-users .NB-story-share-profile .NB-user-avatar img, .NB-story-comment .NB-story-comment-friend-share .NB-story-comment-likes-users .NB-story-share-profile .NB-user-avatar img { width: 12px; height: 12px; } + .NB-story-comment .NB-story-comment-content { float: left; color: #303030; } + .NB-story-comment .NB-story-comment-reply-button { padding: 4px 8px 4px 12px; float: right; cursor: pointer; } + .NB-story-comment .NB-story-comment-reply-button img { padding-right: 6px; width: 9px; @@ -4188,6 +4719,7 @@ body { vertical-align: bottom; float: left; } + .NB-story-comment .NB-story-comment-reply-button .NB-story-comment-reply-button-wrapper { background-color: #E9AF86; color: white; @@ -4195,7 +4727,7 @@ body { line-height: 9px; font-size: 9px; height: 9px; - + border-top: 1px solid rgba(255, 255, 255, .15); border-left: 1px solid rgba(255, 255, 255, .15); border-bottom: 1px solid rgba(0, 0, 0, .1); @@ -4206,9 +4738,11 @@ body { .NB-story-comment .NB-story-comment-reply-button:hover .NB-story-comment-reply-button-wrapper { background-color: #DE772B; } + .NB-story-comment .NB-story-comment-reply-button:active .NB-story-comment-reply-button-wrapper { background-color: #9F3A00; } + .NB-story-comment .NB-story-comment-error { float: left; font-size: 10px; @@ -4225,6 +4759,7 @@ body { padding: 5px 0 7px 32px; line-height: 18px; } + .NB-story-comment-reply .NB-story-comment-reply-photo { width: 24px; height: 24px; @@ -4240,6 +4775,7 @@ body { float: right; cursor: pointer; } + .NB-story-comment-edit-button .NB-story-comment-edit-button-wrapper { background-color: #74A2E7; color: white; @@ -4253,15 +4789,19 @@ body { border-right: 1px solid rgba(0, 0, 0, .1); text-shadow: 0 1px 0 rgba(0, 0, 0, .1); } + .NB-story-comment-edit-button:hover .NB-story-comment-edit-button-wrapper { background-color: #5073BC; } + .NB-story-comment-edit-button:active .NB-story-comment-edit-button-wrapper { background-color: #2A3B72; } + .NB-story-comment-share-edit-button { padding-right: 0; } + .NB-story-comment-reply-content { clear: both; color: #303030; @@ -4271,9 +4811,11 @@ body { .NB-story-comment-reply-form { padding-top: 11px; } + .NB-story-comment-reply-form .NB-story-comment-reply-username { margin: 1px 8px 6px 0; } + .NB-story-comment-reply-form .NB-story-comment-reply-comments { margin: 0 8px 4px 0; width: 50%; @@ -4281,6 +4823,7 @@ body { float: left; font-size: 12px; } + .NB-story-comment-reply-form .NB-modal-submit-button { float: left; font-size: 10px; @@ -4288,9 +4831,11 @@ body { line-height: 16px; margin: 0 12px 0 0; } + .NB-story-comment-reply-form .NB-modal-submit-delete { font-size: 9px; } + .NB-story-comment-reply-form .NB-error { font-size: 10px; color: #6A1000; @@ -4307,10 +4852,12 @@ body { .NB-story-comments-public-teaser-wrapper { cursor: pointer; } + .NB-story-comments-public-teaser-wrapper, .NB-story-comments-public-header-wrapper { margin-top: 12px; } + .NB-story-comments-shares-teaser { background-color: #F5F5EF; color: #898989; @@ -4328,19 +4875,22 @@ body { } .NB-story-comments-public-teaser-wrapper:hover .NB-story-comments-public-teaser { - background-image: -webkit-linear-gradient(center top , #EAECE5, #DDDFD6); - background-image: -moz-linear-gradient(center top , #EAECE5, #DDDFD6); - background-image: linear-gradient(center top , #EAECE5, #DDDFD6); + background-image: -webkit-linear-gradient(center top, #EAECE5, #DDDFD6); + background-image: -moz-linear-gradient(center top, #EAECE5, #DDDFD6); + background-image: linear-gradient(center top, #EAECE5, #DDDFD6); text-shadow: 0 1px 0 rgba(255, 255, 255, .8); } + .NB-story-comments-public-teaser-wrapper:active .NB-story-comments-public-teaser { - background-image: -webkit-linear-gradient(center top , #D6D9D0, #C7CABF); - background-image: -moz-linear-gradient(center top , #D6D9D0, #C7CABF); - background-image: linear-gradient(center top , #D6D9D0, #C7CABF); + background-image: -webkit-linear-gradient(center top, #D6D9D0, #C7CABF); + background-image: -moz-linear-gradient(center top, #D6D9D0, #C7CABF); + background-image: linear-gradient(center top, #D6D9D0, #C7CABF); } + .NB-story-comments-public-teaser b { padding: 0 6px; } + .NB-story-comments-expand-icon { background: transparent url("/media/embed/icons/circular/folder_expand.png") no-repeat center center; background-size: 13px 12px; @@ -4350,18 +4900,22 @@ body { margin: 2px 2px; opacity: .8; } + .NB-story-comments-expand-icon.NB-loading { background: transparent url('/media/embed/reader/ring_spinner.svg') no-repeat center center; background-size: 16px; } + .NB-story-comments-public-teaser-wrapper:hover .NB-story-comments-expand-icon { opacity: 1; } + .NB-story-share-label { display: inline-block; margin: 0 4px 0 0; float: right; } + .NB-story-share-profiles { display: inline-block; vertical-align: top; @@ -4369,32 +4923,40 @@ body { padding-top: 2px; overflow: hidden; } + .NB-story-share-profiles.NB-story-share-profiles-shares { float: right; } + .NB-story-share-profiles.NB-story-share-profiles-shares .NB-story-share-profile { float: right; } + .NB-story-share-profiles .NB-story-share-profiles-comments-friends, .NB-story-share-profiles .NB-story-share-profiles-comments-public { float: left; } + .NB-story-share-profiles .NB-story-share-profiles-shares-public, .NB-story-share-profiles .NB-story-share-profiles-shares-friends { display: inline; } + .NB-story-share-profiles .NB-story-share-profiles-comments-public, .NB-story-share-profiles .NB-story-share-profiles-shares-public { opacity: .5; } + .NB-story-comments-label { float: left; margin-right: 12px; cursor: pointer; } + .NB-story-comments-label b { -/* font-size: 12px;*/ + /* font-size: 12px;*/ } + .NB-story-share-label { float: right; margin-left: 12px; @@ -4404,6 +4966,7 @@ body { .NB-story-share-profile { display: inline-block; } + .NB-story-share-profile .NB-user-avatar { float: left; font-size: 0; @@ -4413,29 +4976,34 @@ body { padding: 0 2px; cursor: pointer; } + .NB-story-share-profile .NB-user-avatar img.NB-user-avatar-image { width: 22px; height: 22px; border-radius: 3px; } + .NB-story-comment-friend-share .NB-story-share-profile .NB-user-avatar img.NB-user-avatar-image { height: 18px; width: 18px; } .NB-story-share-profile .NB-user-avatar img.NB-user-avatar-private { - width: 8px; - height: 8px; - bottom: 0; - left: 0; + width: 8px; + height: 8px; + bottom: 0; + left: 0; } + .NB-story-share-profile .NB-user-username { float: left; } + .NB-story-comment .NB-story-comment-content { clear: both; padding: 0 0 6px 0; } + .NB-feed-story-endbar { height: 8px; border-top: 1px solid #404040; @@ -4465,6 +5033,7 @@ body { .NB-user-avatar { position: relative; } + .NB-user-avatar .NB-user-avatar-private { position: absolute; width: 12px; @@ -4472,6 +5041,7 @@ body { bottom: -2px; left: -1px; } + /* ============================= */ /* = Side Options in Feed view = */ /* ============================= */ @@ -4486,6 +5056,7 @@ body { line-height: 12px; transition: width 0.36s ease-out; } + .NB-story-starred .NB-feed-story-sideoptions-container { width: 164px; } @@ -4499,17 +5070,20 @@ body { clear: both; overflow: hidden; } + .NB-pref-story-position-center .NB-feed-story-sideoptions-container { margin-left: auto; margin-right: auto; max-width: 700px; padding: 0 28px; } + .NB-pref-story-position-right .NB-feed-story-sideoptions-container { margin-left: auto; max-width: 700px; padding: 0 0 0 28px; } + .NB-sideoption { cursor: pointer; position: relative; @@ -4528,6 +5102,7 @@ body { -o-user-select: none; user-select: none; } + .NB-narrow-content .NB-sideoption { float: left; margin: 8px 8px 0 0; @@ -4535,9 +5110,11 @@ body { min-width: 90px; clear: right; } + .NB-extra-narrow-content .NB-sideoption { min-width: 80px; } + .NB-extra-narrow-content .NB-sideoption-title span { display: none; } @@ -4551,14 +5128,17 @@ body { background-size: 16px; transition: background 0.36s ease-out; } + .NB-sideoption.NB-feed-story-train .NB-sideoption-icon { background-image: url("/media/embed/icons/nouns/train.svg"); background-size: 16px; } + .NB-sideoption.NB-feed-story-email .NB-sideoption-icon { background-image: url("/media/embed/icons/nouns/email.svg"); background-size: 16px; } + .NB-sideoption.NB-feed-story-share .NB-sideoption-icon { background-image: url('/media/embed/icons/nouns/share.svg'); background-size: 16px; @@ -4572,15 +5152,18 @@ body { height: 32px; box-sizing: border-box; transition: padding 0.36s ease-out, - color 0.36s ease-out; + color 0.36s ease-out; } + .NB-content-narrow .NB-sideoption .NB-sideoption-title { padding-left: 10px; padding-right: 30px; } + .NB-sideoption:hover { cursor: pointer; } + .NB-sideoption:hover, .NB-sideoption.NB-active, .NB-story-starred .NB-sideoption.NB-feed-story-save, @@ -4609,9 +5192,10 @@ body { overflow: hidden; display: none; } + .NB-sideoption-save-tag ::-moz-selection { background: transparent; -} +} .NB-sideoption-save-tag ::selection { background: transparent; @@ -4627,12 +5211,14 @@ body { width: 100%; margin: 0; } + .NB-sideoption-save { padding: 4px 6px 6px; border: 1px solid #DBE6EA; text-align: left; color: #606060; } + .NB-sideoption-save .NB-sideoption-save-icon { float: left; margin: 11px 2px 0 0; @@ -4641,6 +5227,7 @@ body { background: transparent url("/media/img/icons/nouns/tag.svg") no-repeat 0 0; background-size: 14px; } + .NB-sideoption-save .NB-sideoption-save-title { font-size: 10px; text-align: left; @@ -4649,11 +5236,11 @@ body { } .NB-sideoption-save .NB-sideoption-save-tag { - width: 100%; - margin: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + width: 100%; + margin: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .NB-sideoption-save .NB-sideoption-save-populate { @@ -4669,12 +5256,15 @@ body { cursor: pointer; display: inline-block; } + .NB-sideoption-save .NB-sideoption-save-populate:hover { - background-color: rgba(205, 205, 205, .8); + background-color: rgba(205, 205, 205, .8); } + .NB-sideoption-save .NB-sideoption-save-populate:active { - background-color: rgba(205, 205, 205, 1); + background-color: rgba(205, 205, 205, 1); } + .NB-tagging-autocomplete.ui-autocomplete { font-size: 11px; width: 150px !important; @@ -4689,17 +5279,20 @@ body { width: 100%; overflow: hidden; } -.NB-menu-manage .NB-sideoption-share-wrapper { -} + +.NB-menu-manage .NB-sideoption-share-wrapper {} + .NB-narrow-content .NB-sideoption-share-wrapper { clear: both; width: 100%; margin: 0; } + .NB-sideoption-share { padding: 9px 12px; border: 1px solid #DBE6EA; } + .NB-sideoption-share .NB-sideoption-share-title { font-size: 12px; margin: 2px 0 8px; @@ -4707,10 +5300,12 @@ body { text-shadow: 0 1px 0 #F6F6F6; color: #202020; } + .NB-sideoption-share .NB-sideoption-share-crosspost { margin: 8px 0; overflow: hidden; } + .NB-sideoption-share .NB-sideoption-share-crosspost-twitter, .NB-sideoption-share .NB-sideoption-share-crosspost-facebook { float: left; @@ -4721,14 +5316,17 @@ body { cursor: pointer; border: 1px solid rgba(0, 0, 0, .05); } + .NB-sideoption-share .NB-sideoption-share-crosspost-twitter { background: transparent url('/media/embed/reader/twitter_service_off.png') no-repeat center center; background-size: 12px; } + .NB-sideoption-share .NB-sideoption-share-crosspost-facebook { background: transparent url('/media/embed/reader/facebook_service_off.png') no-repeat center center; background-size: 12px; } + .NB-sideoption-share .NB-sideoption-share-crosspost-twitter.NB-active, .NB-sideoption-share .NB-sideoption-share-crosspost-twitter:hover { background: transparent url('/media/embed/reader/twitter_service.png') no-repeat center center; @@ -4736,6 +5334,7 @@ body { border-color: #4E8ECD; background-color: rgba(78, 142, 205, .1); } + .NB-sideoption-share .NB-sideoption-share-crosspost-facebook.NB-active, .NB-sideoption-share .NB-sideoption-share-crosspost-facebook:hover { background: transparent url('/media/embed/reader/facebook_service.png') no-repeat center center; @@ -4743,6 +5342,7 @@ body { border-color: #6884CD; background-color: rgba(104, 132, 205, .1); } + .NB-sideoption-share .NB-sideoption-share-crosspost-text { font-size: 9px; text-align: left; @@ -4750,6 +5350,7 @@ body { text-shadow: 0 1px 0 #FBFBFB; line-height: 16px; } + .NB-sideoption-save .NB-sideoption-save-message { float: right; opacity: 0; @@ -4758,30 +5359,35 @@ body { font-size: 10px; color: yellowgreen; } + .NB-sideoption-save .NB-sideoption-save-message.NB-active { opacity: 1; } + .NB-sideoption-save .NB-sideoption-save-notes, .NB-sideoption-share .NB-sideoption-share-comments { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 52px; - border-color: #C6C6C6; - border-radius: 4px; - margin: 0 0 12px; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + height: 52px; + border-color: #C6C6C6; + border-radius: 4px; + margin: 0 0 12px; } + .NB-sideoption-share .NB-sideoption-share-save { - margin: 2px 0; - max-width: 138px; - box-sizing: border-box; - cursor: pointer; + margin: 2px 0; + max-width: 138px; + box-sizing: border-box; + cursor: pointer; } + .NB-sideoption-share .NB-sideoption-share-save.NB-saving { background-color: #b5b4bB; text-shadow: none; } + .NB-sideoption-share .NB-sideoption-share-unshare { line-height: 1; margin: 6px 0; @@ -4789,6 +5395,7 @@ body { box-sizing: border-box; font-weight: normal; } + .NB-sideoption-share .NB-error { font-size: 10px; color: #6A1000; @@ -4802,46 +5409,48 @@ body { /* ====================== */ #story_taskbar .NB-river-progress { - margin: 6px 12px 0; - width: 150px; - height: 20px; + margin: 6px 12px 0; + width: 150px; + height: 20px; } #story_taskbar .NB-river-progress .NB-river-progress-text { - text-transform: uppercase; - font-size: 10px; - text-align: center; - font-weight: bold; - color: #304056; - text-shadow: 1px 1px 0 #D8D8D8; + text-transform: uppercase; + font-size: 10px; + text-align: center; + font-weight: bold; + color: #304056; + text-shadow: 1px 1px 0 #D8D8D8; } #story_taskbar .NB-river-progress .NB-river-progress-bar { - width: 150px; - height: 8px; + width: 150px; + height: 8px; } + .NB-taskbar-info { position: absolute; top: 0; left: 0; } + /* ============== */ /* = Feed Error = */ /* ============== */ #story_taskbar .NB-feed-error { - margin: 5px 12px 0; - height: 20px; + margin: 5px 12px 0; + height: 20px; } #story_taskbar .NB-feed-error .NB-feed-error-text { - font-size: 10px; - text-align: center; - font-weight: bold; - color: #4E0A0B; - text-shadow: 1px 1px 0 #D8D8D8; - float: left; - margin-left: 8px; + font-size: 10px; + text-align: center; + font-weight: bold; + color: #4E0A0B; + text-shadow: 1px 1px 0 #D8D8D8; + float: left; + margin-left: 8px; } #story_taskbar .NB-feed-error-type-proxy .NB-feed-error-text { @@ -4856,6 +5465,7 @@ body { background: transparent url('/media/embed/reader/warning.png') no-repeat 0 0; float: left; } + #story_taskbar .NB-feed-error-type-proxy .NB-feed-error-icon { opacity: .5; } @@ -4884,17 +5494,20 @@ body { background-image: linear-gradient(180deg, #F3F3EE, #f0f3ed); border-bottom: 1px solid #dbdbda; } + .NB-feeds-header-user .NB-feeds-header-user-image { position: absolute; left: 0; top: 0; padding: 4px; } + .NB-feeds-header-user .NB-feeds-header-user-image img { width: 28px; height: 28px; border-radius: 2px; } + .NB-feeds-header-collapse-sidebar, .NB-feeds-header-user-interactions { cursor: pointer; @@ -4906,19 +5519,23 @@ body { line-height: 0; text-align: center; } + .NB-feeds-header-user-interactions { right: 32px; } + .NB-feeds-header-collapse-sidebar img, .NB-feeds-header-user-interactions img { width: 16px; height: 16px; opacity: .7; } + .NB-feeds-header-collapse-sidebar:hover img, .NB-feeds-header-user-interactions:hover img { opacity: .9; } + .NB-feeds-header-user-interactions .NB-feeds-header-user-interactions-badge { position: absolute; top: 4px; @@ -4937,14 +5554,17 @@ body { text-shadow: 0 1px 0 rgba(0, 0, 0, .2); z-index: 1; } + .NB-feeds-header-collapse-sidebar { opacity: .3; cursor: default; } + .NB-feeds-header-collapse-sidebar img, .NB-feeds-header-collapse-sidebar:hover img { opacity: .7; } + .NB-feeds-header-user .NB-feeds-header-user-name { padding: 4px 0 0 6px; font-weight: bold; @@ -4955,33 +5575,41 @@ body { .NB-feeds-header-dashboard { font-size: 10px; } + .NB-feeds-header-dashboard .NB-feeds-header-count { - padding: 0 0 0 16px; - text-align: left; + padding: 0 0 0 16px; + text-align: left; } + .NB-feeds-header-dashboard .NB-feeds-header-count.NB-empty { opacity: .45; color: #B0B0B0; } + .NB-feeds-header-dashboard .NB-feeds-header-negative { background: transparent url('/media/embed/icons/nouns/indicator-hidden.svg') no-repeat 6px 2px; display: none; } + .NB-feeds-header-dashboard .NB-feeds-header-neutral { background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 6px 2px; background-size: 8px; } + .NB-feeds-header-dashboard .NB-feeds-header-positive { background: transparent url('/media/embed/icons/nouns/indicator-focus.svg') no-repeat 6px 2px; background-size: 8px; } + .NB-feeds-header-dashboard .NB-feeds-header-starred { background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 6px 2px; background-size: 8px; } + .NB-feeds-header-dashboard .NB-feeds-header-right { position: relative; } + .NB-feeds-header-dashboard .NB-feeds-header-sites { float: right; padding-right: 6px; @@ -5016,12 +5644,15 @@ body { .NB-theme-feed-size-xs .NB-feeds-header { font-size: 10px; } + .NB-theme-feed-size-m .NB-feeds-header { font-size: 12px; } + .NB-theme-feed-size-l .NB-feeds-header { font-size: 13px; } + .NB-theme-feed-size-xl .NB-feeds-header { font-size: 14px; } @@ -5057,9 +5688,7 @@ body { } -.NB-feeds-header.NB-selected .NB-feeds-header-title { - -} +.NB-feeds-header.NB-selected .NB-feeds-header-title {} /* ========================== */ /* = Header - River of News = */ @@ -5070,18 +5699,20 @@ body { } .NB-feeds-header-river-container .NB-feeds-header-count { - background-color: #11448B; - display: block; - padding: 0 4px; - margin: 2px 3px 0 0; + background-color: #11448B; + display: block; + padding: 0 4px; + margin: 2px 3px 0 0; } + .NB-feeds-header-river-container .NB-feeds-header.NB-empty .NB-feeds-header-count { - display: none; + display: none; } .NB-feeds-header-river-dashboard-container { display: block; } + /* ============================ */ /* = Header - Starred Stories = */ /* ============================ */ @@ -5092,11 +5723,13 @@ body { margin-right: 11px; display: block; } + .NB-starred-feeds { background-color: #E9EBEE; } + .NB-starred-feeds .unread_count_positive { - background-color: #506B9A; + background-color: #506B9A; } .NB-starred-feeds .feed { @@ -5104,11 +5737,13 @@ body { /* border-bottom: 1px solid #E9EBEE; */ background-color: #dfdcd5; } + .NB-starred-feeds { background-color: #dfdcd5; } + .NB-feeds-header-starred.NB-empty .NB-feeds-header-count { - display: none; + display: none; } /* ========================= */ @@ -5118,12 +5753,13 @@ body { .NB-feeds-header-read-container.NB-block { display: block; } + .NB-feeds-header-read { display: block; } .NB-feeds-header-read.NB-empty .NB-feeds-header-count { - display: none; + display: none; } @@ -5136,6 +5772,7 @@ body { background-image: inherit; background-color: inherit; } + .NB-feeds-header.NB-feeds-header-searches:hover { background-image: inherit; background-color: inherit; @@ -5151,16 +5788,19 @@ body { .NB-searches-feeds { background-color: #dadfe7; } + .NB-searches-feeds .feed { /* border-top: 1px solid #E9EBEE; border-bottom: 1px solid #E9EBEE; */ background-color: #dadfe7; } + .NB-searches-feeds .feed_title { padding-right: 4px; } + .NB-feeds-header-searches.NB-empty .NB-feeds-header-count { - display: none; + display: none; } .NB-searches-folder .NB-searches-feeds.NB-feedlist .feed { @@ -5176,11 +5816,11 @@ body { } .NB-feeds-header-tryfeed .NB-feeds-header-title { - text-transform: none; + text-transform: none; } .NB-feeds-header-tryfeed.NB-empty .NB-feeds-header-count { - display: none; + display: none; } /* ============ */ @@ -5197,8 +5837,10 @@ body { .NB-taskbar { position: relative; -/* overflow: hidden;*/ /* No hidden overflow so ftux callouts can show */ + /* overflow: hidden;*/ + /* No hidden overflow so ftux callouts can show */ } + .NB-taskbar .NB-task-view-switch-arrow { position: absolute; top: 5px; @@ -5223,29 +5865,36 @@ body { .NB-feed-taskbar { overflow: hidden; } + .NB-task-button { padding: 10px 7px; cursor: pointer; opacity: .9; } + .NB-task-button:hover { opacity: 1; } + .NB-task-button.NB-disabled, .NB-task-button.NB-disabled:hover { opacity: .2; cursor: default; } + #story_taskbar .NB-task-story-next-unread { min-width: 32px; } + .NB-task-add { float: left; } + .NB-task-manage { float: right; margin-right: 16px; } + .NB-task-drag { position: absolute; bottom: 0; @@ -5259,14 +5908,17 @@ body { background-size: 16px 36px; opacity: 0.3; } + .NB-task-image { width: 16px; height: 16px; } + .NB-task-add .NB-task-image { background: transparent url('/media/embed/icons/nouns/add.svg') no-repeat 0 0; background-size: 16px; } + .NB-task-manage .NB-task-image { background: transparent url('/media/embed/icons/nouns/settings.svg') no-repeat 0 0; background-size: 16px; @@ -5277,30 +5929,34 @@ body { /* =================== */ .NB-taskbar ::-moz-selection { -background: transparent; -} + background: transparent; +} .NB-taskbar ::selection { -background: transparent; + background: transparent; } #story_taskbar { - overflow: hidden; + overflow: hidden; display: none; border-top: 1px solid #dbdbda; } + #story_taskbar .NB-taskbar { - margin: 5px 0 0 0; + margin: 5px 0 0 0; } + .NB-feedbar .NB-taskbar { margin: 2px 0 0 0; } + #story_taskbar .NB-taskbar .NB-taskbar-button { font-size: 11px; padding: 4px 8px; line-height: 18px; position: relative; } + .NB-feedbar .NB-taskbar .NB-taskbar-button { font-size: 11px; padding: 3px 8px; @@ -5318,13 +5974,16 @@ background: transparent; margin: 6px 18px 0 0; font-weight: normal; } + #story_taskbar .NB-taskbar-layout { float: right; margin-right: 18px; } + .NB-taskbar .NB-task-title { padding: 0 0 0 2px; } + .NB-taskbar .NB-task-image { vertical-align: top; width: 18px; @@ -5334,6 +5993,7 @@ background: transparent; opacity: 0.7; transition: opacity 0.25s ease-out; } + .NB-taskbar .NB-active .NB-task-image { opacity: 1; } @@ -5342,70 +6002,85 @@ background: transparent; background: transparent url('/media/img/icons/nouns/content-view-original.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .task_view_page.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .task_view_feed .NB-task-image { background: transparent url('/media/img/icons/nouns/content-view-feed.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .task_view_feed.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .task_view_story .NB-task-image { background: transparent url('/media/img/icons/nouns/content-view-story.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .task_view_story.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .task_view_text .NB-task-image { background: transparent url('/media/img/icons/nouns/content-view-text.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .task_view_text.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .task_view_story.NB-disabled-page .NB-task-image { background-image: url('/media/embed/icons/circular/exclamation.png'); background-size: 16px; } + .NB-taskbar .NB-task-story-next-starred .NB-task-image { - background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat center center; + background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat center center; background-size: 8px; width: 8px; height: 8px; margin-top: 5px; } + .NB-taskbar .NB-task-story-next-positive .NB-task-image { - background: transparent url('/media/embed/icons/nouns/indicator-focus.svg') no-repeat center center; + background: transparent url('/media/embed/icons/nouns/indicator-focus.svg') no-repeat center center; background-size: 8px; width: 8px; height: 8px; margin-top: 5px; } + .NB-taskbar .NB-task-story-next-neutral .NB-task-image { - background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat center center; + background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat center center; background-size: 8px; width: 8px; height: 8px; margin-top: 5px; } + .NB-taskbar .NB-task-story-next-negative .NB-task-image { - background: transparent url('/media/embed/icons/nouns/indicator-hidden.svg') no-repeat center center; + background: transparent url('/media/embed/icons/nouns/indicator-hidden.svg') no-repeat center center; background-size: 8px; width: 8px; height: 8px; margin-top: 5px; } + .NB-taskbar .task_view_page.NB-exception-page .NB-task-image { background-image: url('/media/embed/icons/circular/exclamation.png'); background-size: 16px; } + .NB-taskbar .task_view_page.NB-disabled-page .NB-task-image { background-image: url('/media/embed/icons/circular/exclamation.png'); background-size: 16px; } + .NB-taskbar .NB-task-story-previous .NB-task-image { left: 12px; width: 12px; @@ -5414,6 +6089,7 @@ background: transparent; background: transparent url('/media/embed/icons/nouns/nav-previous.svg') no-repeat center center; background-size: 12px; } + .NB-taskbar .NB-task-story-next .NB-task-image { left: 12px; width: 12px; @@ -5428,38 +6104,47 @@ background: transparent; background: transparent url('/media/img/icons/nouns/layout-full.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .NB-task-layout-full.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .NB-task-layout-split .NB-task-image { left: 12px; background: transparent url('/media/img/icons/nouns/layout-split.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .NB-task-layout-split.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .NB-task-layout-list .NB-task-image { left: 12px; background: transparent url('/media/img/icons/nouns/layout-list.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .NB-task-layout-list.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .NB-task-layout-grid .NB-task-image { left: 12px; background: transparent url('/media/img/icons/nouns/layout-grid.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .NB-task-layout-grid.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } + .NB-taskbar .NB-task-layout-magazine .NB-task-image { left: 12px; background: transparent url('/media/img/icons/nouns/layout-magazine.svg') no-repeat center center; background-size: 16px; } + .NB-taskbar .NB-task-layout-magazine.NB-active .NB-task-image { filter: hue-rotate(150deg) saturate(10); } @@ -5467,17 +6152,20 @@ background: transparent; .NB-taskbar .NB-task-return .NB-task-image { background: transparent url('/media/embed/icons/silk/arrow_undo.png') no-repeat center center; } + .NB-taskbar .task_view_page .NB-task-title.NB-task-original-return { display: none; font-size: 12px; padding-top: 1px; line-height: 13px; } + .NB-taskbar .task_view_page.NB-task-return .NB-task-title { - display: none; + display: none; } + .NB-taskbar .task_view_page.NB-task-return .NB-task-title.NB-task-original-return { - display: inline-block; + display: inline-block; } @@ -5494,6 +6182,7 @@ background: transparent; box-shadow: #9A9A9A 2px 2px 0px; text-shadow: 0 1px 0 rgba(0, 0, 0, .4); } + .NB-narrow #story_taskbar .NB-task-title { display: none; } @@ -5506,6 +6195,7 @@ background: transparent; float: right; margin: 3px 24px 0 0; } + #story_taskbar .NB-taskbar-options { cursor: pointer; float: right; @@ -5519,10 +6209,12 @@ background: transparent; line-height: 14px; margin: 6px 0 0; } + #story_taskbar .NB-taskbar-options:hover, #story_taskbar .NB-taskbar-options.NB-active { background-color: rgba(0, 0, 0, .1); } + #story_taskbar .NB-taskbar-options .NB-icon { float: right; width: 16px; @@ -5545,49 +6237,57 @@ background: transparent; line-height: 16px; font-weight: bold; } -.NB-filter-popover .segmented-control.NB-options-feed-size li { -} +.NB-filter-popover .segmented-control.NB-options-feed-size li {} + .NB-filter-popover .segmented-control li.NB-options-feed-size-xs, .NB-style-popover li.NB-options-font-size-xs, .NB-style-popover li.NB-options-feed-size-xs { font-size: 9px; padding: 3px 0 1px; } + .NB-filter-popover .segmented-control .NB-options-feed-size-s, .NB-style-popover .NB-options-font-size-s, .NB-style-popover .NB-options-feed-size-s { font-size: 10px; } + .NB-filter-popover .segmented-control li.NB-options-feed-size-m, .NB-style-popover li.NB-options-font-size-m, .NB-style-popover li.NB-options-feed-size-m { font-size: 12px; padding: 3px 0 1px; } + .NB-filter-popover .segmented-control .NB-options-feed-size-l, .NB-style-popover .NB-options-font-size-l, .NB-style-popover .NB-options-feed-size-l { font-size: 13px; } + .NB-filter-popover .segmented-control li.NB-options-feed-size-xl, .NB-style-popover li.NB-options-font-size-xl, .NB-style-popover li.NB-options-feed-size-xl { font-size: 15px; } + .NB-filter-popover .segmented-control.NB-options-feed-font li { padding: 4px 15px; width: auto; } + .NB-style-popover .NB-options-line-spacing { margin-top: 6px; } + .NB-style-popover .NB-options-line-spacing li { width: 45px; padding: 2px 0; line-height: 16px; font-weight: bold; } + .NB-filter-popover .NB-menu-manage-view-setting-contentpreview .NB-icon, .NB-style-popover .NB-menu-manage-view-setting-contentpreview .NB-icon, .NB-style-popover .NB-options-line-spacing .NB-icon { @@ -5598,33 +6298,41 @@ background: transparent; background-repeat: no-repeat; margin: 0 auto; } + .NB-filter-popover .NB-view-setting-contentpreview-small .NB-icon, .NB-style-popover .NB-view-setting-contentpreview-small .NB-icon { background-image: url("/media/embed/icons/nouns/content-preview-s.svg"); background-size: 16px; } + .NB-filter-popover .NB-view-setting-contentpreview-medium .NB-icon, .NB-style-popover .NB-view-setting-contentpreview-medium .NB-icon { background-image: url("/media/embed/icons/nouns/content-preview-m.svg"); background-size: 16px; } + .NB-filter-popover .NB-view-setting-contentpreview-large .NB-icon, .NB-style-popover .NB-view-setting-contentpreview-large .NB-icon { background-image: url("/media/embed/icons/nouns/content-preview-l.svg"); background-size: 16px; } + .NB-style-popover .NB-options-line-spacing-xs .NB-icon { background-image: url("/media/embed/reader/line_spacing_xs.png"); } + .NB-style-popover .NB-options-line-spacing-s .NB-icon { background-image: url("/media/embed/reader/line_spacing_s.png"); } + .NB-style-popover .NB-options-line-spacing-m .NB-icon { background-image: url("/media/embed/reader/line_spacing_m.png"); } + .NB-style-popover .NB-options-line-spacing-l .NB-icon { background-image: url("/media/embed/reader/line_spacing_l.png"); } + .NB-style-popover .NB-options-line-spacing-xl .NB-icon { background-image: url("/media/embed/reader/line_spacing_xl.png"); } @@ -5632,43 +6340,50 @@ background: transparent; .NB-style-popover .NB-options-font-family { width: 236px; } + .NB-style-popover .NB-options-font-family li { padding: 5px 0; font-size: 13px; line-height: 15px; text-transform: none; } + .NB-style-popover .NB-options-font-family li.NB-disabled { color: #AEAEAE; } + .NB-style-popover li.NB-options-font-family-sans-serif { font-size: 12px; } + .NB-style-popover li.NB-options-font-family-serif { font-family: Georgia, serif; } + .NB-style-popover li.NB-options-font-family-gotham { - font-family: 'Gotham Narrow A', 'Gotham Narrow B'; - font-weight: 400; + font-family: 'Gotham Narrow A', 'Gotham Narrow B'; + font-weight: 400; font-style: normal; } + .NB-style-popover li.NB-options-font-family-sentinel { - font-family: 'Sentinel A', 'Sentinel B'; - font-weight: 400; + font-family: 'Sentinel A', 'Sentinel B'; + font-weight: 400; font-style: normal; } .NB-style-popover li.NB-options-font-family-whitney { - font-family: 'Whitney SSm A', 'Whitney SSm B'; - font-weight: 400; + font-family: 'Whitney SSm A', 'Whitney SSm B'; + font-weight: 400; font-style: normal; } .NB-style-popover li.NB-options-font-family-chronicle { - font-family: 'Chronicle Display A', 'Chronicle Display B'; - font-weight: 400; + font-family: 'Chronicle Display A', 'Chronicle Display B'; + font-weight: 400; font-style: normal; } + .NB-style-popover .NB-premium-only .NB-tag { float: right; font-size: 8px; @@ -5682,6 +6397,7 @@ background: transparent; margin: 2px 4px 0; /* font-family: "Lucida Grande", Verdana, "Helvetica Neue", Helvetica, sans-serif; */ } + .NB-style-popover .NB-premium-explainer { font-size: 10px; text-align: center; @@ -5693,6 +6409,7 @@ background: transparent; font-size: 12px; padding: 4px 0; } + .NB-style-popover .NB-options-story-titles-pane-south { width: 84px; } @@ -5704,18 +6421,22 @@ background: transparent; margin: 0 8px 1px 0; vertical-align: bottom; } + .NB-options-story-titles-pane-north .NB-icon { background: transparent url("/media/embed/icons/nouns/layout-top.svg") no-repeat center center; background-size: 16px; } + .NB-options-story-titles-pane-west .NB-icon { background: transparent url("/media/embed/icons/nouns/layout-left.svg") no-repeat center center; background-size: 16px; } + .NB-options-story-titles-pane-south .NB-icon { background: transparent url("/media/embed/icons/nouns/layout-bottom.svg") no-repeat center center; background-size: 16px; } + .NB-style-popover .NB-story-position-option .NB-icon { width: 16px; height: 16px; @@ -5723,26 +6444,30 @@ background: transparent; margin: 0; vertical-align: text-bottom; } + .NB-options-story-position .NB-story-position-option { width: auto; font-size: 12px; padding: 4px 10px; } -.NB-options-story-position-stretch { - -} + +.NB-options-story-position-stretch {} + .NB-options-story-position-left .NB-icon { background: transparent url("/media/embed/icons/nouns/position-left.svg") no-repeat center center; background-size: 16px; } + .NB-options-story-position-center .NB-icon { background: transparent url("/media/embed/icons/nouns/position-center.svg") no-repeat center center; background-size: 16px; } + .NB-options-story-position-right .NB-icon { background: transparent url("/media/embed/icons/nouns/position-right.svg") no-repeat center center; background-size: 16px; } + .NB-style-popover .NB-options-story-position-stretch .NB-icon { background: transparent url("/media/embed/icons/nouns/position-stretch.svg") no-repeat center center; background-size: 16px; @@ -5752,11 +6477,13 @@ background: transparent; .NB-style-popover .NB-options-single-story { margin-top: 6px; } + .NB-style-popover .NB-single-story-option { width: 116px; font-size: 12px; padding: 4px 0; } + .NB-style-popover .NB-single-story-option .NB-icon { width: 16px; height: 16px; @@ -5764,10 +6491,12 @@ background: transparent; margin: 0 8px 1px 0; vertical-align: bottom; } + .NB-options-single-story-off .NB-icon { background: transparent url("/media/embed/icons/nouns/single-story-all.svg") no-repeat center center; background-size: 16px; } + .NB-options-single-story-on .NB-icon { background: transparent url("/media/embed/icons/nouns/single-story-one.svg") no-repeat center center; background-size: 16px; @@ -5777,12 +6506,15 @@ background: transparent; .NB-style-popover .NB-options-grid-height { margin-top: 6px; } + .NB-style-popover .NB-options-grid-columns-0 { flex-grow: 3; } + .NB-style-popover .NB-options-grid-height-s { flex-grow: 2; } + .NB-style-popover .NB-options-grid-height-m { flex-grow: 3; } @@ -5793,9 +6525,11 @@ background: transparent; font-size: 12px; padding: 4px 8px; } + .NB-style-popover .NB-grid-height-option { min-width: 22px; } + .NB-style-popover .NB-grid-columns-option .NB-icon { width: 16px; height: 16px; @@ -5814,12 +6548,15 @@ background: transparent; border-left: 1px solid #e5e5e4; width: 2px; } + .left-south { border-top: 1px solid #dbdbda; } + .ui-layout-toggler { -/* display: none !important;*/ + /* display: none !important;*/ } + .right-pane .ui-layout-resizer-west { background-color: #F7F8F5; } @@ -5835,8 +6572,7 @@ background: transparent; /* = OPML Import Form = */ /* ==================== */ -form.opml_import_form { -} +form.opml_import_form {} form.opml_import_form textarea { width: 100%; @@ -5851,6 +6587,7 @@ form.opml_import_form .section { form.opml_import_form label { display: block; } + form.opml_import_form input { display: block; clear: both; @@ -5887,8 +6624,7 @@ form.opml_import_form input { /* = Feed Frame = */ /* ============== */ -.NB-feed-frame { -} +.NB-feed-frame {} /* =============== */ /* = Splash Pane = */ @@ -5914,63 +6650,79 @@ form.opml_import_form input { gap: 24px; padding: 0 24px; } + @media screen and (max-width: 1100px) { .NB-splash-modules { display: block; } + .NB-splash-modules .NB-modules-center, .NB-splash-modules .NB-dashboard-account { max-width: none; } + .NB-splash-modules .NB-modules-center, .NB-splash-modules .NB-account-wide { margin-right: 0; } + .NB-splash-modules .NB-dashboard-account { margin: 0 24px; } } + .NB-dashboard-columns-single .NB-splash-modules { flex-flow: column; } + .NB-dashboard-columns-single .NB-splash-modules .NB-modules-center, .NB-dashboard-columns-single .NB-splash-modules .NB-dashboard-account { max-width: none; } + .NB-dashboard-columns-single .NB-splash-modules .NB-modules-center, .NB-dashboard-columns-single .NB-splash-modules .NB-account-wide { margin-right: 0; } + .NB-dashboard-columns-triple .NB-splash-modules { display: block; } + .NB-dashboard-columns-triple .NB-splash-modules .NB-modules-center, .NB-dashboard-columns-triple .NB-splash-modules .NB-dashboard-account { max-width: none; } + .NB-dashboard-columns-triple .NB-dashboard-rivers-left .NB-module-header, .NB-dashboard-columns-triple .NB-dashboard-rivers-right .NB-module-header { margin: 0 0 24px; } + .NB-density-compact.NB-dashboard-columns-triple .NB-dashboard-rivers-left .NB-module-header, .NB-density-compact.NB-dashboard-columns-triple .NB-dashboard-rivers-right .NB-module-header { margin: 0 0 4px; } + .NB-dashboard-columns-triple .NB-splash-modules .NB-modules-center, .NB-dashboard-columns-triple .NB-splash-modules .NB-account-wide { margin: 24px 0 0; } + .NB-density-compact.NB-dashboard-columns-triple .NB-splash-modules { padding: 0; } + .NB-density-compact.NB-dashboard-columns-triple .NB-splash-modules .NB-modules-center, .NB-density-compact.NB-dashboard-columns-triple .NB-splash-modules .NB-account-wide { margin: 4px 0 0; padding: 0 4px; } + .NB-dashboard-columns-triple .NB-splash-modules .NB-dashboard-account { margin: 0 24px; } + .NB-dashboard-columns-triple .NB-dashboard-rivers-left, .NB-dashboard-columns-triple .NB-dashboard-rivers-right { display: flex; @@ -5980,18 +6732,22 @@ form.opml_import_form input { justify-content: center; gap: 24px; } + .NB-density-compact.NB-dashboard-columns-triple .NB-dashboard-rivers-left, .NB-density-compact.NB-dashboard-columns-triple .NB-dashboard-rivers-right { gap: 4px; } + .NB-dashboard-columns-triple .NB-dashboard-rivers-left .NB-dashboard-river, .NB-dashboard-columns-triple .NB-dashboard-rivers-right .NB-dashboard-river { flex-basis: 100%; } -.NB-dashboard-columns-triple .NB-dashboard-rivers-left > *, -.NB-dashboard-columns-triple .NB-dashboard-rivers-right > * { + +.NB-dashboard-columns-triple .NB-dashboard-rivers-left>*, +.NB-dashboard-columns-triple .NB-dashboard-rivers-right>* { flex: 1; } + .NB-dashboard-columns-triple .NB-dashboard-rivers-left .NB-feedbar-options, .NB-dashboard-columns-triple .NB-dashboard-rivers-right .NB-feedbar-options { text-indent: 258%; @@ -5999,31 +6755,36 @@ form.opml_import_form input { overflow: hidden; width: 12px; } + .NB-splash-info { - width: 100%; - height: 55px; - - bottom: 0; - position: absolute; - right: 0; - z-index: 1; + width: 100%; + height: 55px; + + bottom: 0; + position: absolute; + right: 0; + z-index: 1; } + .NB-splash-info.NB-splash-top { - top: 0; - bottom: inherit; - border-bottom: 1px solid rgba(0,0,0,0.1); + top: 0; + bottom: inherit; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); } + .NB-body-main .NB-splash-info.NB-splash-top { display: none; } + .NB-splash-info.NB-splash-bottom { - border-top: 1px solid rgba(0,0,0,0.1); - height: 36px; - overflow: hidden; - background: rgba(243, 245, 241, .7); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); + border-top: 1px solid rgba(0, 0, 0, 0.1); + height: 36px; + overflow: hidden; + background: rgba(243, 245, 241, .7); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); } + .NB-splash-info .NB-splash-title { position: absolute; bottom: -1px; @@ -6039,6 +6800,7 @@ form.opml_import_form input { right: 24px; width: 168px; } + .NB-body-main .NB-splash-info.NB-splash-top .NB-splash-title { display: none; } @@ -6058,26 +6820,26 @@ form.opml_import_form input { } .NB-splash-info .NB-splash-links .NB-splash-link { - display: block; - overflow: hidden; - line-height: 12px; - height: 36px; - margin: 0; - float: left; + display: block; + overflow: hidden; + line-height: 12px; + height: 36px; + margin: 0; + float: left; } .NB-splash-info .NB-splash-links .NB-splash-link a { - margin: 0; - padding: 12px 12px 12px 20px; - display: block; - -webkit-transition: all 0.36s ease-out; - -moz-transition: all 0.36s ease-out; - -o-transition: all 0.36s ease-out; - -ms-transition: all 0.36s ease-out; - background: transparent url('/media/embed/reader/spacer_16.png') no-repeat 0 -17px; - background-size: 16px; - font-weight: bold; - text-rendering: optimizeLegibility; + margin: 0; + padding: 12px 12px 12px 20px; + display: block; + -webkit-transition: all 0.36s ease-out; + -moz-transition: all 0.36s ease-out; + -o-transition: all 0.36s ease-out; + -ms-transition: all 0.36s ease-out; + background: transparent url('/media/embed/reader/spacer_16.png') no-repeat 0 -17px; + background-size: 16px; + font-weight: bold; + text-rendering: optimizeLegibility; } @media screen and (max-width: 1150px) { @@ -6086,30 +6848,36 @@ form.opml_import_form input { padding-left: 19px; } } + @media screen and (max-width: 950px) { .NB-splash-info .NB-splash-links .NB-splash-link a { padding-right: 1px; padding-left: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link a:hover { -/* background: none !important;*/ + /* background: none !important;*/ } } + .NB-splash-info .NB-splash-link-logo { display: none; } + .NB-body-main .NB-splash-info .NB-splash-link-logo { float: right; display: block; opacity: 0.4; } + .NB-splash-blurred-logo { - background: transparent url('/media/embed/logo_newsblur_blur.png') no-repeat center center; - background-size: contain; - width: 146px; - height: 24px; - margin-top: 6px; + background: transparent url('/media/embed/logo_newsblur_blur.png') no-repeat center center; + background-size: contain; + width: 146px; + height: 24px; + margin-top: 6px; } + .NB-splash-info .NB-splash-links .NB-splash-link.NB-splash-link-logo a { padding-top: 0; padding-bottom: 0; @@ -6121,36 +6889,43 @@ form.opml_import_form input { .NB-splash-info .NB-splash-links .NB-splash-link-faq a:hover, .NB-splash-info .NB-splash-links .NB-splash-link-api a:hover, .NB-splash-info .NB-splash-links .NB-splash-link-press a:hover { - background: transparent url('/media/embed/favicon_32.png') no-repeat 0 10px; - background-size: 16px; + background: transparent url('/media/embed/favicon_32.png') no-repeat 0 10px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-ios a:hover { - background: transparent url('/media/embed/reader/apple_icon.png') no-repeat 0 9px; - background-size: 16px; + background: transparent url('/media/embed/reader/apple_icon.png') no-repeat 0 9px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-android a:hover { - background: transparent url('/media/embed/reader/android_icon_round.png') no-repeat 0 10px; - background-size: 16px; + background: transparent url('/media/embed/reader/android_icon_round.png') no-repeat 0 10px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-github a:hover { - background: transparent url('/media/embed/reader/github.png') no-repeat 0 9px; - background-size: 16px; + background: transparent url('/media/embed/reader/github.png') no-repeat 0 9px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-discourse a:hover { - background: transparent url('/media/embed/reader/discourse.png') no-repeat 0 9px; - background-size: 16px; + background: transparent url('/media/embed/reader/discourse.png') no-repeat 0 9px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-blog a:hover { - background: transparent url('/media/embed/reader/ofbrooklyn_icon.png') no-repeat 0 9px; - background-size: 16px; + background: transparent url('/media/embed/reader/ofbrooklyn_icon.png') no-repeat 0 9px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-twitter a:hover { - background: transparent url('/media/embed/reader/twitter_bird.png') no-repeat 0 10px; - background-size: 16px; + background: transparent url('/media/embed/reader/twitter_bird.png') no-repeat 0 10px; + background-size: 16px; } + .NB-splash-info .NB-splash-links .NB-splash-link-facebook a:hover { - background: transparent url('/media/embed/reader/facebook.png') no-repeat 0 9px; - background-size: 16px; + background: transparent url('/media/embed/reader/facebook.png') no-repeat 0 9px; + background-size: 16px; } .NB-splash-info .NB-splash-links a { @@ -6177,17 +6952,21 @@ form.opml_import_form input { .NB-module-logo, .NB-module-login { height: 264px; } + .NB-module-logo { text-align: center; line-height: 32px; font-size: 20px; } + .NB-module-logo .NB-module-logo-image { margin: 12px 0 0; } + .NB-module-logo .NB-module-logo-tagline { margin: 36px 0 42px; } + .NB-module-logo .NB-module-logo-tagline b { padding: 2px 8px; background-color: #F0F0F0; @@ -6196,6 +6975,7 @@ form.opml_import_form input { color: #191F37; text-shadow: 0 1px 0 #FFF; } + .NB-module-logo .NB-module-logo-elsewhere { font-size: 16px; color: #797979; @@ -6208,12 +6988,14 @@ form.opml_import_form input { #simplemodal-container.NB-full-container.NB-classifier-container .simplemodal-wrap { overflow: hidden !important; } + .NB-modal.NB-modal-classifiers { position: static; overflow: hidden; max-height: 600px; padding: 18px; } + .NB-modal-classifiers form { height: 500px; padding: 12px 12px 12px 0; @@ -6221,14 +7003,15 @@ form.opml_import_form input { overflow-y: auto; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; - box-sizing: border-box; + box-sizing: border-box; } + .NB-classifier { border-radius: 14px; } .NB-classifier .NB-modal-loading { - margin: 8px 0px 0 0; + margin: 8px 0px 0 0; } .NB-modal-classifiers .NB-modal-title .NB-icon { @@ -6244,6 +7027,7 @@ form.opml_import_form input { .NB-modal-classifiers h2.NB-dislike { color: #700000; } + .NB-modal-classifiers .NB-classifier-facet-disabled { color: #A0A0A0; } @@ -6313,38 +7097,46 @@ form.opml_import_form input { .NB-modal-trainer .NB-modal-submit .NB-modal-submit-back { display: none; } + .NB-modal-trainer .NB-modal-submit .NB-modal-submit-back, .NB-modal-trainer .NB-modal-submit .NB-modal-submit-reset { float: left; color: #FFF; background-color: #b5b4bB; } + .NB-modal-trainer .NB-modal-submit .NB-modal-submit-grey { float: left; color: #FFF; background-color: #b5b4bB; } + .NB-modal-trainer .NB-modal-submit .NB-modal-submit-begin { float: right; } + .NB-modal-trainer .NB-modal-submit .NB-modal-submit-end { float: right; color: #FFF; background-color: #4679BB; } + .NB-modal-trainer .NB-modal-submit .NB-modal-submit-green { float: right; padding-left: 12px !important; padding-right: 12px !important; } + .NB-modal-trainer .NB-classifier-trainer-counts { float: right; color: #606060; font-size: 14px; } + .NB-modal-trainer .NB-trainer-points { margin-bottom: 64px; } + .NB-modal-trainer .NB-trainer-points li { line-height: 23px; margin: 0 0 18px 0; @@ -6352,11 +7144,13 @@ form.opml_import_form input { font-size: 13px; color: #707070; } + .NB-modal-trainer .NB-trainer-points li b { display: block; font-size: 16px; color: #202020; } + .NB-modal-trainer .NB-trainer-points li img { padding: 0 0 0 8px; } @@ -6369,6 +7163,7 @@ form.opml_import_form input { float: right; margin-left: 4px; } + .NB-modal-trainer .NB-trainer-points li img.NB-trainer-bullet { float: left; margin: 8px 8px 0 0; @@ -6379,13 +7174,14 @@ form.opml_import_form input { } .NB-modal-trainer .NB-trainer-not-authenticated { - font-size: 13px; - color: #801A14; - font-weight: bold; - padding: 4px 4px 4px 24px; - background: #FFE1DB url('/media/embed/icons/circular/exclamation.png') no-repeat 4px 4px; - background-size: 16px; + font-size: 13px; + color: #801A14; + font-weight: bold; + padding: 4px 4px 4px 24px; + background: #FFE1DB url('/media/embed/icons/circular/exclamation.png') no-repeat 4px 4px; + background-size: 16px; } + /* ======================= */ /* = Intelligence Slider = */ /* ======================= */ @@ -6437,10 +7233,12 @@ form.opml_import_form input { .NB-intelligence-slider { display: inline-block; } + .NB-intelligence-slider .segmented-control { float: left; width: auto; } + .NB-intelligence-slider .NB-intelligence-slider-control { line-height: 13px; height: 13px; @@ -6448,15 +7246,19 @@ form.opml_import_form input { padding: 4px 8px 4px; display: flex; } + .NB-narrow-pane-blue .NB-intelligence-slider .NB-intelligence-slider-blue .NB-intelligence-label { display: none; } + .NB-narrow-pane-green .NB-intelligence-slider .NB-intelligence-slider-green .NB-intelligence-label { display: none; } + .NB-narrow-pane-yellow .NB-intelligence-slider .NB-intelligence-slider-yellow .NB-intelligence-label { display: none; } + .NB-intelligence-slider img { width: 8px; height: 8px; @@ -6464,17 +7266,21 @@ form.opml_import_form input { float: left; vertical-align: bottom; } + .NB-intelligence-slider .NB-intelligence-slider-blue img { width: 12px; height: 12px; margin: 1px 5px -1px 0px; } + .NB-narrow-pane-green .NB-intelligence-slider .NB-intelligence-slider-green img { margin: 3px 6px 2px; } + .NB-narrow-pane-blue .NB-intelligence-slider .NB-intelligence-slider-blue img { margin: 1px 4px 0px; } + .NB-narrow-pane-yellow .NB-intelligence-slider .NB-intelligence-slider-yellow img { margin: 3px 6px 2px; } @@ -6510,6 +7316,7 @@ form.opml_import_form input { box-sizing: border-box; margin: 2px 0 6px; } + .NB-add-form .NB-folders { clear: both; float: left; @@ -6523,12 +7330,13 @@ form.opml_import_form input { margin: 11px 0 0 4px; width: 16px; height: 16px; - background: transparent url('/media/embed/icons/circular/g_icn_folder_add.png') no-repeat 0 0; + background: transparent url('/media/embed/icons/circular/g_icn_folder_add.png') no-repeat 0 0; background-size: 16px; cursor: pointer; } + .NB-add-form .NB-add-folder-icon:hover { - background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0; + background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0; background-size: 16px; } @@ -6538,6 +7346,7 @@ form.opml_import_form input { height: 16px; margin: 10px 8px 0; } + .NB-add .NB-add-form .NB-add-url-submit, .NB-add .NB-add-form .NB-add-folder-submit { float: right; @@ -6547,6 +7356,7 @@ form.opml_import_form input { .NB-add-form .NB-add-folder { overflow: hidden; } + .NB-add-form .NB-add-folder-input { width: 180px; -webkit-box-sizing: border-box; @@ -6556,6 +7366,7 @@ form.opml_import_form input { font-size: 12px; padding: 4px; } + .NB-add-form .NB-error { font-size: 11px; clear: both; @@ -6564,9 +7375,11 @@ form.opml_import_form input { font-weight: bold; display: none; } + .NB-add-form .NB-error-message { padding: 6px 0 0; } + .NB-add .NB-add-danger { display: block; clear: both; @@ -6579,7 +7392,7 @@ form.opml_import_form input { .NB-add .NB-add-danger img { vertical-align: bottom; - padding: 0 4px 0 0 ; + padding: 0 4px 0 0; } .NB-add input[type=text].ui-autocomplete-loading { @@ -6592,39 +7405,43 @@ form.opml_import_form input { } .ui-menu.ui-autocomplete.ui-widget-content { - width: 344px; - padding: 0; - margin: 0; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: none; - -webkit-border-top-right-radius: 5px; - -webkit-border-top-left-radius: 5px; - -moz-border-radius-topright: 5px; - -moz-border-radius-topleft: 5px; - border-top-right-radius: 5px; - border-top-left-radius: 5px; - -webkit-border-bottom-right-radius: 0; - -webkit-border-bottom-left-radius: 0; - -moz-border-radius-bottomright: 0; - -moz-border-radius-bottomleft: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; + width: 344px; + padding: 0; + margin: 0; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: none; + -webkit-border-top-right-radius: 5px; + -webkit-border-top-left-radius: 5px; + -moz-border-radius-topright: 5px; + -moz-border-radius-topleft: 5px; + border-top-right-radius: 5px; + border-top-left-radius: 5px; + -webkit-border-bottom-right-radius: 0; + -webkit-border-bottom-left-radius: 0; + -moz-border-radius-bottomright: 0; + -moz-border-radius-bottomleft: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } + .ui-menu.ui-autocomplete li { - padding: 0; - list-style: none; - margin: 0; - border-bottom: 1px solid #eaeaf7; + padding: 0; + list-style: none; + margin: 0; + border-bottom: 1px solid #eaeaf7; } + .ui-menu.ui-autocomplete li:last-child { - border-bottom: none; + border-bottom: none; } + .ui-menu.ui-autocomplete li a { - padding: 4px 8px; - display: block; - cursor: pointer; + padding: 4px 8px; + display: block; + cursor: pointer; } + .ui-menu.ui-autocomplete li a.ui-state-hover, .ui-menu.ui-autocomplete li a.ui-state-active, .ui-menu.ui-autocomplete li a.ui-state-focus { @@ -6637,47 +7454,49 @@ form.opml_import_form input { } .ui-menu.ui-autocomplete li a .NB-add-autocomplete-subscribers { - float: right; - font-size: 10px; - color: #909090; - font-weight: bold; - text-transform: uppercase; - line-height: 16px; - padding: 2px 4px 0 0; + float: right; + font-size: 10px; + color: #909090; + font-weight: bold; + text-transform: uppercase; + line-height: 16px; + padding: 2px 4px 0 0; } + .ui-menu.ui-autocomplete li a .NB-add-autocomplete-title { - line-height: 16px; - color: #202020; - font-weight: bold; - font-size: 13px; - height: 16px; - overflow: hidden; - padding: 2px 0 0 4px; + line-height: 16px; + color: #202020; + font-weight: bold; + font-size: 13px; + height: 16px; + overflow: hidden; + padding: 2px 0 0 4px; } + .ui-menu.ui-autocomplete li a .NB-add-autocomplete-favicon { float: left; margin: 2px 4px 0 2px; width: 16px; height: 16px; } + .ui-autocomplete li a .NB-add-autocomplete-address { - display: block; - clear: both; - color: #3F3D6E; - font-size: 10px; - font-weight: normal; - height: 12px; - line-height: 16px; - padding: 4px 0 4px 4px; - overflow: hidden; + display: block; + clear: both; + color: #3F3D6E; + font-size: 10px; + font-weight: normal; + height: 12px; + line-height: 16px; + padding: 4px 0 4px 4px; + overflow: hidden; } /* ================ */ /* = Manage Feeds = */ /* ================ */ -.NB-manage .NB-manage-field { -} +.NB-manage .NB-manage-field {} .NB-manage .NB-manage-container { height: 375px; @@ -6730,22 +7549,24 @@ form.opml_import_form input { .NB-manage .NB-manage-rename { margin: 0 0 12px 12px; - display: none; /* Sorry, but this is not v1.0. Maybe next tuesday. No, next, next tuesday. */ + display: none; + /* Sorry, but this is not v1.0. Maybe next tuesday. No, next, next tuesday. */ } .NB-manage .NB-manage-rename label { font-size: 14px; font-weight: bold; } + .NB-manage .NB-manage-rename input { font-size: 14px; padding: 2px; margin: 0 4px; border: 1px solid #606060; width: 400px; - -moz-box-shadow:2px 2px 0 #D0D0D0; - -webkit-box-shadow:2px 2px 0 #D0D0D0; - box-shadow:2px 2px 0 #D0D0D0; + -moz-box-shadow: 2px 2px 0 #D0D0D0; + -webkit-box-shadow: 2px 2px 0 #D0D0D0; + box-shadow: 2px 2px 0 #D0D0D0; } .NB-manage .NB-manage-delete { @@ -6783,6 +7604,7 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/circular/g_modal_markread.png'); background-size: 28px; } + .NB-modal-markread .NB-markread-slider { margin: 24px 12px; } @@ -6803,7 +7625,7 @@ form.opml_import_form input { .NB-classifiers .NB-classifier ::-moz-selection { background: transparent; -} +} .NB-classifiers .NB-classifier ::selection { background: transparent; @@ -6837,13 +7659,13 @@ form.opml_import_form input { } .NB-classifiers .NB-classifier label b { - color: rgba(0,0,0,.4); + color: rgba(0, 0, 0, .4); text-shadow: none; font-weight: normal; } .NB-classifiers .NB-classifier label span { - text-shadow: 0 1px 0 rgba(255,255,255,0.5); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); } .NB-classifiers .NB-classifier.NB-classifier-facet-disabled { @@ -6851,133 +7673,154 @@ form.opml_import_form input { } .NB-classifiers .NB-classifier input { - display: none; + display: none; } .NB-classifiers .NB-classifier .feed_favicon { - margin-top: -2px; - width: 16px; - height: 16px; + margin-top: -2px; + width: 16px; + height: 16px; } .NB-classifiers .NB-classifier .NB-classifier-icon-like { - width: 16px; - height: 16px; - background: transparent url('/media/embed/icons/nouns/thumbs-up.svg') no-repeat 0 0; - background-size: 14px; - position: absolute; - left: 6px; - top: 2px; - opacity: .2; + width: 16px; + height: 16px; + background: transparent url('/media/embed/icons/nouns/thumbs-up.svg') no-repeat 0 0; + background-size: 14px; + position: absolute; + left: 6px; + top: 2px; + opacity: .2; } .NB-classifiers .NB-classifier .NB-classifier-icon-dislike { - width: 27px; - height: 22px; - position: absolute; - top: -1px; - right: -1px; - background: transparent url('/media/embed/icons/nouns/thumbs-down.svg') no-repeat 4px 3px; - background-size: 14px; - opacity: .2; + width: 27px; + height: 22px; + position: absolute; + top: -1px; + right: -1px; + background: transparent url('/media/embed/icons/nouns/thumbs-down.svg') no-repeat 4px 3px; + background-size: 14px; + opacity: .2; } .NB-classifiers .NB-classifier .NB-classifier-icon-dislike-inner { - margin: 4px 4px 0 0; - width: 18px; - height: 13px; - border-left: 1px solid #EBEBE1; + margin: 4px 4px 0 0; + width: 18px; + height: 13px; + border-left: 1px solid #EBEBE1; } .NB-classifiers .NB-classifier.NB-classifier-like { - background-color: #34912E; - border: 1px solid #202020; - -webkit-box-shadow: 1px 1px 1px #BDC0D7; - -moz-box-shadow: 1px 1px 1px #BDC0D7; - box-shadow: 1px 1px 1px #BDC0D7; + background-color: #34912E; + border: 1px solid #202020; + -webkit-box-shadow: 1px 1px 1px #BDC0D7; + -moz-box-shadow: 1px 1px 1px #BDC0D7; + box-shadow: 1px 1px 1px #BDC0D7; } + .NB-classifiers .NB-classifier.NB-classifier-dislike { - background-color: #A90103; - border: 1px solid #000; - -webkit-box-shadow: 1px 1px 1px #BDC0D7; - -moz-box-shadow: 1px 1px 1px #BDC0D7; - box-shadow: 1px 1px 1px #BDC0D7; + background-color: #A90103; + border: 1px solid #000; + -webkit-box-shadow: 1px 1px 1px #BDC0D7; + -moz-box-shadow: 1px 1px 1px #BDC0D7; + box-shadow: 1px 1px 1px #BDC0D7; } .NB-classifiers .NB-classifier.NB-classifier-hover-like { - background-color: #54A54E; + background-color: #54A54E; } + .NB-classifiers .NB-classifier.NB-classifier-like.NB-classifier-hover-like { - background-color: #34912E; + background-color: #34912E; } + .NB-classifiers .NB-classifier.NB-classifier-dislike.NB-classifier-hover-like { - border: 1px solid transparent; + border: 1px solid transparent; } + .NB-classifiers .NB-classifier.NB-classifier-like label b, .NB-classifiers .NB-classifier.NB-classifier-hover-like label b { - color: white; + color: white; } + .NB-classifiers .NB-classifier.NB-classifier-like label span, .NB-classifiers .NB-classifier.NB-classifier-hover-like label span { - color: white; - text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); + color: white; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); } + .NB-classifiers .NB-classifier.NB-classifier-like .NB-classifier-icon-dislike, .NB-classifiers .NB-classifier.NB-classifier-hover-like .NB-classifier-icon-dislike { - opacity: .1; + opacity: .1; } + .NB-classifiers .NB-classifier.NB-classifier-dislike .NB-classifier-icon-dislike, .NB-classifiers .NB-classifier.NB-classifier-hover-dislike .NB-classifier-icon-dislike { - opacity: 1; + opacity: 1; } + .NB-classifiers .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like, .NB-classifiers .NB-classifier.NB-classifier-hover-dislike .NB-classifier-icon-like { - opacity: .1; + opacity: .1; } + .NB-classifiers .NB-classifier.NB-classifier-dislike.NB-classifier-hover-like .NB-classifier-icon-like { - opacity: 1; + opacity: 1; } + .NB-classifiers .NB-classifier.NB-classifier-dislike.NB-classifier-hover-like .NB-classifier-icon-dislike { - opacity: .1; + opacity: .1; } + .NB-classifiers .NB-classifier.NB-classifier-hover-like.NB-classifier-hover-dislike .NB-classifier-icon-dislike { - opacity: 1; + opacity: 1; } + .NB-classifiers .NB-classifier.NB-classifier-hover-like.NB-classifier-hover-dislike .NB-classifier-icon-like { - opacity: .1; + opacity: .1; } + .NB-classifiers .NB-classifier.NB-classifier-like .NB-classifier-icon-like, .NB-classifiers .NB-classifier.NB-classifier-hover-like .NB-classifier-icon-like, .NB-classifiers .NB-classifier.NB-classifier-dislike .NB-classifier-icon-dislike, .NB-classifiers .NB-classifier.NB-classifier-hover-dislike .NB-classifier-icon-dislike { - opacity: 1; + opacity: 1; } + .NB-classifiers .NB-classifier.NB-classifier-hover-dislike { - background-color: #C92123; + background-color: #C92123; } + .NB-classifiers .NB-classifier.NB-classifier-like.NB-classifier-hover-dislike { - border: 1px solid transparent; + border: 1px solid transparent; } + .NB-classifiers .NB-classifier.NB-classifier-like.NB-classifier-hover-like.NB-classifier-hover-dislike { - background-color: #C92123; + background-color: #C92123; } + .NB-classifiers .NB-classifier.NB-classifier-dislike.NB-classifier-hover-like.NB-classifier-hover-dislike { - border: 1px solid #000; - background-color: #A90103; + border: 1px solid #000; + background-color: #A90103; } + .NB-classifiers .NB-classifier.NB-classifier-dislike .NB-classifier-icon-dislike-inner, .NB-classifiers .NB-classifier.NB-classifier-hover-dislike .NB-classifier-icon-dislike-inner { - border-left-color: #C17C52; + border-left-color: #C17C52; } + .NB-classifiers .NB-classifier.NB-classifier-dislike label b, .NB-classifiers .NB-classifier.NB-classifier-hover-dislike label b { - color: white; + color: white; } + .NB-classifiers .NB-classifier.NB-classifier-dislike label span, .NB-classifiers .NB-classifier.NB-classifier-hover-dislike label span { - color: white; - text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); + color: white; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); } + /* =================== */ /* = Mouse Indicator = */ /* =================== */ @@ -7015,39 +7858,47 @@ form.opml_import_form input { .NB-module .NB-module-next-page { margin-right: -12px; padding-right: 8px; - + background: transparent url('/media/embed/reader/next_page.png') no-repeat 4px center; background-size: 14px; } + .NB-module .NB-module-next-page.NB-javascript { - opacity: .2; + opacity: .2; } + .NB-module .NB-module-next-page:link { background: transparent url('/media/embed/reader/next_page.png') no-repeat 4px center; background-size: 14px; } + .NB-module .NB-module-next-page:hover { background: transparent url('/media/embed/reader/next_page_active.png') no-repeat 4px center; background-size: 14px; } + .NB-module .NB-module-next-page.NB-disabled:hover { background: transparent url('/media/embed/reader/next_page.png') no-repeat 4px center; background-size: 14px; cursor: default; } + .NB-module .NB-module-previous-page { padding: 0 4px 0 4px; background: transparent url('/media/embed/reader/previous_page.png') no-repeat 8px center; background-size: 14px; } + .NB-module .NB-module-previous-page:link { background: transparent url('/media/embed/reader/previous_page.png') no-repeat 8px center; background-size: 14px; } + .NB-module .NB-module-previous-page:hover { background: transparent url('/media/embed/reader/previous_page_active.png') no-repeat 8px center; background-size: 14px; } + .NB-module .NB-module-previous-page.NB-disabled:hover { background: transparent url('/media/embed/reader/previous_page.png') no-repeat 8px center; background-size: 14px; @@ -7064,9 +7915,11 @@ form.opml_import_form input { cursor: pointer; background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat center center; } + .NB-module .NB-module-page-indicator:hover { opacity: 1; } + .NB-module .NB-module-page-indicator.NB-active { opacity: 1; background: transparent url('/media/embed/icons/silk/bullet_orange.png') no-repeat center center; @@ -7078,11 +7931,11 @@ form.opml_import_form input { .NB-module a { text-decoration: none; -/* color: #3E4773;*/ + /* color: #3E4773;*/ } .NB-module a:hover { -/* color: #0E1763;*/ + /* color: #0E1763;*/ } .NB-module h5 { @@ -7094,11 +7947,13 @@ form.opml_import_form input { .NB-module .NB-module-header-left { float: left; } + .NB-module .NB-module-header-center { text-align: center; margin: 0 auto; width: 132px; } + .NB-module .NB-module-header-right { font-size: 13px; line-height: 16px; @@ -7107,9 +7962,11 @@ form.opml_import_form input { order: 2; flex: 0 1 0; } + .NB-module .NB-module-header-text { flex: 2 1 0; } + .NB-module .NB-spinner { background: transparent url('/media/embed/reader/ring_spinner.svg') no-repeat center center; background-size: 16px; @@ -7119,13 +7976,16 @@ form.opml_import_form input { opacity: .6; display: none; } + .NB-module.NB-loading .NB-spinner, .NB-module-item.NB-loading .NB-spinner { display: block; } + .NB-module .NB-module-content-header { padding-left: 4px; } + .NB-module-content-header { padding: 0 0 0px; margin: 0 0 4px 0; @@ -7155,12 +8015,15 @@ form.opml_import_form input { height: 16px; margin: 0 4px 0 0; } + .NB-module .NB-module-content-account-realtime.NB-active { background: transparent url('/media/img/reader/realtime_spinner.gif') no-repeat 0 1px; } + .NB-module .NB-module-content-account-realtime.NB-error { background: transparent url('/media/img/reader/realtime_spinner_error.gif') no-repeat 0 1px; } + .NB-module .NB-module-item { position: relative; min-height: 77px; @@ -7172,6 +8035,7 @@ form.opml_import_form input { .NB-module .NB-module-item.NB-last { margin-bottom: 0; } + .NB-module .NB-module-item-image { position: absolute; left: 0; @@ -7192,6 +8056,7 @@ form.opml_import_form input { .NB-module .NB-modal-submit-grey { color: #505050; } + .NB-module .NB-module-item .NB-module-item-title { margin: 0 0 4px 128px; } @@ -7200,9 +8065,10 @@ form.opml_import_form input { float: left; margin: 4px 0 0 0; } + .NB-module .NB-module-item .NB-menu-manage-logout { - float: right; - margin: 4px 4px 0 0; + float: right; + margin: 4px 4px 0 0; } /* =========================== */ @@ -7229,17 +8095,19 @@ form.opml_import_form input { float: right; cursor: pointer; } + .NB-module .NB-module-account-settings.NB-javascript { - opacity: .2; - cursor: default; + opacity: .2; + cursor: default; } .NB-account .NB-module .NB-module-item .NB-module-account-premium { float: right; margin-right: 12px; } + .NB-account .NB-module .NB-module-item .NB-modal-submit-button.NB-javascript { - opacity: .2; + opacity: .2; } .NB-module.NB-module-features .NB-module-content-header { @@ -7251,11 +8119,13 @@ form.opml_import_form input { position: relative; float: left; } + .NB-module-account .NB-module-content-header, .NB-module-account .NB-module-item .NB-module-item-title { clear: none; overflow: hidden; } + .NB-module-account .NB-module-item .NB-module-item-image img { width: 100px; margin-right: 14px; @@ -7337,27 +8207,33 @@ form.opml_import_form input { overflow: hidden; border-radius: 4px; } + .NB-module-gettingstarted .NB-intro-avatar { margin: 12px auto 42px; } + .NB-module-gettingstarted .NB-intro-avatar-bezel { padding: 3px; width: 48px; line-height: 0; margin: 0 auto 24px; } + .NB-module-gettingstarted .NB-intro-avatar img { width: 48px; height: 48px; } + .NB-module-gettingstarted .NB-intro-progress { position: relative; margin: 24px 64px; } + .NB-module-gettingstarted .NB-intro-progress .progress { height: 10px; margin-top: -30px; } + .NB-module-gettingstarted .NB-intro-progress-goal { top: -10px; left: 0px; @@ -7372,30 +8248,36 @@ form.opml_import_form input { height: 28px; z-index: 1; } + .NB-module-gettingstarted .NB-intro-progress-goal.NB-1 { left: 0; background: #fff url(/media/embed/icons/silk/flag_blue.png) no-repeat center center; } + .NB-module-gettingstarted .NB-intro-progress-goal.NB-2 { margin: 0 auto; left: auto; position: relative; background: #fff url(/media/embed/icons/silk/flag_green.png) no-repeat center center; } + .NB-module-gettingstarted .NB-intro-progress-goal.NB-3 { left: auto; right: 0; background: #fff url(/media/embed/icons/silk/flag_yellow.png) no-repeat center center; } + .NB-module-gettingstarted .NB-intro-progress-goal.NB-done { background: transparent url('/media/embed/icons/nouns/accept.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(20deg) saturate(18); } + .NB-module-gettingstarted .NB-intro-goals { margin: 24px 0 0; overflow: hidden; } + .NB-module-gettingstarted .NB-intro-goal { width: 33.33%; float: left; @@ -7407,6 +8289,7 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-module-gettingstarted .NB-icon { display: inline-block; margin: 0 2px 0 0; @@ -7414,31 +8297,38 @@ form.opml_import_form input { height: 16px; vertical-align: text-bottom; } + .NB-module-gettingstarted .NB-modal-submit-button { padding: 4px 6px; } + .NB-module-gettingstarted .NB-module-launch-intro .NB-icon { background: transparent url(/media/embed/icons/silk/lorry.png) no-repeat center center; background-size: 16px; } + .NB-module-gettingstarted .NB-module-friends-button .NB-icon { background: transparent url('/media/embed/icons/nouns/share.svg') no-repeat 0 0; background-size: 16px; } + .NB-module-gettingstarted .NB-module-account-train .NB-icon { background: transparent url("/media/embed/icons/nouns/dialog-trainer.svg") no-repeat center center; background-size: 16px; filter: hue-rotate(140deg) saturate(15); } + .NB-module-gettingstarted .NB-intro-goal .NB-not-done { margin: 6px auto; float: none; } + .NB-module-gettingstarted .NB-intro-goal .NB-done { color: #7CB621; font-weight: bold; padding: 12px 0; } + .NB-module-gettingstarted .NB-done .NB-not-done, .NB-module-gettingstarted .NB-not-done .NB-done { display: none; @@ -7448,8 +8338,7 @@ form.opml_import_form input { /* = Premium Upgrade Module = */ /* ========================== */ -.NB-account .NB-module-premium { -} +.NB-account .NB-module-premium {} .NB-module-premium .NB-module-item-intro { border-radius: 3px; @@ -7457,63 +8346,76 @@ form.opml_import_form input { margin-bottom: 24px; padding: 12px; overflow: hidden; - font-size: 14px; - text-align: center; + font-size: 14px; + text-align: center; } + .NB-module-premium .NB-module-item .NB-modal-submit-button { - float: none; + float: none; } + .NB-module-premium .NB-module-premium-price { - font-size: 12px; - text-transform: uppercase; - margin: 12px 0 0; - color: #ebc17c; + font-size: 12px; + text-transform: uppercase; + margin: 12px 0 0; + color: #ebc17c; } -.NB-module.NB-module-premium .NB-module-premium-button { + +.NB-module.NB-module-premium .NB-module-premium-button { padding-top: 12px; padding-bottom: 12px; margin-left: 20%; margin-right: 20%; margin-top: 36px; } + .NB-module-premium .NB-module-premium-icon { width: 36px; height: 36px; display: block; margin: 0 auto 12px; } + .NB-module-premium-icon.NB-1 { background: transparent url('/media/embed/icons/icons8/icons8-sheets-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-2 { background: transparent url('/media/embed/icons/icons8/icons8-lightning-bolt-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-3 { background: transparent url('/media/embed/icons/icons8/icons8-comics-magazine-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-4 { background: transparent url('/media/embed/icons/icons8/icons8-search-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-5 { background: transparent url('/media/embed/icons/icons8/icons8-tags-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-6 { background: transparent url('/media/embed/icons/icons8/icons8-security-wi-fi-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-7 { background: transparent url('/media/embed/icons/icons8/icons8-rss-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-8 { background: transparent url('/media/embed/icons/icons8/icons8-activity-history-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-icon.NB-9 { background: transparent url('/media/embed/icons/icons8/icons8-knife-and-spatchula-100.png') no-repeat 0 0; background-size: 36px; @@ -7523,29 +8425,36 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/icons8/icons8-bursts-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-archive-icon.NB-2 { background: transparent url('/media/embed/icons/icons8/icons8-calendar-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-archive-icon.NB-3 { background: transparent url('/media/embed/icons/icons8/icons8-filing-cabinet-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-archive-icon.NB-4 { background: transparent url('/media/embed/icons/icons8/icons8-quadcopter-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-archive-icon.NB-5 { background: transparent url('/media/embed/icons/icons8/icons8-rss-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-archive-icon.NB-6 { background: transparent url('/media/embed/icons/icons8/icons8-relax-with-book-100.png') no-repeat 0 0; background-size: 36px; } + .NB-module-premium-reason { padding: 0 24px; } + .NB-module-premium-reason .NB-feedchooser-premium-poor-hungry-dog { display: block; margin: 12px auto 4px; @@ -7553,6 +8462,7 @@ form.opml_import_form input { width: 128px; height: 96px; } + /* ================= */ /* = Manage Module = */ /* ================= */ @@ -7566,8 +8476,9 @@ form.opml_import_form input { /* ================ */ .NB-splash-module-section { - overflow: hidden; + overflow: hidden; } + .NB-module-stats-counts { border-radius: 4px; background-color: #F7F8F5; @@ -7584,29 +8495,34 @@ form.opml_import_form input { overflow: hidden; padding: 6px 0; } + .NB-module-stats-count:last-child { border-bottom: none; } + .NB-module-stats-count-shared-stories { width: 38%; } + .NB-module-stats-count-following, .NB-module-stats-count-followers { width: 31%; } .NB-module-stats-count-number { - clear: both; - font-size: 18px; - color: #0D003C; - text-shadow: 1px 1px 0 #E8E8E8; - font-weight: bold; + clear: both; + font-size: 18px; + color: #0D003C; + text-shadow: 1px 1px 0 #E8E8E8; + font-weight: bold; } + .NB-module-stats-count-description { - margin: 4px 0 0 0; - color: #A0A0A0; - clear: both; + margin: 4px 0 0 0; + color: #A0A0A0; + clear: both; } + .NB-module-account-subscription .NB-module-stats-count-description { color: #0D003C; margin-bottom: 4px; @@ -7614,45 +8530,47 @@ form.opml_import_form input { } .NB-module-stats-count-graph { - clear: both; - margin: 0 auto; - width: 100px; + clear: both; + margin: 0 auto; + width: 100px; } + .NB-graph-value { - float: left; - height: 30px; - width: 3px; - padding: 0 1px 0 0; - margin: 0 0 14px 0; - position: relative; + float: left; + height: 30px; + width: 3px; + padding: 0 1px 0 0; + margin: 0 0 14px 0; + position: relative; } .NB-graph-value .NB-graph-bar { - background-color: darkblue; - position: absolute; - bottom: 0; - left: 0; - width: 3px; + background-color: darkblue; + position: absolute; + bottom: 0; + left: 0; + width: 3px; } .NB-graph-value .NB-graph-label { - width: 20px; - height: 20px; - font-size: 10px; - text-align: center; - position: absolute; - bottom: -25px; - left: -8px; - display: none; - padding: 3px 0 0 0; - color: #A0A0A0; - text-shadow: 1px 1px 0 #F0F0F0; - background: transparent url('/media/embed/reader/graph_arrow_up.png') no-repeat 8px 0; + width: 20px; + height: 20px; + font-size: 10px; + text-align: center; + position: absolute; + bottom: -25px; + left: -8px; + display: none; + padding: 3px 0 0 0; + color: #A0A0A0; + text-shadow: 1px 1px 0 #F0F0F0; + background: transparent url('/media/embed/reader/graph_arrow_up.png') no-repeat 8px 0; } .NB-graph-value:hover .NB-graph-label { - display: block; + display: block; } + /* ================== */ /* = Center Modules = */ /* ================== */ @@ -7662,12 +8580,14 @@ form.opml_import_form input { margin: 24px 0 0 0px; flex: 1; transition: margin 0.36s ease-out, - padding 0.36s ease-out, - gap 0.36s ease-out; + padding 0.36s ease-out, + gap 0.36s ease-out; } + .NB-modules-center { flex: 4 1 0; } + .NB-account-wide { flex: 3 1 0; } @@ -7675,27 +8595,33 @@ form.opml_import_form input { .NB-module-header { transition: margin 0.36s ease-out; } + .NB-dashboard-rivers-left .NB-module, .NB-dashboard-rivers-right .NB-module { margin: 0 0 34px; - clear: both; + clear: both; } + .NB-module-river .NB-story-title-container:first-child .NB-story-title { border-top: none; } + .NB-module-river .NB-story-title-container:last-child .NB-story-title { border-bottom: none; } + .NB-module-river .NB-module-river-favicon { display: inline-block; cursor: pointer; } + .NB-module-river .NB-module-river-favicon img { width: 16px; height: 16px; transform: translate(0px, 2px); margin-right: 8px; } + .NB-module-river .NB-module-river-title { display: inline-block; cursor: pointer; @@ -7704,32 +8630,39 @@ form.opml_import_form input { justify-content: center; flex: 1 1 0; } + .NB-module-river .NB-dashboard-column-control { float: left; line-height: 0; display: none; margin-right: 6px; } + .NB-module-river .NB-dashboard-column-control.NB-active { display: inline-block; } + .NB-module-river .NB-dashboard-column-control li { width: 32px; padding: 5px 0; } + .NB-module-river .NB-dashboard-column-control li img { opacity: 0.3; width: 16px; height: 16px; } + .NB-module-river .NB-dashboard-column-control li.NB-active img { opacity: 0.5; } + .NB-module-river .NB-module-river-settings { order: 3; } + .NB-module-river .NB-view-river { - box-shadow: 0 1px 2px rgba(0,0,0,0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); border-radius: 3px; margin: 0 2px; } @@ -7737,6 +8670,7 @@ form.opml_import_form input { .NB-module-river .NB-feedbar-options-container { float: right; } + .NB-module-river .NB-feedbar-options { cursor: pointer; float: right; @@ -7750,10 +8684,12 @@ form.opml_import_form input { line-height: 14px; margin: 1px 0 0; } + .NB-module-river .NB-feedbar-options:hover, .NB-module-river .NB-feedbar-options.NB-active { background-color: rgba(0, 0, 0, .1); } + .NB-module-river .NB-feedbar-options .NB-icon { float: right; width: 16px; @@ -7777,11 +8713,13 @@ form.opml_import_form input { .NB-module-features .NB-feedback-table { margin-bottom: 34px; } + .NB-module-features .NB-features-add { float: left; display: none; margin-right: 12px; } + .NB-module-features .NB-module-header:hover .NB-features-add { display: block; } @@ -7797,6 +8735,7 @@ form.opml_import_form input { .NB-module-features .NB-module-feature.NB-module-feature-new td { background-color: #FCFFB4; } + .NB-module-features .NB-module-feature .NB-module-feature-date { float: left; padding: 4px 8px; @@ -7828,6 +8767,7 @@ form.opml_import_form input { .NB-module-features .NB-module-feature .NB-module-feature-description a { color: #405BA8; } + .NB-module-features .NB-module-feature .NB-module-feature-description a:hover { color: #A85B40; } @@ -7856,19 +8796,24 @@ form.opml_import_form input { display: inline-block; vertical-align: text-bottom; } + .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-problem { background-color: #EC8C35; } + .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-praise { background-color: #FAD477; color: white; } + .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-idea { background-color: #9DDC5F; } + .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-question { background-color: #6DAEDC; } + .NB-module-features .NB-module-feature .NB-module-feature-tag.NB-tag-updates { background-color: #12A89D; } @@ -7882,10 +8827,11 @@ form.opml_import_form input { /* ============================ */ .NB-module-recommended { - overflow: hidden; - margin-bottom: 42px; + overflow: hidden; + margin-bottom: 42px; } - .NB-module-recommended .NB-module-recommended-date { + +.NB-module-recommended .NB-module-recommended-date { line-height: 14px; position: absolute; top: 18px; @@ -7894,25 +8840,28 @@ form.opml_import_form input { font-size: 11px; margin: 1px 2px 0 0; text-shadow: 0 1px 0 #E9E9E9; - } - .NB-module-recommended .NB-module-recommended-date span { +} + +.NB-module-recommended .NB-module-recommended-date span { vertical-align: text-top; margin: 0 0 0 -3px; line-height: 12px; font-size: 8px; - } - .NB-module-recommended .NB-recommended { +} + +.NB-module-recommended .NB-recommended { margin: 12px 0 0 0; padding: 0 12px; - } - - .NB-module-recommended .NB-javascript.NB-module-direction, - .NB-module-recommended .NB-javascript.NB-modal-submit-button, - .NB-module-recommended .NB-javascript.NB-recommended-statistics, - .NB-module-recommended .NB-javascript.NB-recommended-intelligence { +} + +.NB-module-recommended .NB-javascript.NB-module-direction, +.NB-module-recommended .NB-javascript.NB-modal-submit-button, +.NB-module-recommended .NB-javascript.NB-recommended-statistics, +.NB-module-recommended .NB-javascript.NB-recommended-intelligence { opacity: .2; - } - .NB-module-recommended .NB-recommended-statistics { +} + +.NB-module-recommended .NB-recommended-statistics { margin: 2px 0 0 0; position: absolute; left: -26px; @@ -7923,18 +8872,21 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/nouns/dialog-statistics.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(284deg) saturate(18); - } - .NB-module-recommended .NB-recommended-favicon { +} + +.NB-module-recommended .NB-recommended-favicon { width: 16px; height: 16px; float: left; margin: 2px 4px 0 0px; - } - .NB-module-recommended .NB-recommended-title { +} + +.NB-module-recommended .NB-recommended-title { padding: 0 0 4px 20px; border-bottom: 1px solid #E0E0E0; - } - .NB-module-recommended .NB-recommended-description { +} + +.NB-module-recommended .NB-recommended-description { font-size: 13px; line-height: 20px; margin: 8px 0 0 0; @@ -7942,24 +8894,28 @@ form.opml_import_form input { padding-bottom: 8px; border-bottom: 1px solid #E0E0E0; overflow: hidden; - } - .NB-module-recommended .NB-modal-submit { +} + +.NB-module-recommended .NB-modal-submit { margin: 8px 0 0 0; - } - .NB-module-recommended .NB-recommended-subscribers { - position: relative; +} + +.NB-module-recommended .NB-recommended-subscribers { + position: relative; float: right; text-transform: uppercase; font-size: 11px; color: #808080; margin: 0 0 0 0; - } - .NB-module-recommended .NB-modal-submit-button { +} + +.NB-module-recommended .NB-modal-submit-button { float: left; margin-left: 0 !important; margin-right: 8px !important; - } - .NB-module-recommended .NB-recommended-added { +} + +.NB-module-recommended .NB-recommended-added { padding: 6px 0 0 20px; background: transparent url('/media/embed/icons/circular/newuser_icn_setup.png') no-repeat 0 2px; background-size: 18px; @@ -7967,18 +8923,21 @@ form.opml_import_form input { font-weight: bold; color: #123B00; overflow: hidden; - } - .NB-recommended-show-moderation-wrapper { - display: none; - float: left; - margin-right: 12px; - } - .NB-module-recommended .NB-module-header:hover .NB-recommended-show-moderation-wrapper { - display: block; - } - .NB-recommended-unmoderated { - display: none; - } +} + +.NB-recommended-show-moderation-wrapper { + display: none; + float: left; + margin-right: 12px; +} + +.NB-module-recommended .NB-module-header:hover .NB-recommended-show-moderation-wrapper { + display: block; +} + +.NB-recommended-unmoderated { + display: none; +} /* ========= */ /* = Menus = */ @@ -8001,6 +8960,7 @@ form.opml_import_form input { -moz-box-shadow: 2px 2px 5px #5E6267; box-shadow: 2px 2px 5px #5E6267; } + .NB-menu-manage-container.NB-inverse { background-color: #F1F3EC; } @@ -8014,14 +8974,14 @@ form.opml_import_form input { } .NB-inverse .NB-menu-manage { - margin: 0 0 4px; + margin: 0 0 4px; } .NB-menu-manage-container .NB-menu-manage-arrow { width: 19px; height: 20px; background-image: -webkit-gradient(linear, left top, left bottom, from(#F1F3EC), to(#DADCD6)); - background-image: -moz-linear-gradient(center top , #F1F3EC 0%, #DADCD6 100%); + background-image: -moz-linear-gradient(center top, #F1F3EC 0%, #DADCD6 100%); background-image: linear-gradient(top, #F1F3EC, #DADCD6); border-top: 1px solid #90928B; border-left: 1px solid #90928B; @@ -8031,6 +8991,7 @@ form.opml_import_form input { top: -21px; left: -1px; } + .NB-menu-manage-container.NB-inverse .NB-menu-manage-arrow { border-top: none; border-bottom: 1px solid #90928B; @@ -8063,6 +9024,7 @@ form.opml_import_form input { .NB-menu-manage li.NB-menu-item.NB-hover:not(.NB-disabled):not(.NB-active) { background-color: #ECEEEA; } + .NB-menu-manage li.NB-menu-item:active:not(.NB-disabled):not(.NB-active) { background-color: #ACB2A6; } @@ -8109,10 +9071,10 @@ form.opml_import_form input { } .NB-menu-manage .NB-menu-manage-title { - font-size: 14px; - text-shadow: 0 1px 0 rgba(255, 255, 255, .6); - padding: 5px 0 5px 0; - margin-left: 36px; + font-size: 14px; + text-shadow: 0 1px 0 rgba(255, 255, 255, .6); + padding: 5px 0 5px 0; + margin-left: 36px; } .NB-menu-manage li.NB-menu-item:hover:not(.NB-disabled) .NB-menu-manage-title { @@ -8128,33 +9090,39 @@ form.opml_import_form input { opacity: .4; cursor: default; } + .NB-menu-manage .NB-menu-manage-story-share-save.NB-disabled { opacity: 1; } + .NB-menu-manage .NB-menu-manage-subtitle { - font-size: 11px; - color: #718C7B; + font-size: 11px; + color: #718C7B; } .NB-menu-manage li.NB-menu-item:hover .NB-menu-manage-subtitle { color: rgba(0, 0, 0, .4); } + .NB-menu-manage li.NB-menu-item:active .NB-menu-manage-subtitle { color: rgba(255, 255, 255, 1); text-shadow: 0 1px 0 rgba(0, 0, 0, .4); } + .NB-menu-manage .NB-menu-manage-subtitle { - margin: -2px 0 0 36px; - padding: 0 0 5px 0; + margin: -2px 0 0 36px; + padding: 0 0 5px 0; } + .NB-menu-manage .NB-menu-manage-image { - padding: 0; - position: absolute; - width: 18px; - height: 18px; - top: 5px; - left: 10px; + padding: 0; + position: absolute; + width: 18px; + height: 18px; + top: 5px; + left: 10px; } + .NB-menu-manage .NB-menu-manage-theme .NB-menu-manage-image { top: 5px; } @@ -8169,10 +9137,12 @@ form.opml_import_form input { text-shadow: 0 1px 0 #FFF; background-color: #F1F3EC; } + .NB-menu-manage .NB-menu-manage-feed-info:hover, .NB-menu-manage .NB-menu-manage-site-info:hover { background-color: #F1F3EC; } + .NB-menu-manage .NB-menu-manage-site-info { padding-top: 6px; } @@ -8183,83 +9153,97 @@ form.opml_import_form input { top: 10px; } -.NB-menu-manage .NB-menu-manage-feed-info .feed_title { -} +.NB-menu-manage .NB-menu-manage-feed-info .feed_title {} .NB-menu-manage .NB-menu-manage-feed-train .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-trainer.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(140deg) saturate(15); } + .NB-menu-manage .NB-menu-manage-feed-notifications .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-notifications.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(320deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-feed-recommend .NB-menu-manage-image { - background: transparent url('/media/embed/icons/circular/g_icn_award.png') no-repeat 0 1px; + background: transparent url('/media/embed/icons/circular/g_icn_award.png') no-repeat 0 1px; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-story-train .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-trainer.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(140deg) saturate(15); } + .NB-menu-manage .NB-menu-manage-story-mark-read-newer.NB-up .NB-menu-manage-image, .NB-menu-manage .NB-menu-manage-story-mark-read-older.NB-up .NB-menu-manage-image { background: transparent url('/media/embed/icons/circular/menu_icn_markread_up.png') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-story-mark-read-newer.NB-down .NB-menu-manage-image, .NB-menu-manage .NB-menu-manage-story-mark-read-older.NB-down .NB-menu-manage-image { background: transparent url('/media/embed/icons/circular/menu_icn_markread_down.png') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-feed-settings .NB-menu-manage-image, .NB-menu-manage .NB-menu-manage-folder-settings .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/settings.svg') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-feed-reload .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/boomerang.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(320deg) saturate(16.5); } + .NB-menu-manage .NB-menu-manage-feed-stats .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/dialog-statistics.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(284deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-mark-read .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/mark-read.svg") no-repeat center center; - background-size: 18px; + background-size: 18px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-folder-subscribe .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/add.svg') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-folder-subfolder .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/folder-open.svg') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-social-profile .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/subscribers.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(87deg) saturate(10.5); } + .NB-menu-manage .NB-menu-manage-keyboard .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-keyboard.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(139deg) saturate(10); } + .NB-menu-manage .NB-menu-manage-tutorial .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-tips.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(321deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-feed-exception .NB-menu-manage-image { - background: transparent url('/media/embed/icons/circular/newuser_icn_sharewith_active.png') no-repeat 0 1px; + background: transparent url('/media/embed/icons/circular/newuser_icn_sharewith_active.png') no-repeat 0 1px; background-size: 18px; } @@ -8268,82 +9252,99 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/nouns/delete.svg') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-delete.NB-menu-manage-feed-delete-cancel .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/cancel.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-delete-confirm .NB-menu-manage-image { - background: transparent url('/media/embed/icons/circular/exclamation.png') no-repeat 0 1px; - background-size: 16px; - font-weight: bold; + background: transparent url('/media/embed/icons/circular/exclamation.png') no-repeat 0 1px; + background-size: 16px; + font-weight: bold; } + .NB-menu-manage .NB-menu-manage-move .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/folder-closed.svg') no-repeat 0 0; - background-size: 16px; + background-size: 16px; } + .NB-menu-manage .NB-menu-manage-move.NB-active .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/accept.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-move.NB-menu-manage-feed-move-cancel .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/cancel.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-mute .NB-menu-manage-image, .NB-menu-manage .NB-menu-manage-unmute .NB-menu-manage-image { - background: transparent url('/media/embed/icons/circular/menu_icn_mute.png') no-repeat 0 0; + background: transparent url('/media/embed/icons/circular/menu_icn_mute.png') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-rename .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/rename.svg') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-rename.NB-active .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/accept.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-rename.NB-menu-manage-feed-rename-cancel .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/cancel.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-story-share .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/share.svg') no-repeat 0 2px; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-share.NB-active .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/accept.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-story-share.NB-menu-manage-story-share-cancel .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/cancel.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-image { - background: transparent url('/media/embed/icons/nouns/right.svg') no-repeat 4px 3px; + background: transparent url('/media/embed/icons/nouns/right.svg') no-repeat 4px 3px; background-size: 8px; } + .NB-menu-manage .NB-menu-manage-confirm { overflow: hidden; position: relative; padding-top: 3px; padding-bottom: 2px; } + .NB-menu-manage .NB-menu-manage-feed-move-confirm { padding: 0; } + .NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-confirm-position { position: absolute; bottom: 2px; left: 0; width: 100%; } + .NB-menu-manage .NB-menu-manage-confirm select, .NB-menu-manage .NB-menu-manage-confirm input.NB-menu-manage-title, .NB-menu-manage .NB-menu-manage-open-input { @@ -8356,83 +9357,100 @@ form.opml_import_form input { height: 15px; border: 1px solid #606060; } + .NB-menu-manage .NB-menu-manage-open-input { float: right; margin: 4px 8px 0 0; } + .NB-menu-manage .NB-menu-manage-confirm select { height: auto; } + .NB-menu-manage li.NB-menu-item.NB-menu-manage-confirm:hover { background-color: #BAE3A8; cursor: default; } + .NB-menu-manage .NB-menu-manage-confirm input.NB-menu-manage-title, .NB-menu-manage .NB-menu-manage-confirm:hover input.NB-menu-manage-title { text-shadow: none; } + .NB-menu-manage .NB-modal-submit-button { margin: 4px 4px 0 0; } + .NB-menu-manage .NB-menu-manage-confirm .NB-modal-submit-button { margin: 2px 4px 2px 0; } + .NB-menu-manage .NB-modal-submit-button, .NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-move-save, .NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-rename-save { - float: right; - font-size: 10px; - font-weight: bold; - color: white; - padding: 2px 6px; - background-color: #639510; - cursor: pointer; + float: right; + font-size: 10px; + font-weight: bold; + color: white; + padding: 2px 6px; + background-color: #639510; + cursor: pointer; } + .NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-story-share-save { - font-size: 10px; - font-weight: bold; - color: white; - padding: 2px 6px; - margin: 4px 0 0; - background-color: #639510; - cursor: pointer; - text-align: center; - width: 100%; + font-size: 10px; + font-weight: bold; + color: white; + padding: 2px 6px; + margin: 4px 0 0; + background-color: #639510; + cursor: pointer; + text-align: center; + width: 100%; } + .NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-story-share-unshare { - width: 100%; - text-align: center; - margin: 4px 0 0; + width: 100%; + text-align: center; + margin: 4px 0 0; } + .NB-menu-manage .NB-menu-manage-confirm .NB-sideoption-save-notes, .NB-menu-manage .NB-menu-manage-confirm .NB-sideoption-share-comments { height: 28px; } + .NB-menu-manage .NB-menu-manage-confirm .NB-add-folders { float: left; max-height: 84px; overflow-y: auto; } + .NB-menu-manage .NB-menu-manage-feed-move-save { display: none; } + .NB-menu-manage .NB-menu-manage-feed-move-save { float: right; display: none; } + .NB-menu-manage .NB-menu-manage-confirm .NB-change-folders { width: 100%; height: 84px; overflow-y: auto; } + .NB-menu-manage .NB-change-folders .NB-folders { padding: 4px 0; } + .NB-menu-manage .NB-change-folders .NB-folder-option { overflow: hidden; clear: both; cursor: pointer; } + .NB-menu-manage .NB-change-folders .NB-folder-option:hover { background-color: rgba(0, 4, 0, .1); } @@ -8447,10 +9465,12 @@ form.opml_import_form input { clear: left; margin: 1px 4px; } + .NB-menu-manage .NB-change-folders .NB-folder-option.NB-folder-option-active .NB-icon { background: transparent url('/media/embed/icons/nouns/folder-closed.svg') no-repeat 0 0; - background-size: 16px; + background-size: 16px; } + .NB-menu-manage .NB-change-folders .NB-folder-option:hover .NB-icon-add { background: transparent url('/media/embed/icons/circular/g_icn_folder_add.png') no-repeat 0 0; float: right; @@ -8459,6 +9479,7 @@ form.opml_import_form input { margin: 1px 4px 0; background-size: 16px; } + .NB-menu-manage .NB-change-folders .NB-folder-option:hover .NB-icon-add:hover { background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0; background-size: 16px; @@ -8467,14 +9488,17 @@ form.opml_import_form input { .NB-menu-manage .NB-change-folders .NB-folder-option-title { padding: 3px 0 2px; } + .NB-menu-manage .NB-change-folders .NB-folder-option.NB-folder-option-active .NB-folder-option-title { font-weight: bold; } + .NB-menu-manage .NB-change-folders .NB-input { font-size: 11px; float: left; margin: 0; } + .NB-menu-manage .NB-change-folders .NB-menu-manage-add-folder-save { margin: 0 4px 0 0; } @@ -8483,69 +9507,82 @@ form.opml_import_form input { overflow: hidden; border: none; } -.NB-menu-manage .NB-menu-manage-story-share-confirm .NB-sideoption-share .NB-modal-submit-button { -} + +.NB-menu-manage .NB-menu-manage-story-share-confirm .NB-sideoption-share .NB-modal-submit-button {} + .NB-menu-manage .NB-menu-manage-site-mark-read .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/mark-read.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(20deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-trainer .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-trainer.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(140deg) saturate(15); } + .NB-menu-manage .NB-menu-manage-goodies .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-goodies.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(186deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-statistics .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-statistics.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(284deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-newsletters .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/email.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(134deg) saturate(16); } + .NB-menu-manage .NB-menu-manage-notifications .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-notifications.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(320deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-import .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-import.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(30deg) saturate(10); } + .NB-menu-manage .NB-menu-manage-friends .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/followers.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(139deg) saturate(18.5); } + .NB-menu-manage .NB-menu-manage-profile-editor .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/subscribers.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(87deg) saturate(10.5); } + .NB-menu-manage .NB-menu-manage-preferences .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/dialog-preferences.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(320deg) saturate(17.5); } + .NB-menu-manage .NB-menu-manage-theme .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/menu-theme.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(130deg) saturate(10); } + .NB-menu-manage .NB-menu-item.NB-menu-manage-size, .NB-menu-manage .NB-menu-item.NB-menu-manage-theme, .NB-menu-manage .NB-menu-item.NB-menu-manage-density, .NB-menu-manage .NB-menu-item.NB-menu-manage-font { padding: 2px 0; } + .NB-menu-manage .NB-menu-manage-density .segmented-control, .NB-menu-manage .NB-menu-manage-font .segmented-control, .NB-menu-manage .NB-menu-manage-size .segmented-control, @@ -8553,76 +9590,94 @@ form.opml_import_form input { margin: 0 6px 0 36px; max-width: 300px; } + .NB-menu-manage .NB-menu-manage-theme .segmented-control li { padding: 4px 8px; } -.NB-menu-manage .NB-menu-manage-font .NB-menu-manage-image { + +.NB-menu-manage .NB-menu-manage-font .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/font.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(226deg) saturate(10.5); } + .NB-menu-manage .NB-menu-manage-font .segmented-control li { padding: 4px 8px; } + .NB-menu-manage .NB-menu-manage-size .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/font-size.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(40deg) saturate(10.5); } + .NB-menu-manage .NB-menu-manage-size .segmented-control li { font-weight: bold; } + .NB-menu-manage .NB-menu-manage-size .segmented-control li.NB-options-feed-size-xs { font-size: 9px; padding: 7px 12px 6px; } + .NB-menu-manage .NB-menu-manage-size .segmented-control li.NB-options-feed-size-s { font-size: 10px; padding: 6px 12px 5px; } + .NB-menu-manage .NB-menu-manage-size .segmented-control li.NB-options-feed-size-m { font-size: 12px; padding: 5px 12px 4px; } + .NB-menu-manage .NB-menu-manage-size .segmented-control li.NB-options-feed-size-l { font-size: 13px; padding: 4px 12px; } + .NB-menu-manage .NB-menu-manage-size .segmented-control li.NB-options-feed-size-xl { font-size: 15px; padding: 3px 12px 2px; } -.NB-menu-manage .NB-menu-manage-density .NB-menu-manage-image { + +.NB-menu-manage .NB-menu-manage-density .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/square-space.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(126deg) saturate(10.5); } + .NB-menu-manage .NB-menu-manage-density .segmented-control li.NB-density-option { padding: 4px 8px; } + .NB-menu-manage .NB-menu-manage-account .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/dialog-account.svg') no-repeat 0 0; background-size: 18px; filter: hue-rotate(287deg) saturate(10.5); } + .NB-menu-manage .NB-menu-manage-feedchooser .NB-menu-manage-image { - background: transparent url('/media/embed/icons/circular/g_icn_mute.png') no-repeat 0 0; + background: transparent url('/media/embed/icons/circular/g_icn_mute.png') no-repeat 0 0; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-organizer .NB-menu-manage-image { background: transparent url("/media/embed/icons/nouns/dialog-organize.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(311deg) saturate(16); } + .NB-menu-manage .NB-menu-manage-premium .NB-menu-manage-image { - background: transparent url('/media/embed/icons/circular/g_icn_greensun.png') no-repeat 0 0px; + background: transparent url('/media/embed/icons/circular/g_icn_greensun.png') no-repeat 0 0px; background-size: 18px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/sendto.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(338deg) saturate(18); } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-icon { width: 16px; height: 16px; @@ -8630,71 +9685,89 @@ form.opml_import_form input { margin: 6px 0 0 0; padding: 0 4px 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-email { background: transparent url("/media/embed/icons/nouns/email.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(134deg) saturate(16); } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-instapaper { background: transparent url('/media/embed/reader/instapaper.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-readitlater { background: transparent url('/media/embed/reader/pocket_ril.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-tumblr { background: transparent url('/media/embed/reader/tumblr.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-blogger { background: transparent url('/media/embed/reader/blogger.png') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-delicious { background: transparent url('/media/embed/reader/delicious.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-pinboard { background: transparent url('/media/embed/reader/pinboard.png') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-raindrop { background: transparent url('/media/embed/reader/raindrop.svg') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-pinterest { background: transparent url('/media/embed/reader/pinterest.png') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-buffer { background: transparent url('/media/embed/reader/buffer.png') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-diigo { background: transparent url('/media/embed/reader/diigo.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-evernote { background: transparent url('/media/embed/reader/evernote.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-googleplus { background: transparent url('/media/embed/reader/googleplus.png') no-repeat 0 0; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-email { background: transparent url("/media/embed/icons/nouns/email.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(134deg) saturate(16); } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-twitter { background: transparent url('/media/embed/reader/twitter_bird.png') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-facebook { background: transparent url('/media/embed/reader/facebook_icon.png') no-repeat 0 0; } .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-icon { - opacity: .2; + opacity: .2; } + .NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-email { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-twitter .NB-menu-manage-thirdparty-email, .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-facebook .NB-menu-manage-thirdparty-email, .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-readitlater .NB-menu-manage-thirdparty-email, @@ -8709,76 +9782,97 @@ form.opml_import_form input { .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-evernote .NB-menu-manage-thirdparty-email, .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-googleplus .NB-menu-manage-thirdparty-email, .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-instapaper .NB-menu-manage-thirdparty-email { - opacity: .2; + opacity: .2; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-twitter .NB-menu-manage-thirdparty-twitter { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-facebook .NB-menu-manage-thirdparty-facebook { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-readitlater .NB-menu-manage-thirdparty-readitlater { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-tumblr .NB-menu-manage-thirdparty-tumblr { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-blogger .NB-menu-manage-thirdparty-blogger { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-delicious .NB-menu-manage-thirdparty-delicious { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-pinboard .NB-menu-manage-thirdparty-pinboard { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-raindrop .NB-menu-manage-thirdparty-raindrop { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-pinterest .NB-menu-manage-thirdparty-pinterest { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-buffer .NB-menu-manage-thirdparty-buffer { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-diigo .NB-menu-manage-thirdparty-diigo { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-evernote .NB-menu-manage-thirdparty-evernote { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-googleplus .NB-menu-manage-thirdparty-googleplus { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-instapaper .NB-menu-manage-thirdparty-instapaper { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-thirdparty.NB-menu-manage-highlight-email .NB-menu-manage-thirdparty-email { - opacity: 1; + opacity: 1; } + .NB-menu-manage .NB-menu-manage-story-unread .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 4px center; background-size: 8px; } + .NB-menu-manage .NB-menu-manage-story-read .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 4px center; background-size: 8px; opacity: 0.2; } + .NB-menu-manage .NB-menu-manage-story-star .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/saved-stories.svg') no-repeat 0 0; background-size: 16px; } + .NB-menu-manage .NB-menu-manage-story-open .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/link.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(168deg) saturate(18); } + .NB-menu-manage.NB-story-starred .NB-menu-manage-story-star .NB-menu-manage-image { background: transparent url('/media/embed/icons/nouns/saved-stories.svg') no-repeat 0 0; background-size: 16px; opacity: .55; } + .NB-menu-manage .NB-menu-manage-confirm, .NB-menu-manage .NB-menu-manage-delete-confirm { display: none; @@ -8789,11 +9883,13 @@ form.opml_import_form input { background: none; padding: 2px 0 3px; } + .NB-menu-manage li.NB-menu-manage-controls:hover { overflow: hidden; background: none; cursor: default; } + .NB-menu-manage .NB-menu-manage-controls .segmented-control { display: inline-block; margin: 4px 0 0 36px; @@ -8803,13 +9899,15 @@ form.opml_import_form input { overflow: hidden; text-align: center; } -.NB-menu-manage .NB-menu-manage-controls .segmented-control:last-child { -} + +.NB-menu-manage .NB-menu-manage-controls .segmented-control:last-child {} + .NB-menu-manage .NB-menu-manage-controls .segmented-control li { clear: none; padding: 1px 12px 0; font-size: 10px; } + /* ==================== */ /* = Statistics Modal = */ /* ==================== */ @@ -8817,14 +9915,15 @@ form.opml_import_form input { .NB-embedded-statistics { overflow: visible; } -.NB-modal-statistics { -} + +.NB-modal-statistics {} .NB-modal-statistics .NB-modal-title .NB-icon { background: transparent url("/media/embed/icons/nouns/dialog-statistics.svg") no-repeat center center; background-size: 28px; filter: hue-rotate(284deg) saturate(18); } + .NB-modal-statistics .NB-statistics-stat { border: 1px solid #e6e6e6; clear: both; @@ -8843,6 +9942,7 @@ form.opml_import_form input { font-size: 11px; margin-top: 8px; } + .NB-modal-statistics .NB-statisics-realtime-spinner { width: 16px; height: 16px; @@ -8850,34 +9950,41 @@ form.opml_import_form input { vertical-align: middle; display: inline-block; } + .NB-modal-statistics .NB-statistics-stat .NB-statistics-count { - font-size: 17px; - padding: 6px 0 0; - margin-bottom: 8px; + font-size: 17px; + padding: 6px 0 0; + margin-bottom: 8px; } + .NB-modal-statistics .NB-statistics-realtime .NB-statistics-count { font-size: 13px; color: #808080; } + .NB-modal-statistics .NB-statistics-history-stat .NB-statistics-count { - font-size: 42px; - margin-top: 12px; + font-size: 42px; + margin-top: 12px; } + .NB-modal-statistics .NB-statistics-stat .NB-statistics-update { flex: 1 1 0; text-align: center; overflow: hidden; margin: 0 0 12px 0; } + .NB-modal-statistics .NB-statistics-update-explainer { clear: both; margin: 6px 24px 6px; font-size: 10px; color: #808080; } + .NB-modal-statistics .NB-statistics-update-explainer b { padding-right: 8px; } + .NB-modal-statistics .NB-statistics-premium-stats { border-top: 1px solid #E0E0E0; padding: 12px 0 0; @@ -8885,6 +9992,7 @@ form.opml_import_form input { clear: both; text-align: center; } + .NB-modal-statistics .NB-statistics-premium-stats .NB-statistics-update { width: auto; margin: 0; @@ -8893,51 +10001,52 @@ form.opml_import_form input { } .NB-modal-statistics .NB-statistics-stat .NB-statistics-fetches-half { - /* float: left; */ - text-align: center; - margin: 0 18px 6px 0; - flex: 1 1 0; - flex-basis: fit-content; + /* float: left; */ + text-align: center; + margin: 0 18px 6px 0; + flex: 1 1 0; + flex-basis: fit-content; } + .NB-modal-statistics .NB-statistics-stat .NB-statistics-fetches-half:last-child { margin-right: 0; } .NB-modal-statistics .NB-statistics-stat .NB-statistics-history-stat { - text-align: center; - margin: 0 24px; - overflow: hidden; - width: 100%; + text-align: center; + margin: 0 24px; + overflow: hidden; + width: 100%; } .NB-modal-statistics .NB-statistics-stat .NB-statistics-history-count-chart { - margin: 12px 24px 18px; - width: 524px; - height: 180px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - margin: 36px auto; + margin: 12px 24px 18px; + width: 524px; + height: 180px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin: 36px auto; } .NB-modal-statistics .NB-statistics-stat .NB-statistics-history-hours-chart { - margin: 12px 24px 18px; - width: 524px; - margin-left: auto; - margin-right: auto; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + margin: 12px 24px 18px; + width: 524px; + margin-left: auto; + margin-right: auto; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .NB-modal-statistics .NB-statistics-stat .NB-statistics-history-days-chart { - margin: 32px 24px; - width: 524px; - height: 300px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - margin: 36px auto; + margin: 32px 24px; + width: 524px; + height: 300px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin: 36px auto; } .NB-modal-statistics .NB-statistics-stat .NB-statistics-history-hours-chart { @@ -8945,51 +10054,62 @@ form.opml_import_form input { color: #808080; font-size: 10px; } + .NB-modal-statistics .NB-statistics-history-chart-hours-row { background-color: #97BBCD; height: 50px; } + .NB-modal-statistics .NB-statistics-history-chart-hours-row td { width: 18px; } + .NB-modal-statistics .NB-modal-loading { - margin: 6px 8px 0; + margin: 6px 8px 0; } .NB-history-fetch { - overflow: hidden; - clear: both; - font-size: 10px; - margin: 2px 0 0 8px; - text-align: left; + overflow: hidden; + clear: both; + font-size: 10px; + margin: 2px 0 0 8px; + text-align: left; } + .NB-history-fetch.NB-ok { - color: #135500; + color: #135500; } + .NB-history-fetch.NB-errorcode { - color: #6A1000; + color: #6A1000; } + .NB-history-fetch .NB-history-fetch-code { - display: inline; - font-weight: normal; + display: inline; + font-weight: normal; } + .NB-history-fetch .NB-history-fetch-date { - float: left; - padding-right: 4px; + float: left; + padding-right: 4px; } + .NB-history-fetch .NB-history-fetch-message { - padding-right: 4px; - margin-left: 110px; - font-weight: bold; + padding-right: 4px; + margin-left: 110px; + font-weight: bold; } + .NB-history-fetch .NB-history-fetch-exception { - display: none; + display: none; } + .NB-history-empty { color: #C0C0C0; font-size: 10px; padding: 4px 12px; } + .NB-modal-statistics .NB-statistics-classifiers { border: 1px solid #e6e6e6; clear: both; @@ -8998,6 +10118,7 @@ form.opml_import_form input { padding: 10px 12px; font-size: 12px; } + .NB-modal-statistics .NB-statistics-facet-title { text-transform: uppercase; text-align: center; @@ -9006,57 +10127,64 @@ form.opml_import_form input { padding: 0 0 4px; clear: both; } + .NB-modal-statistics .NB-statistics-facet { - clear: both; - padding: 2px 0; - position: relative; - border-bottom: 1px solid #F6F6F6; - overflow: hidden; + clear: both; + padding: 2px 0; + position: relative; + border-bottom: 1px solid #F6F6F6; + overflow: hidden; } .NB-modal-statistics .NB-statistics-facet-name { - width: 40%; + width: 40%; } + .NB-modal-statistics .NB-statistics-facet-pos { - position: absolute; - top: 0; - right: 0; - width: 30%; - padding: 4px 0; + position: absolute; + top: 0; + right: 0; + width: 30%; + padding: 4px 0; } + .NB-modal-statistics .NB-statistics-facet-separator { - position: absolute; - height: 16px; - width: 2px; - right: 30%; - margin-left: -2px; - background-color: black; + position: absolute; + height: 16px; + width: 2px; + right: 30%; + margin-left: -2px; + background-color: black; } + .NB-modal-statistics .NB-statistics-facet-neg { - position: absolute; - top: 0; - right: 30%; - width: 30%; - padding: 4px 0; - text-align: right; + position: absolute; + top: 0; + right: 30%; + width: 30%; + padding: 4px 0; + text-align: right; } + .NB-modal-statistics .NB-statistics-facet-pos .NB-statistics-facet-bar { - height: 8px; - position: absolute; - left: 0; - top: 6px; - background-color: #6EA74A; + height: 8px; + position: absolute; + left: 0; + top: 6px; + background-color: #6EA74A; } + .NB-modal-statistics .NB-statistics-facet-neg .NB-statistics-facet-bar { - height: 8px; - position: absolute; - right: 2px; - top: 6px; - background-color: #CC2A2E; + height: 8px; + position: absolute; + right: 2px; + top: 6px; + background-color: #CC2A2E; } + .NB-modal-statistics .NB-statistics-facet-count { - font-size: 9px; - color: #C0C0C0; + font-size: 9px; + color: #C0C0C0; } /* ============================= */ @@ -9067,40 +10195,44 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/circular/g_modal_recommend.png'); background-size: 28px; } + .NB-modal-recommend .NB-modal-loading { - margin: 6px 8px 0; + margin: 6px 8px 0; } + .NB-modal.NB-modal-recommend .NB-modal-recommend-tagline-container { - padding: 6px 0; - margin: 4px 0; - border-top: 1px solid #C0C0C0; - border-bottom: 1px solid #C0C0C0; + padding: 6px 0; + margin: 4px 0; + border-top: 1px solid #C0C0C0; + border-bottom: 1px solid #C0C0C0; } + .NB-modal-recommend .NB-modal-recommend-tagline { - width: 558px; - height: 80px; - font-size: 14px; - color: #404040; - line-height: 20px; - padding: 8px; - margin: 0; - border: 1px solid #E0E0E0; - /* font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; */ + width: 558px; + height: 80px; + font-size: 14px; + color: #404040; + line-height: 20px; + padding: 8px; + margin: 0; + border: 1px solid #E0E0E0; + /* font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; */ } + .NB-modal-recommend .NB-modal-recommend-credit { - color: #505050; - font-size: 12px; - margin: 4px 0; -} -.NB-modal-recommend .NB-modal-recommend-twitter { - + color: #505050; + font-size: 12px; + margin: 4px 0; } + +.NB-modal-recommend .NB-modal-recommend-twitter {} + .NB-modal-recommend .NB-modal-recommend-explanation { - clear: both; - color: #505050; - font-size: 12px; - margin: 12px 0 14px; - padding: 0 1px; + clear: both; + color: #505050; + font-size: 12px; + margin: 12px 0 14px; + padding: 0 1px; } /* ==================== */ @@ -9111,24 +10243,30 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/circular/g_modal_admin.png'); background-size: 28px; } + .NB-modal-admin .NB-profile-badge-actions, .NB-modal-admin .NB-profile-badge-action-admin { display: none; } + .NB-modal-admin .NB-profile-badge { border-bottom: none; } + .NB-modal-admin .NB-account-payments, .NB-modal-admin .NB-admin-actions { overflow: hidden; padding: 12px 24px; } + .NB-modal-admin .NB-modal-submit-button { float: left; } + .NB-modal-admin dl { font-size: 12px; } + .NB-modal-admin dt { display: block; float: left; @@ -9139,6 +10277,7 @@ form.opml_import_form input { text-align: right; line-height: 16px; } + .NB-modal-admin dd { color: #000; margin-left: 100px; @@ -9148,15 +10287,19 @@ form.opml_import_form input { word-wrap: break-word; overflow: hidden; } + .NB-modal-admin .NB-admin-training-counts span { padding-right: 8px; } + .NB-modal-admin .NB-admin-training-counts span.NB-grey { color: #D0D0D0; } + .NB-modal-admin .NB-admin-training-counts span.NB-green { color: #2B8B19; } + .NB-modal-admin .NB-admin-training-counts span.NB-red { color: #761113; } @@ -9166,86 +10309,100 @@ form.opml_import_form input { /* ===================== */ .NB-modal-email .NB-modal-loading { - margin: 6px 8px 0; + margin: 6px 8px 0; } + .NB-modal-email label { - float: left; - padding-top: 4px; - font-weight: bold; + float: left; + padding-top: 4px; + font-weight: bold; } .NB-modal-email .NB-input, .NB-modal-email .NB-modal-email-cc-wrapper { - width: 430px; - margin-left: 120px; - display: block; - overflow: hidden; + width: 430px; + margin-left: 120px; + display: block; + overflow: hidden; } + .NB-modal-email .NB-modal-email-feed { - font-size: 11px; - margin: 0 0 6px; - line-height: 10px; + font-size: 11px; + margin: 0 0 6px; + line-height: 10px; } + .NB-modal-email .NB-modal-email-feed .NB-modal-feed-image { - height: 12px; - width: 12px; + height: 12px; + width: 12px; } + .NB-modal-email .NB-modal-email-feed .NB-modal-feed-title { - margin-left: 0; - float: none; - overflow: visible; + margin-left: 0; + float: none; + overflow: visible; } + .NB-modal-email .NB-modal-email-story-title { clear: both; margin: 8px 0 4px; } + .NB-modal-email .NB-modal-email-story-permalink { - color: #808080; - font-size: 11px; + color: #808080; + font-size: 11px; } + .NB-modal.NB-modal-email .NB-modal-email-comments-container { - padding: 6px 0 0; - margin: 0; + padding: 6px 0 0; + margin: 0; } + .NB-modal-email .NB-modal-email-comments { - width: 550px; - height: 80px; - font-size: 14px; - color: #404040; - line-height: 20px; - padding: 8px 0 0 8px; - margin: 0; - border: 1px solid #808080; - /* font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; */ -} -.NB-modal-email .NB-modal-email-to-container, + width: 550px; + height: 80px; + font-size: 14px; + color: #404040; + line-height: 20px; + padding: 8px 0 0 8px; + margin: 0; + border: 1px solid #808080; + /* font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; */ +} + +.NB-modal-email .NB-modal-email-to-container, .NB-modal-email .NB-modal-email-from-container { - color: #505050; - font-size: 12px; - padding: 12px 0 0; + color: #505050; + font-size: 12px; + padding: 12px 0 0; } + .NB-modal-email .NB-modal-email-to-container { - margin: 4px 0; -} -.NB-modal-email .NB-modal-email-from-container { + margin: 4px 0; } + +.NB-modal-email .NB-modal-email-from-container {} + .NB-modal-email form { margin: 24px 0 0; } + .NB-modal-email .NB-modal-email-cc { float: left; margin: 4px 6px 0 0; } + .NB-modal-email .NB-modal-email-explanation { - clear: both; - color: #505050; - font-size: 12px; - margin: 0; - padding: 14px 1px 0; + clear: both; + color: #505050; + font-size: 12px; + margin: 0; + padding: 14px 1px 0; } + .NB-modal.NB-modal-email .NB-error { - font-size: 12px; - margin-top: 8px; + font-size: 12px; + margin-top: 8px; } /* ================== */ @@ -9258,142 +10415,174 @@ form.opml_import_form input { background-size: 28px; filter: hue-rotate(321deg) saturate(18); } + .NB-modal-tutorial h4 { - clear: both; + clear: both; } + .NB-modal-tutorial .NB-page { - display: none; + display: none; } + .NB-modal-tutorial .NB-modal-page { - float: right; - margin-top: 7px; + float: right; + margin-top: 7px; } + .NB-modal-tutorial .NB-page { -/* height: 446px;*/ - overflow: hidden; + /* height: 446px;*/ + overflow: hidden; } + .NB-modal-tutorial .NB-page.NB-page-1 { - display: block; + display: block; } + .NB-modal-tutorial .NB-modal-submit .NB-modal-submit-button { - float: right; + float: right; } + .NB-modal-tutorial ul { - clear: both; - list-style: none; - padding: 0; + clear: both; + list-style: none; + padding: 0; } + .NB-modal-tutorial li { - margin: 0 0 12px; - padding: 0; - padding-left: 20px; - background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat 0 2px; + margin: 0 0 12px; + padding: 0; + padding-left: 20px; + background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat 0 2px; } + .NB-modal-tutorial .NB-page-1 .NB-right { - padding-right: 12px; + padding-right: 12px; } + .NB-modal-tutorial .NB-tutorial-view { - float: left; - width: 33%; - text-align: center; - margin: 0 0 18px 0; + float: left; + width: 33%; + text-align: center; + margin: 0 0 18px 0; } + .NB-modal-tutorial .NB-tutorial-view .NB-tutorial-view-title { - font-size: 18px; - font-weight: bold; - margin: 0 0 2px 0; - padding-right: 16px; + font-size: 18px; + font-weight: bold; + margin: 0 0 2px 0; + padding-right: 16px; } + .NB-modal-tutorial .NB-tutorial-view .NB-tutorial-view-title img { - vertical-align: middle; - margin: -2px 4px 0 0; + vertical-align: middle; + margin: -2px 4px 0 0; } + .NB-modal-tutorial .NB-tutorial-view .NB-tutorial-view-image { - border: 1px solid #303030; - width: 180px; - height: 150px; + border: 1px solid #303030; + width: 180px; + height: 150px; } + .NB-modal-tutorial .NB-tutorial-view span { - font-size: 11px; - color: #808080; + font-size: 11px; + color: #808080; } + .NB-modal-tutorial .NB-page-2 ul { - margin-bottom: 0; + margin-bottom: 0; } + .NB-modal-tutorial .NB-page-3 b { - display: block; - margin: 0 0 8px 0; + display: block; + margin: 0 0 8px 0; } + .NB-modal-tutorial .NB-tutorial-slider-demo { - position: relative; + position: relative; } + .NB-modal-tutorial .NB-taskbar-intelligence { - left: 64px; - right: auto; - top: 24px; - position: relative; + left: 64px; + right: auto; + top: 24px; + position: relative; } + .NB-modal-tutorial .NB-page-3 .NB-tutorial-train-1 { - margin-bottom: 12px; + margin-bottom: 12px; } + .NB-modal-tutorial .NB-page-3 .NB-tutorial-train-1 img { - margin-right: 24px; - border: 1px solid #505050; + margin-right: 24px; + border: 1px solid #505050; } + .NB-modal-tutorial .NB-page-3 ul img.NB-trainer-bullet { - width: 8px; - margin: 0px 8px 2px 0; + width: 8px; + margin: 0px 8px 2px 0; } + .NB-modal-tutorial .NB-page-4 ul li { - clear: both; - padding: 0 224px 0 20px; - margin: 0 0 18px 0; + clear: both; + padding: 0 224px 0 20px; + margin: 0 0 18px 0; } + .NB-modal-tutorial .NB-page-4 img { - border: 1px solid #303030; - float: right; - clear: right; - margin: 0 -212px 18px 12px; + border: 1px solid #303030; + float: right; + clear: right; + margin: 0 -212px 18px 12px; } + .NB-modal-tutorial .NB-page-4 .NB-modal-keyboard .NB-keyboard-group { - width: 510px; + width: 510px; } + .NB-modal-tutorial .NB-page-4 .NB-modal-keyboard .NB-keyboard-shortcut { - margin: 0 20px 0 0; - width: 235px; + margin: 0 20px 0 0; + width: 235px; } + .NB-modal-tutorial .NB-page-4 .NB-modal-keyboard .NB-keyboard-shortcut:last-child { - margin-bottom: 0; - margin-right: 0; + margin-bottom: 0; + margin-right: 0; } + .NB-modal-tutorial .NB-page-5 .NB-tutorial-twitter { - overflow: hidden; + overflow: hidden; } + .NB-modal-tutorial .NB-page-5 .NB-tutorial-twitter a { - background-color: #E9F4FD; - float: left; - font-size: 20px; - margin: 0px 16px; - padding: 12px; - text-align: center; - width: 40%; + background-color: #E9F4FD; + float: left; + font-size: 20px; + margin: 0px 16px; + padding: 12px; + text-align: center; + width: 40%; } + .NB-modal-tutorial .NB-page-5 .NB-tutorial-twitter a:hover { - background-color: #FAE3DB; + background-color: #FAE3DB; } + .NB-modal-tutorial .NB-page-5 .NB-tutorial-twitter img { - margin: 0 12px 0 0; - vertical-align: middle; - border: 1px solid transparent; - width: 64px; - height: 64px; + margin: 0 12px 0 0; + vertical-align: middle; + border: 1px solid transparent; + width: 64px; + height: 64px; } + .NB-modal-tutorial .NB-page-5 h4.NB-tutorial-feedback-header { - margin: 24px 0px 12px; + margin: 24px 0px 12px; } + .NB-modal-tutorial .NB-page-5 ul, .NB-modal-tutorial .NB-page-5 h4 { - clear: none; + clear: none; } @@ -9401,13 +10590,14 @@ form.opml_import_form input { /* = Tutorial Modal = */ /* ================== */ -.NB-modal-intro { -} +.NB-modal-intro {} + .NB-modal.NB-modal-intro .NB-modal-title { text-align: center; font-weight: normal; line-height: 46px; } + .NB-modal-intro .NB-divider { height: 1px; width: 100%; @@ -9419,47 +10609,57 @@ form.opml_import_form input { .NB-modal-intro .NB-modal-loading { margin: 16px 8px 0 0; } + .NB-modal-intro .NB-intro-section { padding: 24px 4px; text-align: center; overflow: hidden; } + .NB-modal-intro h4 { - clear: both; + clear: both; } + .NB-modal-intro .NB-page { - display: none; + display: none; } + .NB-modal-intro .NB-modal-page { position: absolute; top: 14px; right: 12px; line-height: 46px; } + .NB-modal-intro .NB-modal-page-text { float: right; } + .NB-modal-intro .NB-page { - height: 390px; - overflow-x: hidden; - overflow-y: auto; + height: 390px; + overflow-x: hidden; + overflow-y: auto; } + .NB-modal-intro .NB-page.NB-page-1 { - display: block; + display: block; } + .NB-modal-intro .NB-modal-submit-bottom .NB-modal-submit-button { - float: right; + float: right; } + .NB-modal-intro ul { - clear: both; - list-style: none; - padding: 0; + clear: both; + list-style: none; + padding: 0; } + .NB-modal-intro li { - margin: 0 0 12px; - padding: 0; - padding-left: 20px; - background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat 0 2px; + margin: 0 0 12px; + padding: 0; + padding-left: 20px; + background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat 0 2px; } .NB-modal-intro .NB-intro-spinning-logo { @@ -9468,20 +10668,32 @@ form.opml_import_form input { left: 180px; width: 256px; height: 256px; - -webkit-animation: -webkit-slow-spin 60s infinite linear; - -moz-animation-duration: 60s; - -moz-animation-name: -moz-slow-spin; - -moz-animation-iteration-count: infinite; - -moz-animation-timing-function: linear; + -webkit-animation: -webkit-slow-spin 60s infinite linear; + -moz-animation-duration: 60s; + -moz-animation-name: -moz-slow-spin; + -moz-animation-iteration-count: infinite; + -moz-animation-timing-function: linear; } + @-webkit-keyframes -webkit-slow-spin { - from {-webkit-transform: rotate(0deg)} - to {-webkit-transform: rotate(360deg)} + from { + -webkit-transform: rotate(0deg) + } + + to { + -webkit-transform: rotate(360deg) + } +} + +@-moz-keyframes -moz-slow-spin { + from { + -moz-transform: rotate(0deg) + } + + to { + -moz-transform: rotate(360deg) + } } -@-moz-keyframes -moz-slow-spin { - from {-moz-transform: rotate(0deg)} - to {-moz-transform: rotate(360deg)} -} .NB-modal-intro .NB-page-1-started { margin: 284px 0 0; @@ -9500,6 +10712,7 @@ form.opml_import_form input { .NB-modal-intro .NB-intro-imports { position: relative; } + .NB-modal-intro .NB-page-2-importing { font-size: 16px; font-weight: bold; @@ -9510,6 +10723,7 @@ form.opml_import_form input { height: 70px; overflow: hidden; } + .NB-modal-intro .NB-carousel-inner { position: absolute; top: 0; @@ -9517,6 +10731,7 @@ form.opml_import_form input { width: 100%; height: 100%; } + .NB-modal-intro .NB-carousel .NB-carousel-item { overflow: hidden; position: absolute; @@ -9525,6 +10740,7 @@ form.opml_import_form input { top: 0; left: 0; } + .NB-modal-intro .NB-carousel-item.NB-intro-imports-start { left: 0; padding: 0 24px; @@ -9532,9 +10748,11 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-modal-intro .NB-carousel-item.NB-intro-imports-progress { left: 100%; } + .NB-modal-intro .NB-carousel-item.NB-intro-imports-sites { left: 200%; } @@ -9546,6 +10764,7 @@ form.opml_import_form input { text-align: center; display: none; } + .NB-modal-intro .NB-intro-import-starred-message { display: none; text-align: center; @@ -9553,18 +10772,22 @@ form.opml_import_form input { font-size: 14px; font-weight: bold; } + .NB-modal-intro .NB-intro-module-containers { overflow: hidden; } + .NB-modal-intro .NB-intro-module-container { width: 46%; text-align: center; } + .NB-modal-intro .NB-module-content-header { text-align: center; padding: 4px 0; margin: 0; } + .NB-modal-intro .NB-intro-module { border-radius: 3px; background-color: #F7F8F5; @@ -9572,7 +10795,7 @@ form.opml_import_form input { padding: 12px; overflow: hidden; width: 100%; - + -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; @@ -9582,9 +10805,11 @@ form.opml_import_form input { margin: 24px 0 32px; font-size: 24px; } + .NB-intro-import-only.NB-modal-intro .NB-page-2 .NB-page-2-started { display: none; } + .NB-modal-intro .NB-intro-module .NB-modal-submit-button { margin: 0; font-weight: bold; @@ -9595,6 +10820,7 @@ form.opml_import_form input { overflow: hidden; line-height: 11px; } + .NB-modal-intro .NB-intro-module .NB-modal-submit-button input[type=file] { position: absolute; right: 0px; @@ -9607,23 +10833,28 @@ form.opml_import_form input { opacity: 0; z-index: 10; } + .NB-modal-intro .NB-intro-module.NB-intro-import-opml { position: relative; } + .NB-modal-intro .NB-intro-imports .NB-intro-import-message { margin: 0 0 8px; padding: 0; } + .NB-modal-intro .NB-intro-module h3 { font-size: 32px; margin: 0 0 4px; } + .NB-modal-intro .NB-add-google-reader-arrow { width: 16px; height: 16px; vertical-align: bottom; margin: 0 4px; } + .NB-modal-intro .NB-intro-bookmarklet, .NB-modal-intro .NB-intro-starredimport { text-align: center; @@ -9633,40 +10864,50 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-modal-intro .NB-intro-starredimport { margin: 12px 0 0; } + .NB-modal-intro .NB-intro-bookmarklet-info { float: left; font-size: 13px; padding: 2px 0 0 12px; } + .NB-modal-intro .NB-intro-starredimport-info { font-size: 13px; padding: 0 0 10px; display: block; } + .NB-modal-intro a.NB-goodies-bookmarklet-button { float: left; margin: 2px 0 0; } + .NB-modal-intro .NB-intro-imports-progress .NB-loading { margin: 24px auto -24px; float: none; } + .NB-modal-intro .NB-intro-imports-progress h4 { margin: 4px 0; } + .NB-modal-intro .NB-intro-imports-sites h4 { margin: 4px 0; } + .NB-modal-intro .NB-intro-imports-sites h6 { text-align: center; font-size: 16px; } + .NB-modal-intro .NB-intro-imports-sites h6.NB-error { font-size: 13px; } + .NB-modal-intro .NB-intro-imports-sites .NB-modal-submit-button { margin: 0 auto 18px; text-align: center; @@ -9678,12 +10919,14 @@ form.opml_import_form input { .NB-modal-intro .NB-page-3-started { margin: 4px; } + .NB-modal-intro .NB-intro-services .NB-error { display: block; clear: both; margin: 12px 24px; float: left; } + .NB-modal-intro .NB-friends-service { width: 50%; -webkit-box-sizing: border-box; @@ -9694,15 +10937,19 @@ form.opml_import_form input { padding: 12px; overflow: hidden; } + .NB-modal-intro .NB-friends-service h3 { margin: 12px 0 24px; } + .NB-modal-intro .NB-friends-service .NB-module-content-header { margin: 0; } + .NB-modal-intro .NB-friends-service .NB-friends-service-title { margin-bottom: 20px; } + .NB-modal-intro .NB-friends-service .NB-modal-submit-button { margin: 0; font-weight: bold; @@ -9713,28 +10960,34 @@ form.opml_import_form input { overflow: hidden; line-height: 11px; } + .NB-modal-intro .NB-friends-service-facebook { position: relative; width: 50%; } + .NB-modal-intro .NB-friends-autofollow { text-align: center; font-size: 11px; margin: 0 0 24px; } + .NB-modal-intro .NB-friends-autofollow input[type=checkbox] { margin: 0 6px 0 0; } + .NB-modal-intro .NB-friends-service .NB-modal-submit-button img, .NB-modal-intro .NB-friends-service-connected img { vertical-align: text-bottom; margin: 0 8px 0 0; } + .NB-modal-intro .NB-friends-service-connected { font-weight: bold; font-size: 12px; color: darkgreen; } + .NB-modal-intro .NB-intro-services .NB-note { display: block; clear: both; @@ -9743,6 +10996,7 @@ form.opml_import_form input { font-size: 11px; text-align: center; } + .NB-modal-intro .NB-services-stats { border-top: 1px solid #E6E6E6; padding: 8px 20% 0; @@ -9751,32 +11005,38 @@ form.opml_import_form input { text-transform: uppercase; font-size: 13px; } + .NB-modal-intro .NB-intro-services-stats-count { float: left; width: 50%; } + .NB-modal-intro .NB-intro-services-stats-count-number { - clear: both; - font-size: 18px; - margin: 8px 0 0 0; - color: #0D003C; - text-shadow: 1px 1px 0 #E8E8E8; - font-weight: bold; + clear: both; + font-size: 18px; + margin: 8px 0 0 0; + color: #0D003C; + text-shadow: 1px 1px 0 #E8E8E8; + font-weight: bold; } + .NB-modal-intro .NB-intro-services-stats-count-description { - margin: 4px 0 0 0; - color: #808080; - clear: both; + margin: 4px 0 0 0; + color: #808080; + clear: both; } + .NB-modal-intro .NB-page-4-started { margin: 6px 0 18px; } + .NB-modal-intro .NB-intro-follows { border-radius: 6px; border: 1px solid #DFE2DA; background-color: #F7F8F5; width: 100%; } + .NB-modal-intro .NB-intro-uptodate-follow { width: 50%; text-align: center; @@ -9785,39 +11045,49 @@ form.opml_import_form input { border-bottom: 1px solid #DFE2DA; padding: 12px; } + .NB-modal-intro .NB-intro-uptodate-follow:last-child { border-right: none; } + .NB-modal-intro .NB-intro-uptodate-subscribe .NB-intro-uptodate-follow { border-bottom: none; } + .NB-modal-intro .NB-intro-uptodate-follow iframe { margin: 6px -76px 0 40px; } + .NB-modal-intro .NB-intro-uptodate-follow .NB-modal-submit-button { width: 100px; text-shadow: none; margin: 12px auto 6px; } + .NB-modal-intro .NB-intro-uptodate-follow .NB-subscribed, .NB-modal-intro .NB-intro-uptodate-follow.NB-active .NB-modal-submit-button { display: none; clear: both; } + .NB-modal-intro .NB-intro-uptodate-follow .NB-subscribed { margin: 12px auto 6px; height: 19px; } + .NB-modal-intro .NB-intro-uptodate-follow.NB-active .NB-subscribed { display: inline-block; } + .NB-modal-intro .NB-intro-uptodate-follow input { float: left; margin: 26px 8px 0; } + .NB-modal-intro .NB-intro-uptodate-follow span { font-weight: bold; } + .NB-modal-intro .NB-intro-uptodate-follow img { margin: 4px 12px; width: 48px; @@ -9825,6 +11095,7 @@ form.opml_import_form input { display: inline-block; vertical-align: middle; } + .NB-modal-intro .NB-intro-uptodate-subscribe img { width: 16px; height: 16px; @@ -9832,10 +11103,12 @@ form.opml_import_form input { float: none; vertical-align: top; } + .NB-modal-intro .NB-intro-uptodate-follow label { display: block; padding: 4px 2px; } + .NB-modal-intro .NB-intro-uptodate-follow .NB-intro-uptodate-newwindow { width: 10px; height: 10px; @@ -9843,9 +11116,11 @@ form.opml_import_form input { margin-top: 22px; display: none; } + .NB-modal-intro .NB-intro-uptodate-follow-twitter.NB-intro-uptodate-twitter-inactive .NB-intro-uptodate-newwindow { display: block; } + .NB-modal-intro .NB-intro-uptodate-follow-twitter.NB-intro-uptodate-twitter-inactive input { display: none; } @@ -9858,6 +11133,7 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-modal-intro .NB-intro-module.NB-intro-categories-container { height: 200px; overflow: auto; @@ -9866,16 +11142,17 @@ form.opml_import_form input { box-sizing: border-box; } -.NB-modal-intro .NB-intro-categories { -} +.NB-modal-intro .NB-intro-categories {} .NB-intro-categories .NB-category { margin: 0 0 12px; cursor: pointer; } + .NB-intro-categories .NB-category .NB-category-title { width: auto; } + .NB-intro-categories .NB-category .NB-category-title .NB-checkmark { background: transparent url(/media/embed/reader/checkmark.png) no-repeat 0 0; background-size: 14px 13px; @@ -9886,9 +11163,11 @@ form.opml_import_form input { top: 3px; display: none; } + .NB-intro-categories .NB-category.NB-active .NB-category-title .NB-checkmark { display: block; } + .NB-intro-categories .NB-category-feed { text-align: left; height: 18px; @@ -9899,9 +11178,11 @@ form.opml_import_form input { margin: 1px 4px 0; opacity: .5; } + .NB-intro-categories .NB-category.NB-active .NB-category-feed { opacity: 1; } + .NB-intro-categories .NB-category-feed img { float: left; width: 16px; @@ -9915,91 +11196,107 @@ form.opml_import_form input { /* ============ */ .carousel { - position: relative; + position: relative; } + .carousel-inner { - overflow: hidden; - width: 100%; - position: relative; + overflow: hidden; + width: 100%; + position: relative; } + .carousel .item { - display: none; - position: relative; - -webkit-transition: 0.6s ease-in-out left; - -moz-transition: 0.6s ease-in-out left; - -ms-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -ms-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; } -.carousel .item > img { - display: block; - line-height: 1; + +.carousel .item>img { + display: block; + line-height: 1; } + .carousel .active, .carousel .next, .carousel .prev { - display: block; + display: block; } + .carousel .active { - left: 0; + left: 0; } + .carousel .next, .carousel .prev { - position: absolute; - top: 0; - width: 100%; + position: absolute; + top: 0; + width: 100%; } + .carousel .next { - left: 100%; + left: 100%; } + .carousel .prev { - left: -100%; + left: -100%; } + .carousel .next.left, .carousel .prev.right { - left: 0; + left: 0; } + .carousel .active.left { - left: -100%; + left: -100%; } + .carousel .active.right { - left: 100%; + left: 100%; } + .carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: #ffffff; - text-align: center; - background: #222222; - border: 3px solid #ffffff; - border-radius: 23px; - opacity: 0.5; - filter: alpha(opacity=50); -} -.carousel-control.right { - left: auto; - right: 15px; -} -.carousel-control:hover { - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + left: auto; + right: 15px; +} + +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); } + .carousel-caption { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 10px 15px 5px; - background: #333333; - background: rgba(0, 0, 0, 0.75); + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 10px 15px 5px; + background: #333333; + background: rgba(0, 0, 0, 0.75); } + .carousel-caption h4, .carousel-caption p { - color: #ffffff; + color: #ffffff; } /* ========================= */ @@ -10009,36 +11306,45 @@ form.opml_import_form input { .NB-modal-exception .NB-exception-only { display: inline; } + .NB-modal-exception .NB-exception-block-only { display: block; } + .NB-modal-exception.NB-modal-feed-settings .NB-modal-title, .NB-modal-exception.NB-modal-folder-settings .NB-modal-title { display: block; } + .NB-modal-exception .NB-modal-title.NB-exception-block-only { display: block; } + .NB-modal-exception.NB-modal-folder-settings .NB-exception-block-only, .NB-modal-exception.NB-modal-feed-settings .NB-exception-block-only { display: none; } + .NB-modal-exception.NB-modal-feed-settings .NB-exception-only, .NB-modal-exception.NB-modal-folder-settings .NB-exception-only, .NB-modal-exception.NB-modal-feed-settings .NB-exception-block-only, .NB-modal-exception.NB-modal-folder-settings .NB-exception-block-only { display: none; } + .NB-modal-exception .NB-settings-only { display: none; } + .NB-modal-exception.NB-modal-folder-settings .NB-settings-only, .NB-modal-exception.NB-modal-feed-settings .NB-settings-only { display: block; } + .NB-modal-exception .NB-modal-title { display: none; } + .NB-modal-exception .NB-exception-explanation { color: #606060; font-size: 12px; @@ -10046,29 +11352,30 @@ form.opml_import_form input { } .NB-modal-exception .NB-exception-option-option { - color: #A0A0A0; - padding: 0 8px 0 0; + color: #A0A0A0; + padding: 0 8px 0 0; } .NB-modal-exception .NB-exception-option-meta { - float: right; - font-size: 11px; - font-weight: bold; - padding: 2px 0 0 0; + float: right; + font-size: 11px; + font-weight: bold; + padding: 2px 0 0 0; } .NB-modal-exception .NB-exception-option-meta-recommended { - color: #4A9937; + color: #4A9937; } .NB-modal-exception .NB-modal-submit input.NB-modal-submit-green { - margin-bottom: 6px; + margin-bottom: 6px; } .NB-modal-exception .NB-exception-input-wrapper { position: relative; margin: 4px 0 0; } + .NB-modal-exception .NB-exception-label { position: absolute; top: 2px; @@ -10076,21 +11383,26 @@ form.opml_import_form input { } .NB-modal-exception input[type=text] { - width: 400px; - margin: 0 0 12px 104px; + width: 400px; + margin: 0 0 12px 104px; } + .NB-modal-exception .NB-modal-submit-button { float: left; } + .NB-modal-exception .NB-exception-submit-wrapper { margin: 0 0 2px 100px; } + .NB-modal-exception .NB-modal-loading { margin: 6px 8px 0; } + .NB-modal-exception .NB-fieldset-fields .NB-error { padding: 6px 0 6px 4px; } + .NB-modal-feed-settings .NB-exception-option-status, .NB-modal-folder-settings .NB-exception-option-status { color: #3945C0; @@ -10106,6 +11418,7 @@ form.opml_import_form input { width: 102px; clear: both; } + .NB-modal-notifications .NB-preference-options, .NB-modal-feed-settings .NB-preference-options, .NB-modal-folder-settings .NB-preference-options { @@ -10113,17 +11426,19 @@ form.opml_import_form input { float: left; overflow: hidden; } + .NB-modal-folder-settings .NB-premium-only .NB-premium-only-divider { - background: transparent url(/media/embed/reader/separator_small.png) no-repeat 50% 100%; - height: 20px; - width: 100%; - margin: 4px 0 0; + background: transparent url(/media/embed/reader/separator_small.png) no-repeat 50% 100%; + height: 20px; + width: 100%; + margin: 4px 0 0; } + .NB-modal-folder-settings .NB-premium-only .NB-premium-only-text { - text-align: center; - margin: 6px 0 8px 0; - color: #707070; - font-size: 11px; + text-align: center; + margin: 6px 0 8px 0; + color: #707070; + font-size: 11px; } /* ======================= */ @@ -10135,16 +11450,20 @@ form.opml_import_form input { background-size: 28px; filter: hue-rotate(320deg) saturate(18); } + .NB-modal.NB-modal-notifications .NB-fieldset { border-bottom: none; width: 100%; } + .NB-modal-notifications .NB-preference-label { clear: none; } + .NB-modal-notifications .NB-preference-options { float: right; } + .NB-modal-notifications .NB-modal-section-site { margin: 12px 0 0; } @@ -10152,6 +11471,7 @@ form.opml_import_form input { .NB-feedbar-options-notifications .NB-feed-notification { padding: 0; } + .NB-feed-notification { width: 100%; overflow: hidden; @@ -10159,6 +11479,7 @@ form.opml_import_form input { padding: 10px 0; border-bottom: 1px solid #F0F0F0; } + .NB-feed-notification:last-child { border-bottom: none; } @@ -10167,6 +11488,7 @@ form.opml_import_form input { font-size: 12px; padding: 0 260px 0 24px; } + .NB-feed-notification .NB-feed-icon { width: 16px; height: 16px; @@ -10174,6 +11496,7 @@ form.opml_import_form input { top: 10px; left: 0; } + .NB-feed-notification .NB-feed-frequency-icon { float: left; clear: left; @@ -10181,6 +11504,7 @@ form.opml_import_form input { height: 16px; margin: 4px 7px 0 0; } + .NB-feed-notification .NB-feed-frequency { float: left; color: #A0A0A0; @@ -10193,10 +11517,12 @@ form.opml_import_form input { top: 0; right: 0; } + .NB-feed-notification .NB-feed-notification-filter { float: right; margin: 0 12px; } + .NB-feed-notification .NB-feed-notification-filter .NB-unread-icon, .NB-feed-notification .NB-feed-notification-filter .NB-focus-icon { width: 8px; @@ -10207,25 +11533,31 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 0 center; background-size: 8px; } + .NB-feed-notification .NB-feed-notification-filter .NB-focus-icon { background: transparent url('/media/embed/icons/nouns/indicator-focus.svg') no-repeat 0 center; - background-size: 8px; + background-size: 8px; } + .NB-feed-notification .segmented-control { margin: 0 0 4px 0; } + .NB-feed-notification .segmented-control li { padding: 2px 6px; font-size: 10px; min-width: 36px; } + .NB-feed-notification .segmented-control li:not(.NB-active) { color: #A0A0A0; } + .NB-feed-notification .NB-feed-notification-types { float: right; clear: right; } + .NB-feed-notification .NB-feed-notification-filter-focus { width: 82px; } @@ -10246,6 +11578,7 @@ form.opml_import_form input { text-shadow: 1px 1px 0 #F0F0F0; width: 715px; } + .NB-modal-feedchooser .NB-modal-subtitle b { padding-right: 8px; color: #303030; @@ -10254,6 +11587,7 @@ form.opml_import_form input { .NB-modal-feedchooser .NB-feedchooser-subtitle-type-prefix { color: #C0C0C0; } + .NB-modal-feedchooser .NB-feedchooser-subtitle-type-price { color: #C0C0C0; float: right; @@ -10274,6 +11608,7 @@ form.opml_import_form input { position: relative; border-left: 1px solid #B0B0B0; } + .NB-modal-feedchooser .NB-feedchooser-type.NB-right .NB-feedchooser-info { clear: both; } @@ -10283,6 +11618,7 @@ form.opml_import_form input { width: auto; margin: 0 auto; } + .NB-modal-feedchooser .NB-modal-title .NB-icon { background: transparent url('/media/embed/icons/circular/g_icn_mute.png'); background-size: 28px; @@ -10310,9 +11646,11 @@ form.opml_import_form input { top: 50%; left: -17px } + .NB-modal-feedchooser .NB-feedchooser-info { overflow: hidden; } + .NB-modal-feedchooser .NB-feedchooser-info-type { padding: 20px 16px; background-color: #505050; @@ -10323,16 +11661,19 @@ form.opml_import_form input { text-transform: uppercase; text-align: left; } + .NB-modal-feedchooser .NB-feedchooser-premium-plan { overflow: hidden; border-radius: 6px 6px 0px 0; - box-shadow: 0 1px 2px rgba(0,0,0,0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); margin: 0 0 28px; background-color: #F7F7F5; } + .NB-modal-feedchooser .NB-feedchooser-premium-plan:last-child { margin-bottom: 0; } + .NB-modal-feedchooser .NB-feedchooser-info-counts { float: right; font-size: 14px; @@ -10340,28 +11681,32 @@ form.opml_import_form input { color: #427700; margin: 12px 0 1px; font-weight: bold; - text-shadow:1px 1px 0 #F0F0F0; + text-shadow: 1px 1px 0 #F0F0F0; } + .NB-modal-feedchooser .NB-feedchooser-info-sort { font-size: 12px; line-height: 16px; color: #C0C0C0; margin: 12px 0 0; font-weight: bold; - text-shadow:1px 1px 0 #F6F6F6; + text-shadow: 1px 1px 0 #F6F6F6; } + .NB-modal-feedchooser .NB-feedchooser-info-reset { font-size: 12px; line-height: 16px; margin: 12px 0 0; font-weight: bold; - text-shadow:1px 1px 0 #F6F6F6; + text-shadow: 1px 1px 0 #F6F6F6; display: none; cursor: pointer; } + .NB-modal-feedchooser .NB-feedchooser-info-counts.NB-full { -/* color: #5090D0;*/ + /* color: #5090D0;*/ } + .NB-modal-feedchooser .NB-feedchooser-info-counts.NB-error { display: block; padding: 0; @@ -10371,40 +11716,39 @@ form.opml_import_form input { .NB-modal-feedchooser .NB-modal-subtitle { width: auto; } + .NB-modal-feedchooser .NB-feedchooser { background-color: #F7F8F5; overflow-y: auto; font-size: 11px; list-style: none; -/* margin: 12px 0;*/ + /* margin: 12px 0;*/ padding: 0; max-height: 742px; min-height: 186px; width: auto; border: 1px solid #909090; } + .NB-modal-feedchooser .NB-feedchooser .NB-hidden { display: block; } + .NB-modal-feedchooser .NB-modal-submit.NB-modal-submit-paypal { border-radius: 3px; border: 1px solid #ebc17c; padding: 0 8px 4px 0px; background-color: #FFED68; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.16, #fff5cc), - color-stop(0.84, #fff7d4) - ); - background-image: -moz-linear-gradient( - center bottom, - #fff5cc 16%, - #fff7d4 84% - ); - -moz-box-shadow:0 2px 0px #ebc17c; - box-shadow:0 2px 0px #ebc17c; + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.16, #fff5cc), + color-stop(0.84, #fff7d4)); + background-image: -moz-linear-gradient(center bottom, + #fff5cc 16%, + #fff7d4 84%); + -moz-box-shadow: 0 2px 0px #ebc17c; + box-shadow: 0 2px 0px #ebc17c; } @@ -10421,6 +11765,7 @@ form.opml_import_form input { .NB-modal-feedchooser .NB-feedchooser-paypal img { margin: 0 auto; } + .NB-modal-feedchooser .NB-feedchooser-stripe { min-height: 48px; width: 44%; @@ -10433,24 +11778,28 @@ form.opml_import_form input { } .NB-modal-feedchooser .NB-feedchooser-stripe .NB-modal-submit-green { -/* -moz-box-shadow:0 2px 0 #1a5e0e;*/ -/* box-shadow:0 2px 0 #1a5e0e;*/ + /* -moz-box-shadow:0 2px 0 #1a5e0e;*/ + /* box-shadow:0 2px 0 #1a5e0e;*/ } + .NB-modal-feedchooser .NB-premium-prorate-message { font-size: 1em; - color: rgba(0,0,0,0.5); + color: rgba(0, 0, 0, 0.5); text-transform: none; padding: 6px 32px 0; } + .NB-modal-feedchooser .NB-creditcards { width: 100%; margin: 8px 0 0; } + .NB-modal-feedchooser .NB-creditcards img { width: 24px; height: 18px; margin: 0 2px 0 0; } + .NB-modal-feedchooser .NB-feedchooser-dollar { margin: 0px auto; padding: 4px 0 4px 2px; @@ -10458,22 +11807,24 @@ form.opml_import_form input { } .NB-modal-feedchooser .NB-feedchooser-dollar-value { -/* padding: 4px 0 4px 36px;*/ + /* padding: 4px 0 4px 36px;*/ padding: 4px 0 4px 0px; text-shadow: 1px 1px 0 #FFF8B1; clear: both; position: relative; color: #9BA6CC; display: none; - + -webkit-transition: all .24s ease-out; -moz-transition: all .24s ease-out; -o-transition: all .24s ease-out; -ms-transition: all .24s ease-out; } + .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected { display: block; } + .NB-modal-feedchooser .NB-feedchooser-dollar-value:hover, .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected { color: #727BA0; @@ -10488,13 +11839,14 @@ form.opml_import_form input { left: -40px; display: block; opacity: 0.1; - + -webkit-transition: all .24s ease-out; -moz-transition: all .24s ease-out; -o-transition: all .24s ease-out; -ms-transition: all .24s ease-out; } + .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-selected.NB-1 .NB-feedchooser-dollar-image, .NB-modal-feedchooser .NB-feedchooser-dollar-value.NB-1:hover .NB-feedchooser-dollar-image { opacity: 1; @@ -10527,16 +11879,20 @@ form.opml_import_form input { color: #86899F; display: inline; } + .NB-modal-feedchooser .NB-selected .NB-feedchooser-dollar-year { color: #455596; } + .NB-modal-feedchooser .NB-feedchooser-payextra { margin: 6px 0; } + .NB-modal-feedchooser #NB-feedchooser-payextra-checkbox { margin: 0 6px; cursor: pointer; } + .NB-modal-feedchooser .NB-feedchooser-payextra label { cursor: pointer; } @@ -10549,38 +11905,48 @@ form.opml_import_form input { .NB-modal-feedchooser #NB-feedchooser-feeds .feed { display: block; } + .NB-modal-feedchooser .NB-feed-organizer-sort { display: none; } + .NB-modal-feedchooser .feed.NB-highlighted .feed_title { font-weight: bold; } + .NB-modal-feedchooser .feed { font-weight: normal; } + .NB-modal-feedchooser .feed:not(.NB-highlighted) .feed_favicon { - opacity: .3; + opacity: .3; } + .NB-modal-feedchooser .feed:not(.NB-highlighted) .feed_title { - color: #808080; + color: #808080; } + .NB-modal-feedchooser .feed .feed_counts .unread_count_negative, .NB-modal-feedchooser .feed .feed_counts .unread_count_positive { display: none; } + .NB-modal-feedchooser .feed.NB-highlighted .feed_counts, .NB-modal-feedchooser .feed.NB-highlighted .feed_counts .unread_count_positive { display: block; } + .NB-modal-feedchooser .feed .feed_counts, .NB-modal-feedchooser .feed .feed_counts .unread_count_negative { display: block; } + .NB-modal-feedchooser .feed.NB-highlighted .feed_counts .unread_count_negative { display: none; } + .NB-modal-feedchooser .NB-feedlist .folder_title .feed_counts_floater { - display: none; + display: none; } .NB-modal-feedchooser .NB-feedlist .feed .NB-feedlist-manage-icon:hover { @@ -10600,20 +11966,21 @@ form.opml_import_form input { .NB-modal-feedchooser .NB-feedlist .folder.NB-hover-inverse .NB-feedlist-manage-icon { background: none; } + .NB-modal-feedchooser .NB-feedlist .folder_title:hover .NB-folder-icon { display: block !important; } .NB-modal-feedchooser .NB-feedlist .folder .NB-feedlist-collapse-icon { - display: none !important; + display: none !important; } .NB-modal-feedchooser .unread_count_positive { - color: yellow; + color: yellow; } .NB-modal-feedchooser .unread_count_negative { - color: orange; + color: orange; } .NB-modal-feedchooser .NB-modal-submit { @@ -10628,6 +11995,7 @@ form.opml_import_form input { padding: 12px; background-color: #FFF; } + .NB-modal-feedchooser .NB-modal-submit-add { display: none; } @@ -10648,6 +12016,7 @@ form.opml_import_form input { border-top: 1px solid white; position: relative; } + .NB-modal-feedchooser .NB-feedchooser-premium-bullets li:first-child { border-top: none; } @@ -10659,6 +12028,7 @@ form.opml_import_form input { width: 16px; height: 16px; } + .NB-welcome-premium-bullet { width: 16px; height: 16px; @@ -10672,46 +12042,55 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/icons8/icons8-sheets-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-2 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-2 { background: transparent url('/media/embed/icons/icons8/icons8-lightning-bolt-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-3 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-3 { background: transparent url('/media/embed/icons/icons8/icons8-comics-magazine-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-4 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-4 { background: transparent url('/media/embed/icons/icons8/icons8-search-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-5 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-5 { background: transparent url('/media/embed/icons/icons8/icons8-tags-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-6 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-6 { background: transparent url('/media/embed/icons/icons8/icons8-security-wi-fi-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-7 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-7 { background: transparent url('/media/embed/icons/icons8/icons8-rss-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-8 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-8 { background: transparent url('/media/embed/icons/icons8/icons8-activity-history-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets li.NB-9 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-bullet.NB-9 { background: transparent url('/media/embed/icons/icons8/icons8-knife-and-spatchula-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-bullets .NB-feedchooser-premium-poor-hungry-dog { display: block; margin: 12px auto 4px; @@ -10719,39 +12098,47 @@ form.opml_import_form input { width: 128px; height: 96px; } + .NB-feedchooser-premium-archive-bullets li.NB-1 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-archive-bullet.NB-1 { background: transparent url('/media/embed/icons/icons8/icons8-bursts-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-archive-bullets li.NB-2 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-archive-bullet.NB-2 { background: transparent url('/media/embed/icons/icons8/icons8-relax-with-book-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-archive-bullets li.NB-3 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-archive-bullet.NB-3 { background: transparent url('/media/embed/icons/icons8/icons8-filing-cabinet-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-archive-bullets li.NB-4 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-archive-bullet.NB-4 { background: transparent url('/media/embed/icons/icons8/icons8-quadcopter-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-archive-bullets li.NB-5 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-archive-bullet.NB-5 { background: transparent url('/media/embed/icons/icons8/icons8-rss-100.png') no-repeat 0 0; background-size: 16px; } + .NB-feedchooser-premium-archive-bullets li.NB-6 .NB-feedchooser-premium-bullet-image, .NB-welcome-premium-archive-bullet.NB-6 { background: transparent url('/media/embed/icons/icons8/icons8-calendar-100.png') no-repeat 0 0; background-size: 16px; } + .NB-modal-feedchooser .NB-payment-providers { margin: 0 24px 24px; } + .NB-modal-feedchooser .NB-provider-main, .NB-modal-feedchooser .NB-provider-alternate { text-align: center; @@ -10760,26 +12147,32 @@ form.opml_import_form input { font-size: 0.7em; color: #838b82; } + .NB-modal-feedchooser .NB-provider-text { text-transform: uppercase; } + .NB-modal-feedchooser .NB-provider-main .NB-modal-submit-button, .NB-modal-feedchooser .NB-provider-alternate .NB-modal-submit-button { padding-top: 12px; padding-bottom: 12px; } + .NB-modal-feedchooser .NB-paypal-button { margin-top: 6px; } + .NB-modal-feedchooser .NB-feedchooser-premium-upgrade { margin-top: 24px; overflow: hidden; display: flex; align-items: center; } + .NB-modal-feedchooser .NB-feedchooser-premium-upgrade:first-child { margin-top: 0; } + .NB-modal-feedchooser .NB-feedchooser-premium-already { margin: 0 24px 0; padding: 16px 16px; @@ -10793,29 +12186,34 @@ form.opml_import_form input { display: flex; align-items: center; } + .NB-modal-feedchooser .NB-provider-note { width: 100%; font-size: 0.75em; color: darkgray; margin-top: 12px; } + .NB-modal-feedchooser .NB-feedchooser-or-bar { flex-grow: 0; width: 1px; - background-color:rgba(0, 0, 0, .1); + background-color: rgba(0, 0, 0, .1); height: 64px; margin: 0 24px; } + .NB-modal-feedchooser .NB-feedchooser-premium-already-icon { background: transparent url('/media/embed/icons/circular/newuser_icn_setup.png') no-repeat center center; background-size: 28px; - width: 36px; + width: 36px; height: 36px; margin: 0 8px; } + .NB-modal-feedchooser .NB-feedchooser-premium-already-message { flex-grow: 1; } + /* ===================== */ /* = Newsletters Modal = */ /* ===================== */ @@ -10823,6 +12221,7 @@ form.opml_import_form input { .NB-modal-newsletters fieldset { margin: 32px 0; } + .NB-modal-newsletters .NB-modal-title .NB-icon { background: transparent url('/media/embed/icons/circular/g_modal_mail.png'); background-size: 28px; @@ -10833,18 +12232,21 @@ form.opml_import_form input { font-size: 18px; width: 80%; } + .NB-modal-newsletters p { margin-left: 24px; font-size: 14px; } + .NB-modal-newsletters .NB-newsletters-gmail { width: 700px; margin: 0 auto; display: block; padding: 12px; border: 1px solid #F0F0F0; - + } + /* ================= */ /* = Goodies Modal = */ /* ================= */ @@ -10852,26 +12254,28 @@ form.opml_import_form input { .NB-modal-goodies fieldset { margin: 32px 0 32px; } + .NB-modal-goodies .NB-modal-title .NB-icon { background: transparent url("/media/embed/icons/nouns/dialog-goodies.svg") no-repeat center center; background-size: 28px; filter: hue-rotate(186deg) saturate(18); } + .NB-modal-goodies .NB-goodies-group { - clear: both; - margin: 16px 0 0 0; - overflow: hidden; - background-color: #F7F7F5; - padding: 8px; + clear: both; + margin: 16px 0 0 0; + overflow: hidden; + background-color: #F7F7F5; + padding: 8px; } .NB-modal-goodies .NB-goodies-title { - font-size: 14px; - font-weight: bold; - text-transform: uppercase; - margin: 8px; - color: #404030; - text-shadow: 1px 1px 0 #E0E0E0; + font-size: 14px; + font-weight: bold; + text-transform: uppercase; + margin: 8px; + color: #404030; + text-shadow: 1px 1px 0 #E0E0E0; } .NB-modal-goodies .NB-goodies-bookmarklet-button, @@ -10891,99 +12295,111 @@ form.opml_import_form input { min-width: 62px; text-align: center; vertical-align: 2px; - color: #fff; - text-decoration: none; + color: #fff; + text-decoration: none; outline: none; margin: 8px 8px; } .NB-modal-goodies .NB-goodies-firefox-link { - float: right; + float: right; } + .NB-modal-goodies .NB-goodies-firefox { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/reader/firefox.png') no-repeat 0 0; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/reader/firefox.png') no-repeat 0 0; } .NB-modal-goodies .NB-modal-submit-button { - float: right; + float: right; } + .NB-modal-goodies .NB-goodies-safari { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/reader/safari.png') no-repeat 0 0; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/reader/safari.png') no-repeat 0 0; } + .NB-modal-goodies .NB-goodies-chrome-link { - float: right; + float: right; } + .NB-modal-goodies .NB-goodies-chrome { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/reader/chrome.png') no-repeat 0 0; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/reader/chrome.png') no-repeat 0 0; } + .NB-modal-goodies .NB-goodies-readkit { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/readkit.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/readkit.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-tafiti { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/tafiti.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/tafiti.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-reeder-ios { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/icon_reeder_ios.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/icon_reeder_ios.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-reeder-mac { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/icon_reeder_mac.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/icon_reeder_mac.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-leaf { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/icon_leaf.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/icon_leaf.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-unread-ios { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/icon_unread.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/icon_unread.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-bluree { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/bluree.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/bluree.png') no-repeat 0 0; + background-size: 28px; } + .NB-modal-goodies .NB-goodies-subtitle { margin: 14px 8px 4px; line-height: 18px; @@ -10991,52 +12407,58 @@ form.opml_import_form input { } .NB-modal-goodies .NB-goodies-custom-input { - float: right; - width: 320px; - border: 1px solid #909090; - padding: 4px; - margin: 6px 0 0 0; + float: right; + width: 320px; + border: 1px solid #909090; + padding: 4px; + margin: 6px 0 0 0; } + .NB-modal-goodies .NB-goodies-mobile-link { - float: right; + float: right; } + .NB-modal-goodies .NB-goodies-ios { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/apple.png') no-repeat 0 0; - background-size: 28px 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/apple.png') no-repeat 0 0; + background-size: 28px 28px; } + .NB-modal-goodies .NB-goodies-android { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/android.png') no-repeat 0 0; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/android.png') no-repeat 0 0; } + .NB-modal-goodies .NB-goodies-nokia { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/nokia.png') no-repeat 0 0; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/nokia.png') no-repeat 0 0; } + .NB-modal-goodies .NB-goodies-sailfish { - float: right; - width: 64px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/sailfish.png') no-repeat 0 0; - background-size: auto 28px; + float: right; + width: 64px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/sailfish.png') no-repeat 0 0; + background-size: auto 28px; } + .NB-modal-goodies .NB-goodies-windows { - float: right; - width: 28px; - height: 28px; - margin: 0 6px 0 0; - background: transparent url('/media/img/mobile/windows_phone_icon.png') no-repeat 0 0; - background-size: 28px; + float: right; + width: 28px; + height: 28px; + margin: 0 6px 0 0; + background: transparent url('/media/img/mobile/windows_phone_icon.png') no-repeat 0 0; + background-size: 28px; } /* ============================ */ @@ -11049,62 +12471,65 @@ form.opml_import_form input { background-size: 28px; filter: hue-rotate(139deg) saturate(10); } + .NB-modal-keyboard .NB-keyboard-group { - clear: both; - margin: 16px 0 0 0; - overflow: hidden; - background-color: #F0F0F0; - padding: 8px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + clear: both; + margin: 16px 0 0 0; + overflow: hidden; + background-color: #F0F0F0; + padding: 8px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + .NB-modal-keyboard .NB-keyboard-shortcut { - width: 50%; - float: left; - padding: 0 12px; - - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + width: 50%; + float: left; + padding: 0 12px; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .NB-keyboard-shortcut-key { - border-radius: 6px; - border-top: 1px solid #717171; - border-left: 1px solid #717171; - border-bottom: 1px solid #303030; - border-right: 1px solid #303030; - background-color: #505050; - color: #FFF; - text-shadow: 1px 1px 0 #000; - font-weight: bold; - padding: 4px 6px; - float: left; - margin: 0 4px 0 0; + border-radius: 6px; + border-top: 1px solid #717171; + border-left: 1px solid #717171; + border-bottom: 1px solid #303030; + border-right: 1px solid #303030; + background-color: #505050; + color: #FFF; + text-shadow: 1px 1px 0 #000; + font-weight: bold; + padding: 4px 6px; + float: left; + margin: 0 4px 0 0; } .NB-keyboard-shortcut-key span { - color: #A0A0A0; - padding: 0 4px; + color: #A0A0A0; + padding: 0 4px; } .NB-keyboard-shortcut-explanation { - float: right; - font-size: 14px; - font-weight: bold; - margin: 6px 12px 0 0; - color: #404030; + float: right; + font-size: 14px; + font-weight: bold; + margin: 6px 12px 0 0; + color: #404030; } + .NB-keyboard-shortcut-image { - clear: both; + clear: both; } .NB-keyboard-shortcut-image img { - margin: 8px 0 0 0; - border: 1px solid #202020; - width: 268px; + margin: 8px 0 0 0; + border: 1px solid #202020; + width: 268px; } /* =============== */ @@ -11114,17 +12539,21 @@ form.opml_import_form input { .NB-modal-preferences { overflow: hidden; } + .NB-modal-preferences .NB-modal-title .NB-icon { background: transparent url('/media/embed/icons/nouns/dialog-preferences.svg') no-repeat 0 0; background-size: 28px; filter: hue-rotate(320deg) saturate(17.5); } + .NB-modal-preferences .NB-modal-submit-button { float: left; } + .NB-modal-preferences .NB-modal-submit-form { margin-top: 12px; } + .NB-modal-preferences .NB-preferences-scroll { overflow: auto; max-height: 600px; @@ -11134,60 +12563,67 @@ form.opml_import_form input { .NB-modal-notifications .NB-preference, .NB-modal-preferences .NB-preference { - overflow: hidden; - margin: 12px 0 0; - border-bottom: 1px solid rgba(0, 0, 0, .05); - padding: 12px; - background-color: #F7F7F5; - border-radius: 4px; + overflow: hidden; + margin: 12px 0 0; + border-bottom: 1px solid rgba(0, 0, 0, .05); + padding: 12px; + background-color: #F7F7F5; + border-radius: 4px; } .NB-modal-preferences .NB-preference.NB-last { - margin-bottom: 0; + margin-bottom: 0; } .NB-modal-preferences .NB-preference .NB-preference-options { - float: right; - width: 420px; - overflow: hidden; - padding: 2px 0; + float: right; + width: 420px; + overflow: hidden; + padding: 2px 0; } + .NB-modal-preferences .NB-preference .NB-preference-label { - float: left; - width: 176px; - position: relative; + float: left; + width: 176px; + position: relative; } + .NB-modal-preferences .NB-preference .NB-preference-sublabel { - font-size: 11px; - color: #808080; - margin: 4px 30px 0px 0px; + font-size: 11px; + color: #808080; + margin: 4px 30px 0px 0px; } + .NB-modal-preferences .NB-preference .NB-preference-sublabel-link { - font-size: 11px; - margin: 4px 30px 0px 0px; + font-size: 11px; + margin: 4px 30px 0px 0px; } .NB-modal-preferences .NB-preference .NB-preference-options input { - clear: both; - float: left; + clear: both; + float: left; } + .NB-modal-preferences .NB-preference .NB-preference-options input[type=radio] { - margin-top: 2px; + margin-top: 2px; } + .NB-modal-preferences .NB-preference .NB-preference-options input[type=checkbox] { - margin-top: 2px; + margin-top: 2px; } + .NB-modal-preferences .NB-preference .NB-preference-options label { - padding-left: 4px; - margin: 0 0 4px 0; - float: left; - cursor: pointer; + padding-left: 4px; + margin: 0 0 4px 0; + float: left; + cursor: pointer; } + .NB-modal-preferences .NB-preference .NB-preference-error { - color: #83210A; - font-size: 11px; - margin-top: 4px; - font-weight: bold; + color: #83210A; + font-size: 11px; + margin-top: 4px; + font-weight: bold; } .NB-modal-preferences .NB-preferences-notpremium { @@ -11198,103 +12634,127 @@ form.opml_import_form input { border-radius: 3px; border-bottom: 1px solid rgba(0, 0, 0, .1); } + .NB-modal-preferences .NB-preference-story-pane-position input { - margin-top: 4px; + margin-top: 4px; } + .NB-modal-preferences .NB-preference-story-pane-position label img { - vertical-align: middle; - margin: -2px 6px 0 2px; + vertical-align: middle; + margin: -2px 6px 0 2px; } + .NB-modal-preferences .NB-preference-story-styling .NB-preference-story-styling-sans-serif { - font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; + font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; } + .NB-modal-preferences .NB-preference-story-styling .NB-preference-story-styling-serif { - font-family: "Palatino Linotype", Georgia, "URW Palladio L", "Century Schoolbook L", serif; + font-family: "Palatino Linotype", Georgia, "URW Palladio L", "Century Schoolbook L", serif; } + .NB-modal-preferences .NB-preference-story-size .NB-preference-options div { clear: both; overflow: hidden; margin: 1px 0 0 0; line-height: 18px; } + .NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-xs { - font-size: 11px; + font-size: 11px; } + .NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-s { - font-size: 12px; + font-size: 12px; } + .NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-m { - font-size: 13px; + font-size: 13px; } + .NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-l { - font-size: 14px; + font-size: 14px; } + .NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-xl { - font-size: 15px; + font-size: 15px; } + .NB-modal-preferences .NB-preference-window input { - margin-top: 4px; + margin-top: 4px; } + .NB-modal-preferences .NB-preference-window label img { - vertical-align: middle; - margin: -2px 6px 0 2px; + vertical-align: middle; + margin: -2px 6px 0 2px; } + .NB-modal-preferences .NB-preference-hidereadfeeds label img { - vertical-align: middle; - margin: -4px 6px 0 2px; + vertical-align: middle; + margin: -4px 6px 0 2px; } + .NB-modal-preferences .NB-preference-showstorychanges label img { - vertical-align: middle; - margin: -4px 6px 0 2px; - width: 16px; - height: 16px; + vertical-align: middle; + margin: -4px 6px 0 2px; + width: 16px; + height: 16px; } + .NB-modal-preferences .NB-preference-singlestory label img { - vertical-align: middle; - margin: -5px 6px 0 2px; + vertical-align: middle; + margin: -5px 6px 0 2px; } + .NB-modal-preferences .NB-preference-animations label img { - vertical-align: middle; - margin: -5px 6px 0 2px; + vertical-align: middle; + margin: -5px 6px 0 2px; } + .NB-modal-preferences .NB-preference-feedorder label img { - vertical-align: middle; - margin: -3px 6px 0 2px; + vertical-align: middle; + margin: -3px 6px 0 2px; } + .NB-modal-preferences .NB-preference-ssl label img { - vertical-align: middle; - margin: -5px 6px 0 2px; - width: 16px; - height: 16px; + vertical-align: middle; + margin: -5px 6px 0 2px; + width: 16px; + height: 16px; } + .NB-modal-preferences .NB-preference-password .NB-preference-option { - float: left; - margin: 0 12px 0 0; + float: left; + margin: 0 12px 0 0; } + .NB-modal-preferences .NB-preference-password label { - text-transform: uppercase; - font-size: 10px; - color: #505050; - margin: 0; + text-transform: uppercase; + font-size: 10px; + color: #505050; + margin: 0; } + .NB-modal-preferences .NB-preference-password input { width: 140px; font-size: 14px; padding: 2px; margin: 0px 4px 2px; border: 1px solid #606060; - -moz-box-shadow:2px 2px 0 #D0D0D0; - -webkit-box-shadow:2px 2px 0 #D0D0D0; - box-shadow:2px 2px 0 #D0D0D0; + -moz-box-shadow: 2px 2px 0 #D0D0D0; + -webkit-box-shadow: 2px 2px 0 #D0D0D0; + box-shadow: 2px 2px 0 #D0D0D0; } + .NB-modal-preferences .NB-preference-daysofunread .NB-tangle-daysofunread-control { margin-top: 4px; margin-left: 32px; width: 300px; } + .NB-modal-preferences .NB-preference-daysofunread .NB-preference-options label { float: none; } + .NB-modal-preferences .NB-preference-readstorydelay .NB-tangle-readstorydelay, .NB-modal-preferences .NB-preference-slider { display: inline-block; @@ -11302,66 +12762,84 @@ form.opml_import_form input { top: 2px; width: 100px; } + .NB-modal-preferences .NB-preference.NB-preference-story-share .NB-preference-option { - float: left; - margin: 0 8px 4px 0; + float: left; + margin: 0 8px 4px 0; } + .NB-modal-preferences .NB-preference.NB-preference-story-share input { - clear: none; + clear: none; } + .NB-modal-preferences .NB-preference.NB-preference-story-share label { - width: 16px; - height: 16px; - margin: 0 0 0 2px; + width: 16px; + height: 16px; + margin: 0 0 0 2px; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-twitter] { background: transparent url('/media/embed/reader/twitter_bird.png') no-repeat 0 0; background-size: 16px; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-facebook] { background: transparent url('/media/embed/reader/facebook.gif') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-readitlater] { background: transparent url('/media/embed/reader/pocket_ril.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-tumblr] { background: transparent url('/media/embed/reader/tumblr.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-blogger] { background: transparent url('/media/embed/reader/blogger.png') no-repeat 0 0; background-size: 16px; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-delicious] { background: transparent url('/media/embed/reader/delicious.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-pinboard] { background: transparent url('/media/embed/reader/pinboard.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-raindrop] { background: transparent url('/media/embed/reader/raindrop.svg') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-pinterest] { background: transparent url('/media/embed/reader/pinterest.png') no-repeat 0 0; background-size: 16px; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-buffer] { background: transparent url('/media/embed/reader/buffer.png') no-repeat 0 0; background-size: 16px; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-diigo] { background: transparent url('/media/embed/reader/diigo.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-evernote] { background: transparent url('/media/embed/reader/evernote.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-googleplus] { background: transparent url('/media/embed/reader/googleplus.png') no-repeat 0 0; } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-email] { background: transparent url("/media/embed/icons/nouns/email.svg") no-repeat center center; background-size: 18px; filter: hue-rotate(134deg) saturate(16); } + .NB-modal-preferences .NB-preference-story-share label[for=NB-preference-story-share-instapaper] { background: transparent url('/media/embed/reader/instapaper.png') no-repeat 0 0; } @@ -11374,23 +12852,26 @@ form.opml_import_form input { display: block; margin: 4px 0 12px 0; float: left; -/* height: 14px;*/ + /* height: 14px;*/ width: auto; overflow: hidden; text-align: center; line-height: 18px; } -.NB-modal-preferences .segmented-control:last-child { -} + +.NB-modal-preferences .segmented-control:last-child {} + .NB-modal-preferences .segmented-control li { clear: none; padding: 1px 12px 0; font-size: 12px; width: 100px; } + .NB-modal-preferences .NB-preference-autoopenfolder .NB-folders { max-width: 240px; } + .NB-modal-preferences .NB-preference .NB-preference-options.NB-view-settings input[type=radio] { margin: 2px 6px 0 2px; } @@ -11400,10 +12881,12 @@ form.opml_import_form input { float: left; margin: 0 6px 0 0; } + .NB-view-settings.NB-preference-options input[type=radio] { float: left; margin: 2px 6px 0 0px; } + .NB-view-settings.NB-preference-options label { margin: 0 0 4px 0; float: left; @@ -11412,9 +12895,10 @@ form.opml_import_form input { color: #303030; display: block; padding: 4px 6px; - border: 1px solid rgba(0,0,0,.2); + border: 1px solid rgba(0, 0, 0, .2); border-radius: 3px; } + .NB-preference-options.NB-view-settings img { float: left; width: 18px; @@ -11422,6 +12906,7 @@ form.opml_import_form input { padding: 1px 0 0 0; margin: 0 4px 0 0; } + .NB-preference-options.NB-view-settings .NB-view-title { margin: 0; padding: 1px 0 0 0; @@ -11443,6 +12928,7 @@ form.opml_import_form input { background-size: 28px; filter: hue-rotate(287deg) saturate(10.5); } + .NB-modal-account .NB-preference-username input, .NB-modal-account .NB-preference-email input { width: 306px; @@ -11450,25 +12936,29 @@ form.opml_import_form input { padding: 2px; margin: 0px 4px 2px; border: 1px solid #606060; - -moz-box-shadow:2px 2px 0 #D0D0D0; - -webkit-box-shadow:2px 2px 0 #D0D0D0; - box-shadow:2px 2px 0 #D0D0D0; + -moz-box-shadow: 2px 2px 0 #D0D0D0; + -webkit-box-shadow: 2px 2px 0 #D0D0D0; + box-shadow: 2px 2px 0 #D0D0D0; } + .NB-modal-preferences .NB-link-account-preferences { - float: right; - line-height: 30px; - text-decoration: none; + float: right; + line-height: 30px; + text-decoration: none; } + .NB-modal-account .NB-block { margin: 8px 0; display: block; overflow: hidden; } + .NB-account-payments { margin: 0; padding: 0; list-style: none; } + .NB-account-payments .NB-account-payment { clear: both; overflow: hidden; @@ -11477,44 +12967,54 @@ form.opml_import_form input { padding: 0 0 12px; border-bottom: 1px solid #E0E0E0; } + .NB-account-payments li:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } + .NB-account-payments .NB-payments-loading { padding: 0 0 0 20px; background: transparent url('/media/embed/reader/ring_spinner.svg') no-repeat left 3px top 3px; background-size: 16px; } + .NB-account-payments .NB-account-payment-date { float: left; width: 140px; font-weight: bold; } + .NB-account-payments .NB-account-payment-amount { float: left; width: 100px; } + .NB-account-payments .NB-account-payment-provider { float: left; width: 100px; } + .NB-account-payments .NB-scheduled .NB-account-payment-provider { color: darkgrey; } + .NB-account-payments .NB-scheduled.NB-canceled .NB-account-payment-provider, .NB-account-payments .NB-refunded .NB-account-payment-amount { text-decoration: line-through; color: darkred; } + .NB-modal-account .NB-error { clear: both; margin-top: 12px; } + .NB-modal-account .NB-account-premium-cancel { margin-top: 0; } + .NB-modal-account .NB-preference-saved-stories-date { margin-bottom: 12px; } @@ -11524,90 +13024,96 @@ form.opml_import_form input { /* ================ */ .NB-static { - overflow: auto; - padding: 78px 0 64px; - color: #303030; + overflow: auto; + padding: 78px 0 64px; + color: #303030; } + .NB-static h3 { line-height: 1.4em; } .NB-static .NB-splash-info { - position: fixed; - -webkit-backdrop-filter: blur(5px); - background-color: rgba(229, 233, 225, 0.7); + position: fixed; + -webkit-backdrop-filter: blur(5px); + background-color: rgba(229, 233, 225, 0.7); } .NB-static .NB-static-title { - top: 0; - left: 0; - position: fixed; - height: 55px; - font-size: 24px; - line-height: 60px; - margin: 0 2px 12px 0; - text-shadow: 1px 1px 0 #F6F6F6; - color: #1A008D; - padding: 0 0 0 12px; - z-index: 2; - font-weight: 300; - font-family: Helvetica; + top: 0; + left: 0; + position: fixed; + height: 55px; + font-size: 24px; + line-height: 60px; + margin: 0 2px 12px 0; + text-shadow: 1px 1px 0 #F6F6F6; + color: #1A008D; + padding: 0 0 0 12px; + z-index: 2; + font-weight: 300; + font-family: Helvetica; } .NB-static .NB-module .NB-module-title { - margin: 44px 0 44px 44px; - padding: 44px 0 44px 64px; - text-align: left; - border-left: 4px solid rgba(0, 0, 0, 0.1); + margin: 44px 0 44px 44px; + padding: 44px 0 44px 64px; + text-align: left; + border-left: 4px solid rgba(0, 0, 0, 0.1); } .NB-static .NB-module .NB-module-content { - width: 620px; - margin: 44px 14px 24px 88px; + width: 620px; + margin: 44px 14px 24px 88px; } .NB-static ul { - padding-left: 20px; - margin-left: 0; + padding-left: 20px; + margin-left: 0; } + .NB-static li { - margin-bottom: 6px; - list-style: square outside url('/media/embed/reader/static_bullet_white.png'); + margin-bottom: 6px; + list-style: square outside url('/media/embed/reader/static_bullet_white.png'); } .NB-static pre, .NB-static code { - background-color: ghostWhite; - padding: 0px 4px; - border: 1px solid #DEDEDE; - font: normal normal normal 13px/normal Menlo, Consolas, Monaco, Courier, monospace; - white-space: pre; + background-color: ghostWhite; + padding: 0px 4px; + border: 1px solid #DEDEDE; + font: normal normal normal 13px/normal Menlo, Consolas, Monaco, Courier, monospace; + white-space: pre; } + .NB-static pre { - width: 604px; - overflow-x: auto; - padding: 8px; + width: 604px; + overflow-x: auto; + padding: 8px; } -.NB-static pre > code { - background-color: inherit; - border: none; + +.NB-static pre>code { + background-color: inherit; + border: none; } .NB-static ul pre { - width: 584px; + width: 584px; } + .NB-static .NB-module-title-prefix { - color: #A0A0A0; - padding: 0 2px 0 0; + color: #A0A0A0; + padding: 0 2px 0 0; } .NB-static h3 { - margin-top: 36px; - font-size: 24px; + margin-top: 36px; + font-size: 24px; } + .NB-static h4 { - font-size: 16px; - margin: 24px 0 12px; + font-size: 16px; + margin: 24px 0 12px; } /* ============== */ @@ -11615,50 +13121,59 @@ form.opml_import_form input { /* ============== */ .NB-static-press-reviews li { - font-size: 12px; - margin: 0 0 12px 0; - line-height: 18px; + font-size: 12px; + margin: 0 0 12px 0; + line-height: 18px; } + .NB-static-press-reviews a { - display: block; - font-size: 14px; + display: block; + font-size: 14px; } + .NB-static-press-reviews .NB-press-publisher { - font-style: italic; + font-style: italic; } + .NB-static-press-reviews .NB-press-publisher img { - vertical-align: text-bottom; - width: 16px; - height: 16px; + vertical-align: text-bottom; + width: 16px; + height: 16px; } + .NB-static-press-reviews .NB-press-author { - font-weight: bold; -} + font-weight: bold; +} + .NB-static-press-reviews .NB-press-date { - color: #404040; + color: #404040; } + .NB-static-press .NB-press-screenshots { - overflow: hidden; - list-style: none; - margin: 0; - padding: 0; + overflow: hidden; + list-style: none; + margin: 0; + padding: 0; } + .NB-static-press .NB-press-screenshot { - float: left; - display: block; - width: 296px; - text-align: center; - margin: 0 24px 24px 0; - font-size: 16px; - line-height: 20px; + float: left; + display: block; + width: 296px; + text-align: center; + margin: 0 24px 24px 0; + font-size: 16px; + line-height: 20px; } + .NB-static-press .NB-press-screenshot.NB-last { - margin-right: 0; + margin-right: 0; } + .NB-static-press .NB-press-screenshot img { - width: 274px; - border: 1px solid #303030; - margin: 0 0 12px 0; + width: 274px; + border: 1px solid #303030; + margin: 0 0 12px 0; } /* ============== */ @@ -11667,97 +13182,103 @@ form.opml_import_form input { .NB-static-about .NB-module ul, .NB-static-faq .NB-module ul { - margin: 2px 12px 4px 0; - font-size: 15px; + margin: 2px 12px 4px 0; + font-size: 15px; } .NB-static-about .NB-module ul li, .NB-static-faq .NB-module ul li { - line-height: 22px; - margin-bottom: 12px; - list-style: none; + line-height: 22px; + margin-bottom: 12px; + list-style: none; } .NB-static-about .NB-module ul li.last, .NB-static-static .NB-module ul li.last { - margin-bottom: 0; + margin-bottom: 0; } .NB-static-about ul.NB-about-what li { - background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat 0 3px; - padding-left: 24px; + background: transparent url('/media/embed/icons/silk/bullet_blue.png') no-repeat 0 3px; + padding-left: 24px; } + .NB-static-about ul.NB-about-server li { - background: transparent url('/media/embed/icons/silk/bullet_yellow.png') no-repeat 0 3px; - padding-left: 24px; + background: transparent url('/media/embed/icons/silk/bullet_yellow.png') no-repeat 0 3px; + padding-left: 24px; } + .NB-static-about ul.NB-about-client li { - background: transparent url('/media/embed/icons/silk/bullet_red.png') no-repeat 0 3px; - padding-left: 24px; + background: transparent url('/media/embed/icons/silk/bullet_red.png') no-repeat 0 3px; + padding-left: 24px; } + .NB-static-about .NB-about-why li { - background: transparent url('/media/embed/icons/silk/bullet_orange.png') no-repeat 0 3px; - padding-left: 24px; + background: transparent url('/media/embed/icons/silk/bullet_orange.png') no-repeat 0 3px; + padding-left: 24px; } .NB-static-about .NB-about-who { overflow: hidden; } + .NB-static-about .NB-about-who li { float: left; margin: 0 24px 0 0; text-align: center; list-style: none; } + .NB-static-about .NB-about-who img { border: 1px solid #505050; margin: 12px 0; width: 170px; } + .NB-static-about .NB-about-tagline img { border: 1px solid #505050; width: 500px; } .NB-static-faq .NB-link-about-faq { - float: right; - line-height: 30px; + float: right; + line-height: 30px; } .NB-static-faq .NB-module ul li { - background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 0 6px; - background-size: 8px; - padding-left: 24px; + background: transparent url('/media/embed/icons/nouns/indicator-unread.svg') no-repeat 0 6px; + background-size: 8px; + padding-left: 24px; } .NB-static-faq ul li.NB-faq-question { - display: block; - font-weight: bold; - margin: 0 0 10px 0; + display: block; + font-weight: bold; + margin: 0 0 10px 0; } .NB-static-faq ul li.NB-faq-answer { - color: #404040; - margin: 0 0 36px 0; - background: none; + color: #404040; + margin: 0 0 36px 0; + background: none; } .NB-static-faq ul li .NB-faq-answer.last { - margin-bottom: 0; + margin-bottom: 0; } .NB-static-faq .NB-trainer-bullet { - vertical-align: top; - margin: 6px 4px; - width: 8px; - height: 8px; + vertical-align: top; + margin: 6px 4px; + width: 8px; + height: 8px; } .NB-static-faq .NB-faq-image { - float: right; - border-color:#C0C0C0 #A0A0A0 #A0A0A0 #C0C0C0; - border-style:solid; - border-width:1px; + float: right; + border-color: #C0C0C0 #A0A0A0 #A0A0A0 #C0C0C0; + border-style: solid; + border-width: 1px; } /* ======= */ @@ -11765,109 +13286,130 @@ form.opml_import_form input { /* ======= */ .NB-static-api .NB-anchor { - position:relative; - top: -120px; - display: block; + position: relative; + top: -120px; + display: block; } + .NB-static-api h3 { -/* margin-top: 64px;*/ + /* margin-top: 64px;*/ } + .NB-static-api h4 { - margin-top: 12px; + margin-top: 12px; } + .NB-static-api .NB-api-endpoint { -/* margin-left: 12px;*/ -/* padding-left: 18px;*/ + /* margin-left: 12px;*/ + /* padding-left: 18px;*/ } + .NB-static-api .NB-api-endpoint:first-child { - border-top: none; + border-top: none; } + .NB-static-api .NB-api-endpoint:last-child { - border-bottom: none; + border-bottom: none; } + .NB-static-api .optional, .NB-static-api .required { - padding: 2px 4px; - margin: 0 4px 0 0; - background-color: #98C892; - font-size: 8px; - color: white; - font-weight: bold; - text-transform: uppercase; + padding: 2px 4px; + margin: 0 4px 0 0; + background-color: #98C892; + font-size: 8px; + color: white; + font-weight: bold; + text-transform: uppercase; } + .NB-static-api .required { - background-color: #D9900F; + background-color: #D9900F; } + .NB-static-api p { - margin: 12px 0; + margin: 12px 0; } + .NB-static-api table { - border-spacing: 0 0; - margin: 24px 0 12px 20px; - background: transparent url('/media/embed/reader/static_bullet_white.png') no-repeat 0 0; - border-bottom: 1px solid #F6F6F6; + border-spacing: 0 0; + margin: 24px 0 12px 20px; + background: transparent url('/media/embed/reader/static_bullet_white.png') no-repeat 0 0; + border-bottom: 1px solid #F6F6F6; } .NB-static-api table th, .NB-static-api table td { - text-align: left; - vertical-align: top; - padding: 7px 8px 7px 8px; - margin: 0; + text-align: left; + vertical-align: top; + padding: 7px 8px 7px 8px; + margin: 0; } + .NB-static-api table td { - border-right: 1px solid #F6F6f6; - white-space: nowrap; -/* border-bottom: 1px solid #F6F6f6;*/ + border-right: 1px solid #F6F6f6; + white-space: nowrap; + /* border-bottom: 1px solid #F6F6f6;*/ } + .NB-static-api table td:last-child { - border-right: none; + border-right: none; } + .NB-static-api table th, .NB-static-api .NB-api-toc-header { - border-right: 1px solid #BBB; - border-bottom: 1px solid #BBB; - padding: 2px 8px; - color: #222; + border-right: 1px solid #BBB; + border-bottom: 1px solid #BBB; + padding: 2px 8px; + color: #222; } + .NB-static-api .NB-api-toc-header { - border-right: none; + border-right: none; } + .NB-static-api table th:last-child { - border-right: none; + border-right: none; } + .NB-static-api table td:first-child { - font-weight: bold; + font-weight: bold; } + .NB-static-api .NB-api-toc-header { - font-weight: bold; + font-weight: bold; } .NB-static-api .NB-api-toc-header { - margin-top: 24px; + margin-top: 24px; } + .NB-static-api .NB-api-toc { -/* margin-top: 8px;*/ + /* margin-top: 8px;*/ } + .NB-static-api .NB-api-toc li { - clear: both; - margin-bottom: 12px; + clear: both; + margin-bottom: 12px; } + .NB-static-api .NB-api-toc a { - display: block; -/* float: left;*/ - width: 50%; + display: block; + /* float: left;*/ + width: 50%; } + .NB-static-api .NB-api-toc .NB-api-link-desc { - text-align: right; - width: 50%; - float: right; - margin-bottom: 12px; - padding-top: 4px; + text-align: right; + width: 50%; + float: right; + margin-bottom: 12px; + padding-top: 4px; } + .NB-static-api .NB-api-endpoint-param-desc { - width: 100%; - white-space: normal; + width: 100%; + white-space: normal; } /* =============== */ @@ -11876,31 +13418,37 @@ form.opml_import_form input { .NB-static-iphone .NB-ios-mockup, .NB-static-iphone .NB-ios-main { - -webkit-transform : translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); } + .NB-static-iphone .NB-splash-info { z-index: 2; } + .NB-static-iphone .NB-ios-main { margin: 24px 36px 24px 0; text-align: center; } + .NB-static-iphone .NB-ios-title { font-size: 20px; margin: 0 0 24px; text-align: center; } + .NB-static-iphone .NB-ios-subtitle { font-size: 16px; text-align: center; margin: 8px 0 0; color: #6D7D88; } + .NB-static-iphone .NB-ios-stripe-wrapper { border-top: 1px solid #505050; - border-bottom: 1px solid #505050; + border-bottom: 1px solid #505050; margin-right: -36px; } + .NB-static-iphone .NB-ios-stripe { border-top: 1px solid white; border-bottom: 1px solid white; @@ -11908,23 +13456,20 @@ form.opml_import_form input { text-align: center; background: #F0F0F0 url('/media/embed/reader/stripe_background.png'); } + .NB-static-iphone .NB-ios-download button { padding: 4px 20px 4px 10px; letter-spacing: -0.03em; text-align: center; background-color: #3A8FCE; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.06, #1E3C54), - color-stop(0.94, #648295) - ); - background-image: -moz-linear-gradient( - center bottom, - #1E3C54 6%, - #648295 94% - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.06, #1E3C54), + color-stop(0.94, #648295)); + background-image: -moz-linear-gradient(center bottom, + #1E3C54 6%, + #648295 94%); color: white; text-shadow: 0 -1px 0px #101C3B; border: 1px solid #2F6EA7; @@ -11936,44 +13481,40 @@ form.opml_import_form input { cursor: pointer; } + .NB-static-iphone .NB-ios-download .NB-big { font-size: 1.45em; font-weight: bold; } + .NB-static-iphone .NB-ios-download img { float: left; margin: 0 8px 0 0; } + .NB-static-iphone .NB-ios-download button:hover { background-color: #3A8FCE; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.06, #182A42), - color-stop(0.94, #516A83) - ); - background-image: -moz-linear-gradient( - center bottom, - #182A42 6%, - #516A83 94% - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.06, #182A42), + color-stop(0.94, #516A83)); + background-image: -moz-linear-gradient(center bottom, + #182A42 6%, + #516A83 94%); color: #C0D7E7; } + .NB-static-iphone .NB-ios-download button:active { background-color: #3A8FCE; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.06, #516A83), - color-stop(0.94, #182A42) - ); - background-image: -moz-linear-gradient( - center bottom, - #516A83 6% - #182A42 94%, - ); + background-image: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0.06, #516A83), + color-stop(0.94, #182A42)); + background-image: -moz-linear-gradient(center bottom, + #516A83 6% #182A42 94%, + ); } .NB-static-iphone .NB-ios-features { @@ -11988,6 +13529,7 @@ form.opml_import_form input { overflow: hidden; display: inline-block; } + .NB-static-iphone .NB-ios-features li { list-style-image: none; list-style-position: outside; @@ -11999,32 +13541,38 @@ form.opml_import_form input { position: relative; width: 158px; } + .NB-static-iphone .NB-ios-features .NB-ios-feature img { border: 1px solid #202020; border-color: #909090 #808080 #505050 #606060; position: absolute; } + .NB-static-iphone .NB-ios-features .NB-ios-feature img.NB-ios-iphone-screenshot { width: 60px; height: 110px; top: 64px; left: 6px; } + .NB-static-iphone .NB-ios-features .NB-ios-feature img.NB-ios-ipad-screenshot { width: 122px; height: 160px; top: 6px; left: 30px; } + .NB-static-iphone .NB-ios-features .NB-ios-feature .NB-ios-feature-title { font-weight: bold; margin: 184px 0 12px; width: 100%; text-align: center; } + .NB-static-iphone .NB-ios-features .NB-ios-feature .NB-ios-feature-subtitle { margin: 8px 0 0; } + .NB-static-iphone .NB-ios-main .NB-ios-feature { -webkit-transition: all .32s ease-out; -moz-transition: all .32s ease-out; @@ -12033,6 +13581,7 @@ form.opml_import_form input { border-radius: 4px; border: 2px solid transparent; } + .NB-static-iphone .NB-ios-feature.NB-active { border: 2px solid #39518B; } @@ -12040,10 +13589,12 @@ form.opml_import_form input { .NB-static-android .NB-ios-features .NB-ios-feature { width: 138px; } + .NB-static-android .NB-ios-features .NB-ios-feature img.NB-ios-ipad-screenshot { width: 100px; height: 160px; } + .NB-static-android .NB-ios-features .NB-ios-feature img.NB-ios-iphone-screenshot { width: 60px; height: 106px; @@ -12061,6 +13612,7 @@ form.opml_import_form input { position: relative; z-index: 1; } + .NB-static-iphone .NB-ios-mockup .NB-ios-iphone-skeleton { position: absolute; bottom: -24px; @@ -12069,10 +13621,12 @@ form.opml_import_form input { height: 483px; z-index: 3; } + .NB-static-iphone .NB-ios-mockup .NB-ios-iphone-skeleton img { width: 100%; height: 100%; } + .NB-static-iphone .NB-ios-mockup .NB-ios-ipad-skeleton { position: absolute; bottom: 0; @@ -12081,10 +13635,12 @@ form.opml_import_form input { height: 640px; z-index: 1; } + .NB-static-iphone .NB-ios-mockup .NB-ios-ipad-skeleton img { width: 100%; height: 100%; } + .NB-static-iphone .NB-ios-mockup .NB-ios-features-iphone { overflow: hidden; width: 192px; @@ -12097,6 +13653,7 @@ form.opml_import_form input { margin: 0; z-index: 2; } + .NB-static-iphone .NB-ios-mockup .NB-ios-features-ipad { overflow: hidden; width: 390px; @@ -12116,6 +13673,7 @@ form.opml_import_form input { left: 0; border: none; } + .NB-static-iphone .NB-ios-mockup .NB-ios-feature img.NB-ios-ipad-screenshot { width: 390px; height: 515px; @@ -12126,6 +13684,7 @@ form.opml_import_form input { border-bottom: none; border-right: 1px solid #505050; } + .NB-static-iphone .NB-ios-mockup .NB-ios-feature img.NB-ios-iphone-screenshot { width: 192px; height: 338px; @@ -12136,23 +13695,29 @@ form.opml_import_form input { border-bottom: none; border-right: 1px solid #505050; } + @media screen and (max-width: 1100px) { .NB-static-iphone .NB-ios-mockup { float: none; margin: 0 auto 48px; } + .NB-static-iphone .NB-ios-mockup .NB-ios-ipad-skeleton { left: 115px; } + .NB-static-iphone .NB-ios-mockup .NB-ios-iphone-skeleton { left: -34px; } + .NB-static-iphone .NB-ios-mockup .NB-ios-features-ipad { left: 170px; } + .NB-static-iphone .NB-ios-mockup .NB-ios-features-iphone { left: 4px; } + .NB-static-iphone .NB-ios-features .NB-ios-feature { padding: 0px; } @@ -12161,22 +13726,26 @@ form.opml_import_form input { .NB-static-android .NB-ios-mockup .NB-ios-ipad-skeleton { height: 729px; } + .NB-static-android .NB-ios-mockup .NB-ios-iphone-skeleton { width: 270px; height: 469px; } + .NB-static-android .NB-ios-mockup .NB-ios-features-ipad { top: 42px; left: 147px; width: 302px; height: 442px; } + .NB-static-android .NB-ios-mockup .NB-ios-features-iphone { top: 279px; left: 16px; width: 172px; height: 268px; } + .NB-static-android .NB-ios-mockup .NB-ios-feature img.NB-ios-ipad-screenshot { width: 302px; height: 484px; @@ -12187,6 +13756,7 @@ form.opml_import_form input { border-bottom: none; border-right: 1px solid #505050; } + .NB-static-android .NB-ios-mockup .NB-ios-feature img.NB-ios-iphone-screenshot { width: 172px; height: 300px; @@ -12197,11 +13767,13 @@ form.opml_import_form input { border-bottom: none; border-right: 1px solid #505050; } + @media screen and (max-width: 1100px) { .NB-static-android .NB-ios-mockup .NB-ios-features-ipad { left: 214px; } } + /* ================= */ /* = Friends Modal = */ /* ================= */ @@ -12216,13 +13788,16 @@ form.opml_import_form input { min-height: 80px; padding: 12px 0; } + .NB-modal-friends .NB-modal-tabs .NB-modal-loading { margin: 6px 8px 0 0; float: left; } + .NB-modal-friends .NB-friends-services { overflow: hidden; } + .NB-modal-friends .NB-friends-service { text-align: center; font-weight: bold; @@ -12230,23 +13805,27 @@ form.opml_import_form input { padding: 12px 12px 0 0; overflow: hidden; } + .NB-modal-friends .NB-friends-service-title { float: left; padding: 0 8px 0 8px; width: 100px; } + .NB-modal-friends .NB-friends-service-connect, .NB-modal-friends .NB-friends-service-disconnect { float: left; width: auto; margin-top: -2px; } + .NB-modal-friends .NB-modal-submit-button img { vertical-align: top; margin: 1px 12px 0 0; width: 12px; height: 12px; } + .NB-modal-friends .NB-friends-autofollow { width: auto; float: right; @@ -12255,12 +13834,14 @@ form.opml_import_form input { margin-top: 12px; padding-top: 0; } + .NB-modal-friends .NB-friends-autofollow-checkbox { margin-right: 4px; position: absolute; top: 2px; left: 0; } + .NB-modal-friends .NB-friends-autofollow-label { margin-left: 20px; display: block; @@ -12276,6 +13857,7 @@ form.opml_import_form input { text-align: center; font-weight: bold; } + .NB-modal-friends .NB-friends-services .NB-error { display: block; margin: 12px 0 0 12px; @@ -12283,6 +13865,7 @@ form.opml_import_form input { float: left; clear: both; } + .NB-friends-service.NB-friends-service-syncing .NB-friends-service-title { padding-right: 28px; background: transparent url('/media/embed/reader/ring_spinner.svg') no-repeat right 3px top 3px; @@ -12293,6 +13876,7 @@ form.opml_import_form input { .NB-modal-friends .NB-friends-findfriends-profile { text-align: left; } + .NB-modal-friends .NB-tab-findfriends .NB-friends-profile-link { margin: 8px 12px 12px; display: inline-block; @@ -12300,7 +13884,7 @@ form.opml_import_form input { .NB-modal-friends .NB-tab-findfriends .NB-friends-profile-link img { vertical-align: text-bottom; -/* float: left;*/ + /* float: left;*/ margin: 0 4px; } @@ -12311,17 +13895,21 @@ form.opml_import_form input { width: 100px; padding: 12px 8px 0 12px; } + .NB-modal.NB-modal-friends .NB-friends-search { overflow: hidden; } + .NB-modal-friends #NB-friends-search-input { float: left; margin: 10px 4px 0; } + .NB-modal-friends .NB-friends-search .NB-loading { float: left; margin: 10px 12px; } + .NB-modal-friends .NB-friends-search-badges { clear: both; float: left; @@ -12333,13 +13921,16 @@ form.opml_import_form input { float: left; margin-right: 6px; } + .NB-modal-friends .NB-friends-search-badges-empty { color: #999; font-size: 13px; } + .NB-modal-friends .NB-tab-findfriends label { - width: auto ; + width: auto; } + .NB-modal-friends .NB-profile-section-heading { margin: 18px 0 0px; } @@ -12359,21 +13950,26 @@ form.opml_import_form input { min-height: 80px; padding: 12px 0; } + .NB-modal-profile-editor .NB-modal-tabs .NB-modal-loading { margin: 6px 8px 0 0; float: left; } + .NB-modal-profile-editor .NB-tab-profile .NB-modal-submit-button { float: left; margin: 12px 0 12px 12px; text-align: center; } + .NB-modal-profile-editor .NB-modal-section.NB-friends-profile { overflow: hidden; } + .NB-modal-profile-editor .NB-modal-section.NB-friends-profilephoto { overflow: hidden; } + .NB-modal-profile-editor .NB-friends-profile-photo-group { float: left; margin: 12px 0 0 12px; @@ -12385,6 +13981,7 @@ form.opml_import_form input { .NB-modal-profile-editor .NB-friends-photo-title input { margin-right: 8px; } + .NB-modal-profile-editor .NB-friends-photo-title label { margin: 2px 0 0 0; } @@ -12402,9 +13999,11 @@ form.opml_import_form input { position: relative; margin: 8px 0 0 22px; } + .NB-modal-profile-editor .NB-friends-profile-photo-group .NB-photo-loader { display: none; } + .NB-modal-profile-editor .NB-friends-profile-photo-group.NB-loading .NB-photo-loader { display: block; width: 32px; @@ -12447,6 +14046,7 @@ form.opml_import_form input { bottom: -6px; height: 20px; } + .NB-modal-profile-editor .NB-friends-link { position: relative; left: 19px; @@ -12454,6 +14054,7 @@ form.opml_import_form input { height: 20px; cursor: pointer; } + .NB-modal-profile-editor .NB-friends-profile .NB-profile-username, .NB-modal-profile-editor input[type=text], .NB-modal-profile-editor .NB-profile-privacy-option input[type=radio] { @@ -12461,24 +14062,30 @@ form.opml_import_form input { width: 200px; margin: 12px 8px 0 0; } + .NB-modal-profile-editor .NB-profile-privacy-option input[type=radio] { width: auto; margin: 18px 0 0; } + .NB-modal-profile-editor .NB-profile-privacy-options { float: left; } + .NB-modal-profile-editor .NB-profile-privacy-option { float: left; clear: left; } + .NB-modal-profile-editor .NB-profile-privacy-option b { padding: 0 6px 0 0; } + .NB-modal-profile-editor .NB-profile-privacy-option img { - width: 16px; - height: 16px; + width: 16px; + height: 16px; } + .NB-modal-profile-editor .NB-friends-profile .NB-count { float: left; color: #404040; @@ -12486,12 +14093,15 @@ form.opml_import_form input { font-weight: bold; margin: 16px 0 0 4px; } + .NB-modal-profile-editor .NB-friends-profile .NB-count.NB-red { color: #7F0000; } + .NB-modal-profile-editor .NB-friends-profile .NB-profile-username { width: auto; } + .NB-modal-profile-editor .NB-friends-profile label { clear: both; float: left; @@ -12499,21 +14109,25 @@ form.opml_import_form input { padding: 12px 8px 0 12px; cursor: pointer; } + .NB-modal-profile-editor .NB-friends-profile .NB-profile-privacy-notpremium { font-size: 10px; margin: 12px 0; color: #808080; } + .NB-modal-profile-editor .NB-friends-profile label img { padding-right: 6px; vertical-align: bottom; } + .NB-modal-profile-editor .NB-friends-profile .NB-profile-protected-label { clear: none; width: auto; font-size: 13px; padding: 16px 0 0 8px; } + .NB-modal-profile-editor .NB-account-link { margin-left: 12px; font-size: 10px; @@ -12525,17 +14139,21 @@ form.opml_import_form input { width: 200px; padding: 0 8px 0 12px; } + .NB-modal-profile-editor .NB-tab-blurblog .NB-profile-blurblog-title { width: 480px; } + .NB-modal-profile-editor .NB-profile-blurblog-colors { width: 516px; float: left; margin: 0 -6px 12px; } + .NB-modal-profile-editor .NB-profile-blurblog-colorline { clear: both; } + .NB-modal-profile-editor .NB-profile-blurblog-color { float: left; width: 29px; @@ -12544,16 +14162,19 @@ form.opml_import_form input { margin: 5px 6px; box-shadow: 1px 1px 0 #CFCFCF; } + .NB-modal-profile-editor .NB-profile-blurblog-color:hover { box-shadow: none; border: 4px solid #D95300; margin: 2px 3px; cursor: pointer; } + .NB-modal-profile-editor .NB-profile-blurblog-color.NB-active { border: 4px solid #8F1F00; margin: 2px 3px; } + .NB-modal-profile-editor .NB-profile-blurblog-css, .NB-modal-account .NB-account-custom-css, .NB-modal-account .NB-account-custom-javascript { @@ -12566,18 +14187,22 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-modal-profile-editor .NB-tab-blurblog input[type=text] { margin: 0 0 12px; } + .NB-modal-profile-editor .NB-profile-blurblog-address { float: left; margin: 0 0 12px; } + .NB-modal-profile-editor .NB-blurblog-save-button { margin: 18px 12px 12px; float: left; clear: both; } + .NB-modal-profile-editor .NB-tab.NB-tab-profile, .NB-modal-profile-editor .NB-tab.NB-tab-blurblog { height: 500px; @@ -12587,40 +14212,46 @@ form.opml_import_form input { margin: 12px 0; overflow: hidden; } + .NB-modal-profile-editor .NB-preference .NB-preference-options { - float: left; - width: 420px; - overflow: hidden; - padding: 2px 0; + float: left; + width: 420px; + overflow: hidden; + padding: 2px 0; } + .NB-modal-profile-editor .NB-preference .NB-preference-label { - float: left; - width: 200px; - position: relative; + float: left; + width: 200px; + position: relative; } + .NB-modal-profile-editor .NB-preference .NB-preference-sublabel { - font-size: 11px; - color: #808080; - margin: 4px 30px 0px 0px; + font-size: 11px; + color: #808080; + margin: 4px 30px 0px 0px; } .NB-modal-profile-editor .NB-preference .NB-preference-options input { - clear: both; - float: left; + clear: both; + float: left; } + .NB-modal-profile-editor .NB-preference .NB-preference-options input[type=radio] { - margin-top: 4px; + margin-top: 4px; } + .NB-modal-profile-editor .NB-preference .NB-preference-options input[type=checkbox] { - margin-top: 5px; + margin-top: 5px; } + .NB-modal-profile-editor .NB-preference .NB-preference-options label { - padding-left: 4px; - margin: 0 0 4px 0; - float: left; - width: auto; - clear: none; - cursor: pointer; + padding-left: 4px; + margin: 0 0 4px 0; + float: left; + width: auto; + clear: none; + cursor: pointer; } /* ================= */ @@ -12629,11 +14260,13 @@ form.opml_import_form input { .NB-profile-badge { border-bottom: 1px solid #F0F0F0; - box-shadow: 0 1px 2px rgba(0,0,0,0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } + .NB-profile-badge:last-child { border-bottom: none; } + .NB-profile-badge table { clear: both; padding: 12px; @@ -12644,42 +14277,52 @@ form.opml_import_form input { min-height: 50px; width: 100%; } + .NB-profile-badge td { vertical-align: top; } + .NB-profile-badge td.NB-profile-badge-info { width: 100%; } + .NB-profile-badge.NB-profile-badge-embiggen { padding: 0; } + .NB-profile-badge .NB-profile-badge-photo { padding: 0 8px 0 0; line-height: 0; } + .NB-profile-badge .NB-profile-badge-photo img { max-width: 48px; max-height: 48px; border-radius: 3px; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-photo-wrapper { width: 108px; -/* height: 108px;*/ + /* height: 108px;*/ vertical-align: top; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-photo img { max-width: 108px; max-height: 108px; } + .NB-profile-badge .NB-profile-badge-username { font-weight: bold; float: left; cursor: pointer; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-username { font-size: 22px; text-shadow: 1px 1px 0 #E0E0E0; } + .NB-profile-badge .NB-profile-badge-location { font-weight: bold; color: #909090; @@ -12687,20 +14330,24 @@ form.opml_import_form input { float: left; padding-left: 12px; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-location { font-size: 12px; padding: 4px 0 0 12px; } + .NB-profile-badge .NB-profile-badge-website { display: none; float: left; padding-left: 12px; font-size: 11px; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-website { padding: 4px 0 0 12px; display: block; } + .NB-profile-badge .NB-profile-badge-bio { clear: left; padding: 2px 0; @@ -12708,26 +14355,32 @@ form.opml_import_form input { color: #878787; line-height: 17px; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-bio { font-size: 13px; padding: 6px 0 0; } + .NB-profile-badge .NB-profile-badge-stats { clear: left; color: #AE5D15; font-size: 11px; text-transform: uppercase; } + .NB-profile-badge.NB-profile-badge-embiggen .NB-profile-badge-stats { padding: 2px 0 0; } + .NB-profile-badge .NB-profile-badge-stats .NB-count { padding-right: 3px; font-weight: bold; } + .NB-profile-badge .NB-profile-badge-blurblog-link { text-transform: none; } + .NB-profile-badge .NB-profile-badge-following-you { padding: 2px 8px; background-color: #FFFFF6; @@ -12735,9 +14388,11 @@ form.opml_import_form input { display: inline; border: 1px solid #EFEFE6; } + .NB-profile-badge-actions { float: right; } + .NB-profile-badge-actions .NB-profile-badge-action-self { color: #C0C0C0; font-weight: bold; @@ -12746,6 +14401,7 @@ form.opml_import_form input { cursor: default; border-color: #C0C0C0; } + .NB-profile-badge-actions .NB-profile-badge-action-self, .NB-profile-badge-actions .NB-modal-submit-button, .NB-badge-actions .NB-modal-submit-button { @@ -12755,6 +14411,7 @@ form.opml_import_form input { clear: right; float: right; } + .NB-profile-badge-actions .NB-profile-badge-action-preview, .NB-badge-actions .NB-badge-action-add, .NB-profile-badge-actions .NB-profile-badge-action-mute, @@ -12763,6 +14420,7 @@ form.opml_import_form input { line-height: 1; font-size: 11px; } + .NB-profile-badge-actions .NB-profile-badge-action-preview.NB-disabled { cursor: default; background: white; @@ -12770,25 +14428,31 @@ form.opml_import_form input { -moz-box-shadow: none; opacity: .25; } + .NB-profile-badge-actions .NB-profile-badge-action-preview.NB-disabled:hover { background: white; } + .NB-profile-badge-actions .NB-profile-badge-action-protected-follow img { vertical-align: top; margin-right: 6px; } + .NB-profile-badge-actions .NB-profile-badge-action-edit { color: #404040; line-height: 1; font-size: 11px; } + .NB-profile-badge-actions .NB-profile-badge-action-buttons { float: right; } + .NB-profile-badge-actions .NB-profile-badge-action-buttons img { - width: 16px; - height: 16px; + width: 16px; + height: 16px; } + .NB-profile-badge-action-admin { display: inline-block; cursor: pointer; @@ -12798,10 +14462,12 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/circular/g_icn_hands.png'); background-size: 16px; } + .NB-profile-badge-info .NB-profile-badge-actions .NB-loading { margin: 6px 12px 0 0; float: left; } + /* ======================== */ /* = Social Profile Modal = */ /* ======================== */ @@ -12809,6 +14475,7 @@ form.opml_import_form input { .NB-modal-profile { min-height: 300px; } + .NB-modal.NB-modal-profile .NB-modal-loading { position: absolute; top: 12px; @@ -12816,16 +14483,20 @@ form.opml_import_form input { float: none; z-index: 1; } + .NB-modal.NB-modal-profile .NB-modal-section { padding: 12px 0 2px; } + .NB-modal-profile .NB-profile-actions { float: right; margin-top: 10px; } + .NB-modal-profile .NB-profile-links { overflow: hidden; } + .NB-modal-profile .NB-profile-links .NB-profile-link { float: left; font-size: 0; @@ -12833,11 +14504,13 @@ form.opml_import_form input { margin: 0 6px 6px 0; cursor: pointer; } + .NB-modal-profile .NB-profile-links .NB-profile-link img { width: 42px; height: 42px; border-radius: 3px; } + .NB-modal-profile .NB-profile-followers { width: 100%; padding: 0; @@ -12846,28 +14519,34 @@ form.opml_import_form input { border-radius: 6px; text-shadow: 0 1px 0 white; } + .NB-modal-profile .NB-profile-followers td { width: 100%; padding-right: 12px; } + .NB-modal-profile .NB-profile-followers td.NB-profile-follow-count { width: 116px; text-align: center; padding-right: 0; } + .NB-modal-profile .NB-profile-followers td.NB-profile-follow-count div { width: 116px; font-size: 28px; } + .NB-modal-profile .NB-profile-followers td.NB-profile-follow-count h3 { font-size: 14px; margin: 4px 0 0; color: #909090; text-transform: uppercase; } + .NB-modal-profile .NB-interactions { margin: 12px 0 36px; } + /* ======================= */ /* = Interactions Module = */ /* ======================= */ @@ -12875,6 +14554,7 @@ form.opml_import_form input { .NB-interactions-popover-container .popover-content { padding: 0; } + .NB-interactions-popover .NB-loading { margin: 12px auto; background: transparent url('/media/img/reader/worm_spinner.svg') no-repeat 0 0; @@ -12882,11 +14562,13 @@ form.opml_import_form input { width: 16px; height: 16px; } + .NB-interactions-popover .NB-interactions-header { font-size: 10px; text-align: center; overflow: hidden; } + .NB-interactions-popover .NB-tab { width: 50%; color: #A0A0A0; @@ -12894,19 +14576,24 @@ form.opml_import_form input { padding: 8px 0; background-color: rgba(0, 0, 0, .05); } + .NB-interactions-popover .NB-tab span { display: block; padding: 2px 0; } + .NB-interactions-popover .NB-tab-interactions { float: left; } + .NB-interactions-popover .NB-tab-interactions span { border-right: 1px solid rgba(0, 0, 0, .2); } + .NB-interactions-popover .NB-tab-activities { float: right; } + .NB-interactions-popover.NB-active-interactions .NB-tab-interactions, .NB-interactions-popover.NB-active-activities .NB-tab-activities { color: #505050; @@ -12924,6 +14611,7 @@ form.opml_import_form input { padding: 0; margin: 0; } + .NB-interaction { list-style: none; position: relative; @@ -12937,33 +14625,41 @@ form.opml_import_form input { cursor: pointer; min-height: 44px; } + .NB-activities-container .NB-interaction { padding-left: 40px; min-height: none; } + .NB-interaction.NB-disabled { cursor: default; } + .NB-interaction.NB-highlighted { background-color: #FFFFF6; border-bottom: 1px solid #F0F0E6; } + .NB-interaction .NB-splash-link:hover { color: #405BA8; } + .NB-interaction-content { font-size: 11px; padding-top: 4px; line-height: 16px; color: #9d9d9d; } + .NB-interaction-content a { color: #A6A9B0; } + .NB-interaction:active:not(.NB-disabled) .NB-interaction-content .NB-splash-link { color: #405BA8; text-decoration: none; } + .NB-interaction-photo { position: absolute; width: 36px; @@ -12973,18 +14669,22 @@ form.opml_import_form input { top: 10px; cursor: pointer; } + .NB-activities-container .NB-interaction-photo { width: 16px; height: 16px; } + .NB-interaction-date { color: #5e828b; font-size: 10px; padding: 5px 0 0; } -.NB-interaction:hover:not(.NB-disabled) .NB-interaction-date { + +.NB-interaction:hover:not(.NB-disabled) .NB-interaction-date { color: #929697; } + .NB-interaction-title { font-size: 12px; line-height: 18px; @@ -12992,21 +14692,27 @@ form.opml_import_form input { padding: 2px 0 0 0; opacity: .9; } + .NB-interaction-username { cursor: pointer; } + .NB-interaction-starred-story-title { cursor: pointer; } + .NB-interaction-reply-content { cursor: pointer; } + .NB-interaction-feed-title { cursor: pointer; } + .NB-interaction-sharedstory-title { cursor: pointer; } + .NB-interaction-sharedstory-content { cursor: pointer; } @@ -13030,6 +14736,7 @@ form.opml_import_form input { background-size: 28px; filter: hue-rotate(311deg) saturate(16); } + .NB-modal-organizer .NB-feedlist { width: 70%; max-height: 500px; @@ -13042,36 +14749,46 @@ form.opml_import_form input { -ms-user-select: none; user-select: none; } + .NB-modal-organizer .NB-feedlist .feed_title { padding-right: 186px; } + .NB-modal-organizer .NB-feedlist .feed { display: block; } + .NB-modal-organizer .NB-feedlist .folder .folder_title .NB-feedlist-collapse-icon, .NB-modal-organizer .NB-feedlist .folder .folder_title .feed_counts_floater { display: none; } + .NB-modal-organizer .NB-feedlist .feed.NB-feed-exception .feed_counts { display: block; } + .NB-modal-organizer .feed.NB-highlighted .NB-feed-exception-icon { display: none; } + .NB-modal-organizer .feed.selected { background: none; border-top: 1px solid transparent; border-bottom: 1px solid transparent; } + .NB-modal-organizer .feed { font-weight: normal; } + .NB-modal-organizer .NB-highlighted { font-weight: bold; } + .NB-modal-organizer .unread_count.unread_count_positive.unread_count_full { display: none; } + .NB-modal-organizer .NB-highlighted .unread_count.unread_count_positive { display: block; width: 24px; @@ -13082,6 +14799,7 @@ form.opml_import_form input { background-position: center center; background-repeat: no-repeat; } + .NB-modal-organizer .NB-feed-organizer-sort { display: none; position: absolute; @@ -13089,24 +14807,31 @@ form.opml_import_form input { width: 124px; top: 4px; } + .NB-modal-organizer .NB-sort-alphabetical .NB-feed-organizer-opens { display: block; } + .NB-modal-organizer .NB-sort-subscribers .NB-feed-organizer-subscribers { display: block; } + .NB-modal-organizer .NB-sort-recency .NB-feed-organizer-laststory { display: block; } + .NB-modal-organizer .NB-sort-frequency .NB-feed-organizer-monthlycount { display: block; } + .NB-modal-organizer .NB-sort-mostused .NB-feed-organizer-opens { display: block; } + .NB-modal-organizer .NB-feed-organizer-sort.NB-active { display: block; } + .NB-modal-organizer .NB-organizer-actionbar { font-size: 12px; line-height: 18px; @@ -13114,26 +14839,32 @@ form.opml_import_form input { width: 70%; overflow: hidden; } + .NB-modal-organizer .NB-organizer-sorts { float: right; } + .NB-modal-organizer .NB-organizer-selects { float: left; } + .NB-modal-organizer .NB-organizer-action-title { float: left; font-size: 11px; font-weight: bold; } + .NB-modal-organizer .NB-organizer-action { float: left; margin-left: 6px; cursor: pointer; color: #405BA8; } + .NB-modal-organizer .NB-organizer-action:hover { color: #A85B40; } + .NB-modal-organizer .NB-organizer-action.NB-active, .NB-modal-organizer .NB-organizer-action.NB-active:hover { color: #000; @@ -13141,15 +14872,18 @@ form.opml_import_form input { background: transparent url('/media/embed/icons/nouns/down.svg') no-repeat right 3px; background-size: 8px; } + .NB-modal-organizer .NB-sort-inverse .NB-organizer-action.NB-active { background: transparent url('/media/embed/icons/nouns/up.svg') no-repeat right 4px; - background-size: 8px; + background-size: 8px; } + .NB-modal-organizer .NB-organizer-selects .NB-organizer-action { float: left; margin-left: 0; margin-right: 6px; } + .NB-modal-organizer .NB-organizer-selects .NB-organizer-action-title { margin-left: 0; margin-right: 6px; @@ -13160,26 +14894,31 @@ form.opml_import_form input { width: 27%; margin: 12px 0 0; } + .NB-modal-organizer .NB-organizer-sidebar-title { font-size: 11px; line-height: 18px; font-weight: bold; margin-top: 12px; } + .NB-modal-organizer .NB-organizer-sidebar-hierarchy .NB-organizer-sidebar-title { margin-top: 0; -} +} + .NB-modal-organizer .NB-organizer-sidebar-container { margin: 4px 0 0; border: 1px solid rgba(0, 0, 0, .2); background-color: #F7F8F5; padding: 6px; } + .NB-modal-organizer .segmented-control { margin: 0; line-height: 18px; overflow: hidden; } + .NB-modal-organizer .segmented-control li { padding: 2px 12px 0; font-size: 11px; @@ -13188,16 +14927,20 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-modal-organizer .NB-modal-submit-button { text-align: center; margin: 6px 0 0; } + .NB-modal-organizer .NB-action-delete { margin-top: 0; } + .NB-modal-organizer .NB-folders { width: 164px; } + .NB-modal-organizer .NB-icon-add { background: transparent url('/media/embed/icons/circular/g_icn_folder_add.png') no-repeat 0 0; float: right; @@ -13207,18 +14950,21 @@ form.opml_import_form input { cursor: pointer; background-size: 16px; } + .NB-modal-organizer .NB-icon-add:hover { background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0; background-size: 16px; } + .NB-modal-organizer .NB-icon-subfolder { background: transparent url('/media/embed/icons/nouns/right.svg') no-repeat 0 0; float: left; width: 8px; height: 8px; margin: 12px 6px 0 4px; - background-size: 8px; + background-size: 8px; } + .NB-modal-organizer .NB-add-folder-input { font-size: 11px; width: 140px; @@ -13272,6 +15018,7 @@ form.opml_import_form input { width: 12px; height: 12px; } + .NB-search-close { display: none; position: absolute; @@ -13294,18 +15041,22 @@ form.opml_import_form input { .NB-search-close:hover { opacity: 1; } + .NB-searching .NB-search-close, .NB-searching .NB-search-save { display: block; } + .NB-search-container { position: relative; } + .NB-story-title-search { float: right; margin: 3px 0 0 12px; position: relative; } + .NB-story-title-search-input { transition: all .22s ease-in-out; @@ -13315,6 +15066,7 @@ form.opml_import_form input { padding: 1px 20px; font-weight: normal; } + .NB-searching .NB-story-title-search-input { width: 154px; } @@ -13340,7 +15092,7 @@ form.opml_import_form input { z-index: 1; margin: 0 0 0 8px; color: rgba(0, 0, 0, .5); - + background-color: #F0F1EC; } @@ -13359,6 +15111,7 @@ form.opml_import_form input { top: 8px; left: 12px; } + .NB-search-header .NB-search-header-title { font-size: 12px; } @@ -13373,27 +15126,32 @@ form.opml_import_form input { text-align: center; text-shadow: 0 1px 0 rgba(255, 255, 255, .5); } + .NB-static-form.NB-delete-form { width: 420px; margin-bottom: 24px; } + .NB-static-form.NB-delete-form label { width: 200px; } + .NB-static-form.NB-delete-form input[type=submit] { width: 206px; margin: 12px 0 4px 200px; - - -moz-box-shadow:2px 2px 0 #A0B998; - -webkit-box-shadow:2px 2px 0 #A0B998; - box-shadow:2px 2px 0 #A0B998; - + + -moz-box-shadow: 2px 2px 0 #A0B998; + -webkit-box-shadow: 2px 2px 0 #A0B998; + box-shadow: 2px 2px 0 #A0B998; + white-space: normal; font-size: 13px; } + .NB-fields { clear: both; } + .NB-static-form .NB-errors { margin-left: 200px; } @@ -13405,6 +15163,7 @@ form.opml_import_form input { .NB-static-oauth { padding-bottom: 0; } + .NB-static-oauth h3 { margin-top: 0; text-align: center; @@ -13415,9 +15174,11 @@ form.opml_import_form input { margin: 24px auto; padding: 0 24px; } + .NB-static-login .NB-static-form { width: 320px; } + .NB-static-oauth .NB-static-form-label label { width: 108px; text-transform: none; @@ -13426,12 +15187,14 @@ form.opml_import_form input { color: rgba(0, 0, 0, .6); display: block; } + .NB-static-oauth .NB-static-form-input input { margin-left: 12px; margin-right: 0; height: 34px; font-size: 22px; } + .NB-static-oauth .NB-static-form-username-label label { padding-top: 0; } @@ -13442,9 +15205,11 @@ form.opml_import_form input { font-size: 18px; padding: 8px 12px; } + .NB-static-login input[type=submit].NB-static-form-submit { margin: 12px 0 0 120px; } + .NB-static-oauth .NB-error { font-size: 12px; text-align: center; @@ -13454,34 +15219,41 @@ form.opml_import_form input { font-weight: bold; clear: both; } + .NB-static-form-alttext { - margin: 24px 0 0 120px; + margin: 24px 0 0 120px; padding-top: 12px; - border-top: 1px solid rgba(0, 0, 0, .1); + border-top: 1px solid rgba(0, 0, 0, .1); } @media screen and (max-width: 580px) { .NB-static-oauth { padding: 12px 0 0; } + .NB-static .NB-splash-info, .NB-static .NB-static-title { position: absolute; } + .NB-splash-info.NB-splash-bottom { position: relative; } + .NB-splash-info { height: 36px; } + .NB-splash-info.NB-splash-top .NB-splash-title { width: auto; height: 36px; } + .NB-static .NB-static-title { font-size: 24px; line-height: 36px; } + .NB-static-oauth .NB-static-form { width: auto; } @@ -13494,132 +15266,161 @@ form.opml_import_form input { .NB-filter-popover-container.popover.bottom-left .arrow { left: 20%; } + .NB-filter-popover-container .popover-content { padding: 0; } + .NB-style-popover .segmented-control { margin-top: 4px; } + .NB-filter-popover .segmented-control, .NB-style-popover .segmented-control { line-height: 18px; float: none; flex-grow: 1; } + .NB-filter-popover .segmented-control li { padding: 2px 12px 2px; font-size: 11px; } + .NB-filter-popover .NB-popover-icon-control { margin: 4px 0 0; display: flex; gap: 8px; align-items: center; } -.NB-filter-popover .NB-popover-icon-control > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control>.NB-icon { width: 24px; height: 16px; } -.NB-filter-popover .NB-popover-icon-control-dashboardcount > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-dashboardcount>.NB-icon { background: transparent url('/media/embed/icons/nouns/count.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(206deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-readfilter > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-readfilter>.NB-icon { background: transparent url('/media/embed/icons/nouns/venn.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(106deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-order > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-order>.NB-icon { background: transparent url('/media/embed/icons/nouns/order.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(312deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-infrequent > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-infrequent>.NB-icon { background: transparent url('/media/embed/icons/nouns/frequency.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(256deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-markscroll > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-markscroll>.NB-icon { background: transparent url('/media/embed/icons/nouns/scroll.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(56deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-density > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-density>.NB-icon { background: transparent url('/media/embed/icons/nouns/square-space.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(126deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-contentpreview > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-contentpreview>.NB-icon { background: transparent url('/media/embed/icons/nouns/content-preview.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(186deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-imagepreview > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-imagepreview>.NB-icon { background: transparent url('/media/embed/icons/nouns/image.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(231deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-feed-font > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-feed-font>.NB-icon { background: transparent url('/media/embed/icons/nouns/font.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(226deg) saturate(10.5); } -.NB-filter-popover .NB-popover-icon-control-feed-size > .NB-icon { + +.NB-filter-popover .NB-popover-icon-control-feed-size>.NB-icon { background: transparent url('/media/embed/icons/nouns/font-size.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(40deg) saturate(10.5); } + .NB-filter-popover .segmented-control.NB-feed-notification-types li { flex-grow: 1; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-dashboardcount li { flex-grow: 1; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-dashboardcount li.NB-active { flex-grow: 3; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-infrequent li { width: auto; padding-left: 10px; padding-right: 10px; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-contentpreview li, .NB-style-popover .segmented-control.NB-menu-manage-view-setting-contentpreview li { padding: 2px 6px 2px; font-size: 11px; height: 20px; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-contentpreview li.NB-view-setting-contentpreview-title, .NB-style-popover .segmented-control.NB-menu-manage-view-setting-contentpreview li.NB-view-setting-contentpreview-title { flex-grow: 2; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-imagepreview li.NB-view-setting-imagepreview-none, .NB-style-popover .segmented-control.NB-menu-manage-view-setting-imagepreview li.NB-view-setting-imagepreview-none { flex-grow: 3; padding: 5px 12px; } + .NB-filter-popover .segmented-control.NB-menu-manage-view-setting-imagepreview li, .NB-style-popover .segmented-control.NB-menu-manage-view-setting-imagepreview li { padding: 6px 8px 2px; font-size: 11px; } + .NB-menu-manage-view-setting-imagepreview .NB-icon { width: 16px; height: 16px; } + .NB-filter-popover .segmented-control .NB-unread-icon, .NB-filter-popover .segmented-control .NB-focus-icon { height: 17px; } + .NB-filter-popover .NB-modal-feed-chooser { width: 100%; margin: 0 0 6px; } + .NB-filter-popover .NB-filter-popover-manage-dashboard-modules { display: flex; gap: 6px; } + .NB-filter-popover .NB-filter-popover-manage-button { flex: 1 2 0; border-radius: 4px; @@ -13629,42 +15430,52 @@ form.opml_import_form input { display: flex; gap: 6px; } + .NB-filter-popover .NB-filter-popover-manage-button:hover { cursor: pointer; background-color: #d5d6d2; } + .NB-filter-popover .NB-filter-popover-manage-button .NB-icon { /* flex: 1 1 0; */ width: 16px; height: 16px; } + .NB-filter-popover .NB-filter-popover-manage-button .NB-text { flex: 2 1 0; } + .NB-filter-popover .NB-filter-popover-dashboard-add-module-left .NB-icon { background: transparent url('/media/embed/icons/nouns/add-list.svg') no-repeat 0 0; background-size: 16px; } + .NB-filter-popover .NB-filter-popover-dashboard-add-module-right .NB-icon { background: transparent url('/media/embed/icons/nouns/add-list-right.svg') no-repeat 0 0; background-size: 16px; order: 2; } + .NB-filter-popover .NB-filter-popover-dashboard-remove-module .NB-icon { background: transparent url('/media/embed/icons/nouns/remove-list.svg') no-repeat 0 0; background-size: 16px; } + .NB-filter-popover .NB-filter-popover-dashboard-add-module-right .NB-text { text-align: right; } + .NB-feedbar-options-stat { position: relative; padding-left: 30px; margin-bottom: 6px; } + .NB-feedbar-options-stat:last-child { margin-bottom: 0; } + .NB-feedbar-options-stat .NB-icon { clear: both; left: 0; @@ -13676,53 +15487,64 @@ form.opml_import_form input { height: 16px; position: absolute; } + .NB-stat-subscribers .NB-icon { background: transparent url('/media/embed/icons/nouns/subscribers.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(87deg) saturate(10.5); } + .NB-stat-updated .NB-icon { background: transparent url('/media/embed/icons/nouns/boomerang.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(320deg) saturate(16.5); } + .NB-stat-average .NB-icon { background: transparent url('/media/embed/icons/nouns/monthly.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(287deg) saturate(10.5); } + .NB-stat-archive-count .NB-icon { background: transparent url('/media/embed/icons/nouns/cabinet.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(143deg) saturate(14.5); } + .NB-stat-decay .NB-icon { background: transparent url('/media/embed/icons/nouns/refresh.svg') no-repeat 0 0; background-size: 16px; filter: hue-rotate(7deg) saturate(10.5); } + .NB-stat-realtime .NB-icon { background: transparent url('/media/embed/icons/nouns/saved-stories.svg'); background-size: 16px; } + .NB-feedbar-options-stat .NB-stat { font-size: 11px; color: #6F716A; } + .NB-filter-popover-stats-icon { background: transparent url('/media/embed/icons/nouns/dialog-statistics.svg') no-repeat 0 0; background-size: 32px; filter: hue-rotate(284deg) saturate(18); } + .NB-filter-popover-filter-icon { background: transparent url('/media/embed/icons/nouns/settings.svg') no-repeat 0 0; background-size: 32px; } + .NB-filter-popover-notifications-icon { background: transparent url("/media/embed/icons/nouns/dialog-notifications.svg") no-repeat center center; background-size: 32px; filter: hue-rotate(320deg) saturate(18); } + .NB-feedbar-notifications-icon { background: transparent url("/media/embed/icons/nouns/dialog-notifications.svg") no-repeat center center; background-size: 12px; @@ -13742,12 +15564,14 @@ form.opml_import_form input { top: 0; left: 0; z-index: 10; - + background-color: #FFF; } + .NB-popover.NB-popover-scroll .popover-content { overflow-y: scroll; } + .NB-popover-section-title { color: #585A55; text-transform: uppercase; @@ -13755,18 +15579,22 @@ form.opml_import_form input { font-size: 12px; font-weight: bold; } + .NB-popover-section-title:first-child { margin-top: 0; } + .NB-popover-section { padding: 22px 18px 22px; border-bottom: 1px solid #E8EAE4; overflow: hidden; } + .NB-popover-section:last-child { border-bottom: none; /* padding-bottom: 8px; */ } + .NB-section-icon { float: right; width: 18px; @@ -13777,156 +15605,179 @@ form.opml_import_form input { } .popover { - position: absolute; - top: 0; - left: 0; - z-index: 1010; - width: 236px; - padding: 1px; - text-align: left; - background-color: #ffffff; - background-clip: padding-box; - border-radius: 4px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - white-space: normal; + position: absolute; + top: 0; + left: 0; + z-index: 1010; + width: 236px; + padding: 1px; + text-align: left; + background-color: #ffffff; + background-clip: padding-box; + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + white-space: normal; } + .popover.top { - margin-top: -10px; + margin-top: -10px; } + .popover.right { - margin-left: 10px; + margin-left: 10px; } + .popover.bottom, .popover.bottom-right, .popover.bottom-left { - margin-top: 10px; + margin-top: 10px; } + .popover.left { - margin-left: -10px; + margin-left: -10px; } + .popover-title { - margin: 0; - padding: 8px 14px; - font-size: 14px; - font-weight: normal; - line-height: 18px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - border-radius: 5px 5px 0 0; + margin: 0; + padding: 8px 14px; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; } + .popover-content { - padding: 9px; + padding: 9px; } + .popover .arrow, .popover .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } + .popover .arrow { - border-width: 11px; + border-width: 11px; } + .popover .arrow:after { - border-width: 10px; - content: ""; + border-width: 10px; + content: ""; } + .popover.top-left .arrow, .popover.top-right .arrow, .popover.top .arrow { - left: 50%; - margin-left: -11px; - border-bottom-width: 0; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, 0.25); - bottom: -11px; + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + bottom: -11px; } + .popover.top-left .arrow { left: 14px; } + .popover.top-right .arrow { left: 90%; } + .popover.top-left .arrow:after, .popover.top-right .arrow:after, .popover.top .arrow:after { - bottom: 1px; - margin-left: -10px; - border-bottom-width: 0; - border-top-color: #ffffff; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #ffffff; } + .popover.right .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-left-width: 0; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, 0.25); + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); } + .popover.right .arrow:after { - left: 1px; - bottom: -10px; - border-left-width: 0; - border-right-color: #ffffff; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: #ffffff; } + .popover.bottom .arrow, .popover.bottom-left .arrow, .popover.bottom-right .arrow { - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, 0.25); - top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + top: -11px; } + .popover.bottom .arrow:after, .popover.bottom-left .arrow:after, .popover.bottom-right .arrow:after { - top: 1px; - margin-left: -10px; - border-top-width: 0; - border-bottom-color: #ffffff; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: #ffffff; } + .popover.bottom-left .arrow { left: 14px; } + .popover.bottom-right .arrow { left: 90%; } + .popover.left .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, 0.25); + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); } + .popover.left .arrow:after { - right: 1px; - border-right-width: 0; - border-left-color: #ffffff; - bottom: -10px; + right: 1px; + border-right-width: 0; + border-left-color: #ffffff; + bottom: -10px; } + .fade { - opacity: 0; - -webkit-transition: opacity 0.22s ease-in-out; - -moz-transition: opacity 0.22s ease-in-out; - -o-transition: opacity 0.22s ease-in-out; - transition: opacity 0.22s ease-in-out; + opacity: 0; + -webkit-transition: opacity 0.22s ease-in-out; + -moz-transition: opacity 0.22s ease-in-out; + -o-transition: opacity 0.22s ease-in-out; + transition: opacity 0.22s ease-in-out; } + .fade.in { - opacity: 1; + opacity: 1; } .NB-overlay { background: rgba(16, 16, 48, 0); - background: -webkit-linear-gradient(rgba(16, 16, 28, 0.3),rgba(16, 16, 28, 0.3) 60%,rgba(16, 16, 28, 0.01)); - background: -moz-linear-gradient(rgba(16, 16, 28, 0.3),rgba(16, 16, 28, 0.3) 60%,rgba(16, 16, 28, 0.01)); - background: -ms-linear-gradient(rgba(16, 16, 28, 0.3),rgba(16, 16, 28, 0.3) 60%,rgba(16, 16, 28, 0.01)); - background: -o-linear-gradient(rgba(16, 16, 28, 0.3),rgba(16, 16, 28, 0.3) 60%,rgba(16, 16, 28, 0.01)); - background: linear-gradient(rgba(16, 16, 28, 0.3),rgba(16, 16, 28, 0.3) 60%,rgba(16, 16, 28, 0.01)); + background: -webkit-linear-gradient(rgba(16, 16, 28, 0.3), rgba(16, 16, 28, 0.3) 60%, rgba(16, 16, 28, 0.01)); + background: -moz-linear-gradient(rgba(16, 16, 28, 0.3), rgba(16, 16, 28, 0.3) 60%, rgba(16, 16, 28, 0.01)); + background: -ms-linear-gradient(rgba(16, 16, 28, 0.3), rgba(16, 16, 28, 0.3) 60%, rgba(16, 16, 28, 0.01)); + background: -o-linear-gradient(rgba(16, 16, 28, 0.3), rgba(16, 16, 28, 0.3) 60%, rgba(16, 16, 28, 0.01)); + background: linear-gradient(rgba(16, 16, 28, 0.3), rgba(16, 16, 28, 0.3) 60%, rgba(16, 16, 28, 0.01)); position: fixed; top: 0; right: 0; @@ -13934,17 +15785,18 @@ form.opml_import_form input { left: 0; z-index: 100; opacity: 0.0; - -webkit-transition: opacity 400ms ease,-webkit-transform 0s ease; - -moz-transition: opacity 400ms ease,-moz-transform 0s ease; - -o-transition: opacity 400ms ease,-o-transform 0s ease; - transition: opacity 400ms ease,transform 0s ease; + -webkit-transition: opacity 400ms ease, -webkit-transform 0s ease; + -moz-transition: opacity 400ms ease, -moz-transform 0s ease; + -o-transition: opacity 400ms ease, -o-transform 0s ease; + transition: opacity 400ms ease, transform 0s ease; } + .NB-overlay.NB-top { - background: -webkit-linear-gradient(rgba(16, 16, 28, 0.01),rgba(16, 16, 28, 0.3) 40%,rgba(16, 16, 28, 0.3)); - background: -moz-linear-gradient(rgba(16, 16, 28, 0.01),rgba(16, 16, 28, 0.3) 40%,rgba(16, 16, 28, 0.3)); - background: -ms-linear-gradient(rgba(16, 16, 28, 0.01),rgba(16, 16, 28, 0.3) 40%,rgba(16, 16, 28, 0.3)); - background: -o-linear-gradient(rgba(16, 16, 28, 0.01),rgba(16, 16, 28, 0.3) 40%,rgba(16, 16, 28, 0.3)); - background: linear-gradient(rgba(16, 16, 28, 0.01),rgba(16, 16, 28, 0.3) 40%,rgba(16, 16, 28, 0.3)); + background: -webkit-linear-gradient(rgba(16, 16, 28, 0.01), rgba(16, 16, 28, 0.3) 40%, rgba(16, 16, 28, 0.3)); + background: -moz-linear-gradient(rgba(16, 16, 28, 0.01), rgba(16, 16, 28, 0.3) 40%, rgba(16, 16, 28, 0.3)); + background: -ms-linear-gradient(rgba(16, 16, 28, 0.01), rgba(16, 16, 28, 0.3) 40%, rgba(16, 16, 28, 0.3)); + background: -o-linear-gradient(rgba(16, 16, 28, 0.01), rgba(16, 16, 28, 0.3) 40%, rgba(16, 16, 28, 0.3)); + background: linear-gradient(rgba(16, 16, 28, 0.01), rgba(16, 16, 28, 0.3) 40%, rgba(16, 16, 28, 0.3)); } /* ================= */ @@ -13954,12 +15806,15 @@ form.opml_import_form input { .NB-module-search .NB-module-search-sites { float: left; } + .NB-module-search .NB-module-search-people { float: right; } + .NB-module-search .NB-module-search-container { padding: 0 12px; } + .NB-module-search .NB-module-search-input { overflow: hidden; padding: 0 0 12px 12px; @@ -13969,17 +15824,19 @@ form.opml_import_form input { -moz-box-sizing: border-box; box-sizing: border-box; } + .NB-module-search .NB-module-search-input:first-child { padding-left: 0; } + .NB-module-search .NB-module-search-input.NB-active .NB-search-close { display: block; top: 10px; right: 16px; } -.NB-module-search .NB-module-search-input.NB-active .NB-search-close { - -} + +.NB-module-search .NB-module-search-input.NB-active .NB-search-close {} + .NB-module-search-input input { width: 85%; float: left; @@ -13988,18 +15845,21 @@ form.opml_import_form input { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - box-shadow: 0 1px 2px rgba(0,0,0,0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); border: none; } + .NB-module-search .NB-module-search-results { padding: 12px; clear: both; } + .NB-module-search .NB-friends-search-badges-empty { clear: both; font-size: 14px; font-weight: 100; } + .NB-module-search .NB-friends-search-badges-empty .NB-raquo { float: left; padding: 2px 6px; @@ -14009,12 +15869,14 @@ form.opml_import_form input { background: transparent url('/media/embed/reader/ring_spinner.svg') no-repeat right 4px top 4px; background-size: 16px; } + .NB-module-search-input label img { width: 16px; height: 16px; float: left; margin: 7px 6px 0 0; } + .NB-modal-friends .NB-module-search-input label img { margin: 3px 0 0 0; } @@ -14027,14 +15889,15 @@ form.opml_import_form input { clear: both; overflow: hidden; border-bottom: 1px solid #F0F0F0; - box-shadow: 0 1px 2px rgba(0,0,0,0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } + .NB-feed-badge:last-child { border-bottom: none; } -.NB-feed-badge .NB-feed-badge-title { -} +.NB-feed-badge .NB-feed-badge-title {} + .NB-feed-badge .NB-feed-badge-title img { float: left; margin: 0 4px 0 0; @@ -14048,17 +15911,21 @@ form.opml_import_form input { color: #878787; line-height: 17px; } + .NB-feed-badge .NB-feed-badge-stats { float: left; font-size: 11px; } + .NB-feed-badge .NB-feed-badge-stats b { padding-right: 4px; } + .NB-feed-badge .NB-feed-badge-stats .NB-icon { vertical-align: middle; display: inline; } + .NB-feed-badge .NB-icon-stats { margin: 2px 0 0 0; position: absolute; @@ -14071,6 +15938,7 @@ form.opml_import_form input { background-size: 16px; filter: hue-rotate(284deg) saturate(18); } + .NB-feed-badge .NB-feed-badge-stats { position: relative; float: right; @@ -14079,11 +15947,13 @@ form.opml_import_form input { color: #808080; margin: 0 0 0 0; } + .NB-feed-badge .NB-modal-submit-button { float: left; margin-left: 0 !important; margin-right: 8px !important; } + .NB-feed-badge .NB-added { padding: 6px 0 0 20px; background: transparent url('/media/embed/icons/circular/newuser_icn_setup.png') no-repeat 0 2px; @@ -14093,6 +15963,7 @@ form.opml_import_form input { color: #123B00; overflow: hidden; } + .NB-subscribed { padding: 6px 0 0 20px; background: transparent url('/media/embed/icons/circular/newuser_icn_setup.png') no-repeat 0 2px; @@ -14102,6 +15973,7 @@ form.opml_import_form input { color: #123B00; overflow: hidden; } + .NB-static-alert { margin: 24px 0 0 0; text-align: center; @@ -14109,6 +15981,7 @@ form.opml_import_form input { line-height: 24px; color: #294A28; } + .NB-static-feedchooser .NB-feedchooser-premium-bullets li { background-color: rgba(255, 255, 255, .4); list-style: none; diff --git a/media/css/reader/status.css b/media/css/reader/status.css index d0d4dbbffe..95ba34e629 100644 --- a/media/css/reader/status.css +++ b/media/css/reader/status.css @@ -1,21 +1,25 @@ .NB-body-status { - overflow: auto; + overflow: auto; } .NB-status { margin: 48px; } + .NB-status th { padding: 0 6px 0 0; } + .NB-status .NB-favicon { width: 16px; height: 16px; } + .NB-status .NB-status-update { line-height: 12px; font-size: 11px; } + .NB-status td { border-top: 1px solid #F0F0F0; margin: 0; diff --git a/media/img/originals/iOS 15.sketch b/media/img/originals/iOS 15.sketch new file mode 100644 index 0000000000..e807f7aef9 Binary files /dev/null and b/media/img/originals/iOS 15.sketch differ diff --git a/media/js/newsblur/common/assetmodel.js b/media/js/newsblur/common/assetmodel.js index ae06b15e24..a465138e7a 100644 --- a/media/js/newsblur/common/assetmodel.js +++ b/media/js/newsblur/common/assetmodel.js @@ -1,6 +1,6 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ - initialize: function() { + initialize: function () { this.defaults = { classifiers: { titles: {}, @@ -35,21 +35,25 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }; this.ajax = {}; - this.ajax['rapid'] = $.manageAjax.create('rapid', {queue: false}); - this.ajax['queue'] = $.manageAjax.create('queue', {queue: true}); - this.ajax['queue_clear'] = $.manageAjax.create('queue_clear', {queue: 'clear'}); - this.ajax['feed'] = $.manageAjax.create('feed', {queue: 'clear', abortOld: true, - domCompleteTrigger: true}); - this.ajax['feed_page'] = $.manageAjax.create('feed_page', {queue: 'clear', abortOld: true, - abortIsNoSuccess: false, - domCompleteTrigger: true}); - this.ajax['statistics'] = $.manageAjax.create('statistics', {queue: 'clear', abortOld: true}); - this.ajax['interactions'] = $.manageAjax.create('interactions', {queue: 'clear', abortOld: true}); - this.ajax['dashboard'] = $.manageAjax.create('dashboard', {queue: false}); + this.ajax['rapid'] = $.manageAjax.create('rapid', { queue: false }); + this.ajax['queue'] = $.manageAjax.create('queue', { queue: true }); + this.ajax['queue_clear'] = $.manageAjax.create('queue_clear', { queue: 'clear' }); + this.ajax['feed'] = $.manageAjax.create('feed', { + queue: 'clear', abortOld: true, + domCompleteTrigger: true + }); + this.ajax['feed_page'] = $.manageAjax.create('feed_page', { + queue: 'clear', abortOld: true, + abortIsNoSuccess: false, + domCompleteTrigger: true + }); + this.ajax['statistics'] = $.manageAjax.create('statistics', { queue: 'clear', abortOld: true }); + this.ajax['interactions'] = $.manageAjax.create('interactions', { queue: 'clear', abortOld: true }); + this.ajax['dashboard'] = $.manageAjax.create('dashboard', { queue: false }); $.ajaxSettings.traditional = true; }, - - make_request: function(url, data, callback, error_callback, options) { + + make_request: function (url, data, callback, error_callback, options) { var self = this; var options = $.extend({ 'ajax_group': 'queue', @@ -61,7 +65,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, options); var request_type = options.request_type || 'POST'; var clear_queue = false; - + if (options['ajax_group'] == 'feed') { clear_queue = true; } @@ -71,7 +75,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if (options['ajax_group'] == 'interactions') { clear_queue = true; } - + if (clear_queue) { this.ajax[options['ajax_group']].clear(true); } @@ -87,14 +91,14 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ type: request_type, cache: false, cacheResponse: false, - beforeSend: function() { + beforeSend: function () { // NEWSBLUR.log(['beforeSend', options]); $.isFunction(options['beforeSend']) && options['beforeSend'](); return true; }, - success: function(o) { + success: function (o) { // NEWSBLUR.log(['make_request 1', o]); - + var lost_authentication = self.check_authentication_lost(o); if (lost_authentication) { if (options.retry) { @@ -107,7 +111,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } return; } - + if (o && o.code < 0 && error_callback) { error_callback(o, data); } else if ($.isFunction(callback)) { @@ -115,13 +119,13 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } }, - error: function(e, textStatus, errorThrown) { + error: function (e, textStatus, errorThrown) { if (errorThrown == 'abort') { return; } - NEWSBLUR.log(['AJAX Error', e, e.status, textStatus, errorThrown, - !!error_callback, error_callback, $.isFunction(callback)]); - + NEWSBLUR.log(['AJAX Error', e, e.status, textStatus, errorThrown, + !!error_callback, error_callback, $.isFunction(callback)]); + if (options.retry && !NEWSBLUR.Globals.debug) { NEWSBLUR.log(['Retrying...', url, data, !!callback, !!error_callback, options]); options.retry = false; @@ -134,19 +138,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } else if ($.isFunction(callback)) { var message = "Please create an account. Not much
to do without an account."; if (NEWSBLUR.Globals.is_authenticated) { - message = "Sorry, there was an unhandled error."; + message = "Sorry, there was an unhandled error."; } - callback({'message': message, status_code: e.status}, data); + callback({ 'message': message, status_code: e.status }, data); } } - }, options)); - + }, options)); + }, theme: function () { var theme = this.preference('theme'); var is_auto = theme == 'auto'; - + if (is_auto) { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { // dark mode @@ -158,33 +162,33 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ return theme; }, - - mark_story_hash_as_read: function(story, callback, error_callback, data) { + + mark_story_hash_as_read: function (story, callback, error_callback, data) { var self = this; - + if (!story.get('read_status')) { story.set('read_status', 1); - + if (NEWSBLUR.Globals.is_authenticated) { if (!('hashes' in this.queued_read_stories)) { this.queued_read_stories['hashes'] = []; } this.queued_read_stories['hashes'].push(story.get('story_hash')); // NEWSBLUR.log(['Marking Read', this.queued_read_stories, story.id]); - + data = _.extend({ story_hash: this.queued_read_stories['hashes'] }, data); - + this.make_request('/reader/mark_story_hashes_as_read', data, callback, error_callback, { 'ajax_group': 'queue_clear', - 'beforeSend': function() { + 'beforeSend': function () { self.queued_read_stories = {}; } }); } - } + } }, - - mark_social_story_as_read: function(story, social_feed, callback) { + + mark_social_story_as_read: function (story, social_feed, callback) { var self = this; var feed_id = story.get('story_feed_id'); var social_user_id = social_feed && social_feed.get('user_id'); @@ -198,9 +202,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if (!story.get('read_status')) { story.set('read_status', 1); - + if (NEWSBLUR.Globals.is_authenticated) { - if (!(social_user_id in this.queued_read_stories)) { + if (!(social_user_id in this.queued_read_stories)) { this.queued_read_stories[social_user_id] = {}; } if (!(feed_id in this.queued_read_stories[social_user_id])) { @@ -208,22 +212,22 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } this.queued_read_stories[social_user_id][feed_id].push(story.id); // NEWSBLUR.log(['Marking Read', this.queued_read_stories, story.id]); - + this.make_request('/reader/mark_social_stories_as_read', { users_feeds_stories: $.toJSON(this.queued_read_stories) }, null, null, { 'ajax_group': 'queue_clear', - 'beforeSend': function() { + 'beforeSend': function () { self.queued_read_stories = {}; } }); } } - + $.isFunction(callback) && callback(read); }, - - mark_story_as_unread: function(story_id, feed_id, callback, error_callback) { + + mark_story_as_unread: function (story_id, feed_id, callback, error_callback) { var self = this; var read = true; var story = this.get_story(story_id); @@ -236,26 +240,26 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ feed_id: feed_id }, null, error_callback, {}); } - + $.isFunction(callback) && callback(); }, - - mark_story_as_starred: function(story_id, callback) { + + mark_story_as_starred: function (story_id, callback) { var self = this; var story = this.get_story(story_id); var selected = this.starred_feeds.selected(); - - var pre_callback = function(data) { + + var pre_callback = function (data) { if (data.starred_counts) { - self.starred_feeds.reset(data.starred_counts, {parse: true}); + self.starred_feeds.reset(data.starred_counts, { parse: true }); var feed = self.get_feed(story.get('story_feed_id')); if (feed && feed.views) _.invoke(feed.views, 'render'); } - + if (selected) { self.starred_feeds.get(selected).set('selected', true); } - + if (callback) callback(data); }; @@ -266,23 +270,23 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ user_notes: story.get('user_notes') }, pre_callback); }, - - mark_story_as_unstarred: function(story_id, callback) { + + mark_story_as_unstarred: function (story_id, callback) { var self = this; var story = this.get_story(story_id); var selected = this.starred_feeds.selected(); - var pre_callback = function(data) { - if (data.starred_counts) { - self.starred_feeds.reset(data.starred_counts, {parse: true, update: true}); + var pre_callback = function (data) { + if (data.starred_counts) { + self.starred_feeds.reset(data.starred_counts, { parse: true, update: true }); var feed = self.get_feed(story.get('story_feed_id')); if (feed && feed.views) _.invoke(feed.views, 'render'); } - + if (selected && self.starred_feeds.get(selected)) { self.starred_feeds.get(selected).set('selected', true); } - + if (callback) callback(data); }; @@ -290,32 +294,32 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ story_hash: story.get('story_hash') }, pre_callback); }, - - update_starred_counts: function(callback) { - var pre_callback = _.bind(function(data) { - this.starred_feeds.reset(data.starred_counts, {parse: true, update: true}); + + update_starred_counts: function (callback) { + var pre_callback = _.bind(function (data) { + this.starred_feeds.reset(data.starred_counts, { parse: true, update: true }); this.starred_count = data.starred_count; - + if (callback) callback(data); }, this); - - this.make_request('/reader/starred_counts', {}, pre_callback, pre_callback, {request_type: 'GET'}); + + this.make_request('/reader/starred_counts', {}, pre_callback, pre_callback, { request_type: 'GET' }); }, - - mark_feed_as_read: function(feed_id, cutoff_timestamp, direction, options, callback) { + + mark_feed_as_read: function (feed_id, cutoff_timestamp, direction, options, callback) { var self = this; - var feed_ids = _.isArray(feed_id) - ? _.select(feed_id, function(f) { return f; }) - : [feed_id]; - - this.stories.each(function(story) { + var feed_ids = _.isArray(feed_id) + ? _.select(feed_id, function (f) { return f; }) + : [feed_id]; + + this.stories.each(function (story) { if (!_.contains(feed_ids, story.get('story_feed_id'))) return; - if (direction == "older" && - cutoff_timestamp && + if (direction == "older" && + cutoff_timestamp && story.get('story_timestamp') > cutoff_timestamp) { return; - } else if (direction == "newer" && - cutoff_timestamp && + } else if (direction == "newer" && + cutoff_timestamp && story.get('story_timestamp') < cutoff_timestamp) { return; } @@ -335,26 +339,26 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }); if (!cutoff_timestamp) { - _.each(feed_ids, function(feed_id) { + _.each(feed_ids, function (feed_id) { var feed = self.get_feed(feed_id); if (!feed) return; - - feed.set({'ps': 0, 'nt': 0, 'ng': 0}); + + feed.set({ 'ps': 0, 'nt': 0, 'ng': 0 }); }); } - + options = $.extend({ feed_id: feed_ids, cutoff_timestamp: cutoff_timestamp, direction: direction }, options); - - + + this.make_request('/reader/mark_feed_as_read', options, callback); }, - - mark_story_as_shared: function(params, callback, error_callback) { - var pre_callback = _.bind(function(data) { + + mark_story_as_shared: function (params, callback, error_callback) { + var pre_callback = _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } @@ -376,9 +380,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ error_callback(); } }, - - mark_story_as_unshared: function(params, callback, error_callback) { - var pre_callback = _.bind(function(data) { + + mark_story_as_unshared: function (params, callback, error_callback) { + var pre_callback = _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } @@ -386,7 +390,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ story.set(data.story); callback(data); }, this); - + if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/social/unshare_story', { story_id: params.story_id, @@ -397,15 +401,15 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ error_callback(); } }, - - save_comment_reply: function(story_id, story_feed_id, comment_user_id, reply_comments, reply_id, callback, error_callback) { - var pre_callback = _.bind(function(data) { + + save_comment_reply: function (story_id, story_feed_id, comment_user_id, reply_comments, reply_id, callback, error_callback) { + var pre_callback = _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } callback(data); }, this); - + this.make_request('/social/save_comment_reply', { story_id: story_id, story_feed_id: story_feed_id, @@ -414,15 +418,15 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ reply_id: reply_id }, pre_callback, error_callback); }, - - delete_comment_reply: function(story_id, story_feed_id, comment_user_id, reply_id, callback, error_callback) { - var pre_callback = _.bind(function(data) { + + delete_comment_reply: function (story_id, story_feed_id, comment_user_id, reply_id, callback, error_callback) { + var pre_callback = _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } callback(data); }, this); - + this.make_request('/social/remove_comment_reply', { story_id: story_id, story_feed_id: story_feed_id, @@ -430,119 +434,119 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ reply_id: reply_id }, pre_callback, error_callback); }, - - like_comment: function(story_id, story_feed_id, comment_user_id, callback, error_callback) { - var pre_callback = _.bind(function(data) { + + like_comment: function (story_id, story_feed_id, comment_user_id, callback, error_callback) { + var pre_callback = _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } callback && callback(data); }, this); - + this.make_request('/social/like_comment', { story_id: story_id, story_feed_id: story_feed_id, comment_user_id: comment_user_id }, pre_callback, error_callback); }, - - remove_like_comment: function(story_id, story_feed_id, comment_user_id, callback, error_callback) { - var pre_callback = _.bind(function(data) { + + remove_like_comment: function (story_id, story_feed_id, comment_user_id, callback, error_callback) { + var pre_callback = _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } callback && callback(data); }, this); - + this.make_request('/social/remove_like_comment', { story_id: story_id, story_feed_id: story_feed_id, comment_user_id: comment_user_id }, pre_callback, error_callback); }, - - add_user_profiles: function(user_profiles) { - var profiles = _.reject(user_profiles, _.bind(function(profile) { + + add_user_profiles: function (user_profiles) { + var profiles = _.reject(user_profiles, _.bind(function (profile) { return profile.id in this.user_profiles._byId; }, this)); this.user_profiles.add(profiles); }, - - load_feeds: function(callback, error_callback) { + + load_feeds: function (callback, error_callback) { var self = this; var selected = this.feeds.selected(); - var pre_callback = function(feeds, subscriptions) { - self.flags['favicons_fetching'] = self.feeds.any(function(feed) { return feed.get('favicons_fetching'); }); + var pre_callback = function (feeds, subscriptions) { + self.flags['favicons_fetching'] = self.feeds.any(function (feed) { return feed.get('favicons_fetching'); }); - self.folders.reset(_.compact(subscriptions.folders), {parse: true}); + self.folders.reset(_.compact(subscriptions.folders), { parse: true }); self.starred_count = subscriptions.starred_count; - self.starred_feeds.reset(subscriptions.starred_counts, {parse: true}); - self.social_feeds.reset(subscriptions.social_feeds, {parse: true}); + self.starred_feeds.reset(subscriptions.starred_counts, { parse: true }); + self.social_feeds.reset(subscriptions.social_feeds, { parse: true }); self.user_profile.set(subscriptions.social_profile); - self.searches_feeds.reset(subscriptions.saved_searches, {parse: true}); + self.searches_feeds.reset(subscriptions.saved_searches, { parse: true }); self.social_services = subscriptions.social_services; self.dashboard_rivers.reset(subscriptions.dashboard_rivers); - + if (selected && self.feeds.get(selected)) { self.feeds.get(selected).set('selected', true); } if (!_.isEqual(self.favicons, {})) { - self.feeds.each(function(feed) { + self.feeds.each(function (feed) { if (self.favicons[feed.id]) { feed.set('favicon', self.favicons[feed.id]); } }); } - + self.flags['has_chosen_feeds'] = self.feeds.has_chosen_feeds(); - + self.feeds.trigger('reset'); - + callback && callback(); }; - + this.feeds.fetch({ success: pre_callback, error: error_callback }); }, - - load_feed_favicons: function(callback, loaded_once, load_all) { - var pre_callback = _.bind(function(favicons) { - this.favicons = favicons; - if (!_.isEqual(this.feeds, {})) { - this.feeds.each(function(feed) { - if (favicons[feed.id]) { - feed.set('favicon', favicons[feed.id]); - } - }); - } - callback(); + + load_feed_favicons: function (callback, loaded_once, load_all) { + var pre_callback = _.bind(function (favicons) { + this.favicons = favicons; + if (!_.isEqual(this.feeds, {})) { + this.feeds.each(function (feed) { + if (favicons[feed.id]) { + feed.set('favicon', favicons[feed.id]); + } + }); + } + callback(); }, this); var data = { - load_all : load_all + load_all: load_all }; if (loaded_once) { - data['feed_ids'] = _.compact(this.feeds.map(function(feed) { - return !feed.get('favicon') && feed.id; - })); + data['feed_ids'] = _.compact(this.feeds.map(function (feed) { + return !feed.get('favicon') && feed.id; + })); } - this.make_request('/reader/favicons', data, pre_callback, pre_callback, {request_type: 'GET'}); + this.make_request('/reader/favicons', data, pre_callback, pre_callback, { request_type: 'GET' }); }, - - load_feed: function(feed_id, page, first_load, callback, error_callback) { + + load_feed: function (feed_id, page, first_load, callback, error_callback) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { return self.load_feed_precallback(data, feed_id, callback, first_load); }; - + this.feed_id = feed_id; var feed = this.feeds.get(feed_id); - + if (feed_id && feed) { - this.make_request('/reader/feed/'+feed_id, + this.make_request('/reader/feed/' + feed_id, { page: page, feed_address: feed.get('feed_address'), @@ -559,19 +563,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ ); } }, - - load_feed_precallback: function(data, feed_id, callback, first_load) { + + load_feed_precallback: function (data, feed_id, callback, first_load) { var self = this; - + if (data.dupe_feed_id && this.feed_id == data.dupe_feed_id) { feed_id = data.dupe_feed_id; } if (feed_id == this.feed_id) { if (data.feeds) { - var river = _.any(['river:', 'social:'], function(prefix) { + var river = _.any(['river:', 'social:'], function (prefix) { return _.isString(feed_id) && _.string.startsWith(feed_id, prefix); }); - if (river) _.each(data.feeds, function(feed) { feed.temp = true; }); + if (river) _.each(data.feeds, function (feed) { feed.temp = true; }); this.feeds.add(data.feeds); } if (data.classifiers) { @@ -581,19 +585,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ this.classifiers[feed_id] = _.extend({}, this.defaults['classifiers'], data.classifiers); } } - + if (data.user_profiles) { - var profiles = _.reject(data.user_profiles, _.bind(function(profile) { + var profiles = _.reject(data.user_profiles, _.bind(function (profile) { return profile.id in this.user_profiles._byId; }, this)); this.user_profiles.add(profiles); } - + if (data.updated) { var feed = this.get_feed(feed_id); feed.set('updated', data.updated); } - + if (data.stories && first_load) { // console.log(['first load river', data.stories.length, ' stories']); this.feed_tags = data.feed_tags || {}; @@ -608,13 +612,13 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } this.feed_id = feed_id; this.starred_stories = data.starred_stories; - this.stories.reset(data.stories, {added: data.stories.length}); + this.stories.reset(data.stories, { added: data.stories.length }); } else if (data.stories) { // console.log(['adding to river', data.stories.length, ' stories']); - this.stories.add(data.stories, {silent: true}); - this.stories.trigger('add', {added: data.stories.length}); + this.stories.add(data.stories, { silent: true }); + this.stories.trigger('add', { added: data.stories.length }); } - + if (data.stories && !data.stories.length) { this.stories.no_more_stories = true; this.stories.trigger('no_more_stories'); @@ -632,8 +636,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ $.isFunction(callback) && callback(data, first_load); } }, - - check_authentication_lost: function(data) { + + check_authentication_lost: function (data) { if (!NEWSBLUR.Globals.is_authenticated) return false; if (_.isUndefined(data.authenticated)) return false; if (NEWSBLUR.Globals.is_authenticated != data.authenticated && data.authenticated === false) { @@ -641,9 +645,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } return false; }, - - load_canonical_feed: function(feed_id, callback) { - var pre_callback = _.bind(function(data) { + + load_canonical_feed: function (feed_id, callback) { + var pre_callback = _.bind(function (data) { var feed = this.feeds.get(data.id); if (feed) { feed.set(data); @@ -656,21 +660,21 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ this.classifiers[feed_id] = data.classifiers || this.defaults['classifiers']; callback && callback(); }, this); - - this.make_request('/rss_feeds/feed/'+feed_id, {}, pre_callback, $.noop, { + + this.make_request('/rss_feeds/feed/' + feed_id, {}, pre_callback, $.noop, { request_type: 'GET' }); }, - - fetch_starred_stories: function(page, tag, highlights, callback, error_callback, first_load) { + + fetch_starred_stories: function (page, tag, highlights, callback, error_callback, first_load) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { return self.load_feed_precallback(data, 'starred', callback, first_load); }; this.feed_id = 'starred'; - + this.make_request('/reader/starred_stories', { page: page, query: NEWSBLUR.reader.flags.search, @@ -684,10 +688,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }); }, - fetch_read_stories: function(page, callback, error_callback, first_load) { + fetch_read_stories: function (page, callback, error_callback, first_load) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { if (!NEWSBLUR.Globals.is_premium && NEWSBLUR.Globals.is_authenticated) { if (first_load) { data.stories = data.stories.splice(0, 3); @@ -699,7 +703,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }; this.feed_id = 'read'; - + this.make_request('/reader/read_stories', { page: page, query: NEWSBLUR.reader.flags.search, @@ -709,8 +713,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'request_type': 'GET' }); }, - - fetch_river_stories: function(feed_id, feeds, page, options, callback, error_callback, first_load) { + + fetch_river_stories: function (feed_id, feeds, page, options, callback, error_callback, first_load) { var self = this; options = $.extend({ feeds: feeds, @@ -721,8 +725,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ include_hidden: true, infrequent: false }, options); - - var pre_callback = function(data) { + + var pre_callback = function (data) { if (!NEWSBLUR.Globals.is_premium && NEWSBLUR.Globals.is_authenticated) { if (first_load) { data.stories = data.stories.splice(0, 3); @@ -730,20 +734,20 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ data.stories = []; } } - + self.load_feed_precallback(data, feed_id, callback, first_load); }; - + this.feed_id = feed_id; this.make_request('/reader/river_stories', options, pre_callback, error_callback, { 'ajax_group': (page ? 'feed_page' : 'feed'), 'request_type': 'GET' }); - + }, - - complete_river: function(feed_id, feeds, page, callback) { + + complete_river: function (feed_id, feeds, page, callback) { this.make_request('/reader/complete_river', { feeds: feeds, page: page, @@ -752,8 +756,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'ajax_group': 'dashboard' }); }, - - fetch_dashboard_stories: function(feed_id, feeds, page, dashboard_stories, options, callback, error_callback) { + + fetch_dashboard_stories: function (feed_id, feeds, page, dashboard_stories, options, callback, error_callback) { var self = this; options = $.extend({ feeds: feeds, @@ -768,7 +772,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ on_dashboard: true }, options); - dashboard_stories.comparator = function(a, b) { + dashboard_stories.comparator = function (a, b) { var a_time = parseInt(a.get('story_timestamp'), 10); var b_time = parseInt(b.get('story_timestamp'), 10); if (options.order == "newest") @@ -776,8 +780,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ else return a_time > b_time ? 1 : (a_time == b_time) ? 0 : -1; }; - - var pre_callback = function(data) { + + var pre_callback = function (data) { if (data.user_profiles) { self.add_user_profiles(data.user_profiles); } @@ -789,24 +793,24 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ dashboard_stories.add(data.stories, { silent: true }); dashboard_stories.limit_visible_on_dashboard(count); // dashboard_stories.trigger('add', { added: data.stories.length }); - dashboard_stories.trigger('reset', {added: data.stories.length}); + dashboard_stories.trigger('reset', { added: data.stories.length }); } else { dashboard_stories.reset(data.stories, { added: data.stories.length, silent: true }); dashboard_stories.limit_visible_on_dashboard(count); - dashboard_stories.trigger('reset', {added: data.stories.length}); + dashboard_stories.trigger('reset', { added: data.stories.length }); } if (data.feeds) { - var river = _.any(['river:', 'social:'], function(prefix) { + var river = _.any(['river:', 'social:'], function (prefix) { return _.isString(feed_id) && _.string.startsWith(feed_id, prefix); }); - if (river) _.each(data.feeds, function(feed) { feed.temp = true; }); + if (river) _.each(data.feeds, function (feed) { feed.temp = true; }); self.feeds.add(data.feeds); } // self.load_feed_precallback(data, feed_id, callback); callback(data); }; - + if (_.string.startsWith(feed_id, 'river:global')) { this.make_request('/social/river_stories', options, pre_callback, error_callback, { 'ajax_group': 'dashboard', @@ -814,7 +818,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }); } else if (_.string.startsWith(feed_id, 'social:')) { var user_id = this.get_feed(feed_id).get('user_id'); - this.make_request('/social/stories/'+user_id+'/', options, pre_callback, error_callback, { + this.make_request('/social/stories/' + user_id + '/', options, pre_callback, error_callback, { 'ajax_group': 'dashboard', 'request_type': 'GET' }); @@ -824,12 +828,12 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'request_type': 'GET' }); } - + }, - - add_dashboard_story: function(story_hash, dashboard_stories, dashboard_count) { + + add_dashboard_story: function (story_hash, dashboard_stories, dashboard_count) { var self = this; - + var pre_callback = (function (dashboard_stories, dashboard_count) { return function (data) { @@ -838,11 +842,11 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ dashboard_stories.trigger('reset', { added: 1 }); }; })(dashboard_stories, dashboard_count); - + if (!('hashes' in this.queued_realtime_stories)) { this.queued_realtime_stories['hashes'] = []; } this.queued_realtime_stories['hashes'].push(story_hash); // NEWSBLUR.log(['Marking real-time load', this.queued_realtime_stories['hashes'], story_hash]); - + this.throttled_add_dashboard_story = this.throttled_add_dashboard_story || _.throttle(_.bind(function () { this.make_request('/reader/river_stories', { h: this.queued_realtime_stories['hashes'], @@ -858,14 +862,14 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, this), 1000); this.throttled_add_dashboard_story(); }, - - fetch_river_blurblogs_stories: function(feed_id, page, options, callback, error_callback, first_load) { + + fetch_river_blurblogs_stories: function (feed_id, page, options, callback, error_callback, first_load) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { self.load_feed_precallback(data, feed_id, callback, first_load); }; - + this.feed_id = feed_id; this.make_request('/social/river_stories', { @@ -878,18 +882,18 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'request_type': 'GET' }); }, - - fetch_social_stories: function(feed_id, page, callback, error_callback, first_load) { + + fetch_social_stories: function (feed_id, page, callback, error_callback, first_load) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { return self.load_feed_precallback(data, feed_id, callback, first_load); }; - + this.feed_id = feed_id; var user_id = this.get_feed(feed_id).get('user_id'); - this.make_request('/social/stories/'+user_id+'/', { + this.make_request('/social/stories/' + user_id + '/', { page: page, order: this.view_setting(feed_id, 'order'), read_filter: this.view_setting(feed_id, 'read_filter'), @@ -899,8 +903,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'request_type': 'GET' }); }, - - fetch_story_changes: function(story_hash, show_changes, callback, error_callback) { + + fetch_story_changes: function (story_hash, show_changes, callback, error_callback) { this.make_request('/rss_feeds/story_changes', { story_hash: story_hash, show_changes: show_changes @@ -908,55 +912,55 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'request_type': 'GET' }); }, - - get_feeds_trainer: function(feed_id, callback) { + + get_feeds_trainer: function (feed_id, callback) { var self = this; var params = {}; - + if (feed_id) { - params['feed_id'] = feed_id; + params['feed_id'] = feed_id; } - - this.make_request('/reader/feeds_trainer', params, callback, null, {'ajax_group': 'feed', 'request_type': 'GET'}); - }, - - get_social_trainer: function(feed_id, callback) { + + this.make_request('/reader/feeds_trainer', params, callback, null, { 'ajax_group': 'feed', 'request_type': 'GET' }); + }, + + get_social_trainer: function (feed_id, callback) { var self = this; var params = {}; - + if (feed_id) { - params['user_id'] = feed_id.replace('social:', ''); + params['user_id'] = feed_id.replace('social:', ''); } - - this.make_request('/social/feed_trainer', params, callback, null, {'ajax_group': 'feed', 'request_type': 'GET'}); - }, - - retrain_all_sites: function(callback) { + + this.make_request('/social/feed_trainer', params, callback, null, { 'ajax_group': 'feed', 'request_type': 'GET' }); + }, + + retrain_all_sites: function (callback) { var self = this; var params = {}; - + if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/retrain_all_sites', params, callback, null); } else { if ($.isFunction(callback)) callback(); } - }, - - refresh_feeds: function(callback, has_unfetched_feeds, feed_id, error_callback) { + }, + + refresh_feeds: function (callback, has_unfetched_feeds, feed_id, error_callback) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { self.post_refresh_feeds(data, callback, { 'refresh_feeds': true }); }; - + var data = {}; if (has_unfetched_feeds) { data['check_fetch_status'] = has_unfetched_feeds; } if (this.flags['favicons_fetching']) { - var favicons_fetching = _.pluck(this.feeds.select(function(feed, k) { + var favicons_fetching = _.pluck(this.feeds.select(function (feed, k) { return feed.get('favicon_fetching') && feed.get('active'); }), 'id'); if (favicons_fetching.length) { @@ -968,41 +972,41 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if (feed_id) { data['feed_id'] = feed_id; } - + if (NEWSBLUR.Globals.is_authenticated || feed_id) { this.make_request('/reader/refresh_feeds', data, pre_callback, error_callback); } }, - - feed_unread_count: function(feed_id, callback, error_callback) { + + feed_unread_count: function (feed_id, callback, error_callback) { var self = this; - - var pre_callback = function(data) { + + var pre_callback = function (data) { self.post_refresh_feeds(data, callback, { 'refresh_feeds': false }); }; - + if (NEWSBLUR.Globals.is_authenticated || feed_id) { - this.make_request('/reader/feed_unread_count', { + this.make_request('/reader/feed_unread_count', { 'feed_id': feed_id - }, pre_callback, error_callback, {request_type: 'GET'}); + }, pre_callback, error_callback, { request_type: 'GET' }); } }, - - post_refresh_feeds: function(data, callback, options) { + + post_refresh_feeds: function (data, callback, options) { if (!data.feeds) return; - + options = options || {}; - - _.each(data.feeds, _.bind(function(feed, feed_id) { + + _.each(data.feeds, _.bind(function (feed, feed_id) { var existing_feed = this.feeds.get(feed_id); if (!existing_feed) { console.log(["Trying to refresh unsub feed", feed_id, feed, this.feeds.length]); return; } var feed_id = feed.id || feed_id; - + if (feed.id && feed_id != feed.id) { NEWSBLUR.log(['Dupe feed being refreshed', feed_id, feed.id, this.feeds.get(f), feed]); this.feeds.get(feed.id).set(feed); @@ -1018,37 +1022,37 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'favicon_fetching': false }); } - + if (existing_feed.get('selected') && options.refresh_feeds) { existing_feed.force_update_counts(); } else { existing_feed.set(feed, options); } }, this)); - - _.each(data.social_feeds, _.bind(function(feed) { + + _.each(data.social_feeds, _.bind(function (feed) { var social_feed = this.social_feeds.get(feed.id); if (!social_feed) return; - + social_feed.set(feed); }, this)); - + callback && callback(data); }, - - refresh_feed: function(feed_id, callback) { + + refresh_feed: function (feed_id, callback) { var self = this; var feed = this.feeds.get(feed_id); if (!feed) return; - - var pre_callback = function(data) { + + var pre_callback = function (data) { // NEWSBLUR.log(['refresh_feed pre_callback', data]); self.load_feed_precallback(data, feed_id, callback); }; - + // NEWSBLUR.log(['refresh_feed', feed_id, page, first_load, callback, pre_callback]); if (feed_id) { - this.make_request('/reader/feed/'+feed_id, + this.make_request('/reader/feed/' + feed_id, { page: 0, feed_address: feed.get('feed_address') @@ -1061,15 +1065,15 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ ); } }, - - interactions_count: function(callback, error_callback) { + + interactions_count: function (callback, error_callback) { this.make_request('/reader/interactions_count', {}, callback, error_callback, { 'request_type': 'GET' }); }, - - count_unfetched_feeds: function() { - var counts = this.feeds.reduce(function(counts, feed) { + + count_unfetched_feeds: function () { + var counts = this.feeds.reduce(function (counts, feed) { if (feed.get('active')) { if (feed.get('fetched_once') || feed.get('has_exception')) { counts['fetched_feeds'] += 1; @@ -1082,17 +1086,17 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'unfetched_feeds': 0, 'fetched_feeds': 0 }); - + return counts; }, - - unfetched_feeds: function() { - return this.feeds.filter(function(feed) { + + unfetched_feeds: function () { + return this.feeds.filter(function (feed) { return feed.get('active') && !feed.get('fetched_once') && !feed.get('has_exception'); }); }, - - set_feed: function(feed_id, feed) { + + set_feed: function (feed_id, feed) { if (!feed) { feed = feed_id; feed_id = feed.id; @@ -1102,11 +1106,11 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } else { this.feeds.get(feed_id).set(feed); } - + return this.feeds.get(feed_id); }, - add_social_feed: function(feed) { + add_social_feed: function (feed) { var social_feed = this.social_feeds.get(feed); if (!social_feed) { var attributes = feed.attributes; @@ -1116,10 +1120,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } return social_feed; }, - - get_feed: function(feed_id) { + + get_feed: function (feed_id) { var self = this; - + if (_.string.startsWith(feed_id, 'social:')) { return this.social_feeds.get(feed_id); } else if (_.string.startsWith(feed_id, 'starred:')) { @@ -1134,69 +1138,69 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ return this.feeds.get(feed_id); } }, - - get_friend_feeds: function(story) { + + get_friend_feeds: function (story) { var shares = story.get('shared_by_friends') || []; var comments = story.get('commented_by_friends') || []; var friend_user_ids = shares.concat(comments); - return _.map(friend_user_ids, _.bind(function(user_id) { - return this.social_feeds.get('social:'+user_id); + return _.map(friend_user_ids, _.bind(function (user_id) { + return this.social_feeds.get('social:' + user_id); }, this)); }, - - get_feeds: function() { + + get_feeds: function () { var self = this; - + return this.feeds; }, - - get_social_feeds: function() { + + get_social_feeds: function () { var self = this; - + return this.social_feeds; }, - - get_starred_feeds: function() { + + get_starred_feeds: function () { var self = this; - + return this.starred_feeds; }, - - get_search_feeds: function(feed_id, query) { + + get_search_feeds: function (feed_id, query) { var self = this; if (!feed_id && !query) return this.searches_feeds; - - return this.searches_feeds.detect(function(feed) { + + return this.searches_feeds.detect(function (feed) { if (!query) { return feed.id == feed_id; } return feed.get('query') == query && feed.get('feed_id') == feed_id; }); }, - - get_folders: function() { + + get_folders: function () { var self = this; - + return this.folders; }, - - get_folder: function(folder_name) { + + get_folder: function (folder_name) { if (_.string.startsWith(folder_name, 'river:')) { folder_name = folder_name.replace('river:', ''); } return this.folders.find_folder(folder_name.toLowerCase()); }, - - get_feed_tags: function() { + + get_feed_tags: function () { return this.feed_tags; }, - - get_feed_authors: function() { + + get_feed_authors: function () { return this.feed_authors; }, - - get_story: function(story_id) { + + get_story: function (story_id) { var self = this; var story = this.stories.get(story_id); if (!story) { @@ -1206,48 +1210,48 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ } return story; }, - - get_user: function(user_id) { + + get_user: function (user_id) { var user = this.user_profiles.find(user_id); if (!user && user_id == this.user_profile.get('user_id')) { user = this.profile; } - + return user; }, - - save_classifier: function(data, callback) { + + save_classifier: function (data, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/classifier/save', data, callback); } else { if ($.isFunction(callback)) callback(); } }, - - get_feed_classifier: function(feed_id, callback) { - this.make_request('/classifier/'+feed_id, {}, callback, null, { + + get_feed_classifier: function (feed_id, callback) { + this.make_request('/classifier/' + feed_id, {}, callback, null, { 'ajax_group': 'feed', 'request_type': 'GET' }); }, - - delete_feed: function(feed_id, in_folder, callback) { + + delete_feed: function (feed_id, in_folder, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/delete_feed', { - 'feed_id': feed_id, + 'feed_id': feed_id, 'in_folder': in_folder }, callback, null); } else { if ($.isFunction(callback)) callback(); } }, - - delete_feeds_by_folder: function(feeds_by_folder, callback, error_callback) { - var pre_callback = _.bind(function(data) { - _.each(feeds_by_folder, _.bind(function(feed_in_folder) { + + delete_feeds_by_folder: function (feeds_by_folder, callback, error_callback) { + var pre_callback = _.bind(function (data) { + _.each(feeds_by_folder, _.bind(function (feed_in_folder) { this.feeds.remove(feed_in_folder[0]); }, this)); - this.folders.reset(_.compact(data.folders), {parse: true}); + this.folders.reset(_.compact(data.folders), { parse: true }); return callback(); }, this); @@ -1255,20 +1259,20 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'feeds_by_folder': $.toJSON(feeds_by_folder) }, pre_callback, error_callback); }, - - delete_feed_by_url: function(url, in_folder, callback) { + + delete_feed_by_url: function (url, in_folder, callback) { this.make_request('/reader/delete_feed_by_url/', { 'url': url, 'in_folder': in_folder || '' - }, callback, function() { - callback({'message': NEWSBLUR.Globals.is_anonymous ? 'Please create an account. Not much to do without an account.' : 'There was a problem trying to add this site. Please try a different URL.'}); + }, callback, function () { + callback({ 'message': NEWSBLUR.Globals.is_anonymous ? 'Please create an account. Not much to do without an account.' : 'There was a problem trying to add this site. Please try a different URL.' }); }); }, - - delete_folder: function(folder_name, in_folder, feeds, callback) { + + delete_folder: function (folder_name, in_folder, feeds, callback) { var self = this; - var pre_callback = function(data) { - self.folders.reset(_.compact(data.folders), {parse: true}); + var pre_callback = function (data) { + self.folders.reset(_.compact(data.folders), { parse: true }); self.feeds.trigger('reset'); callback(data); @@ -1283,53 +1287,53 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if ($.isFunction(callback)) callback(); } }, - - rename_feed: function(feed_id, feed_title, callback) { + + rename_feed: function (feed_id, feed_title, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/rename_feed', { - 'feed_id' : feed_id, - 'feed_title' : feed_title + 'feed_id': feed_id, + 'feed_title': feed_title }, callback, null); } else { if ($.isFunction(callback)) callback(); } }, - - rename_folder: function(folder_name, new_folder_name, in_folder, callback) { + + rename_folder: function (folder_name, new_folder_name, in_folder, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/rename_folder', { - 'folder_name' : folder_name, - 'new_folder_name' : new_folder_name, - 'in_folder' : in_folder + 'folder_name': folder_name, + 'new_folder_name': new_folder_name, + 'in_folder': in_folder }, callback, null); } else { if ($.isFunction(callback)) callback(); } }, - - save_add_url: function(url, folder, callback, options) { - options = _.extend({'auto_active': true}, options); + + save_add_url: function (url, folder, callback, options) { + options = _.extend({ 'auto_active': true }, options); this.make_request('/reader/add_url/', { 'url': url, 'folder': folder, 'auto_active': options.auto_active - }, callback, function(data) { - callback({'message': NEWSBLUR.Globals.is_anonymous ? 'Please create an account. Not much to do without an account.' : data.message || 'There was a problem trying to add this site. Please try a different URL.'}); + }, callback, function (data) { + callback({ 'message': NEWSBLUR.Globals.is_anonymous ? 'Please create an account. Not much to do without an account.' : data.message || 'There was a problem trying to add this site. Please try a different URL.' }); }); }, - - save_add_folder: function(folder, parent_folder, callback) { + + save_add_folder: function (folder, parent_folder, callback) { this.make_request('/reader/add_folder/', { 'folder': folder, 'parent_folder': parent_folder - }, callback, function(data) { - callback({'message': NEWSBLUR.Globals.is_anonymous ? 'Please create an account. Not much to do without an account.' : data.message || 'There was a problem trying to add this folder.'}); + }, callback, function (data) { + callback({ 'message': NEWSBLUR.Globals.is_anonymous ? 'Please create an account. Not much to do without an account.' : data.message || 'There was a problem trying to add this folder.' }); }); }, - - move_feed_to_folder: function(feed_id, in_folder, to_folder, callback) { - var pre_callback = _.bind(function(data) { - this.folders.reset(_.compact(data.folders), {parse: true}); + + move_feed_to_folder: function (feed_id, in_folder, to_folder, callback) { + var pre_callback = _.bind(function (data) { + this.folders.reset(_.compact(data.folders), { parse: true }); return callback(); }, this); @@ -1339,10 +1343,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'to_folder': to_folder }, pre_callback); }, - - move_feed_to_folders: function(feed_id, in_folders, to_folders, callback) { - var pre_callback = _.bind(function(data) { - this.folders.reset(_.compact(data.folders), {parse: true}); + + move_feed_to_folders: function (feed_id, in_folders, to_folders, callback) { + var pre_callback = _.bind(function (data) { + this.folders.reset(_.compact(data.folders), { parse: true }); return callback(); }, this); @@ -1352,10 +1356,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'to_folders': to_folders }, pre_callback); }, - - move_folder_to_folder: function(folder_name, in_folder, to_folder, callback) { - var pre_callback = _.bind(function(data) { - this.folders.reset(_.compact(data.folders), {parse: true}); + + move_folder_to_folder: function (folder_name, in_folder, to_folder, callback) { + var pre_callback = _.bind(function (data) { + this.folders.reset(_.compact(data.folders), { parse: true }); return callback(); }, this); @@ -1365,10 +1369,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'to_folder': to_folder }, pre_callback); }, - - move_feeds_by_folder: function(feeds_by_folder, to_folder, new_folder, callback, error_callback) { - var pre_callback = _.bind(function(data) { - this.folders.reset(_.compact(data.folders), {parse: true}); + + move_feeds_by_folder: function (feeds_by_folder, to_folder, new_folder, callback, error_callback) { + var pre_callback = _.bind(function (data) { + this.folders.reset(_.compact(data.folders), { parse: true }); return callback(); }, this); @@ -1378,18 +1382,18 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ 'new_folder': new_folder }, pre_callback, error_callback); }, - - preference: function(preference, value, callback) { + + preference: function (preference, value, callback) { if (typeof value == 'undefined') { var pref = NEWSBLUR.Preferences[preference]; if ((/^\d+$/).test(pref)) return parseInt(pref, 10); return pref; } - + if (NEWSBLUR.Preferences[preference] == value) { - return $.isFunction(callback) && callback(); + return $.isFunction(callback) && callback(); } - + NEWSBLUR.Preferences[preference] = value; var preferences = {}; preferences[preference] = value; @@ -1399,33 +1403,33 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if (callback) callback(); } }, - - save_preferences: function(preferences, callback) { - _.each(preferences, function(value, preference) { + + save_preferences: function (preferences, callback) { + _.each(preferences, function (value, preference) { NEWSBLUR.Preferences[preference] = value; }); - + this.make_request('/profile/set_preference', preferences, callback, null); }, - - save_account_settings: function(settings, callback) { + + save_account_settings: function (settings, callback) { var self = this; - this.make_request('/profile/set_account_settings', settings, function(data) { + this.make_request('/profile/set_account_settings', settings, function (data) { if (data.social_profile) { self.user_profile.set(data.social_profile); } callback(data); }, null); }, - - view_setting: function(feed_id, setting, callback) { - if (NEWSBLUR.reader.flags['feed_list_showing_starred'] && + + view_setting: function (feed_id, setting, callback) { + if (NEWSBLUR.reader.flags['feed_list_showing_starred'] && setting == 'read_filter') return "starred"; if (feed_id == "river:global" && setting == "order") return "newest"; if (_.isUndefined(setting) || _.isString(setting)) { setting = setting || 'view'; var s = setting.substr(0, 1); - var feed = NEWSBLUR.Preferences.view_settings[feed_id+'']; + var feed = NEWSBLUR.Preferences.view_settings[feed_id + '']; var default_setting = NEWSBLUR.Preferences['default_' + setting]; if (setting == 'layout') default_setting = NEWSBLUR.Preferences['story_layout']; if (setting == 'read_filter' && _.string.contains(feed_id, 'river:')) { @@ -1435,44 +1439,44 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ // if (view_setting == "magazine") view_setting = "list"; return view_setting; } - - var view_settings = _.clone(NEWSBLUR.Preferences.view_settings[feed_id+'']) || {}; + + var view_settings = _.clone(NEWSBLUR.Preferences.view_settings[feed_id + '']) || {}; if (_.isString(view_settings)) { - view_settings = {'view': view_settings}; + view_settings = { 'view': view_settings }; } - var params = {'feed_id': feed_id+''}; - _.each(['view', 'order', 'read_filter', 'layout', 'dashboard_count'], function(facet) { + var params = { 'feed_id': feed_id + '' }; + _.each(['view', 'order', 'read_filter', 'layout', 'dashboard_count'], function (facet) { if (setting[facet]) { view_settings[facet.substr(0, 1)] = setting[facet]; - params['feed_'+facet+'_setting'] = setting[facet]; + params['feed_' + facet + '_setting'] = setting[facet]; } }); - - if (!_.isEqual(NEWSBLUR.Preferences.view_settings[feed_id+''], view_settings)) { - NEWSBLUR.Preferences.view_settings[feed_id+''] = view_settings; + + if (!_.isEqual(NEWSBLUR.Preferences.view_settings[feed_id + ''], view_settings)) { + NEWSBLUR.Preferences.view_settings[feed_id + ''] = view_settings; this.make_request('/profile/set_view_setting', params, callback, null); return true; } }, - - clear_view_settings: function(view_setting_type, callback) { - var pre_callback = _.bind(function(data) { + + clear_view_settings: function (view_setting_type, callback) { + var pre_callback = _.bind(function (data) { if (data.view_settings) { NEWSBLUR.Preferences.view_settings = data.view_settings; } callback(data); }, this); - + this.make_request('/profile/clear_view_setting', { view_setting_type: view_setting_type }, pre_callback, null); - + }, - - collapsed_folders: function(folder_title, is_collapsed, callback) { + + collapsed_folders: function (folder_title, is_collapsed, callback) { var folders = NEWSBLUR.Preferences.collapsed_folders; var changed = false; - + if (is_collapsed && !_.contains(NEWSBLUR.Preferences.collapsed_folders, folder_title)) { NEWSBLUR.Preferences.collapsed_folders.push(folder_title); changed = true; @@ -1480,107 +1484,107 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ NEWSBLUR.Preferences.collapsed_folders = _.without(folders, folder_title); changed = true; } - + if (changed) { this.make_request('/profile/set_collapsed_folders', { 'collapsed_folders': $.toJSON(NEWSBLUR.Preferences.collapsed_folders) }, callback, null); } }, - - save_mark_read: function(days, callback) { - this.make_request('/reader/mark_all_as_read', {'days': days}, callback); + + save_mark_read: function (days, callback) { + this.make_request('/reader/mark_all_as_read', { 'days': days }, callback); }, - - get_features_page: function(page, callback, error_callback) { - this.make_request('/reader/features', {'page': page}, callback, error_callback, { + + get_features_page: function (page, callback, error_callback) { + this.make_request('/reader/features', { 'page': page }, callback, error_callback, { 'ajax_group': 'queue', request_type: 'GET' }); }, - - load_recommended_feed: function(page, refresh, unmoderated, callback, error_callback) { + + load_recommended_feed: function (page, refresh, unmoderated, callback, error_callback) { this.make_request('/recommendations/load_recommended_feed', { - 'page' : page, - 'refresh' : refresh, - 'unmoderated' : unmoderated + 'page': page, + 'refresh': refresh, + 'unmoderated': unmoderated }, callback, error_callback, { 'ajax_group': 'queue', request_type: 'GET' }); }, - - load_interactions_page: function(page, callback, error_callback) { + + load_interactions_page: function (page, callback, error_callback) { this.make_request('/social/interactions', { 'page': page, 'format': 'html' - }, function(data) { + }, function (data) { callback(data, 'interactions'); }, error_callback, { 'ajax_group': 'interactions', 'request_type': 'GET' }); }, - - load_activities_page: function(page, callback, error_callback) { + + load_activities_page: function (page, callback, error_callback) { this.make_request('/profile/activities', { 'page': page, 'format': 'html' - }, function(data) { + }, function (data) { callback(data, 'activities'); }, error_callback, { 'ajax_group': 'interactions', 'request_type': 'GET' }); }, - - cancel_premium_subscription: function(callback, error_callback) { + + cancel_premium_subscription: function (callback, error_callback) { this.make_request('/profile/cancel_premium', {}, callback, error_callback); }, - - approve_feed_in_moderation_queue: function(feed_id, date, callback) { + + approve_feed_in_moderation_queue: function (feed_id, date, callback) { this.make_request('/recommendations/approve_feed', { - 'feed_id' : feed_id, - 'date' : date, - 'unmoderated' : true - }, callback, {request_type: 'GET'}); + 'feed_id': feed_id, + 'date': date, + 'unmoderated': true + }, callback, { request_type: 'GET' }); }, - - decline_feed_in_moderation_queue: function(feed_id, callback) { + + decline_feed_in_moderation_queue: function (feed_id, callback) { this.make_request('/recommendations/decline_feed', { - 'feed_id' : feed_id, - 'unmoderated' : true - }, callback, {request_type: 'GET'}); + 'feed_id': feed_id, + 'unmoderated': true + }, callback, { request_type: 'GET' }); }, - - load_dashboard_graphs: function(callback, error_callback) { + + load_dashboard_graphs: function (callback, error_callback) { this.make_request('/statistics/dashboard_graphs', {}, callback, error_callback, { 'ajax_group': 'statistics', request_type: 'GET' }); }, - - load_feedback_table: function(callback, error_callback) { + + load_feedback_table: function (callback, error_callback) { this.make_request('/statistics/feedback_table', {}, callback, error_callback, { 'ajax_group': 'queue', request_type: 'GET' }); }, - - save_feed_order: function(folders, callback) { - this.make_request('/reader/save_feed_order', {'folders': $.toJSON(folders)}, callback); + + save_feed_order: function (folders, callback) { + this.make_request('/reader/save_feed_order', { 'folders': $.toJSON(folders) }, callback); }, - - save_search: function(feed_id, query, callback) { + + save_search: function (feed_id, query, callback) { var self = this; - var pre_callback = function(data) { + var pre_callback = function (data) { if (data.saved_searches) { - self.searches_feeds.reset(data.saved_searches, {parse: true}); + self.searches_feeds.reset(data.saved_searches, { parse: true }); } - + if (callback) callback(data); }; - + if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/save_search', { 'feed_id': feed_id, @@ -1588,19 +1592,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, pre_callback); } else { if ($.isFunction(callback)) callback(); - } + } }, - - delete_saved_search: function(feed_id, query, callback) { + + delete_saved_search: function (feed_id, query, callback) { var self = this; - var pre_callback = function(data) { + var pre_callback = function (data) { if (data.saved_searches) { - self.searches_feeds.reset(data.saved_searches, {parse: true}); + self.searches_feeds.reset(data.saved_searches, { parse: true }); } - + if (callback) callback(data); }; - + if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/delete_search', { 'feed_id': feed_id, @@ -1608,77 +1612,77 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, pre_callback); } else { if ($.isFunction(callback)) callback(); - } + } }, - - get_feed_statistics: function(feed_id, callback) { - this.make_request('/rss_feeds/statistics/'+feed_id, {}, callback, callback, { + + get_feed_statistics: function (feed_id, callback) { + this.make_request('/rss_feeds/statistics/' + feed_id, {}, callback, callback, { 'ajax_group': 'statistics', 'request_type': 'GET' }); }, - - get_social_statistics: function(social_feed_id, callback) { - this.make_request('/social/statistics/'+_.string.ltrim(social_feed_id, 'social:'), {}, callback, callback, { + + get_social_statistics: function (social_feed_id, callback) { + this.make_request('/social/statistics/' + _.string.ltrim(social_feed_id, 'social:'), {}, callback, callback, { 'ajax_group': 'statistics', 'request_type': 'GET' }); }, - - get_feed_recommendation_info: function(feed_id, callback) { - this.make_request('/recommendations/load_feed_info/'+feed_id, {}, callback, callback, { + + get_feed_recommendation_info: function (feed_id, callback) { + this.make_request('/recommendations/load_feed_info/' + feed_id, {}, callback, callback, { 'ajax_group': 'statistics', 'request_type': 'GET' }); }, - - get_feed_settings: function(feed_id, callback) { - this.make_request('/rss_feeds/feed_settings/'+feed_id, {}, callback, callback, { + + get_feed_settings: function (feed_id, callback) { + this.make_request('/rss_feeds/feed_settings/' + feed_id, {}, callback, callback, { 'ajax_group': 'statistics', 'request_type': 'GET' }); }, - - get_social_settings: function(social_feed_id, callback) { - this.make_request('/social/settings/'+_.string.ltrim(social_feed_id, 'social:'), {}, callback, callback, { + + get_social_settings: function (social_feed_id, callback) { + this.make_request('/social/settings/' + _.string.ltrim(social_feed_id, 'social:'), {}, callback, callback, { 'ajax_group': 'statistics', 'request_type': 'GET' }); }, - - save_recommended_site: function(data, callback) { + + save_recommended_site: function (data, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/recommendations/save_recommended_feed', data, callback); } else { if ($.isFunction(callback)) callback(); } }, - - save_exception_retry: function(feed_id, callback, error_callback) { + + save_exception_retry: function (feed_id, callback, error_callback) { var self = this; var feed = this.feeds.get(feed_id); if (!feed) return; - var pre_callback = function(data) { + var pre_callback = function (data) { // NEWSBLUR.log(['refresh_feed pre_callback', data]); self.post_refresh_feeds(data, callback); }; - + this.make_request('/rss_feeds/exception_retry', { - 'feed_id': feed_id, - 'reset_fetch': !!(feed.get('has_feed_exception') || - feed.get('has_page_exception')) + 'feed_id': feed_id, + 'reset_fetch': !!(feed.get('has_feed_exception') || + feed.get('has_page_exception')) }, pre_callback, error_callback); }, - - save_exception_change_feed_link: function(feed_id, feed_link, callback, error_callback) { + + save_exception_change_feed_link: function (feed_id, feed_link, callback, error_callback) { var self = this; - + if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/rss_feeds/exception_change_feed_link', { 'feed_id': feed_id, 'feed_link': feed_link - }, function(data) { + }, function (data) { // NEWSBLUR.log(['save_exception_change_feed_link pre_callback', feed_id, feed_link, data]); if (data.code < 0 || data.status_code != 200) { return callback(data); @@ -1689,15 +1693,15 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if ($.isFunction(callback)) callback(); } }, - - save_exception_change_feed_address: function(feed_id, feed_address, callback, error_callback) { + + save_exception_change_feed_address: function (feed_id, feed_address, callback, error_callback) { var self = this; - + if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/rss_feeds/exception_change_feed_address', { 'feed_id': feed_id, 'feed_address': feed_address - }, function(data) { + }, function (data) { // NEWSBLUR.log(['save_exception_change_feed_address pre_callback', feed_id, feed_address, data]); if (data.code < 0 || data.status_code != 200) { return callback(data); @@ -1708,18 +1712,18 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ if ($.isFunction(callback)) callback(); } }, - - save_feed_chooser: function(approved_feeds, callback) { + + save_feed_chooser: function (approved_feeds, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/reader/save_feed_chooser', { - 'approved_feeds': approved_feeds && _.select(approved_feeds, function(f) { return f; }) + 'approved_feeds': approved_feeds && _.select(approved_feeds, function (f) { return f; }) }, callback); } else { if ($.isFunction(callback)) callback(); } }, - - set_notifications_for_feed: function(feed, callback) { + + set_notifications_for_feed: function (feed, callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/notifications/feed/', { 'feed_id': feed.id, @@ -1728,41 +1732,41 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, callback); } else { if ($.isFunction(callback)) callback(); - } + } }, - - send_story_email: function(data, callback, error_callback) { + + send_story_email: function (data, callback, error_callback) { if (NEWSBLUR.Globals.is_authenticated) { - this.make_request('/reader/send_story_email', data, callback, error_callback, {'timeout': 6000}); + this.make_request('/reader/send_story_email', data, callback, error_callback, { 'timeout': 6000 }); } else { - callback({'code': -1, 'message': 'You must be logged in to send a story over email.'}); + callback({ 'code': -1, 'message': 'You must be logged in to send a story over email.' }); } }, - - load_tutorial: function(data, callback) { - this.make_request('/reader/load_tutorial', data, callback, null, { - request_type: 'GET' - }); + + load_tutorial: function (data, callback) { + this.make_request('/reader/load_tutorial', data, callback, null, { + request_type: 'GET' + }); }, - - fetch_categories: function(callback, error_callback) { - this.make_request('/categories/', null, _.bind(function(data) { + + fetch_categories: function (callback, error_callback) { + this.make_request('/categories/', null, _.bind(function (data) { callback(data); }, this), error_callback, { request_type: 'GET' }); }, - - subscribe_to_categories: function(categories, callback, error_callback) { - this.make_request('/categories/subscribe', {category: categories}, _.bind(function(data) { + + subscribe_to_categories: function (categories, callback, error_callback) { + this.make_request('/categories/subscribe', { category: categories }, _.bind(function (data) { callback(data); }, this), error_callback, { request_type: 'POST' }); }, - - fetch_friends: function(callback, error_callback) { - this.make_request('/social/load_user_friends', null, _.bind(function(data) { + + fetch_friends: function (callback, error_callback) { + this.make_request('/social/load_user_friends', null, _.bind(function (data) { this.user_profile.set(data.user_profile); this.social_services = data.services; this.follower_profiles.reset(data.follower_profiles); @@ -1772,29 +1776,29 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ request_type: 'GET' }); }, - - fetch_follow_requests: function(callback) { - this.make_request('/social/load_follow_requests', null, _.bind(function(data) { + + fetch_follow_requests: function (callback) { + this.make_request('/social/load_follow_requests', null, _.bind(function (data) { this.user_profile.set(data.user_profile); callback(data); }, this), null, { request_type: 'GET' }); }, - - fetch_user_profile: function(user_id, callback) { + + fetch_user_profile: function (user_id, callback) { this.make_request('/social/profile', { 'user_id': user_id, 'include_activities_html': true - }, _.bind(function(data) { + }, _.bind(function (data) { this.add_user_profiles(data.profiles); callback(data); }, this), callback, { request_type: 'GET' }); }, - - search_for_feeds: function(query, callback) { + + search_for_feeds: function (query, callback) { this.make_request('/rss_feeds/feed_autocomplete', { 'query': query, 'format': 'full', @@ -1804,41 +1808,41 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ request_type: 'GET' }); }, - - search_for_friends: function(query, callback) { - this.make_request('/social/find_friends', {'query': query}, callback, callback, { + + search_for_friends: function (query, callback) { + this.make_request('/social/find_friends', { 'query': query }, callback, callback, { ajax_group: 'feed', request_type: 'GET' }); }, - - disconnect_social_service: function(service, callback) { - this.make_request('/oauth/'+service+'_disconnect/', null, callback); + + disconnect_social_service: function (service, callback) { + this.make_request('/oauth/' + service + '_disconnect/', null, callback); }, - - load_current_user_profile: function(callback) { - this.make_request('/social/load_user_profile', null, _.bind(function(data) { + + load_current_user_profile: function (callback) { + this.make_request('/social/load_user_profile', null, _.bind(function (data) { this.user_profile.set(data.user_profile); callback(data); }, this), null, { request_type: 'GET' }); }, - - save_user_profile: function(data, callback) { - this.make_request('/social/save_user_profile/', data, _.bind(function(response) { + + save_user_profile: function (data, callback) { + this.make_request('/social/save_user_profile/', data, _.bind(function (response) { this.user_profile.set(response.user_profile); callback(response); }, this)); }, - - save_blurblog_settings: function(data, callback) { - this.make_request('/social/save_blurblog_settings/', data, _.bind(function(response) { + + save_blurblog_settings: function (data, callback) { + this.make_request('/social/save_blurblog_settings/', data, _.bind(function (response) { this.user_profile.set(response.user_profile); callback(response); }, this)); }, - + save_dashboard_river: function (river_id, river_side, river_order, callback, error_callback) { this.make_request('/reader/save_dashboard_river', { river_id: river_id, @@ -1861,11 +1865,11 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, this), error_callback); }, - follow_user: function(user_id, callback) { - this.make_request('/social/follow', {'user_id': user_id}, _.bind(function(data) { + follow_user: function (user_id, callback) { + this.make_request('/social/follow', { 'user_id': user_id }, _.bind(function (data) { NEWSBLUR.log(["follow data", data]); this.user_profile.set(data.user_profile); - var following_profile = this.following_profiles.detect(function(profile) { + var following_profile = this.following_profiles.detect(function (profile) { return profile.get('user_id') == data.follow_profile.user_id; }); var follow_user; @@ -1879,11 +1883,11 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ callback(data); }, this)); }, - - unfollow_user: function(user_id, callback) { - this.make_request('/social/unfollow', {'user_id': user_id}, _.bind(function(data) { + + unfollow_user: function (user_id, callback) { + this.make_request('/social/unfollow', { 'user_id': user_id }, _.bind(function (data) { this.user_profile.set(data.user_profile); - this.following_profiles.remove(function(profile) { + this.following_profiles.remove(function (profile) { return profile.get('user_id') == data.unfollow_profile.user_id; }); this.social_feeds.remove(data.unfollow_profile.id); @@ -1892,78 +1896,78 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, mute_user: function (user_id, callback) { - this.make_request('/social/mute_user', {'user_id': user_id}, _.bind(function(data) { + this.make_request('/social/mute_user', { 'user_id': user_id }, _.bind(function (data) { this.user_profile.set(data.user_profile); callback(data); - }, this)); + }, this)); }, unmute_user: function (user_id, callback) { - this.make_request('/social/unmute_user', {'user_id': user_id}, _.bind(function(data) { + this.make_request('/social/unmute_user', { 'user_id': user_id }, _.bind(function (data) { this.user_profile.set(data.user_profile); callback(data); - }, this)); + }, this)); }, - - approve_follower: function(user_id, callback) { - this.make_request('/social/approve_follower', {'user_id': user_id}, _.bind(function(data) { + + approve_follower: function (user_id, callback) { + this.make_request('/social/approve_follower', { 'user_id': user_id }, _.bind(function (data) { callback(data); }, this)); }, - - ignore_follower: function(user_id, callback) { - this.make_request('/social/ignore_follower', {'user_id': user_id}, _.bind(function(data) { + + ignore_follower: function (user_id, callback) { + this.make_request('/social/ignore_follower', { 'user_id': user_id }, _.bind(function (data) { callback(data); }, this)); }, - - load_public_story_comments: function(story_id, feed_id, callback) { + + load_public_story_comments: function (story_id, feed_id, callback) { this.make_request('/social/public_comments', { 'story_id': story_id, 'feed_id': feed_id - }, _.bind(function(data) { + }, _.bind(function (data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); } var comments = new NEWSBLUR.Collections.Comments(data.comments); callback(comments); - }, this), null, {request_type: 'GET'}); + }, this), null, { request_type: 'GET' }); }, - - fetch_payment_history: function(user_id, callback) { + + fetch_payment_history: function (user_id, callback) { this.make_request('/profile/payment_history', { user_id: user_id - }, callback, null, {request_type: 'GET'}); + }, callback, null, { request_type: 'GET' }); }, - - upgrade_premium: function(user_id, callback, error_callback) { + + upgrade_premium: function (user_id, callback, error_callback) { this.make_request('/profile/upgrade_premium', { user_id: user_id }, callback, error_callback); }, - - update_payment_history: function(user_id, callback, error_callback) { + + update_payment_history: function (user_id, callback, error_callback) { this.make_request('/profile/update_payment_history', { user_id: user_id }, callback, error_callback); }, - - refund_premium: function(data, callback, error_callback) { + + refund_premium: function (data, callback, error_callback) { this.make_request('/profile/refund_premium', data, callback, error_callback); }, - - never_expire_premium: function(data, callback, error_callback) { + + never_expire_premium: function (data, callback, error_callback) { this.make_request('/profile/never_expire_premium', data, callback, error_callback); }, - - delete_saved_stories: function(timestamp, callback, error_callback) { + + delete_saved_stories: function (timestamp, callback, error_callback) { var self = this; - var pre_callback = function(data) { + var pre_callback = function (data) { if (data.starred_counts) { - self.starred_feeds.reset(data.starred_counts, {parse: true}); + self.starred_feeds.reset(data.starred_counts, { parse: true }); } self.starred_count = data.starred_count; - + if (callback) callback(data); }; @@ -1971,24 +1975,24 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ timestamp: timestamp }, pre_callback, error_callback); }, - - delete_all_sites: function(callback, error_callback) { + + delete_all_sites: function (callback, error_callback) { this.make_request('/profile/delete_all_sites', {}, callback, error_callback); }, - - follow_twitter_account: function(username, callback) { - this.make_request('/oauth/follow_twitter_account', {'username': username}, callback); + + follow_twitter_account: function (username, callback) { + this.make_request('/oauth/follow_twitter_account', { 'username': username }, callback); }, - - unfollow_twitter_account: function(username, callback) { - this.make_request('/oauth/unfollow_twitter_account', {'username': username}, callback); + + unfollow_twitter_account: function (username, callback) { + this.make_request('/oauth/unfollow_twitter_account', { 'username': username }, callback); }, - - fetch_original_text: function(story_hash, callback, error_callback) { + + fetch_original_text: function (story_hash, callback, error_callback) { var story = this.get_story(story_hash); this.make_request('/rss_feeds/original_text', { story_hash: story_hash - }, function(data) { + }, function (data) { story.set('original_text', data.original_text); story.set('image_urls', data.image_urls); story.set('secure_image_urls', data.secure_image_urls); @@ -1998,10 +2002,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ ajax_group: 'statistics' }); }, - - recalculate_story_scores: function(feed_id, options) { + + recalculate_story_scores: function (feed_id, options) { options = options || {}; - this.stories.each(_.bind(function(story, i) { + this.stories.each(_.bind(function (story, i) { if (story.get('story_feed_id') != feed_id) return; var intelligence = { author: 0, @@ -2009,35 +2013,35 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ tags: 0, title: 0 }; - - _.each(this.classifiers[feed_id].titles, function(classifier_score, classifier_title) { - if (intelligence.title <= 0 && + + _.each(this.classifiers[feed_id].titles, function (classifier_score, classifier_title) { + if (intelligence.title <= 0 && story.get('story_title', '').toLowerCase().indexOf(classifier_title.toLowerCase()) != -1) { intelligence.title = classifier_score; } }); - - _.each(this.classifiers[feed_id].authors, function(classifier_score, classifier_author) { + + _.each(this.classifiers[feed_id].authors, function (classifier_score, classifier_author) { if (intelligence.author <= 0 && story.get('story_authors', '').indexOf(classifier_author) != -1) { intelligence.author = classifier_score; } }); - - _.each(this.classifiers[feed_id].tags, function(classifier_score, classifier_tag) { + + _.each(this.classifiers[feed_id].tags, function (classifier_score, classifier_tag) { if (intelligence.tags <= 0 && story.get('story_tags') && _.contains(story.get('story_tags'), classifier_tag)) { intelligence.tags = classifier_score; } }); - - _.each(this.classifiers[feed_id].feeds, function(classifier_score, classifier_feed_id) { + + _.each(this.classifiers[feed_id].feeds, function (classifier_score, classifier_feed_id) { if (intelligence.feed <= 0 && story.get('story_feed_id') == classifier_feed_id) { intelligence.feed = classifier_score; } }); - + story.set('intelligence', intelligence, options); }, this)); } diff --git a/media/js/newsblur/common/generate_bookmarklet.js b/media/js/newsblur/common/generate_bookmarklet.js index 6cb01a351e..7353af71fb 100644 --- a/media/js/newsblur/common/generate_bookmarklet.js +++ b/media/js/newsblur/common/generate_bookmarklet.js @@ -1,10 +1,10 @@ -NEWSBLUR.generate_bookmarklet = function() { - var href = "javascript:function newsblur_bookmarklet() { var d=document,z=d.createElement('scr'+'ipt'),b=d.body,l=d.location; try{ if(!b) { throw(0); } d.title = '(Sharing...) ' + d.title; z.setAttribute('src','https://"+NEWSBLUR.URLs['domain']+"/api/add_site_load_script/"+NEWSBLUR.Globals['secret_token']+"?url='+encodeURIComponent(l.href)+'&time='+(new Date().getTime())); b.appendChild(z); } catch(e) {alert('Please wait until the page has loaded.');}}newsblur_bookmarklet();void(0)"; - - var $bookmarklet = $.make('a', { +NEWSBLUR.generate_bookmarklet = function () { + var href = "javascript:function newsblur_bookmarklet() { var d=document,z=d.createElement('scr'+'ipt'),b=d.body,l=d.location; try{ if(!b) { throw(0); } d.title = '(Sharing...) ' + d.title; z.setAttribute('src','https://" + NEWSBLUR.URLs['domain'] + "/api/add_site_load_script/" + NEWSBLUR.Globals['secret_token'] + "?url='+encodeURIComponent(l.href)+'&time='+(new Date().getTime())); b.appendChild(z); } catch(e) {alert('Please wait until the page has loaded.');}}newsblur_bookmarklet();void(0)"; + + var $bookmarklet = $.make('a', { className: 'NB-goodies-bookmarklet-button', href: href }, 'Share on NewsBlur'); - + return $bookmarklet; -}; \ No newline at end of file +}; diff --git a/media/js/newsblur/common/modal.js b/media/js/newsblur/common/modal.js index 9d80e44ab4..0febd59c31 100644 --- a/media/js/newsblur/common/modal.js +++ b/media/js/newsblur/common/modal.js @@ -1,9 +1,9 @@ -NEWSBLUR.Modal = function(options) { +NEWSBLUR.Modal = function (options) { var defaults = { width: 600, overlayClose: true }; - + this.options = _.extend({}, defaults, options); this.model = NEWSBLUR.assets; this.runner(); @@ -11,12 +11,12 @@ NEWSBLUR.Modal = function(options) { }; NEWSBLUR.Modal.prototype = { - - runner: function() {}, - - open_modal: function(callback) { + + runner: function () { }, + + open_modal: function (callback) { var self = this; - + this.simplemodal = this.$modal.modal({ 'minWidth': this.options.width || 600, 'maxWidth': this.options.width || 600, @@ -26,7 +26,7 @@ NEWSBLUR.Modal.prototype = { dialog.overlay.fadeIn(200, function () { dialog.container.addClass(self.options.modal_container_class); dialog.container.fadeIn(200); - dialog.data.fadeIn(200, function() { + dialog.data.fadeIn(200, function () { if (self.options.onOpen) { self.options.onOpen(); } @@ -34,24 +34,24 @@ NEWSBLUR.Modal.prototype = { callback(); } }); - setTimeout(function() { + setTimeout(function () { // $(window).resize(); self.resize(); self.flags.modal_loaded = true; }, 0); }); }, - 'onShow': function(dialog) { + 'onShow': function (dialog) { $('#simplemodal-container').corner('6px'); if (self.options.onShow) { self.options.onShow(); } }, - 'onClose': function(dialog, callback) { + 'onClose': function (dialog, callback) { self.flags.open = false; dialog.data.hide().empty().remove(); dialog.container.hide().empty().remove(); - dialog.overlay.fadeOut(200, function() { + dialog.overlay.fadeOut(200, function () { if (self.options.onOpen) { self.options.onOpen(); } @@ -62,57 +62,57 @@ NEWSBLUR.Modal.prototype = { } }); }, - - resize: function() { - // $(window).trigger('resize.simplemodal'); - $.modal.resize(); + + resize: function () { + // $(window).trigger('resize.simplemodal'); + $.modal.resize(); }, - - close: function(callback) { + + close: function (callback) { $('.NB-modal-loading', this.$modal).removeClass('NB-active'); $.modal.close(callback); }, - + make_feed_chooser: function (options) { options = options || {}; options.selected_folder_title = this.model.folder_title; options.feed_id = this.feed_id; - + return NEWSBLUR.utils.make_feed_chooser(options); }, - - initialize_feed: function(feed_id) { + + initialize_feed: function (feed_id) { this.feed_id = feed_id; if (this.options.embedded) { - this.feed = NEWSBLUR.stats_feed; + this.feed = NEWSBLUR.stats_feed; } else { - this.feed = this.model.get_feed(feed_id); + this.feed = this.model.get_feed(feed_id); } this.options.social_feed = this.feed && this.feed.is_social(); - + $('.NB-modal-subtitle .NB-modal-feed-image', this.$modal).attr('src', $.favicon(this.feed)); $('.NB-modal-subtitle .NB-modal-feed-title', this.$modal).html(this.feed.get('feed_title')); $('.NB-modal-subtitle .NB-modal-feed-subscribers', this.$modal).html(Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true)).show(); }, - - initialize_folder: function(folder_title) { + + initialize_folder: function (folder_title) { this.folder_title = folder_title; this.folder = this.model.get_folder(folder_title); - + $('.NB-modal-subtitle .NB-modal-feed-image', this.$modal).attr('src', NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/folder-open.svg'); $('.NB-modal-subtitle .NB-modal-feed-title', this.$modal).html(this.folder_title); $('.NB-modal-subtitle .NB-modal-feed-subscribers', this.$modal).hide(); }, - - switch_tab: function(newtab) { + + switch_tab: function (newtab) { var $modal_tabs = $('.NB-modal-tab', this.$modal); var $tabs = $('.NB-tab', this.$modal); - + $modal_tabs.removeClass('NB-active'); $tabs.removeClass('NB-active'); - - $modal_tabs.filter('.NB-modal-tab-'+newtab).addClass('NB-active'); - $tabs.filter('.NB-tab-'+newtab).addClass('NB-active'); + + $modal_tabs.filter('.NB-modal-tab-' + newtab).addClass('NB-active'); + $tabs.filter('.NB-tab-' + newtab).addClass('NB-active'); } - + }; diff --git a/media/js/newsblur/common/router.js b/media/js/newsblur/common/router.js index eb32968a53..c9beaea042 100644 --- a/media/js/newsblur/common/router.js +++ b/media/js/newsblur/common/router.js @@ -1,5 +1,5 @@ NEWSBLUR.Router = Backbone.Router.extend({ - + routes: { "add/?": "add_site", "try/?": "try_site", @@ -18,17 +18,17 @@ NEWSBLUR.Router = Backbone.Router.extend({ "social/:user_id": "social", "user/*user": "user" }, - - add_site: function() { + + add_site: function () { NEWSBLUR.log(["add", window.location, $.getQueryString('url')]); - NEWSBLUR.reader.open_add_feed_modal({url: $.getQueryString('url')}); + NEWSBLUR.reader.open_add_feed_modal({ url: $.getQueryString('url') }); }, - - try_site: function() { + + try_site: function () { NEWSBLUR.log(["try", window.location]); }, - - site: function(site_id, slug) { + + site: function (site_id, slug) { // NEWSBLUR.log(["site", site_id, slug]); site_id = parseInt(site_id, 10); var feed = NEWSBLUR.assets.get_feed(site_id); @@ -38,11 +38,11 @@ NEWSBLUR.Router = Backbone.Router.extend({ NEWSBLUR.reader.flags.search = query; } if (feed) { - NEWSBLUR.reader.open_feed(site_id, {router: true, force: true, search: query}); + NEWSBLUR.reader.open_feed(site_id, { router: true, force: true, search: query }); } else { NEWSBLUR.reader.load_feed_in_tryfeed_view(site_id, { router: true, - force: true, + force: true, search: query, feed: { feed_title: _.string.humanize(slug || "") @@ -50,8 +50,8 @@ NEWSBLUR.Router = Backbone.Router.extend({ }); } }, - - read: function() { + + read: function () { var options = { router: true }; @@ -64,8 +64,8 @@ NEWSBLUR.Router = Backbone.Router.extend({ console.log(["read stories", options]); NEWSBLUR.reader.open_read_stories(options); }, - - starred: function(tag) { + + starred: function (tag) { var options = { router: true, tag: tag @@ -79,11 +79,11 @@ NEWSBLUR.Router = Backbone.Router.extend({ console.log(["starred", options, tag]); NEWSBLUR.reader.open_starred_stories(options); }, - - folder: function(folder_name) { + + folder: function (folder_name) { folder_name = folder_name.replace(/-/g, ' '); // NEWSBLUR.log(["folder", folder_name]); - var options = {router: true}; + var options = { router: true }; var query = this.extract_query(); if (query) { NEWSBLUR.reader.flags.searching = true; @@ -108,8 +108,8 @@ NEWSBLUR.Router = Backbone.Router.extend({ } } }, - - social: function(user_id, slug) { + + social: function (user_id, slug) { NEWSBLUR.log(["router:social", user_id, slug]); var query = this.extract_query(); if (query) { @@ -118,11 +118,11 @@ NEWSBLUR.Router = Backbone.Router.extend({ } var feed_id = "social:" + user_id; if (NEWSBLUR.assets.get_feed(feed_id)) { - NEWSBLUR.reader.open_social_stories(feed_id, {router: true, force: true, search: query}); + NEWSBLUR.reader.open_social_stories(feed_id, { router: true, force: true, search: query }); } else { NEWSBLUR.reader.load_social_feed_in_tryfeed_view(feed_id, { - router: true, - force: true, + router: true, + force: true, search: query, feed: { username: _.string.humanize(slug), @@ -133,18 +133,18 @@ NEWSBLUR.Router = Backbone.Router.extend({ }); } }, - - extract_query: function() { + + extract_query: function () { var search = $.getQueryString('search'); var sanitized = search && search.replace(/[^\w\s]+/g, " "); - + // console.log('extract_query', search, sanitized); - + return sanitized; }, - - user: function(user) { + + user: function (user) { NEWSBLUR.log(["user", user]); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/models/comments.js b/media/js/newsblur/models/comments.js index 83e1b84170..c565b64f60 100644 --- a/media/js/newsblur/models/comments.js +++ b/media/js/newsblur/models/comments.js @@ -1,48 +1,48 @@ NEWSBLUR.Models.Comment = Backbone.Model.extend({ - + urlRoot: '/social/comment', - - initialize: function() { + + initialize: function () { this.bind('change:replies', this.changes_replies); this.bind('change:comments', this.strip_html_in_comments); this.changes_replies(); }, - - changes_replies: function() { + + changes_replies: function () { if (this.get('replies')) { this.replies = new NEWSBLUR.Collections.CommentReplies(this.get('replies')); } }, - - strip_html_in_comments: function() { + + strip_html_in_comments: function () { this.attributes['comments'] = this.strip_html(this.get('comments')); }, - - strip_html: function(html) { + + strip_html: function (html) { return html.replace(/<\/?[^>]+(>|$)/g, ""); } - + }); NEWSBLUR.Collections.Comments = Backbone.Collection.extend({ - + url: '/social/comments', - + model: NEWSBLUR.Models.Comment - + }); NEWSBLUR.Models.CommentReply = Backbone.Model.extend({ - - stripped_comments: function() { + + stripped_comments: function () { return NEWSBLUR.Models.Comment.prototype.strip_html(this.get('comments')); } - + }); NEWSBLUR.Collections.CommentReplies = Backbone.Collection.extend({ - + model: NEWSBLUR.Models.CommentReply - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/models/dashboard_rivers.js b/media/js/newsblur/models/dashboard_rivers.js index edf3004b35..e6cba9861c 100644 --- a/media/js/newsblur/models/dashboard_rivers.js +++ b/media/js/newsblur/models/dashboard_rivers.js @@ -1,14 +1,14 @@ NEWSBLUR.Models.DashboardRiver = Backbone.Model.extend({ - - initialize: function() { + + initialize: function () { var feed_title = NEWSBLUR.reader.feed_title(this.get('river_id')); this.set('feed_title', "\"" + this.get('query') + "\" in " + feed_title + ""); }, - - favicon_url: function() { + + favicon_url: function () { var url; var river_id = this.get('river_id'); - + return $.favicon(river_id); }, @@ -21,10 +21,10 @@ NEWSBLUR.Models.DashboardRiver = Backbone.Model.extend({ }); NEWSBLUR.Collections.DashboardRivers = Backbone.Collection.extend({ - + model: NEWSBLUR.Models.DashboardRiver, - - comparator: function(a, b) { + + comparator: function (a, b) { if (a.get('sort_order') > b.get('sort_order')) return 1; else if (a.get('sort_order') < b.get('sort_order')) return -1; @@ -43,10 +43,10 @@ NEWSBLUR.Collections.DashboardRivers = Backbone.Collection.extend({ return this.side('right'); }, - side: function(side) { + side: function (side) { return this.select(function (river) { return river.get('river_side') == side; }); } - + }); diff --git a/media/js/newsblur/models/feeds.js b/media/js/newsblur/models/feeds.js index 7e3f704bab..ae8a90a802 100644 --- a/media/js/newsblur/models/feeds.js +++ b/media/js/newsblur/models/feeds.js @@ -1,6 +1,6 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({ - - initialize: function() { + + initialize: function () { _.bindAll(this, 'on_change', 'delete_feed', 'update_folder_counts'); // this.bind('change', this.on_change); this.bind('change:ps', this.change_counts); @@ -10,166 +10,166 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({ this.views = []; this.folders = []; }, - - on_change: function() { + + on_change: function () { if (!('selected' in this.changedAttributes())) { NEWSBLUR.log(['Feed Change', this.changedAttributes(), this.previousAttributes()]); } }, - - change_counts: function(data, count, options) { + + change_counts: function (data, count, options) { options = options || {}; // console.log(["change_counts", data, count, options]); this.update_folder_counts(); - + if (this.get('selected') && options.refresh_feeds) { console.log(["Selected feed count change", this]); NEWSBLUR.reader.feed_unread_count(this.id); } }, - - force_update_counts: function() { + + force_update_counts: function () { NEWSBLUR.reader.feed_unread_count(this.id); }, - - update_folder_counts: function() { - _.each(this.folders, function(folder) { + + update_folder_counts: function () { + _.each(this.folders, function (folder) { folder.trigger('change:counts'); }); }, - - update_folder_visibility: function() { - _.each(this.folders, function(folder) { + + update_folder_visibility: function () { + _.each(this.folders, function (folder) { folder.trigger('change:feed_selected'); }); }, - - delete_feed: function(options) { + + delete_feed: function (options) { options = options || {}; var view = options.view || this.get_view(); NEWSBLUR.reader.flags['reloading_feeds'] = true; - NEWSBLUR.assets.delete_feed(this.id, view.options.folder_title, function() { + NEWSBLUR.assets.delete_feed(this.id, view.options.folder_title, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; }); view.delete_feed(); }, - - move_to_folder: function(to_folder, options) { + + move_to_folder: function (to_folder, options) { options = options || {}; var view = options.view || this.get_view(); var in_folder = view.options.folder_title; - + if (in_folder == to_folder) return false; NEWSBLUR.reader.flags['reloading_feeds'] = true; - NEWSBLUR.assets.move_feed_to_folder(this.id, in_folder, to_folder, function() { + NEWSBLUR.assets.move_feed_to_folder(this.id, in_folder, to_folder, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; - _.delay(function() { - NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({'opacity': 0}, { - 'duration': 100, - 'complete': function() { + _.delay(function () { + NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({ 'opacity': 0 }, { + 'duration': 100, + 'complete': function () { NEWSBLUR.app.feed_list.make_feeds(); } }); }, 250); }); - + return true; }, - - move_to_folders: function(to_folders, options) { + + move_to_folders: function (to_folders, options) { options = options || {}; var view = options.view || this.get_view(); var in_folders = this.in_folders(); - + if (_.isEqual(in_folders, to_folders)) return false; NEWSBLUR.reader.flags['reloading_feeds'] = true; - NEWSBLUR.assets.move_feed_to_folders(this.id, in_folders, to_folders, function() { + NEWSBLUR.assets.move_feed_to_folders(this.id, in_folders, to_folders, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; - _.delay(function() { - NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({'opacity': 0}, { - 'duration': 100, - 'complete': function() { + _.delay(function () { + NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({ 'opacity': 0 }, { + 'duration': 100, + 'complete': function () { NEWSBLUR.app.feed_list.make_feeds(); } }); }, 250); }); - + return true; }, - - parent_folder_names: function() { - var names = _.compact(_.flatten(_.map(this.folders, function(folder) { + + parent_folder_names: function () { + var names = _.compact(_.flatten(_.map(this.folders, function (folder) { return folder.parent_folder_names(); }))); - + return names; }, - - in_folders: function() { + + in_folders: function () { var in_folders = _.pluck(_.pluck(this.folders, 'options'), 'title'); return in_folders; }, - - rename: function(new_title) { + + rename: function (new_title) { this.set('feed_title', new_title); NEWSBLUR.assets.rename_feed(this.id, new_title); }, - - get_view: function($feed, fallback) { - var found_view = _.detect(this.views, function(view) { + + get_view: function ($feed, fallback) { + var found_view = _.detect(this.views, function (view) { if ($feed) { return view.el == $feed.get(0); } else { return true; } }); - + if (!found_view && fallback && this.views.length) { found_view = this.views[0]; } - + return found_view; }, - - is_social: function() { + + is_social: function () { return false; }, - - is_feed: function() { + + is_feed: function () { return true; }, - - is_starred: function() { + + is_starred: function () { return false; }, - - is_search: function() { + + is_search: function () { return false; }, - - is_light: function() { + + is_light: function () { var is_light = this._is_light; if (!_.isUndefined(is_light)) { return is_light; } var color = this.get('favicon_color'); if (!color) return false; - + var r = parseInt(color.substr(0, 2), 16) / 255.0; var g = parseInt(color.substr(2, 2), 16) / 255.0; var b = parseInt(color.substr(4, 2), 16) / 255.0; - is_light = $.textColor({r: r, g: g, b: b}) != 'white'; + is_light = $.textColor({ r: r, g: g, b: b }) != 'white'; this._is_light = is_light; return is_light; }, - - unread_counts: function() { + + unread_counts: function () { var starred_feed = NEWSBLUR.assets.starred_feeds.get_feed(this.id); return { ps: this.get('ps') || 0, @@ -178,17 +178,17 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({ st: starred_feed && starred_feed.get('count') || 0 }; }, - - has_unreads: function(options) { + + has_unreads: function (options) { options = options || {}; var unread_view = NEWSBLUR.assets.preference('unread_view'); - + if (options.include_selected && this.get('selected')) { return true; } - + if (!this.get('active')) return false; - + if (unread_view <= -1) { return !!(this.get('ng') || this.get('nt') || this.get('ps')); } else if (unread_view == 0) { @@ -200,87 +200,87 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({ return !!(this.get('ps')); } }, - - relative_last_story_date: function() { + + relative_last_story_date: function () { var diff = this.get('last_story_seconds_ago'); - var lasthour = 60*60; - var lastday = 24*60*60; + var lasthour = 60 * 60; + var lastday = 24 * 60 * 60; - if (diff > 1000*60*60*24*365*20 || diff <= 0) { + if (diff > 1000 * 60 * 60 * 24 * 365 * 20 || diff <= 0) { return "Never"; } else if (diff < lasthour) { - return Inflector.pluralize("minute", Math.floor(diff/60), true) + " ago"; + return Inflector.pluralize("minute", Math.floor(diff / 60), true) + " ago"; } else if (diff < lastday) { - return Inflector.pluralize("hour", Math.floor(diff/60/60), true) + " ago"; + return Inflector.pluralize("hour", Math.floor(diff / 60 / 60), true) + " ago"; } else { - return Inflector.pluralize("day", Math.floor(diff/60/60/24), true) + " ago"; + return Inflector.pluralize("day", Math.floor(diff / 60 / 60 / 24), true) + " ago"; } }, - - highlighted_in_folder: function(folder_title) { - return !!(this.get('highlighted') && - this.get('highlighted_in_folders') && - _.contains(this.get('highlighted_in_folders'), folder_title)); + + highlighted_in_folder: function (folder_title) { + return !!(this.get('highlighted') && + this.get('highlighted_in_folders') && + _.contains(this.get('highlighted_in_folders'), folder_title)); }, - - highlight_in_folder: function(folder_title, on, off, options) { + + highlight_in_folder: function (folder_title, on, off, options) { options = options || {}; - + if (!this.get('highlighted_in_folders')) { - this.set('highlighted_in_folders', [], {silent: true}); + this.set('highlighted_in_folders', [], { silent: true }); } - + if (!off && (on || !_.contains(this.get('highlighted_in_folders'), folder_title))) { - this.set('highlighted_in_folders', - this.get('highlighted_in_folders').concat(folder_title), {silent: true}); + this.set('highlighted_in_folders', + this.get('highlighted_in_folders').concat(folder_title), { silent: true }); } else { - this.set('highlighted_in_folders', - _.without(this.get('highlighted_in_folders'), folder_title), {silent: true}); + this.set('highlighted_in_folders', + _.without(this.get('highlighted_in_folders'), folder_title), { silent: true }); } - this.set('highlighted', !!this.get('highlighted_in_folders').length, {silent: true}); + this.set('highlighted', !!this.get('highlighted_in_folders').length, { silent: true }); if (!options.silent) this.trigger('change:highlighted'); }, - - highlight_in_all_folders: function(on, off, options) { + + highlight_in_all_folders: function (on, off, options) { options = options || {}; - + if (!this.get('highlighted_in_folders')) { - this.set('highlighted_in_folders', [], {silent: true}); + this.set('highlighted_in_folders', [], { silent: true }); } var folders = _.unique(this.in_folders()) || []; - + if (!off && (on || !this.get('highlighted_in_folders').length)) { - this.set('highlighted_in_folders', folders, {silent: true}); + this.set('highlighted_in_folders', folders, { silent: true }); } else { - this.set('highlighted_in_folders', [], {silent: true}); + this.set('highlighted_in_folders', [], { silent: true }); } - this.set('highlighted', !!this.get('highlighted_in_folders').length, {silent: true}); + this.set('highlighted', !!this.get('highlighted_in_folders').length, { silent: true }); if (!options.silent) this.trigger('change:highlighted'); } - + }); NEWSBLUR.Collections.Feeds = Backbone.Collection.extend({ - + model: NEWSBLUR.Models.Feed, - + url: '/reader/feeds', - + active_feed: null, - - initialize: function() { + + initialize: function () { this.bind('change', this.detect_active_feed); }, - + comparator: 'feed_title', - + // =========== // = Actions = // =========== - - fetch: function(options) { + + fetch: function (options) { var data = { 'v': 2, 'update_counts': false @@ -292,24 +292,24 @@ NEWSBLUR.Collections.Feeds = Backbone.Collection.extend({ }, options); return Backbone.Collection.prototype.fetch.call(this, options); }, - - parse: function(data) { - _.each(data.feeds, function(feed) { + + parse: function (data) { + _.each(data.feeds, function (feed) { feed.selected = false; }); this.ensure_authenticated(data); - + return data.feeds; }, - - deselect: function() { - this.each(function(feed){ - feed.set('selected', false); + + deselect: function () { + this.each(function (feed) { + feed.set('selected', false); }); }, - - ensure_authenticated: function(data) { + + ensure_authenticated: function (data) { if (!NEWSBLUR.Globals.is_authenticated) return; if (_.isUndefined(data.authenticated)) return; if (NEWSBLUR.Globals.is_authenticated != data.authenticated) { @@ -317,51 +317,51 @@ NEWSBLUR.Collections.Feeds = Backbone.Collection.extend({ // NEWSBLUR.reader.show_authentication_lost(); } }, - + // ================== // = Model Managers = // ================== - - selected: function() { - return this.detect(function(feed) { return feed.get('selected'); }); + + selected: function () { + return this.detect(function (feed) { return feed.get('selected'); }); }, - - active: function() { - return this.select(function(feed) { return feed.get('active'); }); + + active: function () { + return this.select(function (feed) { return feed.get('active'); }); }, - - has_chosen_feeds: function() { - return this.any(function(feed) { + + has_chosen_feeds: function () { + return this.any(function (feed) { return feed.get('active'); }); }, - - has_unfetched_feeds: function() { - return this.any(function(feed) { + + has_unfetched_feeds: function () { + return this.any(function (feed) { return feed.get('not_yet_fetched'); }); }, - + // ============ // = Counters = // ============ - - search_indexed: function() { - var indexed = this.select(function(feed) { + + search_indexed: function () { + var indexed = this.select(function (feed) { return feed.get('search_indexed'); }).length; return indexed; }, - + // ========== // = Events = // ========== - - detect_active_feed: function() { - this.active_feed = this.detect(function(feed) { + + detect_active_feed: function () { + this.active_feed = this.detect(function (feed) { return feed.get('selected'); }); } - + }); diff --git a/media/js/newsblur/models/folders.js b/media/js/newsblur/models/folders.js index 4d52f4c503..71c24d8ad2 100644 --- a/media/js/newsblur/models/folders.js +++ b/media/js/newsblur/models/folders.js @@ -1,6 +1,6 @@ NEWSBLUR.Models.FeedOrFolder = Backbone.Model.extend({ - - initialize: function(model) { + + initialize: function (model) { if (_.isNumber(model) || model['feed_id']) { this.feed = NEWSBLUR.assets.feeds.get(model['feed_id'] || model); @@ -22,27 +22,27 @@ NEWSBLUR.Models.FeedOrFolder = Backbone.Model.extend({ parent_folder: this.collection, parse: true }); - this.folders.reset(_.compact(children), {parse: true}); + this.folders.reset(_.compact(children), { parse: true }); } }, - - parse: function(attrs) { + + parse: function (attrs) { if (_.isNumber(attrs)) { - attrs = {'feed_id': attrs}; + attrs = { 'feed_id': attrs }; } return attrs; }, - - is_feed: function() { + + is_feed: function () { return !!this.get('is_feed'); }, - - is_folder: function() { + + is_folder: function () { return !!this.get('is_folder'); }, - - get_view: function($folder) { - var view = _.detect(this.folder_views, function(view) { + + get_view: function ($folder) { + var view = _.detect(this.folder_views, function (view) { if ($folder) { return view.el == $folder.get(0); } @@ -52,8 +52,8 @@ NEWSBLUR.Models.FeedOrFolder = Backbone.Model.extend({ } return view; }, - - feed_ids_in_folder: function(options) { + + feed_ids_in_folder: function (options) { options = options || {}; if (this.is_feed()) { if (options.include_inactive) { @@ -75,88 +75,88 @@ NEWSBLUR.Models.FeedOrFolder = Backbone.Model.extend({ return this.folders.feed_ids_in_folder(options); } }, - - feeds_with_unreads: function(options) { + + feeds_with_unreads: function (options) { if (this.is_feed()) { return this.feed.has_unreads(options) && this.feed; } else if (this.is_folder()) { return this.folders.feeds_with_unreads(options); } }, - - move_to_folder: function(to_folder, options) { + + move_to_folder: function (to_folder, options) { options = options || {}; var view = options.view || this.get_view(); var in_folder = this.collection.options.title; var folder_title = this.get('folder_title'); if (in_folder == to_folder) return false; - + NEWSBLUR.reader.flags['reloading_feeds'] = true; - NEWSBLUR.assets.move_folder_to_folder(folder_title, in_folder, to_folder, function() { + NEWSBLUR.assets.move_folder_to_folder(folder_title, in_folder, to_folder, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; - _.delay(function() { - NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({'opacity': 0}, { - 'duration': 100, - 'complete': function() { + _.delay(function () { + NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({ 'opacity': 0 }, { + 'duration': 100, + 'complete': function () { NEWSBLUR.app.feed_list.make_feeds(); } }); }, 250); }); - + return true; }, - - rename: function(new_folder_name) { + + rename: function (new_folder_name) { var folder_title = this.get('folder_title'); var in_folder = this.collection.options.title; NEWSBLUR.assets.rename_folder(folder_title, new_folder_name, in_folder); this.set('folder_title', new_folder_name); }, - - delete_folder: function() { + + delete_folder: function () { var folder_title = this.get('folder_title'); var in_folder = this.collection.options.title; var feed_ids_in_folder = this.feed_ids_in_folder(); - + NEWSBLUR.reader.flags['reloading_feeds'] = true; - NEWSBLUR.assets.delete_folder(folder_title, in_folder, feed_ids_in_folder, function() { + NEWSBLUR.assets.delete_folder(folder_title, in_folder, feed_ids_in_folder, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; }); this.trigger('delete'); }, - - has_unreads: function(options) { + + has_unreads: function (options) { options = options || {}; if (options.include_selected && this.get('selected')) { return true; } - + return this.folders.has_unreads(options); }, - rss_url: function(filter) { + rss_url: function (filter) { return this.folders.rss_url(filter); }, - - view_setting: function(setting) { + + view_setting: function (setting) { if (this.is_folder()) { return NEWSBLUR.assets.view_setting('river:' + this.get('folder_title'), setting); } else { return NEWSBLUR.assets.view_setting(this.id, setting); } } - + }); NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ - + options: { title: '' }, - - initialize: function(models, options) { + + initialize: function (models, options) { _.bindAll(this, 'propagate_feed_selected'); this.options = _.extend({}, this.options, options); this.parent_folder = options && options.parent_folder; @@ -165,27 +165,27 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ this.bind('change:counts', this.propagate_change_counts); this.bind('reset', this.reset_folder_views); }, - + model: NEWSBLUR.Models.FeedOrFolder, - - reset_folder_views: function() { - this.each(function(item) { + + reset_folder_views: function () { + this.each(function (item) { if (item.is_feed()) { item.feed.views = []; item.feed.folders = []; } - }); + }); }, - - folders: function() { - return this.select(function(item) { + + folders: function () { + return this.select(function (item) { return item.is_folder(); }); }, - - find_folder: function(folder_name) { + + find_folder: function (folder_name) { var found_folder; - this.any(function(folder) { + this.any(function (folder) { if (folder.is_folder()) { if (folder.get('folder_title').toLowerCase() == folder_name || folder.get('folder_title').toLowerCase().replace(/-/g, ' ') == folder_name) { @@ -198,10 +198,10 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ }); return found_folder; }, - - get_view: function($folder) { + + get_view: function ($folder) { var view; - this.any(function(item) { + this.any(function (item) { if (item.is_folder()) { view = item.get_view($folder); return view; @@ -211,48 +211,48 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ return view; } }, - - child_folder_names: function() { + + child_folder_names: function () { var names = []; - this.each(function(item) { + this.each(function (item) { if (item.is_folder()) { names.push(item.get('folder_title')); - _.each(item.folders.child_folder_names(), function(name) { + _.each(item.folders.child_folder_names(), function (name) { names.push(name); }); } }); return names; }, - - parent_folder_names: function() { + + parent_folder_names: function () { var names = [this.options.title]; if (this.parent_folder) { var parents = _.compact(_.flatten(this.parent_folder.parent_folder_names())); names = names.concat(parents); } - + return names; }, - - feed_ids_in_folder: function(options) { + + feed_ids_in_folder: function (options) { options = options || {}; - return _.compact(_.flatten(this.map(function(item) { + return _.compact(_.flatten(this.map(function (item) { return item.feed_ids_in_folder(options); }))); }, - - feeds_with_unreads: function(options) { + + feeds_with_unreads: function (options) { options = options || {}; - - return _.compact(_.flatten(this.map(function(item) { + + return _.compact(_.flatten(this.map(function (item) { return item.feeds_with_unreads(options); }))); }, - - selected: function() { + + selected: function () { var selected_folder; - this.any(function(folder) { + this.any(function (folder) { if (folder.is_folder()) { if (folder.get('selected')) { selected_folder = folder; @@ -264,19 +264,19 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ }); return selected_folder; }, - - deselect: function() { - this.each(function(item) { + + deselect: function () { + this.each(function (item) { if (item.is_folder()) { item.set('selected', false); item.folders.deselect(); } }); }, - - unread_counts: function(sum_total, seen_feeds) { + + unread_counts: function (sum_total, seen_feeds) { if (!seen_feeds) seen_feeds = []; - var counts = this.reduce(function(counts, item) { + var counts = this.reduce(function (counts, item) { if (item.is_feed() && !_.contains(seen_feeds, item.feed.id) && item.feed.get('active')) { var feed_counts = item.feed.unread_counts(); counts['ps'] += feed_counts['ps']; @@ -295,22 +295,22 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ nt: 0, ng: 0 }); - + this.counts = counts; - + if (sum_total) { var unread_view = NEWSBLUR.reader.get_unread_view_name(); if (unread_view == 'positive') return counts['ps']; - if (unread_view == 'neutral') return counts['ps'] + counts['nt']; + if (unread_view == 'neutral') return counts['ps'] + counts['nt']; if (unread_view == 'negative') return counts['ps'] + counts['nt'] + counts['ng']; } return counts; }, - - has_unreads: function(options) { + + has_unreads: function (options) { options = options || {}; - - return this.any(function(item) { + + return this.any(function (item) { if (item.is_feed()) { return item.feed.has_unreads(options); } else if (item.is_folder()) { @@ -318,49 +318,49 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ } }); }, - - propagate_feed_selected: function() { + + propagate_feed_selected: function () { if (this.parent_folder) { this.parent_folder.trigger('change:feed_selected'); } }, - propagate_change_counts: function() { + propagate_change_counts: function () { if (this.parent_folder) { this.parent_folder.trigger('change:counts'); } }, - - update_all_folder_visibility: function() { - this.each(function(item) { + + update_all_folder_visibility: function () { + this.each(function (item) { if (item.is_folder()) { item.folders.trigger('change:counts'); item.folders.update_all_folder_visibility(); } }); }, - - rss_url: function(filter) { + + rss_url: function (filter) { var url = NEWSBLUR.URLs['folder_rss']; url = url.replace('{user_id}', NEWSBLUR.Globals.user_id); url = url.replace('{secret_token}', NEWSBLUR.Globals.secret_token); url = url.replace('{unread_filter}', filter); url = url.replace('{folder_title}', Inflector.sluggify(this.options.title)); console.log(['rss_url', this]); - + return "https://" + NEWSBLUR.URLs.domain + url; }, - - view_setting: function(setting) { + + view_setting: function (setting) { return NEWSBLUR.assets.view_setting('river:' + (this.get('folder_title') || ''), setting); } }, { - comparator: function(modelA, modelB) { + comparator: function (modelA, modelB) { // toUpperCase for historical reasons var sort_order = NEWSBLUR.assets.preference('feed_order').toUpperCase(); - + if (NEWSBLUR.Collections.Folders.organizer_sortorder) { sort_order = NEWSBLUR.Collections.Folders.organizer_sortorder.toUpperCase(); } @@ -370,7 +370,7 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ high = -1; low = 1; } - + if (modelA.is_feed() != modelB.is_feed()) { // Feeds above folders return modelA.is_feed() ? -1 : 1; @@ -379,42 +379,42 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({ // Folders are alphabetical return modelA.get('folder_title').toLowerCase() > modelB.get('folder_title').toLowerCase() ? 1 : -1; } - + var feedA = modelA.feed; var feedB = modelB.feed; - + if (!feedA || !feedB) { return !feedA ? 1 : -1; } - - var remove_articles = function(str) { + + var remove_articles = function (str) { var words = str.split(" "); if (words.length <= 1) return str; if (words[0] == 'the') return words.splice(1).join(" "); return str; }; - + var feed_a_title = remove_articles(feedA.get('feed_title').toLowerCase()); var feed_b_title = remove_articles(feedB.get('feed_title').toLowerCase()); - + if (sort_order == 'ALPHABETICAL' || !sort_order) { return feed_a_title > feed_b_title ? high : low; } else if (sort_order == 'MOSTUSED') { - return feedA.get('feed_opens') < feedB.get('feed_opens') ? high : - (feedA.get('feed_opens') > feedB.get('feed_opens') ? low : - (feed_a_title > feed_b_title ? high : low)); + return feedA.get('feed_opens') < feedB.get('feed_opens') ? high : + (feedA.get('feed_opens') > feedB.get('feed_opens') ? low : + (feed_a_title > feed_b_title ? high : low)); } else if (sort_order == 'RECENCY') { - return feedA.get('last_story_seconds_ago') < feedB.get('last_story_seconds_ago') ? high : - (feedA.get('last_story_seconds_ago') > feedB.get('last_story_seconds_ago') ? low : - (feed_a_title > feed_b_title ? high : low)); + return feedA.get('last_story_seconds_ago') < feedB.get('last_story_seconds_ago') ? high : + (feedA.get('last_story_seconds_ago') > feedB.get('last_story_seconds_ago') ? low : + (feed_a_title > feed_b_title ? high : low)); } else if (sort_order == 'FREQUENCY') { - return feedA.get('average_stories_per_month') < feedB.get('average_stories_per_month') ? high : - (feedA.get('average_stories_per_month') > feedB.get('average_stories_per_month') ? low : - (feed_a_title > feed_b_title ? high : low)); + return feedA.get('average_stories_per_month') < feedB.get('average_stories_per_month') ? high : + (feedA.get('average_stories_per_month') > feedB.get('average_stories_per_month') ? low : + (feed_a_title > feed_b_title ? high : low)); } else if (sort_order == 'SUBSCRIBERS') { - return feedA.get('num_subscribers') < feedB.get('num_subscribers') ? high : - (feedA.get('num_subscribers') > feedB.get('num_subscribers') ? low : - (feed_a_title > feed_b_title ? high : low)); + return feedA.get('num_subscribers') < feedB.get('num_subscribers') ? high : + (feedA.get('num_subscribers') > feedB.get('num_subscribers') ? low : + (feed_a_title > feed_b_title ? high : low)); } } diff --git a/media/js/newsblur/models/saved_searches.js b/media/js/newsblur/models/saved_searches.js index 33f58cf8c7..b55e4d6260 100644 --- a/media/js/newsblur/models/saved_searches.js +++ b/media/js/newsblur/models/saved_searches.js @@ -1,17 +1,17 @@ NEWSBLUR.Models.SavedSearchFeed = Backbone.Model.extend({ - - initialize: function() { + + initialize: function () { var feed_title = NEWSBLUR.reader.feed_title(this.get('feed_id')); var favicon_url = this.favicon_url(); this.set('feed_title', "\"" + this.get('query') + "\" in " + feed_title + ""); this.set('favicon_url', favicon_url); this.list_view; }, - - favicon_url: function() { + + favicon_url: function () { var url; var feed_id = this.get('feed_id'); - + if (feed_id == 'river:' || feed_id == 'river:infrequent') { url = NEWSBLUR.Globals.MEDIA_URL + 'img/icons/nouns/all-stories.svg'; } else if (_.string.startsWith(feed_id, 'river:')) { @@ -27,56 +27,56 @@ NEWSBLUR.Models.SavedSearchFeed = Backbone.Model.extend({ } else if (_.string.startsWith(feed_id, 'social:')) { url = $.favicon(NEWSBLUR.assets.get_feed(feed_id)); } - + if (!url) { url = NEWSBLUR.Globals.MEDIA_URL + 'img/icons/nouns/search.svg'; } - + return url; }, - - is_social: function() { + + is_social: function () { return false; }, - - is_feed: function() { + + is_feed: function () { return false; }, - - is_starred: function() { + + is_starred: function () { return false; }, - - is_search: function() { + + is_search: function () { return true; }, - - unread_counts: function() { + + unread_counts: function () { return { ps: this.get('count') || 0, nt: 0, ng: 0 }; }, - - tag_slug: function() { + + tag_slug: function () { return Inflector.sluggify(this.get('tag') || ''); } - + }); NEWSBLUR.Collections.SearchesFeeds = Backbone.Collection.extend({ - + model: NEWSBLUR.Models.SavedSearchFeed, - - parse: function(models) { - _.each(models, function(feed) { + + parse: function (models) { + _.each(models, function (feed) { feed.id = 'search:' + feed.feed_id + ":" + feed.query; }); return models; }, - - comparator: function(a, b) { + + comparator: function (a, b) { var sort_order = NEWSBLUR.reader.model.preference('feed_order'); var title_a = a.get('query') || ''; var title_b = b.get('query') || ''; @@ -89,33 +89,33 @@ NEWSBLUR.Collections.SearchesFeeds = Backbone.Collection.extend({ if (opens_a > opens_b) return -1; if (opens_a < opens_b) return 1; } - + // if (!sort_order || sort_order == 'ALPHABETICAL') - if (title_a > title_b) return 1; + if (title_a > title_b) return 1; else if (title_a < title_b) return -1; return 0; }, - - selected: function() { - return this.detect(function(feed) { return feed.get('selected'); }); + + selected: function () { + return this.detect(function (feed) { return feed.get('selected'); }); }, - - deselect: function() { - this.chain().select(function(feed) { - return feed.get('selected'); - }).each(function(feed){ - feed.set('selected', false); + + deselect: function () { + this.chain().select(function (feed) { + return feed.get('selected'); + }).each(function (feed) { + feed.set('selected', false); }); }, - - all_searches: function() { + + all_searches: function () { return this.pluck('saved_search'); }, - - get_feed: function(feed_id) { - return this.detect(function(feed) { + + get_feed: function (feed_id) { + return this.detect(function (feed) { return feed.get('feed_id') == feed_id; }); } - + }); diff --git a/media/js/newsblur/models/social_subscription.js b/media/js/newsblur/models/social_subscription.js index 9b811114f8..319a64fef8 100644 --- a/media/js/newsblur/models/social_subscription.js +++ b/media/js/newsblur/models/social_subscription.js @@ -1,72 +1,72 @@ NEWSBLUR.Models.SocialSubscription = Backbone.Model.extend({ - - initialize: function() { + + initialize: function () { if (!this.get('page_url')) { this.set('page_url', '/social/page/' + this.get('user_id')); } - + _.bindAll(this, 'on_change', 'on_remove', 'update_counts'); // this.bind('change', this.on_change); this.bind('remove', this.on_remove); - + this.bind('change:ps', this.update_counts); this.bind('change:nt', this.update_counts); this.bind('change:ng', this.update_counts); - + this.views = []; }, - on_change: function() { + on_change: function () { NEWSBLUR.log(['Social Feed Change', this.changedAttributes(), this.previousAttributes()]); }, - - on_remove: function() { + + on_remove: function () { NEWSBLUR.log(["Remove Feed", this, this.views]); - _.each(this.views, function(view) { view.remove(); }); + _.each(this.views, function (view) { view.remove(); }); }, - - update_counts: function() { + + update_counts: function () { NEWSBLUR.assets.social_feeds.trigger('change:counts'); }, - is_social: function() { + is_social: function () { return true; }, - - is_feed: function() { + + is_feed: function () { return false; }, - - is_starred: function() { + + is_starred: function () { return false; }, - - is_search: function() { + + is_search: function () { return false; }, - - unread_counts: function() { + + unread_counts: function () { return { ps: this.get('ps') || 0, nt: this.get('nt') || 0, ng: this.get('ng') || 0 }; } - + }); NEWSBLUR.Collections.SocialSubscriptions = Backbone.Collection.extend({ - - model : NEWSBLUR.Models.SocialSubscription, - - parse: function(models) { - _.each(models, function(feed) { + + model: NEWSBLUR.Models.SocialSubscription, + + parse: function (models) { + _.each(models, function (feed) { feed.selected = false; }); return models; }, - - comparator: function(a, b) { + + comparator: function (a, b) { var sort_order = NEWSBLUR.reader.model.preference('feed_order'); var title_a = a.get('feed_title') || ''; var title_b = b.get('feed_title') || ''; @@ -79,28 +79,28 @@ NEWSBLUR.Collections.SocialSubscriptions = Backbone.Collection.extend({ if (opens_a > opens_b) return -1; if (opens_a < opens_b) return 1; } - + // if (!sort_order || sort_order == 'ALPHABETICAL') - if (title_a > title_b) return 1; + if (title_a > title_b) return 1; else if (title_a < title_b) return -1; return 0; }, - - selected: function() { - return this.detect(function(feed) { return feed.get('selected'); }); + + selected: function () { + return this.detect(function (feed) { return feed.get('selected'); }); }, - - deselect: function() { - this.chain().select(function(feed) { - return feed.get('selected'); - }).each(function(feed){ - feed.set('selected', false); + + deselect: function () { + this.chain().select(function (feed) { + return feed.get('selected'); + }).each(function (feed) { + feed.set('selected', false); }); }, - - unread_counts: function(existing_counts) { + + unread_counts: function (existing_counts) { existing_counts = existing_counts || {}; - var counts = this.reduce(function(counts, item) { + var counts = this.reduce(function (counts, item) { var feed_counts = item.unread_counts(); counts['ps'] += feed_counts['ps']; counts['nt'] += feed_counts['nt']; @@ -111,8 +111,8 @@ NEWSBLUR.Collections.SocialSubscriptions = Backbone.Collection.extend({ nt: existing_counts['nt'] || 0, ng: existing_counts['ng'] || 0 }); - + return counts; } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/models/starred_counts.js b/media/js/newsblur/models/starred_counts.js index 2e877934bf..a51cd06412 100644 --- a/media/js/newsblur/models/starred_counts.js +++ b/media/js/newsblur/models/starred_counts.js @@ -1,52 +1,52 @@ NEWSBLUR.Models.StarredFeed = Backbone.Model.extend({ - - initialize: function() { + + initialize: function () { this.set('feed_title', this.get('tag')); if (this.get('is_highlights')) { this.set('feed_title', "Highlights"); } this.views = []; }, - - is_social: function() { + + is_social: function () { return false; }, - - is_feed: function() { + + is_feed: function () { return false; }, - - is_starred: function() { + + is_starred: function () { return true; }, - - is_search: function() { + + is_search: function () { return false; }, - - unread_counts: function() { + + unread_counts: function () { return { ps: this.get('count') || 0, nt: 0, ng: 0 }; }, - - tag_slug: function() { + + tag_slug: function () { if (this.get('is_highlights')) { return "highlights"; } return Inflector.sluggify(this.get('tag') || ''); } - + }); NEWSBLUR.Collections.StarredFeeds = Backbone.Collection.extend({ - + model: NEWSBLUR.Models.StarredFeed, - - parse: function(models) { - _.each(models, function(feed) { + + parse: function (models) { + _.each(models, function (feed) { feed.id = 'starred:' + (feed.tag || feed.feed_id); if (feed.is_highlights) { feed.id = 'starred:highlights'; @@ -56,8 +56,8 @@ NEWSBLUR.Collections.StarredFeeds = Backbone.Collection.extend({ }); return models; }, - - comparator: function(a, b) { + + comparator: function (a, b) { var sort_order = NEWSBLUR.reader.model.preference('feed_order'); var title_a = a.get('feed_title') || ''; var title_b = b.get('feed_title') || ''; @@ -70,33 +70,33 @@ NEWSBLUR.Collections.StarredFeeds = Backbone.Collection.extend({ if (opens_a > opens_b) return -1; if (opens_a < opens_b) return 1; } - + // if (!sort_order || sort_order == 'ALPHABETICAL') - if (title_a > title_b) return 1; + if (title_a > title_b) return 1; else if (title_a < title_b) return -1; return 0; }, - - selected: function() { - return this.detect(function(feed) { return feed.get('selected'); }); + + selected: function () { + return this.detect(function (feed) { return feed.get('selected'); }); }, - - deselect: function() { - this.chain().select(function(feed) { - return feed.get('selected'); - }).each(function(feed){ - feed.set('selected', false); + + deselect: function () { + this.chain().select(function (feed) { + return feed.get('selected'); + }).each(function (feed) { + feed.set('selected', false); }); }, - - all_tags: function() { + + all_tags: function () { return this.pluck('tag'); }, - - get_feed: function(feed_id) { - return this.detect(function(feed) { + + get_feed: function (feed_id) { + return this.detect(function (feed) { return feed.get('feed_id') == feed_id; }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/models/stories.js b/media/js/newsblur/models/stories.js index f1a81062bf..ac4b7ffa2a 100644 --- a/media/js/newsblur/models/stories.js +++ b/media/js/newsblur/models/stories.js @@ -1,6 +1,6 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ - - initialize: function() { + + initialize: function () { this.bind('change:shared_comments', this.populate_comments); this.bind('change:comments', this.populate_comments); this.bind('change:comment_count', this.populate_comments); @@ -13,41 +13,41 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ this.story_permalink = this.get('story_permalink'); this.story_title = this.get('story_title'); }, - - select_story: function(story, selected) { + + select_story: function (story, selected) { // console.log(['select_story', this, this.collection, story, selected]); if (this.collection) this.collection.detect_selected_story(this, selected); }, - - populate_comments: function(story, collection) { + + populate_comments: function (story, collection) { this.friend_comments = new NEWSBLUR.Collections.Comments(this.get('friend_comments')); this.friend_shares = new NEWSBLUR.Collections.Comments(this.get('friend_shares')); this.public_comments = new NEWSBLUR.Collections.Comments(this.get('public_comments')); }, - - feed: function() { + + feed: function () { return NEWSBLUR.assets.get_feed(this.get('story_feed_id')); }, - - score: function() { + + score: function () { if (NEWSBLUR.reader.flags['starred_view']) { return 2; } else { return NEWSBLUR.utils.compute_story_score(this); } }, - - score_name: function(score) { + + score_name: function (score) { score = !_.isUndefined(score) ? score : this.score(); var score_name = 'neutral'; if (score > 0) score_name = 'positive'; if (score < 0) score_name = 'negative'; return score_name; }, - - content_preview: function(attribute, length, preserve_paragraphs) { + + content_preview: function (attribute, length, preserve_paragraphs) { var content = this.get(attribute); - if (!attribute || !content) content = this.story_content(); + if (!attribute || !content) content = this.story_content(); // First do a naive strip, which is faster than rendering which makes network calls content = content && content .replace(/| [^>]+>)/ig, '\n\n') @@ -61,7 +61,7 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ .replace(/(\n\s*\n){1,}/gm, '\n\n') .replace(/\n\n>\s+/gm, '\n\n> ') .replace(/([^\n])\n([^\n])/gm, '$1 $2'); - + if (!preserve_paragraphs) { content = content && content.replace(/\s+/gm, ' '); } @@ -72,12 +72,12 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ } return content }, - - image_url: function(index) { + + image_url: function (index) { if (!index) index = 0; - if (this.get('image_urls') && this.get('image_urls').length >= index+1) { + if (this.get('image_urls') && this.get('image_urls').length >= index + 1) { var url = this.get('image_urls')[index]; - if (window.location.protocol == 'https:' && + if (window.location.protocol == 'https:' && _.str.startsWith(url, "http://")) { var secure_url = this.get('secure_image_urls')[url]; if (secure_url) url = secure_url; @@ -85,56 +85,56 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ return url; } }, - - story_content: function() { + + story_content: function () { return this.secure_content('story_content'); }, - - original_text: function() { + + original_text: function () { return this.secure_content('original_text'); }, - - secure_content: function(content_attr) { + + secure_content: function (content_attr) { var content = this.get(content_attr); - + if (window.location.protocol == 'https:') { - _.each(this.get('secure_image_urls'), function(secure_url, url) { + _.each(this.get('secure_image_urls'), function (secure_url, url) { if (_.str.startsWith(url, "http://")) { // console.log(['Securing image url', url, secure_url]); content = content.split(url).join(secure_url); } }); } - + return content; }, - + story_authors: function () { let authors = this.get('story_authors') || ""; return authors.replace(//g, '>'); }, - - user_highlights: function() { + + user_highlights: function () { var highlights = this.get('highlights'); return highlights; }, - - formatted_short_date: function() { + + formatted_short_date: function () { var timestamp = this.get('story_timestamp'); var dateformat = NEWSBLUR.assets.preference('dateformat'); var date = new Date(parseInt(timestamp, 10) * 1000); - var midnight_today = function() { + var midnight_today = function () { var midnight = new Date(); midnight.setHours(0); midnight.setMinutes(0); midnight.setSeconds(0); return midnight; }; - var midnight_tomorrow = function(midnight, days) { + var midnight_tomorrow = function (midnight, days) { if (!days) days = 1; - return new Date(midnight.getTime() + days*60*60*24*1000); + return new Date(midnight.getTime() + days * 60 * 60 * 24 * 1000); }; - var midnight_yesterday = function(midnight) { + var midnight_yesterday = function (midnight) { return midnight_tomorrow(midnight, -1); }; var midnight = midnight_today(); @@ -156,26 +156,26 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ return date.format("d M Y, ") + time; } }, - - formatted_long_date: function() { + + formatted_long_date: function () { var timestamp = this.get('story_timestamp'); var dateformat = NEWSBLUR.assets.preference('dateformat'); var date = new Date(parseInt(timestamp, 10) * 1000); - var midnight_today = function() { + var midnight_today = function () { var midnight = new Date(); midnight.setHours(0); midnight.setMinutes(0); midnight.setSeconds(0); return midnight; }; - var midnight_tomorrow = function(midnight, days) { + var midnight_tomorrow = function (midnight, days) { if (!days) days = 1; - return new Date(midnight.getTime() + days*60*60*24*1000); + return new Date(midnight.getTime() + days * 60 * 60 * 24 * 1000); }; - var midnight_yesterday = function(midnight) { + var midnight_yesterday = function (midnight) { return midnight_tomorrow(midnight, -1); }; - var beginning_of_month = function() { + var beginning_of_month = function () { var month = new Date(); month.setHours(0); month.setMinutes(0); @@ -204,12 +204,12 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ } }, - mark_read: function(options) { + mark_read: function (options) { return NEWSBLUR.assets.stories.mark_read(this, options); }, - - open_story_in_new_tab: function(background) { - this.mark_read({skip_delay: true}); + + open_story_in_new_tab: function (background) { + this.mark_read({ skip_delay: true }); // Safari browser on Linux is an impossibility, and thus we're actually // on a WebKit-based browser (WebKitGTK or QTWebKit). These can't handle @@ -218,11 +218,11 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ if ($.browser.safari && (/(\(X11|Linux)/.test(navigator.userAgent))) { background = false; } - + if (background && $.browser.webkit) { var event = new CustomEvent("openInNewTab", { bubbles: true, - detail: {background: background} + detail: { background: background } }); var success = !this.story_title_view.$st.find('a')[0].dispatchEvent(event); if (!success) { @@ -235,7 +235,7 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ // console.log(['Safari extension failed to open link in background', success, this.story_title_view.$st.find('a')[0]]); } } - + if (background && !$.browser.mozilla && false) { var anchor, event; @@ -251,18 +251,18 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ console.log(['Failed to open link in background', anchor.href]); } } - + window.open(this.get('story_permalink'), '_blank'); if (!background) { window.focus(); } }, - - open_share_dialog: function(e, view) { + + open_share_dialog: function (e, view) { if (view == 'title') { var $story_title = this.story_title_view.$st; this.story_title_view.mouseenter_manage_icon(); - NEWSBLUR.reader.show_manage_menu('story', $story_title, {story_id: this.id}); + NEWSBLUR.reader.show_manage_menu('story', $story_title, { story_id: this.id }); NEWSBLUR.reader.show_confirm_story_share_menu_item(this.id); } else { var $story = this.latest_story_detail_view.$el; @@ -271,14 +271,14 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ }); } }, - + // ================= // = Saved Stories = // ================= - - toggle_starred: function(force_starred) { - this.set('user_tags', this.existing_tags(), {silent: true}); - + + toggle_starred: function (force_starred) { + this.set('user_tags', this.existing_tags(), { silent: true }); + if (!this.get('starred')) { NEWSBLUR.assets.starred_count += 1; this.set('starred', true); @@ -288,8 +288,8 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ } NEWSBLUR.reader.update_starred_count(); }, - - update_highlights: function() { + + update_highlights: function () { console.log(['update_highlights', this.get('highlights')]); if (!this.get('starred')) { NEWSBLUR.assets.starred_count += 1; @@ -297,43 +297,43 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ } else { NEWSBLUR.assets.mark_story_as_starred(this.id); } - NEWSBLUR.reader.update_starred_count(); + NEWSBLUR.reader.update_starred_count(); }, - - update_notes: function() { + + update_notes: function () { NEWSBLUR.assets.mark_story_as_starred(this.id); }, - - change_starred: function() { + + change_starred: function () { if (this.get('starred')) { NEWSBLUR.assets.mark_story_as_starred(this.id); } else { NEWSBLUR.assets.mark_story_as_unstarred(this.id); } }, - - change_user_tags: function(tags, options, etc) { + + change_user_tags: function (tags, options, etc) { NEWSBLUR.assets.mark_story_as_starred(this.id); }, - - existing_tags: function() { + + existing_tags: function () { var tags = this.get('user_tags'); - + if (!tags) { tags = this.folder_tags(); } - + return tags || []; }, - - unused_story_tags: function() { - var tags = _.reduce(this.get('user_tags') || [], function(m, t) { + + unused_story_tags: function () { + var tags = _.reduce(this.get('user_tags') || [], function (m, t) { return _.without(m, t); }, this.get('story_tags')); return tags; }, - - folder_tags: function() { + + folder_tags: function () { var folder_tags = []; var feed_id = this.get('story_feed_id'); var feed = NEWSBLUR.assets.get_feed(feed_id); @@ -342,8 +342,8 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ } return folder_tags; }, - - all_tags: function() { + + all_tags: function () { var tags = []; var story_tags = this.get('story_tags') || []; var user_tags = this.get('user_tags') || []; @@ -351,49 +351,49 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({ var existing_tags = NEWSBLUR.assets.starred_feeds.all_tags(); var all_tags = _.unique(_.compact(_.reduce([ story_tags, user_tags, folder_tags, existing_tags - ], function(x, m) { - return m.concat(x); + ], function (x, m) { + return m.concat(x); }, []))); - + return all_tags; } - + }); NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ - + model: NEWSBLUR.Models.Story, - + read_stories: [], - + previous_stories_stack: [], - + active_story: null, page_fill_outs: 0, no_more_stories: false, - - initialize: function() { + + initialize: function () { // this.bind('change:selected', this.detect_selected_story, this); // Handled in the Story model so it fires first this.bind('reset', this.clear_previous_stories_stack, this); // this.bind('change:selected', this.change_selected); }, - + // =========== // = Actions = // =========== - - deselect_other_stories: function(selected_story) { - this.any(function(story) { + + deselect_other_stories: function (selected_story) { + this.any(function (story) { if (story.get('selected') && story.id != selected_story.id) { story.set('selected', false); return true; } }); }, - - mark_read: function(story, options) { + + mark_read: function (story, options) { options = options || {}; var previously_read = story.get('read_status'); var delay = NEWSBLUR.assets.preference('read_story_delay'); @@ -406,62 +406,62 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ } clearTimeout(this.read_story_delay); - - var _mark_read = _.bind(function() { + + var _mark_read = _.bind(function () { if (!delay || (delay && this.active_story.id == story.id)) { var feed = NEWSBLUR.assets.get_feed(NEWSBLUR.reader.active_feed); if (!feed) { feed = NEWSBLUR.assets.get_feed(story.get('story_feed_id')); } - NEWSBLUR.assets.mark_story_hash_as_read(story, null, _.bind(function() { + NEWSBLUR.assets.mark_story_hash_as_read(story, null, _.bind(function () { this.store_failed_marked_read_story(story); }, this)); - this.update_read_count(story, {previously_read: previously_read}); + this.update_read_count(story, { previously_read: previously_read }); } }, this); - + if (delay) { this.read_story_delay = _.delay(_mark_read, delay * 1000); } else { _mark_read(); } }, - - mark_read_pubsub: function(story_hash) { + + mark_read_pubsub: function (story_hash) { var story = this.get_by_story_hash(story_hash); if (!story) return; story.set('read_status', 1); }, - - mark_unread_pubsub: function(story_hash) { + + mark_unread_pubsub: function (story_hash) { var story = this.get_by_story_hash(story_hash); if (!story) return; story.set('read_status', 0); }, - - mark_unread: function(story, options) { + + mark_unread: function (story, options) { options = options || {}; - NEWSBLUR.assets.mark_story_as_unread(story.id, story.get('story_feed_id'), _.bind(function(read) { + NEWSBLUR.assets.mark_story_as_unread(story.id, story.get('story_feed_id'), _.bind(function (read) { this.update_read_count(story, { unread: true }); NEWSBLUR.assets.get_feed(story.get('story_feed_id')).force_update_counts(); - }, this), _.bind(function(data) { - story.set('read_status', 1, {'error_marking_unread': true, 'message': data.message}); - this.update_read_count(story, {unread: false}); + }, this), _.bind(function (data) { + story.set('read_status', 1, { 'error_marking_unread': true, 'message': data.message }); + this.update_read_count(story, { unread: false }); NEWSBLUR.assets.get_feed(story.get('story_feed_id')).force_update_counts(); }, this)); story.set('read_status', 0); }, - - update_read_count: function(story, options) { + + update_read_count: function (story, options) { options = options || {}; - + if (options.previously_read) return; - var active_feed = NEWSBLUR.assets.get_feed(NEWSBLUR.reader.active_feed); - var story_feed = NEWSBLUR.assets.get_feed(story.get('story_feed_id')); - var friend_feeds = NEWSBLUR.assets.get_friend_feeds(story); + var active_feed = NEWSBLUR.assets.get_feed(NEWSBLUR.reader.active_feed); + var story_feed = NEWSBLUR.assets.get_feed(story.get('story_feed_id')); + var friend_feeds = NEWSBLUR.assets.get_friend_feeds(story); if (!active_feed) { // River of News does not have an active feed. @@ -469,82 +469,82 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ } else if (active_feed && active_feed.is_feed() && active_feed.is_social()) { friend_feeds = _.without(friend_feeds, active_feed); } - + if (story.score() > 0) { - var active_count = active_feed && Math.max(active_feed.get('ps') + (options.unread?1:-1), 0); - var story_count = story_feed && Math.max(story_feed.get('ps') + (options.unread?1:-1), 0); - if (active_feed) active_feed.set('ps', active_count, {instant: true}); - if (story_feed) story_feed.set('ps', story_count, {instant: true}); - _.each(friend_feeds, function(socialsub) { - var socialsub_count = Math.max(socialsub.get('ps') + (options.unread?1:-1), 0); - socialsub.set('ps', socialsub_count, {instant: true}); + var active_count = active_feed && Math.max(active_feed.get('ps') + (options.unread ? 1 : -1), 0); + var story_count = story_feed && Math.max(story_feed.get('ps') + (options.unread ? 1 : -1), 0); + if (active_feed) active_feed.set('ps', active_count, { instant: true }); + if (story_feed) story_feed.set('ps', story_count, { instant: true }); + _.each(friend_feeds, function (socialsub) { + var socialsub_count = Math.max(socialsub.get('ps') + (options.unread ? 1 : -1), 0); + socialsub.set('ps', socialsub_count, { instant: true }); }); } else if (story.score() == 0) { - var active_count = active_feed && Math.max(active_feed.get('nt') + (options.unread?1:-1), 0); - var story_count = story_feed && Math.max(story_feed.get('nt') + (options.unread?1:-1), 0); - if (active_feed) active_feed.set('nt', active_count, {instant: true}); - if (story_feed) story_feed.set('nt', story_count, {instant: true}); - _.each(friend_feeds, function(socialsub) { - var socialsub_count = Math.max(socialsub.get('nt') + (options.unread?1:-1), 0); - socialsub.set('nt', socialsub_count, {instant: true}); + var active_count = active_feed && Math.max(active_feed.get('nt') + (options.unread ? 1 : -1), 0); + var story_count = story_feed && Math.max(story_feed.get('nt') + (options.unread ? 1 : -1), 0); + if (active_feed) active_feed.set('nt', active_count, { instant: true }); + if (story_feed) story_feed.set('nt', story_count, { instant: true }); + _.each(friend_feeds, function (socialsub) { + var socialsub_count = Math.max(socialsub.get('nt') + (options.unread ? 1 : -1), 0); + socialsub.set('nt', socialsub_count, { instant: true }); }); } else if (story.score() < 0) { - var active_count = active_feed && Math.max(active_feed.get('ng') + (options.unread?1:-1), 0); - var story_count = story_feed && Math.max(story_feed.get('ng') + (options.unread?1:-1), 0); - if (active_feed) active_feed.set('ng', active_count, {instant: true}); - if (story_feed) story_feed.set('ng', story_count, {instant: true}); - _.each(friend_feeds, function(socialsub) { - var socialsub_count = Math.max(socialsub.get('ng') + (options.unread?1:-1), 0); - socialsub.set('ng', socialsub_count, {instant: true}); + var active_count = active_feed && Math.max(active_feed.get('ng') + (options.unread ? 1 : -1), 0); + var story_count = story_feed && Math.max(story_feed.get('ng') + (options.unread ? 1 : -1), 0); + if (active_feed) active_feed.set('ng', active_count, { instant: true }); + if (story_feed) story_feed.set('ng', story_count, { instant: true }); + _.each(friend_feeds, function (socialsub) { + var socialsub_count = Math.max(socialsub.get('ng') + (options.unread ? 1 : -1), 0); + socialsub.set('ng', socialsub_count, { instant: true }); }); } - + // if ((unread_view == 'positive' && feed.get('ps') == 0) || // (unread_view == 'neutral' && feed.get('ps') == 0 && feed.get('nt') == 0) || // (unread_view == 'negative' && feed.get('ps') == 0 && feed.get('nt') == 0 && feed.get('ng') == 0)) { // story_unread_counter.fall(); // } }, - - store_failed_marked_read_story: function(story) { + + store_failed_marked_read_story: function (story) { console.log(['Failed to mark story as read, storing for later', story, story.get('story_hash')]); - + var failed_stories = JSON.parse(localStorage.getItem("NB:failed_marked_read_stories")) || []; - + if (!_.contains(failed_stories, story.get('story_hash'))) { failed_stories.push(story.get('story_hash')); } - + localStorage.setItem("NB:failed_marked_read_stories", JSON.stringify(failed_stories)); }, - - retry_failed_marked_read_stories: function(failed_stories) { + + retry_failed_marked_read_stories: function (failed_stories) { if (!failed_stories) { failed_stories = JSON.parse(localStorage.getItem("NB:failed_marked_read_stories")) || []; } if (failed_stories.length == 0) return; - - var failed_story = _.first(failed_stories); + + var failed_story = _.first(failed_stories); console.log(['Retrying failed marked read stories', failed_stories, failed_story]); - var story = new NEWSBLUR.Models.Story({'story_hash': failed_story, read_status: 0}); - - NEWSBLUR.assets.mark_story_hash_as_read(story, _.bind(function(read) { - localStorage.setItem("NB:failed_marked_read_stories", - JSON.stringify(_.without(failed_stories, failed_story))); + var story = new NEWSBLUR.Models.Story({ 'story_hash': failed_story, read_status: 0 }); + + NEWSBLUR.assets.mark_story_hash_as_read(story, _.bind(function (read) { + localStorage.setItem("NB:failed_marked_read_stories", + JSON.stringify(_.without(failed_stories, failed_story))); this.retry_failed_marked_read_stories(); - }, this), _.bind(function() { + }, this), _.bind(function () { this.store_failed_marked_read_story(story); - }, this), {retrying_failed: true}); - - this.update_read_count(story, {previously_read: story.get('read_status')}); + }, this), { retrying_failed: true }); + + this.update_read_count(story, { previously_read: story.get('read_status') }); }, - - clear_previous_stories_stack: function() { + + clear_previous_stories_stack: function () { this.previous_stories_stack = []; this.active_story = null; }, - - select_previous_story: function() { + + select_previous_story: function () { if (this.previous_stories_stack.length) { var previous_story = this.previous_stories_stack.pop(); if (previous_story.get('selected') || @@ -552,58 +552,58 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ this.select_previous_story(); return; } - + previous_story.set('selected', true); } }, - + // ================== // = Model Managers = // ================== - - visible: function(score) { + + visible: function (score) { score = _.isUndefined(score) ? NEWSBLUR.reader.get_unread_view_score() : score; - - return this.select(function(story) { + + return this.select(function (story) { return story.score() >= score || story.get('visible'); }); }, - - last_visible: function(score) { + + last_visible: function (score) { score = _.isUndefined(score) ? NEWSBLUR.reader.get_unread_view_score() : score; var size = this.size(); if (!size || size <= 0) return; - for (var i=size-1; i >= 0; i--) { + for (var i = size - 1; i >= 0; i--) { var story = this.at(i); if (story.score() >= score || story.get('visible')) { return story; } } }, - - visible_and_unread: function(score, include_active_story) { + + visible_and_unread: function (score, include_active_story) { var active_story_id = this.active_story && this.active_story.id; score = _.isUndefined(score) ? NEWSBLUR.reader.get_unread_view_score() : score; - - return this.select(function(story) { + + return this.select(function (story) { var visible = story.score() >= score || story.get('visible'); var same_story = include_active_story && story.id == active_story_id; var read = !!story.get('read_status'); - + return visible && (!read || same_story); }); }, - - hidden: function(score) { + + hidden: function (score) { score = _.isUndefined(score) ? NEWSBLUR.reader.get_unread_view_score() : score; - return this.select(function(story) { + return this.select(function (story) { return story.score() < score && !story.get('visible'); }); }, - - limit: function(count) { + + limit: function (count) { this.models = this.models.slice(0, count); }, @@ -619,12 +619,12 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ this.models.pop(); } }, - + // =========== // = Getters = // =========== - - get_next_story: function(direction, options) { + + get_next_story: function (direction, options) { options = options || {}; if (direction == -1) return this.get_previous_story(options); @@ -636,12 +636,12 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ var current_index = _.indexOf(visible_stories, this.active_story); - if (current_index+1 <= visible_stories.length) { - return visible_stories[current_index+1]; + if (current_index + 1 <= visible_stories.length) { + return visible_stories[current_index + 1]; } }, - - get_previous_story: function(options) { + + get_previous_story: function (options) { options = options || {}; var visible_stories = this.visible(options.score); @@ -651,61 +651,61 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ var current_index = _.indexOf(visible_stories, this.active_story); - if (current_index-1 >= 0) { - return visible_stories[current_index-1]; + if (current_index - 1 >= 0) { + return visible_stories[current_index - 1]; } }, - - get_next_unread_story: function(options) { + + get_next_unread_story: function (options) { options = options || {}; var visible_stories = this.visible_and_unread(options.score, true); if (!visible_stories.length) return; - + if (!this.active_story) { return visible_stories[0]; } var current_index = _.indexOf(visible_stories, this.active_story); - + // The +1+1 is because the currently selected story is included, so it // counts for more than what is available. - if (current_index+1+1 <= visible_stories.length) { - if (visible_stories[current_index+1]) - return visible_stories[current_index+1]; - } else if (current_index-1 >= 0) { - return visible_stories[current_index-1]; + if (current_index + 1 + 1 <= visible_stories.length) { + if (visible_stories[current_index + 1]) + return visible_stories[current_index + 1]; + } else if (current_index - 1 >= 0) { + return visible_stories[current_index - 1]; } else if (visible_stories.length == 1 && visible_stories[0] == this.active_story && !this.active_story.get('read_status')) { // If the current story is unread yet selected, switch it back. visible_stories[current_index].set('selected', false); return visible_stories[current_index]; } }, - - get_last_unread_story: function(unread_count, options) { + + get_last_unread_story: function (unread_count, options) { options = options || {}; var visible_stories = this.visible_and_unread(options.score); if (!visible_stories.length || visible_stories.length < unread_count) return; - + return _.last(visible_stories); }, - - get_by_story_hash: function(story_hash) { - return this.detect(function(s) { return s.get('story_hash') == story_hash; }); + + get_by_story_hash: function (story_hash) { + return this.detect(function (s) { return s.get('story_hash') == story_hash; }); }, - - deselect: function() { - this.each(function(story){ + + deselect: function () { + this.each(function (story) { if (story.get('selected')) { - story.set('selected', false); + story.set('selected', false); } }); }, - + // ========== // = Events = // ========== - - detect_selected_story: function(selected_story, selected) { + + detect_selected_story: function (selected_story, selected) { if (selected) { // console.log(['detect_selected_story', selected, selected_story, this.active_story, this == NEWSBLUR.assets.stories ? "stories" : "dashboard"]); this.deselect_other_stories(selected_story); @@ -717,5 +717,5 @@ NEWSBLUR.Collections.Stories = Backbone.Collection.extend({ } } } - + }); diff --git a/media/js/newsblur/models/users.js b/media/js/newsblur/models/users.js index 9b5cb8b5b6..464395b664 100644 --- a/media/js/newsblur/models/users.js +++ b/media/js/newsblur/models/users.js @@ -1,14 +1,14 @@ NEWSBLUR.Models.User = Backbone.Model.extend({ - - get: function(attr) { + + get: function (attr) { var value = Backbone.Model.prototype.get.call(this, attr); if (attr == 'photo_url' && !value) { value = NEWSBLUR.Globals.MEDIA_URL + 'img/reader/default_profile_photo.png'; } return value; }, - - photo_url: function(options) { + + photo_url: function (options) { options = options || {}; var url = this.get('photo_url'); if (options.size && _.string.include(url, 'graph.facebook.com')) { @@ -18,8 +18,8 @@ NEWSBLUR.Models.User = Backbone.Model.extend({ } return url; }, - - blurblog_url: function() { + + blurblog_url: function () { return [ 'http://', Inflector.sluggify(this.get('username')), @@ -27,19 +27,19 @@ NEWSBLUR.Models.User = Backbone.Model.extend({ window.location.host.replace('www.', '') ].join(''); } - + }); NEWSBLUR.Collections.Users = Backbone.Collection.extend({ - - model : NEWSBLUR.Models.User, - - find: function(user_id) { - return this.detect(function(user) { return user.get('user_id') == user_id; }); + + model: NEWSBLUR.Models.User, + + find: function (user_id) { + return this.detect(function (user) { return user.get('user_id') == user_id; }); }, - - comparator: function(model) { + + comparator: function (model) { return -1 * model.get('shared_stories_count'); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/payments/paypal_return.js b/media/js/newsblur/payments/paypal_return.js index 0a65e9c521..45544425ad 100644 --- a/media/js/newsblur/payments/paypal_return.js +++ b/media/js/newsblur/payments/paypal_return.js @@ -1,32 +1,32 @@ -(function($) { +(function ($) { - $(document).ready(function() { - if($('.NB-paypal-return').length) { + $(document).ready(function () { + if ($('.NB-paypal-return').length) { NEWSBLUR.paypal_return = new NEWSBLUR.PaypalReturn(); } }); - NEWSBLUR.PaypalReturn = function() { + NEWSBLUR.PaypalReturn = function () { this.retries = 0; _.delay(_.bind(function () { if (_.string.include(window.location.pathname, 'paypal_archive')) { this.detect_premium_archive(); - setInterval(_.bind(function() { this.detect_premium_archive(); }, this), 2000); + setInterval(_.bind(function () { this.detect_premium_archive(); }, this), 2000); } else { this.detect_premium(); - setInterval(_.bind(function() { this.detect_premium(); }, this), 2000); + setInterval(_.bind(function () { this.detect_premium(); }, this), 2000); } }, this), 2000); }; NEWSBLUR.PaypalReturn.prototype = { - detect_premium: function() { + detect_premium: function () { $.ajax({ - 'url' : '/profile/is_premium', - 'data' : {'retries': this.retries}, - 'dataType' : 'json', - 'success' : _.bind(function(resp) { + 'url': '/profile/is_premium', + 'data': { 'retries': this.retries }, + 'dataType': 'json', + 'success': _.bind(function (resp) { // NEWSBLUR.log(['resp', resp]); if (resp.code < 0) { this.homepage(); @@ -39,7 +39,7 @@ }); } }, this), - 'error' : _.bind(function() { + 'error': _.bind(function () { this.retries += 1; if (this.retries > 30) { this.homepage(); @@ -48,12 +48,12 @@ }); }, - detect_premium_archive: function() { + detect_premium_archive: function () { $.ajax({ - 'url' : '/profile/is_premium_archive', - 'data' : {'retries': this.retries}, - 'dataType' : 'json', - 'success' : _.bind(function(resp) { + 'url': '/profile/is_premium_archive', + 'data': { 'retries': this.retries }, + 'dataType': 'json', + 'success': _.bind(function (resp) { // NEWSBLUR.log(['resp', resp]); if (resp.code < 0) { this.homepage(); @@ -66,7 +66,7 @@ }); } }, this), - 'error' : _.bind(function() { + 'error': _.bind(function () { this.retries += 1; if (this.retries > 30) { this.homepage(); @@ -74,11 +74,11 @@ }, this) }); }, - - homepage: function() { + + homepage: function () { window.location.href = '/'; } }; - + })(jQuery); diff --git a/media/js/newsblur/payments/stripe_form.js b/media/js/newsblur/payments/stripe_form.js index ad2934e8c5..11e745843a 100644 --- a/media/js/newsblur/payments/stripe_form.js +++ b/media/js/newsblur/payments/stripe_form.js @@ -1,4 +1,4 @@ -$(function() { +$(function () { if ($('.NB-stripe-form').length) { // $("#id_card_number").parents("form").submit(function() { // if ( $("#id_card_number").is(":visible")) { @@ -27,7 +27,7 @@ $(function() { // // return true; // }); - + function addInputNames() { // Not ideal, but jQuery's validate plugin requires fields to have names // so we add them at the last possible minute, in case any javascript @@ -53,13 +53,13 @@ $(function() { $("button[type=submit]").addClass("NB-disabled"); $("button[type=submit]").removeClass("NB-modal-submit-green"); $("button[type=submit]").text("Submitting..."); - + Stripe.createToken({ number: $('.card-number').val(), cvc: $('.card-cvv').val(), - exp_month: $('.card-expiry-month').val(), + exp_month: $('.card-expiry-month').val(), exp_year: $('.card-expiry-year').val() - }, function(status, response) { + }, function (status, response) { if (response.error) { // re-enable the submit button $("button[type=submit]").removeAttr("disabled"); @@ -86,24 +86,24 @@ $(function() { // add custom rules for credit card validating jQuery.validator.addMethod("cardNumber", Stripe.validateCardNumber, "Please enter a valid card number"); jQuery.validator.addMethod("cardCVC", Stripe.validateCVC, "Please enter a valid security code"); - jQuery.validator.addMethod("cardExpiry", function() { - return Stripe.validateExpiry($(".card-expiry-month").val(), - $(".card-expiry-year").val()); + jQuery.validator.addMethod("cardExpiry", function () { + return Stripe.validateExpiry($(".card-expiry-month").val(), + $(".card-expiry-year").val()); }, "Please enter a valid expiration"); // We use the jQuery validate plugin to validate required params on submit $("#id_card_number").parents("form").validate({ submitHandler: submit, rules: { - "card-cvc" : { + "card-cvc": { cardCVC: true, required: true }, - "card-number" : { + "card-number": { cardNumber: true, required: true }, - "card-expiry-year" : "cardExpiry", // we don't validate month separately + "card-expiry-year": "cardExpiry", // we don't validate month separately "email": { required: true, email: true @@ -114,14 +114,14 @@ $(function() { // adding the input field names is the last step, in case an earlier step errors addInputNames(); } - - + + var $payextra = $("input[name=payextra]"); var $label2 = $("label[for=id_plan_1]"); var $label3 = $("label[for=id_plan_2]"); var $radio2 = $("input#id_plan_1"); var $radio3 = $("input#id_plan_2"); - var change_payextra = function() { + var change_payextra = function () { if ($payextra.is(':checked')) { $label2.hide(); $label3.show(); diff --git a/media/js/newsblur/reader/reader.js b/media/js/newsblur/reader/reader.js index ea698cf581..9433ab7fd5 100644 --- a/media/js/newsblur/reader/reader.js +++ b/media/js/newsblur/reader/reader.js @@ -1,16 +1,16 @@ -(function($) { +(function ($) { NEWSBLUR.Reader = Backbone.Router.extend({ - - init: function(options) { - + + init: function (options) { + var defaults = {}; // if (console && console.clear && _.isFunction(console.clear)) console.clear(); - + // =========== // = Globals = // =========== - + NEWSBLUR.assets = new NEWSBLUR.AssetModel(); this.model = NEWSBLUR.assets; this.story_view = 'page'; @@ -87,27 +87,27 @@ this.views = {}; this.layout = {}; this.constants = { - FEED_REFRESH_INTERVAL: (1000 * 60) * 1, // 1 minute - FILL_OUT_PAGES: 100, - FIND_NEXT_UNREAD_STORY_TRIES: 100, - RIVER_STORIES_FOR_STANDARD_ACCOUNT: 3, - MIN_FEED_LIST_SIZE: 225, - MIN_STORY_LIST_SIZE: 68 + FEED_REFRESH_INTERVAL: (1000 * 60) * 1, // 1 minute + FILL_OUT_PAGES: 100, + FIND_NEXT_UNREAD_STORY_TRIES: 100, + RIVER_STORIES_FOR_STANDARD_ACCOUNT: 3, + MIN_FEED_LIST_SIZE: 225, + MIN_STORY_LIST_SIZE: 68 }; - + // ================== // = Event Handlers = // ================== - + $(window).bind('resize.reader', _.throttle($.rescope(this.resize_window, this), 1000)); this.$s.$body.bind('click.reader', $.rescope(this.handle_clicks, this)); this.$s.$body.bind('keyup.reader', $.rescope(this.handle_keyup, this)); this.handle_keystrokes(); - + // ================== // = Initialization = // ================== - + var refresh_page = this.check_and_load_ssl(); if (refresh_page) return; this.load_javascript_elements_on_page(); @@ -118,14 +118,14 @@ socialfeed_collection: NEWSBLUR.assets.social_feeds }); NEWSBLUR.app.sidebar = new NEWSBLUR.Views.Sidebar(); - NEWSBLUR.app.feed_list = new NEWSBLUR.Views.FeedList({el: this.$s.$feed_list[0]}); + NEWSBLUR.app.feed_list = new NEWSBLUR.Views.FeedList({ el: this.$s.$feed_list[0] }); NEWSBLUR.app.story_titles = new NEWSBLUR.Views.StoryTitlesView({ el: this.$s.$story_titles.find('.NB-story-titles'), collection: NEWSBLUR.assets.stories }); - NEWSBLUR.app.story_list = new NEWSBLUR.Views.StoryListView({collection: NEWSBLUR.assets.stories}); - NEWSBLUR.app.original_tab_view = new NEWSBLUR.Views.OriginalTabView({collection: NEWSBLUR.assets.stories}); - NEWSBLUR.app.story_tab_view = new NEWSBLUR.Views.StoryTabView({collection: NEWSBLUR.assets.stories}); + NEWSBLUR.app.story_list = new NEWSBLUR.Views.StoryListView({ collection: NEWSBLUR.assets.stories }); + NEWSBLUR.app.original_tab_view = new NEWSBLUR.Views.OriginalTabView({ collection: NEWSBLUR.assets.stories }); + NEWSBLUR.app.story_tab_view = new NEWSBLUR.Views.StoryTabView({ collection: NEWSBLUR.assets.stories }); NEWSBLUR.app.text_tab_view = new NEWSBLUR.Views.TextTabView({ el: this.$s.$text_view, collection: NEWSBLUR.assets.stories @@ -136,7 +136,7 @@ NEWSBLUR.app.taskbar_info = new NEWSBLUR.Views.ReaderTaskbarInfo().render(); NEWSBLUR.app.story_titles_header = new NEWSBLUR.Views.StoryTitlesHeader(); NEWSBLUR.app.search_header = new NEWSBLUR.Views.FeedSearchHeader(); - + NEWSBLUR.assets.feeds.bind('reset', _.bind(function () { this.load_dashboard_rivers(); this.load_intelligence_slider(); @@ -159,27 +159,27 @@ // ======== // = Page = // ======== - - logout: function() { + + logout: function () { console.log(['Logout']); window.location.href = "/reader/logout"; }, - - check_and_load_ssl: function() { + + check_and_load_ssl: function () { if (window.location.protocol == 'http:' && this.model.preference('ssl')) { window.location.href = window.location.href.replace('http:', 'https:'); return true; } }, - - load_javascript_elements_on_page: function() { - $('.NB-javascript').removeClass('NB-javascript'); + + load_javascript_elements_on_page: function () { + $('.NB-javascript').removeClass('NB-javascript'); }, - - resize_window: function() { + + resize_window: function () { var flag; var view = this.story_view; - + if (this.flags['page_view_showing_feed_view']) { view = 'feed'; flag = 'page'; @@ -190,14 +190,14 @@ view = 'text'; flag = 'text'; } - + this.flags.scrolling_by_selecting_story_title = true; clearTimeout(this.locks.scrolling); - this.locks.scrolling = _.delay(_.bind(function() { + this.locks.scrolling = _.delay(_.bind(function () { this.flags.scrolling_by_selecting_story_title = false; }, this), 1000); this.position_mouse_indicator(); - + this.switch_taskbar_view(view, { skip_save_type: flag, resize: true @@ -211,23 +211,23 @@ } this.flags.fetch_story_locations_in_feed_view = this.flags.fetch_story_locations_in_feed_view || - _.throttle(function() { - NEWSBLUR.app.story_list.reset_story_positions(); - }, 2000); + _.throttle(function () { + NEWSBLUR.app.story_list.reset_story_positions(); + }, 2000); this.flags.fetch_story_locations_in_feed_view(); this.adjust_for_narrow_window(); }, - - adjust_for_narrow_window: function() { + + adjust_for_narrow_window: function () { // Check if layout is ready or still being assembled if (!NEWSBLUR.reader.layout.contentLayout) return; - + var north, center, west; var story_layout = NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); var content_width; var $windows = this.$s.$body.add(this.$s.$feed_view) - .add(this.$s.$story_titles) - .add(this.$s.$text_view); + .add(this.$s.$story_titles) + .add(this.$s.$text_view); if (story_layout == 'split') { north = NEWSBLUR.reader.layout.contentLayout.panes.north; center = NEWSBLUR.reader.layout.contentLayout.panes.center; @@ -252,27 +252,27 @@ this.flags.narrow_content = !!narrow; content_width = center_width + (west ? west.width() : 0); } - + if ((north && north.width() < 640) || (content_width && content_width < 780)) { $windows.addClass('NB-narrow'); } else { $windows.removeClass('NB-narrow'); } - + var pane = this.layout.outerLayout.panes.west; var width = this.layout.outerLayout.state.west.size; pane.toggleClass("NB-narrow-pane-blue", width < 306); pane.toggleClass("NB-narrow-pane-green", width < 278); pane.toggleClass("NB-narrow-pane-yellow", width < 258); - + this.apply_tipsy_titles(); }, - - apply_resizable_layout: function(options) { + + apply_resizable_layout: function (options) { options = options || {}; var story_anchor = this.model.preference('story_pane_anchor'); - + if (options.right_side) { this.layout.contentLayout && this.layout.contentLayout.destroy(); this.layout.rightLayout && this.layout.rightLayout.destroy(); @@ -283,146 +283,146 @@ var feed_stories_bin = $.make('div').append(this.$s.$feed_stories.children()); var story_titles_bin = $.make('div').append(this.$s.$story_titles.children()); } - + $('.right-pane').removeClass('NB-story-pane-west') - .removeClass('NB-story-pane-north') - .removeClass('NB-story-pane-south') - .removeClass('NB-story-pane-hidden') - .toggleClass('NB-story-pane-'+story_anchor, - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'split'); + .removeClass('NB-story-pane-north') + .removeClass('NB-story-pane-south') + .removeClass('NB-story-pane-hidden') + .toggleClass('NB-story-pane-' + story_anchor, + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'split'); if (!options.right_side) { - this.layout.outerLayout = this.$s.$layout.layout({ - zIndex: 2, - fxName: "slideOffscreen", - fxSettings: { duration: 560, easing: "easeInOutQuint" }, - center__paneSelector: ".right-pane", - west__paneSelector: ".left-pane", - west__size: this.model.preference('feed_pane_size'), - west__minSize: this.constants.MIN_FEED_LIST_SIZE, - west__onresize_end: _.bind(this.save_feed_pane_size, this), - west__onopen: _.bind(this.resize_window, this), + this.layout.outerLayout = this.$s.$layout.layout({ + zIndex: 2, + fxName: "slideOffscreen", + fxSettings: { duration: 560, easing: "easeInOutQuint" }, + center__paneSelector: ".right-pane", + west__paneSelector: ".left-pane", + west__size: this.model.preference('feed_pane_size'), + west__minSize: this.constants.MIN_FEED_LIST_SIZE, + west__onresize_end: _.bind(this.save_feed_pane_size, this), + west__onopen: _.bind(this.resize_window, this), // west__initHidden: this.options.hide_sidebar, - west__spacing_open: this.options.hide_sidebar ? 1 : 1, - resizerDragOpacity: 0.6, - resizeWhileDragging: true, - enableCursorHotkey: false, - togglerLength_open: 0 - }); + west__spacing_open: this.options.hide_sidebar ? 1 : 1, + resizerDragOpacity: 0.6, + resizeWhileDragging: true, + enableCursorHotkey: false, + togglerLength_open: 0 + }); this.layout.leftLayout = $('.left-pane').layout({ - closable: false, - resizeWhileDragging: true, - fxName: "slideOffscreen", - fxSettings: { duration: 560, easing: "easeInOutQuint" }, - animatePaneSizing: true, - north__paneSelector: ".left-north", - north__size: 37, - north__resizeable: false, - north__spacing_open: 0, - center__paneSelector: ".left-center", - center__resizable: false, - south__paneSelector: ".left-south", - south__size: 37, - south__resizable: false, - enableCursorHotkey: false, - togglerLength_open: 0, - south__spacing_open: 0 + closable: false, + resizeWhileDragging: true, + fxName: "slideOffscreen", + fxSettings: { duration: 560, easing: "easeInOutQuint" }, + animatePaneSizing: true, + north__paneSelector: ".left-north", + north__size: 37, + north__resizeable: false, + north__spacing_open: 0, + center__paneSelector: ".left-center", + center__resizable: false, + south__paneSelector: ".left-south", + south__size: 37, + south__resizable: false, + enableCursorHotkey: false, + togglerLength_open: 0, + south__spacing_open: 0 }); - + this.layout.leftCenterLayout = $('.left-center').layout({ - closable: false, - slidable: false, - resizeWhileDragging: true, - center__paneSelector: ".left-center-content", - center__resizable: false, - south__paneSelector: ".left-center-footer", - south__size: 'auto', - south__resizable: false, - south__slidable: true, - south__spacing_open: 0, - south__spacing_closed: 0, - south__closable: true, - south__initClosed: true, - fxName: "slideOffscreen", - fxSettings_close: { duration: 560, easing: "easeInOutQuint" }, - fxSettings_open: { duration: 0 }, - enableCursorHotkey: false, - togglerLength_open: 0 + closable: false, + slidable: false, + resizeWhileDragging: true, + center__paneSelector: ".left-center-content", + center__resizable: false, + south__paneSelector: ".left-center-footer", + south__size: 'auto', + south__resizable: false, + south__slidable: true, + south__spacing_open: 0, + south__spacing_closed: 0, + south__closable: true, + south__initClosed: true, + fxName: "slideOffscreen", + fxSettings_close: { duration: 560, easing: "easeInOutQuint" }, + fxSettings_open: { duration: 0 }, + enableCursorHotkey: false, + togglerLength_open: 0 }); } - if (_.contains(['split', 'full'], + if (_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { - var rightLayoutOptions = { - resizeWhileDragging: true, - center__paneSelector: ".content-pane", - spacing_open: 0, - resizerDragOpacity: 0.6, - enableCursorHotkey: false, - togglerLength_open: 0, - fxName: "slideOffscreen", - fxSettings_close: { duration: 560, easing: "easeInOutQuint" }, - fxSettings_open: { duration: 0, easing: "easeInOutQuint" }, - north__paneSelector: ".content-north", - north__size: 37, - south__paneSelector: ".content-south", - south__size: 37 + var rightLayoutOptions = { + resizeWhileDragging: true, + center__paneSelector: ".content-pane", + spacing_open: 0, + resizerDragOpacity: 0.6, + enableCursorHotkey: false, + togglerLength_open: 0, + fxName: "slideOffscreen", + fxSettings_close: { duration: 560, easing: "easeInOutQuint" }, + fxSettings_open: { duration: 0, easing: "easeInOutQuint" }, + north__paneSelector: ".content-north", + north__size: 37, + south__paneSelector: ".content-south", + south__size: 37 }; - this.layout.rightLayout = $('.right-pane').layout(rightLayoutOptions); - - var contentLayoutOptions = { - fxName: "slideOffscreen", - fxSettings_close: { duration: 560, easing: "easeInOutQuint" }, - fxSettings_open: { duration: 0, easing: "easeInOutQuint" }, - resizeWhileDragging: true, - center__paneSelector: ".content-center", - spacing_open: story_anchor == 'west' ? 1 : 1, - resizerDragOpacity: 0.6, - enableCursorHotkey: false, - togglerLength_open: 0 + this.layout.rightLayout = $('.right-pane').layout(rightLayoutOptions); + + var contentLayoutOptions = { + fxName: "slideOffscreen", + fxSettings_close: { duration: 560, easing: "easeInOutQuint" }, + fxSettings_open: { duration: 0, easing: "easeInOutQuint" }, + resizeWhileDragging: true, + center__paneSelector: ".content-center", + spacing_open: story_anchor == 'west' ? 1 : 1, + resizerDragOpacity: 0.6, + enableCursorHotkey: false, + togglerLength_open: 0 }; if (NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full') { - contentLayoutOptions[story_anchor+'__initHidden'] = true; - this.flags['story_titles_closed'] = true; + contentLayoutOptions[story_anchor + '__initHidden'] = true; + this.flags['story_titles_closed'] = true; } else { this.flags['story_titles_closed'] = false; } - contentLayoutOptions[story_anchor+'__paneSelector'] = '.right-north'; - contentLayoutOptions[story_anchor+'__minSize'] = this.constants.MIN_STORY_LIST_SIZE; - contentLayoutOptions[story_anchor+'__size'] = this.model.preference('story_titles_pane_size'); - contentLayoutOptions[story_anchor+'__onresize_end'] = $.rescope(this.save_story_titles_pane_size, this); - contentLayoutOptions[story_anchor+'__onclose_start'] = $.rescope(this.toggle_story_titles_pane, this); - contentLayoutOptions[story_anchor+'__onopen_start'] = $.rescope(this.toggle_story_titles_pane, this); - this.layout.contentLayout = this.$s.$content_pane.layout(contentLayoutOptions); + contentLayoutOptions[story_anchor + '__paneSelector'] = '.right-north'; + contentLayoutOptions[story_anchor + '__minSize'] = this.constants.MIN_STORY_LIST_SIZE; + contentLayoutOptions[story_anchor + '__size'] = this.model.preference('story_titles_pane_size'); + contentLayoutOptions[story_anchor + '__onresize_end'] = $.rescope(this.save_story_titles_pane_size, this); + contentLayoutOptions[story_anchor + '__onclose_start'] = $.rescope(this.toggle_story_titles_pane, this); + contentLayoutOptions[story_anchor + '__onopen_start'] = $.rescope(this.toggle_story_titles_pane, this); + this.layout.contentLayout = this.$s.$content_pane.layout(contentLayoutOptions); } else if (_.contains(['list', 'grid', 'magazine'], - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { - var rightLayoutOptions = { - resizeWhileDragging: true, - center__paneSelector: ".content-pane", - spacing_open: 0, - resizerDragOpacity: 0.6, - enableCursorHotkey: false, - togglerLength_open: 0, - fxName: "slideOffscreen", - fxSettings: { duration: 560, easing: "easeInOutQuint" }, - north__paneSelector: ".content-north", - north__size: 37, - south__paneSelector: ".content-south", - south__size: 37 - + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { + var rightLayoutOptions = { + resizeWhileDragging: true, + center__paneSelector: ".content-pane", + spacing_open: 0, + resizerDragOpacity: 0.6, + enableCursorHotkey: false, + togglerLength_open: 0, + fxName: "slideOffscreen", + fxSettings: { duration: 560, easing: "easeInOutQuint" }, + north__paneSelector: ".content-north", + north__size: 37, + south__paneSelector: ".content-south", + south__size: 37 + }; - this.layout.rightLayout = $('.right-pane').layout(rightLayoutOptions); - - var contentLayoutOptions = { - resizeWhileDragging: true, - center__paneSelector: ".right-north", - spacing_open: 0, - resizerDragOpacity: 0.6, - enableCursorHotkey: false, - togglerLength_open: 0 + this.layout.rightLayout = $('.right-pane').layout(rightLayoutOptions); + + var contentLayoutOptions = { + resizeWhileDragging: true, + center__paneSelector: ".right-north", + spacing_open: 0, + resizerDragOpacity: 0.6, + enableCursorHotkey: false, + togglerLength_open: 0 }; - this.layout.contentLayout = this.$s.$content_pane.layout(contentLayoutOptions); + this.layout.contentLayout = this.$s.$content_pane.layout(contentLayoutOptions); this.flags['story_titles_closed'] = false; } @@ -431,18 +431,18 @@ this.$s.$story_titles.append(story_titles_bin.children()); this.resize_window(); } - + this.adjust_for_narrow_window(); this.add_drag_handles(); }, - - apply_tipsy_titles: function() { + + apply_tipsy_titles: function () { $('.NB-task-add').tipsy('disable'); $('.NB-task-manage').tipsy('disable'); - $('.NB-taskbar-button.NB-tipsy').each(function() { + $('.NB-taskbar-button.NB-tipsy').each(function () { $(this).tipsy('disable'); }); - + if (this.model.preference('show_tooltips')) { $('.NB-task-add').tipsy({ gravity: 'sw', @@ -456,25 +456,25 @@ gravity: 's', delayIn: 175, title: 'tipsy-title' - }).each(function() { + }).each(function () { $(this).tipsy('enable'); }); } - + $('.NB-module-content-account-realtime').tipsy('disable').tipsy({ gravity: 'se', delayIn: 0 }).tipsy('enable'); }, - - save_feed_pane_size: function(pane, $pane, state, options, name) { + + save_feed_pane_size: function (pane, $pane, state, options, name) { if (!this.layout.outerLayout) return; var feed_pane_size = this.layout.outerLayout.state.west.size; - + $('#NB-splash').css('left', feed_pane_size); this.adjust_for_narrow_window(); - this.flags.set_feed_pane_size = this.flags.set_feed_pane_size || _.debounce( _.bind(function() { + this.flags.set_feed_pane_size = this.flags.set_feed_pane_size || _.debounce(_.bind(function () { var feed_pane_size = this.layout.outerLayout.state.west.size; // console.log('debounced save_feed_pane_size', this, feed_pane_size); this.model.preference('feed_pane_size', feed_pane_size); @@ -482,13 +482,13 @@ }, this), 1000); this.flags.set_feed_pane_size(); }, - - save_story_titles_pane_size: function(pane, $pane, state, options, name) { + + save_story_titles_pane_size: function (pane, $pane, state, options, name) { if (!this.layout.contentLayout) return; this.flags.scrolling_by_selecting_story_title = true; clearTimeout(this.locks.scrolling); - + var offset = 0; if (this.story_view == 'feed') { offset = this.$s.$feed_iframe.width(); @@ -496,37 +496,37 @@ offset = 2 * this.$s.$feed_iframe.width(); } this.$s.$story_pane.css('left', -1 * offset); - - this.flags.set_story_titles_size = this.flags.set_story_titles_size || _.debounce( _.bind(function() { + + this.flags.set_story_titles_size = this.flags.set_story_titles_size || _.debounce(_.bind(function () { var story_titles_size = this.layout.contentLayout.state[this.model.preference('story_pane_anchor')].size; this.model.preference('story_titles_pane_size', story_titles_size); this.flags.set_story_titles_size = null; - this.locks.scrolling = _.delay(_.bind(function() { + this.locks.scrolling = _.delay(_.bind(function () { this.flags.scrolling_by_selecting_story_title = false; }, this), 100); }, this), 1000); this.flags.set_story_titles_size(); - - this.flags.resize_window = this.flags.resize_window || _.debounce( _.bind(function() { + + this.flags.resize_window = this.flags.resize_window || _.debounce(_.bind(function () { this.resize_window(); this.flags.resize_window = null; }, this), 10); this.flags.resize_window(); - + }, - + add_drag_handles: function () { var $resizer = NEWSBLUR.reader.layout.outerLayout.resizers.west; if ($resizer.find(".NB-task-drag").length) return; - + $resizer.append($.make('div', { className: "NB-task-drag" }, [ $.make('div', { className: "NB-task-image" }) ])).css('overflow', 'visible'); }, - add_body_classes: function() { - this.$s.$body.toggleClass('NB-is-premium', NEWSBLUR.Globals.is_premium); - this.$s.$body.toggleClass('NB-is-anonymous', NEWSBLUR.Globals.is_anonymous); + add_body_classes: function () { + this.$s.$body.toggleClass('NB-is-premium', NEWSBLUR.Globals.is_premium); + this.$s.$body.toggleClass('NB-is-anonymous', NEWSBLUR.Globals.is_anonymous); this.$s.$body.toggleClass('NB-is-authenticated', NEWSBLUR.Globals.is_authenticated); if (!!this.model.preference('full_width_story')) { this.model.preference('story_position', 'stretch'); @@ -545,20 +545,20 @@ break; } this.$s.$body.removeClass('NB-pref-story-position-stretch') - .removeClass('NB-pref-story-position-left') - .removeClass('NB-pref-story-position-center') - .removeClass('NB-pref-story-position-right') - .toggleClass('NB-pref-story-position-' + this.model.preference('story_position')); + .removeClass('NB-pref-story-position-left') + .removeClass('NB-pref-story-position-center') + .removeClass('NB-pref-story-position-right') + .toggleClass('NB-pref-story-position-' + this.model.preference('story_position')); this.$s.$body.removeClass('NB-dashboard-columns-single') - .removeClass('NB-dashboard-columns-double') - .removeClass('NB-dashboard-columns-triple') - .toggleClass('NB-dashboard-columns-' + columns); + .removeClass('NB-dashboard-columns-double') + .removeClass('NB-dashboard-columns-triple') + .toggleClass('NB-dashboard-columns-' + columns); }, - - load_delayed_stylesheets: function() { - _.delay(function() { + + load_delayed_stylesheets: function () { + _.delay(function () { var $stylesheets = $("head link"); - $stylesheets.each(function() { + $stylesheets.each(function () { var $ss = $(this); if (!$ss.attr('delay')) return; $("head").append($.make('link', { @@ -569,8 +569,8 @@ }); }, 500); }, - - hide_splash_page: function() { + + hide_splash_page: function () { var self = this; var resize = false; if (!$('.right-pane').is(':visible')) { @@ -584,27 +584,27 @@ this.$s.$layout.layout().resizeAll(); this.adjust_for_narrow_window(); } - + this.apply_tipsy_titles(); if (NEWSBLUR.Globals.is_anonymous) { this.setup_ftux_signup_callout(); } }, - - show_splash_page: function(skip_router) { + + show_splash_page: function (skip_router) { this.reset_feed(); this.open_sidebar(); this.$s.$body.removeClass('NB-show-reader'); this.$s.$header_dashboard.addClass('NB-selected'); this.flags['splash_page_frontmost'] = true; - + if (!skip_router) { NEWSBLUR.log(["Navigating to splash"]); NEWSBLUR.router.navigate(''); } }, - - animate_progress_bar: function($bar, seconds, percentage) { + + animate_progress_bar: function ($bar, seconds, percentage) { var self = this; percentage = percentage || 0; seconds = parseFloat(Math.max(1, parseInt(seconds, 10)), 10); @@ -630,45 +630,45 @@ } else { time = seconds / 500; } - + if (percentage <= 100) { - this.locks['animate_progress_bar'] = setTimeout(function() { + this.locks['animate_progress_bar'] = setTimeout(function () { percentage += 1; - $bar.progressbar({value: percentage}); + $bar.progressbar({ value: percentage }); self.animate_progress_bar($bar, seconds, percentage); }, time * 1000); } }, - - blur_to_page: function(options) { + + blur_to_page: function (options) { options = options || {}; - + if (options.manage_menu) { $('.NB-menu-manage-container .NB-menu-manage :focus').blur(); } else { $(':focus').blur(); } }, - + // ============== // = Navigation = // ============== - - active_story_view: function(feed_id) { + + active_story_view: function (feed_id) { feed_id = feed_id || this.active_feed; - + if (_.string.startsWith(feed_id, 'river:')) { if (this.active_story && this.active_story.get('story_feed_id')) { feed_id = this.active_story.get('story_feed_id'); - console.log(['Hijacking view setting', feed_id, ' --> ', - this.active_story && this.active_story.get('story_feed_id'), NEWSBLUR.assets.view_setting(feed_id, 'view')]); + console.log(['Hijacking view setting', feed_id, ' --> ', + this.active_story && this.active_story.get('story_feed_id'), NEWSBLUR.assets.view_setting(feed_id, 'view')]); } } - + return feed_id; }, - - show_next_story: function(direction) { + + show_next_story: function (direction) { var story = NEWSBLUR.assets.stories.get_next_story(direction, { score: this.get_unread_view_score() }); @@ -676,20 +676,20 @@ if (story) { story.set('selected', true); } - + if (NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full' && !NEWSBLUR.assets.stories.no_more_stories) { var visible = NEWSBLUR.assets.stories.visible(); var visible_count = visible.length; var visible_index = visible.indexOf(this.active_story); - + if (visible_index >= visible_count - 3) { this.load_page_of_feed_stories(); } } }, - - show_next_unread_story: function() { + + show_next_unread_story: function () { var unread_count = this.get_total_unread_count(); if (this.flags['feed_list_showing_starred']) { @@ -702,19 +702,19 @@ this.counts['find_next_unread_on_page_of_feed_stories_load'] = 0; next_story.set('selected', true); } else if (this.counts['find_next_unread_on_page_of_feed_stories_load'] < - this.constants.FIND_NEXT_UNREAD_STORY_TRIES && - !NEWSBLUR.assets.stories.no_more_stories) { + this.constants.FIND_NEXT_UNREAD_STORY_TRIES && + !NEWSBLUR.assets.stories.no_more_stories) { // Nothing up, nothing down, but still unread. Load 1 page then find it. this.counts['find_next_unread_on_page_of_feed_stories_load'] += 1; this.load_page_of_feed_stories(); } else if (this.counts['find_next_unread_on_page_of_feed_stories_load'] >= - this.constants.FIND_NEXT_UNREAD_STORY_TRIES) { + this.constants.FIND_NEXT_UNREAD_STORY_TRIES) { this.open_next_unread_story_across_feeds(true); } } }, - - open_next_unread_story_across_feeds: function(force_next_feed) { + + open_next_unread_story_across_feeds: function (force_next_feed) { var unread_count = !force_next_feed && this.active_feed && this.get_total_unread_count(); if (!unread_count && !this.flags['feed_list_showing_starred']) { @@ -730,52 +730,52 @@ if (!$next_feed || !$next_feed.length) return; var next_feed_id = $next_feed.data('id'); if (next_feed_id == this.active_feed) return; - + if (NEWSBLUR.utils.is_feed_social(next_feed_id)) { - this.open_social_stories(next_feed_id, {force: true, $feed: $next_feed}); + this.open_social_stories(next_feed_id, { force: true, $feed: $next_feed }); } else { next_feed_id = parseInt(next_feed_id, 10); - this.open_feed(next_feed_id, {force: true, $feed: $next_feed}); + this.open_feed(next_feed_id, { force: true, $feed: $next_feed }); } } } this.show_next_unread_story(); }, - - show_last_unread_story: function() { + + show_last_unread_story: function () { var unread_count = this.get_total_unread_count(); - + if (unread_count) { var last_story = NEWSBLUR.assets.stories.get_last_unread_story(unread_count); - + if (last_story) { this.counts['find_last_unread_on_page_of_feed_stories_load'] = 0; last_story.set('selected', true); - } else if (this.counts['find_last_unread_on_page_of_feed_stories_load'] < this.constants.FILL_OUT_PAGES && - !NEWSBLUR.assets.stories.no_more_stories) { + } else if (this.counts['find_last_unread_on_page_of_feed_stories_load'] < this.constants.FILL_OUT_PAGES && + !NEWSBLUR.assets.stories.no_more_stories) { // Nothing up, nothing down, but still unread. Load 1 page then find it. this.counts['find_last_unread_on_page_of_feed_stories_load'] += 1; this.load_page_of_feed_stories(); } } }, - - select_story_in_feed: function(options) { + + select_story_in_feed: function (options) { options = _.extend({}, options); var story_id = this.flags['select_story_in_feed']; var story = NEWSBLUR.assets.stories.get(story_id); if (!story) story = NEWSBLUR.assets.stories.get_by_story_hash(story_id); // NEWSBLUR.log(['select_story_in_feed', story_id, story, this.story_view, this.counts['select_story_in_feed'], this.flags['no_more_stories']]); - + if (story) { this.counts['select_story_in_feed'] = 0; this.flags['select_story_in_feed'] = null; - _.delay(_.bind(function() { - story.set('selected', true, {scroll_to_comments: options.scroll_to_comments}); + _.delay(_.bind(function () { + story.set('selected', true, { scroll_to_comments: options.scroll_to_comments }); }, this), 100); - } else if (this.counts['select_story_in_feed'] < this.constants.FILL_OUT_PAGES && - !NEWSBLUR.assets.stories.no_more_stories) { + } else if (this.counts['select_story_in_feed'] < this.constants.FILL_OUT_PAGES && + !NEWSBLUR.assets.stories.no_more_stories) { // Nothing up, nothing down, but still not found. Load 1 page then find it. this.counts['select_story_in_feed'] += 1; this.load_page_of_feed_stories(); @@ -784,48 +784,48 @@ this.flags['select_story_in_feed'] = null; } }, - - show_previous_story: function() { + + show_previous_story: function () { NEWSBLUR.assets.stories.select_previous_story(); }, - - show_next_feed: function(direction, $current_feed) { + + show_next_feed: function (direction, $current_feed) { var $feed_list = this.$s.$feed_list.add(this.$s.$social_feeds); - + if (this.flags.river_view && !this.flags.social_view) { return this.show_next_folder(direction, $current_feed); } - + var $next_feed = this.get_next_feed(direction, $current_feed, { include_selected: true, feed_id: this.active_feed }); - + if (!$next_feed || $current_feed == $next_feed) return; if ($current_feed && $current_feed.data('id') == $next_feed.data('id')) return; - + var next_feed_id = $next_feed.data('id'); if (next_feed_id && next_feed_id == this.active_feed) { this.show_next_feed(direction, $next_feed); } else if (NEWSBLUR.utils.is_feed_social(next_feed_id)) { - this.open_social_stories(next_feed_id, {force: true, $feed: $next_feed}); + this.open_social_stories(next_feed_id, { force: true, $feed: $next_feed }); } else { next_feed_id = parseInt(next_feed_id, 10); - this.open_feed(next_feed_id, {force: true, $feed: $next_feed}); + this.open_feed(next_feed_id, { force: true, $feed: $next_feed }); } }, - - show_next_folder: function(direction, $current_folder) { + + show_next_folder: function (direction, $current_folder) { var $next_folder = this.get_next_folder(direction, $current_folder); - + if (!$next_folder) return; - + var folder = NEWSBLUR.assets.folders.get_view($next_folder); this.open_river_stories($next_folder, folder && folder.model); }, - - get_next_feed: function(direction, $current_feed, options) { + + get_next_feed: function (direction, $current_feed, options) { options = options || {}; var self = this; var $feed_list = this.$s.$feed_list.add(this.$s.$social_feeds); @@ -848,78 +848,78 @@ if (options.include_selected) { $feeds = $feeds.add('.NB-feedlists .feed.NB-selected'); } - $current_feed = $('.feed:visible:not(.NB-empty)', $feed_list)[direction==-1?'last':'first'](); + $current_feed = $('.feed:visible:not(.NB-empty)', $feed_list)[direction == -1 ? 'last' : 'first'](); $next_feed = $current_feed; } else { var current_feed = 0; - $feeds.each(function(i) { + $feeds.each(function (i) { if (this == $current_feed[0]) { current_feed = i; return false; } }); - $next_feed = $feeds.eq((current_feed+direction) % ($feeds.length)); + $next_feed = $feeds.eq((current_feed + direction) % ($feeds.length)); } - + return $next_feed; }, - - get_next_folder: function(direction, $current_folder) { + + get_next_folder: function (direction, $current_folder) { var self = this; var $feed_list = this.$s.$feed_list.add(this.$s.$social_feeds); var $current_folder = $('.folder.NB-selected', $feed_list); var $folders = $('li.folder:visible:not(.NB-empty)', $feed_list); var current_folder = 0; - $folders.each(function(i) { + $folders.each(function (i) { if (this == $current_folder[0]) { current_folder = i; return false; } }); - - var next_folder_index = (current_folder+direction) % ($folders.length); + + var next_folder_index = (current_folder + direction) % ($folders.length); var $next_folder = $folders.eq(next_folder_index); - + return $next_folder; }, - get_next_unread_feed: function(direction, $current_feed) { + get_next_unread_feed: function (direction, $current_feed) { var self = this; var $feed_list = this.$s.$feed_list.add(this.$s.$social_feeds); $current_feed = $current_feed || $('.selected', $feed_list); var unread_view = this.get_unread_view_name(); var $next_feed; var current_feed; - - var $feeds = $('.feed:visible:not(.NB-empty)', $feed_list).filter(function() { - var $this = $(this); - if ($this.hasClass('selected')) { - return true; - } else if (unread_view == 'positive') { - return $this.is('.unread_positive'); - } else if (unread_view == 'neutral') { - return $this.is('.unread_positive,.unread_neutral'); - } else if (unread_view == 'negative') { - return $this.is('.unread_positive,.unread_neutral,.unread_negative'); - } + + var $feeds = $('.feed:visible:not(.NB-empty)', $feed_list).filter(function () { + var $this = $(this); + if ($this.hasClass('selected')) { + return true; + } else if (unread_view == 'positive') { + return $this.is('.unread_positive'); + } else if (unread_view == 'neutral') { + return $this.is('.unread_positive,.unread_neutral'); + } else if (unread_view == 'negative') { + return $this.is('.unread_positive,.unread_neutral,.unread_negative'); + } }).add('.NB-feedlists .feed.NB-selected'); if (!$current_feed.length) { - $next_feed = $feeds.first(); + $next_feed = $feeds.first(); } else { - $feeds.each(function(i) { - if (this == $current_feed.get(0)) { - current_feed = i; - return false; - } - }); - $next_feed = $feeds.eq((current_feed+direction) % ($feeds.length)); - } - + $feeds.each(function (i) { + if (this == $current_feed.get(0)) { + current_feed = i; + return false; + } + }); + $next_feed = $feeds.eq((current_feed + direction) % ($feeds.length)); + } + return $next_feed; }, - - get_next_unread_folder: function(direction) { + + get_next_unread_folder: function (direction) { var self = this; var $feed_list = this.$s.$feed_list.add(this.$s.$social_feeds); var $current_folder = $('.folder.NB-selected', $feed_list); @@ -927,17 +927,17 @@ var $next_folder; var current_folder = 0; var $folders = $('li.folder:visible:not(.NB-empty)', $feed_list); - - $folders = $folders.filter(function() { + + $folders = $folders.filter(function () { var $this = $(this); var folder_view = NEWSBLUR.assets.folders.get_view($current_folder); var folder_model = folder_view && folder_view.model; if (!folder_model) return false; - + var counts = folder_model.collection.unread_counts(); - + if (this == $current_folder[0]) return true; - + if (unread_view == 'positive') { return counts.ps; } else if (unread_view == 'neutral') { @@ -947,48 +947,48 @@ } }); - $folders.each(function(i) { + $folders.each(function (i) { if (this == $current_folder[0]) { current_folder = i; return false; } }); - $next_folder = $folders.eq((current_folder+direction) % ($folders.length)); + $next_folder = $folders.eq((current_folder + direction) % ($folders.length)); return $next_folder; }, - - page_in_story: function(amount, direction) { + + page_in_story: function (amount, direction) { amount = parseInt(amount, 10) / 100.0; var page_height = this.$s.$story_pane.height(); if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { page_height = this.$s.$story_titles.height(); } var scroll_height = parseInt(page_height * amount, 10); - var feed_view = NEWSBLUR.assets.preference('feed_view_single_story') && - ((this.story_view == 'feed' && !this.flags['temporary_story_view']) || - this.flags['page_view_showing_feed_view']); + var feed_view = NEWSBLUR.assets.preference('feed_view_single_story') && + ((this.story_view == 'feed' && !this.flags['temporary_story_view']) || + this.flags['page_view_showing_feed_view']); var text_view = this.story_view == 'text' || this.flags['temporary_story_view']; - + this.scroll_in_story(scroll_height, direction); - + if (NEWSBLUR.assets.preference('space_bar_action') == 'scroll_only') return; if (NEWSBLUR.assets.preference('space_bar_action') == 'next_unread_50') { page_height = page_height / 2 + 36; } - + if (!this.active_story || !this.active_story.get('selected')) { - this.open_next_unread_story_across_feeds(); + this.open_next_unread_story_across_feeds(); } else if (_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { if (direction > 0) { if (feed_view) { var scroll_top = this.$s.$feed_scroll.scrollTop(); - var story_height = this.$s.$feed_scroll.find('.NB-feed-story.NB-selected').height()-12; + var story_height = this.$s.$feed_scroll.find('.NB-feed-story.NB-selected').height() - 12; if (page_height + scroll_top >= story_height) { this.open_next_unread_story_across_feeds(); } } else if (text_view) { var scroll_top = this.$s.$text_view.scrollTop(); - var story_height = this.$s.$text_view.find('.NB-feed-story').height()-24; + var story_height = this.$s.$text_view.find('.NB-feed-story').height() - 24; if (page_height + scroll_top >= story_height) { this.open_next_unread_story_across_feeds(); } @@ -1006,18 +1006,18 @@ var $story = this.active_story.story_title_view.$el; var story_height = $story.height(); var story_offset = $story.position().top; - console.log(['space list', $story[0], scroll_top, story_height, story_offset, story_height+story_offset-scroll_height, page_height]); - if (direction > 0 && - (story_height+story_offset-scroll_height+52 < 0 || - story_height+story_offset+52 < page_height)) { - this.open_next_unread_story_across_feeds(); - } else if (direction < 0 && story_offset >= 0) { - this.show_previous_story(); - } - } - }, - - scroll_in_story: function(amount, direction) { + console.log(['space list', $story[0], scroll_top, story_height, story_offset, story_height + story_offset - scroll_height, page_height]); + if (direction > 0 && + (story_height + story_offset - scroll_height + 52 < 0 || + story_height + story_offset + 52 < page_height)) { + this.open_next_unread_story_across_feeds(); + } else if (direction < 0 && story_offset >= 0) { + this.show_previous_story(); + } + } + }, + + scroll_in_story: function (amount, direction) { var dir = '+'; if (direction == -1) { dir = '-'; @@ -1025,60 +1025,60 @@ // NEWSBLUR.log(['scroll_in_story', this.$s.$story_pane, direction, amount]); if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { this.$s.$story_titles.stop().scrollTo({ - top: dir+'='+amount, - left:'+=0' - }, 130, {queue: false}); - } else if (this.story_view == 'page' && + top: dir + '=' + amount, + left: '+=0' + }, 130, { queue: false }); + } else if (this.story_view == 'page' && !this.flags['page_view_showing_feed_view'] && !this.flags['temporary_story_view']) { this.$s.$feed_iframe.stop().scrollTo({ - top: dir+'='+amount, - left:'+=0' - }, 130, {queue: false}); + top: dir + '=' + amount, + left: '+=0' + }, 130, { queue: false }); } else if ((this.story_view == 'feed' && - !this.flags['temporary_story_view']) || - this.flags['page_view_showing_feed_view']) { + !this.flags['temporary_story_view']) || + this.flags['page_view_showing_feed_view']) { this.$s.$feed_scroll.stop().scrollTo({ - top: dir+'='+amount, - left:'+=0' - }, 130, {queue: false}); + top: dir + '=' + amount, + left: '+=0' + }, 130, { queue: false }); } else if (this.story_view == 'text' || - this.flags['temporary_story_view']) { + this.flags['temporary_story_view']) { this.$s.$text_view.stop().scrollTo({ - top: dir+'='+amount, - left:'+=0' - }, 130, {queue: false}); + top: dir + '=' + amount, + left: '+=0' + }, 130, { queue: false }); } - + this.show_mouse_indicator(); // _.delay(_.bind(this.hide_mouse_indicator, this), 350); }, - - find_story_with_action_preference_on_open_feed: function() { + + find_story_with_action_preference_on_open_feed: function () { var open_feed_action = this.model.preference('open_feed_action'); if (!this.active_story && open_feed_action == 'newest' && !this.flags['feed_list_showing_starred']) { this.show_next_unread_story(); - } else if (!this.active_story && - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full' && - NEWSBLUR.assets.preference('feed_view_single_story')) { - this.show_next_unread_story(); + } else if (!this.active_story && + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full' && + NEWSBLUR.assets.preference('feed_view_single_story')) { + this.show_next_unread_story(); } }, - + // ============= // = Feed Pane = // ============= - - sort_feeds: function($feeds) { - $('.feed', $feeds).tsort('', {sortFunction: NEWSBLUR.Collections.Folders.comparator}); + + sort_feeds: function ($feeds) { + $('.feed', $feeds).tsort('', { sortFunction: NEWSBLUR.Collections.Folders.comparator }); $('.folder', $feeds).tsort('.folder_title_text'); }, - - load_sortable_feeds: function() { + + load_sortable_feeds: function () { var self = this; - + this.$s.$feed_list.sortable({ items: '.feed,li.folder', connectWith: 'ul.folder,.feed.NB-empty', @@ -1089,7 +1089,7 @@ containment: '#feed_list', tolerance: 'pointer', scrollSensitivity: 35, - start: function(e, ui) { + start: function (e, ui) { self.flags['sorting_feed'] = true; ui.placeholder.attr('class', ui.item.attr('class') + ' NB-feeds-list-highlight'); NEWSBLUR.app.feed_list.start_sorting(); @@ -1106,21 +1106,21 @@ ui.placeholder.html(ui.item.children().clone()); } }, - change: function(e, ui) { + change: function (e, ui) { var $feeds = ui.placeholder.closest('ul.folder'); self.sort_feeds($feeds); }, - stop: function(e, ui) { - setTimeout(function() { + stop: function (e, ui) { + setTimeout(function () { self.flags['sorting_feed'] = false; }, 100); ui.item.removeClass('NB-feed-sorting'); NEWSBLUR.app.feed_list.end_sorting(); self.sort_feeds(e.target); self.save_feed_order(); - ui.item.css({'backgroundColor': '#D7DDE6'}) - .animate({'backgroundColor': '#F0F076'}, {'duration': 800}) - .animate({'backgroundColor': '#D7DDE6'}, {'duration': 1000}); + ui.item.css({ 'backgroundColor': '#D7DDE6' }) + .animate({ 'backgroundColor': '#F0F076' }, { 'duration': 800 }) + .animate({ 'backgroundColor': '#D7DDE6' }, { 'duration': 1000 }); if (ui.item.is('.folder') && !ui.item.data('previously_collapsed')) { self.collapse_folder(ui.item.children('.folder_title')); self.collapse_folder(ui.placeholder.children('.folder_title')); @@ -1128,13 +1128,13 @@ } }); }, - - save_feed_order: function() { - var combine_folders = function($folder) { + + save_feed_order: function () { + var combine_folders = function ($folder) { var folders = []; var $items = $folder.children('li.folder, .feed'); - - for (var i=0, i_count=$items.length; i < i_count; i++) { + + for (var i = 0, i_count = $items.length; i < i_count; i++) { var $item = $items.eq(i); if ($item.hasClass('feed')) { @@ -1149,44 +1149,44 @@ folders.push(child_folders); } } - + return folders; }; - + var combined_folders = combine_folders(this.$s.$feed_list); // NEWSBLUR.log(['Save new folder/feed order', {'combined': combined_folders}]); this.model.save_feed_order(combined_folders); }, - - show_feed_chooser_button: function() { + + show_feed_chooser_button: function () { var self = this; var $progress = this.$s.$feeds_progress; var $bar = $('.NB-progress-bar', $progress); var percentage = 0; - + $('.NB-progress-title', $progress).text('Get Started'); $('.NB-progress-counts', $progress).hide(); $('.NB-progress-percentage', $progress).hide(); $progress.addClass('NB-progress-error').addClass('NB-progress-big'); - $('.NB-progress-link', $progress).html($.make('div', { + $('.NB-progress-link', $progress).html($.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-menu-manage-feedchooser' }, ['Choose your 64 sites'])); - + this.show_progress_bar(); }, - - hide_feed_chooser_button: function() { + + hide_feed_chooser_button: function () { var $progress = this.$s.$feeds_progress; var $bar = $('.NB-progress-bar', $progress); $progress.removeClass('NB-progress-error').removeClass('NB-progress-big'); - + this.hide_progress_bar(); }, - - open_dialog_after_feeds_loaded: function(options) { + + open_dialog_after_feeds_loaded: function (options) { options = options || {}; if (!NEWSBLUR.Globals.is_authenticated) return; - + if (!NEWSBLUR.assets.folders.length || !NEWSBLUR.assets.preference('has_setup_feeds')) { if (options.delayed_import || this.flags.delayed_import) { @@ -1197,9 +1197,9 @@ _.defer(_.bind(this.open_intro_modal, this), 100); } } else if (!NEWSBLUR.assets.flags['has_chosen_feeds'] && - NEWSBLUR.assets.folders.length) { + NEWSBLUR.assets.folders.length) { if (NEWSBLUR.Globals.is_premium) { - this.model.save_feed_chooser(null, function() { + this.model.save_feed_chooser(null, function () { NEWSBLUR.reader.hide_feed_chooser_button(); NEWSBLUR.assets.load_feeds(); }); @@ -1207,21 +1207,21 @@ _.defer(_.bind(this.open_feedchooser_modal, this), 100); } } else if (!NEWSBLUR.Globals.is_premium && - NEWSBLUR.assets.feeds.active().length > 64) { + NEWSBLUR.assets.feeds.active().length > 64) { _.defer(_.bind(this.open_feedchooser_modal, this), 100); } }, - + // ================ // = Progress Bar = // ================ - - check_feed_fetch_progress: function() { + + check_feed_fetch_progress: function () { $.extend(this.counts, { 'unfetched_feeds': 0, 'fetched_feeds': 0 }); - + var counts = this.model.count_unfetched_feeds(); this.counts['unfetched_feeds'] = counts['unfetched_feeds']; this.counts['fetched_feeds'] = counts['fetched_feeds']; @@ -1234,8 +1234,8 @@ this.show_unfetched_feed_progress(); } }, - - show_progress_bar: function() { + + show_progress_bar: function () { var $layout = this.$s.$feeds_progress.parents('.left-center').layout(); if (!this.flags['showing_progress_bar']) { this.flags['showing_progress_bar'] = true; @@ -1244,18 +1244,18 @@ $layout.sizePane('south'); }, - hide_progress_bar: function(permanent) { + hide_progress_bar: function (permanent) { var self = this; - + if (permanent) { this.model.preference('hide_fetch_progress', true); } - + this.flags['showing_progress_bar'] = false; this.$s.$feeds_progress.parents('.left-center').layout().close('south'); }, - - show_unfetched_feed_progress: function() { + + show_unfetched_feed_progress: function () { var self = this; var $progress = this.$s.$feeds_progress; var percentage = parseInt(this.counts['fetched_feeds'] / (this.counts['unfetched_feeds'] + this.counts['fetched_feeds']) * 100, 10); @@ -1268,32 +1268,32 @@ $('.NB-progress-bar', $progress).progressbar({ value: percentage }); - + if (!$progress.is(':visible') && !this.model.preference('hide_fetch_progress')) { - setTimeout(function() { + setTimeout(function () { self.show_progress_bar(); }, 1000); } - + this.setup_feed_refresh(true); }, - - hide_unfetched_feed_progress: function(permanent) { + + hide_unfetched_feed_progress: function (permanent) { if (permanent) { this.model.preference('hide_fetch_progress', true); } - + this.setup_feed_refresh(); this.hide_progress_bar(); }, - + // =============================== // = Feed bar - Individual Feeds = // =============================== - - reset_feed: function(options) { + + reset_feed: function (options) { options = options || {}; - + $.extend(this.flags, { 'scrolling_by_selecting_story_title': false, 'page_view_showing_feed_view': false, @@ -1311,7 +1311,7 @@ 'global_blurblogs': false, 'reloading_feeds': false }); - + $.extend(this.cache, { 'iframe': {}, 'iframe_stories': {}, @@ -1325,16 +1325,16 @@ 'feed_title_floater_story_id': null, '$feed_in_social_feed_list': {} }); - + $.extend(this.counts, { 'page': 1, 'find_next_unread_on_page_of_feed_stories_load': 0, 'find_last_unread_on_page_of_feed_stories_load': 0, 'select_story_in_feed': 0 }); - + // console.log(['reset feed', options, options.search]); - + if (_.isUndefined(options.search)) { this.flags.search = ""; this.flags.searching = false; @@ -1352,7 +1352,7 @@ $('.task_view_story', this.$s.$taskbar).removeClass('NB-disabled'); $('.task_view_page', this.$s.$taskbar).removeClass('NB-task-return'); clearTimeout(this.flags['next_fetch']); - + if (this.flags['showing_feed_in_tryfeed_view'] || this.flags['showing_social_feed_in_tryfeed_view']) { this.hide_tryfeed_view(); @@ -1364,7 +1364,7 @@ } this.hide_tryout_signup_button(); } - + this.model.feeds.deselect(); this.model.stories.deselect(); this.model.starred_feeds.deselect(); @@ -1379,17 +1379,17 @@ this.active_folder = null; this.active_feed = null; this.active_story = null; - + NEWSBLUR.assets.stories.reset(); NEWSBLUR.app.feed_selector.hide_feed_selector(); NEWSBLUR.app.original_tab_view.unload_feed_iframe(); NEWSBLUR.app.story_tab_view.unload_story_iframe(); NEWSBLUR.app.text_tab_view.unload(); }, - - reload_feed: function(options) { + + reload_feed: function (options) { options = options || {}; - + if (this.flags['starred_view'] && this.flags['starred_tag']) { options['tag'] = this.flags['starred_tag']; this.open_starred_stories(options); @@ -1397,41 +1397,41 @@ this.open_starred_stories(options); } else if (this.active_feed == 'read') { this.open_read_stories(options); - } else if (this.flags['social_view'] && - this.active_feed == 'river:blurblogs') { + } else if (this.flags['social_view'] && + this.active_feed == 'river:blurblogs') { this.open_river_blurblogs_stories(); - } else if (this.flags['social_view'] && - this.active_feed == 'river:global') { - this.open_river_blurblogs_stories({'global': true}); + } else if (this.flags['social_view'] && + this.active_feed == 'river:global') { + this.open_river_blurblogs_stories({ 'global': true }); } else if (this.flags['social_view']) { this.open_social_stories(this.active_feed, options); } else if (this.flags['river_view']) { if (this.active_feed == 'river:infrequent') { options.infrequent = NEWSBLUR.assets.preference('infrequent_stories_per_month'); } - this.open_river_stories(this.active_folder && - this.active_folder.folder_view && - this.active_folder.folder_view.$el, - this.active_folder, - options); + this.open_river_stories(this.active_folder && + this.active_folder.folder_view && + this.active_folder.folder_view.$el, + this.active_folder, + options); } else if (this.active_feed) { this.open_feed(this.active_feed, options); } - + if (options.search && !_.contains(['feed', 'text', 'story'], this.story_view)) { this.switch_taskbar_view('feed', { skip_save_type: true }); } }, - - open_feed: function(feed_id, options) { + + open_feed: function (feed_id, options) { options = options || {}; var self = this; var $story_titles = this.$s.$story_titles; var feed = this.model.get_feed(feed_id) || options.feed; var temp = feed && feed.get('temp') && !feed.get('subscribed'); - + if (!feed || (temp && !options.try_feed)) { // Setup tryfeed views first, then come back here. console.log(["Temp open feed", feed_id, feed, options, temp]); @@ -1440,23 +1440,23 @@ } this.flags['opening_feed'] = true; - + if (options.try_feed || feed) { this.reset_feed(options); this.hide_splash_page(); if (options.story_id) { this.flags['select_story_in_feed'] = options.story_id; } - + this.active_feed = feed.id; this.next_feed = feed.id; - + if (options.$feed) { - var selected_title_view = _.detect(feed.views, function(view) { + var selected_title_view = _.detect(feed.views, function (view) { return view.el == options.$feed.get(0); }); if (selected_title_view) { - feed.set("selected_title_view", selected_title_view, {silent: true}); + feed.set("selected_title_view", selected_title_view, { silent: true }); } } if (options.feed && options.feed.set) { @@ -1464,30 +1464,30 @@ } else { feed.set('selected', true, options); } - + NEWSBLUR.app.taskbar_info.hide_stories_error(); this.iframe_scroll = null; this.set_correct_story_view_for_feed(feed.id); this.make_feed_title_in_stories(); this.switch_taskbar_view(this.story_view); this.switch_story_layout(); - + if (NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full') { NEWSBLUR.app.story_list.show_loading(options); } else { NEWSBLUR.app.story_titles.show_loading(options); } - _.delay(_.bind(function() { + _.delay(_.bind(function () { if (!options.delay || feed.id == self.next_feed) { - this.model.load_feed(feed.id, 1, true, $.rescope(this.post_open_feed, this), - NEWSBLUR.app.taskbar_info.show_stories_error); + this.model.load_feed(feed.id, 1, true, $.rescope(this.post_open_feed, this), + NEWSBLUR.app.taskbar_info.show_stories_error); } }, this), options.delay || 0); - if (_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && + if (_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && (!this.story_view || this.story_view == 'page')) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { if (!options.delay || feed.id == this.next_feed) { NEWSBLUR.app.original_tab_view.load_feed_iframe(feed.id); } @@ -1499,7 +1499,7 @@ this.flags['iframe_prevented_from_loading'] = true; } this.setup_mousemove_on_views(); - + if (!options.silent) { var feed_title = feed.get('feed_title') || ''; var slug = _.string.words(_.string.clean(feed_title.replace(/[^a-z0-9\. ]/ig, ''))).join('-').toLowerCase(); @@ -1514,20 +1514,20 @@ } } }, - - post_open_feed: function(e, data, first_load) { + + post_open_feed: function (e, data, first_load) { if (!data) { NEWSBLUR.log(["No data from feed, trying again..."]); - return this.open_feed(this.active_feed, {force: true}); + return this.open_feed(this.active_feed, { force: true }); } var stories = data.stories; var tags = data.tags; var feed_id = data.feed_id; - + if (data.dupe_feed_id && this.active_feed == data.dupe_feed_id) { this.active_feed = data.feed_id; } - + this.flags['opening_feed'] = false; this.load_next_after_load(); NEWSBLUR.app.story_titles_header.show_feed_hidden_story_title_indicator(first_load); @@ -1543,7 +1543,7 @@ if (first_load) { this.find_story_with_action_preference_on_open_feed(); this.position_mouse_indicator(); - + if (_.contains(['story', 'text'], this.story_view) && !this.active_story && _.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && @@ -1551,7 +1551,7 @@ if (this.story_view == 'text') { NEWSBLUR.app.text_tab_view.show_explainer_single_story_mode(); } else if (this.story_view == 'story') { - NEWSBLUR.app.story_tab_view.show_explainer_single_story_mode(); + NEWSBLUR.app.story_tab_view.show_explainer_single_story_mode(); } } } @@ -1564,21 +1564,21 @@ this.correct_tryfeed_title(); } }, - - load_next_after_load: function() { + + load_next_after_load: function () { if (this.flags['loaded_next_after_load']) return; this.flags['loaded_next_after_load'] = true; - + var next = $.getQueryString('next') || $.getQueryString('test'); var add_url = $.getQueryString('add') || $.getQueryString('url'); var story = $.getQueryString('story'); if (next == 'notifications') { - _.defer(function() { + _.defer(function () { NEWSBLUR.reader.open_notifications_modal(NEWSBLUR.assets.active_feed && NEWSBLUR.assets.active_feed.id); }); } if (add_url) { - NEWSBLUR.reader.open_add_feed_modal({url: url}); + NEWSBLUR.reader.open_add_feed_modal({ url: url }); } if (story) { this.flags['select_story_in_feed'] = story; @@ -1589,8 +1589,8 @@ // window.history.replaceState({}, null, '/'); // } }, - - set_correct_story_view_for_feed: function(feed_id, view) { + + set_correct_story_view_for_feed: function (feed_id, view) { feed_id = feed_id || this.active_feed; feed_id = this.active_story_view(feed_id); var feed = NEWSBLUR.assets.get_feed(feed_id); @@ -1598,28 +1598,28 @@ var $page_tab = $('.task_view_page'); view = view || NEWSBLUR.assets.view_setting(feed_id); // console.log(['set_correct_story_view_for_feed', feed_id, view]); - + $original_tabs.removeClass('NB-disabled-page') - .removeClass('NB-disabled') - .removeClass('NB-hidden') - .removeClass('NB-exception-page'); - $original_tabs.each(function() { + .removeClass('NB-disabled') + .removeClass('NB-hidden') + .removeClass('NB-exception-page'); + $original_tabs.each(function () { $(this).tipsy('disable'); }); - if (feed && view == 'original' && + if (feed && view == 'original' && (feed.get('disabled_page') || - NEWSBLUR.utils.is_url_iframe_buster(feed.get('feed_link')))) { + NEWSBLUR.utils.is_url_iframe_buster(feed.get('feed_link')))) { view = 'feed'; $original_tabs.addClass('NB-disabled-page') - .addClass('NB-disabled') - .attr('title', 'The original page has been disabled by the publisher.') - .tipsy({ - gravity: 'n', - fade: true, - delayIn: 200 - }); - $original_tabs.each(function() { + .addClass('NB-disabled') + .attr('title', 'The original page has been disabled by the publisher.') + .tipsy({ + gravity: 'n', + fade: true, + delayIn: 200 + }); + $original_tabs.each(function () { $(this).tipsy('enable'); }); } else if (this.flags.river_view) { @@ -1632,8 +1632,8 @@ } $('.task_view_page').addClass('NB-exception-page'); } - - + + var $split = $(".NB-task-layout-split"); var $list = $(".NB-task-layout-list"); var $grid = $(".NB-task-layout-grid"); @@ -1642,7 +1642,7 @@ var story_layout = NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); this.$s.$story_titles[0].className = this.$s.$story_titles[0].className.replace(/ ?NB-layout-\w+/gi, ''); this.$s.$story_titles.addClass('NB-layout-' + story_layout); - + if (story_layout == 'list') { $('.NB-taskbar-button.task_view_page').addClass('NB-hidden'); $('.NB-taskbar-button.task_view_feed').addClass('NB-first'); @@ -1674,7 +1674,7 @@ $grid.removeClass('NB-active'); $magazine.addClass('NB-active'); } else if (story_layout == 'split') { - if (!this.flags.river_view) { + if (!this.flags.river_view) { $('.NB-taskbar-button.task_view_page').removeClass('NB-hidden'); $('.NB-taskbar-button.task_view_feed').removeClass('NB-first'); } @@ -1686,7 +1686,7 @@ $grid.removeClass('NB-active'); $magazine.removeClass('NB-active'); } else if (story_layout == 'full') { - if (!this.flags.river_view) { + if (!this.flags.river_view) { $('.NB-taskbar-button.task_view_page').removeClass('NB-hidden'); $('.NB-taskbar-button.task_view_feed').removeClass('NB-first'); } @@ -1702,34 +1702,34 @@ if (_.contains(['starred', 'read'], feed_id)) { $page_tab.addClass('NB-disabled'); } - + // console.log(['setting reader.story_view', view, " was:", this.story_view]); this.story_view = view; }, - + // ================ // = Story Layout = // ================ - - switch_story_layout: function(story_layout, force) { + + switch_story_layout: function (story_layout, force) { var feed_layout = NEWSBLUR.assets.view_setting(this.active_feed, 'layout'); var active_layout = this.story_layout; var original_layout = this.story_layout; story_layout = story_layout || feed_layout || active_layout; - + // console.log(['switch_story_layout', active_layout, feed_layout, story_layout, this.active_feed, story_layout == active_layout]); if (story_layout == active_layout && !force) return; - + this.story_layout = story_layout; - + if (!this.active_feed) return; - - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, {'layout': story_layout}); + + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, { 'layout': story_layout }); this.set_correct_story_view_for_feed(); - this.apply_resizable_layout({right_side: true}); - - NEWSBLUR.app.story_titles.render({immediate: !!force}); + this.apply_resizable_layout({ right_side: true }); + + NEWSBLUR.app.story_titles.render({ immediate: !!force }); if (story_layout == 'list') { if (this.active_story) { @@ -1763,16 +1763,16 @@ this.find_story_with_action_preference_on_open_feed(); } } - + this.switch_to_correct_view(); this.make_feed_title_in_stories(); this.add_body_classes(); - - _.defer(function() { + + _.defer(function () { NEWSBLUR.app.story_titles.scroll_to_selected_story(); NEWSBLUR.app.story_list.scroll_to_selected_story(); NEWSBLUR.app.feed_list.scroll_to_selected(); - if (_.contains(['split', 'list', 'grid', 'magazine'], + if (_.contains(['split', 'list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { NEWSBLUR.app.story_titles.fill_out(); } else { @@ -1780,12 +1780,12 @@ } }); }, - + // ================== // = Saved Searches = // ================== - - open_saved_search: function(options) { + + open_saved_search: function (options) { var search_model = options.search_model; var feed_id = search_model.get('feed_id'); var query = search_model.get('query'); @@ -1796,7 +1796,7 @@ options['router'] = true; search_model.set('selected', true); options.feed = options.search_model; - + if (feed_id == 'river:') { this.open_river_stories(options.$feed, feed_model, options); } else if (feed_id == 'river:infrequent') { @@ -1816,24 +1816,24 @@ } else if (_.string.startsWith(feed_id, 'social:')) { this.open_social_stories(feed_id, options); } - + window.history.replaceState({}, "", $.updateQueryString('search', query, window.location.pathname)); - + NEWSBLUR.reader.reload_feed(options); NEWSBLUR.app.story_titles_header.show_hidden_story_titles(); }, - + // =================== // = Starred Stories = // =================== - - update_starred_counts: function() { - NEWSBLUR.assets.update_starred_counts(_.bind(function(data) { + + update_starred_counts: function () { + NEWSBLUR.assets.update_starred_counts(_.bind(function (data) { this.update_starred_count(); }, this)); }, - update_starred_count: function() { + update_starred_count: function () { var starred_count = this.model.starred_count; var $starred_count = $('.NB-feeds-header-count', this.$s.$starred_header); var $starred_container = this.$s.$starred_header.closest('.NB-feeds-header-container'); @@ -1848,11 +1848,11 @@ $starred_container.slideDown(350); } }, - - open_starred_stories: function(options) { + + open_starred_stories: function (options) { options = options || {}; var $story_titles = this.$s.$story_titles; - + this.reset_feed(options); this.hide_splash_page(); if (options.story_id) { @@ -1862,11 +1862,11 @@ this.iframe_scroll = null; if (options.tag && !options.model) { if (options.tag == 'highlights') { - var model = NEWSBLUR.assets.starred_feeds.detect(function(feed) { + var model = NEWSBLUR.assets.starred_feeds.detect(function (feed) { return feed.get('is_highlights'); }); } else { - var model = NEWSBLUR.assets.starred_feeds.detect(function(feed) { + var model = NEWSBLUR.assets.starred_feeds.detect(function (feed) { return feed.tag_slug() == options.tag || feed.get('tag') == options.tag; }); } @@ -1901,17 +1901,17 @@ this.flags['starred_view'] = true; this.$s.$layout.addClass('NB-view-river'); this.flags.river_view = true; - + $('.task_view_page', this.$s.$taskbar).addClass('NB-disabled'); var explicit_view_setting = this.model.view_setting(this.active_feed, 'view'); if (!explicit_view_setting || explicit_view_setting == 'page') { - explicit_view_setting = 'feed'; + explicit_view_setting = 'feed'; } this.set_correct_story_view_for_feed(this.active_feed, explicit_view_setting); this.switch_taskbar_view(this.story_view); - this.switch_story_layout(); + this.switch_story_layout(); this.setup_mousemove_on_views(); - this.make_feed_title_in_stories(); + this.make_feed_title_in_stories(); NEWSBLUR.app.feed_list.scroll_to_show_selected_folder(); if (!options.silent) { @@ -1931,14 +1931,14 @@ NEWSBLUR.app.story_titles.show_loading(options); } NEWSBLUR.app.taskbar_info.hide_stories_error(); - - this.model.fetch_starred_stories(1, this.flags['starred_tag'], this.flags['starred_tag'] == 'highlights', _.bind(this.post_open_starred_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, true); + + this.model.fetch_starred_stories(1, this.flags['starred_tag'], this.flags['starred_tag'] == 'highlights', _.bind(this.post_open_starred_stories, this), + NEWSBLUR.app.taskbar_info.show_stories_error, true); }, - - post_open_starred_stories: function(data, first_load) { + + post_open_starred_stories: function (data, first_load) { if (!this.flags['starred_view']) return; // NEWSBLUR.log(['post_open_starred_stories', data.stories.length, first_load]); @@ -1952,15 +1952,15 @@ // this.show_story_titles_above_intelligence_level({'animate': false}); this.flags['story_titles_loaded'] = true; }, - + // ===================== // = Read Stories Feed = // ===================== - - open_read_stories: function(options) { + + open_read_stories: function (options) { options = options || {}; var $story_titles = this.$s.$story_titles; - + this.reset_feed(options); this.hide_splash_page(); this.active_feed = 'read'; @@ -1982,7 +1982,7 @@ this.switch_taskbar_view(this.story_view); this.switch_story_layout(); this.setup_mousemove_on_views(); - this.make_feed_title_in_stories(); + this.make_feed_title_in_stories(); if (!options.silent) { var url = "/read"; @@ -1998,12 +1998,12 @@ NEWSBLUR.app.story_titles.show_loading(options); } NEWSBLUR.app.taskbar_info.hide_stories_error(); - - this.model.fetch_read_stories(1, _.bind(this.post_open_read_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, true); + + this.model.fetch_read_stories(1, _.bind(this.post_open_read_stories, this), + NEWSBLUR.app.taskbar_info.show_stories_error, true); }, - - post_open_read_stories: function(data, first_load) { + + post_open_read_stories: function (data, first_load) { if (this.active_feed == 'read') { // NEWSBLUR.log(['post_open_read_stories', data.stories.length, first_load]); this.flags['opening_feed'] = false; @@ -2017,17 +2017,17 @@ this.flags['story_titles_loaded'] = true; } }, - + // ================= // = River of News = // ================= - - open_river_stories: function($folder, folder, options) { + + open_river_stories: function ($folder, folder, options) { options = options || {}; var $story_titles = this.$s.$story_titles; $folder = $folder || this.$s.$feed_list; var folder_title = folder && folder.get('folder_title') || "Everything"; - + this.reset_feed(options); if (options.story_id) { @@ -2056,17 +2056,17 @@ options.feed.set('selected', true); } else { var folder_view = NEWSBLUR.assets.folders.get_view($folder) || - this.active_folder && this.active_folder.folder_view; + this.active_folder && this.active_folder.folder_view; folder_view.model.set('selected', true); } } this.active_folder = folder || NEWSBLUR.assets.folders; - + this.iframe_scroll = null; this.flags['opening_feed'] = true; this.$s.$layout.addClass('NB-view-river'); this.flags.river_view = true; - + $('.task_view_page', this.$s.$taskbar).addClass('NB-disabled'); var explicit_view_setting = this.model.view_setting(this.active_feed, 'view'); if (!explicit_view_setting || explicit_view_setting == 'page') { @@ -2087,7 +2087,7 @@ NEWSBLUR.router.navigate(url); } } - + var visible_only = this.model.view_setting(this.active_feed, 'read_filter') == 'unread'; if (NEWSBLUR.reader.flags.search) visible_only = false; if (NEWSBLUR.reader.flags.feed_list_showing_starred) visible_only = false; @@ -2109,25 +2109,25 @@ NEWSBLUR.app.taskbar_info.hide_stories_error(); // NEWSBLUR.app.taskbar_info.show_stories_progress_bar(feeds.length); if (options.dashboard_transfer) { - NEWSBLUR.assets.stories.reset(options.dashboard_transfer.map(function(story) { + NEWSBLUR.assets.stories.reset(options.dashboard_transfer.map(function (story) { return story.toJSON(); })); if (this.counts['select_story_in_feed'] || this.flags['select_story_in_feed']) { this.select_story_in_feed(); } - this.model.fetch_river_stories(this.active_feed, feeds, 1, {'infrequent': options.infrequent}, + this.model.fetch_river_stories(this.active_feed, feeds, 1, { 'infrequent': options.infrequent }, _.bind(this.post_open_river_stories, this), NEWSBLUR.app.taskbar_info.show_stories_error, false); } else { - this.model.fetch_river_stories(this.active_feed, feeds, 1, {'infrequent': options.infrequent}, + this.model.fetch_river_stories(this.active_feed, feeds, 1, { 'infrequent': options.infrequent }, _.bind(this.post_open_river_stories, this), NEWSBLUR.app.taskbar_info.show_stories_error, true); } }, - - post_open_river_stories: function(data, first_load) { + + post_open_river_stories: function (data, first_load) { if (!data) { - return NEWSBLUR.app.taskbar_info.show_stories_error(data); + return NEWSBLUR.app.taskbar_info.show_stories_error(data); } - + if (this.active_feed && _.isString(this.active_feed) && this.active_feed.indexOf('river:') != -1) { this.flags['opening_feed'] = false; @@ -2142,9 +2142,9 @@ this.select_story_in_feed(); } // NEWSBLUR.app.taskbar_info.hide_stories_progress_bar(_.bind(function() { - if (first_load) { - this.position_mouse_indicator(); - } + if (first_load) { + this.position_mouse_indicator(); + } // }, this)); if (NEWSBLUR.Globals.is_anonymous) { this.show_tryout_signup_button(); @@ -2160,19 +2160,19 @@ } } }, - + // =================== // = River Blurblogs = // =================== - - open_river_blurblogs_stories: function(options) { + + open_river_blurblogs_stories: function (options) { options = options || {}; var $story_titles = this.$s.$story_titles; var folder_title = options.global ? "Global Blurblogs" : "Blurblogs"; - + this.reset_feed(options); this.hide_splash_page(); - + this.active_feed = options.global ? 'river:global' : 'river:blurblogs'; this.active_folder = new Backbone.Model({ id: this.active_feed, @@ -2180,13 +2180,13 @@ fake: true, show_options: true }); - + if (options.global) { this.$s.$river_global_header.addClass('NB-selected'); } else { this.$s.$river_blurblogs_header.addClass('NB-selected'); } - + this.iframe_scroll = null; this.flags['opening_feed'] = true; this.$s.$layout.addClass('NB-view-river'); @@ -2196,7 +2196,7 @@ if (options.story_id) { this.flags['select_story_in_feed'] = options.story_id; } - + $('.task_view_page', this.$s.$taskbar).addClass('NB-disabled'); var explicit_view_setting = this.model.view_setting(this.active_feed, 'view'); if (!explicit_view_setting || explicit_view_setting == 'page') { @@ -2226,30 +2226,30 @@ NEWSBLUR.app.taskbar_info.hide_stories_error(); NEWSBLUR.app.taskbar_info.show_stories_progress_bar(100); // Assume 100 followees for popular if (options.dashboard_transfer) { - NEWSBLUR.assets.stories.reset(options.dashboard_transfer.map(function(story) { + NEWSBLUR.assets.stories.reset(options.dashboard_transfer.map(function (story) { return story.toJSON(); })); if (this.counts['select_story_in_feed'] || this.flags['select_story_in_feed']) { this.select_story_in_feed(); } - this.model.fetch_river_blurblogs_stories(this.active_feed, 1, - {'global': this.flags.global_blurblogs}, - _.bind(this.post_open_river_blurblogs_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, false); + this.model.fetch_river_blurblogs_stories(this.active_feed, 1, + { 'global': this.flags.global_blurblogs }, + _.bind(this.post_open_river_blurblogs_stories, this), + NEWSBLUR.app.taskbar_info.show_stories_error, false); } else { - this.model.fetch_river_blurblogs_stories(this.active_feed, 1, - {'global': this.flags.global_blurblogs}, - _.bind(this.post_open_river_blurblogs_stories, this), + this.model.fetch_river_blurblogs_stories(this.active_feed, 1, + { 'global': this.flags.global_blurblogs }, + _.bind(this.post_open_river_blurblogs_stories, this), NEWSBLUR.app.taskbar_info.show_stories_error, true); } }, - - post_open_river_blurblogs_stories: function(data, first_load) { + + post_open_river_blurblogs_stories: function (data, first_load) { // NEWSBLUR.log(['post_open_river_stories', data, this.active_feed]); if (!data) { - return NEWSBLUR.app.taskbar_info.show_stories_error(data); + return NEWSBLUR.app.taskbar_info.show_stories_error(data); } - + if (this.active_feed && _.isString(this.active_feed) && this.active_feed.indexOf('river:') != -1) { this.flags['opening_feed'] = false; @@ -2261,7 +2261,7 @@ } else if (this.counts['find_last_unread_on_page_of_feed_stories_load']) { this.show_last_unread_story(true); } else if (this.counts['select_story_in_feed'] || - this.flags['select_story_in_feed']) { + this.flags['select_story_in_feed']) { this.select_story_in_feed(); } if (first_load) { @@ -2274,16 +2274,16 @@ } } }, - + // ================== // = Social Stories = // ================== - - open_social_stories: function(feed_id, options) { + + open_social_stories: function (feed_id, options) { // NEWSBLUR.log(["open_social_stories", feed_id, options]); options = options || {}; if (_.isNumber(feed_id)) feed_id = "social:" + feed_id; - + var feed = this.model.get_feed(feed_id); var $story_titles = this.$s.$story_titles; var $social_feed = this.find_social_feed_with_feed_id(feed_id); @@ -2296,10 +2296,10 @@ }); return this.load_social_feed_in_tryfeed_view(socialsub, options); } - + this.reset_feed(options); this.hide_splash_page(); - + this.active_feed = feed.id; this.next_feed = feed.id; this.flags.river_view = false; @@ -2307,23 +2307,23 @@ if (options.story_id) { this.flags['select_story_in_feed'] = options.story_id; } - + this.iframe_scroll = null; this.flags['opening_feed'] = true; if (options.feed) { - options.feed.set('selected', true, options); + options.feed.set('selected', true, options); } else { - feed.set('selected', true, options); + feed.set('selected', true, options); } this.set_correct_story_view_for_feed(this.active_feed); this.make_feed_title_in_stories(); this.$s.$layout.addClass('NB-view-river'); - + // TODO: Only make feed the default for blurblogs, not overriding an explicit pref. this.switch_taskbar_view(this.story_view); this.switch_story_layout(); this.setup_mousemove_on_views(); - + if (NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full') { NEWSBLUR.app.story_list.show_loading(options); } else { @@ -2347,7 +2347,7 @@ } if (this.story_view == 'page') { - _.delay(_.bind(function() { + _.delay(_.bind(function () { if (!options.delay || feed.id == this.next_feed) { NEWSBLUR.app.original_tab_view.load_feed_iframe(); } @@ -2372,13 +2372,13 @@ NEWSBLUR.router.navigate(''); } }, - - post_open_social_stories: function(data, first_load) { + + post_open_social_stories: function (data, first_load) { // NEWSBLUR.log(['post_open_river_stories', data, this.active_feed, this.flags['select_story_in_feed']]); if (!data) { - return NEWSBLUR.app.taskbar_info.show_stories_error(data); + return NEWSBLUR.app.taskbar_info.show_stories_error(data); } - + if (this.active_feed && NEWSBLUR.utils.is_feed_social(this.active_feed)) { this.flags['opening_feed'] = false; // this.show_story_titles_above_intelligence_level({'animate': false}); @@ -2405,31 +2405,31 @@ } } }, - - find_social_feed_with_feed_id: function(feed_id) { + + find_social_feed_with_feed_id: function (feed_id) { if (_.contains(this.cache.$feed_in_social_feed_list, feed_id)) { return this.cache.$feed_in_social_feed_list[feed_id]; } - + var $social_feeds = this.$s.$social_feeds; var $feeds = $([]); - - $('.feed', $social_feeds).each(function() { + + $('.feed', $social_feeds).each(function () { if ($(this).data('id') == feed_id) { $feeds.push($(this).get(0)); } }); - + this.cache.$feed_in_social_feed_list[feed_id] = $feeds; - + return $feeds; }, - + // ========================== // = Story Pane - All Views = // ========================== - - switch_to_correct_view: function(options) { + + switch_to_correct_view: function (options) { options = options || {}; // NEWSBLUR.log(['Found story', this.story_view, options.found_story_in_page, this.flags['page_view_showing_feed_view'], this.flags['feed_view_showing_story_view']]); @@ -2440,13 +2440,13 @@ this.flags['page_view_showing_feed_view'] = true; this.flags['feed_view_showing_story_view'] = false; this.flags['temporary_story_view'] = false; - this.switch_taskbar_view('feed', {skip_save_type: 'page'}); + this.switch_taskbar_view('feed', { skip_save_type: 'page' }); NEWSBLUR.app.story_list.show_stories_preference_in_feed_view(); } - } else if (_.contains(['list', 'grid', 'magazine'], - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && + } else if (_.contains(['list', 'grid', 'magazine'], + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && (this.story_view == 'page' || this.story_view == 'story')) { - this.switch_taskbar_view('feed', {skip_save_type: 'layout'}); + this.switch_taskbar_view('feed', { skip_save_type: 'layout' }); } else if (this.story_view == 'page' && this.flags['page_view_showing_feed_view']) { // NEWSBLUR.log(['turn off feed view', this.flags['page_view_showing_feed_view'], this.flags['feed_view_showing_story_view']]); this.flags['page_view_showing_feed_view'] = false; @@ -2458,35 +2458,35 @@ this.flags['page_view_showing_feed_view'] = false; this.flags['feed_view_showing_story_view'] = false; this.flags['temporary_story_view'] = false; - this.switch_taskbar_view(this.story_view, {skip_save_type: true}); + this.switch_taskbar_view(this.story_view, { skip_save_type: true }); } else if (this.flags['temporary_story_view']) { // NEWSBLUR.log(['turn off story view', this.flags['page_view_showing_feed_view'], this.flags['feed_view_showing_story_view']]); this.flags['page_view_showing_feed_view'] = false; this.flags['feed_view_showing_story_view'] = false; this.flags['temporary_story_view'] = false; - this.switch_taskbar_view(this.story_view, {skip_save_type: true}); + this.switch_taskbar_view(this.story_view, { skip_save_type: true }); } }, - - mark_active_story_read: function() { + + mark_active_story_read: function () { if (!this.active_story) return; var story_id = this.active_story.id; var story = this.model.get_story(story_id); if (this.active_story && !this.active_story.get('read_status')) { - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); } else if (this.active_story && this.active_story.get('read_status')) { NEWSBLUR.assets.stories.mark_unread(story); } }, - - maybe_mark_all_as_read: function() { + + maybe_mark_all_as_read: function () { if (_.contains(['river:blurblogs', 'river:global'], this.active_feed)) { return; } else if (this.flags.social_view) { this.mark_feed_as_read(); } else if (this.flags.river_view) { if (this.active_feed == 'river:' && NEWSBLUR.assets.preference('mark_read_river_confirm')) { - this.open_mark_read_modal({days: 0}); + this.open_mark_read_modal({ days: 0 }); } else { this.mark_folder_as_read(); } @@ -2494,22 +2494,22 @@ this.mark_feed_as_read(); } }, - - mark_feed_as_read: function(feed_id, days_back, direction) { + + mark_feed_as_read: function (feed_id, days_back, direction) { feed_id = feed_id || this.active_feed; this.mark_feeds_as_read([feed_id], days_back, direction); - - if (!direction && NEWSBLUR.assets.preference('markread_nextfeed') == 'nextfeed' && + + if (!direction && NEWSBLUR.assets.preference('markread_nextfeed') == 'nextfeed' && NEWSBLUR.reader.active_feed == feed_id) { this.show_next_feed(1); } }, - - mark_folder_as_read: function(folder, days_back, direction) { + + mark_folder_as_read: function (folder, days_back, direction) { var folder = folder || this.active_folder; - var feeds = folder.feed_ids_in_folder({unreads_only: true}); - + var feeds = folder.feed_ids_in_folder({ unreads_only: true }); + if (direction) { var order = NEWSBLUR.assets.view_setting(this.active_feed, 'order'); if ((direction == "newer" && order == "oldest") || (direction == "older" && order == "newest")) { @@ -2522,53 +2522,53 @@ } } } - + this.mark_feeds_as_read(feeds, days_back, direction); - + if (!direction && NEWSBLUR.assets.preference('markread_nextfeed') == 'nextfeed' && NEWSBLUR.reader.active_folder == folder) { this.show_next_feed(1); } }, - - mark_feeds_as_read: function(feeds, days_back, direction) { + + mark_feeds_as_read: function (feeds, days_back, direction) { var options = {}; var order = NEWSBLUR.assets.view_setting(this.active_feed, 'order'); var stories_not_visible = true; var cutoff_timestamp = parseInt(NEWSBLUR.utils.days_back_to_timestamp(days_back) || 0, 10); - if (!days_back && this.model.stories.length && + if (!days_back && this.model.stories.length && _.contains(feeds, this.model.stories.first().get('story_feed_id')) && order == 'newest') { cutoff_timestamp = this.model.stories.first().get('story_timestamp'); } - + if ((order == 'newest' && direction == 'newer') || (order == 'oldest' && direction == 'older')) { - var stories = this.model.stories.select(function(story) { + var stories = this.model.stories.select(function (story) { return direction == 'newer' ? story.get('story_timestamp') >= cutoff_timestamp : - story.get('story_timestamp') <= cutoff_timestamp; + story.get('story_timestamp') <= cutoff_timestamp; }); - feeds = _.unique(_.map(stories, function(story) { return story.get('story_feed_id'); })); + feeds = _.unique(_.map(stories, function (story) { return story.get('story_feed_id'); })); } if (this.active_feed == 'river:infrequent') options.infrequent = NEWSBLUR.assets.preference('infrequent_stories_per_month'); this.model.mark_feed_as_read(feeds, cutoff_timestamp, direction, options, - _.bind(function() { - if (feeds.length == 1) { - this.feed_unread_count(feeds[0]); - } else if (!this.socket || !this.socket || !this.socket.connected) { - this.force_feeds_refresh(null, false, feeds); - } - }, this)); + _.bind(function () { + if (feeds.length == 1) { + this.feed_unread_count(feeds[0]); + } else if (!this.socket || !this.socket || !this.socket.connected) { + this.force_feeds_refresh(null, false, feeds); + } + }, this)); }, - - open_story_trainer: function(story_id, feed_id, options) { + + open_story_trainer: function (story_id, feed_id, options) { options = options || {}; story_id = story_id || this.active_story && this.active_story.id; feed_id = feed_id || (story_id && this.model.get_story(story_id).get('story_feed_id')); var story = this.model.get_story(story_id); // console.log(["open_story_trainer", story_id, feed_id, options]); - + if (story_id && feed_id) { options['feed_loaded'] = !this.flags['river_view']; if (this.flags['social_view']) { @@ -2586,227 +2586,227 @@ // =========== // = Send To = // =========== - - send_story_to_instapaper: function(story_id) { + + send_story_to_instapaper: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.instapaper.com/edit'; var instapaper_url = [ - url, - '?url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')) + url, + '?url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')) ].join(''); window.open(instapaper_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_readitlater: function(story_id) { + + send_story_to_readitlater: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://getpocket.com/save'; var readitlater_url = [ - url, - '?url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')) + url, + '?url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')) ].join(''); window.open(readitlater_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_tumblr: function(story_id) { + + send_story_to_tumblr: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.tumblr.com/share'; var tumblr_url = [ - url, - '?v=3&u=', - encodeURIComponent(story.get('story_permalink')), - '&t=', - encodeURIComponent(story.get('story_title')) + url, + '?v=3&u=', + encodeURIComponent(story.get('story_permalink')), + '&t=', + encodeURIComponent(story.get('story_title')) ].join(''); window.open(tumblr_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_blogger: function(story_id) { + + send_story_to_blogger: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.blogger.com/blog-this.g'; var blogger_url = [ - url, - '?n=', - encodeURIComponent(story.get('story_title')), - '&source=newsblur&b=', - encodeURIComponent(story.get('story_permalink')) + url, + '?n=', + encodeURIComponent(story.get('story_title')), + '&source=newsblur&b=', + encodeURIComponent(story.get('story_permalink')) ].join(''); window.open(blogger_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_delicious: function(story_id) { + + send_story_to_delicious: function (story_id) { var story = this.model.get_story(story_id); var url = 'http://www.delicious.com/save'; var delicious_url = [ - url, - '?v=6&url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')) + url, + '?v=6&url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')) ].join(''); window.open(delicious_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_twitter: function(story_id) { + + send_story_to_twitter: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://twitter.com/intent/tweet'; var twitter_url = [ - url, - '?text=', - encodeURIComponent(story.get('story_title')), - '&url=', - encodeURIComponent(story.get('story_permalink')), - '&via=NewsBlur' + url, + '?text=', + encodeURIComponent(story.get('story_title')), + '&url=', + encodeURIComponent(story.get('story_permalink')), + '&via=NewsBlur' ].join(''); window.open(twitter_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_facebook: function(story_id) { + + send_story_to_facebook: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.facebook.com/sharer/sharer.php'; var facebook_url = [ - url, - '?u=', - encodeURIComponent(story.get('story_permalink')), - '&t=', - encodeURIComponent(story.get('story_title')) + url, + '?u=', + encodeURIComponent(story.get('story_permalink')), + '&t=', + encodeURIComponent(story.get('story_title')) ].join(''); window.open(facebook_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_pinboard: function(story_id) { + + send_story_to_pinboard: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://pinboard.in/add/?'; var pinboard_url = [ - url, - 'url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')), - '&tags=', - encodeURIComponent(story.get('story_tags').join(', ')) + url, + 'url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')), + '&tags=', + encodeURIComponent(story.get('story_tags').join(', ')) ].join(''); window.open(pinboard_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_raindrop: function(story_id) { + + send_story_to_raindrop: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://app.raindrop.io/add?'; var raindrop_url = [ - url, - 'link=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')), - '&tags=', - encodeURIComponent(story.get('story_tags').join(', ')) + url, + 'link=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')), + '&tags=', + encodeURIComponent(story.get('story_tags').join(', ')) ].join(''); window.open(raindrop_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_pinterest: function(story_id) { + + send_story_to_pinterest: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.pinterest.com/pin/find/?'; var pinterest_url = [ - url, - 'url=', - encodeURIComponent(story.get('story_permalink')) + url, + 'url=', + encodeURIComponent(story.get('story_permalink')) ].join(''); window.open(pinterest_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_buffer: function(story_id) { + + send_story_to_buffer: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://bufferapp.com/add?source=newsblur&'; var buffer_url = [ - url, - 'url=', - encodeURIComponent(story.get('story_permalink')), - '&text=', - encodeURIComponent(story.get('story_title')) + url, + 'url=', + encodeURIComponent(story.get('story_permalink')), + '&text=', + encodeURIComponent(story.get('story_title')) ].join(''); window.open(buffer_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_diigo: function(story_id) { + + send_story_to_diigo: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.diigo.com/post?'; var url = [ - url, - 'url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')), - '&tags=', - encodeURIComponent(story.get('story_tags').join(', ')) + url, + 'url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')), + '&tags=', + encodeURIComponent(story.get('story_tags').join(', ')) ].join(''); window.open(url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_evernote: function(story_id) { + + send_story_to_evernote: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://www.evernote.com/clip.action?'; var url = [ - url, - 'url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')), - '&tags=', - encodeURIComponent(story.get('story_tags').join(', ')) + url, + 'url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')), + '&tags=', + encodeURIComponent(story.get('story_tags').join(', ')) ].join(''); window.open(url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_googleplus: function(story_id) { + + send_story_to_googleplus: function (story_id) { var story = this.model.get_story(story_id); var url = 'https://plus.google.com/share'; var googleplus_url = [ - url, - '?url=', - encodeURIComponent(story.get('story_permalink')), - '&title=', - encodeURIComponent(story.get('story_title')), - '&tags=', - encodeURIComponent(story.get('story_tags').join(', ')) + url, + '?url=', + encodeURIComponent(story.get('story_permalink')), + '&title=', + encodeURIComponent(story.get('story_title')), + '&tags=', + encodeURIComponent(story.get('story_tags').join(', ')) ].join(''); window.open(googleplus_url, '_blank'); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - - send_story_to_email: function(story) { + + send_story_to_email: function (story) { NEWSBLUR.reader_send_email = new NEWSBLUR.ReaderSendEmail(story); - NEWSBLUR.assets.stories.mark_read(story, {skip_delay: true}); + NEWSBLUR.assets.stories.mark_read(story, { skip_delay: true }); }, - + // =========== // = Stories = // =========== - - load_page_of_feed_stories: function(options) { - options = _.extend({}, {'scroll_to_loadbar': true}, options); + + load_page_of_feed_stories: function (options) { + options = _.extend({}, { 'scroll_to_loadbar': true }, options); var $story_titles = this.$s.$story_titles; var feed_id = this.active_feed; var feed = this.model.get_feed(feed_id); - + // console.log(['load_page_of_feed_stories', this.flags['opening_feed'], this.counts['page']]); if (this.flags['opening_feed']) return; @@ -2817,45 +2817,45 @@ } else { NEWSBLUR.app.story_titles.show_loading(options); } - + if (this.active_feed == 'river:infrequent') options.infrequent = NEWSBLUR.assets.preference('infrequent_stories_per_month'); - + if (this.flags['starred_view']) { this.model.fetch_starred_stories(this.counts['page'], this.flags['starred_tag'], this.flags['starred_tag'] == 'highlights', _.bind(this.post_open_starred_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, false); + NEWSBLUR.app.taskbar_info.show_stories_error, false); } else if (this.active_feed == 'read') { this.model.fetch_read_stories(this.counts['page'], _.bind(this.post_open_read_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, false); + NEWSBLUR.app.taskbar_info.show_stories_error, false); } else if (this.flags['social_view'] && _.contains(['river:blurblogs', 'river:global'], this.active_feed)) { this.model.fetch_river_blurblogs_stories(this.active_feed, - this.counts['page'], - {'global': this.flags.global_blurblogs}, - _.bind(this.post_open_river_blurblogs_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, false); + this.counts['page'], + { 'global': this.flags.global_blurblogs }, + _.bind(this.post_open_river_blurblogs_stories, this), + NEWSBLUR.app.taskbar_info.show_stories_error, false); } else if (this.flags['social_view']) { this.model.fetch_social_stories(this.active_feed, - this.counts['page'], _.bind(this.post_open_social_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, false); + this.counts['page'], _.bind(this.post_open_social_stories, this), + NEWSBLUR.app.taskbar_info.show_stories_error, false); } else if (this.flags['river_view']) { this.model.fetch_river_stories(this.active_feed, this.cache['river_feeds_with_unreads'], - this.counts['page'], {'infrequent': options.infrequent}, - _.bind(this.post_open_river_stories, this), - NEWSBLUR.app.taskbar_info.show_stories_error, false); + this.counts['page'], { 'infrequent': options.infrequent }, + _.bind(this.post_open_river_stories, this), + NEWSBLUR.app.taskbar_info.show_stories_error, false); } else { - this.model.load_feed(feed_id, this.counts['page'], false, - $.rescope(this.post_open_feed, this), NEWSBLUR.app.taskbar_info.show_stories_error); + this.model.load_feed(feed_id, this.counts['page'], false, + $.rescope(this.post_open_feed, this), NEWSBLUR.app.taskbar_info.show_stories_error); } }, - - make_feed_title_in_stories: function(options) { + + make_feed_title_in_stories: function (options) { NEWSBLUR.app.search_header.render(); - + if ((this.flags.search || this.flags.searching) && NEWSBLUR.app.story_titles_header.search_has_focus()) { console.log(["make_feed_title_in_stories not destroying", this.flags.search]); return; } - + if (NEWSBLUR.app.story_titles_header) { NEWSBLUR.app.story_titles_header.remove(); } @@ -2865,8 +2865,8 @@ layout: NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') }); }, - - feed_title: function(feed_id) { + + feed_title: function (feed_id) { if (!feed_id) { feed_id = this.active_feed; } @@ -2889,7 +2889,7 @@ } else if (_.string.startsWith(feed_id, 'starred')) { feed_title = "Saved Stories"; var tag = feed_id.replace('starred:', ''); - var model = NEWSBLUR.assets.starred_feeds.detect(function(feed) { + var model = NEWSBLUR.assets.starred_feeds.detect(function (feed) { return feed.tag_slug() == tag || feed.get('tag') == tag; }); if (model && model.get('tag')) { @@ -2903,26 +2903,26 @@ } var feed = NEWSBLUR.assets.get_feed(feed_id); if (!feed) return; - + feed_title = feed.get('feed_title'); - } else if (_.string.startsWith(feed_id, 'social:')){ + } else if (_.string.startsWith(feed_id, 'social:')) { var feed = NEWSBLUR.assets.get_feed(feed_id); if (!feed) return; - + feed_title = feed.get('feed_title'); - } else if (_.string.startsWith(feed_id, 'search:')){ + } else if (_.string.startsWith(feed_id, 'search:')) { var feed = NEWSBLUR.assets.get_feed(feed_id); if (!feed) return; - + feed_title = feed.get('feed_title'); } - + return feed_title; }, - - open_feed_intelligence_modal: function(score, feed_id, feed_loaded) { + + open_feed_intelligence_modal: function (score, feed_id, feed_loaded) { feed_id = feed_id || this.active_feed; - + if (feed_id) { NEWSBLUR.classifier = new NEWSBLUR.ReaderClassifierFeed(feed_id, { 'score': score, @@ -2930,70 +2930,70 @@ }); } }, - - open_trainer_modal: function(score) { + + open_trainer_modal: function (score) { var feed_id = this.active_feed; // NEWSBLUR.classifier = new NEWSBLUR.ReaderClassifierFeed(feed_id, {'score': score}); - NEWSBLUR.classifier = new NEWSBLUR.ReaderClassifierTrainer({'score': score}); - }, - - open_friends_modal: function() { + NEWSBLUR.classifier = new NEWSBLUR.ReaderClassifierTrainer({ 'score': score }); + }, + + open_friends_modal: function () { NEWSBLUR.assets.preference('has_found_friends', true); NEWSBLUR.reader.check_hide_getting_started(); NEWSBLUR.reader_friends = new NEWSBLUR.ReaderFriends(); }, - - open_profile_editor_modal: function() { + + open_profile_editor_modal: function () { NEWSBLUR.reader_profile_editor = new NEWSBLUR.ReaderProfileEditor(); }, - - open_recommend_modal: function(feed_id) { + + open_recommend_modal: function (feed_id) { NEWSBLUR.recommend_feed = new NEWSBLUR.ReaderRecommendFeed(feed_id); }, - - open_tutorial_modal: function() { + + open_tutorial_modal: function () { NEWSBLUR.tutorial = new NEWSBLUR.ReaderTutorial(); }, - - open_intro_modal: function(options) { + + open_intro_modal: function (options) { NEWSBLUR.intro = new NEWSBLUR.ReaderIntro(options); }, - - open_user_admin_modal: function(options) { - $.modal.close(function() { + + open_user_admin_modal: function (options) { + $.modal.close(function () { NEWSBLUR.user_admin = new NEWSBLUR.ReaderUserAdmin(options); }); }, - - open_story_options_popover: function() { + + open_story_options_popover: function () { NEWSBLUR.StoryOptionsPopover.create({ anchor: this.$s.$taskbar_options }); }, - - show_authentication_lost: function() { + + show_authentication_lost: function () { NEWSBLUR.auth_lost = new NEWSBLUR.ReaderAuthLost(); }, - - check_hide_getting_started: function(force) { + + check_hide_getting_started: function (force) { var feeds = this.model.preference('has_setup_feeds'); var friends = this.model.preference('has_found_friends'); var trained = this.model.preference('has_trained_intelligence'); - + if (force || (friends && trained && feeds)) { var $gettingstarted = $('.NB-module-gettingstarted'); $gettingstarted.animate({ - 'opacity': 0 - }, { - 'duration': 380, - 'complete': function() { - $gettingstarted.slideUp(350); - } - }); - this.model.preference('hide_getting_started', true); + 'opacity': 0 + }, { + 'duration': 380, + 'complete': function () { + $gettingstarted.slideUp(350); + } + }); + this.model.preference('hide_getting_started', true); } else { var $progress = $(".NB-intro-progress"); var $sites = $('.NB-intro-goal-sites'); @@ -3003,7 +3003,7 @@ $sites.toggleClass('NB-done', feeds); $findfriends.toggleClass('NB-done', friends); $trainer.toggleClass('NB-done', trained); - + $sites.toggleClass('NB-not-done', !feeds); $findfriends.toggleClass('NB-not-done', !friends); $trainer.toggleClass('NB-not-done', !trained); @@ -3012,43 +3012,43 @@ $(".bar-second", $progress).toggleClass('bar-striped', !friends); } }, - + // ========================== // = Story Pane - Feed View = // ========================== - - apply_story_styling: function(reset_stories) { + + apply_story_styling: function (reset_stories) { var $body = this.$s.$body; $body.removeClass('NB-theme-sans-serif') - .removeClass('NB-theme-serif') - .removeClass('NB-theme-gotham') - .removeClass('NB-theme-sentinel') - .removeClass('NB-theme-whitney') - .removeClass('NB-theme-chronicle'); - $body.addClass('NB-theme-'+NEWSBLUR.Preferences['story_styling']); - + .removeClass('NB-theme-serif') + .removeClass('NB-theme-gotham') + .removeClass('NB-theme-sentinel') + .removeClass('NB-theme-whitney') + .removeClass('NB-theme-chronicle'); + $body.addClass('NB-theme-' + NEWSBLUR.Preferences['story_styling']); + $body.removeClass('NB-theme-size-xs') - .removeClass('NB-theme-size-s') - .removeClass('NB-theme-size-m') - .removeClass('NB-theme-size-l') - .removeClass('NB-theme-size-xl'); + .removeClass('NB-theme-size-s') + .removeClass('NB-theme-size-m') + .removeClass('NB-theme-size-l') + .removeClass('NB-theme-size-xl'); $body.addClass('NB-theme-size-' + NEWSBLUR.Preferences['story_size']); $body.removeClass('NB-theme-feed-size-xs') - .removeClass('NB-theme-feed-size-s') - .removeClass('NB-theme-feed-size-m') - .removeClass('NB-theme-feed-size-l') - .removeClass('NB-theme-feed-size-xl'); + .removeClass('NB-theme-feed-size-s') + .removeClass('NB-theme-feed-size-m') + .removeClass('NB-theme-feed-size-l') + .removeClass('NB-theme-feed-size-xl'); $body.addClass('NB-theme-feed-size-' + NEWSBLUR.Preferences['feed_size']); $body.removeClass('NB-theme-feed-font-whitney') - .removeClass('NB-theme-feed-font-lucida') - .removeClass('NB-theme-feed-font-gotham'); + .removeClass('NB-theme-feed-font-lucida') + .removeClass('NB-theme-feed-font-gotham'); $body.addClass('NB-theme-feed-font-' + NEWSBLUR.Preferences['feed_font']); - + $body.removeClass('NB-line-spacing-xs') - .removeClass('NB-line-spacing-s') - .removeClass('NB-line-spacing-m') - .removeClass('NB-line-spacing-l') - .removeClass('NB-line-spacing-xl'); + .removeClass('NB-line-spacing-s') + .removeClass('NB-line-spacing-m') + .removeClass('NB-line-spacing-l') + .removeClass('NB-line-spacing-xl'); $body.addClass('NB-line-spacing-' + NEWSBLUR.Preferences['story_line_spacing']); $body.removeClass('NB-density-compact') @@ -3060,14 +3060,14 @@ .removeClass('NB-content-preview-medium') .removeClass('NB-content-preview-large'); $body.addClass('NB-content-preview-' + NEWSBLUR.Preferences['show_content_preview']); - + $body.removeClass('NB-image-preview-none') .removeClass('NB-image-preview-small-left') .removeClass('NB-image-preview-small-right') .removeClass('NB-image-preview-large-left') .removeClass('NB-image-preview-large-right'); $body.addClass('NB-image-preview-' + NEWSBLUR.Preferences['image_preview']); - + if (reset_stories) { this.show_story_titles_above_intelligence_level({ 'animate': true, 'follow': true }); NEWSBLUR.app.dashboard_rivers.left.redraw(); @@ -3078,12 +3078,12 @@ this.load_theme(); }, - + // =================== // = Taskbar - Story = // =================== - - switch_taskbar_view: function(view, options) { + + switch_taskbar_view: function (view, options) { options = options || {}; var self = this; var $story_pane = this.$s.$story_pane; @@ -3091,35 +3091,35 @@ var feed = this.model.get_feed(feed_id); view = view || this.story_view; // NEWSBLUR.log(['switch_taskbar_view', view, options.skip_save_type, feed]); - - if (view == 'page' && feed && feed.get('has_exception') && + + if (view == 'page' && feed && feed.get('has_exception') && feed.get('exception_type') == 'page') { - this.open_feed_exception_modal(); - return; - } else if (_.contains(['page', 'story'], view) && - feed && (feed.get('disabled_page') || - NEWSBLUR.utils.is_url_iframe_buster(feed.get('feed_link')))) { + this.open_feed_exception_modal(); + return; + } else if (_.contains(['page', 'story'], view) && + feed && (feed.get('disabled_page') || + NEWSBLUR.utils.is_url_iframe_buster(feed.get('feed_link')))) { view = 'feed'; - } else if ($('.task_view_'+view).hasClass('NB-disabled') || - $('.task_view_'+view).hasClass('NB-hidden')) { + } else if ($('.task_view_' + view).hasClass('NB-disabled') || + $('.task_view_' + view).hasClass('NB-hidden')) { return; } else if (_.contains(['list', 'grid', 'magazine'], - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && - _.contains(['page', 'story'], view)) { + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && + _.contains(['page', 'story'], view)) { view = 'feed'; } - + var $taskbar_buttons = $('.NB-taskbar-view .NB-taskbar-button'); var $feed_view = this.$s.$feed_view; var $feed_iframe = this.$s.$feed_iframe; var $to_feed_arrow = $('.NB-taskbar .NB-task-view-to-feed-arrow'); var $to_story_arrow = $('.NB-taskbar .NB-task-view-to-story-arrow'); var $to_text_arrow = $('.NB-taskbar .NB-task-view-to-text-arrow'); - + if (!options.skip_save_type && this.story_view != view) { - this.model.view_setting(feed_id, {'view': view}); + this.model.view_setting(feed_id, { 'view': view }); } - + NEWSBLUR.app.taskbar_info.hide_stories_error(); $to_feed_arrow.hide(); $to_story_arrow.hide(); @@ -3138,13 +3138,13 @@ this.flags['temporary_story_view'] = true; } else { $taskbar_buttons.removeClass('NB-active'); - $('.NB-taskbar-button.task_view_'+view).addClass('NB-active'); + $('.NB-taskbar-button.task_view_' + view).addClass('NB-active'); this.story_view = view; } - + this.flags.scrolling_by_selecting_story_title = true; clearInterval(this.locks.scrolling); - this.locks.scrolling = setTimeout(function() { + this.locks.scrolling = setTimeout(function () { self.flags.scrolling_by_selecting_story_title = false; }, 550); if (view == 'page') { @@ -3156,7 +3156,7 @@ immediate: true, only_if_hidden: options.resize }); - + $story_pane.animate({ 'left': 0 }, { @@ -3171,7 +3171,7 @@ }); NEWSBLUR.app.story_list.show_stories_preference_in_feed_view(); NEWSBLUR.app.story_titles.scroll_to_selected_story(this.active_story); - + $story_pane.animate({ 'left': -1 * $feed_iframe.width() }, { @@ -3179,11 +3179,11 @@ 'duration': this.model.preference('animations') ? 550 : 0, 'queue': false }); - + NEWSBLUR.app.story_list.reset_story_positions(); - if (!options.resize && this.active_story && - _.contains(['list', 'grid', 'magazine'], - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { + if (!options.resize && this.active_story && + _.contains(['list', 'grid', 'magazine'], + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { NEWSBLUR.app.text_tab_view.unload(); if (this.active_story.get('selected')) { this.active_story.story_title_view.render_inline_story_detail(); @@ -3191,7 +3191,7 @@ } } else if (view == 'text') { NEWSBLUR.app.story_titles.scroll_to_selected_story(this.active_story); - + $story_pane.animate({ 'left': -2 * $feed_iframe.width() }, { @@ -3199,16 +3199,16 @@ 'duration': this.model.preference('animations') ? 550 : 0, 'queue': false }); - if (_.contains(['split', 'full'], - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { + if (_.contains(['split', 'full'], + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { NEWSBLUR.app.text_tab_view.fetch_and_render(); if (!this.active_story) { NEWSBLUR.app.text_tab_view.show_explainer_single_story_mode(); } - } else if (!options.resize && this.active_story && - this.active_story.get('selected') && - _.contains(['list', 'grid', 'magazine'], - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { + } else if (!options.resize && this.active_story && + this.active_story.get('selected') && + _.contains(['list', 'grid', 'magazine'], + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { this.active_story.story_title_view.render_inline_story_detail(); } } else if (view == 'story') { @@ -3220,34 +3220,34 @@ 'queue': false }); if (!this.active_story) { - NEWSBLUR.app.story_tab_view.show_explainer_single_story_mode(); + NEWSBLUR.app.story_tab_view.show_explainer_single_story_mode(); } else if (!options.resize) { NEWSBLUR.app.story_tab_view.open_story(); } } - + this.setup_mousemove_on_views(); }, - - switch_taskbar_view_direction: function(direction) { + + switch_taskbar_view_direction: function (direction) { var $active = $('.NB-taskbar-view .NB-active'); var view; - - if (_.contains(['list', 'grid', 'magazine'], + + if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { if (direction == -1) { if ($active.hasClass('task_view_feed')) { // view = 'page'; } else if ($active.hasClass('task_view_text')) { view = 'feed'; - } + } } else if (direction == 1) { if ($active.hasClass('task_view_feed')) { view = 'text'; } else if ($active.hasClass('task_view_text')) { // view = 'story'; - } - } + } + } } else { if (direction == -1) { if ($active.hasClass('task_view_page')) { @@ -3258,7 +3258,7 @@ view = 'feed'; } else if ($active.hasClass('task_view_story')) { view = 'text'; - } + } } else if (direction == 1) { if ($active.hasClass('task_view_page')) { view = 'feed'; @@ -3268,142 +3268,142 @@ view = 'story'; } else if ($active.hasClass('task_view_story')) { // view = 'text'; - } + } } } - + if (view) { - this.switch_taskbar_view(view); + this.switch_taskbar_view(view); } }, - + // =================== // = Taskbar - Feeds = // =================== - - open_add_feed_modal: function(options) { + + open_add_feed_modal: function (options) { clearInterval(this.flags['bouncing_callout']); $.modal.close(); - + if (NEWSBLUR.Globals.is_anonymous && NEWSBLUR.welcome) { - NEWSBLUR.welcome.show_signin_form(); + NEWSBLUR.welcome.show_signin_form(); } else { - NEWSBLUR.add_feed = NEWSBLUR.ReaderAddFeed.create(options); + NEWSBLUR.add_feed = NEWSBLUR.ReaderAddFeed.create(options); } }, - - open_manage_feed_modal: function(feed_id) { + + open_manage_feed_modal: function (feed_id) { feed_id = feed_id || this.active_feed; - + NEWSBLUR.manage_feed = new NEWSBLUR.ReaderManageFeed(feed_id); }, - open_mark_read_modal: function(options) { + open_mark_read_modal: function (options) { NEWSBLUR.mark_read = new NEWSBLUR.ReaderMarkRead(options); }, - open_keyboard_shortcuts_modal: function() { + open_keyboard_shortcuts_modal: function () { NEWSBLUR.keyboard = new NEWSBLUR.ReaderKeyboard(); }, - - open_goodies_modal: function() { + + open_goodies_modal: function () { NEWSBLUR.goodies = new NEWSBLUR.ReaderGoodies(); }, - - open_notifications_modal: function(feed_id) { + + open_notifications_modal: function (feed_id) { NEWSBLUR.notifications = new NEWSBLUR.ReaderNotifications(feed_id); }, - - open_newsletters_modal: function() { + + open_newsletters_modal: function () { NEWSBLUR.newsletters = new NEWSBLUR.ReaderNewsletters(); }, - - open_facebook_modal: function() { + + open_facebook_modal: function () { NEWSBLUR.facebook_dialog = new NEWSBLUR.ReaderFacebook(this.active_story.get('story_permalink'), this.active_story.get('shared_comments')); }, - - open_preferences_modal: function() { + + open_preferences_modal: function () { NEWSBLUR.preferences = new NEWSBLUR.ReaderPreferences(); }, - - open_account_modal: function(options) { + + open_account_modal: function (options) { NEWSBLUR.account = new NEWSBLUR.ReaderAccount(options); }, - - open_feedchooser_modal: function(options) { + + open_feedchooser_modal: function (options) { NEWSBLUR.feedchooser = new NEWSBLUR.ReaderFeedchooser(options); }, - - open_organizer_modal: function(options) { + + open_organizer_modal: function (options) { NEWSBLUR.organizer = new NEWSBLUR.ReaderOrganizer(options); }, - - open_feed_exception_modal: function(feed_id, options) { + + open_feed_exception_modal: function (feed_id, options) { feed_id = feed_id || this.active_feed; - + NEWSBLUR.feed_exception = new NEWSBLUR.ReaderFeedException(feed_id, options); }, - - open_feed_statistics_modal: function(feed_id) { + + open_feed_statistics_modal: function (feed_id) { feed_id = feed_id || this.active_feed; - + NEWSBLUR.statistics = new NEWSBLUR.ReaderStatistics(feed_id); }, - - open_social_profile_modal: function(user_id) { + + open_social_profile_modal: function (user_id) { if (!user_id) user_id = NEWSBLUR.Globals.user_id; if (_.string.contains(user_id, 'social:')) { user_id = parseInt(user_id.replace('social:', ''), 10); } NEWSBLUR.social_profile = new NEWSBLUR.ReaderSocialProfile(user_id); }, - - close_social_profile: function() { + + close_social_profile: function () { if (NEWSBLUR.social_profile) { NEWSBLUR.social_profile.close(); } }, - - switch_feed_font: function(feed_font) { + + switch_feed_font: function (feed_font) { this.model.preference('feed_font', feed_font); this.apply_story_styling(); }, - - switch_feed_font_size: function(feed_size) { + + switch_feed_font_size: function (feed_size) { this.model.preference('feed_size', feed_size); this.apply_story_styling(); }, - - switch_density: function(density) { + + switch_density: function (density) { this.model.preference('density', density); this.apply_story_styling(); }, - - switch_theme: function(theme) { + + switch_theme: function (theme) { this.model.preference('theme', theme); this.apply_story_styling(); }, - - load_theme: function() { + + load_theme: function () { var theme = NEWSBLUR.assets.theme(); var auto_theme = NEWSBLUR.assets.preference('theme'); // Add auto - var feed_font = NEWSBLUR.assets.preference('feed_font'); - var feed_size = NEWSBLUR.assets.preference('feed_size'); - var density = NEWSBLUR.assets.preference('density'); - + var feed_font = NEWSBLUR.assets.preference('feed_font'); + var feed_size = NEWSBLUR.assets.preference('feed_size'); + var density = NEWSBLUR.assets.preference('density'); + if (!this.flags.watching_system_theme && window.matchMedia) { var darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); try { // Chrome & Firefox - darkMediaQuery.addEventListener('change', _.bind(function(e) { + darkMediaQuery.addEventListener('change', _.bind(function (e) { console.log(['Chrome/FF switching themes', e]); this.load_theme(); }, this)); } catch (e1) { try { // Safari - darkMediaQuery.addListener(_.bind(function(e) { + darkMediaQuery.addListener(_.bind(function (e) { console.log(['Safari switching themes', e]); this.load_theme(); }, this)); @@ -3413,35 +3413,35 @@ } this.flags.watching_system_theme = true; } - + // Select theme options in manage menu on the dashboard $('.NB-theme-option').removeClass('NB-active'); - $('.NB-options-theme-'+auto_theme).addClass('NB-active'); + $('.NB-options-theme-' + auto_theme).addClass('NB-active'); $('.NB-feed-font-option').removeClass('NB-active'); - $('.NB-options-feed-font-'+feed_font).addClass('NB-active'); + $('.NB-options-feed-font-' + feed_font).addClass('NB-active'); $('.NB-feed-size-option').removeClass('NB-active'); - $('.NB-options-feed-size-'+feed_size).addClass('NB-active'); + $('.NB-options-feed-size-' + feed_size).addClass('NB-active'); $('.NB-density-option').removeClass('NB-active'); - $('.NB-options-density-'+density).addClass('NB-active'); - + $('.NB-options-density-' + density).addClass('NB-active'); + $("body").addClass('NB-theme-transitioning'); - + if (theme == 'dark') { $("body").addClass('NB-dark'); } else { $("body").removeClass('NB-dark'); } - _.delay(function() { + _.delay(function () { $("body").removeClass("NB-theme-transitioning"); }, 2000); }, - - close_interactions_popover: function() { + + close_interactions_popover: function () { NEWSBLUR.InteractionsPopover.close(); }, - - toggle_sidebar: function() { + + toggle_sidebar: function () { if (this.flags['sidebar_closed']) { this.open_sidebar(); return true; @@ -3450,30 +3450,30 @@ return false; } }, - - close_sidebar: function() { + + close_sidebar: function () { this.layout.outerLayout.hide('west'); this.resize_window(); this.flags['sidebar_closed'] = true; - NEWSBLUR.app.story_titles_header.watch_toggled_sidebar(); + NEWSBLUR.app.story_titles_header.watch_toggled_sidebar(); }, - - open_sidebar: function() { + + open_sidebar: function () { this.layout.outerLayout.open('west'); this.resize_window(); this.flags['sidebar_closed'] = false; NEWSBLUR.app.story_titles_header.watch_toggled_sidebar(); }, - - toggle_story_titles_pane: function(update_layout) { + + toggle_story_titles_pane: function (update_layout) { if (this.flags['story_titles_closed']) { this.open_story_titles_pane(update_layout === true); } else { this.close_story_titles_pane(update_layout === true); } }, - - close_story_titles_pane: function(update_layout) { + + close_story_titles_pane: function (update_layout) { var story_anchor = this.model.preference('story_pane_anchor'); if (update_layout) { NEWSBLUR.reader.layout.contentLayout.hide(story_anchor); @@ -3481,24 +3481,24 @@ this.resize_window(); this.flags['story_titles_closed'] = true; }, - - open_story_titles_pane: function(update_layout) { + + open_story_titles_pane: function (update_layout) { var story_anchor = this.model.preference('story_pane_anchor'); if (update_layout) { NEWSBLUR.reader.layout.contentLayout.open(story_anchor); } this.resize_window(); this.flags['story_titles_closed'] = false; - _.defer(function() { + _.defer(function () { NEWSBLUR.app.story_titles.scroll_to_selected_story(); }); }, - + // ======================= // = Sidebar Manage Menu = // ======================= - make_manage_menu: function(type, feed_id, story_id, inverse, $item) { + make_manage_menu: function (type, feed_id, story_id, inverse, $item) { var $manage_menu; // NEWSBLUR.log(["make_manage_menu", type, feed_id, story_id, inverse, $item]); @@ -3509,7 +3509,7 @@ $.make('div', { className: 'NB-menu-manage-image' }), $.make('span', { className: 'NB-menu-manage-title' }, "Manage NewsBlur") ]).corner('top 8px').corner('bottom 0px'), - $.make('li', { className: 'NB-menu-separator' }), + $.make('li', { className: 'NB-menu-separator' }), $.make('li', { className: 'NB-menu-item NB-menu-manage-mark-read NB-menu-manage-site-mark-read', role: "button" }, [ $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-title' }, 'Mark everything as read'), @@ -3536,7 +3536,7 @@ $.make('div', { className: 'NB-menu-manage-subtitle' }, 'Cleanup and rearrange feeds') ]), - $.make('li', { className: 'NB-menu-separator' }), + $.make('li', { className: 'NB-menu-separator' }), $.make('li', { className: 'NB-menu-item NB-menu-manage-keyboard', role: "button" }, [ $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-title' }, 'Keyboard shortcuts') @@ -3565,7 +3565,7 @@ $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-title' }, 'Import or upload sites') ]), - $.make('li', { className: 'NB-menu-separator' }), + $.make('li', { className: 'NB-menu-separator' }), $.make('li', { className: 'NB-menu-item NB-menu-manage-account', role: "button" }, [ $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-logout NB-modal-submit-green NB-modal-submit-button' }, 'Logout'), @@ -3583,12 +3583,12 @@ $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-title' }, 'Preferences') ]), - (show_chooser && $.make('li', { className: 'NB-menu-separator' })), + (show_chooser && $.make('li', { className: 'NB-menu-separator' })), (show_chooser && $.make('li', { className: 'NB-menu-item NB-menu-manage-premium', role: "button" }, [ $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-title' }, 'Upgrade to premium') ])), - $.make('li', { className: 'NB-menu-separator' }), + $.make('li', { className: 'NB-menu-separator' }), $.make('li', { className: 'NB-menu-item NB-menu-manage-font' }, [ $.make('div', { className: 'NB-menu-manage-image' }), $.make('ul', { className: 'segmented-control NB-options-feed-font' }, [ @@ -3710,7 +3710,7 @@ $.make('div', { className: 'NB-menu-manage-title' }, 'Change folders') ]), $.make('li', { className: 'NB-menu-subitem NB-menu-manage-confirm NB-menu-manage-feed-move-confirm NB-modal-submit', role: "button" }, [ - $.make('div', { className: 'NB-menu-manage-confirm-position'}, [ + $.make('div', { className: 'NB-menu-manage-confirm-position' }, [ $.make('div', { className: 'NB-change-folders' }) ]) ]), @@ -3727,7 +3727,7 @@ $.make('div', { className: 'NB-menu-manage-title' }, 'Rename this site') ]), $.make('li', { className: 'NB-menu-subitem NB-menu-manage-confirm NB-menu-manage-feed-rename-confirm NB-modal-submit', role: "button" }, [ - $.make('div', { className: 'NB-menu-manage-confirm-position'}, [ + $.make('div', { className: 'NB-menu-manage-confirm-position' }, [ $.make('div', { className: 'NB-menu-manage-rename-save NB-menu-manage-feed-rename-save NB-modal-submit-green NB-modal-submit-button' }, 'Save'), $.make('div', { className: 'NB-menu-manage-image' }), $.make('input', { name: 'new_title', className: 'NB-menu-manage-title NB-input', value: feed.get('feed_title') }) @@ -3843,7 +3843,7 @@ $.make('div', { className: 'NB-menu-manage-title' }, 'Move to folder') ]), $.make('li', { className: 'NB-menu-subitem NB-menu-manage-confirm NB-menu-manage-folder-move-confirm NB-modal-submit', role: "button" }, [ - $.make('div', { className: 'NB-menu-manage-confirm-position'}, [ + $.make('div', { className: 'NB-menu-manage-confirm-position' }, [ $.make('div', { className: 'NB-menu-manage-move-save NB-menu-manage-folder-move-save NB-modal-submit-green NB-modal-submit-button' }, 'Save'), $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-add-folders' }, NEWSBLUR.utils.make_folders()) @@ -3854,7 +3854,7 @@ $.make('div', { className: 'NB-menu-manage-title' }, 'Rename this folder') ]), $.make('li', { className: 'NB-menu-subitem NB-menu-manage-confirm NB-menu-manage-folder-rename-confirm NB-modal-submit' }, [ - $.make('div', { className: 'NB-menu-manage-confirm-position'}, [ + $.make('div', { className: 'NB-menu-manage-confirm-position' }, [ $.make('div', { className: 'NB-menu-manage-rename-save NB-menu-manage-folder-rename-save NB-modal-submit-green NB-modal-submit-button' }, 'Save'), $.make('div', { className: 'NB-menu-manage-image' }), $.make('input', { name: 'new_title', className: 'NB-menu-manage-title NB-input', value: feed_id }) @@ -3872,17 +3872,17 @@ $manage_menu.data('folder_name', feed_id); $manage_menu.data('$folder', $item); } else if (type == 'story') { - var feed = this.model.get_feed(feed_id); - var story = this.model.get_story(story_id); + var feed = this.model.get_feed(feed_id); + var story = this.model.get_story(story_id); var starred_class = story.get('starred') ? ' NB-story-starred ' : ''; var starred_title = story.get('starred') ? 'Unsave this story' : 'Save this story'; var shared_class = story.get('shared') ? ' NB-story-shared ' : ''; var shared_title = story.get('shared') ? 'Shared' : 'Share to your Blurblog'; - var order = NEWSBLUR.assets.view_setting(this.active_feed, 'order'); + var order = NEWSBLUR.assets.view_setting(this.active_feed, 'order'); story.story_share_menu_view = new NEWSBLUR.Views.StoryShareView({ model: story }); - + $manage_menu = $.make('ul', { className: 'NB-menu-manage NB-menu-manage-story ' + starred_class + shared_class }, [ $.make('li', { className: 'NB-menu-separator' }), $.make('li', { className: 'NB-menu-item NB-menu-manage-story-open', role: "button" }, [ @@ -3904,114 +3904,114 @@ $.make('div', { className: 'NB-menu-manage-title' }, starred_title) ]), $.make('li', { className: 'NB-menu-item NB-menu-manage-story-thirdparty' }, [ - (NEWSBLUR.Preferences['story_share_facebook'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-facebook'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_facebook'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-facebook' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Facebook').parent().addClass('NB-menu-manage-highlight-facebook'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-facebook'); }, this))), - (NEWSBLUR.Preferences['story_share_twitter'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-twitter'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_twitter'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-twitter' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Twitter').parent().addClass('NB-menu-manage-highlight-twitter'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-twitter'); }, this))), - (NEWSBLUR.Preferences['story_share_readitlater'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-readitlater'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_readitlater'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-readitlater' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Pocket (RIL)').parent().addClass('NB-menu-manage-highlight-readitlater'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-readitlater'); }, this))), - (NEWSBLUR.Preferences['story_share_tumblr'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-tumblr'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_tumblr'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-tumblr' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Tumblr').parent().addClass('NB-menu-manage-highlight-tumblr'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-tumblr'); }, this))), - (NEWSBLUR.Preferences['story_share_blogger'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-blogger'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_blogger'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-blogger' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Blogger').parent().addClass('NB-menu-manage-highlight-blogger'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-blogger'); }, this))), - (NEWSBLUR.Preferences['story_share_delicious'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-delicious'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_delicious'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-delicious' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Delicious').parent().addClass('NB-menu-manage-highlight-delicious'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-delicious'); }, this))), - (NEWSBLUR.Preferences['story_share_pinboard'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-pinboard'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_pinboard'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-pinboard' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Pinboard').parent().addClass('NB-menu-manage-highlight-pinboard'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-pinboard'); }, this))), - (NEWSBLUR.Preferences['story_share_raindrop'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-raindrop'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_raindrop'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-raindrop' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Raindrop.io').parent().addClass('NB-menu-manage-highlight-raindrop'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-raindrop'); }, this))), - (NEWSBLUR.Preferences['story_share_pinterest'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-pinterest'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_pinterest'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-pinterest' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Pinterest').parent().addClass('NB-menu-manage-highlight-pinterest'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-pinterest'); }, this))), - (NEWSBLUR.Preferences['story_share_buffer'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-buffer'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_buffer'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-buffer' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Buffer').parent().addClass('NB-menu-manage-highlight-buffer'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-buffer'); }, this))), - (NEWSBLUR.Preferences['story_share_diigo'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-diigo'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_diigo'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-diigo' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Diigo').parent().addClass('NB-menu-manage-highlight-diigo'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-diigo'); }, this))), - (NEWSBLUR.Preferences['story_share_evernote'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-evernote'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_evernote'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-evernote' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Evernote').parent().addClass('NB-menu-manage-highlight-evernote'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-evernote'); }, this))), - (NEWSBLUR.Preferences['story_share_googleplus'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-googleplus'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_googleplus'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-googleplus' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Google+').parent().addClass('NB-menu-manage-highlight-googleplus'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-googleplus'); }, this))), - (NEWSBLUR.Preferences['story_share_instapaper'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-instapaper'}).bind('mouseenter', _.bind(function(e) { + (NEWSBLUR.Preferences['story_share_instapaper'] && $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-instapaper' }).bind('mouseenter', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Instapaper').parent().addClass('NB-menu-manage-highlight-instapaper'); - }, this)).bind('mouseleave', _.bind(function(e) { + }, this)).bind('mouseleave', _.bind(function (e) { $(e.target).siblings('.NB-menu-manage-title').text('Email story').parent().removeClass('NB-menu-manage-highlight-instapaper'); }, this))), - $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-email', role: "button"}), + $.make('div', { className: 'NB-menu-manage-thirdparty-icon NB-menu-manage-thirdparty-email', role: "button" }), $.make('div', { className: 'NB-menu-manage-image' }), $.make('div', { className: 'NB-menu-manage-title' }, 'Email story') - ]).bind('click', _.bind(function(e) { - e.preventDefault(); - e.stopPropagation(); - var $target = $(e.target); - if ($target.hasClass('NB-menu-manage-thirdparty-facebook')) { - this.send_story_to_facebook(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-twitter')) { - this.send_story_to_twitter(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-readitlater')) { - this.send_story_to_readitlater(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-tumblr')) { - this.send_story_to_tumblr(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-blogger')) { - this.send_story_to_blogger(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-delicious')) { - this.send_story_to_delicious(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-pinboard')) { - this.send_story_to_pinboard(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-raindrop')) { - this.send_story_to_raindrop(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-pinterest')) { - this.send_story_to_pinterest(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-buffer')) { - this.send_story_to_buffer(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-diigo')) { - this.send_story_to_diigo(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-evernote')) { - this.send_story_to_evernote(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-googleplus')) { - this.send_story_to_googleplus(story.id); - } else if ($target.hasClass('NB-menu-manage-thirdparty-instapaper')) { - this.send_story_to_instapaper(story.id); - } else { - this.send_story_to_email(story); - } + ]).bind('click', _.bind(function (e) { + e.preventDefault(); + e.stopPropagation(); + var $target = $(e.target); + if ($target.hasClass('NB-menu-manage-thirdparty-facebook')) { + this.send_story_to_facebook(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-twitter')) { + this.send_story_to_twitter(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-readitlater')) { + this.send_story_to_readitlater(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-tumblr')) { + this.send_story_to_tumblr(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-blogger')) { + this.send_story_to_blogger(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-delicious')) { + this.send_story_to_delicious(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-pinboard')) { + this.send_story_to_pinboard(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-raindrop')) { + this.send_story_to_raindrop(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-pinterest')) { + this.send_story_to_pinterest(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-buffer')) { + this.send_story_to_buffer(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-diigo')) { + this.send_story_to_diigo(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-evernote')) { + this.send_story_to_evernote(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-googleplus')) { + this.send_story_to_googleplus(story.id); + } else if ($target.hasClass('NB-menu-manage-thirdparty-instapaper')) { + this.send_story_to_instapaper(story.id); + } else { + this.send_story_to_email(story); + } }, this)), $.make('li', { className: 'NB-menu-item NB-menu-manage-story NB-menu-manage-story-share', role: "button" }, [ $.make('div', { className: 'NB-menu-manage-image' }), @@ -4045,38 +4045,38 @@ $manage_menu.data('feed_id', feed_id); $manage_menu.data('story_id', story_id); $manage_menu.data('$story', $item); - + // this.update_share_button_label($('.NB-sideoption-share-comments', $manage_menu)); } - + return $manage_menu; }, - - show_manage_menu: function(type, $item, options) { + + show_manage_menu: function (type, $item, options) { var self = this; var options = _.extend({ - 'toplevel': false, - 'inverse': false + 'toplevel': false, + 'inverse': false }, options); var $manage_menu_container = $('.NB-menu-manage-container'); clearTimeout(this.flags.closed_manage_menu); this.flags['showing_confirm_input_on_manage_menu'] = false; - + // If another menu is open, hide it first. // If this menu is already open, then hide it instead. - if (($item && $item[0] == $manage_menu_container.data('item')) && + if (($item && $item[0] == $manage_menu_container.data('item')) && parseInt($manage_menu_container.css('opacity'), 10) == 1) { this.hide_manage_menu(type, $item); return; } else { this.hide_manage_menu(type, $item); } - + if ($item.hasClass('NB-empty')) return; - + $item.addClass('NB-showing-menu'); - + // Create menu, size and position it, then attach to the right place. var feed_id, inverse, story_id; if (type == 'folder') { @@ -4096,21 +4096,21 @@ inverse = options.inverse || $item.hasClass("NB-hover-inverse"); } else if (type == 'story') { story_id = options.story_id; - if ($item.hasClass('NB-hover-inverse')) inverse = true; + if ($item.hasClass('NB-hover-inverse')) inverse = true; } else if (type == 'site') { $('.NB-task-manage').tipsy('hide'); $('.NB-task-manage').tipsy('disable'); if (options.inverse) inverse = true; } var toplevel = options.toplevel || $item.hasClass("NB-toplevel") || - $item.children('.folder_title').hasClass("NB-toplevel"); + $item.children('.folder_title').hasClass("NB-toplevel"); var $manage_menu = this.make_manage_menu(type, feed_id, story_id, inverse, $item); $manage_menu_container.empty().append($manage_menu); $manage_menu_container.data('item', $item && $item[0]); $('.NB-task-manage').parents('.NB-taskbar').css('z-index', 2); if (type == 'site') { if (inverse) { - $('li', $manage_menu_container).each(function() { + $('li', $manage_menu_container).each(function () { $(this).prependTo($(this).parent()); }); $manage_menu_container.corner('bottom 8px').corner('top 0px'); @@ -4121,17 +4121,17 @@ if ($item.hasClass('NB-task-manage')) { $manage_menu_container.align($item, 'top -left', { - 'top': 0, + 'top': 0, 'left': -2 }); } else if (options.right) { $manage_menu_container.align($item, '-top -left', { - 'top': -34, + 'top': -34, 'left': 0 - }); + }); } else { $manage_menu_container.align($item, '-top left', { - 'top': -24, + 'top': -24, 'left': 20 }); } @@ -4140,8 +4140,8 @@ $manage_menu_container.css('z-index', $("#simplemodal-container").css('z-index')); } $('.NB-task-manage').addClass('NB-hover'); - } else if (type == 'feed' || type == 'folder' || type == 'story' || - type == 'socialfeed' || type == 'starred' || type == 'search') { + } else if (type == 'feed' || type == 'folder' || type == 'story' || + type == 'socialfeed' || type == 'starred' || type == 'search') { var left, top; // NEWSBLUR.log(['menu open', $item, inverse, toplevel, type]); if (inverse) { @@ -4163,14 +4163,14 @@ $align = $('.NB-storytitles-sentiment,.NB-feed-story-sentiment', $item); } } - + $manage_menu_container.align($align, 'top -left', { - 'top': -1 * top, + 'top': -1 * top, 'left': left }); $manage_menu_container.corner('br 8px').corner('bl top 0px'); - $('li', $manage_menu_container).each(function() { + $('li', $manage_menu_container).each(function () { $(this).prependTo($(this).parent()); }); } else { @@ -4195,16 +4195,16 @@ } } $manage_menu_container.align($align, '-bottom -left', { - 'top': top, + 'top': top, 'left': left }); $manage_menu_container.corner('tr 8px').corner('tl bottom 0px'); } } - $manage_menu_container.stop().css({'display': 'block', 'opacity': 1}); - + $manage_menu_container.stop().css({ 'display': 'block', 'opacity': 1 }); + // Create and position the arrow tab - if (type == 'feed' || type == 'folder' || type == 'story' || + if (type == 'feed' || type == 'folder' || type == 'story' || type == 'socialfeed' || type == 'starred' || type == 'search') { var $arrow = $.make('div', { className: 'NB-menu-manage-arrow' }, [ $.make('div', { className: 'NB-icon' }) @@ -4219,12 +4219,12 @@ $manage_menu_container.removeClass('NB-inverse'); } } - + // Hide menu on click outside menu. - _.defer(function() { - var close_menu_handler = function(e) { - _.defer(function() { - $(document).bind('click.menu', function(e) { + _.defer(function () { + var close_menu_handler = function (e) { + _.defer(function () { + $(document).bind('click.menu', function (e) { // console.log(['click outside menu']); self.hide_manage_menu(type, $item, false); }); @@ -4238,37 +4238,37 @@ close_menu_handler(); } }); - + // Hide menu on mouseout (on a delay). - $manage_menu_container.hover(function() { + $manage_menu_container.hover(function () { clearTimeout(self.flags.closed_manage_menu); - }, function() { + }, function () { clearTimeout(self.flags.closed_manage_menu); - self.flags.closed_manage_menu = setTimeout(function() { + self.flags.closed_manage_menu = setTimeout(function () { if (self.flags.closed_manage_menu) { self.hide_manage_menu(type, $item, true); } }, 1000); }); - + // Hide menu on esc. $(document).add($('input,textarea', $manage_menu_container)) .unbind('keydown.manage_menu') - .bind('keydown.manage_menu', 'esc', function(e) { - e.preventDefault(); - self.flags['showing_confirm_input_on_manage_menu'] = false; - self.hide_manage_menu(type, $item, true); - }); + .bind('keydown.manage_menu', 'esc', function (e) { + e.preventDefault(); + self.flags['showing_confirm_input_on_manage_menu'] = false; + self.hide_manage_menu(type, $item, true); + }); if (type == 'story') { - var share = _.bind(function(e) { + var share = _.bind(function (e) { e.preventDefault(); var story = NEWSBLUR.assets.get_story(story_id); - story.story_share_menu_view.mark_story_as_shared({'source': 'menu'}); + story.story_share_menu_view.mark_story_as_shared({ 'source': 'menu' }); }, this); $('.NB-sideoption-share-comments', $manage_menu_container).bind('keydown', 'ctrl+return', share); $('.NB-sideoption-share-comments', $manage_menu_container).bind('keydown', 'meta+return', share); } - + // Hide menu on scroll. var $scroll; this.flags['feed_list_showing_manage_menu'] = true; @@ -4277,7 +4277,7 @@ } else if (type == 'story') { $scroll = this.$s.$story_titles.add(this.$s.$feed_scroll); } - $scroll && $scroll.unbind('scroll.manage_menu').bind('scroll.manage_menu', function(e) { + $scroll && $scroll.unbind('scroll.manage_menu').bind('scroll.manage_menu', function (e) { if (self.flags['feed_list_showing_manage_menu']) { self.hide_manage_menu(type, $item, true); } else { @@ -4285,128 +4285,128 @@ } }); }, - - hide_manage_menu: function(type, $item, animate) { + + hide_manage_menu: function (type, $item, animate) { var $manage_menu_container = $('.NB-menu-manage-container'); var height = $manage_menu_container.outerHeight(); if (this.flags['showing_confirm_input_on_manage_menu'] && animate) return; // NEWSBLUR.log(['hide_manage_menu', type, $item, animate, $manage_menu_container.css('opacity')]); - + clearTimeout(this.flags.closed_manage_menu); this.flags['feed_list_showing_manage_menu'] = false; $(document).unbind('click.menu'); $(document).unbind('mouseup.menu'); $(document).add($('input,textarea', $manage_menu_container)) - .unbind('keydown.manage_menu'); + .unbind('keydown.manage_menu'); if (this.model.preference('show_tooltips')) { $('.NB-task-manage').tipsy('enable'); } - + if ($item) $item.removeClass('NB-showing-menu'); - + if (animate) { $manage_menu_container.stop().animate({ 'opacity': 0 }, { - 'duration': 250, + 'duration': 250, 'queue': false, - 'complete': function() { - $manage_menu_container.css({'display': 'none', 'opacity': 0}); + 'complete': function () { + $manage_menu_container.css({ 'display': 'none', 'opacity': 0 }); } }); } else { - $manage_menu_container.css({'display': 'none', 'opacity': 0}); + $manage_menu_container.css({ 'display': 'none', 'opacity': 0 }); } $('.NB-task-manage').removeClass('NB-hover'); - - this.blur_to_page({manage_menu: true}); + + this.blur_to_page({ manage_menu: true }); }, - + // ======================== // = Manage menu - Delete = // ======================== - - show_confirm_delete_menu_item: function() { + + show_confirm_delete_menu_item: function () { var $delete = $('.NB-menu-manage-feed-delete,.NB-menu-manage-folder-delete'); var $confirm = $('.NB-menu-manage-feed-delete-confirm,.NB-menu-manage-folder-delete-confirm'); - + $delete.addClass('NB-menu-manage-feed-delete-cancel'); $('.NB-menu-manage-title', $delete).text('Cancel delete'); $confirm.slideDown(500); }, - - hide_confirm_delete_menu_item: function() { + + hide_confirm_delete_menu_item: function () { var $delete = $('.NB-menu-manage-feed-delete,.NB-menu-manage-folder-delete'); var $confirm = $('.NB-menu-manage-feed-delete-confirm,.NB-menu-manage-folder-delete-confirm'); - + $delete.removeClass('NB-menu-manage-feed-delete-cancel'); var text = $delete.hasClass('NB-menu-manage-folder-delete') ? - 'Delete this folder' : - 'Delete this site'; + 'Delete this folder' : + 'Delete this site'; $('.NB-menu-manage-title', $delete).text(text); $confirm.slideUp(500); }, - - manage_menu_delete_feed: function(feed_id, $feed) { + + manage_menu_delete_feed: function (feed_id, $feed) { var self = this; feed_id = feed_id || this.active_feed; var feed = this.model.get_feed(feed_id); var feed_view = feed.get_view($feed); - feed.delete_feed({view: feed_view}); + feed.delete_feed({ view: feed_view }); }, - manage_menu_delete_search: function(search_model_id, $feed) { + manage_menu_delete_search: function (search_model_id, $feed) { var search_model = NEWSBLUR.assets.get_search_feeds(search_model_id); - NEWSBLUR.assets.delete_saved_search(search_model.get('feed_id'), search_model.get('query'), _.bind(function(e) { + NEWSBLUR.assets.delete_saved_search(search_model.get('feed_id'), search_model.get('query'), _.bind(function (e) { console.log(['Saved searches', e]); }, this)); - + }, - - show_confirm_unfollow_menu_item: function() { + + show_confirm_unfollow_menu_item: function () { var $unfollow = $('.NB-menu-manage-socialfeed-delete'); var $confirm = $('.NB-menu-manage-socialfeed-delete-confirm'); - + $unfollow.addClass('NB-menu-manage-socialfeed-delete-cancel'); $('.NB-menu-manage-title', $unfollow).text('Cancel unfollow'); $confirm.slideDown(500); }, - - hide_confirm_unfollow_menu_item: function() { + + hide_confirm_unfollow_menu_item: function () { var $unfollow = $('.NB-menu-manage-socialfeed-delete,.NB-menu-manage-folder-delete'); var $confirm = $('.NB-menu-manage-socialfeed-delete-confirm,.NB-menu-manage-folder-delete-confirm'); - + $unfollow.removeClass('NB-menu-manage-socialfeed-delete-cancel'); $('.NB-menu-manage-title', $unfollow).text('Unfollow'); $confirm.slideUp(500); }, - - manage_menu_unfollow_feed: function(feed, $feed) { + + manage_menu_unfollow_feed: function (feed, $feed) { var self = this; var feed_id = feed || this.active_feed; - - this.model.unfollow_user(feed_id, function() { + + this.model.unfollow_user(feed_id, function () { NEWSBLUR.app.feed_list.make_social_feeds(); }); }, - - - manage_menu_delete_folder: function(folder_title, $folder) { + + + manage_menu_delete_folder: function (folder_title, $folder) { var self = this; var folder_view = NEWSBLUR.assets.folders.get_view($folder) || - this.active_folder.folder_view; - + this.active_folder.folder_view; + folder_view.model.delete_folder(); }, - + // ======================== // = Manage menu - Move = // ======================== - - show_confirm_move_menu_item: function(feed_id, $feed) { + + show_confirm_move_menu_item: function (feed_id, $feed) { var self = this; var $move = $('.NB-menu-manage-feed-move,.NB-menu-manage-folder-move'); var $confirm = $('.NB-menu-manage-feed-move-confirm,.NB-menu-manage-folder-move-confirm'); @@ -4415,38 +4415,38 @@ var $save = $(".NB-menu-manage-feed-move-save"); var $select = $('select', $confirm); var isFeed = _.isNumber(feed_id); - + if (isFeed) { - var feed = this.model.get_feed(feed_id); + var feed = this.model.get_feed(feed_id); var feed_view = feed.get_view($feed, true); var in_folder = feed_view.options.folder_title; - feed.set('menu_folders', null, {silent: true}); + feed.set('menu_folders', null, { silent: true }); var $folders = this.make_folders_multiselect(feed); $add.html($folders); $save.addClass("NB-disabled").attr('disabled', "disabled").text('Select folders'); } else { var folder_view = NEWSBLUR.assets.folders.get_view($feed) || - this.active_folder.folder_view; + this.active_folder.folder_view; var in_folder = folder_view.collection.options.title; } - + $move.addClass('NB-menu-manage-feed-move-cancel'); $('.NB-menu-manage-title', $move).text('Cancel'); $position.css('position', 'relative'); var height = $confirm.height(); $position.css('position', 'absolute'); - $confirm.css({'height': 0, 'display': 'block'}).animate({'height': height}, { - 'duration': 380, + $confirm.css({ 'height': 0, 'display': 'block' }).animate({ 'height': height }, { + 'duration': 380, 'easing': 'easeOutQuart' }); if (isFeed) { $save.fadeIn(380); } this.flags['showing_confirm_input_on_manage_menu'] = true; - + if (!_.isNumber(feed_id)) { $('select', $confirm).focus().select(); - $('option', $select).each(function() { + $('option', $select).each(function () { if ($(this).attr('value') == in_folder) { $(this).prop('selected', 'selected'); return false; @@ -4454,50 +4454,50 @@ }); } }, - - make_folders_multiselect: function(feed, in_folders) { + + make_folders_multiselect: function (feed, in_folders) { var folders = NEWSBLUR.assets.get_folders(); if (!in_folders) in_folders = feed.in_folders(); in_folders = _.unique(in_folders.concat(feed.get('menu_folders') || [])); - feed.set('menu_folders', in_folders, {silent: true}); + feed.set('menu_folders', in_folders, { silent: true }); var $options = $.make('div', { className: 'NB-folders' }); - var $option = this.make_folder_selectable('Top Level', '', 0, _.any(in_folders, function(folder) { + var $option = this.make_folder_selectable('Top Level', '', 0, _.any(in_folders, function (folder) { return !folder; })); $options.append($option); - + $options = this.make_folders_multiselect_options($options, folders, 1, in_folders); return $options; }, - - make_folders_multiselect_options: function($options, items, depth, in_folders) { + + make_folders_multiselect_options: function ($options, items, depth, in_folders) { var self = this; - items.each(function(item) { + items.each(function (item) { if (item.is_folder()) { var title = item.get('folder_title'); var $option = self.make_folder_selectable(title, title, depth, _.contains(in_folders, title)); $options.append($option); - $options = self.make_folders_multiselect_options($options, item.folders, depth+1, in_folders); + $options = self.make_folders_multiselect_options($options, item.folders, depth + 1, in_folders); } }); - + return $options; }, - - make_folder_selectable: function(folder_title, folder_value, depth, selected) { - return $.make('div', { + + make_folder_selectable: function (folder_title, folder_value, depth, selected) { + return $.make('div', { className: "NB-folder-option " + (selected ? "NB-folder-option-active" : ""), - style: 'padding-left: ' + depth*12 + 'px;' + style: 'padding-left: ' + depth * 12 + 'px;' }, [ $.make('div', { className: 'NB-icon-add' }), $.make('div', { className: 'NB-icon' }), $.make('div', { className: 'NB-folder-option-title' }, folder_title) ]).data('folder', folder_value); }, - - switch_change_folder: function(feed_id, folder_value) { - var feed = this.model.get_feed(feed_id); + + switch_change_folder: function (feed_id, folder_value) { + var feed = this.model.get_feed(feed_id); var in_folders = feed.get('menu_folders'); if (_.contains(in_folders, folder_value)) { @@ -4506,43 +4506,43 @@ in_folders = in_folders.concat(folder_value); } - feed.set('menu_folders', in_folders, {silent: true}); - + feed.set('menu_folders', in_folders, { silent: true }); + this.render_change_folders(feed, in_folders); }, - - render_change_folders: function(feed, in_folders) { + + render_change_folders: function (feed, in_folders) { var $confirm = $('.NB-menu-manage-feed-move-confirm,.NB-menu-manage-folder-move-confirm'); var $add = $(".NB-add-folders,.NB-change-folders", $confirm); var $save = $(".NB-menu-manage-feed-move-save"); var $folders = this.make_folders_multiselect(feed, in_folders); $add.html($folders); - + if (_.isEqual(in_folders, feed.in_folders())) { $save.addClass("NB-disabled").attr('disabled', "disabled").text('Select folders'); } else { $save.toggleClass("NB-disabled", !in_folders.length) - .attr('disabled', !in_folders.length ? "disabled" : false); + .attr('disabled', !in_folders.length ? "disabled" : false); } - + if (!in_folders.length) { $save.text('Select a folder'); } else { $save.text("Save " + Inflector.pluralize(' folder', in_folders.length, true)); } }, - - show_add_folder_in_menu: function(feed_id, $folder, folder) { + + show_add_folder_in_menu: function (feed_id, $folder, folder) { var self = this; - + if ($folder.siblings('.NB-add-folder-form').length) { - var feed = this.model.get_feed(feed_id); + var feed = this.model.get_feed(feed_id); var in_folders = feed.get('menu_folders'); this.render_change_folders(feed, in_folders); return; } - + var $add = $.make('div', { className: 'NB-add-folder-form' }, [ $.make('div', { className: 'NB-icon' }), $.make('input', { className: 'NB-input', placeholder: "New folder name..." }), @@ -4550,81 +4550,81 @@ ]).data('in_folder', $folder.data('folder')).data('feed_id', feed_id); $add.css('paddingLeft', parseInt($folder.css('paddingLeft'), 10) + 12); $folder.after($add); - - $('input', $add).focus().bind('keyup', 'return', function(e) { + + $('input', $add).focus().bind('keyup', 'return', function (e) { self.add_folder_to_folder(); - }).bind('keyup', 'esc', function(e) { - var feed = self.model.get_feed(feed_id); + }).bind('keyup', 'esc', function (e) { + var feed = self.model.get_feed(feed_id); var in_folders = feed.get('menu_folders'); self.render_change_folders(feed, in_folders); }); }, - - add_folder_to_folder: function() { + + add_folder_to_folder: function () { var $form = $('.NB-add-folder-form'); var folder_name = $('.NB-input', $form).val(); var parent_folder = $form.data('in_folder'); this.model.save_add_folder(folder_name, parent_folder, - $.rescope(this.post_add_folder_to_folder, this)); + $.rescope(this.post_add_folder_to_folder, this)); }, - - post_add_folder_to_folder: function(e, data) { + + post_add_folder_to_folder: function (e, data) { if (data.folders) { - NEWSBLUR.assets.folders.reset(_.compact(data.folders), {parse: true}); + NEWSBLUR.assets.folders.reset(_.compact(data.folders), { parse: true }); } - + var $form = $('.NB-add-folder-form'); - var feed_id = $form.data('feed_id'); - var feed = this.model.get_feed(feed_id); + var feed_id = $form.data('feed_id'); + var feed = this.model.get_feed(feed_id); var in_folders = feed.get('menu_folders'); NEWSBLUR.assets.feeds.trigger('reset'); this.render_change_folders(feed, in_folders); }, - - manage_menu_move_feed: function(feed_id, $feed) { - var self = this; - var feed_id = feed_id || this.active_feed; - var feed = this.model.get_feed(feed_id); + + manage_menu_move_feed: function (feed_id, $feed) { + var self = this; + var feed_id = feed_id || this.active_feed; + var feed = this.model.get_feed(feed_id); var in_folders = feed.get('menu_folders'); - var feed_view = feed.get_view($feed); + var feed_view = feed.get_view($feed); - var moved = feed.move_to_folders(in_folders, {view: feed_view}); + var moved = feed.move_to_folders(in_folders, { view: feed_view }); this.hide_confirm_move_menu_item(moved); if (moved) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.hide_manage_menu('feed', $feed, true); }, this), 500); } }, - - manage_menu_move_folder: function(folder, $folder) { - var self = this; - var to_folder = $('.NB-menu-manage-folder-move-confirm select').val(); - var folder_view = NEWSBLUR.assets.folders.get_view($folder) || - this.active_folder.folder_view; - var in_folder = folder_view.collection.options.title; + + manage_menu_move_folder: function (folder, $folder) { + var self = this; + var to_folder = $('.NB-menu-manage-folder-move-confirm select').val(); + var folder_view = NEWSBLUR.assets.folders.get_view($folder) || + this.active_folder.folder_view; + var in_folder = folder_view.collection.options.title; var folder_name = folder_view.options.folder_title; var child_folders = folder_view.collection.child_folder_names(); - - if (to_folder == in_folder || + + if (to_folder == in_folder || to_folder == folder_name || - _.contains(child_folders, to_folder)) { + _.contains(child_folders, to_folder)) { return this.hide_confirm_move_menu_item(); } - - var moved = folder_view.model.move_to_folder(to_folder, {view: folder_view}); + + var moved = folder_view.model.move_to_folder(to_folder, { view: folder_view }); this.hide_confirm_move_menu_item(moved); if (moved) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.hide_manage_menu('folder', $folder, true); }, this), 500); } }, - - hide_confirm_move_menu_item: function(moved) { + + hide_confirm_move_menu_item: function (moved) { var $move_folder = $('.NB-menu-manage-folder-move'); var $move_feed = $('.NB-menu-manage-feed-move'); var $confirm_folder = $('.NB-menu-manage-folder-move-confirm'); @@ -4651,9 +4651,9 @@ $save.hide(); this.flags['showing_confirm_input_on_manage_menu'] = false; }, - - manage_menu_mute_feed: function(feed_id, unmute) { - var approve_list = _.pluck(NEWSBLUR.assets.feeds.filter(function(feed) { + + manage_menu_mute_feed: function (feed_id, unmute) { + var approve_list = _.pluck(NEWSBLUR.assets.feeds.filter(function (feed) { if (unmute) { return feed.get('active') || feed.get('id') == feed_id; } @@ -4663,7 +4663,7 @@ console.log(["Saving", approve_list, feed_id]); NEWSBLUR.reader.flags['reloading_feeds'] = true; - this.model.save_feed_chooser(approve_list, _.bind(function() { + this.model.save_feed_chooser(approve_list, _.bind(function () { this.flags['has_saved'] = true; NEWSBLUR.reader.flags['reloading_feeds'] = false; NEWSBLUR.reader.hide_feed_chooser_button(); @@ -4671,50 +4671,50 @@ this.hide_manage_menu(); }, this)); }, - + // ======================== // = Manage menu - Rename = // ======================== - - show_confirm_rename_menu_item: function() { + + show_confirm_rename_menu_item: function () { var self = this; var $rename = $('.NB-menu-manage-feed-rename,.NB-menu-manage-folder-rename'); var $confirm = $('.NB-menu-manage-feed-rename-confirm,.NB-menu-manage-folder-rename-confirm'); var $position = $('.NB-menu-manage-confirm-position', $confirm); - + $rename.addClass('NB-menu-manage-feed-rename-cancel'); $('.NB-menu-manage-title', $rename).text('Cancel rename'); $position.css('position', 'relative'); var height = $confirm.height(); $position.css('position', 'absolute'); - $confirm.css({'height': 0, 'display': 'block'}).animate({'height': height}, { - 'duration': 380, + $confirm.css({ 'height': 0, 'display': 'block' }).animate({ 'height': height }, { + 'duration': 380, 'easing': 'easeOutQuart' }); $('input', $confirm).focus().select(); this.flags['showing_confirm_input_on_manage_menu'] = true; - $('.NB-menu-manage-feed-rename-confirm input.NB-menu-manage-title').bind('keyup', 'return', function(e) { + $('.NB-menu-manage-feed-rename-confirm input.NB-menu-manage-title').bind('keyup', 'return', function (e) { var $t = $(e.target); var feed_id = $t.closest('.NB-menu-manage').data('feed_id'); var $feed = $t.closest('.NB-menu-manage').data('$feed'); self.manage_menu_rename_feed(feed_id, $feed); }); - $('.NB-menu-manage-folder-rename-confirm input.NB-menu-manage-title').bind('keyup', 'return', function(e) { + $('.NB-menu-manage-folder-rename-confirm input.NB-menu-manage-title').bind('keyup', 'return', function (e) { var $t = $(e.target); var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); self.manage_menu_rename_folder(folder_name, $folder); }); }, - - hide_confirm_rename_menu_item: function(renamed) { + + hide_confirm_rename_menu_item: function (renamed) { var $rename = $('.NB-menu-manage-feed-rename,.NB-menu-manage-folder-rename'); var $confirm = $('.NB-menu-manage-feed-rename-confirm,.NB-menu-manage-folder-rename-confirm'); - + $rename.removeClass('NB-menu-manage-feed-rename-cancel'); var text = $rename.hasClass('NB-menu-manage-folder-rename') ? - 'Rename this folder' : - 'Rename this site'; + 'Rename this folder' : + 'Rename this site'; if (renamed) { text = 'Renamed'; $rename.addClass('NB-active'); @@ -4725,31 +4725,31 @@ $confirm.slideUp(500); this.flags['showing_confirm_input_on_manage_menu'] = false; }, - - manage_menu_rename_feed: function(feed_id) { - var feed_id = feed_id || this.active_feed; + + manage_menu_rename_feed: function (feed_id) { + var feed_id = feed_id || this.active_feed; var feed = this.model.get_feed(feed_id); var new_title = $('.NB-menu-manage-feed-rename-confirm .NB-menu-manage-title').val(); - + if (new_title.length > 0) feed.rename(new_title); this.hide_confirm_rename_menu_item(true); }, - - manage_menu_rename_folder: function(folder, $folder) { - var self = this; + + manage_menu_rename_folder: function (folder, $folder) { + var self = this; var new_folder_name = $('.NB-menu-manage-folder-rename-confirm .NB-menu-manage-title').val(); var folder_view = NEWSBLUR.assets.folders.get_view($folder) || - this.active_folder.folder_view; - + this.active_folder.folder_view; + if (new_folder_name.length > 0) folder_view.model.rename(new_folder_name); this.hide_confirm_rename_menu_item(true); }, - + // ============================= // = Manage Menu - Share Story = // ============================= - - show_confirm_story_share_menu_item: function(story_id) { + + show_confirm_story_share_menu_item: function (story_id) { var self = this; if (!story_id) story_id = $('.NB-menu-manage').data('story_id'); var story = NEWSBLUR.assets.get_story(story_id); @@ -4757,28 +4757,28 @@ var $confirm = $('.NB-menu-manage-story-share-confirm'); var $story_share = story.story_share_menu_view.$el; var $position = $('.NB-menu-manage-confirm-position', $confirm); - + $share.addClass('NB-menu-manage-story-share-cancel'); $('.NB-menu-manage-title', $share).text('Cancel share'); - $confirm.css({'height': 0, 'display': 'block'}); - story.story_share_menu_view.toggle_feed_story_share_dialog({immediate: true}); + $confirm.css({ 'height': 0, 'display': 'block' }); + story.story_share_menu_view.toggle_feed_story_share_dialog({ immediate: true }); $position.css('position', 'relative'); var height = $story_share.height(); $position.css('position', 'absolute'); - $confirm.css({'height': 0, 'display': 'block'}).animate({'height': height}, { - 'duration': 380, + $confirm.css({ 'height': 0, 'display': 'block' }).animate({ 'height': height }, { + 'duration': 380, 'easing': 'easeOutQuart' }); $('textarea', $confirm).focus().select(); this.flags['showing_confirm_input_on_manage_menu'] = true; }, - - hide_confirm_story_share_menu_item: function(shared) { + + hide_confirm_story_share_menu_item: function (shared) { var story_id = $('.NB-menu-manage-container .NB-menu-manage').data('story_id'); var story = NEWSBLUR.assets.get_story(story_id); var $share = $('.NB-menu-manage-story-share'); var $confirm = $('.NB-menu-manage-story-share-confirm'); - + $share.removeClass('NB-menu-manage-story-share-cancel'); var text = 'Share to your Blurblog'; if (shared) { @@ -4788,15 +4788,15 @@ $share.removeClass('NB-active'); } $('.NB-menu-manage-title', $share).text(text); - $confirm.slideUp(500, _.bind(function() { + $confirm.slideUp(500, _.bind(function () { if (shared && story) { this.hide_manage_menu('story', story.story_title_view.$el, true); } }, this)); this.flags['showing_confirm_input_on_manage_menu'] = false; - + }, - + // ==================== // = Dashboard Rivers = // ==================== @@ -4816,10 +4816,10 @@ }; }, - choose_dashboard_rivers: function() { + choose_dashboard_rivers: function () { var existing_rivers = NEWSBLUR.assets.dashboard_rivers; var sides = existing_rivers.count_sides(); - + if (!existing_rivers.contains(function (river) { river.get('river_id') == "river:"; })) { @@ -4829,32 +4829,32 @@ this.load_dashboard_rivers(); }, this), function (e) { console.log(['Error saving dashboard river', e]); - }); + }); }, this), function (e) { console.log(['Error saving dashboard river', e]); - }); + }); }, this), function (e) { - console.log(['Error saving dashboard river', e]); + console.log(['Error saving dashboard river', e]); }); - } + } }, // ========================== // = Taskbar - Intelligence = // ========================== - - load_intelligence_slider: function() { + + load_intelligence_slider: function () { var self = this; var $slider = this.$s.$intelligence_slider; var unread_view = this.get_unread_view_score(); - + if (unread_view == 0 && !NEWSBLUR.assets.preference('hide_read_feeds')) { unread_view = -1; } this.slide_intelligence_slider(unread_view, true); }, - - toggle_focus_in_slider: function() { + + toggle_focus_in_slider: function () { var $slider = this.$s.$intelligence_slider; var $focus = $(".NB-intelligence-slider-green", $slider); var $unread = $(".NB-intelligence-slider-yellow", $slider); @@ -4862,24 +4862,24 @@ var all_mode = !NEWSBLUR.assets.preference('hide_read_feeds'); var starred_mode = this.flags['feed_list_showing_starred']; if (!NEWSBLUR.assets.feeds.size()) return; - + var view_not_empty; if (unread_view == 'starred') { - view_not_empty = NEWSBLUR.assets.starred_feeds.any(function(feed) { + view_not_empty = NEWSBLUR.assets.starred_feeds.any(function (feed) { return feed.get('count'); }); } else if (unread_view == 'positive') { - view_not_empty = NEWSBLUR.assets.feeds.any(function(feed) { + view_not_empty = NEWSBLUR.assets.feeds.any(function (feed) { return feed.get('ps'); - }) || NEWSBLUR.assets.social_feeds.any(function(feed) { + }) || NEWSBLUR.assets.social_feeds.any(function (feed) { return feed.get('ps'); }); } else { - view_not_empty = NEWSBLUR.assets.feeds.any(function(feed) { + view_not_empty = NEWSBLUR.assets.feeds.any(function (feed) { return feed.get('ps') || feed.get('nt'); - }) || NEWSBLUR.assets.social_feeds.any(function(feed) { + }) || NEWSBLUR.assets.social_feeds.any(function (feed) { return feed.get('ps') || feed.get('nt'); - }); + }); } $(".NB-feeds-list-empty").remove(); // console.log(["toggle_focus_in_slider", unread_view, view_not_empty, starred_mode]); @@ -4912,11 +4912,11 @@ // this.model.preference('lock_green_slider', true); // } }, - - slide_intelligence_slider: function(value, initial_load) { + + slide_intelligence_slider: function (value, initial_load) { var $slider = this.$s.$intelligence_slider; var real_value = value; - + var showing_starred = this.flags['feed_list_showing_starred']; this.flags['feed_list_showing_starred'] = value == 2; @@ -4947,7 +4947,7 @@ if (NEWSBLUR.app.story_titles_header && this.model.active_feed) { NEWSBLUR.app.story_titles_header.show_feed_hidden_story_title_indicator(true); } - this.show_story_titles_above_intelligence_level({'animate': true, 'follow': true}); + this.show_story_titles_above_intelligence_level({ 'animate': true, 'follow': true }); this.toggle_focus_in_slider(); if (!initial_load && this.flags['feed_list_showing_starred'] != showing_starred) { this.reload_feed(); @@ -4972,62 +4972,62 @@ $('.NB-intelligence-slider-yellow', $slider).addClass('NB-active'); } }, - - move_intelligence_slider: function(direction) { + + move_intelligence_slider: function (direction) { var unread_view = this.model.preference('unread_view'); if (!this.model.preference('hide_read_feeds')) unread_view = -1; var value = unread_view + direction; this.slide_intelligence_slider(value); }, - - toggle_read_filter: function() { + + toggle_read_filter: function () { var read_filter = NEWSBLUR.assets.view_setting(this.active_feed, 'read_filter'); var setting = { 'read_filter': (read_filter == 'unread' ? 'all' : 'unread') }; var changed = NEWSBLUR.assets.view_setting(this.active_feed, setting); if (!changed) return; - + NEWSBLUR.reader.reload_feed(setting); }, - - switch_feed_view_unread_view: function(unread_view) { + + switch_feed_view_unread_view: function (unread_view) { if (!_.isNumber(unread_view)) unread_view = this.get_unread_view_score(); - var $sidebar = this.$s.$sidebar; - var unread_view_name = this.get_unread_view_name(unread_view); - var $next_story_button = $('.NB-task-story-next-unread'); + var $sidebar = this.$s.$sidebar; + var unread_view_name = this.get_unread_view_name(unread_view); + var $next_story_button = $('.NB-task-story-next-unread'); var $story_title_indicator = $('.NB-story-title-indicator', this.$story_titles); this.$s.$body.removeClass('NB-intelligence-positive') - .removeClass('NB-intelligence-neutral') - .removeClass('NB-intelligence-negative') - .removeClass('NB-intelligence-starred') - .addClass('NB-intelligence-'+unread_view_name); - + .removeClass('NB-intelligence-neutral') + .removeClass('NB-intelligence-negative') + .removeClass('NB-intelligence-starred') + .addClass('NB-intelligence-' + unread_view_name); + $sidebar.removeClass('unread_view_positive') - .removeClass('unread_view_neutral') - .removeClass('unread_view_negative') - .removeClass('unread_view_starred') - .addClass('unread_view_'+unread_view_name); + .removeClass('unread_view_neutral') + .removeClass('unread_view_negative') + .removeClass('unread_view_starred') + .addClass('unread_view_' + unread_view_name); $next_story_button.removeClass('NB-task-story-next-positive') - .removeClass('NB-task-story-next-neutral') - .removeClass('NB-task-story-next-negative') - .removeClass('NB-task-story-next-starred') - .addClass('NB-task-story-next-'+unread_view_name); - + .removeClass('NB-task-story-next-neutral') + .removeClass('NB-task-story-next-negative') + .removeClass('NB-task-story-next-starred') + .addClass('NB-task-story-next-' + unread_view_name); + $story_title_indicator.removeClass('unread_threshold_positive') - .removeClass('unread_threshold_neutral') - .removeClass('unread_threshold_negative') - .removeClass('unread_threshold_starred') - .addClass('unread_threshold_'+unread_view_name); - - NEWSBLUR.assets.stories.each(function(story){ - story.unset('visible'); + .removeClass('unread_threshold_neutral') + .removeClass('unread_threshold_negative') + .removeClass('unread_threshold_starred') + .addClass('unread_threshold_' + unread_view_name); + + NEWSBLUR.assets.stories.each(function (story) { + story.unset('visible'); }); }, - - get_unread_view_score: function(ignore_temp) { + + get_unread_view_score: function (ignore_temp) { if (this.flags['feed_list_showing_starred']) return -1; if (this.flags['unread_threshold_temporarily'] && !ignore_temp) { var score_name = this.flags['unread_threshold_temporarily']; @@ -5037,33 +5037,33 @@ return -1; } } - + return this.model.preference('unread_view'); }, - - get_unread_view_name: function(unread_view, ignore_temp) { + + get_unread_view_name: function (unread_view, ignore_temp) { if (this.flags['unread_threshold_temporarily'] && !ignore_temp) { return this.flags['unread_threshold_temporarily']; } - + if (typeof unread_view == 'undefined' || unread_view === null) { unread_view = this.get_unread_view_score(ignore_temp); } - + if (this.flags['feed_list_showing_starred']) return 'starred'; - + return (unread_view > 0 - ? 'positive' - : unread_view < 0 - ? 'negative' - : 'neutral'); + ? 'positive' + : unread_view < 0 + ? 'negative' + : 'neutral'); }, - - get_unread_count: function(feed_id) { + + get_unread_count: function (feed_id) { var total = 0; feed_id = feed_id || this.active_feed; var feed = this.model.get_feed(feed_id); - + if (_.contains(['starred', 'read'], feed_id)) { // Umm, no. Not yet. } else if (feed && feed.unread_counts) { @@ -5080,11 +5080,11 @@ } else if (this.flags['river_view'] && this.flags['social_view']) { return NEWSBLUR.assets.social_feeds.unread_counts(); } - + return {}; }, - - get_total_unread_count: function(feed_id) { + + get_total_unread_count: function (feed_id) { var counts = this.get_unread_count(feed_id); var unread_view_name = this.get_unread_view_name(); @@ -5098,8 +5098,8 @@ return counts['st']; } }, - - show_story_titles_above_intelligence_level: function(opts) { + + show_story_titles_above_intelligence_level: function (opts) { var defaults = { 'unread_view_name': null, 'animate': true, @@ -5110,19 +5110,19 @@ var self = this; var $story_titles = this.$s.$story_titles; var unread_view_name = options['unread_view_name'] || this.get_unread_view_name(); - + if (this.model.stories.length > 18) { options['animate'] = false; } - + if (this.flags['unread_threshold_temporarily']) { options['temporary'] = true; } - + NEWSBLUR.assets.stories.trigger('render:intelligence', options); - + if (!NEWSBLUR.assets.preference('feed_view_single_story')) { - _.delay(function() { + _.delay(function () { NEWSBLUR.app.story_list.reset_story_positions(); }, 500); } @@ -5142,12 +5142,12 @@ NEWSBLUR.app.story_titles.scroll_to_selected_story(self.active_story); } }, - + // =================== // = Feed Refreshing = // =================== - - force_instafetch_stories: function(feed_id) { + + force_instafetch_stories: function (feed_id) { var self = this; feed_id = feed_id || this.active_feed; var feed = this.model.get_feed(feed_id); @@ -5158,10 +5158,10 @@ }); this.model.save_exception_retry(feed_id, _.bind(this.force_feed_refresh, this, feed_id), - NEWSBLUR.app.taskbar_info.show_stories_error); + NEWSBLUR.app.taskbar_info.show_stories_error); }, - - setup_socket_realtime_unread_counts: function(force) { + + setup_socket_realtime_unread_counts: function (force) { if (!force && NEWSBLUR.Globals.is_anonymous) return; // if (!force && !NEWSBLUR.Globals.is_premium) return; if (this.socket && !this.socket.connected) { @@ -5169,7 +5169,7 @@ } else if (force || !this.socket || !this.socket.connected) { var server = window.location.protocol + '//' + window.location.hostname; var https = _.string.startsWith(window.location.protocol, 'https'); - var local = false && _.any(['nb.local.com'], function(hostname) { + var local = false && _.any(['nb.local.com'], function (hostname) { return _.string.contains(window.location.host, hostname); }); var port = https ? 443 : 80; @@ -5183,15 +5183,15 @@ "transports": ['websocket'], "upgrade": false }); - + // this.socket.refresh_feeds = _.debounce(_.bind(this.force_feeds_refresh, this), 1000*10); - this.socket.on('connect', _.bind(function() { + this.socket.on('connect', _.bind(function () { var active_feeds = this.send_socket_active_feeds(); NEWSBLUR.log(["Connected to real-time pubsub with " + active_feeds.length + " feeds."]); this.flags.feed_refreshing_in_realtime = true; this.setup_feed_refresh(); NEWSBLUR.assets.stories.retry_failed_marked_read_stories(); - + // $('.NB-module-content-account-realtime-subtitle').html($.make('b', 'Updating in real-time')); $('.NB-module-content-account-realtime').attr('title', 'Updating sites in real-time...').removeClass('NB-error').addClass('NB-active'); this.apply_tipsy_titles(); @@ -5201,7 +5201,7 @@ this.socket.on('feed:update', _.bind(this.handle_realtime_update, this)); this.socket.removeAllListeners('feed:story:new'); - this.socket.on('feed:story:new', _.bind(function(feed_id, message) { + this.socket.on('feed:story:new', _.bind(function (feed_id, message) { var story_hash = message.split(',')[0]; var timestamp = message.split(',')[1]; NEWSBLUR.log(['Real-time new story', feed_id, story_hash, timestamp, message]); @@ -5215,8 +5215,8 @@ this.socket.removeAllListeners("user:update"); this.socket.on('user:update', _.bind(this.handle_realtime_update, this)); - - this.socket.on('disconnect', _.bind(function() { + + this.socket.on('disconnect', _.bind(function () { NEWSBLUR.log(["Lost connection to real-time pubsub. Falling back to polling."]); this.flags.feed_refreshing_in_realtime = false; this.setup_feed_refresh(); @@ -5224,26 +5224,26 @@ $('.NB-module-content-account-realtime').attr('title', 'Updating sites every ' + this.flags.refresh_interval + ' seconds...').addClass('NB-error').removeClass('NB-active'); this.apply_tipsy_titles(); }, this)); - this.socket.on('error', _.bind(function() { + this.socket.on('error', _.bind(function () { NEWSBLUR.log(["Can't connect to real-time pubsub."]); this.flags.feed_refreshing_in_realtime = false; this.setup_feed_refresh(); // $('.NB-module-content-account-realtime-subtitle').html($.make('b', 'Updating every 60 sec')); $('.NB-module-content-account-realtime').attr('title', 'Updating sites every ' + this.flags.refresh_interval + ' seconds...').addClass('NB-error').removeClass('NB-active'); this.apply_tipsy_titles(); - _.delay(_.bind(this.setup_socket_realtime_unread_counts, this), Math.random()*60*1000); + _.delay(_.bind(this.setup_socket_realtime_unread_counts, this), Math.random() * 60 * 1000); }, this)); - this.socket.on('reconnect_failed', _.bind(function() { + this.socket.on('reconnect_failed', _.bind(function () { console.log(["Socket.io reconnect failed"]); }, this)); - this.socket.on('reconnect', _.bind(function() { + this.socket.on('reconnect', _.bind(function () { console.log(["Socket.io reconnected successfully!"]); }, this)); - this.socket.on('reconnecting', _.bind(function() { + this.socket.on('reconnecting', _.bind(function () { console.log(["Socket.io reconnecting..."]); }, this)); } - + // this.watch_navigator_online(); }, @@ -5323,41 +5323,41 @@ } } }, - - watch_navigator_online: function() { + + watch_navigator_online: function () { window.removeEventListener('online', _.bind(this.setup_socket_realtime_unread_counts, this)); window.removeEventListener('offline', _.bind(this.setup_socket_realtime_unread_counts, this)); window.addEventListener('online', _.bind(this.setup_socket_realtime_unread_counts, this)); window.addEventListener('offline', _.bind(this.setup_socket_realtime_unread_counts, this)); }, - send_socket_active_feeds: function() { + send_socket_active_feeds: function () { if (!this.socket) return; - - var active_feeds = _.compact(this.model.feeds.map(function(feed) { + + var active_feeds = _.compact(this.model.feeds.map(function (feed) { return feed.get('active') && feed.id; })); active_feeds = active_feeds.concat(this.model.social_feeds.pluck('id')); - + if (active_feeds.length) { this.socket.emit('subscribe:feeds', active_feeds, NEWSBLUR.Globals.username); } return active_feeds; }, - - handle_wakeup: function() { + + handle_wakeup: function () { $.dreamOn(); - $.wakeUp(_.bind(function() { + $.wakeUp(_.bind(function () { console.log(["Wakeup, reconnecting to real-time socket.io...", new Date()]); this.setup_socket_realtime_unread_counts(); }, this), {}, 1 * 1000); }, - setup_feed_refresh: function(new_feeds) { + setup_feed_refresh: function (new_feeds) { var refresh_interval = this.constants.FEED_REFRESH_INTERVAL; var feed_count = this.model.feeds.size(); - + if (!NEWSBLUR.Globals.is_premium) { refresh_interval *= 2; } @@ -5373,20 +5373,20 @@ } if (new_feeds && feed_count < 250) { - refresh_interval = (1000 * 60) * 1/6; + refresh_interval = (1000 * 60) * 1 / 6; } else if (new_feeds && feed_count < 500) { - refresh_interval = (1000 * 60) * 1/4; + refresh_interval = (1000 * 60) * 1 / 4; } - + // 10 second minimum - refresh_interval = Math.max(10*1000, refresh_interval); - + refresh_interval = Math.max(10 * 1000, refresh_interval); + // Add 0-100% random delay refresh_interval = parseInt(refresh_interval * (1 + Math.random()), 10); - + clearInterval(this.flags.feed_refresh); - - this.flags.feed_refresh = setInterval(function() { + + this.flags.feed_refresh = setInterval(function () { if (!NEWSBLUR.reader.flags['pause_feed_refreshing']) { NEWSBLUR.reader.force_feeds_refresh(); } @@ -5395,7 +5395,7 @@ if (!this.socket || !this.socket.connected) { $('.NB-module-content-account-realtime').attr('title', 'Updating sites every ' + this.flags.refresh_interval + ' seconds...').addClass('NB-error'); this.apply_tipsy_titles(); - } + } NEWSBLUR.log(["Setting refresh interval to every " + this.flags.refresh_interval + " seconds."]); // if (this.socket && !this.socket.connected) { // // force disconnected since it's probably in a bad reconnect state. @@ -5403,22 +5403,22 @@ // this.socket.disconnect(); // } }, - - force_feed_refresh: function(feed_id, new_feed_id) { - feed_id = feed_id || this.active_feed; + + force_feed_refresh: function (feed_id, new_feed_id) { + feed_id = feed_id || this.active_feed; new_feed_id = _.isNumber(new_feed_id) && new_feed_id || feed_id; console.log(["force_feed_refresh", feed_id, new_feed_id]); - this.force_feeds_refresh(function() { + this.force_feeds_refresh(function () { // Open the feed back up if it is being refreshed and is still open. if (NEWSBLUR.reader.active_feed == feed_id || NEWSBLUR.reader.active_feed == new_feed_id) { - NEWSBLUR.reader.open_feed(new_feed_id, {force: true}); + NEWSBLUR.reader.open_feed(new_feed_id, { force: true }); } - + NEWSBLUR.reader.check_feed_fetch_progress(); }, true, new_feed_id, NEWSBLUR.app.taskbar_info.show_stories_error); }, - - force_feeds_refresh: function(callback, replace_active_feed, feed_id, error_callback) { + + force_feeds_refresh: function (callback, replace_active_feed, feed_id, error_callback) { if (callback) { this.cache.refresh_callback = callback; } else { @@ -5426,23 +5426,23 @@ } this.flags['pause_feed_refreshing'] = true; - this.model.refresh_feeds(_.bind(function(data) { + this.model.refresh_feeds(_.bind(function (data) { this.post_feed_refresh(data); }, this), this.flags['has_unfetched_feeds'], feed_id, error_callback); - + if (this.socket && this.socket.connected) { NEWSBLUR.assets.stories.retry_failed_marked_read_stories(); } }, - - post_feed_refresh: function(data) { + + post_feed_refresh: function (data) { var feeds = this.model.feeds; - + if (this.cache.refresh_callback && $.isFunction(this.cache.refresh_callback)) { this.cache.refresh_callback(feeds); delete this.cache.refresh_callback; } - + NEWSBLUR.app.sidebar_header.update_interactions_count(data.interactions_count); this.flags['refresh_inline_feed_delay'] = false; @@ -5450,30 +5450,30 @@ this.check_feed_fetch_progress(); this.toggle_focus_in_slider(); }, - - feed_unread_count: function(feed_id, options) { + + feed_unread_count: function (feed_id, options) { options = options || {}; feed_id = feed_id || this.active_feed; if (!feed_id) return; - + var feed = this.model.get_feed(feed_id); if (!feed) return; var subs = feed.get('num_subscribers'); var delay = options.realtime ? subs * 2 : 0; // 1,000 subs = 2 seconds - - _.delay(_.bind(function() { + + _.delay(_.bind(function () { this.model.feed_unread_count(feed_id, options.callback); }, this), Math.random() * delay); }, - - update_interactions_count: function() { - this.model.interactions_count(function(data) { + + update_interactions_count: function () { + this.model.interactions_count(function (data) { NEWSBLUR.app.sidebar_header.update_interactions_count(data.interactions_count); }, $.noop); }, - - push_notification_setup: function(feed_id) { + + push_notification_setup: function (feed_id) { var feed = NEWSBLUR.assets.get_feed(feed_id); if (!feed) { console.log(['Notification setup failed, no feed!', feed_id]); @@ -5488,15 +5488,15 @@ } }); }, - - push_notification: function(story_hash, story_title) { + + push_notification: function (story_hash, story_title) { var feed_id = story_hash.slice(0, story_hash.indexOf(':')); var feed = NEWSBLUR.assets.get_feed(feed_id); if (!feed) { console.log(['Error: Couldn\'t find find for notification', feed_id, story_hash, story_title]); return; } - + Push.create(feed.get('feed_title'), { body: story_title, icon: $.favicon(feed), @@ -5511,14 +5511,14 @@ } }); }, - + // =================== // = Mouse Indicator = // =================== - setup_mousemove_on_views: function() { + setup_mousemove_on_views: function () { this.hide_mouse_indicator(); - + if (this.story_view == 'story' || this.story_view == 'text' || this.flags['feed_view_showing_story_view'] || @@ -5528,91 +5528,91 @@ _.delay(_.bind(this.show_mouse_indicator, this), 350); } }, - - hide_mouse_indicator: function() { + + hide_mouse_indicator: function () { var self = this; if (!this.flags['mouse_indicator_hidden']) { this.flags['mouse_indicator_hidden'] = true; - this.$s.$mouse_indicator.animate({'opacity': 0, 'left': -10}, { - 'duration': 200, - 'queue': false, - 'complete': function() { + this.$s.$mouse_indicator.animate({ 'opacity': 0, 'left': -10 }, { + 'duration': 200, + 'queue': false, + 'complete': function () { self.flags['mouse_indicator_hidden'] = true; } }); } }, - - show_mouse_indicator: function() { + + show_mouse_indicator: function () { var self = this; if (NEWSBLUR.assets.preference('feed_view_single_story')) return; if (this.flags['mouse_indicator_hidden']) { this.flags['mouse_indicator_hidden'] = false; - this.$s.$mouse_indicator.animate({'opacity': 1, 'left': 0}, { - 'duration': 200, + this.$s.$mouse_indicator.animate({ 'opacity': 1, 'left': 0 }, { + 'duration': 200, 'queue': false, - 'complete': function() { + 'complete': function () { self.flags['mouse_indicator_hidden'] = false; } }); } }, - - handle_mouse_indicator_hover: function() { + + handle_mouse_indicator_hover: function () { var self = this; var $callout = $('.NB-callout-mouse-indicator'); $('.NB-callout-text', $callout).text('Lock'); $callout.corner('5px'); - - this.$s.$mouse_indicator.hover(function() { + + this.$s.$mouse_indicator.hover(function () { if (self.model.preference('lock_mouse_indicator')) { $('.NB-callout-text', $callout).text('Unlock'); } else { $('.NB-callout-text', $callout).text('Lock'); } self.flags['still_hovering_on_mouse_indicator'] = true; - setTimeout(function() { + setTimeout(function () { if (self.flags['still_hovering_on_mouse_indicator']) { $callout.css({ 'display': 'block' }).animate({ 'opacity': 1, 'left': '20px' - }, {'duration': 200, 'queue': false}); + }, { 'duration': 200, 'queue': false }); } }, 50); - }, function() { + }, function () { self.flags['still_hovering_on_mouse_indicator'] = false; - $callout.animate({'opacity': 0, 'left': '-100px'}, {'duration': 200, 'queue': false}); + $callout.animate({ 'opacity': 0, 'left': '-100px' }, { 'duration': 200, 'queue': false }); }); }, - - lock_mouse_indicator: function() { + + lock_mouse_indicator: function () { var self = this; var $callout = $('.NB-callout-mouse-indicator'); - + if (self.model.preference('lock_mouse_indicator')) { self.model.preference('lock_mouse_indicator', 0); $('.NB-callout-text', $callout).text('Unlocked'); } else { - + self.model.preference('lock_mouse_indicator', this.cache.mouse_position_y); $('.NB-callout-text', $callout).text('Locked'); } - - setTimeout(function() { + + setTimeout(function () { self.flags['still_hovering_on_mouse_indicator'] = true; $callout.fadeOut(200); }, 500); }, - - position_mouse_indicator: function() { - if (!_.contains(['split', 'full'], + + position_mouse_indicator: function () { + if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; - + var position = this.model.preference('lock_mouse_indicator'); var container = this.layout.contentLayout.state.container.innerHeight - 30; @@ -5626,37 +5626,37 @@ // console.log(["position_mouse_indicator", NEWSBLUR.reader.cache.mouse_position_y, position]); this.cache.mouse_position_y = position; }, - + // ========================== // = Login and Signup Forms = // ========================== - - handle_login_and_signup_forms: function() { + + handle_login_and_signup_forms: function () { var self = this; var $hidden_inputs = $('.NB-signup-hidden'); var $signup_username = $('input[name=signup-username]'); - - $signup_username.bind('focus', function() { + + $signup_username.bind('focus', function () { $hidden_inputs.slideDown(300); - }).bind('blur', function() { + }).bind('blur', function () { if ($signup_username.val().length < 1) { $hidden_inputs.slideUp(300); } }); }, - + // ================== // = Features Board = // ================== - - load_feature_page: function(direction) { + + load_feature_page: function (direction) { var self = this; var $module = $('.NB-module-features'); var $next = $('.NB-module-features .NB-module-next-page'); var $previous = $('.NB-module-features .NB-module-previous-page'); $module.addClass('NB-loading'); - + if (direction == -1 && this.counts['feature_page'] <= 0) { $module.removeClass('NB-loading'); this.counts['feature_page'] = 0; @@ -5666,14 +5666,14 @@ $module.removeClass('NB-loading'); return; } - - this.model.get_features_page(this.counts['feature_page']+direction, function(features) { + + this.model.get_features_page(this.counts['feature_page'] + direction, function (features) { $module.removeClass('NB-loading'); if (!features) return; - + self.counts['feature_page'] += direction; - + var $table = $.make('table', { className: 'NB-features', cellSpacing: 0, cellPadding: 0 }); for (var f in features) { if (f == 3) break; @@ -5684,9 +5684,9 @@ ]); $table.append($tr); } - + $('.NB-module-features .NB-features').replaceWith($table); - + var features_count = features.length; if (features_count < 4) { $next.addClass('NB-disabled'); @@ -5700,40 +5700,40 @@ } else { $previous.addClass('NB-disabled'); } - - }, function() { + + }, function () { $module.removeClass('NB-loading'); }); }, - - setup_howitworks_hovers: function() { + + setup_howitworks_hovers: function () { var $page_indicators = $('.NB-module-howitworks .NB-module-page-indicator'); - $page_indicators.bind('mouseenter', _.bind(function(e) { + $page_indicators.bind('mouseenter', _.bind(function (e) { var page = $(e.target).prevAll('.NB-module-page-indicator').length; this.load_howitworks_page(page); }, this)); }, - - load_howitworks_page: function(page) { + + load_howitworks_page: function (page) { var self = this; var $next = $('.NB-module-howitworks .NB-module-next-page'); var $previous = $('.NB-module-howitworks .NB-module-previous-page'); var $pages = $('.NB-howitworks-page'); var $page_indicators = $('.NB-module-howitworks .NB-module-page-indicator'); var pages_count = $pages.length; - + if (page == -1) { return; } if (page >= pages_count) { return; } - + $pages.removeClass("NB-active"); $page_indicators.removeClass("NB-active"); $pages.eq(page).addClass("NB-active"); $page_indicators.eq(page).addClass("NB-active"); - + if (page >= pages_count - 1) { $next.addClass('NB-disabled'); } else { @@ -5745,15 +5745,15 @@ $previous.removeClass('NB-disabled'); } }, - + // ======== // = FTUX = // ======== - - setup_ftux_add_feed_callout: function(message) { + + setup_ftux_add_feed_callout: function (message) { var self = this; if (this.flags['bouncing_callout']) return; - + $('.NB-callout-ftux .NB-callout-text').text(message || 'First things first...'); $('.NB-callout-ftux').corner('5px'); $('.NB-callout-ftux').css({ @@ -5765,19 +5765,19 @@ }, { 'duration': 750, 'easing': 'easeInOutQuint' - }).each(function() { + }).each(function () { var $this = $(this); - self.flags['bouncing_callout'] = setInterval(function() { - $this.animate({'bottom': '+=2px'}, {'duration': 200, 'easing': 'easeInOutQuint'}) - .animate({'bottom': '+=0px'}, {'duration': 50}) - .animate({'bottom': '-=2px'}, {'duration': 200, 'easing': 'easeInOutQuint'}); + self.flags['bouncing_callout'] = setInterval(function () { + $this.animate({ 'bottom': '+=2px' }, { 'duration': 200, 'easing': 'easeInOutQuint' }) + .animate({ 'bottom': '+=0px' }, { 'duration': 50 }) + .animate({ 'bottom': '-=2px' }, { 'duration': 200, 'easing': 'easeInOutQuint' }); }, 1000); }); }, - - setup_ftux_signup_callout: function() { + + setup_ftux_signup_callout: function () { var self = this; - + if (!this.flags['bouncing_callout']) { $('.NB-callout-ftux-signup .NB-callout-text').text('Signup'); $('.NB-callout-ftux-signup').corner('5px'); @@ -5790,40 +5790,40 @@ }, { 'duration': 750, 'easing': 'easeInOutQuint' - }).each(function() { + }).each(function () { var $this = $(this); - self.flags['bouncing_callout'] = setInterval(function() { - $this.animate({'bottom': '+=2px'}, {'duration': 200, 'easing': 'easeInOutQuint'}) - .animate({'bottom': '+=0px'}, {'duration': 50}) - .animate({'bottom': '-=2px'}, {'duration': 200, 'easing': 'easeInOutQuint'}); - }, 10000); + self.flags['bouncing_callout'] = setInterval(function () { + $this.animate({ 'bottom': '+=2px' }, { 'duration': 200, 'easing': 'easeInOutQuint' }) + .animate({ 'bottom': '+=0px' }, { 'duration': 50 }) + .animate({ 'bottom': '-=2px' }, { 'duration': 200, 'easing': 'easeInOutQuint' }); + }, 10000); }); } }, - + // ============================= // = Import from Google Reader = // ============================= - start_count_unreads_after_import: function() { + start_count_unreads_after_import: function () { var self = this; var $progress = this.$s.$feeds_progress; var $bar = $('.NB-progress-bar', $progress); var percentage = 0; var feeds_count = _.keys(this.model.feeds).length; - + if (!this.flags['pause_feed_refreshing'] || this.flags['has_unfetched_feeds']) return; - + this.flags['count_unreads_after_import_working'] = true; - + $('.NB-progress-title', $progress).text('Counting is difficult'); $('.NB-progress-counts', $progress).hide(); $('.NB-progress-percentage', $progress).hide(); $bar.progressbar({ value: percentage }); - - setTimeout(function() { + + setTimeout(function () { if (self.flags['count_unreads_after_import_working']) { self.animate_progress_bar($bar, feeds_count / 30); self.show_progress_bar(); @@ -5831,7 +5831,7 @@ }, 500); }, - finish_count_unreads_after_import: function(data) { + finish_count_unreads_after_import: function (data) { data = data || {}; $('.NB-progress-bar', this.$s.$feeds_progress).progressbar({ value: 100 @@ -5839,32 +5839,32 @@ this.flags['count_unreads_after_import_working'] = false; clearTimeout(this.locks['animate_progress_bar']); this.$s.$feed_link_loader.fadeOut(250); - this.$s.$feed_link_error.css({'display': 'none'}); + this.$s.$feed_link_error.css({ 'display': 'none' }); this.setup_feed_refresh(); if (!this.flags['has_unfetched_feeds']) { this.hide_progress_bar(); } }, - + // ===================== // = Recommended Feeds = // ===================== - - load_recommended_feeds: function() { - // Reload recommended feeds every 60 minutes. - clearInterval(this.locks.load_recommended_feed); - this.locks.load_recommended_feed = setInterval(_.bind(function() { - this.load_recommended_feed(0, true); - }, this), 60*60*1000); - }, - - load_feed_in_tryfeed_view: function(feed_id, options) { + + load_recommended_feeds: function () { + // Reload recommended feeds every 60 minutes. + clearInterval(this.locks.load_recommended_feed); + this.locks.load_recommended_feed = setInterval(_.bind(function () { + this.load_recommended_feed(0, true); + }, this), 60 * 60 * 1000); + }, + + load_feed_in_tryfeed_view: function (feed_id, options) { options = options || {}; var feed = _.extend({ - id : feed_id, - feed_id : feed_id, - feed_title : options.feed && options.feed.feed_title, - temp : true + id: feed_id, + feed_id: feed_id, + feed_title: options.feed && options.feed.feed_title, + temp: true }, options.feed && options.feed.attributes); var $tryfeed_container = this.$s.$tryfeed_header.closest('.NB-feeds-header-container'); @@ -5872,9 +5872,9 @@ feed = this.model.set_feed(feed_id, feed); $('.NB-feeds-header-title', this.$s.$tryfeed_header).text(feed.get('feed_title')); - $('.NB-feeds-header-icon', this.$s.$tryfeed_header).attr('src', $.favicon(feed)); + $('.NB-feeds-header-icon', this.$s.$tryfeed_header).attr('src', $.favicon(feed)); - $tryfeed_container.slideDown(350, _.bind(function() { + $tryfeed_container.slideDown(350, _.bind(function () { options.force = true; options.try_feed = true; this.open_feed(feed_id, options); @@ -5882,8 +5882,8 @@ this.$s.$tryfeed_header.addClass('NB-selected'); }, this)); }, - - load_social_feed_in_tryfeed_view: function(social_feed, options) { + + load_social_feed_in_tryfeed_view: function (social_feed, options) { options = options || {}; if (_.isNumber(social_feed)) { social_feed = this.model.get_feed('social:' + social_feed); @@ -5894,15 +5894,15 @@ if (!social_feed) { social_feed = this.model.add_social_feed(options.feed); } - + var $tryfeed_container = this.$s.$tryfeed_header.closest('.NB-feeds-header-container'); this.reset_feed(); - + $('.NB-feeds-header-title', this.$s.$tryfeed_header).text(social_feed.get('username')); - $('.NB-feeds-header-icon', this.$s.$tryfeed_header).attr('src', $.favicon(social_feed)); + $('.NB-feeds-header-icon', this.$s.$tryfeed_header).attr('src', $.favicon(social_feed)); - $tryfeed_container.slideDown(350, _.bind(function() { + $tryfeed_container.slideDown(350, _.bind(function () { this.open_social_stories(social_feed.get('id'), options); this.switch_taskbar_view('feed'); this.switch_story_layout(); @@ -5910,8 +5910,8 @@ this.$s.$tryfeed_header.addClass('NB-selected'); }, this)); }, - - hide_tryfeed_view: function() { + + hide_tryfeed_view: function () { var $tryfeed_container = this.$s.$tryfeed_header.closest('.NB-feeds-header-container'); $tryfeed_container.slideUp(350); this.$s.$story_taskbar.find('.NB-tryfeed-add').remove(); @@ -5919,392 +5919,392 @@ this.flags['showing_feed_in_tryfeed_view'] = false; this.flags['showing_social_feed_in_tryfeed_view'] = false; }, - - show_tryfeed_add_button: function() { + + show_tryfeed_add_button: function () { if (this.$s.$story_taskbar.find('.NB-tryfeed-add:visible').length) return; - + var $add = $.make('div', { className: 'NB-modal-submit' }, [ - $.make('div', { className: 'NB-tryfeed-add NB-modal-submit-green NB-modal-submit-button' }, 'Subscribe') - ]).css({'opacity': 0}); + $.make('div', { className: 'NB-tryfeed-add NB-modal-submit-green NB-modal-submit-button' }, 'Subscribe') + ]).css({ 'opacity': 0 }); this.$s.$story_taskbar.append($add); - $add.animate({'opacity': 1}, {'duration': 600}); + $add.animate({ 'opacity': 1 }, { 'duration': 600 }); }, - - correct_tryfeed_title: function() { + + correct_tryfeed_title: function () { var feed = this.model.get_feed(this.active_feed); $('.NB-feeds-header-title', this.$s.$tryfeed_header).text(feed.get('feed_title')); this.make_feed_title_in_stories(); }, - - show_tryfeed_follow_button: function() { + + show_tryfeed_follow_button: function () { if (this.$s.$story_taskbar.find('.NB-tryfeed-follow:visible').length) return; - + var $add = $.make('div', { className: 'NB-modal-submit' }, [ - $.make('div', { className: 'NB-tryfeed-follow NB-modal-submit-green NB-modal-submit-button' }, 'Follow') - ]).css({'opacity': 0}); + $.make('div', { className: 'NB-tryfeed-follow NB-modal-submit-green NB-modal-submit-button' }, 'Follow') + ]).css({ 'opacity': 0 }); this.$s.$story_taskbar.append($add); - $add.animate({'opacity': 1}, {'duration': 600}); + $add.animate({ 'opacity': 1 }, { 'duration': 600 }); }, - - show_tryout_signup_button: function() { + + show_tryout_signup_button: function () { if (this.$s.$story_taskbar.find('.NB-tryout-signup:visible').length) return; - + var $add = $.make('div', { className: 'NB-modal-submit' }, [ - $.make('div', { className: 'NB-tryout-signup NB-modal-submit-green NB-modal-submit-button' }, 'Sign Up') - ]).css({'opacity': 0}); + $.make('div', { className: 'NB-tryout-signup NB-modal-submit-green NB-modal-submit-button' }, 'Sign Up') + ]).css({ 'opacity': 0 }); this.$s.$story_taskbar.append($add); - $add.animate({'opacity': 1}, {'duration': 600}); + $add.animate({ 'opacity': 1 }, { 'duration': 600 }); }, - - hide_tryout_signup_button: function() { + + hide_tryout_signup_button: function () { this.$s.$story_taskbar.find('.NB-tryout-signup:visible').remove(); }, - - add_recommended_feed: function(feed_id) { + + add_recommended_feed: function (feed_id) { feed_id = feed_id || this.active_feed; var feed_address = this.model.get_feed(feed_id).get('feed_address'); - - this.open_add_feed_modal({url: feed_address}); + + this.open_add_feed_modal({ url: feed_address }); }, - - follow_user_in_tryfeed: function(feed_id) { + + follow_user_in_tryfeed: function (feed_id) { var self = this; var socialsub = this.model.get_feed(feed_id); - this.model.follow_user(socialsub.get('user_id'), function(data) { + this.model.follow_user(socialsub.get('user_id'), function (data) { NEWSBLUR.app.feed_list.make_social_feeds(); self.open_social_stories(feed_id); }); }, - - approve_feed_in_moderation_queue: function(feed_id) { + + approve_feed_in_moderation_queue: function (feed_id) { var self = this; var $module = $('.NB-module-recommended.NB-recommended-unmoderated'); $module.addClass('NB-loading'); var date = $('.NB-recommended-moderation-date').val(); - - this.model.approve_feed_in_moderation_queue(feed_id, date, function(resp) { + + this.model.approve_feed_in_moderation_queue(feed_id, date, function (resp) { if (!resp) return; $module.removeClass('NB-loading'); $module.replaceWith(resp); self.load_javascript_elements_on_page(); }); }, - - decline_feed_in_moderation_queue: function(feed_id) { + + decline_feed_in_moderation_queue: function (feed_id) { var self = this; var $module = $('.NB-module-recommended.NB-recommended-unmoderated'); $module.addClass('NB-loading'); - - this.model.decline_feed_in_moderation_queue(feed_id, function(resp) { + + this.model.decline_feed_in_moderation_queue(feed_id, function (resp) { if (!resp) return; $module.removeClass('NB-loading'); $module.replaceWith(resp); self.load_javascript_elements_on_page(); }); }, - - load_recommended_feed: function(direction, refresh, unmoderated) { + + load_recommended_feed: function (direction, refresh, unmoderated) { var self = this; - var $module = unmoderated ? - $('.NB-module-recommended.NB-recommended-unmoderated') : - $('.NB-module-recommended:not(.NB-recommended-unmoderated)'); - + var $module = unmoderated ? + $('.NB-module-recommended.NB-recommended-unmoderated') : + $('.NB-module-recommended:not(.NB-recommended-unmoderated)'); + if (!refresh) { $module.addClass('NB-loading'); } direction = direction || 0; - - this.model.load_recommended_feed(this.counts['recommended_feed_page']+direction, - !!refresh, unmoderated, function(resp) { - $module.removeClass('NB-loading'); - if (!resp) return; - $module.replaceWith(resp); - self.counts['recommended_feed_page'] += direction; - self.load_javascript_elements_on_page(); - }, function() { - $module.removeClass('NB-loading'); - }); + + this.model.load_recommended_feed(this.counts['recommended_feed_page'] + direction, + !!refresh, unmoderated, function (resp) { + $module.removeClass('NB-loading'); + if (!resp) return; + $module.replaceWith(resp); + self.counts['recommended_feed_page'] += direction; + self.load_javascript_elements_on_page(); + }, function () { + $module.removeClass('NB-loading'); + }); }, - + // ==================== // = Dashboard Graphs = // ==================== - - setup_dashboard_graphs: function() { + + setup_dashboard_graphs: function () { // Reload dashboard graphs every 30 min in debug, 1 min for staff, 10 minutes otherwise. - var reload_interval = NEWSBLUR.Globals.debug ? 30*60*1000: NEWSBLUR.Globals.is_staff ? 60*1000 : 10*60*1000; + var reload_interval = NEWSBLUR.Globals.debug ? 30 * 60 * 1000 : NEWSBLUR.Globals.is_staff ? 60 * 1000 : 10 * 60 * 1000; clearInterval(this.locks.load_dashboard_graphs); - this.locks.load_dashboard_graphs = setInterval(_.bind(function() { + this.locks.load_dashboard_graphs = setInterval(_.bind(function () { this.load_dashboard_graphs(); }, this), reload_interval * (Math.random() * (1.25 - 0.75) + 0.75)); }, - - load_dashboard_graphs: function() { + + load_dashboard_graphs: function () { var self = this; var $module = $('.NB-module-site-stats'); $module.addClass('NB-loading'); - - this.model.load_dashboard_graphs(function(resp) { + + this.model.load_dashboard_graphs(function (resp) { $module.removeClass('NB-loading'); if (!resp) return; $module.replaceWith(resp); self.load_javascript_elements_on_page(); - }, function() { + }, function () { $module.removeClass('NB-loading'); }); - }, - - setup_feedback_table: function() { + }, + + setup_feedback_table: function () { // Reload feedback every 30 min in debug, 30 sec for staff, 10 minutes otherwise. var reload_interval = NEWSBLUR.Globals.debug ? 30 * 60 * 1000 : NEWSBLUR.Globals.is_staff ? 0.5 * 60 * 1000 : 10 * 60 * 1000; - + clearInterval(this.locks.load_feedback_table); - this.locks.load_feedback_table = setInterval(_.bind(function() { + this.locks.load_feedback_table = setInterval(_.bind(function () { this.load_feedback_table(); this.load_feature_page(0); }, this), reload_interval * (Math.random() * (1.25 - 0.75) + 0.75)); }, - - load_feedback_table: function() { + + load_feedback_table: function () { var self = this; var $module = $('.NB-feedback-table'); $module.addClass('NB-loading'); - - this.model.load_feedback_table(function(resp) { + + this.model.load_feedback_table(function (resp) { $module.removeClass('NB-loading'); if (!resp) return; $module.replaceWith(resp); self.load_javascript_elements_on_page(); - }, function() { + }, function () { $module.removeClass('NB-loading'); }); }, - + // =================== // = Unfetched Feeds = // =================== - + setup_unfetched_feed_check: function () { clearInterval(this.locks.unfetched_feed_check); - this.locks.unfetched_feed_check = setInterval(_.bind(function() { + this.locks.unfetched_feed_check = setInterval(_.bind(function () { var unfetched_feeds = NEWSBLUR.assets.unfetched_feeds(); if (unfetched_feeds.length) { this.force_instafetch_stories(unfetched_feeds[0].id); } - }, this), 60*1*1000); + }, this), 60 * 1 * 1000); }, - + // ========== // = Events = // ========== - handle_clicks: function(elem, e) { + handle_clicks: function (elem, e) { var self = this; var stopPropagation = false; - + // NEWSBLUR.log(['click', e, e.button]); - $.targetIs(e, { tagSelector: '.NB-feeds-list-retry' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-feeds-list-retry' }, function ($t, $p) { NEWSBLUR.app.feed_list.retry(); - }); + }); // = Taskbar ====================================================== - - $.targetIs(e, { tagSelector: '.NB-task-add' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-task-add' }, function ($t, $p) { e.preventDefault(); self.open_add_feed_modal(); - }); - $.targetIs(e, { tagSelector: '.NB-task-manage' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-task-manage' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { self.show_manage_menu('site', $t); } - }); - $.targetIs(e, { tagSelector: '.NB-module-account-settings' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-account-settings' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - self.show_manage_menu('site', $t, {inverse: true}); + self.show_manage_menu('site', $t, { inverse: true }); } - }); - $.targetIs(e, { tagSelector: '.NB-modal-title' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-modal-title' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var $item = $(".NB-icon", $t); if ($item.length) { - self.show_manage_menu('site', $item, {inverse: true, right: true, body: true}); + self.show_manage_menu('site', $item, { inverse: true, right: true, body: true }); } } }); - $.targetIs(e, { tagSelector: '.NB-story-titles-expand-sidebar' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-story-titles-expand-sidebar' }, function ($t, $p) { e.preventDefault(); self.open_sidebar(); - }); - + }); + // = Context Menu ================================================ - - $.targetIs(e, { tagSelector: '.NB-menu-manage-open-input' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-menu-manage-open-input' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); self.flags['showing_confirm_input_on_manage_menu'] = true; - $t.select().blur(function() { + $t.select().blur(function () { self.flags['showing_confirm_input_on_manage_menu'] = false; }); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-train' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-train' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.open_feed_intelligence_modal(1, feed_id, false); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-train' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-train' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); var story_id = $t.parents('.NB-menu-manage').data('story_id'); self.open_story_trainer(story_id, feed_id); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-recommend' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-recommend' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.open_recommend_modal(feed_id); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-mark-read-newer' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-mark-read-newer' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var story_id = $t.closest('.NB-menu-manage').data('story_id'); var story = NEWSBLUR.assets.get_story(story_id); var timestamp = story.get('story_timestamp'); - + if (self.flags.river_view && !self.flags.social_view) { self.mark_folder_as_read(self.active_folder, timestamp, 'newer'); } else { self.mark_feed_as_read(self.active_feed, timestamp, 'newer'); } } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-mark-read-older' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-mark-read-older' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var story_id = $t.closest('.NB-menu-manage').data('story_id'); var story = NEWSBLUR.assets.get_story(story_id); var timestamp = story.get('story_timestamp'); - + if (self.flags.river_view && !self.flags.social_view) { self.mark_folder_as_read(self.active_folder, timestamp, 'older'); } else { self.mark_feed_as_read(self.active_feed, timestamp, 'older'); } } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-trainer' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-trainer' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_trainer_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-tutorial' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-tutorial' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_tutorial_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-intro' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-intro' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { - self.open_intro_modal({page_number: 1}); + $.modal.close(function () { + self.open_intro_modal({ page_number: 1 }); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-stats' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-stats' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); NEWSBLUR.log(['statistics feed_id', feed_id]); self.open_feed_statistics_modal(feed_id); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-settings' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-settings' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); + var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.open_feed_exception_modal(feed_id); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-notifications' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-notifications' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); + var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.open_notifications_modal(feed_id); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-settings' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-settings' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var folder_title = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); self.open_feed_exception_modal(folder_title, { - folder_title: folder_title, + folder_title: folder_title, $folder: $folder }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-reload' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-reload' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.force_instafetch_stories(feed_id); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-delete' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-delete' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); if ($t.hasClass('NB-menu-manage-feed-delete-cancel') || $t.hasClass('NB-menu-manage-folder-delete-cancel')) { self.hide_confirm_delete_menu_item(); } else if ($t.hasClass('NB-menu-manage-feed-delete') || - $t.hasClass('NB-menu-manage-folder-delete')) { + $t.hasClass('NB-menu-manage-folder-delete')) { self.show_confirm_delete_menu_item(); } else if ($t.hasClass('NB-menu-manage-socialfeed-delete-cancel')) { self.hide_confirm_unfollow_menu_item(); } else if ($t.hasClass('NB-menu-manage-socialfeed-delete')) { self.show_confirm_unfollow_menu_item(); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-delete-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-delete-confirm' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); var $feed = $t.parents('.NB-menu-manage').data('$feed'); self.manage_menu_delete_feed(feed_id, $feed); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-delete-search' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-delete-search' }, function ($t, $p) { e.preventDefault(); var search_model_id = $t.parents('.NB-menu-manage').data('feed_id'); var $feed = $t.parents('.NB-menu-manage').data('$feed'); self.manage_menu_delete_search(search_model_id, $feed); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-socialfeed-delete-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-socialfeed-delete-confirm' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); var $feed = $t.parents('.NB-menu-manage').data('$feed'); self.manage_menu_unfollow_feed(feed_id, $feed); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-delete-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-delete-confirm' }, function ($t, $p) { e.preventDefault(); var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); self.manage_menu_delete_folder(folder_name, $folder); - }); + }); var adding_icon = false; - $.targetIs(e, { tagSelector: '.NB-icon-add', childOf: '.NB-menu-manage' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-icon-add', childOf: '.NB-menu-manage' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); adding_icon = true; @@ -6312,45 +6312,45 @@ var $folder = $t.parents('.NB-folder-option'); var folder = $folder.data('folder'); self.show_add_folder_in_menu(feed_id, $folder, folder || ''); - }); - $.targetIs(e, { tagSelector: '.NB-folder-option', childOf: '.NB-menu-manage' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-folder-option', childOf: '.NB-menu-manage' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); if (adding_icon) return; var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.switch_change_folder(feed_id, $t.data('folder') || ''); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-move' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-move' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - + var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); var $feed = $t.parents('.NB-menu-manage').data('$feed'); - + var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); - + if ($t.hasClass('NB-menu-manage-feed-move-cancel') || $t.hasClass('NB-menu-manage-folder-move-cancel')) { self.hide_confirm_move_menu_item(); } else { self.show_confirm_move_menu_item(feed_id || folder_name, $feed || $folder); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-add-folder-save' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-add-folder-save' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - + self.add_folder_to_folder(); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-move-save' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-move-save' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); self.manage_menu_move_folder(folder_name, $folder); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-move-save' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-move-save' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); if ($t.hasClass('NB-disabled')) return; @@ -6358,30 +6358,30 @@ var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); var $feed = $t.parents('.NB-menu-manage').data('$feed'); self.manage_menu_move_feed(feed_id, $feed); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-move-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-move-confirm' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-move-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-move-confirm' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-controls' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-controls' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-mute' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-menu-manage-mute' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); self.manage_menu_mute_feed($t.parents('.NB-menu-manage').data('feed_id'), false); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-unmute' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-unmute' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); self.manage_menu_mute_feed($t.parents('.NB-menu-manage').data('feed_id'), true); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-rename' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-rename' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); if ($t.hasClass('NB-menu-manage-feed-rename-cancel') || @@ -6390,30 +6390,30 @@ } else { self.show_confirm_rename_menu_item(); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-rename-save' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-rename-save' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); self.manage_menu_rename_folder(folder_name, $folder); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-rename-save' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-rename-save' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); var $feed = $t.parents('.NB-menu-manage').data('$feed'); self.manage_menu_rename_feed(feed_id, $feed); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-rename-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-rename-confirm' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-rename-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-rename-confirm' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-share' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-share' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); var story_id = $t.parents('.NB-menu-manage').data('story_id'); @@ -6422,36 +6422,36 @@ } else { self.show_confirm_story_share_menu_item(story_id); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-share-confirm' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-share-confirm' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-mark-read' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-mark-read' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.mark_feed_as_read(feed_id); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-mark-read' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-mark-read' }, function ($t, $p) { e.preventDefault(); var $folder = $t.parents('.NB-menu-manage').data('$folder'); - var folder_view = NEWSBLUR.assets.folders.get_view($folder) || - self.active_folder.folder_view; + var folder_view = NEWSBLUR.assets.folders.get_view($folder) || + self.active_folder.folder_view; self.mark_folder_as_read(folder_view.model); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-subscribe' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-subscribe' }, function ($t, $p) { e.preventDefault(); var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); - self.open_add_feed_modal({folder_title: folder_name}); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-subfolder' }, function($t, $p){ + self.open_add_feed_modal({ folder_title: folder_name }); + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-folder-subfolder' }, function ($t, $p) { e.preventDefault(); var folder_name = $t.parents('.NB-menu-manage').data('folder_name'); var $folder = $t.parents('.NB-menu-manage').data('$folder'); - self.open_add_feed_modal({folder_title: folder_name, init_folder: true}); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-open' }, function($t, $p){ + self.open_add_feed_modal({ folder_title: folder_name, init_folder: true }); + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-open' }, function ($t, $p) { e.preventDefault(); if (!self.flags['showing_confirm_input_on_manage_menu']) { var story_id = $t.closest('.NB-menu-manage-story').data('story_id'); @@ -6459,326 +6459,326 @@ story.open_story_in_new_tab(true); } }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-star' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-star' }, function ($t, $p) { e.preventDefault(); var story_id = $t.closest('.NB-menu-manage-story').data('story_id'); var story = NEWSBLUR.assets.get_story(story_id); story.toggle_starred(); }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-exception' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-menu-manage-feed-exception' }, function ($t, $p) { e.preventDefault(); - var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); + var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); self.open_feed_exception_modal(feed_id); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-site-mark-read' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-site-mark-read' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_mark_read_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-social-profile' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-social-profile' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.parents('.NB-menu-manage').data('feed_id'); - $.modal.close(function() { + $.modal.close(function () { self.open_social_profile_modal(feed_id); }); - }); + }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-keyboard' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-menu-manage-keyboard' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_keyboard_shortcuts_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-goodies' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-goodies' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_goodies_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-statistics' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-statistics' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_feed_statistics_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-notifications' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-notifications' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_notifications_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-newsletters' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-newsletters' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_newsletters_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-import' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-import' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { NEWSBLUR.reader.open_intro_modal({ 'page_number': 2, 'force_import': true }); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-friends' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-friends' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_friends_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-profile-editor' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-profile-editor' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_profile_editor_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-preferences' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-preferences' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_preferences_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-theme' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-theme' }, function ($t, $p) { e.preventDefault(); - }); - $.targetIs(e, { tagSelector: '.NB-options-theme-light' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-theme-light' }, function ($t, $p) { e.preventDefault(); self.switch_theme('light'); - }); - $.targetIs(e, { tagSelector: '.NB-options-theme-dark' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-theme-dark' }, function ($t, $p) { e.preventDefault(); self.switch_theme('dark'); - }); - $.targetIs(e, { tagSelector: '.NB-options-theme-auto' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-theme-auto' }, function ($t, $p) { e.preventDefault(); self.switch_theme('auto'); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-font' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-font' }, function ($t, $p) { e.preventDefault(); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-font-whitney' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-font-whitney' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font('whitney'); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-font-lucida' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-font-lucida' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font('lucida'); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-font-gotham' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-font-gotham' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font('gotham'); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-size' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-size' }, function ($t, $p) { e.preventDefault(); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-size-xs' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-size-xs' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font_size('xs'); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-size-s' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-size-s' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font_size('s'); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-size-m' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-size-m' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font_size('m'); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-size-l' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-size-l' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font_size('l'); - }); - $.targetIs(e, { tagSelector: '.NB-options-feed-size-xl' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-feed-size-xl' }, function ($t, $p) { e.preventDefault(); self.switch_feed_font_size('xl'); - }); - $.targetIs(e, { tagSelector: '.NB-options-density-compact' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-density-compact' }, function ($t, $p) { e.preventDefault(); self.switch_density('compact'); - }); - $.targetIs(e, { tagSelector: '.NB-options-density-comfortable' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-options-density-comfortable' }, function ($t, $p) { e.preventDefault(); self.switch_density('comfortable'); - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-logout' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-logout' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - + if (!$t.hasClass('NB-disabled')) { self.logout(); } }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-account' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-menu-manage-account' }, function ($t, $p) { e.preventDefault(); - + if (!$t.hasClass('NB-disabled') && !$($t.prevObject).hasClass('NB-menu-manage-logout')) { - $.modal.close(function() { + $.modal.close(function () { self.open_account_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-feedchooser' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-feedchooser' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_feedchooser_modal({ 'chooser_only': NEWSBLUR.Globals.is_premium }); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-organizer' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-organizer' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { + $.modal.close(function () { self.open_organizer_modal(); }); } - }); - $.targetIs(e, { tagSelector: '.NB-menu-manage-premium' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-menu-manage-premium' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - $.modal.close(function() { - self.open_feedchooser_modal({'premium_only': true}); + $.modal.close(function () { + self.open_feedchooser_modal({ 'premium_only': true }); }); } - }); - $.targetIs(e, { tagSelector: '.NB-module-account-upgrade' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-account-upgrade' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - self.open_feedchooser_modal({'premium_only': true}); + self.open_feedchooser_modal({ 'premium_only': true }); } - }); - $.targetIs(e, { tagSelector: '.NB-module-account-train' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-account-train' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { self.open_trainer_modal(); } - }); - $.targetIs(e, { tagSelector: '.NB-module-friends-button' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-friends-button' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { self.open_friends_modal(); } - }); - $.targetIs(e, { tagSelector: '.NB-module-launch-tutorial' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-launch-tutorial' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { self.open_tutorial_modal(); } - }); - $.targetIs(e, { tagSelector: '.NB-module-launch-intro' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-launch-intro' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - self.open_intro_modal({page_number: 2}); + self.open_intro_modal({ page_number: 2 }); } - }); - $.targetIs(e, { tagSelector: '.NB-module-premium-button' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-premium-button' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { - self.open_feedchooser_modal({'premium_only': true}); + self.open_feedchooser_modal({ 'premium_only': true }); } - }); - $.targetIs(e, { tagSelector: '.NB-module-gettingstarted-hide' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-gettingstarted-hide' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { self.check_hide_getting_started(true); } - }); - - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-unread' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-unread' }, function ($t, $p) { e.preventDefault(); var story_id = $t.closest('.NB-menu-manage').data('story_id'); var story = self.model.get_story(story_id); NEWSBLUR.assets.stories.mark_unread(story); - }); - - $.targetIs(e, { tagSelector: '.NB-menu-manage-story-read' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-menu-manage-story-read' }, function ($t, $p) { e.preventDefault(); var story_id = $t.closest('.NB-menu-manage').data('story_id'); var story = self.model.get_story(story_id); NEWSBLUR.assets.stories.mark_read(story); - }); - - $.targetIs(e, { tagSelector: '.task_view_page:not(.NB-task-return)' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.task_view_page:not(.NB-task-return)' }, function ($t, $p) { e.preventDefault(); self.switch_taskbar_view('page'); }); - $.targetIs(e, { tagSelector: '.task_view_feed' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.task_view_feed' }, function ($t, $p) { e.preventDefault(); self.switch_taskbar_view('feed'); }); - $.targetIs(e, { tagSelector: '.task_view_story' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.task_view_story' }, function ($t, $p) { e.preventDefault(); self.switch_taskbar_view('story'); }); - $.targetIs(e, { tagSelector: '.task_view_text' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.task_view_text' }, function ($t, $p) { e.preventDefault(); self.switch_taskbar_view('text'); }); - $.targetIs(e, { tagSelector: '.NB-task-return' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-task-return' }, function ($t, $p) { e.preventDefault(); NEWSBLUR.app.original_tab_view.load_feed_iframe(); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-next-unread' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-next-unread' }, function ($t, $p) { e.preventDefault(); self.open_next_unread_story_across_feeds(); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-next' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-next' }, function ($t, $p) { e.preventDefault(); self.show_next_story(1); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-previous' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-story-previous' }, function ($t, $p) { e.preventDefault(); self.show_next_story(-1); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-full' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-full' }, function ($t, $p) { e.preventDefault(); self.switch_story_layout('full'); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-split' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-split' }, function ($t, $p) { e.preventDefault(); self.switch_story_layout('split'); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-list' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-list' }, function ($t, $p) { e.preventDefault(); self.switch_story_layout('list'); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-grid' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-grid' }, function ($t, $p) { e.preventDefault(); self.switch_story_layout('grid'); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-magazine' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-button.NB-task-layout-magazine' }, function ($t, $p) { e.preventDefault(); self.switch_story_layout('magazine'); - }); - $.targetIs(e, { tagSelector: '.NB-taskbar-options' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-taskbar-options' }, function ($t, $p) { e.preventDefault(); self.open_story_options_popover(); - }); - $.targetIs(e, { tagSelector: '.NB-intelligence-slider-control' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-intelligence-slider-control' }, function ($t, $p) { e.preventDefault(); var unread_value; if ($t.hasClass('NB-intelligence-slider-red')) { @@ -6790,113 +6790,115 @@ } else if ($t.hasClass('NB-intelligence-slider-blue')) { unread_value = 2; } - + self.slide_intelligence_slider(unread_value); - }); - + }); + // ===================== // = Recommended Feeds = // ===================== - - $.targetIs(e, { tagSelector: '.NB-recommended-statistics' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-recommended-statistics' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.closest('.NB-recommended').data('feed-id'); $('.NB-module-recommended').addClass('NB-loading'); - self.model.load_canonical_feed(feed_id, function() { + self.model.load_canonical_feed(feed_id, function () { $('.NB-module-recommended').removeClass('NB-loading'); self.open_feed_statistics_modal(feed_id); }); - }); - - $.targetIs(e, { tagSelector: '.NB-recommended-intelligence' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-recommended-intelligence' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.closest('.NB-recommended').data('feed-id'); $('.NB-module-recommended').addClass('NB-loading'); - self.model.load_canonical_feed(feed_id, function() { + self.model.load_canonical_feed(feed_id, function () { $('.NB-module-recommended').removeClass('NB-loading'); self.open_feed_intelligence_modal(1, feed_id); }); - }); - - $.targetIs(e, { tagSelector: '.NB-recommended-try' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-recommended-try' }, function ($t, $p) { e.preventDefault(); var $recommended_feeds = $('.NB-module-recommended'); var feed_id = $t.closest('.NB-recommended').data('feed-id'); - self.open_feed(feed_id, {'feed': new NEWSBLUR.Models.Feed({ - 'feed_title': $('.NB-recommended-title', $recommended_feeds).text(), - 'favicon_url': $('.NB-recommended-favicon', $recommended_feeds).attr('src'), - 'temp': true - })}); - }); - - $.targetIs(e, { tagSelector: '.NB-recommended-add' }, function($t, $p){ + self.open_feed(feed_id, { + 'feed': new NEWSBLUR.Models.Feed({ + 'feed_title': $('.NB-recommended-title', $recommended_feeds).text(), + 'favicon_url': $('.NB-recommended-favicon', $recommended_feeds).attr('src'), + 'temp': true + }) + }); + }); + + $.targetIs(e, { tagSelector: '.NB-recommended-add' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.closest('.NB-recommended').data('feed-id'); $('.NB-module-recommended').addClass('NB-loading'); - self.model.load_canonical_feed(feed_id, function() { + self.model.load_canonical_feed(feed_id, function () { $('.NB-module-recommended').removeClass('NB-loading'); self.add_recommended_feed(feed_id); }); - }); - - $.targetIs(e, { tagSelector: '.NB-recommended-decline' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-recommended-decline' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.closest('.NB-recommended').data('feed-id'); self.decline_feed_in_moderation_queue(feed_id); - }); - - $.targetIs(e, { tagSelector: '.NB-recommended-approve' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-recommended-approve' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.closest('.NB-recommended').data('feed-id'); self.approve_feed_in_moderation_queue(feed_id); - }); - - $.targetIs(e, { tagSelector: '.NB-module-recommended .NB-module-next-page' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-module-recommended .NB-module-next-page' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var unmoderated = $t.closest('.NB-module-recommended').hasClass('NB-recommended-unmoderated'); self.load_recommended_feed(1, false, unmoderated); } - }); - - $.targetIs(e, { tagSelector: '.NB-module-recommended .NB-module-previous-page' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-module-recommended .NB-module-previous-page' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { var unmoderated = $t.closest('.NB-module-recommended').hasClass('NB-recommended-unmoderated'); self.load_recommended_feed(-1, false, unmoderated); } - }); - - $.targetIs(e, { tagSelector: '.NB-tryfeed-add' }, function($t, $p){ + }); + + $.targetIs(e, { tagSelector: '.NB-tryfeed-add' }, function ($t, $p) { e.preventDefault(); var feed_id = self.active_feed; self.add_recommended_feed(feed_id); - }); - $.targetIs(e, { tagSelector: '.NB-tryfeed-follow' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-tryfeed-follow' }, function ($t, $p) { e.preventDefault(); var feed_id = self.active_feed; self.follow_user_in_tryfeed(feed_id); - }); - $.targetIs(e, { tagSelector: '.NB-tryout-signup' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-tryout-signup' }, function ($t, $p) { e.preventDefault(); self.show_splash_page(); if (NEWSBLUR.welcome) { NEWSBLUR.welcome.show_signin_form(); } - }); - + }); + // = Interactions Module ========================================== - - $.targetIs(e, { tagSelector: '.NB-interaction-follow, .NB-activity-follow' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-interaction-follow, .NB-activity-follow' }, function ($t, $p) { e.preventDefault(); var user_id = $t.data('userId'); var username = $t.closest('.NB-interaction').find('.NB-interaction-username').text(); - + self.close_interactions_popover(); - self.model.add_user_profiles([{user_id: user_id, username: username}]); + self.model.add_user_profiles([{ user_id: user_id, username: username }]); self.open_social_profile_modal(user_id); - }); - $.targetIs(e, { tagSelector: '.NB-interaction-comment_reply, .NB-interaction-reply_reply, .NB-interaction-story_reshare, .NB-interaction-comment_like, .NB-activity-comment_reply, .NB-activity-comment_like, .NB-activity-sharedstory' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-interaction-comment_reply, .NB-interaction-reply_reply, .NB-interaction-story_reshare, .NB-interaction-comment_like, .NB-activity-comment_reply, .NB-activity-comment_like, .NB-activity-sharedstory' }, function ($t, $p) { e.preventDefault(); var $interaction = $t.closest('.NB-interaction'); var feed_id = $interaction.data('feedId'); @@ -6907,38 +6909,38 @@ self.close_interactions_popover(); self.close_social_profile(); if (self.model.get_feed(feed_id)) { - self.open_social_stories(feed_id, {'story_id': story_id}); + self.open_social_stories(feed_id, { 'story_id': story_id }); } else { var comment_user_matches = feed_id.match(/social:(\d+)/, '$1'); if (comment_user_matches) user_id = parseInt(comment_user_matches[1], 10); var socialsub = self.model.add_social_feed({ - id: feed_id, - user_id: user_id, + id: feed_id, + user_id: user_id, username: username }); - self.load_social_feed_in_tryfeed_view(socialsub, {'story_id': story_id}); + self.load_social_feed_in_tryfeed_view(socialsub, { 'story_id': story_id }); } - }); - + }); + // = Activities Module ========================================== - - $.targetIs(e, { tagSelector: '.NB-activity-star' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-activity-star' }, function ($t, $p) { e.preventDefault(); var story_id = $t.closest('.NB-interaction').data('contentId'); - + self.close_interactions_popover(); self.close_social_profile(); - self.open_starred_stories({'story_id': story_id}); - }); - $.targetIs(e, { tagSelector: '.NB-activity-feedsub' }, function($t, $p){ + self.open_starred_stories({ 'story_id': story_id }); + }); + $.targetIs(e, { tagSelector: '.NB-activity-feedsub' }, function ($t, $p) { e.preventDefault(); var feed_id = $t.closest('.NB-interaction').data('feedId'); - + self.close_interactions_popover(); self.close_social_profile(); self.open_feed(feed_id); - }); - $.targetIs(e, { tagSelector: '.NB-activity-opml_import' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-activity-opml_import' }, function ($t, $p) { e.preventDefault(); self.close_interactions_popover(); self.close_social_profile(); @@ -6947,84 +6949,84 @@ 'page_number': 2, 'force_import': true }); - }); - $.targetIs(e, { tagSelector: '.NB-activity-opml_export' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-activity-opml_export' }, function ($t, $p) { e.preventDefault(); self.close_interactions_popover(); self.close_social_profile(); self.open_account_modal(); - }); - + }); + // = One-offs ===================================================== - + var clicked = false; - $.targetIs(e, { tagSelector: '#mouse-indicator' }, function($t, $p){ + $.targetIs(e, { tagSelector: '#mouse-indicator' }, function ($t, $p) { e.preventDefault(); self.lock_mouse_indicator(); - }); - $.targetIs(e, { tagSelector: '.NB-load-user-profile img' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-load-user-profile img' }, function ($t, $p) { e.preventDefault(); self.open_social_profile_modal(); }); - $.targetIs(e, { tagSelector: '.NB-progress-close' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-progress-close' }, function ($t, $p) { e.preventDefault(); self.hide_unfetched_feed_progress(true); }); - $.targetIs(e, { tagSelector: '.NB-module-next-page', childOf: '.NB-module-features' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-module-next-page', childOf: '.NB-module-features' }, function ($t, $p) { e.preventDefault(); self.load_feature_page(1); - }); - $.targetIs(e, { tagSelector: '.NB-module-previous-page', childOf: '.NB-module-features' }, function($t, $p){ + }); + $.targetIs(e, { tagSelector: '.NB-module-previous-page', childOf: '.NB-module-features' }, function ($t, $p) { e.preventDefault(); self.load_feature_page(-1); }); - $.targetIs(e, { tagSelector: '.NB-module-next-page', childOf: '.NB-module-howitworks' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-module-next-page', childOf: '.NB-module-howitworks' }, function ($t, $p) { e.preventDefault(); var page = $('.NB-howitworks-page.NB-active').prevAll('.NB-howitworks-page').length; - self.load_howitworks_page(page+1); - }); - $.targetIs(e, { tagSelector: '.NB-module-previous-page', childOf: '.NB-module-howitworks' }, function($t, $p){ + self.load_howitworks_page(page + 1); + }); + $.targetIs(e, { tagSelector: '.NB-module-previous-page', childOf: '.NB-module-howitworks' }, function ($t, $p) { e.preventDefault(); var page = $('.NB-howitworks-page.NB-active').prevAll('.NB-howitworks-page').length; - self.load_howitworks_page(page-1); + self.load_howitworks_page(page - 1); }); - $.targetIs(e, { tagSelector: '.NB-module-page-indicator', childOf: '.NB-module-howitworks' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-module-page-indicator', childOf: '.NB-module-howitworks' }, function ($t, $p) { e.preventDefault(); var page = $t.prevAll('.NB-module-page-indicator').length; self.load_howitworks_page(page); - }); - $.targetIs(e, { tagSelector: '.NB-splash-meta-about' }, function($t, $p){ - e.preventDefault(); - NEWSBLUR.about = new NEWSBLUR.About(); - }); - $.targetIs(e, { tagSelector: '.NB-splash-meta-faq' }, function($t, $p){ - e.preventDefault(); - NEWSBLUR.faq = new NEWSBLUR.Faq(); - }); - - }, - - handle_keyup: function(elem, e) { + }); + $.targetIs(e, { tagSelector: '.NB-splash-meta-about' }, function ($t, $p) { + e.preventDefault(); + NEWSBLUR.about = new NEWSBLUR.About(); + }); + $.targetIs(e, { tagSelector: '.NB-splash-meta-faq' }, function ($t, $p) { + e.preventDefault(); + NEWSBLUR.faq = new NEWSBLUR.Faq(); + }); + + }, + + handle_keyup: function (elem, e) { var self = this; - + }, - - handle_keystrokes: function() { + + handle_keystrokes: function () { var self = this; var $document = $(document); - + NEWSBLUR.hotkeys.initialize(); - - $document.bind('keydown', '?', function(e) { + + $document.bind('keydown', '?', function (e) { e.preventDefault(); self.open_keyboard_shortcuts_modal(); }); - $document.bind('keydown', 'shift+/', function(e) { + $document.bind('keydown', 'shift+/', function (e) { e.preventDefault(); self.open_keyboard_shortcuts_modal(); }); - $document.bind('keydown', 'down', function(e) { + $document.bind('keydown', 'down', function (e) { e.preventDefault(); if (NEWSBLUR.assets.preference('keyboard_verticalarrows') == 'scroll') { var amount = NEWSBLUR.assets.preference('arrow_scroll_spacing'); @@ -7033,7 +7035,7 @@ self.show_next_story(1); } }); - $document.bind('keydown', 'up', function(e) { + $document.bind('keydown', 'up', function (e) { e.preventDefault(); if (NEWSBLUR.assets.preference('keyboard_verticalarrows') == 'scroll') { var amount = NEWSBLUR.assets.preference('arrow_scroll_spacing'); @@ -7041,40 +7043,40 @@ } else { self.show_next_story(-1); } - }); - $document.bind('keydown', 'j', function(e) { + }); + $document.bind('keydown', 'j', function (e) { e.preventDefault(); self.show_next_story(1); }); - $document.bind('keydown', 'k', function(e) { + $document.bind('keydown', 'k', function (e) { e.preventDefault(); self.show_next_story(-1); - }); - $document.bind('keydown', 'shift+j', function(e) { + }); + $document.bind('keydown', 'shift+j', function (e) { e.preventDefault(); self.show_next_feed(1); }); - $document.bind('keydown', 'shift+k', function(e) { + $document.bind('keydown', 'shift+k', function (e) { e.preventDefault(); self.show_next_feed(-1); }); - $document.bind('keydown', 'shift+n', function(e) { + $document.bind('keydown', 'shift+n', function (e) { e.preventDefault(); self.show_next_feed(1); }); - $document.bind('keydown', 'shift+p', function(e) { + $document.bind('keydown', 'shift+p', function (e) { e.preventDefault(); self.show_next_feed(-1); - }); - $document.bind('keydown', 'shift+down', function(e) { + }); + $document.bind('keydown', 'shift+down', function (e) { e.preventDefault(); self.show_next_feed(1); }); - $document.bind('keydown', 'shift+up', function(e) { + $document.bind('keydown', 'shift+up', function (e) { e.preventDefault(); self.show_next_feed(-1); }); - $document.bind('keydown', 'left', function(e) { + $document.bind('keydown', 'left', function (e) { e.preventDefault(); if (NEWSBLUR.assets.preference('keyboard_horizontalarrows') == 'site') { self.show_next_feed(-1); @@ -7082,7 +7084,7 @@ self.switch_taskbar_view_direction(-1); } }); - $document.bind('keydown', 'right', function(e) { + $document.bind('keydown', 'right', function (e) { e.preventDefault(); if (NEWSBLUR.assets.preference('keyboard_horizontalarrows') == 'site') { self.show_next_feed(1); @@ -7090,27 +7092,27 @@ self.switch_taskbar_view_direction(1); } }); - $document.bind('keydown', 'h', function(e) { + $document.bind('keydown', 'h', function (e) { e.preventDefault(); self.switch_taskbar_view_direction(-1); }); - $document.bind('keydown', 'l', function(e) { + $document.bind('keydown', 'l', function (e) { e.preventDefault(); self.switch_taskbar_view_direction(1); }); - $document.bind('keydown', 'r', function(e) { + $document.bind('keydown', 'r', function (e) { e.preventDefault(); if (self.active_feed) { self.reload_feed(); } }); - $document.bind('keydown', 'shift+r', function(e) { + $document.bind('keydown', 'shift+r', function (e) { e.preventDefault(); if (self.active_feed) { self.force_instafetch_stories(); } }); - $document.bind('keydown', 'enter', function(e) { + $document.bind('keydown', 'enter', function (e) { e.preventDefault(); if (self.flags['feed_view_showing_story_view']) { self.switch_to_correct_view(); @@ -7119,7 +7121,7 @@ NEWSBLUR.app.story_tab_view.open_story(); } }); - $document.bind('keydown', 'return', function(e) { + $document.bind('keydown', 'return', function (e) { e.preventDefault(); if (self.flags['feed_view_showing_story_view']) { self.switch_to_correct_view(); @@ -7128,9 +7130,9 @@ NEWSBLUR.app.story_tab_view.open_story(); } }); - $document.bind('keydown', 'shift+enter', function(e) { + $document.bind('keydown', 'shift+enter', function (e) { e.preventDefault(); - if (_.contains(['list', 'grid'], + if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { if (!self.active_story) NEWSBLUR.reader.show_next_story(1); self.active_story.story_title_view.render_inline_story_detail(true); @@ -7140,9 +7142,9 @@ NEWSBLUR.app.text_tab_view.fetch_and_render(null, true); } }); - $document.bind('keydown', 'shift+return', function(e) { + $document.bind('keydown', 'shift+return', function (e) { e.preventDefault(); - if (_.contains(['list', 'grid'], + if (_.contains(['list', 'grid'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { if (!self.active_story) NEWSBLUR.reader.show_next_story(1); self.active_story.story_title_view.render_inline_story_detail(true); @@ -7152,41 +7154,41 @@ NEWSBLUR.app.text_tab_view.fetch_and_render(null, true); } }); - $document.bind('keydown', 'space', function(e) { + $document.bind('keydown', 'space', function (e) { e.preventDefault(); var amount = NEWSBLUR.assets.preference('space_scroll_spacing'); self.page_in_story(amount, 1); }); - $document.bind('keydown', 'shift+space', function(e) { + $document.bind('keydown', 'shift+space', function (e) { e.preventDefault(); var amount = NEWSBLUR.assets.preference('space_scroll_spacing'); self.page_in_story(amount, -1); }); - $document.bind('keydown', 'shift+u', function(e) { + $document.bind('keydown', 'shift+u', function (e) { e.preventDefault(); self.toggle_sidebar(); }); - $document.bind('keydown', 'shift+f', function(e) { + $document.bind('keydown', 'shift+f', function (e) { e.preventDefault(); self.toggle_sidebar(); self.toggle_story_titles_pane(true); }); - $document.bind('keydown', 'n', function(e) { + $document.bind('keydown', 'n', function (e) { e.preventDefault(); self.open_next_unread_story_across_feeds(); }); - $document.bind('keydown', 'p', function(e) { + $document.bind('keydown', 'p', function (e) { e.preventDefault(); self.show_previous_story(); }); - $document.bind('keydown', 'c', function(e) { + $document.bind('keydown', 'c', function (e) { e.preventDefault(); NEWSBLUR.app.story_list.scroll_to_selected_story(self.active_story, { scroll_to_comments: true, scroll_offset: -50 }); }); - $document.bind('keydown', 'x', function(e) { + $document.bind('keydown', 'x', function (e) { e.preventDefault(); var story = NEWSBLUR.reader.active_story; if (story && story.get('selected')) { @@ -7195,11 +7197,11 @@ NEWSBLUR.reader.active_story.set('selected', true); } }); - $document.bind('keydown', 'shift+x', function(e) { + $document.bind('keydown', 'shift+x', function (e) { e.preventDefault(); NEWSBLUR.reader.active_story.story_view.expand_story(); }); - $document.bind('keydown', 'shift+y', function(e) { + $document.bind('keydown', 'shift+y', function (e) { e.preventDefault(); if (!NEWSBLUR.reader.active_story) return; var story = NEWSBLUR.assets.get_story(NEWSBLUR.reader.active_story); @@ -7207,14 +7209,14 @@ var direction = 'newer'; var order = NEWSBLUR.assets.view_setting(self.active_feed, 'order'); if (order == 'oldest') direction = 'older'; - + if (self.flags.river_view && !self.flags.social_view) { self.mark_folder_as_read(self.active_folder, timestamp, direction); } else { self.mark_feed_as_read(self.active_feed, timestamp, direction); } }); - $document.bind('keydown', 'shift+b', function(e) { + $document.bind('keydown', 'shift+b', function (e) { e.preventDefault(); if (!NEWSBLUR.reader.active_story) return; var story = NEWSBLUR.assets.get_story(NEWSBLUR.reader.active_story); @@ -7229,20 +7231,20 @@ self.mark_feed_as_read(self.active_feed, timestamp, direction); } }); - $document.bind('keydown', 'm', function(e) { + $document.bind('keydown', 'm', function (e) { e.preventDefault(); // self.show_last_unread_story(); self.mark_active_story_read(); }); - $document.bind('keydown', 'shift+m', function(e) { + $document.bind('keydown', 'shift+m', function (e) { e.preventDefault(); self.show_last_unread_story(); }); - $document.bind('keydown', 'b', function(e) { + $document.bind('keydown', 'b', function (e) { e.preventDefault(); self.show_previous_story(); }); - $document.bind('keydown', 's', function(e) { + $document.bind('keydown', 's', function (e) { e.preventDefault(); if (self.active_story) { var story_id = self.active_story.id; @@ -7250,93 +7252,93 @@ story.toggle_starred(); } }); - $document.bind('keypress', '+', function(e) { + $document.bind('keypress', '+', function (e) { e.preventDefault(); self.move_intelligence_slider(1); }); - $document.bind('keypress', '-', function(e) { + $document.bind('keypress', '-', function (e) { e.preventDefault(); self.move_intelligence_slider(-1); }); - $document.bind('keypress', 'shift+l', function(e) { + $document.bind('keypress', 'shift+l', function (e) { e.preventDefault(); self.toggle_read_filter(); }); - $document.bind('keypress', 'shift+d', function(e) { + $document.bind('keypress', 'shift+d', function (e) { e.preventDefault(); self.show_splash_page(); }); - $document.bind('keydown', 'esc', function(e) { + $document.bind('keydown', 'esc', function (e) { e.preventDefault(); if (NEWSBLUR.assets.preference("keyboard-ignore-esc")) return; - if (!_.keys($.modal.impl.d).length && - !NEWSBLUR.ReaderPopover.is_open() && + if (!_.keys($.modal.impl.d).length && + !NEWSBLUR.ReaderPopover.is_open() && !self.flags['feed_list_showing_manage_menu']) { self.show_splash_page(); } }); - $document.bind('keypress', 't', function(e) { + $document.bind('keypress', 't', function (e) { e.preventDefault(); self.open_story_trainer(); }); - $document.bind('keypress', 'shift+t', function(e) { + $document.bind('keypress', 'shift+t', function (e) { e.preventDefault(); self.open_feed_intelligence_modal(1); }); - $document.bind('keypress', 'a', function(e) { + $document.bind('keypress', 'a', function (e) { e.preventDefault(); self.open_add_feed_modal(); }); - $document.bind('keypress', 'o', function(e) { + $document.bind('keypress', 'o', function (e) { e.preventDefault(); var story_id = self.active_story; if (!story_id) return; var story = self.model.get_story(story_id); story.open_story_in_new_tab(true); }); - $document.bind('keypress', 'v', function(e) { + $document.bind('keypress', 'v', function (e) { e.preventDefault(); var story_id = self.active_story; if (!story_id) return; var story = self.model.get_story(story_id); story.open_story_in_new_tab(true); }); - $document.bind('keypress', 'shift+v', function(e) { + $document.bind('keypress', 'shift+v', function (e) { e.preventDefault(); var story_id = self.active_story; if (!story_id) return; var story = self.model.get_story(story_id); story.open_story_in_new_tab(); }); - $document.bind('keypress', 'e', function(e) { + $document.bind('keypress', 'e', function (e) { e.preventDefault(); var story = self.active_story; if (!story) return; self.send_story_to_email(story); }); - $document.bind('keydown', 'shift+a', function(e) { + $document.bind('keydown', 'shift+a', function (e) { e.preventDefault(); self.maybe_mark_all_as_read(); }); - $document.bind('keydown', 'shift+e', function(e) { + $document.bind('keydown', 'shift+e', function (e) { e.preventDefault(); self.open_river_stories(); }); - $document.bind('keydown', 'u', function(e) { + $document.bind('keydown', 'u', function (e) { e.preventDefault(); self.mark_active_story_read(); }); - $document.bind('keydown', 'g', function(e) { + $document.bind('keydown', 'g', function (e) { e.preventDefault(); NEWSBLUR.app.feed_selector.toggle(); }); - $document.bind('keydown', '/', function(e) { + $document.bind('keydown', '/', function (e) { e.preventDefault(); if (NEWSBLUR.app.story_titles_header.search_view) { NEWSBLUR.app.story_titles_header.search_view.focus(); } }); - $document.bind('keydown', 'shift+s', function(e) { + $document.bind('keydown', 'shift+s', function (e) { e.preventDefault(); if (self.active_story) { var view = 'feed'; @@ -7349,7 +7351,7 @@ } }); } - + }); })(jQuery); diff --git a/media/js/newsblur/reader/reader_account.js b/media/js/newsblur/reader/reader_account.js index 2d63b0202b..8409dff130 100644 --- a/media/js/newsblur/reader/reader_account.js +++ b/media/js/newsblur/reader/reader_account.js @@ -1,16 +1,16 @@ -NEWSBLUR.ReaderAccount = function(options) { +NEWSBLUR.ReaderAccount = function (options) { var defaults = { 'width': 700, 'animate_email': false, 'change_password': false, - 'onOpen': _.bind(function() { + 'onOpen': _.bind(function () { this.animate_fields(); $(window).trigger('resize.simplemodal'); }, this) }; - + this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; + this.model = NEWSBLUR.assets; this.runner(); }; @@ -19,9 +19,9 @@ NEWSBLUR.ReaderAccount.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderAccount.prototype.constructor = NEWSBLUR.ReaderAccount; _.extend(NEWSBLUR.ReaderAccount.prototype, { - - runner: function() { - this.options.onOpen = _.bind(function() { + + runner: function () { + this.options.onOpen = _.bind(function () { // $(window).resize(); }, this); this.make_modal(); @@ -30,18 +30,18 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { this.$modal.bind('click', $.rescope(this.handle_click, this)); this.handle_change(); this.select_preferences(); - + this.fetch_payment_history(); this.render_dates(); - + if (this.options.tab) { this.switch_tab(this.options.tab); } }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-preferences NB-modal-account NB-modal' }, [ $.make('div', { className: 'NB-modal-tabs' }, [ $.make('div', { className: 'NB-modal-loading' }), @@ -63,10 +63,10 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('input', { id: 'NB-preference-username', type: 'text', name: 'username', value: NEWSBLUR.Globals.username }) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ $.make('label', { 'for': 'NB-preference-username' }, 'Username'), - $.make('div', { className: 'NB-preference-error'}) + $.make('div', { className: 'NB-preference-error' }) ]) ]), $.make('div', { className: 'NB-preference NB-preference-email' }, [ @@ -75,10 +75,10 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('input', { id: 'NB-preference-email', type: 'text', name: 'email', value: NEWSBLUR.Globals.email }) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ $.make('label', { 'for': 'NB-preference-email' }, 'Email address'), - $.make('div', { className: 'NB-preference-error'}) + $.make('div', { className: 'NB-preference-error' }) ]) ]), $.make('div', { className: 'NB-preference NB-preference-password' }, [ @@ -92,16 +92,16 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('input', { id: 'NB-preference-password-new', type: 'password', name: 'new_password', value: '' }) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Change password', - $.make('div', { className: 'NB-preference-error'}) + $.make('div', { className: 'NB-preference-error' }) ]) ]), $.make('div', { className: 'NB-preference NB-preference-opml' }, [ $.make('div', { className: 'NB-preference-options' }, [ $.make('a', { className: 'NB-modal-submit-button NB-modal-submit-green', href: NEWSBLUR.URLs['opml-export'] }, 'Download OPML') ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Backup your sites', $.make('div', { className: 'NB-preference-sublabel' }, 'Download this XML file as a backup') ]) @@ -111,7 +111,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-preference-saved-stories-date' }), $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-red NB-account-delete-saved-stories' }, 'Delete my saved stories') ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Erase your saved stories' ]) ]), @@ -119,7 +119,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-preference-options' }, [ $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-red NB-account-delete-all-sites' }, 'Delete all of my sites') ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Erase yourself', $.make('div', { className: 'NB-preference-sublabel' }, 'Friendly note: You will be emailed a backup of your sites') ]) @@ -128,7 +128,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-preference-options' }, [ $.make('a', { className: 'NB-modal-submit-button NB-modal-submit-red', href: NEWSBLUR.URLs['delete-account'] }, 'Delete my account') ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Erase yourself permanently', $.make('div', { className: 'NB-preference-sublabel' }, 'Warning: This is actually permanent') ]) @@ -138,13 +138,13 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-preference NB-preference-premium' }, [ $.make('div', { className: 'NB-preference-options' }, [ (!NEWSBLUR.Globals.is_premium && $.make('div', [ - $.make('div', {style: 'margin-bottom: 12px;' }, [ + $.make('div', { style: 'margin-bottom: 12px;' }, [ 'You have a ', $.make('b', 'free account'), '.' ]), - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green NB-account-premium-modal' + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green NB-account-premium-modal' }, 'Upgrade to a Premium account') ])), (NEWSBLUR.Globals.is_premium && $.make('div', [ @@ -153,12 +153,12 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { (!NEWSBLUR.Globals.is_pro && NEWSBLUR.Globals.is_archive && $.make('b', 'premium archive account')), (!NEWSBLUR.Globals.is_pro && !NEWSBLUR.Globals.is_archive && NEWSBLUR.Globals.is_premium && $.make('b', 'premium account')), '.', - (!NEWSBLUR.Globals.is_archive && $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green NB-account-premium-modal NB-block' - }, 'Upgrade to a Premium Archive account')) + (!NEWSBLUR.Globals.is_archive && $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green NB-account-premium-modal NB-block' + }, 'Upgrade to a Premium Archive account')) ])) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Premium status' ]) ]), @@ -168,7 +168,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-block NB-premium-expire-container' }, this.make_premium_expire()), $.make('a', { href: '#', className: 'NB-block NB-account-premium-renew NB-modal-submit-button NB-modal-submit-green' }, 'Change your credit card') ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Premium details' ]) ])), @@ -178,13 +178,13 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('li', { className: 'NB-payments-loading' }, 'Loading...') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Payment history' ]) ]), (NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-preference NB-preference-premium-cancel' }, [ $.make('div', { className: 'NB-preference-options NB-premium-renewal-container' }, this.make_premium_renewal()), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Premium renewal' ]) ])) @@ -205,7 +205,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Emails' ]) ]) @@ -213,13 +213,13 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-tab NB-tab-custom' }, [ $.make('fieldset', [ $.make('legend', 'Custom CSS'), - $.make('div', { className: 'NB-modal-section NB-profile-editor-blurblog-custom-css'}, [ + $.make('div', { className: 'NB-modal-section NB-profile-editor-blurblog-custom-css' }, [ $.make('textarea', { 'className': 'NB-account-custom-css', name: 'custom_css' }, _.string.trim($("#NB-custom-css").text())) ]) ]), $.make('fieldset', [ $.make('legend', 'Custom JavaScript'), - $.make('div', { className: 'NB-modal-section NB-profile-editor-blurblog-custom-js'}, [ + $.make('div', { className: 'NB-modal-section NB-profile-editor-blurblog-custom-js' }, [ $.make('textarea', { 'className': 'NB-account-custom-javascript', name: 'custom_js' }, _.string.trim($("#NB-custom-js").text())) ]) ]) @@ -227,53 +227,53 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $.make('div', { className: 'NB-modal-submit' }, [ $.make('input', { type: 'submit', disabled: 'true', className: 'NB-modal-submit-button NB-modal-submit-green NB-disabled', value: 'Change what you like above...' }) ]) - ]).bind('submit', function(e) { + ]).bind('submit', function (e) { e.preventDefault(); self.save_account_settings(); return false; }) ]); }, - - render_dates: function() { + + render_dates: function () { var now = new Date(); var this_year = now.getFullYear(); var this_month = now.getMonth(); var this_day = now.getDate(); - + var $dates = $(".NB-preference-saved-stories-date", this.$modal); - + var $months = $.make('select', { name: 'month' }); - _.each(NEWSBLUR.utils.monthNames, function(name, i) { - var $option = $.make('option', { value: i+"" }, name); + _.each(NEWSBLUR.utils.monthNames, function (name, i) { + var $option = $.make('option', { value: i + "" }, name); if (this_month == i) $option.prop('selected', true); $months.append($option); }); var $days = $.make('select', { name: 'day' }); - _.each(_.range(0, 31), function(name, i) { - var $option = $.make('option', { value: i+1+"" }, i+1); - if (this_day == i+1) $option.prop('selected', true); + _.each(_.range(0, 31), function (name, i) { + var $option = $.make('option', { value: i + 1 + "" }, i + 1); + if (this_day == i + 1) $option.prop('selected', true); $days.append($option); }); var $years = $.make('select', { name: 'year' }); - _.each(_.range(2009, this_year+1), function(name, i) { - var $option = $.make('option', { value: name+"" }, name); + _.each(_.range(2009, this_year + 1), function (name, i) { + var $option = $.make('option', { value: name + "" }, name); if (this_year == name) $option.prop('selected', true); $years.append($option); }); - + $dates.append($.make('span', 'Older than: ')); $dates.append($months); $dates.append($days); $dates.append($years); }, - - animate_fields: function() { + + animate_fields: function () { if (this.options.animate_email) { this.switch_tab('emails'); - _.delay(_.bind(function() { + _.delay(_.bind(function () { var $emails = $('.NB-preference-emails', this.$modal); var bgcolor = $emails.css('backgroundColor'); $emails.css('backgroundColor', bgcolor).animate({ @@ -282,7 +282,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { 'queue': false, 'duration': 1200, 'easing': 'easeInQuad', - 'complete': function() { + 'complete': function () { $emails.animate({ 'backgroundColor': bgcolor }, { @@ -294,7 +294,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { }); }, this), 200); } else if (this.options.change_password) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { var $emails = $('.NB-preference-password', this.$modal); var bgcolor = $emails.css('backgroundColor'); $emails.css('backgroundColor', bgcolor).animate({ @@ -303,7 +303,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { 'queue': false, 'duration': 1200, 'easing': 'easeInQuad', - 'complete': function() { + 'complete': function () { $emails.animate({ 'backgroundColor': bgcolor }, { @@ -317,22 +317,22 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { } }, - - close_and_load_premium: function(options) { - options = _.extend({}, {'premium_only': true}, options); - this.close(function() { + + close_and_load_premium: function (options) { + options = _.extend({}, { 'premium_only': true }, options); + this.close(function () { NEWSBLUR.reader.open_feedchooser_modal(options); }); }, - - cancel_premium: function() { + + cancel_premium: function () { var $cancel = $(".NB-account-premium-cancel", this.$modal); $cancel.attr('disabled', 'disabled'); $cancel.removeClass('NB-modal-submit-red'); $cancel.addClass('NB-modal-submit-grey'); $cancel.text("Cancelling..."); - - var post_cancel = function(message) { + + var post_cancel = function (message) { $cancel.remove(); $(".NB-account-payment.NB-scheduled").addClass('NB-canceled'); $(".NB-preference-premium-cancel .NB-error").remove(); @@ -341,98 +341,98 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { }, message).fadeIn(500).css('display', 'block')); }; - this.model.cancel_premium_subscription(_.bind(function(data) { + this.model.cancel_premium_subscription(_.bind(function (data) { NEWSBLUR.Globals.premium_renewal = false; post_cancel("Your subscription will no longer automatically renew."); - }, this), _.bind(function(data) { + }, this), _.bind(function (data) { NEWSBLUR.Globals.premium_renewal = false; post_cancel(data.message || "You have no active subscriptions."); }, this)); }, - - delete_all_sites: function() { + + delete_all_sites: function () { var $link = $(".NB-account-delete-all-sites", this.$modal); if (window.confirm("Positive you want to delete everything?")) { - NEWSBLUR.assets.delete_all_sites(_.bind(function() { + NEWSBLUR.assets.delete_all_sites(_.bind(function () { NEWSBLUR.assets.load_feeds(); $link.replaceWith($.make('div', 'Everything has been deleted.')); - }, this), _.bind(function() { + }, this), _.bind(function () { $link.replaceWith($.make('div', { className: 'NB-error' }, 'There was a problem deleting your sites.')); }, this)); } }, - - delete_saved_stories: function() { + + delete_saved_stories: function () { var $link = $(".NB-account-delete-saved-stories", this.$modal); var $loading = $('.NB-modal-loading', this.$modal); var year = parseInt($("select[name=year]", this.$modal).val(), 10); var month = parseInt($("select[name=month]", this.$modal).val(), 10); var day = parseInt($("select[name=day]", this.$modal).val(), 10); - + var timestamp = (new Date(year, month, day)).getTime() / 1000; if (window.confirm("Positive you want to delete your saved stories?")) { $loading.addClass('NB-active'); $link.attr('disabled', 'disabled'); $link.text("Deleting..."); - NEWSBLUR.assets.delete_saved_stories(timestamp, _.bind(function(data) { + NEWSBLUR.assets.delete_saved_stories(timestamp, _.bind(function (data) { $loading.removeClass('NB-active'); NEWSBLUR.reader.update_starred_count(); - $link.replaceWith($.make('div', Inflector.pluralize('story', data.stories_deleted, true) + ' ' + Inflector.pluralize('has', data.stories_deleted) + - ' been deleted.')); - }, this), _.bind(function() { + $link.replaceWith($.make('div', Inflector.pluralize('story', data.stories_deleted, true) + ' ' + Inflector.pluralize('has', data.stories_deleted) + + ' been deleted.')); + }, this), _.bind(function () { $loading.removeClass('NB-active'); NEWSBLUR.reader.update_starred_count(); $link.replaceWith($.make('div', { className: 'NB-error' }, 'There was a problem deleting your saved stories.')).show(); }, this)); } }, - - handle_cancel: function() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); }, - - select_preferences: function() { + + select_preferences: function () { var pref = this.model.preference; - $('input[name=send_emails]', this.$modal).each(function() { - if ($(this).val() == ""+pref('send_emails')) { + $('input[name=send_emails]', this.$modal).each(function () { + if ($(this).val() == "" + pref('send_emails')) { $(this).prop('checked', true); return false; } }); }, - - serialize_preferences: function() { + + serialize_preferences: function () { var preferences = {}; - $('input[type=radio]:checked, select, textarea, input[type=text], input[type=password]', this.$modal).each(function() { - var name = $(this).attr('name'); + $('input[type=radio]:checked, select, textarea, input[type=text], input[type=password]', this.$modal).each(function () { + var name = $(this).attr('name'); var preference = preferences[name] = $(this).val(); - if (preference == 'true') preferences[name] = true; + if (preference == 'true') preferences[name] = true; else if (preference == 'false') preferences[name] = false; }); - $('input[type=checkbox]', this.$modal).each(function() { + $('input[type=checkbox]', this.$modal).each(function () { preferences[$(this).attr('name')] = $(this).is(':checked'); }); return preferences; }, - - save_account_settings: function() { + + save_account_settings: function () { var self = this; var form = this.serialize_preferences(); $('.NB-preference-error', this.$modal).text(''); $('input[type=submit]', this.$modal).val('Saving...').attr('disabled', true).addClass('NB-disabled'); - + NEWSBLUR.log(["form['send_emails']", form['send_emails']]); this.model.preference('send_emails', form['send_emails']); - this.model.save_account_settings(form, function(data) { + this.model.save_account_settings(form, function (data) { if (data.code == -1) { $('.NB-preference-username .NB-preference-error', this.$modal).text(data.message); return self.disable_save(); @@ -443,7 +443,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $('.NB-preference-password .NB-preference-error', this.$modal).text(data.message); return self.disable_save(); } - + NEWSBLUR.Globals.username = data.payload.username; NEWSBLUR.Globals.email = data.payload.email; $('.NB-module-account-username').text(NEWSBLUR.Globals.username); @@ -451,7 +451,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { self.close(); }); }, - + make_premium_expire: function () { return $.make('div', [ $.make('span', { className: 'NB-raquo' }, '»'), @@ -477,9 +477,9 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { }, fetch_payment_history: function () { - this.model.fetch_payment_history(NEWSBLUR.Globals.user_id, _.bind(function(data) { + this.model.fetch_payment_history(NEWSBLUR.Globals.user_id, _.bind(function (data) { var $history = $('.NB-account-payments', this.$modal).empty(); - + if (NEWSBLUR.Globals.premium_renewal != data.premium_renewal) { NEWSBLUR.Globals.premium_renewal = data.premium_renewal; $(".NB-premium-renewal-container", this.$modal).html(this.make_premium_renewal()); @@ -494,7 +494,7 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { } if (!data.payments || !data.payments.length) { - $history.append($.make('li', { className: 'NB-account-payment' }, [ + $history.append($.make('li', { className: 'NB-account-payment' }, [ $.make('i', 'No payments found.') ])); } else { @@ -514,15 +514,15 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { $(window).resize(); }, this)); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function ($t, $p) { e.preventDefault(); var newtab; if ($t.hasClass('NB-modal-tab-account')) { @@ -535,53 +535,53 @@ _.extend(NEWSBLUR.ReaderAccount.prototype, { newtab = 'custom'; } self.switch_tab(newtab); - }); - $.targetIs(e, { tagSelector: '.NB-account-premium-modal' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-account-premium-modal' }, function ($t, $p) { e.preventDefault(); - + self.close_and_load_premium(); - }); - $.targetIs(e, { tagSelector: '.NB-account-premium-renew' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-account-premium-renew' }, function ($t, $p) { e.preventDefault(); - - self.close_and_load_premium({'renew': true}); - }); - $.targetIs(e, { tagSelector: '.NB-account-premium-cancel' }, function($t, $p) { + + self.close_and_load_premium({ 'renew': true }); + }); + $.targetIs(e, { tagSelector: '.NB-account-premium-cancel' }, function ($t, $p) { e.preventDefault(); - + self.cancel_premium(); - }); - $.targetIs(e, { tagSelector: '.NB-account-delete-all-sites' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-account-delete-all-sites' }, function ($t, $p) { e.preventDefault(); - + self.delete_all_sites(); - }); - $.targetIs(e, { tagSelector: '.NB-account-delete-saved-stories' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-account-delete-saved-stories' }, function ($t, $p) { e.preventDefault(); - + self.delete_saved_stories(); - }); - $.targetIs(e, { tagSelector: '.NB-modal-cancel' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-modal-cancel' }, function ($t, $p) { e.preventDefault(); - + self.close(); }); }, - - handle_change: function() { + + handle_change: function () { $('input[type=radio],input[type=checkbox],select,input', this.$modal).bind('change', _.bind(this.enable_save, this)); $('input', this.$modal).bind('keydown', _.bind(this.enable_save, this)); $('.NB-tab-custom', this.$modal).delegate('input[type=text],textarea', 'keydown', _.bind(this.enable_save, this)); $('.NB-tab-custom', this.$modal).delegate('input,textarea', 'change', _.bind(this.enable_save, this)); }, - - enable_save: function() { + + enable_save: function () { $('input[type=submit]', this.$modal).removeAttr('disabled').removeClass('NB-disabled').val('Save My Account'); }, - - disable_save: function() { + + disable_save: function () { this.resize(); $('input[type=submit]', this.$modal).attr('disabled', true).addClass('NB-disabled').val('Change what you like above...'); } - + }); diff --git a/media/js/newsblur/reader/reader_add_feed.js b/media/js/newsblur/reader/reader_add_feed.js index 88762cb900..5491017b49 100644 --- a/media/js/newsblur/reader/reader_add_feed.js +++ b/media/js/newsblur/reader/reader_add_feed.js @@ -1,10 +1,10 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ - + className: "NB-add-popover", - + options: { 'width': 380, - 'anchor': function() { + 'anchor': function () { return NEWSBLUR.reader.$s.$add_button; }, 'placement': 'top -left', @@ -12,46 +12,46 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ top: 6, left: 1 }, - 'onOpen': _.bind(function() { + 'onOpen': _.bind(function () { this.focus_add_feed(); }, this) }, - + events: { - "click .NB-modal-cancel" : "close", - "click .NB-add-url-submit" : "save_add_url", - "click .NB-add-folder-icon" : "open_add_folder", - "click .NB-add-folder-submit" : "save_add_folder", - "click .NB-add-import-button" : "close_and_open_import", - "focus .NB-add-url" : "handle_focus_add_site", - "blur .NB-add-url" : "handle_blur_add_site" + "click .NB-modal-cancel": "close", + "click .NB-add-url-submit": "save_add_url", + "click .NB-add-folder-icon": "open_add_folder", + "click .NB-add-folder-submit": "save_add_folder", + "click .NB-add-import-button": "close_and_open_import", + "focus .NB-add-url": "handle_focus_add_site", + "blur .NB-add-url": "handle_blur_add_site" }, - - initialize: function(options) { + + initialize: function (options) { this.options = _.extend({}, this.options, options); NEWSBLUR.ReaderPopover.prototype.initialize.call(this); this.model = NEWSBLUR.assets; this.render(); this.handle_keystrokes(); this.setup_autocomplete(); - + // this.setup_chosen(); this.focus_add_feed(); }, - on_show: function() { + on_show: function () { this.options.onOpen(); }, - - on_hide: function() { - + + on_hide: function () { + }, - - render: function() { + + render: function () { var self = this; NEWSBLUR.ReaderPopover.prototype.render.call(this); - + this.$el.html($.make('div', { className: 'NB-add' }, [ $.make('div', { className: 'NB-add-form' }, [ $.make('div', { className: 'NB-fieldset NB-modal-submit' }, [ @@ -83,30 +83,30 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ $.make('div', { className: 'NB-fieldset-fields' }, [ $.make('div', { className: 'NB-add-import-button NB-modal-submit-green NB-modal-submit-button' }, [ 'Import from Google Reader or upload OPML', - $.make('img', { className: 'NB-add-google-reader-arrow', src: NEWSBLUR.Globals['MEDIA_URL']+'img/icons/silk/arrow_right.png' }) + $.make('img', { className: 'NB-add-google-reader-arrow', src: NEWSBLUR.Globals['MEDIA_URL'] + 'img/icons/silk/arrow_right.png' }) ]), $.make('div', { className: 'NB-add-danger' }, (NEWSBLUR.Globals.is_authenticated && _.size(this.model.feeds) > 0 && [ - $.make('img', { src: NEWSBLUR.Globals['MEDIA_URL']+'img/icons/silk/server_go.png' }), + $.make('img', { src: NEWSBLUR.Globals['MEDIA_URL'] + 'img/icons/silk/server_go.png' }), 'This will erase all existing feeds and folders.' ])) ]) ]) ]) ])); - + if (NEWSBLUR.Globals.is_anonymous) { this.$el.addClass('NB-signed-out'); } return this; }, - - focus_add_feed: function() { - var $add = this.options.init_folder ? - this.$('.NB-add-folder-input') : - this.$('.NB-add-url'); + + focus_add_feed: function () { + var $add = this.options.init_folder ? + this.$('.NB-add-folder-input') : + this.$('.NB-add-url'); if (!NEWSBLUR.Globals.is_anonymous) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { if (this.options.init_folder) { this.open_add_folder(); } @@ -114,11 +114,11 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ }, this), 200); } }, - - setup_autocomplete: function() { + + setup_autocomplete: function () { var self = this; var $add = this.$('.NB-add-url'); - + $add.autocomplete({ minLength: 1, appendTo: ".NB-add-form", @@ -128,32 +128,32 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ at: "left top", collision: "none" }, - select: function(e, ui) { + select: function (e, ui) { $add.val(ui.item.value); // self.save_add_url(); return false; }, - search: function(e, ui) { + search: function (e, ui) { }, - open: function(e, ui) { + open: function (e, ui) { if (!$add.is(":focus")) { e.preventDefault(); $add.autocomplete('close'); return false; } }, - close: function(e, ui) { + close: function (e, ui) { }, - change: function(e, ui) { + change: function (e, ui) { } - }).data("ui-autocomplete")._renderItem = function(ul, item) { + }).data("ui-autocomplete")._renderItem = function (ul, item) { var feed = new NEWSBLUR.Models.Feed(item); return $.make('li', [ $.make('a', [ - $.make('div', { className: 'NB-add-autocomplete-subscribers'}, Inflector.pluralize(' subscriber', item.num_subscribers, true)), + $.make('div', { className: 'NB-add-autocomplete-subscribers' }, Inflector.pluralize(' subscriber', item.num_subscribers, true)), $.make('img', { className: 'NB-add-autocomplete-favicon', src: $.favicon(feed) }), - $.make('div', { className: 'NB-add-autocomplete-title'}, item.label), - $.make('div', { className: 'NB-add-autocomplete-address'}, item.value) + $.make('div', { className: 'NB-add-autocomplete-title' }, item.label), + $.make('div', { className: 'NB-add-autocomplete-address' }, item.value) ]) ]).data("ui-autocomplete-item", item).prependTo(ul); }; @@ -162,74 +162,74 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ ul.outerWidth(this.element.outerWidth()); }; }, - - handle_focus_add_site: function() { + + handle_focus_add_site: function () { var $add = this.$('.NB-add-url'); $add.autocomplete('search'); }, - - handle_blur_add_site: function() { + + handle_blur_add_site: function () { var $add = this.$('.NB-add-url'); $add.autocomplete('close'); }, - - setup_chosen: function() { + + setup_chosen: function () { var $select = this.$('select'); $select.chosen(); }, - - handle_keystrokes: function() { + + handle_keystrokes: function () { var self = this; - - this.$('.NB-add-url').bind('keyup', 'return', function(e) { + + this.$('.NB-add-url').bind('keyup', 'return', function (e) { e.preventDefault(); self.save_add_url(); - }); - - this.$('.NB-add-folder-input').bind('keyup', 'return', function(e) { + }); + + this.$('.NB-add-folder-input').bind('keyup', 'return', function (e) { e.preventDefault(); self.save_add_folder(); - }); + }); }, - close_and_open_import: function() { - this.close(function() { + close_and_open_import: function () { + this.close(function () { NEWSBLUR.reader.open_intro_modal({ 'page_number': 2, 'force_import': true }); }); }, - + // =========== // = Actions = // =========== - - save_add_url: function() { + + save_add_url: function () { var $submit = this.$('.NB-add-url-submit'); var $error = this.$('.NB-error'); var $loading = this.$('.NB-add-site .NB-loading'); - + var url = this.$('.NB-add-url').val(); var folder = this.$('.NB-folders').val(); - + $error.slideUp(300); $loading.addClass('NB-active'); $submit.addClass('NB-disabled').text('Adding...'); - + NEWSBLUR.reader.flags['reloading_feeds'] = true; this.model.save_add_url(url, folder, $.rescope(this.post_save_add_url, this), $.rescope(this.error, this)); }, - - post_save_add_url: function(e, data) { + + post_save_add_url: function (e, data) { NEWSBLUR.log(['Data', data]); var $submit = this.$('.NB-add-url-submit'); var $loading = this.$('.NB-add-site .NB-loading'); $loading.removeClass('NB-active'); NEWSBLUR.reader.flags['reloading_feeds'] = false; - + if (data.code > 0) { - NEWSBLUR.assets.load_feeds(function() { + NEWSBLUR.assets.load_feeds(function () { if (data.feed) { NEWSBLUR.reader.open_feed(data.feed.id); } @@ -245,8 +245,8 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ $submit.removeClass('NB-disabled'); } }, - - error: function(data) { + + error: function (data) { var $submit = this.$('.NB-add-url-submit'); var $error = this.$('.NB-error'); @@ -255,11 +255,11 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ $submit.text('Add Site'); NEWSBLUR.reader.flags['reloading_feeds'] = false; }, - - open_add_folder: function() { + + open_add_folder: function () { var $folder = this.$(".NB-add-folder"); var $icon = this.$(".NB-add-folder-icon"); - + if (this._open_folder) { $folder.slideUp(300); $icon.removeClass('NB-active'); @@ -270,33 +270,33 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ $folder.slideDown(300); } }, - - save_add_folder: function() { + + save_add_folder: function () { var $submit = this.$('.NB-add-folder-submit'); var $error = this.$('.NB-error'); var $loading = this.$('.NB-add-folder .NB-loading'); - + var folder = $('.NB-add-folder-input').val(); var parent_folder = this.$('.NB-folders').val(); - + $error.slideUp(300); $loading.addClass('NB-active'); $submit.addClass('NB-disabled').text('Adding...'); this.model.save_add_folder(folder, parent_folder, $.rescope(this.post_save_add_folder, this)); }, - - post_save_add_folder: function(e, data) { + + post_save_add_folder: function (e, data) { var $submit = this.$('.NB-add-folder-submit'); var $error = this.$('.NB-error'); var $loading = this.$('.NB-add-folder .NB-loading'); var $folder = $('.NB-add-folder-input'); $loading.removeClass('NB-active'); $submit.removeClass('NB-disabled'); - + if (data.code > 0) { $submit.text('Added!'); - NEWSBLUR.assets.load_feeds(_.bind(function() { + NEWSBLUR.assets.load_feeds(_.bind(function () { var $folders = NEWSBLUR.utils.make_folders($folder.val()); this.$(".NB-folders").replaceWith($folders); this.open_add_folder(); @@ -310,5 +310,5 @@ NEWSBLUR.ReaderAddFeed = NEWSBLUR.ReaderPopover.extend({ $submit.text('Add Folder'); } } - + }); diff --git a/media/js/newsblur/reader/reader_admin.js b/media/js/newsblur/reader/reader_admin.js index 5c2acdbde5..68ec43e0d7 100644 --- a/media/js/newsblur/reader/reader_admin.js +++ b/media/js/newsblur/reader/reader_admin.js @@ -1,8 +1,8 @@ -NEWSBLUR.ReaderUserAdmin = function(options) { +NEWSBLUR.ReaderUserAdmin = function (options) { var defaults = { width: 700 }; - + this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.assets; this.user = this.options.user; @@ -13,19 +13,19 @@ NEWSBLUR.ReaderUserAdmin.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderUserAdmin.prototype.constructor = NEWSBLUR.ReaderUserAdmin; _.extend(NEWSBLUR.ReaderUserAdmin.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); this.open_modal(); this.fetch_payment_history(); - - + + this.$modal.bind('click', $.rescope(this.handle_click, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-admin NB-modal' }, [ $.make('h2', { className: 'NB-modal-title' }, [ $.make('div', { className: 'NB-icon' }), @@ -51,18 +51,18 @@ _.extend(NEWSBLUR.ReaderUserAdmin.prototype, { ]) ]); }, - + // ============ // = Payments = // ============ - fetch_payment_history: function() { - this.model.fetch_payment_history(this.user.get('user_id'), _.bind(function(data) { + fetch_payment_history: function () { + this.model.fetch_payment_history(this.user.get('user_id'), _.bind(function (data) { var $history = $('.NB-account-payments', this.$modal).empty(); var $actions = $(".NB-admin-actions", this.$modal).empty(); var $statistics = $(".NB-admin-statistics", this.$modal).empty(); - - _.each(data.payments, function(payment) { + + _.each(data.payments, function (payment) { $history.append($.make('li', { className: 'NB-account-payment' }, [ $.make('div', { className: 'NB-account-payment-date' }, payment.payment_date), $.make('div', { className: 'NB-account-payment-amount' }, "$" + payment.payment_amount), @@ -72,7 +72,7 @@ _.extend(NEWSBLUR.ReaderUserAdmin.prototype, { if (!data.payments.length) { $history.append($.make('i', 'No payments found.')); } - + if (data.is_premium) { $actions.append($.make('div', { style: 'margin-bottom: 12px' }, [ (data.is_premium && !data.is_archive && "User is premium, expires: "), @@ -147,86 +147,86 @@ _.extend(NEWSBLUR.ReaderUserAdmin.prototype, { $(window).resize(); }, this)); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-admin-action-refund' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-admin-action-refund' }, function ($t, $p) { e.preventDefault(); - + NEWSBLUR.assets.refund_premium({ 'user_id': self.user.get('user_id') - }, function(data) { + }, function (data) { $(".NB-admin-action-refund").replaceWith($.make('div', 'Refunded $' + data.refunded)); - }, function(data) { + }, function (data) { $(".NB-admin-action-refund").replaceWith($.make('div', 'Error: ' + JSON.stringify(data))); }); }); - $.targetIs(e, { tagSelector: '.NB-admin-action-refund-partial' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-admin-action-refund-partial' }, function ($t, $p) { e.preventDefault(); - + NEWSBLUR.assets.refund_premium({ 'user_id': self.user.get('user_id'), 'partial': true - }, function(data) { + }, function (data) { $(".NB-admin-action-refund").replaceWith($.make('div', 'Refunded $' + data.refunded)); - }, function(data) { + }, function (data) { $(".NB-admin-action-refund").replaceWith($.make('div', 'Error: ' + JSON.stringify(data))); }); }); - $.targetIs(e, { tagSelector: '.NB-admin-action-never-expire' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-admin-action-never-expire' }, function ($t, $p) { e.preventDefault(); - + NEWSBLUR.assets.never_expire_premium({ 'user_id': self.user.get('user_id') - }, function(data) { + }, function (data) { self.fetch_payment_history(); - }, function(data) { + }, function (data) { $(".NB-admin-action-never-expire").replaceWith($.make('div', 'Error: ' + JSON.stringify(data))); }); }); - $.targetIs(e, { tagSelector: '.NB-admin-action-1yr-expire' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-admin-action-1yr-expire' }, function ($t, $p) { e.preventDefault(); - + NEWSBLUR.assets.never_expire_premium({ 'user_id': self.user.get('user_id'), 'years': 1 - }, function(data) { + }, function (data) { self.fetch_payment_history(); - }, function(data) { + }, function (data) { $(".NB-admin-action-never-expire").replaceWith($.make('div', 'Error: ' + JSON.stringify(data))); }); }); - $.targetIs(e, { tagSelector: '.NB-admin-action-upgrade' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-admin-action-upgrade' }, function ($t, $p) { e.preventDefault(); - - NEWSBLUR.assets.upgrade_premium(self.user.get('user_id'), function() { + + NEWSBLUR.assets.upgrade_premium(self.user.get('user_id'), function () { $(".NB-admin-action-upgrade").replaceWith($.make('div', 'Upgraded!')); self.fetch_payment_history(); - }, function(data) { + }, function (data) { $(".NB-admin-action-upgrade").replaceWith($.make('div', 'Error: ' + JSON.stringify(data))); }); }); - $.targetIs(e, { tagSelector: '.NB-admin-action-history' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-admin-action-history' }, function ($t, $p) { e.preventDefault(); - - NEWSBLUR.assets.update_payment_history(self.user.get('user_id'), function() { + + NEWSBLUR.assets.update_payment_history(self.user.get('user_id'), function () { $(".NB-admin-action-history").replaceWith($.make('div', 'Updated!')); self.fetch_payment_history(); - }, function(data) { + }, function (data) { $(".NB-admin-action-history").replaceWith($.make('div', 'Error: ' + JSON.stringify(data))); }); }); - $.targetIs(e, { tagSelector: '.NB-admin-action-opml' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-admin-action-opml' }, function ($t, $p) { e.preventDefault(); - + window.location.href = NEWSBLUR.URLs['opml-export'] + "?user_id=" + self.user.get('user_id'); }); } - + }); diff --git a/media/js/newsblur/reader/reader_auth_lost.js b/media/js/newsblur/reader/reader_auth_lost.js index 1179723b38..bab01f2a08 100644 --- a/media/js/newsblur/reader/reader_auth_lost.js +++ b/media/js/newsblur/reader/reader_auth_lost.js @@ -1,9 +1,9 @@ -NEWSBLUR.ReaderAuthLost = function(options) { +NEWSBLUR.ReaderAuthLost = function (options) { var defaults = { 'overlayClose': false, 'height': 100 }; - + this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.assets; this.runner(); @@ -13,17 +13,17 @@ NEWSBLUR.ReaderAuthLost.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderAuthLost.prototype.constructor = NEWSBLUR.ReaderAuthLost; _.extend(NEWSBLUR.ReaderAuthLost.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); this.open_modal(); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-authlost NB-modal' }, [ $.make('h2', { className: 'NB-modal-title' }, [ $.make('div', { className: 'NB-icon' }), @@ -34,20 +34,20 @@ _.extend(NEWSBLUR.ReaderAuthLost.prototype, { ]) ]); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-submit-button' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-submit-button' }, function ($t, $p) { e.preventDefault(); - + window.location.href = "/"; }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/reader/reader_classifier.js b/media/js/newsblur/reader/reader_classifier.js index f3265ce380..a9e6f495d5 100644 --- a/media/js/newsblur/reader/reader_classifier.js +++ b/media/js/newsblur/reader/reader_classifier.js @@ -1,10 +1,10 @@ -NEWSBLUR.ReaderClassifierTrainer = function(options) { +NEWSBLUR.ReaderClassifierTrainer = function (options) { var defaults = { 'width': 620, 'training': true, modal_container_class: "NB-full-container NB-classifier-container" }; - + this.flags = { 'publisher': true, 'story': false, @@ -19,14 +19,14 @@ NEWSBLUR.ReaderClassifierTrainer = function(options) { this.runner_trainer(); }; -NEWSBLUR.ReaderClassifierFeed = function(feed_id, options) { +NEWSBLUR.ReaderClassifierFeed = function (feed_id, options) { var defaults = { 'width': 620, 'training': false, 'feed_loaded': true, modal_container_class: "NB-full-container NB-classifier-container" }; - + this.flags = { 'publisher': true, 'story': false, @@ -42,13 +42,13 @@ NEWSBLUR.ReaderClassifierFeed = function(feed_id, options) { }; -NEWSBLUR.ReaderClassifierStory = function(story_id, feed_id, options) { +NEWSBLUR.ReaderClassifierStory = function (story_id, feed_id, options) { var defaults = { 'width': 620, 'feed_loaded': true, modal_container_class: "NB-full-container NB-classifier-container" }; - + this.flags = { 'publisher': false, 'story': true, @@ -67,8 +67,8 @@ NEWSBLUR.ReaderClassifierStory = function(story_id, feed_id, options) { }; var classifier_prototype = { - - runner_trainer: function(reload) { + + runner_trainer: function (reload) { if (!reload) { this.user_classifiers = {}; } @@ -77,20 +77,20 @@ var classifier_prototype = { this.get_feeds_trainer(); this.handle_cancel(); this.open_modal(); - + this.model.preference('has_trained_intelligence', true); NEWSBLUR.reader.check_hide_getting_started(); - + this.$modal.parent().bind('click.reader_classifer', $.rescope(this.handle_clicks, this)); }, - - runner_feed: function() { + + runner_feed: function () { this.options.social_feed = _.string.include(this.feed_id, 'social:'); if (!this.model.classifiers[this.feed_id]) { this.model.classifiers[this.feed_id] = _.extend({}, this.model.defaults['classifiers']); } - + if (this.options.feed_loaded) { this.user_classifiers = this.model.classifiers[this.feed_id]; } else { @@ -100,49 +100,49 @@ var classifier_prototype = { this.make_modal_feed(); this.make_modal_title(); this.handle_cancel(); - this.open_modal(_.bind(function() { + this.open_modal(_.bind(function () { this.fit_classifiers(); }, this)); this.$modal.parent().bind('click.reader_classifer', $.rescope(this.handle_clicks, this)); if (!this.options.feed_loaded) { - _.defer(_.bind(function() { + _.defer(_.bind(function () { this.load_single_feed_trainer(); }, this)); } }, - - runner_story: function() { + + runner_story: function () { this.options.social_feed = _.string.include(this.feed_id, 'social:'); - + if (!this.model.classifiers[this.feed_id]) { this.model.classifiers[this.feed_id] = _.extend({}, this.model.defaults['classifiers']); } - + if (this.options.feed_loaded) { this.user_classifiers = this.model.classifiers[this.feed_id]; } else { this.user_classifiers = {}; } - + this.find_story_and_feed(); this.make_modal_story(); this.handle_text_highlight(); this.make_modal_title(); this.handle_cancel(); - this.open_modal(_.bind(function() { + this.open_modal(_.bind(function () { this.fit_classifiers(); }, this)); this.$modal.parent().bind('click.reader_classifer', $.rescope(this.handle_clicks, this)); if (!this.options.feed_loaded) { - _.defer(_.bind(function() { + _.defer(_.bind(function () { this.load_single_feed_trainer(); }, this)); } }, - - load_previous_feed_in_trainer: function() { + + load_previous_feed_in_trainer: function () { var trainer_data_length = this.trainer_data.length; this.trainer_iterator = this.trainer_iterator - 1; var trainer_data = this.trainer_data[this.trainer_iterator]; @@ -155,8 +155,8 @@ var classifier_prototype = { this.load_feed(trainer_data); } }, - - load_next_feed_in_trainer: function() { + + load_next_feed_in_trainer: function () { var trainer_data_length = this.trainer_data.length; this.trainer_iterator += 1; var trainer_data = this.trainer_data[this.trainer_iterator]; @@ -174,21 +174,21 @@ var classifier_prototype = { } } }, - - load_feed: function(trainer_data) { + + load_feed: function (trainer_data) { this.feed_id = trainer_data['feed_id'] || trainer_data['id']; this.feed = this.model.get_feed(this.feed_id); this.feed_tags = trainer_data['feed_tags']; this.feed_authors = trainer_data['feed_authors']; this.user_classifiers = trainer_data['classifiers']; this.feed_publishers = new Backbone.Collection(trainer_data['popular_publishers']); - this.feed.set('num_subscribers', trainer_data['num_subscribers'], {silent: true}); + this.feed.set('num_subscribers', trainer_data['num_subscribers'], { silent: true }); this.options.feed_loaded = true; - + if (!this.model.classifiers[this.feed_id]) { this.model.classifiers[this.feed_id] = _.extend({}, this.model.defaults['classifiers']); } - + if (this.feed_id in this.cache) { this.$modal = this.cache[this.feed_id]; } else { @@ -201,12 +201,12 @@ var classifier_prototype = { } this.make_modal_title(); } - + this.reload_modal(); }, - - reload_modal: function(callback) { - this.flags.modal_loading = setInterval(_.bind(function() { + + reload_modal: function (callback) { + this.flags.modal_loading = setInterval(_.bind(function () { if (this.flags.modal_loaded) { clearInterval(this.flags.modal_loading); $('.NB-modal').empty().append(this.$modal.children()); @@ -219,8 +219,8 @@ var classifier_prototype = { } }, this), 125); }, - - fit_classifiers: function() { + + fit_classifiers: function () { var $form = $("form", this.$modal); if (!$form.length) return; var form_height = $form.innerHeight(); @@ -237,77 +237,77 @@ var classifier_prototype = { new_form_height = $form.innerHeight(); form_outerheight = $form.outerHeight(true); if (new_form_height == form_height || i > 500) break; - form_height = Math.min(new_form_height, form_height-1); + form_height = Math.min(new_form_height, form_height - 1); } }, - - get_feeds_trainer: function() { + + get_feeds_trainer: function () { this.model.get_feeds_trainer(null, $.rescope(this.load_feeds_trainer, this)); }, - - load_feeds_trainer: function(e, data) { + + load_feeds_trainer: function (e, data) { var $begin = $('.NB-modal-submit-begin', this.$modal); - + this.trainer_data = data; if (!data || !data.length) { this.make_trainer_outro(); this.reload_modal(); } else { - $begin.text('Begin Training') + $begin.text('Begin Training') .addClass('NB-modal-submit-green') .removeClass('NB-modal-submit-grey') .removeClass('NB-disabled'); } }, - - retrain_all_sites: function() { + + retrain_all_sites: function () { $('.NB-modal-submit-reset', this.$modal).text('Rewinding...').attr('disabled', true).addClass('NB-disabled'); - - this.model.retrain_all_sites(_.bind(function(data) { + + this.model.retrain_all_sites(_.bind(function (data) { this.load_feeds_trainer(null, data); this.load_next_feed_in_trainer(); }, this)); }, - - find_story_and_feed: function() { + + find_story_and_feed: function () { if (this.story_id) { this.story = this.model.get_story(this.story_id); } - + this.feed = this.model.get_feed(this.feed_id); - + if (this.options.feed_loaded && this.feed) { - this.feed_tags = this.model.get_feed_tags(); - this.feed_authors = this.model.get_feed_authors(); - $('.NB-modal-subtitle .NB-modal-feed-image', this.$modal).attr('src', $.favicon(this.feed)); - $('.NB-modal-subtitle .NB-modal-feed-title', this.$modal).html(this.feed.get('feed_title')); - $('.NB-modal-subtitle .NB-modal-feed-subscribers', this.$modal).html(Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true)); + this.feed_tags = this.model.get_feed_tags(); + this.feed_authors = this.model.get_feed_authors(); + $('.NB-modal-subtitle .NB-modal-feed-image', this.$modal).attr('src', $.favicon(this.feed)); + $('.NB-modal-subtitle .NB-modal-feed-title', this.$modal).html(this.feed.get('feed_title')); + $('.NB-modal-subtitle .NB-modal-feed-subscribers', this.$modal).html(Inflector.pluralize(' subscriber', this.feed.get('num_subscribers'), true)); } }, - - load_single_feed_trainer: function() { + + load_single_feed_trainer: function () { var self = this; var $loading = $('.NB-modal-loading', this.$modal); $loading.addClass('NB-active'); - + var get_trainer_fn = this.model.get_feeds_trainer; if (this.options.social_feed) { get_trainer_fn = this.model.get_social_trainer; } - get_trainer_fn.call(this.model, this.feed_id, function(data) { + get_trainer_fn.call(this.model, this.feed_id, function (data) { self.trainer_data = data; if (data && data.length) { - // Should only be one feed - self.load_feed(data[0]); + // Should only be one feed + self.load_feed(data[0]); } }); }, - - make_trainer_intro: function() { + + make_trainer_intro: function () { var self = this; - - this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal NB-modal-trainer'}, [ + + this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal NB-modal-trainer' }, [ $.make('h2', { className: 'NB-modal-title' }, [ $.make('div', { className: 'NB-icon' }), 'Intelligence Trainer', @@ -322,13 +322,13 @@ var classifier_prototype = { ]), $.make('li', [ $.make('b', 'The intelligence slider filters stories.'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-focus.svg'}), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-focus.svg' }), ' are stories you like', $.make('br'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-unread.svg'}), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-unread.svg' }), ' are stories you have not yet rated', $.make('br'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-hidden.svg'}), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-hidden.svg' }), ' are stories you don\'t like' ]), $.make('li', [ @@ -345,13 +345,13 @@ var classifier_prototype = { ]) ]) ]); - + }, - - make_trainer_outro: function() { + + make_trainer_outro: function () { var self = this; - - this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal NB-modal-trainer'}, [ + + this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal NB-modal-trainer' }, [ $.make('h2', { className: 'NB-modal-title' }, 'Congratulations! You\'re done.'), $.make('h3', { className: 'NB-modal-subtitle' }, 'Here\'s what happens next:'), $.make('ol', { className: 'NB-trainer-points' }, [ @@ -366,13 +366,13 @@ var classifier_prototype = { $.make('li', [ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/intelligence_slider_positive.png', style: 'float: right', width: 114, height: 29 }), $.make('b', 'As a reminder, use the intelligence slider to select a filter:'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-hidden.svg'}), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-hidden.svg' }), ' are stories you don\'t like', $.make('br'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-unread.svg'}), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-unread.svg' }), ' are stories you have not yet rated', $.make('br'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-focus.svg'}), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-focus.svg' }), ' are stories you like' ]), @@ -388,15 +388,15 @@ var classifier_prototype = { ]) ]) ]); - + }, - - make_modal_feed: function() { + + make_modal_feed: function () { var self = this; var feed = this.feed; // NEWSBLUR.log(['Make feed', feed, this.feed_authors, this.feed_tags, this.options['feed_loaded']]); - + this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal ' + (this.options['training'] && 'NB-modal-trainer') }, [ $.make('div', { className: 'NB-modal-loading' }), $.make('h2', { className: 'NB-modal-title' }, ''), @@ -409,69 +409,69 @@ var classifier_prototype = { ]) ]), (this.options['feed_loaded'] && - $.make('form', { method: 'post', className: 'NB-publisher' }, [ - (!_.isEmpty(this.user_classifiers.titles) && $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ - $.make('h5', 'Titles and Phrases'), - $.make('div', { className: 'NB-classifier-titles NB-fieldset-fields NB-classifiers' }, - this.make_user_titles() - ) - ])), - (this.feed_authors.length && $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ - $.make('h5', 'Authors'), - $.make('div', { className: 'NB-classifier-authors NB-fieldset-fields NB-classifiers' }, - this.make_authors(this.feed_authors).concat(this.make_user_authors()) - ) - ])), - (this.feed_tags.length && $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ - $.make('h5', 'Categories & Tags'), - $.make('div', { className: 'NB-classifier-tags NB-fieldset-fields NB-classifiers' }, - this.make_tags(this.feed_tags).concat(this.make_user_tags()) - ) - ])), - (this.feed_publishers && this.feed_publishers.length && $.make('div', { className: 'NB-modal-field NB-fieldset NB-publishers' }, [ - $.make('h5', 'Sharing Stories From These Sites'), - $.make('div', { className: 'NB-classifier-publishers NB-fieldset-fields NB-classifiers' }, - this.make_publishers(this.feed_publishers) - ) - ])), - $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ - $.make('h5', 'Everything by This Publisher'), - $.make('div', { className: 'NB-fieldset-fields NB-classifiers' }, - this.make_publisher(feed) - ) - ]) - ]) - ), - (!this.options['feed_loaded'] && - $.make('form', { method: 'post', className: 'NB-publisher' })), - (this.options['training'] && $.make('div', { className: 'NB-modal-submit-bottom' }, [ - $.make('div', { className: 'NB-modal-submit' }, [ - $.make('input', { name: 'feed_id', value: this.feed_id, type: 'hidden' }), - $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-back' }, $.entity('«') + ' Back'), - $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-next NB-modal-submit-save' }, 'Save & Next '+$.entity('»')), - $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-grey' }, 'Close') - ]) - ])), - (!this.options['training'] && $.make('div', { className: 'NB-modal-submit-bottom' }, [ - $.make('div', { className: 'NB-modal-submit' }, [ - $.make('input', { name: 'story_id', value: this.story_id, type: 'hidden' }), - $.make('input', { name: 'feed_id', value: this.feed_id, type: 'hidden' }), - $.make('div', { className: 'NB-modal-submit-save NB-modal-submit-button NB-modal-submit-green NB-disabled' }, 'Check what you like above...') - ]) - ])) + $.make('form', { method: 'post', className: 'NB-publisher' }, [ + (!_.isEmpty(this.user_classifiers.titles) && $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ + $.make('h5', 'Titles and Phrases'), + $.make('div', { className: 'NB-classifier-titles NB-fieldset-fields NB-classifiers' }, + this.make_user_titles() + ) + ])), + (this.feed_authors.length && $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ + $.make('h5', 'Authors'), + $.make('div', { className: 'NB-classifier-authors NB-fieldset-fields NB-classifiers' }, + this.make_authors(this.feed_authors).concat(this.make_user_authors()) + ) + ])), + (this.feed_tags.length && $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ + $.make('h5', 'Categories & Tags'), + $.make('div', { className: 'NB-classifier-tags NB-fieldset-fields NB-classifiers' }, + this.make_tags(this.feed_tags).concat(this.make_user_tags()) + ) + ])), + (this.feed_publishers && this.feed_publishers.length && $.make('div', { className: 'NB-modal-field NB-fieldset NB-publishers' }, [ + $.make('h5', 'Sharing Stories From These Sites'), + $.make('div', { className: 'NB-classifier-publishers NB-fieldset-fields NB-classifiers' }, + this.make_publishers(this.feed_publishers) + ) + ])), + $.make('div', { className: 'NB-modal-field NB-fieldset NB-classifiers' }, [ + $.make('h5', 'Everything by This Publisher'), + $.make('div', { className: 'NB-fieldset-fields NB-classifiers' }, + this.make_publisher(feed) + ) + ]) + ]) + ), + (!this.options['feed_loaded'] && + $.make('form', { method: 'post', className: 'NB-publisher' })), + (this.options['training'] && $.make('div', { className: 'NB-modal-submit-bottom' }, [ + $.make('div', { className: 'NB-modal-submit' }, [ + $.make('input', { name: 'feed_id', value: this.feed_id, type: 'hidden' }), + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-back' }, $.entity('«') + ' Back'), + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-modal-submit-next NB-modal-submit-save' }, 'Save & Next ' + $.entity('»')), + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-grey' }, 'Close') + ]) + ])), + (!this.options['training'] && $.make('div', { className: 'NB-modal-submit-bottom' }, [ + $.make('div', { className: 'NB-modal-submit' }, [ + $.make('input', { name: 'story_id', value: this.story_id, type: 'hidden' }), + $.make('input', { name: 'feed_id', value: this.feed_id, type: 'hidden' }), + $.make('div', { className: 'NB-modal-submit-save NB-modal-submit-button NB-modal-submit-green NB-disabled' }, 'Check what you like above...') + ]) + ])) ]); }, - - make_modal_story: function() { + + make_modal_story: function () { var self = this; var story = this.story; var feed = this.feed; - + // NEWSBLUR.log(['Make Story', story, feed]); - + // HTML entities decoding. var story_title = _.string.trim($('
').html(story.get('story_title')).text()); - + this.$modal = $.make('div', { className: 'NB-modal-classifiers NB-modal' }, [ $.make('div', { className: 'NB-modal-loading' }), $.make('h2', { className: 'NB-modal-title' }), @@ -533,10 +533,10 @@ var classifier_prototype = { ]) ]); }, - - make_modal_title: function() { + + make_modal_title: function () { var $modal_title = $('.NB-modal-title', this.$modal); - + var $title = $.make('div', [ 'What do you ', $.make('b', { className: 'NB-classifier-title-like' }, 'like'), @@ -550,31 +550,31 @@ var classifier_prototype = { $modal_title.html($title); }, - - make_modal_trainer_count: function() { + + make_modal_trainer_count: function () { var $count = $('.NB-classifier-trainer-counts', this.$modal); var count = this.trainer_iterator + 1; var total = this.trainer_data.length; $count.html(count + '/' + total); }, - - make_user_titles: function(existing_title) { + + make_user_titles: function (existing_title) { var $titles = []; var titles = _.keys(this.user_classifiers.titles); - - _.each(titles, _.bind(function(title) { + + _.each(titles, _.bind(function (title) { if (!existing_title || existing_title.toLowerCase().indexOf(title.toLowerCase()) != -1) { var $title = this.make_classifier(title, title, 'title'); $titles.push($title); } }, this)); - + return $titles; }, - - make_authors: function(authors) { + + make_authors: function (authors) { var $authors = []; - + for (var a in authors) { var author_obj = authors[a]; if (typeof author_obj == 'string') { @@ -584,30 +584,30 @@ var classifier_prototype = { var author = author_obj[0]; var author_count = author_obj[1]; } - + if (!author) continue; - - var $author = this.make_classifier(author, author, 'author', author_count); + + var $author = this.make_classifier(author, author, 'author', author_count); $authors.push($author); } return $authors; }, - - make_user_authors: function() { + + make_user_authors: function () { var $authors = []; var user_authors = _.keys(this.user_classifiers.authors); - var feed_authors = _.map(this.feed_authors, function(author) { return author[0]; }); - var authors = _.reduce(user_authors, function(memo, author, i) { + var feed_authors = _.map(this.feed_authors, function (author) { return author[0]; }); + var authors = _.reduce(user_authors, function (memo, author, i) { if (!_.contains(feed_authors, author)) return memo.concat(author); return memo; }, []); - + return this.make_authors(authors); }, - - make_tags: function(tags) { + + make_tags: function (tags) { var $tags = []; - + for (var t in tags) { var tag_obj = tags[t]; if (typeof tag_obj == 'string') { @@ -617,66 +617,66 @@ var classifier_prototype = { var tag = tag_obj[0]; var tag_count = tag_obj[1]; } - + if (!tag) continue; - + var $tag = this.make_classifier(tag, tag, 'tag', tag_count); $tags.push($tag); } - + return $tags; }, - - make_user_tags: function() { + + make_user_tags: function () { var $tags = []; var user_tags = _.keys(this.user_classifiers.tags); - var feed_tags = _.map(this.feed_tags, function(tag) { return tag[0]; }); - var tags = _.reduce(user_tags, function(memo, tag, i) { + var feed_tags = _.map(this.feed_tags, function (tag) { return tag[0]; }); + var tags = _.reduce(user_tags, function (memo, tag, i) { if (!_.contains(feed_tags, tag)) return memo.concat(tag); return memo; }, []); - + return this.make_tags(tags); }, - - make_publishers: function(publishers) { - var $publishers = publishers.map(_.bind(function(publisher) { + + make_publishers: function (publishers) { + var $publishers = publishers.map(_.bind(function (publisher) { return this.make_publisher(publisher); }, this)); - + return $publishers; }, - - make_publisher: function(publisher) { - var $publisher = this.make_classifier(_.string.truncate(publisher.get('feed_title'), 50), - publisher.id, 'feed', publisher.get('story_count'), publisher); + + make_publisher: function (publisher) { + var $publisher = this.make_classifier(_.string.truncate(publisher.get('feed_title'), 50), + publisher.id, 'feed', publisher.get('story_count'), publisher); return $publisher; }, - - make_classifier: function(classifier_title, classifier_value, classifier_type, classifier_count, classifier) { + + make_classifier: function (classifier_title, classifier_value, classifier_type, classifier_count, classifier) { var score = 0; // NEWSBLUR.log(['classifiers', this.user_classifiers, classifier_value, this.user_classifiers[classifier_type+'s']]); - if (this.user_classifiers[classifier_type+'s'] && - classifier_value in this.user_classifiers[classifier_type+'s']) { - score = this.user_classifiers[classifier_type+'s'][classifier_value]; + if (this.user_classifiers[classifier_type + 's'] && + classifier_value in this.user_classifiers[classifier_type + 's']) { + score = this.user_classifiers[classifier_type + 's'][classifier_value]; } - - var classifier_type_title = Inflector.capitalize(classifier_type=='feed' ? - 'site' : - classifier_type); - + + var classifier_type_title = Inflector.capitalize(classifier_type == 'feed' ? + 'site' : + classifier_type); + var $classifier = $.make('span', { className: 'NB-classifier-container' }, [ - $.make('span', { className: 'NB-classifier NB-classifier-'+classifier_type }, [ - $.make('input', { - type: 'checkbox', - className: 'NB-classifier-input-like', - name: 'like_'+classifier_type, + $.make('span', { className: 'NB-classifier NB-classifier-' + classifier_type }, [ + $.make('input', { + type: 'checkbox', + className: 'NB-classifier-input-like', + name: 'like_' + classifier_type, value: classifier_value }), - $.make('input', { - type: 'checkbox', - className: 'NB-classifier-input-dislike', - name: 'dislike_'+classifier_type, + $.make('input', { + type: 'checkbox', + className: 'NB-classifier-input-dislike', + name: 'dislike_' + classifier_type, value: classifier_value }), $.make('div', { className: 'NB-classifier-icon-like' }), @@ -684,12 +684,12 @@ var classifier_prototype = { $.make('div', { className: 'NB-classifier-icon-dislike-inner' }) ]), $.make('label', [ - (classifier_type == 'feed' && - $.make('img', { - className: 'feed_favicon', + (classifier_type == 'feed' && + $.make('img', { + className: 'feed_favicon', src: $.favicon(classifier) })), - $.make('b', classifier_type_title+': '), + $.make('b', classifier_type_title + ': '), $.make('span', classifier_title) ]) ]), @@ -698,7 +698,7 @@ var classifier_prototype = { classifier_count ])) ]); - + if (score > 0) { $('.NB-classifier', $classifier).addClass('NB-classifier-like'); $('.NB-classifier-input-like', $classifier).prop('checked', true); @@ -706,30 +706,30 @@ var classifier_prototype = { $('.NB-classifier', $classifier).addClass('NB-classifier-dislike'); $('.NB-classifier-input-dislike', $classifier).prop('checked', true); } - - $('.NB-classifier', $classifier).bind('mouseenter', function(e) { + + $('.NB-classifier', $classifier).bind('mouseenter', function (e) { $(e.currentTarget).addClass('NB-classifier-hover-like'); - }).bind('mouseleave', function(e) { + }).bind('mouseleave', function (e) { $(e.currentTarget).removeClass('NB-classifier-hover-like'); }); - - $('.NB-classifier-icon-dislike', $classifier).bind('mouseenter', function(e) { + + $('.NB-classifier-icon-dislike', $classifier).bind('mouseenter', function (e) { $('.NB-classifier', $classifier).addClass('NB-classifier-hover-dislike'); - }).bind('mouseleave', function(e) { + }).bind('mouseleave', function (e) { $('.NB-classifier', $classifier).removeClass('NB-classifier-hover-dislike'); }); - + return $classifier; }, - - change_classifier: function($classifier, classifier_opinion) { + + change_classifier: function ($classifier, classifier_opinion) { var $like = $('.NB-classifier-input-like', $classifier); var $dislike = $('.NB-classifier-input-dislike', $classifier); - + var $save = $('.NB-modal-submit-save', this.$modal); var $close = $('.NB-modal-submit-grey', this.$modal); var $back = $('.NB-modal-submit-back', this.$modal); - + if (classifier_opinion == 'like') { if ($classifier.is('.NB-classifier-like')) { $classifier.removeClass('NB-classifier-like'); @@ -753,7 +753,7 @@ var classifier_prototype = { $dislike.prop('checked', true); } } - + if (this.options['training']) { $close.text('Save & Close'); } else { @@ -761,27 +761,27 @@ var classifier_prototype = { } // NEWSBLUR.log(['change_classifier', classifier_opinion, $classifier, $like.is(':checked'), $dislike.is(':checked')]); }, - - end: function() { + + end: function () { this.model.preference('has_trained_intelligence', true); NEWSBLUR.reader.check_hide_getting_started(); $.modal.close(); }, - + // ========== // = Events = // ========== - - handle_text_highlight: function() { + + handle_text_highlight: function () { var self = this; var $title_highlight = $('.NB-classifier-title-highlight', this.$modal); var $title_placeholder = $('.NB-classifier-title-placeholder', this.$modal); var $title_classifier = $title_placeholder.parents('.NB-classifier').eq(0); var $title_checkboxs = $('.NB-classifier-input-like, .NB-classifier-input-dislike', $title_classifier); - var update = function() { + var update = function () { var text = $.trim($(this).getSelection().text); - + if (text.length && $title_placeholder.text() != text) { $title_placeholder.text(text); $title_checkboxs.val(text); @@ -790,58 +790,58 @@ var classifier_prototype = { } } }; - + $title_highlight .keydown(update).keyup(update) .mousedown(update).mouseup(update).mousemove(update); $title_checkboxs.val($title_highlight.val()); - $title_placeholder.parents('.NB-classifier').bind('click', function() { + $title_placeholder.parents('.NB-classifier').bind('click', function () { if ($title_highlight.val() == $title_checkboxs.val()) { $title_placeholder.text($title_highlight.val()); } }); }, - - handle_cancel: function() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); }, - - handle_clicks: function(elem, e) { + + handle_clicks: function (elem, e) { var self = this; - + if (this.options['training']) { - $.targetIs(e, { tagSelector: '.NB-modal-submit-begin' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-modal-submit-begin' }, function ($t, $p) { e.preventDefault(); self.load_next_feed_in_trainer(); }); - $.targetIs(e, { tagSelector: '.NB-modal-submit-save.NB-modal-submit-next' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-modal-submit-save.NB-modal-submit-next' }, function ($t, $p) { e.preventDefault(); self.save(true); self.load_next_feed_in_trainer(); }); - $.targetIs(e, { tagSelector: '.NB-modal-submit-back' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-modal-submit-back' }, function ($t, $p) { e.preventDefault(); self.load_previous_feed_in_trainer(); }); - - $.targetIs(e, { tagSelector: '.NB-modal-submit-reset' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-modal-submit-reset' }, function ($t, $p) { e.preventDefault(); self.retrain_all_sites(); }); - $.targetIs(e, { tagSelector: '.NB-modal-submit-grey' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-modal-submit-grey' }, function ($t, $p) { e.preventDefault(); self.save(); }); - $.targetIs(e, { tagSelector: '.NB-modal-submit-end' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-modal-submit-end' }, function ($t, $p) { e.preventDefault(); NEWSBLUR.reader.force_feed_refresh(); self.end(); @@ -849,49 +849,49 @@ var classifier_prototype = { // TODO: Update counts in active feed. }); } else { - $.targetIs(e, { tagSelector: '.NB-modal-submit-save:not(.NB-modal-submit-next)' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-modal-submit-save:not(.NB-modal-submit-next)' }, function ($t, $p) { e.preventDefault(); self.save(); return false; }); } - + var stop = false; - $.targetIs(e, { tagSelector: '.NB-classifier-icon-dislike' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-classifier-icon-dislike' }, function ($t, $p) { e.preventDefault(); stop = true; self.change_classifier($t.closest('.NB-classifier'), 'dislike'); }); if (stop) return; - $.targetIs(e, { tagSelector: '.NB-classifier' }, function($t, $p){ + $.targetIs(e, { tagSelector: '.NB-classifier' }, function ($t, $p) { e.preventDefault(); self.change_classifier($t, 'like'); }); }, - - serialize_classifier: function() { + + serialize_classifier: function () { var data = {}; - $('.NB-classifier', this.$modal).each(function() { + $('.NB-classifier', this.$modal).each(function () { var value = $('.NB-classifier-input-like', this).val(); if ($('.NB-classifier-input-like, .NB-classifier-input-dislike', this).is(':checked')) { var name = $('input:checked', this).attr('name'); if (!data[name]) data[name] = []; data[name].push(value); } else { - var name = 'remove_'+$('.NB-classifier-input-like', this).attr('name'); + var name = 'remove_' + $('.NB-classifier-input-like', this).attr('name'); if (!data[name]) data[name] = []; data[name].push(value); } }); - + data['feed_id'] = this.feed_id; if (this.story_id) { data['story_id'] = this.story_id; } return data; }, - - save: function(keep_modal_open) { + + save: function (keep_modal_open) { var self = this; var $save = $('.NB-modal-submit-save', this.$modal); var data = this.serialize_classifier(); @@ -899,35 +899,35 @@ var classifier_prototype = { if (this.options.social_feed && this.story_id) { feed_id = this.original_feed_id; } - + if (this.options['training']) { this.cache[this.feed_id] = this.$modal.clone(); } $save.text('Saving...'); $save.addClass('NB-disabled'); - + this.update_opinions(); NEWSBLUR.assets.recalculate_story_scores(feed_id); NEWSBLUR.assets.stories.trigger('render:intelligence'); - this.model.save_classifier(data, function() { + this.model.save_classifier(data, function () { if (!keep_modal_open) { NEWSBLUR.reader.feed_unread_count(feed_id); $.modal.close(); } }); }, - - update_opinions: function() { + + update_opinions: function () { var self = this; var feed_id = this.feed_id; - - $('input[type=checkbox]', this.$modal).each(function() { + + $('input[type=checkbox]', this.$modal).each(function () { var $this = $(this); var name = $this.attr('name').replace(/^(dis)?like_/, ''); var score = /^dislike/.test($this.attr('name')) ? -1 : 1; var value = $this.val(); var checked = $this.prop('checked'); - + if (checked) { if (name == 'tag') { self.model.classifiers[feed_id].tags[value] = score; @@ -951,7 +951,7 @@ var classifier_prototype = { } }); } - + }; NEWSBLUR.ReaderClassifierStory.prototype = new NEWSBLUR.Modal; diff --git a/media/js/newsblur/reader/reader_facebook.js b/media/js/newsblur/reader/reader_facebook.js index e1497974f1..723815a9ce 100644 --- a/media/js/newsblur/reader/reader_facebook.js +++ b/media/js/newsblur/reader/reader_facebook.js @@ -1,41 +1,41 @@ -window.fbAsyncInit = function() { - FB.init({ - appId : NEWSBLUR.Globals.debug ? '111137799005981' : '230426707030569', - autoLogAppEvents : true, - xfbml : true, - version : 'v3.2' - }); +window.fbAsyncInit = function () { + FB.init({ + appId: NEWSBLUR.Globals.debug ? '111137799005981' : '230426707030569', + autoLogAppEvents: true, + xfbml: true, + version: 'v3.2' + }); +}; + +NEWSBLUR.ReaderFacebook = function (url, comments, options) { + var defaults = { + 'width': 800 }; - -NEWSBLUR.ReaderFacebook = function(url, comments, options) { - var defaults = { - 'width': 800 - }; - - this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; - this.url = url; - this.comments = comments; - this.runner(); + + this.options = $.extend({}, defaults, options); + this.model = NEWSBLUR.assets; + this.url = url; + this.comments = comments; + this.runner(); }; NEWSBLUR.ReaderFacebook.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderFacebook.prototype.constructor = NEWSBLUR.ReaderFacebook; _.extend(NEWSBLUR.ReaderFacebook.prototype, { - - runner: function() { - $.getScript("https://connect.facebook.net/en_US/sdk.js").done(_.bind(function() { - _.delay(_.bind(function() { - console.log(['Opening facebook dialog', this.url, this.comments]); - FB.ui({ - method: 'share', - quote: this.comments, - href: this.url - }, function(response){}); - }, this), 100); - }, this)); - } - - -}); \ No newline at end of file + + runner: function () { + $.getScript("https://connect.facebook.net/en_US/sdk.js").done(_.bind(function () { + _.delay(_.bind(function () { + console.log(['Opening facebook dialog', this.url, this.comments]); + FB.ui({ + method: 'share', + quote: this.comments, + href: this.url + }, function (response) { }); + }, this), 100); + }, this)); + } + + +}); diff --git a/media/js/newsblur/reader/reader_feed_exception.js b/media/js/newsblur/reader/reader_feed_exception.js index 51fd554870..06fdfb834a 100644 --- a/media/js/newsblur/reader/reader_feed_exception.js +++ b/media/js/newsblur/reader/reader_feed_exception.js @@ -1,17 +1,17 @@ -NEWSBLUR.ReaderFeedException = function(feed_id, options) { +NEWSBLUR.ReaderFeedException = function (feed_id, options) { var defaults = { - 'onOpen': function() { + 'onOpen': function () { $(window).trigger('resize.simplemodal'); } }; - + this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; + this.model = NEWSBLUR.assets; this.feed_id = _.isString(feed_id) && _.string.startsWith(feed_id, 'feed:') ? parseInt(feed_id.replace('feed:', ''), 10) : 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.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(); }; @@ -19,8 +19,8 @@ NEWSBLUR.ReaderFeedException.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderFeedException.prototype.constructor = NEWSBLUR.ReaderFeedException; _.extend(NEWSBLUR.ReaderFeedException.prototype, { - - runner: function() { + + runner: function () { if (this.folder) { NEWSBLUR.Modal.prototype.initialize_folder.call(this, this.folder_title); } else { @@ -29,19 +29,19 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { this.make_modal(); if (this.feed) { this.show_recommended_options_meta(); - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.get_feed_settings(); }, this), 50); } 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) { + initialize_feed: function (feed_id) { var view_setting = this.model.view_setting(feed_id, 'view'); var story_layout = this.model.view_setting(feed_id, 'layout'); @@ -54,20 +54,20 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { } else if (this.folder) { NEWSBLUR.Modal.prototype.initialize_folder.call(this, feed_id); } - - $('input[name=view_settings]', this.$modal).each(function() { + + $('input[name=view_settings]', this.$modal).each(function () { if ($(this).val() == view_setting) { $(this).prop('checked', true); return false; } }); - $('input[name=story_layout]', this.$modal).each(function() { + $('input[name=story_layout]', this.$modal).each(function () { if ($(this).val() == story_layout) { $(this).prop('checked', true); return false; } }); - + if (this.folder) { this.$modal.addClass('NB-modal-folder-settings'); this.$modal.removeClass('NB-modal-feed-settings'); @@ -84,36 +84,36 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { this.resize(); }, - - get_feed_settings: function() { + + 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; + this.model.get_feed_settings; settings_fn.call(this.model, this.feed_id, _.bind(this.populate_settings, this)); }, - - populate_settings: function(data) { + + 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() { + + 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.feed && $.make('div', { className: 'NB-modal-feed-chooser-container' }, [ this.make_feed_chooser() ])), $.make('div', { className: 'NB-modal-loading' }), @@ -134,79 +134,79 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { ]), $.make('div', { className: 'NB-fieldset-fields' }, [ $.make('div', { className: 'NB-exception-input-wrapper' }, [ - $.make('div', { className: 'NB-preference-label'}, [ + $.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("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("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("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("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'}, [ + $.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("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("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("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("img", { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/circular/nav_story_grid_active.png' }), $.make("div", { className: "NB-layout-title" }, "Grid") ]) ]), $.make('div', [ $.make('label', { 'for': 'NB-preference-layout-5' }, [ $.make('input', { id: 'NB-preference-layout-5', type: 'radio', name: 'story_layout', value: 'magazine' }), - $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_magazine_active.png' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/circular/nav_story_magazine_active.png' }), $.make("div", { className: "NB-layout-title" }, "Magazine") ]) ]) - ]) + ]) ]) ]) ]), @@ -291,8 +291,8 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { $.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'}, [ + $.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'), '.' @@ -315,20 +315,20 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { ]) ]); }, - - make_history: function(data, fetch_type) { - var fetches = data[fetch_type+'_history']; + + make_history: function (data, fetch_type) { + var fetches = data[fetch_type + '_history']; var $history; - + if (fetches && fetches.length) { - $history = _.map(fetches, function(fetch) { + $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+')')) + (fetch.status_code && $.make('div', { className: 'NB-history-fetch-code' }, ' (' + fetch.status_code + ')')) ]) ]); }); @@ -336,83 +336,83 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { 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'); - } - } + + 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() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); }, - - save_retry_feed: function() { + + 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() { + + this.model.save_exception_retry(feed_id, function () { NEWSBLUR.reader.force_feed_refresh(feed_id); $.modal.close(); }); }, - - delete_feed: function() { + + 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); + _.delay(function () { $.modal.close(); }, 500); // }); }, - - change_feed_address: function() { + + 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) { + 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); @@ -420,15 +420,15 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { 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() { + 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."; @@ -443,8 +443,8 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { }, this)); } }, - - change_feed_link: function() { + + 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); @@ -457,22 +457,22 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { $error.hide().html(''); if (feed_link.length) { - this.model.save_exception_change_feed_link(feed_id, feed_link, _.bind(function(data) { + 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() { + 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') { @@ -486,84 +486,84 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, { }, this)); } }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-submit-retry' }, function($t, $p) { + + $.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) { + $.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) { + $.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) { + $.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){ + $.targetIs(e, { tagSelector: '.NB-premium-only-link' }, function ($t, $p) { e.preventDefault(); - - self.close(function() { - NEWSBLUR.reader.open_feedchooser_modal({premium_only: true}); + + self.close(function () { + NEWSBLUR.reader.open_feedchooser_modal({ premium_only: true }); }); }); }, - - animate_saved: function() { + + 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}); + 'complete': function () { + _.delay(function () { + $status.animate({ 'opacity': 0 }, { 'queue': false, 'duration': 1000 }); }, 300); } }); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function($t, $p){ + + $.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){ + + $.targetIs(e, { tagSelector: 'input[name=view_settings]' }, function ($t, $p) { if (self.folder) { - self.folder.view_setting({'view': $t.val()}); + self.folder.view_setting({ 'view': $t.val() }); } else { - NEWSBLUR.assets.view_setting(self.feed_id, {'view': $t.val()}); + NEWSBLUR.assets.view_setting(self.feed_id, { 'view': $t.val() }); } self.animate_saved(); }); - $.targetIs(e, { tagSelector: 'input[name=story_layout]' }, function($t, $p){ + $.targetIs(e, { tagSelector: 'input[name=story_layout]' }, function ($t, $p) { if (self.folder) { - self.folder.view_setting({'layout': $t.val()}); + self.folder.view_setting({ 'layout': $t.val() }); } else { - NEWSBLUR.assets.view_setting(self.feed_id, {'layout': $t.val()}); + NEWSBLUR.assets.view_setting(self.feed_id, { 'layout': $t.val() }); } self.animate_saved(); }); } - + }); diff --git a/media/js/newsblur/reader/reader_feedchooser.js b/media/js/newsblur/reader/reader_feedchooser.js index 7c590c7eb1..b5e13b0985 100644 --- a/media/js/newsblur/reader/reader_feedchooser.js +++ b/media/js/newsblur/reader/reader_feedchooser.js @@ -1,20 +1,20 @@ -NEWSBLUR.ReaderFeedchooser = function(options) { +NEWSBLUR.ReaderFeedchooser = function (options) { options = options || {}; var defaults = { 'width': options.premium_only || options.chooser_only ? 600 : 900, 'height': 750, 'premium_only': false, 'chooser_only': false, - 'onOpen': _.bind(function() { + 'onOpen': _.bind(function () { this.resize_modal(); }, this), - 'onClose': _.bind(function() { + 'onClose': _.bind(function () { if (!this.flags['has_saved'] && !this.model.flags['has_chosen_feeds']) { NEWSBLUR.reader.show_feed_chooser_button(); } dialog.data.hide().empty().remove(); dialog.container.hide().empty().remove(); - dialog.overlay.fadeOut(200, function() { + dialog.overlay.fadeOut(200, function () { dialog.overlay.empty().remove(); $.modal.close(callback); }); @@ -31,16 +31,16 @@ NEWSBLUR.ReaderFeedchooser.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderFeedchooser.prototype.constructor = NEWSBLUR.ReaderFeedchooser; _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { - - runner: function() { + + runner: function () { var self = this; this.start = new Date(); this.MAX_FEEDS = 64; - NEWSBLUR.assets.feeds.each(function(feed) { + NEWSBLUR.assets.feeds.each(function (feed) { self.add_feed_to_decline(feed); }); - + this.make_modal(); this.make_paypal_button(); @@ -48,17 +48,17 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { this.initial_load_feeds(); } - _.defer(_.bind(function() { this.update_counts(true); }, this)); + _.defer(_.bind(function () { this.update_counts(true); }, this)); this.flags = { 'has_saved': false }; this.open_modal(); - + this.$modal.bind('mousedown', $.rescope(this.handle_mousedown, this)); this.$modal.bind('change', $.rescope(this.handle_change, this)); }, - + make_modal: function () { var self = this; var $creditcards = $.make('div', { className: 'NB-creditcards' }, [ @@ -113,10 +113,10 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { 'Text view conveniently extracts the story' ]), $.make('li', { className: 'NB-9' }, [ - $.make('div', { className: 'NB-feedchooser-premium-bullet-image' }), - 'You feed Lyric, NewsBlur\'s hungry hound, for ', - $.make('span', { className: 'NB-feedchooser-hungry-dog' }, '6 days'), - $.make('img', { className: 'NB-feedchooser-premium-poor-hungry-dog', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/lyric.jpg' }) + $.make('div', { className: 'NB-feedchooser-premium-bullet-image' }), + 'You feed Lyric, NewsBlur\'s hungry hound, for ', + $.make('span', { className: 'NB-feedchooser-hungry-dog' }, '6 days'), + $.make('img', { className: 'NB-feedchooser-premium-poor-hungry-dog', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/lyric.jpg' }) ]) ]), $.make('div', { className: 'NB-payment-providers' }, [ @@ -322,20 +322,20 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { ])) ]); }, - + make_premium_archive_prorate_message: function () { if (!_.contains(["paypal", "stripe"], NEWSBLUR.Globals.active_provider)) return; return $.make('div', { className: "NB-premium-prorate-message" }, "Your existing subscription will be prorated"); }, - make_paypal_button: function() { + make_paypal_button: function () { jQuery.ajax({ type: "GET", url: NEWSBLUR.URLs.paypal_checkout_js, dataType: "script", cache: true - }).done(_.bind(function() { + }).done(_.bind(function () { $(".NB-paypal-button").each(function () { var $button = $(this); var plan = $button.data('plan'); @@ -355,9 +355,9 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { shape: 'rect', color: 'silver', layout: 'horizontal', - label: 'paypal', + label: 'paypal', }, - + createSubscription: function (data, actions) { return actions.subscription.create({ 'plan_id': plan_id, @@ -368,7 +368,7 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { 'custom_id': NEWSBLUR.Globals.user_id }); }, - + onApprove: function (data, actions) { // Full available details console.log('Paypal approve result', data.subscriptionID, JSON.stringify(data, null, 2)); @@ -378,7 +378,7 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { actions.redirect(NEWSBLUR.URLs.paypal_return); } }, - + onError: function (err) { console.log(err); } @@ -386,33 +386,33 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { }); }, this)); }, - - make_google_button: function() { - var checkout = '
'; - var $checkout = $(checkout); - return $checkout; + + make_google_button: function () { + var checkout = '
'; + var $checkout = $(checkout); + return $checkout; }, - - make_feeds: function() { + + make_feeds: function () { var feeds = this.model.feeds; - this.feed_count = _.unique(NEWSBLUR.assets.folders.feed_ids_in_folder({include_inactive: true})).length; - + this.feed_count = _.unique(NEWSBLUR.assets.folders.feed_ids_in_folder({ include_inactive: true })).length; + this.feedlist = new NEWSBLUR.Views.FeedList({ feed_chooser: true, sorting: this.options.sorting }).make_feeds(); var $feeds = this.feedlist.$el; if (this.options.resize) { - $feeds.css({'max-height': this.options.resize}); + $feeds.css({ 'max-height': this.options.resize }); } if ($feeds.data('sortable')) $feeds.data('sortable').disable(); - + // Expand collapsed folders $('.NB-folder-collapsed', $feeds).css({ 'display': 'block', 'opacity': 1 }).removeClass('NB-folder-collapsed'); - + // Pretend unfetched feeds are fine $('.NB-feed-unfetched', $feeds).removeClass('NB-feed-unfetched'); @@ -420,56 +420,56 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { $('.NB-folder.NB-hidden', $feeds).removeClass('NB-hidden'); NEWSBLUR.assets.folders.sort(); - + NEWSBLUR.assets.feeds.off('change:highlighted') - .on('change:highlighted', _.bind(this.change_selection, this)); - - + .on('change:highlighted', _.bind(this.change_selection, this)); + + return $feeds; }, - resize_modal: function(previous_height) { + resize_modal: function (previous_height) { var content_height = $('.NB-feedchooser-left', this.$modal).height() + 54; var container_height = this.$modal.parent().height(); if (content_height > container_height && previous_height != content_height) { var chooser_height = $('#NB-feedchooser-feeds').height(); var diff = Math.max(4, content_height - container_height); - $('#NB-feedchooser-feeds').css({'max-height': chooser_height - diff}); - _.defer(_.bind(function() { this.resize_modal(content_height); }, this), 1); + $('#NB-feedchooser-feeds').css({ 'max-height': chooser_height - diff }); + _.defer(_.bind(function () { this.resize_modal(content_height); }, this), 1); } }, - - add_feed_to_decline: function(feed, update) { - feed.highlight_in_all_folders(false, true, {silent: !update}); - + + add_feed_to_decline: function (feed, update) { + feed.highlight_in_all_folders(false, true, { silent: !update }); + if (update) { this.update_counts(true); } }, - - add_feed_to_approve: function(feed, update) { - feed.highlight_in_all_folders(true, false, {silent: false}); + + add_feed_to_approve: function (feed, update) { + feed.highlight_in_all_folders(true, false, { silent: false }); if (update) { this.update_counts(true); } }, - change_selection: function(update) { + change_selection: function (update) { this.update_counts(); }, - update_counts: function(autoselected) { + update_counts: function (autoselected) { if (this.options.premium_only) return; - + var $count = $('.NB-feedchooser-info-counts'); var approved = this.feedlist.folder_view.highlighted_count(); var $submit = $('.NB-modal-submit-save', this.$modal); var difference = approved - this.MAX_FEEDS; var muted = this.feed_count - approved; - + $count.text(approved + '/' + Inflector.commas(this.feed_count)); - + if (NEWSBLUR.Globals.is_premium) { $submit.removeClass('NB-disabled').removeClass('NB-modal-submit-grey').attr('disabled', false); if (muted == 0) { @@ -486,25 +486,25 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { this.hide_autoselected_label(); } if (approved > this.MAX_FEEDS) { - $submit.addClass('NB-disabled').addClass('NB-modal-submit-grey').attr('disabled', true).val('Too many sites! Deselect ' + ( - difference == 1 ? - '1 site...' : - difference + ' sites...' - )); + $submit.addClass('NB-disabled').addClass('NB-modal-submit-grey').attr('disabled', true).val('Too many sites! Deselect ' + ( + difference == 1 ? + '1 site...' : + difference + ' sites...' + )); } else { - $submit.removeClass('NB-disabled').removeClass('NB-modal-submit-grey').attr('disabled', false).val('Turn on these '+ approved +' sites, please'); + $submit.removeClass('NB-disabled').removeClass('NB-modal-submit-grey').attr('disabled', false).val('Turn on these ' + approved + ' sites, please'); } } }, - - initial_load_feeds: function(reset) { + + initial_load_feeds: function (reset) { var start = new Date(); var self = this; var feeds = this.model.get_feeds(); var approved = 0; // this.feedlist.folder_view.highlighted_count(); if (!feeds.size()) { - _.defer(_.bind(function() { + _.defer(_.bind(function () { var $info = $('.NB-feedchooser-info', this.$modal); $('.NB-feedchooser-info-counts', $info).hide(); $('.NB-feedchooser-info-sort', $info).hide(); @@ -515,37 +515,37 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { }, this)); return; } - + if (reset) { - feeds.each(function(feed) { + feeds.each(function (feed) { self.add_feed_to_decline(feed, true); }); } - - var active_feeds = feeds.any(function(feed) { return feed.get('active'); }); + + var active_feeds = feeds.any(function (feed) { return feed.get('active'); }); if (!active_feeds || reset) { // Get feed subscribers bottom cut-off var min_subscribers = _.last( - _.first( - _.map(feeds.select(function(f) { return !f.has_exception; }), function(f) { return f.get('subs'); }).sort(function(a,b) { - return b-a; - }), - this.MAX_FEEDS - ) + _.first( + _.map(feeds.select(function (f) { return !f.has_exception; }), function (f) { return f.get('subs'); }).sort(function (a, b) { + return b - a; + }), + this.MAX_FEEDS + ) ); - + // Decline everything var approve_feeds = []; - feeds.each(function(feed) { + feeds.each(function (feed) { // self.add_feed_to_decline(feed); - + if (feed.get('subs') >= min_subscribers) { approve_feeds.push(feed); } }); - + // Approve feeds in subs - _.each(approve_feeds, function(feed) { + _.each(approve_feeds, function (feed) { if (feed.get('subs') > min_subscribers && approved < self.MAX_FEEDS && !feed.get('has_exception')) { @@ -553,63 +553,63 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { self.add_feed_to_approve(feed, false); } }); - _.each(approve_feeds, function(feed) { + _.each(approve_feeds, function (feed) { if (feed.get('subs') == min_subscribers && approved < self.MAX_FEEDS) { approved++; self.add_feed_to_approve(feed, false); } }); - + this.show_autoselected_label(); } else { // Get active feeds - var active_feeds = feeds.select(function(feed) { + var active_feeds = feeds.select(function (feed) { return feed.get('active'); }); // Approve or decline - _.each(active_feeds, function(feed) { + _.each(active_feeds, function (feed) { self.add_feed_to_approve(feed, false); }); - + this.hide_autoselected_label(); } this.update_counts(true); }, - + show_autoselected_label: function () { // console.log('show_autoselected_label'); $('.NB-feedchooser-info-sort', this.$modal).stop(); - $('.NB-feedchooser-info-reset', this.$modal).stop().fadeOut(500, _.bind(function() { + $('.NB-feedchooser-info-reset', this.$modal).stop().fadeOut(500, _.bind(function () { // console.log('show_autoselected_label done'); $('.NB-feedchooser-info-reset', this.$modal).hide(); $('.NB-feedchooser-info-sort', this.$modal).fadeIn(500); }, this)); }, - + hide_autoselected_label: function () { // console.log('hide_autoselected_label'); $('.NB-feedchooser-info-reset', this.$modal).stop(); - $('.NB-feedchooser-info-sort', this.$modal).stop().fadeOut(500, _.bind(function() { + $('.NB-feedchooser-info-sort', this.$modal).stop().fadeOut(500, _.bind(function () { // console.log('hide_autoselected_label done'); $('.NB-feedchooser-info-sort', this.$modal).hide(); $('.NB-feedchooser-info-reset', this.$modal).fadeIn(500); }, this)); }, - - save: function() { + + save: function () { var self = this; var $submit = $('.NB-modal-submit-save', this.$modal); $submit.addClass('NB-disabled').removeClass('NB-modal-submit-green').val('Saving...'); - var approve_list = _.pluck(NEWSBLUR.assets.feeds.filter(function(feed) { + var approve_list = _.pluck(NEWSBLUR.assets.feeds.filter(function (feed) { return feed.get('highlighted'); }), 'id'); console.log(["Saving", approve_list]); NEWSBLUR.reader.flags['reloading_feeds'] = true; - this.model.save_feed_chooser(approve_list, function() { + this.model.save_feed_chooser(approve_list, function () { self.flags['has_saved'] = true; NEWSBLUR.reader.flags['reloading_feeds'] = false; NEWSBLUR.reader.hide_feed_chooser_button(); @@ -617,18 +617,18 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { $.modal.close(); }); }, - - close_and_add: function() { - $.modal.close(function() { + + close_and_add: function () { + $.modal.close(function () { NEWSBLUR.add_feed = new NEWSBLUR.ReaderAddFeed(); }); }, - - open_stripe_form: function() { + + open_stripe_form: function () { var renew = (this.options.renew ? "&renew=true" : ""); window.location.href = "/profile/stripe_form?plan=" + this.plan + renew; }, - + open_stripe_checkout: function (plan, $button) { if ($button.hasClass('NB-disabled')) return; $button.attr('disabled', 'disabled'); @@ -638,7 +638,7 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { $.redirectPost("/profile/switch_stripe_subscription", { "plan": plan }); }, - + open_paypal_checkout: function (plan, $button) { if ($button.hasClass('NB-disabled')) return; $button.attr('disabled', 'disabled'); @@ -653,40 +653,40 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { // = Actions = // =========== - handle_mousedown: function(elem, e) { + handle_mousedown: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-submit-save' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-submit-save' }, _.bind(function ($t, $p) { e.preventDefault(); this.save(); }, this)); - - $.targetIs(e, { tagSelector: '.NB-modal-submit-add' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-submit-add' }, _.bind(function ($t, $p) { e.preventDefault(); this.close_and_add(); }, this)); - - $.targetIs(e, { tagSelector: '.NB-stripe-button-switch-premium' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-stripe-button-switch-premium' }, _.bind(function ($t, $p) { e.preventDefault(); this.open_stripe_checkout('premium', $t); }, this)); - - $.targetIs(e, { tagSelector: '.NB-stripe-button-switch-archive' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-stripe-button-switch-archive' }, _.bind(function ($t, $p) { e.preventDefault(); this.open_stripe_checkout('archive', $t); }, this)); - - $.targetIs(e, { tagSelector: '.NB-paypal-button-archive' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-paypal-button-archive' }, _.bind(function ($t, $p) { e.preventDefault(); this.open_paypal_checkout('archive', $t); }, this)); - - $.targetIs(e, { tagSelector: '.NB-paypal-button-pro' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-paypal-button-pro' }, _.bind(function ($t, $p) { e.preventDefault(); this.open_paypal_checkout('pro', $t); }, this)); - - $.targetIs(e, { tagSelector: '.NB-provider-button-change' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-provider-button-change' }, _.bind(function ($t, $p) { e.preventDefault(); if (NEWSBLUR.Globals.active_provider == "stripe") { this.open_stripe_checkout('change_stripe', $t); @@ -694,16 +694,17 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { this.open_paypal_checkout('change_paypal', $t); } }, this)); - - $.targetIs(e, { tagSelector: '.NB-provider-button-premium' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-provider-button-premium' }, _.bind(function ($t, $p) { e.preventDefault(); if (!NEWSBLUR.Globals.active_provider || NEWSBLUR.Globals.active_provider == "stripe") { this.open_stripe_checkout('premium', $t); } else if (NEWSBLUR.Globals.active_provider == "paypal") { this.open_paypal_checkout('premium', $t); - } }, this)); - - $.targetIs(e, { tagSelector: '.NB-provider-button-archive' }, _.bind(function($t, $p) { + } + }, this)); + + $.targetIs(e, { tagSelector: '.NB-provider-button-archive' }, _.bind(function ($t, $p) { e.preventDefault(); if (!NEWSBLUR.Globals.active_provider || NEWSBLUR.Globals.active_provider == "stripe") { this.open_stripe_checkout('archive', $t); @@ -711,33 +712,34 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, { this.open_paypal_checkout('archive', $t); } }, this)); - - $.targetIs(e, { tagSelector: '.NB-provider-button-pro' }, _.bind(function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-provider-button-pro' }, _.bind(function ($t, $p) { e.preventDefault(); if (!NEWSBLUR.Globals.active_provider || NEWSBLUR.Globals.active_provider == "stripe") { this.open_stripe_checkout('pro', $t); } else if (NEWSBLUR.Globals.active_provider == "paypal") { this.open_paypal_checkout('pro', $t); - } }, this)); - - $.targetIs(e, { tagSelector: '.NB-feedchooser-info-reset' }, _.bind(function($t, $p) { + } + }, this)); + + $.targetIs(e, { tagSelector: '.NB-feedchooser-info-reset' }, _.bind(function ($t, $p) { e.preventDefault(); this.initial_load_feeds(true); }, this)); }, - - handle_change: function(elem, e) { - - + + handle_change: function (elem, e) { + + }, - handle_cancel: function() { + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); } - + }); diff --git a/media/js/newsblur/reader/reader_friends.js b/media/js/newsblur/reader/reader_friends.js index 0db0f16291..f505b1b745 100644 --- a/media/js/newsblur/reader/reader_friends.js +++ b/media/js/newsblur/reader/reader_friends.js @@ -1,8 +1,8 @@ -NEWSBLUR.ReaderFriends = function(options) { +NEWSBLUR.ReaderFriends = function (options) { var defaults = { width: 800 }; - + this.options = $.extend({}, defaults, options); this.sync_checks = 0; this.runner(); @@ -11,8 +11,8 @@ NEWSBLUR.ReaderFriends = function(options) { NEWSBLUR.ReaderFriends.prototype = new NEWSBLUR.Modal; _.extend(NEWSBLUR.ReaderFriends.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); this.open_modal(); this.fetch_friends(); @@ -22,10 +22,10 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { this.$modal.bind('keyup', $.rescope(this.handle_keyup, this)); this.handle_profile_counts(); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal NB-modal-friends' }, [ $.make('div', { className: 'NB-modal-tabs' }, [ $.make('div', { className: 'NB-modal-loading' }), @@ -59,11 +59,11 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { $.make('div', { className: 'NB-tab NB-tab-profile' }, [ $.make('fieldset', [ $.make('legend', 'Profile picture'), - $.make('div', { className: 'NB-modal-section NB-friends-profilephoto'}) + $.make('div', { className: 'NB-modal-section NB-friends-profilephoto' }) ]), $.make('fieldset', [ $.make('legend', 'Profile'), - $.make('div', { className: 'NB-modal-section NB-friends-profile'}) + $.make('div', { className: 'NB-modal-section NB-friends-profile' }) ]), $.make('div', { className: 'NB-modal-submit-grey NB-profile-save-button NB-modal-submit-button' }, 'Save my profile') ]), @@ -71,10 +71,10 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { $.make('div', { className: 'NB-tab NB-tab-followers' }) ]); }, - - fetch_friends: function(callback) { + + fetch_friends: function (callback) { $('.NB-modal-loading', this.$modal).addClass('NB-active'); - NEWSBLUR.assets.fetch_friends(_.bind(function(data) { + NEWSBLUR.assets.fetch_friends(_.bind(function (data) { this.profile = NEWSBLUR.assets.user_profile.clone(); this.services = data.services; this.autofollow = data.autofollow; @@ -85,45 +85,45 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { this.make_following_tab(); callback && callback(); _.defer(_.bind(this.resize, this)); - }, this), _.bind(function(data) { + }, this), _.bind(function (data) { console.log(['Friends fetch error', data]); this.make_find_friends_and_services(); this.make_profile_section(); this.make_followers_tab(); this.make_following_tab(); callback && callback(); - _.defer(_.bind(this.resize, this)); + _.defer(_.bind(this.resize, this)); }, this)); }, - - check_services_sync_status: function() { - NEWSBLUR.assets.fetch_friends(_.bind(function(data) { + + check_services_sync_status: function () { + NEWSBLUR.assets.fetch_friends(_.bind(function (data) { console.log(["Find friends", data]); this.profile = NEWSBLUR.assets.user_profile; this.services = data.services; // if (!this.services['twitter'].syncing && !this.services['facebook'].syncing) { - clearTimeout(this.sync_interval); - this.make_find_friends_and_services(); + clearTimeout(this.sync_interval); + this.make_find_friends_and_services(); // } - }, this), _.bind(function(data) { + }, this), _.bind(function (data) { console.log(['Friends fetch error', data]); clearTimeout(this.sync_interval); this.make_find_friends_and_services(); }, this)); }, - - make_find_friends_and_services: function() { + + make_find_friends_and_services: function () { $('.NB-modal-loading', this.$modal).removeClass('NB-active'); var $services = $('.NB-friends-services', this.$modal).empty(); var service_syncing = false; - - _.each(['twitter', 'facebook'], _.bind(function(service) { + + _.each(['twitter', 'facebook'], _.bind(function (service) { var $service; - - if (this.services && this.services[service][service+'_uid']) { + + if (this.services && this.services[service][service + '_uid']) { var syncing = this.services[service].syncing; if (syncing) service_syncing = true; - $service = $.make('div', { className: 'NB-friends-service NB-connected NB-friends-service-'+service + (this.services[service].syncing ? ' NB-friends-service-syncing' : '') }, [ + $service = $.make('div', { className: 'NB-friends-service NB-connected NB-friends-service-' + service + (this.services[service].syncing ? ' NB-friends-service-syncing' : '') }, [ $.make('div', { className: 'NB-friends-service-title' }, NEWSBLUR.utils.service_name(service)), $.make('div', { className: 'NB-friends-service-connect NB-modal-submit-button NB-modal-submit-grey' }, [ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/' + service + '_service.png' }), @@ -131,7 +131,7 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { ]) ]); } else { - $service = $.make('div', { className: 'NB-friends-service NB-friends-service-'+service }, [ + $service = $.make('div', { className: 'NB-friends-service NB-friends-service-' + service }, [ $.make('div', { className: 'NB-friends-service-title' }, NEWSBLUR.utils.service_name(service)), $.make('div', { className: 'NB-friends-service-connect NB-modal-submit-button NB-modal-submit-green' }, [ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/' + service + '_service_off.png' }), @@ -141,8 +141,8 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { } $services.append($service); }, this)); - - var $autofollow = $.make('div', { className: 'NB-friends-service NB-friends-autofollow'}, [ + + var $autofollow = $.make('div', { className: 'NB-friends-service NB-friends-autofollow' }, [ $.make('input', { type: 'checkbox', className: 'NB-friends-autofollow-checkbox', id: 'NB-friends-autofollow-checkbox', checked: this.autofollow ? 'checked' : null }), $.make('label', { className: 'NB-friends-autofollow-label', 'for': 'NB-friends-autofollow-checkbox' }, [ 'Auto-follow', @@ -151,7 +151,7 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { ]) ]); $services.prepend($autofollow); - + $('.NB-friends-search').html($.make('div', [ $.make('div', { className: "NB-module-search-input NB-module-search-people" }, [ $.make('div', { className: "NB-search-close" }), @@ -163,10 +163,10 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { $.make('div', { className: 'NB-loading NB-friends-search-loading' }), $.make('div', { className: 'NB-friends-search-badges' }) ])); - + var $findlist = $('.NB-friends-findlist', this.$modal).empty(); if (this.recommended_users && this.recommended_users.length) { - _.each(this.recommended_users, function(profile) { + _.each(this.recommended_users, function (profile) { var profile_model = new NEWSBLUR.Models.User(profile); var $profile_badge = new NEWSBLUR.Views.SocialProfileBadge({ model: profile_model @@ -177,21 +177,21 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { var $ghost = $.make('div', { className: 'NB-ghost' }, 'Nobody left to recommend. Good job!'); $findlist.append($ghost); } - + if (service_syncing) { clearTimeout(this.sync_interval); this.sync_checks += 1; - this.sync_interval = _.delay(_.bind(function() { + this.sync_interval = _.delay(_.bind(function () { this.check_services_sync_status(); }, this), this.sync_checks * 1000); } }, - - make_profile_section: function() { + + make_profile_section: function () { var $badge = $('.NB-friends-findfriends-profile', this.$modal).empty(); var $profile_badge; var profile = this.profile; - + // if (!profile.get('location') && !profile.get('bio') && !profile.get('website')) { // $profile_badge = $.make('a', { // className: 'NB-friends-profile-link NB-modal-submit-button NB-modal-submit-green', @@ -202,16 +202,16 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { // $.make('img', { src: NEWSBLUR.Globals['MEDIA_URL']+'img/icons/silk/eye.png' }) // ]); // } else { - $profile_badge = new NEWSBLUR.Views.SocialProfileBadge({ - model: profile, - show_edit_button: true - }); + $profile_badge = new NEWSBLUR.Views.SocialProfileBadge({ + model: profile, + show_edit_button: true + }); // } - + $badge.append($profile_badge); }, - - make_followers_tab: function() { + + make_followers_tab: function () { var $tab = $('.NB-tab-followers', this.$modal).empty(); if (this.profile.get('follower_count') <= 0) { var $ghost = $.make('div', { className: 'NB-ghost NB-modal-section' }, 'Nobody has yet subscribed to your shared stories.'); @@ -224,13 +224,13 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { ]) ]); $tab.append($heading); - NEWSBLUR.assets.follower_profiles.each(_.bind(function(profile) { - $tab.append(new NEWSBLUR.Views.SocialProfileBadge({model: profile})); + NEWSBLUR.assets.follower_profiles.each(_.bind(function (profile) { + $tab.append(new NEWSBLUR.Views.SocialProfileBadge({ model: profile })); }, this)); } }, - - make_following_tab: function() { + + make_following_tab: function () { var $tab = $('.NB-tab-following', this.$modal).empty(); if (this.profile.get('following_count') <= 0) { var $ghost = $.make('div', { className: 'NB-ghost NB-modal-section' }, 'You have not yet subscribed to anybody\'s shared stories.'); @@ -243,15 +243,15 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { ]) ]); $tab.append($heading); - NEWSBLUR.assets.following_profiles.each(_.bind(function(profile) { - $tab.append(new NEWSBLUR.Views.SocialProfileBadge({model: profile})); + NEWSBLUR.assets.following_profiles.each(_.bind(function (profile) { + $tab.append(new NEWSBLUR.Views.SocialProfileBadge({ model: profile })); }, this)); } }, - - open_modal: function(callback) { + + open_modal: function (callback) { var self = this; - + this.$modal.modal({ 'minWidth': this.options.width, 'maxWidth': this.options.width, @@ -259,27 +259,27 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { 'onOpen': function (dialog) { dialog.overlay.fadeIn(200, function () { dialog.container.fadeIn(200); - dialog.data.fadeIn(200, function() { + dialog.data.fadeIn(200, function () { if (self.options.onOpen) { self.options.onOpen(); } }); - setTimeout(function() { + setTimeout(function () { $(window).resize(); }); }); }, - 'onShow': function(dialog) { + 'onShow': function (dialog) { $('#simplemodal-container').corner('6px'); if (self.options.onShow) { self.options.onShow(); } }, - 'onClose': _.bind(function(dialog, callback) { + 'onClose': _.bind(function (dialog, callback) { clearTimeout(this.sync_interval); dialog.data.hide().empty().remove(); dialog.container.hide().empty().remove(); - dialog.overlay.fadeOut(200, function() { + dialog.overlay.fadeOut(200, function () { dialog.overlay.empty().remove(); $.modal.close(callback); }); @@ -287,36 +287,36 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { }, this) }); }, - - switch_tab: function(newtab) { + + switch_tab: function (newtab) { var $modal_tabs = $('.NB-modal-tab', this.$modal); var $tabs = $('.NB-tab', this.$modal); - + $modal_tabs.removeClass('NB-active'); $tabs.removeClass('NB-active'); - - $modal_tabs.filter('.NB-modal-tab-'+newtab).addClass('NB-active'); - $tabs.filter('.NB-tab-'+newtab).addClass('NB-active'); - + + $modal_tabs.filter('.NB-modal-tab-' + newtab).addClass('NB-active'); + $tabs.filter('.NB-tab-' + newtab).addClass('NB-active'); + if (newtab == 'following') { this.make_following_tab(); } else if (newtab == 'followers') { this.make_followers_tab(); } }, - - connect: function(service) { + + connect: function (service) { var self = this; var options = "location=0,status=0,width=800,height=500"; var url = "/oauth/" + service + "_connect"; this.connect_window = window.open(url, '_blank', options); clearInterval(this.connect_window_timer); this.sync_checks = 0; - this.connect_window_timer = setInterval(function() { + this.connect_window_timer = setInterval(function () { console.log(["post connect window?", self, self.connect_window, self.connect_window.closed]); try { - if (!self.connect_window || - !self.connect_window.location || + if (!self.connect_window || + !self.connect_window.location || self.connect_window.closed) { self.post_connect({}); } @@ -325,18 +325,18 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { } }, 1000); }, - - disconnect: function(service) { - var $service = $('.NB-friends-service-'+service, this.$modal); + + disconnect: function (service) { + var $service = $('.NB-friends-service-' + service, this.$modal); $('.NB-friends-service-connect', $service).text('Disconnecting...'); - NEWSBLUR.assets.disconnect_social_service(service, _.bind(function(data) { + NEWSBLUR.assets.disconnect_social_service(service, _.bind(function (data) { this.services = data.services; this.make_find_friends_and_services(); this.make_profile_section(); }, this)); }, - - post_connect: function(data) { + + post_connect: function (data) { data = data || {}; console.log(["post_connect", data, this, this.connect_window_timer]); clearInterval(this.connect_window_timer); @@ -347,60 +347,60 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { data.error ]).css('opacity', 0); $('.NB-friends-services', this.$modal).append($error); - $error.animate({'opacity': 1}, {'duration': 1000}); + $error.animate({ 'opacity': 1 }, { 'duration': 1000 }); } else { this.fetch_friends(); } - + NEWSBLUR.assets.preference('has_found_friends', true); NEWSBLUR.reader.check_hide_getting_started(); }, - search_for_friends: function(query) { + search_for_friends: function (query) { var $loading = $('.NB-friends-search .NB-friends-search-loading', this.$modal); var $badges = $('.NB-friends-search .NB-friends-search-badges', this.$modal); - + if (this.last_query && this.last_query == query) { return; } else { this.last_query = query; } - + if (!query) { $badges.html(''); return; } - + $loading.addClass('NB-active'); - - NEWSBLUR.assets.search_for_friends(query, _.bind(function(data) { + + NEWSBLUR.assets.search_for_friends(query, _.bind(function (data) { $loading.removeClass('NB-active'); if (!data || !data.profiles || !data.profiles.length) { - $badges.html($.make('div', { - className: 'NB-friends-search-badges-empty' + $badges.html($.make('div', { + className: 'NB-friends-search-badges-empty' }, [ $.make('div', { className: 'NB-raquo' }, '»'), - 'Sorry, nobody matches "'+query+'".' + 'Sorry, nobody matches "' + query + '".' ])); return; } - - $badges.html($.make('div', _.map(data.profiles, function(profile) { + + $badges.html($.make('div', _.map(data.profiles, function (profile) { var user = new NEWSBLUR.Models.User(profile); - return new NEWSBLUR.Views.SocialProfileBadge({model: user}); + return new NEWSBLUR.Views.SocialProfileBadge({ model: user }); }))); - + }, this)); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function ($t, $p) { e.preventDefault(); var newtab; if ($t.hasClass('NB-modal-tab-findfriends')) { @@ -411,8 +411,8 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { newtab = 'following'; } self.switch_tab(newtab); - }); - $.targetIs(e, { tagSelector: '.NB-friends-service-connect' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-friends-service-connect' }, function ($t, $p) { e.preventDefault(); var service; var $service = $t.closest('.NB-friends-service'); @@ -427,44 +427,44 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { self.connect(service); } }); - $.targetIs(e, { tagSelector: '.NB-friends-profile-link' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-friends-profile-link' }, function ($t, $p) { e.preventDefault(); - - self.close(function() { + + self.close(function () { NEWSBLUR.reader.open_profile_editor_modal(); }); }); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-friends-autofollow-checkbox' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-friends-autofollow-checkbox' }, function ($t, $p) { e.preventDefault(); - + NEWSBLUR.assets.preference('autofollow_friends', $t.is(':checked')); }); }, - - handle_keyup: function(elem, e) { + + handle_keyup: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '#NB-friends-search-input' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '#NB-friends-search-input' }, function ($t, $p) { self.search_for_friends($t.val()); }); }, - - handle_cancel: function() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); }, - - handle_profile_counts: function() { - var focus = function(e) { + + handle_profile_counts: function () { + var focus = function (e) { var $input = $(e.currentTarget); var $count = $input.next('.NB-count').eq(0); var count = parseInt($input.data('max'), 10) - $input.val().length; @@ -476,11 +476,11 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, { .delegate('input[type=text]', 'keyup', focus) .delegate('input[type=text]', 'keydown', focus) .delegate('input[type=text]', 'change', focus) - .delegate('input[type=text]', 'blur', function(e) { - var $input = $(e.currentTarget); - var $count = $input.next('.NB-count').eq(0); - $count.hide(); - }); + .delegate('input[type=text]', 'blur', function (e) { + var $input = $(e.currentTarget); + var $count = $input.next('.NB-count').eq(0); + $count.hide(); + }); } - + }); diff --git a/media/js/newsblur/reader/reader_goodies.js b/media/js/newsblur/reader/reader_goodies.js index c972139fba..502643c260 100644 --- a/media/js/newsblur/reader/reader_goodies.js +++ b/media/js/newsblur/reader/reader_goodies.js @@ -1,398 +1,398 @@ -NEWSBLUR.ReaderGoodies = function(options) { - var defaults = {}; - - this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; - this.runner(); +NEWSBLUR.ReaderGoodies = function (options) { + var defaults = {}; + + this.options = $.extend({}, defaults, options); + this.model = NEWSBLUR.assets; + this.runner(); }; NEWSBLUR.ReaderGoodies.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderGoodies.prototype.constructor = NEWSBLUR.ReaderGoodies; _.extend(NEWSBLUR.ReaderGoodies.prototype, { - - runner: function() { - this.make_modal(); - this.open_modal(); - - this.$modal.bind('click', $.rescope(this.handle_click, this)); - }, - - make_modal: function() { - var self = this; - - this.$modal = $.make('div', { className: 'NB-modal-goodies NB-modal' }, [ - $.make('h2', { className: 'NB-modal-title' }, [ - $.make('div', { className: 'NB-icon' }), - 'Goodies & Extras', - $.make('div', { className: 'NB-icon-dropdown' }) - ]), - - $.make('fieldset', [ - $.make('legend', 'Bookmarklet') - ]), - $.make('div', { className: 'NB-goodies-group' }, [ - NEWSBLUR.generate_bookmarklet(), - $.make('div', { className: 'NB-goodies-title' }, 'Add Site & Share Story Bookmarklet') - ]), - - $.make('fieldset', [ - $.make('legend', 'Dark theme') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'https://userstyles.org/styles/124890/newsblur-dark-theme-by-splike' - }, 'Download an alternate Dark Theme'), - $.make('div', { className: 'NB-goodies-title' }, 'Dark theme for the web'), - $.make('div', { className: 'NB-goodies-subtitle' }, [ - 'Use the Stylus browser extension to install a user-contributed dark theme. Note that you should use the Stylus extension and not the Stylish extension due to privacy concerns. ', - $.make('a', { href: 'https://www.ghacks.net/2017/05/16/stylus-is-a-stylish-fork-without-analytics/' }, 'You can install Stylus for Firefox, Opera, and Chrome.') - ]) - ]), - - $.make('fieldset', [ - $.make('legend', 'Mobile Apps for NewsBlur') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: '/ios/' - }, 'See the iOS App'), - $.make('div', { className: 'NB-goodies-ios' }), - $.make('div', { className: 'NB-goodies-title' }, 'Official NewsBlur iPhone/iPad App') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: '/android/' - }, 'See the Android App'), - $.make('div', { className: 'NB-goodies-android' }), - $.make('div', { className: 'NB-goodies-title' }, 'Official NewsBlur Android App') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://reederapp.com/ios' - }, 'Download for iPhone and iPad'), - $.make('div', { className: 'NB-goodies-reeder-ios' }), - $.make('div', { className: 'NB-goodies-title' }, 'Reeder for iOS') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://supertop.co/unread/' - }, 'Download for iPhone and iPad'), - $.make('div', { className: 'NB-goodies-unread-ios' }), - $.make('div', { className: 'NB-goodies-title' }, 'Unread for iOS') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://addmyfeed.cubesoft.fr' - }, 'Download for iPhone'), - $.make('div', { className: 'NB-goodies-ios' }), - $.make('div', { className: 'NB-goodies-title' }, 'Add My Feed for iOS') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'https://play.google.com/store/apps/details?id=com.grazeten' - }, 'View in Play Store'), - $.make('div', { className: 'NB-goodies-android' }), - $.make('div', { className: 'NB-goodies-title' }, 'GrazeTEN') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'https://www.microsoft.com/store/productId/9N85PV1RJD6V' - }, 'View in Microsoft Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'RSS Tracker for Windows 10') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'https://www.microsoft.com/en-us/store/apps/hypersonic/9nblggh5wnb6' - }, 'View in Windows Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'Hypersonic for Windows 10 & Phone') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://windowsphone.com/s?appid=900e67fd-9934-e011-854c-00237de2db9e' - }, 'View in Windows Phone Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'Feed Me') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://windowsphone.com/s?appid=2585d348-0894-41b6-8c26-77aeb257f9d8' - }, 'View in Windows Phone Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'Metroblur') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://www.windowsphone.com/s?appid=f001b025-94d7-4769-a33d-7dd34778141c' - }, 'View in Windows Phone Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'NewsSpot') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://www.windowsphone.com/s?appid=5bef74a6-9ccc-df11-9eae-00237de2db9e' - }, 'View in Windows Phone Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'Feed Reader') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://www.windowsphone.com/en-us/store/app/swift-reader/e1e672a1-dd3a-483d-8457-81d3ca4a13ef' - }, 'View in Windows Phone Store'), - $.make('div', { className: 'NB-goodies-windows' }), - $.make('div', { className: 'NB-goodies-title' }, 'Swift Reader') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://projects.developer.nokia.com/feed_reader' - }, 'View in Nokia Store'), - $.make('div', { className: 'NB-goodies-nokia' }), - $.make('div', { className: 'NB-goodies-title' }, 'Web Feeds') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'https://github.com/jrutila/harbour-newsblur' - }, 'View in Sailfish OS'), - $.make('div', { className: 'NB-goodies-sailfish' }), - $.make('div', { className: 'NB-goodies-title' }, 'Sailblur') - ]), - $.make('fieldset', [ - $.make('legend', 'Native Apps for NewsBlur') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://readkitapp.com' - }, 'Download ReadKit for Mac'), - $.make('div', { className: 'NB-goodies-readkit' }), - $.make('div', { className: 'NB-goodies-title' }, 'ReadKit') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://reederapp.com/mac' - }, 'Download Reeder for Mac'), - $.make('div', { className: 'NB-goodies-reeder-mac' }), - $.make('div', { className: 'NB-goodies-title' }, 'Reeder for Mac') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CB8QFjAAahUKEwio9f6u_bDHAhXLOIgKHdRdAuY&url=https%3A%2F%2Fitunes.apple.com%2Fus%2Fapp%2Fleaf-rss-news-reader%2Fid576338668%3Fmt%3D12&ei=IUfSVejgIcvxoATUu4mwDg&usg=AFQjCNGAqtn9qxkLqh5LfjPUZ0QFKr1mLg&sig2=yreuovrI2rRrWvzUkB4ydw&bvm=bv.99804247,d.cGU' - }, 'Download Leaf for Mac'), - $.make('div', { className: 'NB-goodies-leaf' }), - $.make('div', { className: 'NB-goodies-title' }, 'Leaf for Mac') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://www.tafitiapp.com/mx/' - }, 'Download Tafiti for Windows 8'), - $.make('div', { className: 'NB-goodies-tafiti' }), - $.make('div', { className: 'NB-goodies-title' }, 'Tafiti') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', - href: 'http://apps.microsoft.com/windows/en-us/app/bluree/35b1d32a-5abb-479a-8fd1-bbed4fa0172e' - }, 'Download Bluree for Windows 8'), - $.make('div', { className: 'NB-goodies-bluree' }), - $.make('div', { className: 'NB-goodies-title' }, 'Bluree') - ]), - $.make('fieldset', [ - $.make('legend', 'Browser Extensions for NewsBlur') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-firefox-link NB-modal-submit-button NB-modal-submit-green', - href: '#' - }, 'Add to Firefox'), - $.make('div', { className: 'NB-goodies-firefox' }), - $.make('div', { className: 'NB-goodies-title' }, 'Firefox: Register NewsBlur as an RSS reader') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: 'https://addons.mozilla.org/en-US/firefox/addon/newsblurcom-notifier/' - }, 'Download'), - $.make('div', { className: 'NB-goodies-firefox' }), - $.make('div', { className: 'NB-goodies-title' }, 'Firefox: NewsBlur Notifier'), - $.make('div', { className: 'NB-goodies-subtitle' }, 'Shows a button with the number of unread articles.') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('div', { className: 'NB-goodies-firefox' }), - $.make('div', { className: 'NB-goodies-title' }, 'Firefox: Open links to background tab'), - $.make('div', { className: 'NB-goodies-subtitle' }, [ - $.make('ul', [ - $.make('li', [ - 'Open a new tab, enter ', - $.make('a', { href: 'about:config', target: '_blank' }, 'about:config') - ]), - $.make('li', [ - 'Search for ', - $.make('b', 'browser.tabs.loadDivertedInBackground') - ]), - $.make('li', 'Double click on \'false\' to set \'Value\' to \'true\''), - $.make('li', 'Go to NewsBlur and open a story with \'o\' and see it load in the background') - ]) - ]) - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: 'https://chrome.google.com/webstore/detail/rss-subscription-extensio/nlbjncdgjeocebhnmkbbbdekmmmcbfjd/details?hl=en' - }, 'Add to Chrome'), - $.make('div', { className: 'NB-goodies-chrome' }), - $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: Register NewsBlur as an RSS reader'), - $.make('div', { className: 'NB-goodies-subtitle' }, [ - 'To use this extension, use the custom add site URL below.' - ]) - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: 'https://chrome.google.com/webstore/detail/rss-subscription-extensio/bmjffnfcokiodbeiamclanljnaheeoke' - }, 'Download'), - $.make('div', { className: 'NB-goodies-chrome' }), - $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: NewsBlur Chrome Web App'), - $.make('div', { className: 'NB-goodies-subtitle'}, 'Adds one-click subscription to your toolbar.') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: 'https://chrome.google.com/webstore/detail/newsblur-notifier-plus/nbmlfepgaaalffdmmjhpkgpjjlnpjjlp' - }, 'Download'), - $.make('div', { className: 'NB-goodies-chrome' }), - $.make('div', { className: 'NB-goodies-title' }, 'Chrome: NewsBlur Notifier Plus'), - $.make('div', { className: 'NB-goodies-subtitle' }, 'Shows the unread count from your NewsBlur account.') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: 'https://chrome.google.com/webstore/detail/ieeimmkgocgaaabphkgjdkophaejfnlk/' - }, 'Download'), - $.make('div', { className: 'NB-goodies-chrome' }), - $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: Open links in background tab'), - $.make('div', { className: 'NB-goodies-subtitle' }, [ - 'This extension allows you to open a link in a background tab by pressing a customizable hotkey (default \'o\' or \'v\'). This feature used to work without an extension, but it broke starting with Chrome 41.' - ]) - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: 'https://chrome.google.com/webstore/detail/unofficial-newsblur-reade/hnegmjknmfninedmmlhndnjlblopjgad?utm_campaign=en&utm_source=en-ha-na-us-bk-webstr&utm_medium=ha' - }, 'Download'), - $.make('div', { className: 'NB-goodies-chrome' }), - $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: Unofficial browser extension'), - $.make('div', { className: 'NB-goodies-subtitle' }, [ - 'This extension displays all of your unread stories and unread counts.' - ]) - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-modal-submit-button NB-modal-submit-green', - href: NEWSBLUR.Globals.MEDIA_URL + 'extensions/NewsBlur Safari Helper.app.zip' - }, 'Add to Safari'), - $.make('div', { className: 'NB-goodies-safari' }), - $.make('div', { className: 'NB-goodies-title' }, 'Safari: Register NewsBlur as an RSS reader'), - $.make('div', { className: 'NB-goodies-subtitle' }, [ - 'To use this extension, extract and move the NewsBlur Safari Helper.app ', - 'to your Applications folder. Then in ', - $.make('b', 'Safari > Settings > RSS'), - ' choose the new NewsBlur Safari Helper.app. If you don\'t have an RSS chooser, ', - 'you will have to use ', - $.make('a', { href: 'http://www.rubicode.com/Software/RCDefaultApp/', className: 'NB-splash-link' }, 'RCDefaultApp'), - ' to select the NewsBlur Safari Helper as your RSS reader. Then loading an RSS ', - 'feed in Safari will open the feed in NewsBlur. Simple!' - ]) - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-safari-notifier NB-modal-submit-button NB-modal-submit-green', - href: 'https://github.com/anaconda/NewsBlur-Counter' - }, 'Download'), - $.make('div', { className: 'NB-goodies-safari' }), - $.make('div', { className: 'NB-goodies-title' }, 'Safari: NewsBlur unread count notifier'), - $.make('div', { className: 'NB-goodies-subtitle' }, 'Safari extension to show on the toolbar how many unread stories are waiting for you on NewsBlur.') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('a', { - className: 'NB-goodies-safari-notifier NB-modal-submit-button NB-modal-submit-green', - href: 'https://github.com/nriley/NewsBlur-Helper' - }, 'Download'), - $.make('div', { className: 'NB-goodies-safari' }), - $.make('div', { className: 'NB-goodies-title' }, 'Safari: Open links in background tab'), - $.make('div', { className: 'NB-goodies-subtitle' }, 'Safari extension to let you choose if links open in a new active tab or background tab.') - ]), - - $.make('fieldset', [ - $.make('legend', 'Custom URLs') - ]), - $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ - $.make('input', { - className: 'NB-goodies-custom-input', - value: 'https://www.newsblur.com/?url=BLOG_URL_GOES_HERE' - }), - $.make('div', { className: 'NB-goodies-custom' }), - $.make('div', { className: 'NB-goodies-title' }, 'Custom Add Site URL') - ]) - ]); - }, - - // =========== - // = Actions = - // =========== - handle_click: function(elem, e) { - var self = this; - - $.targetIs(e, { tagSelector: '.NB-goodies-bookmarklet-button' }, function($t, $p) { - e.preventDefault(); - - alert('Drag this button to your bookmark toolbar.'); - }); + runner: function () { + this.make_modal(); + this.open_modal(); + + this.$modal.bind('click', $.rescope(this.handle_click, this)); + }, + + make_modal: function () { + var self = this; + + this.$modal = $.make('div', { className: 'NB-modal-goodies NB-modal' }, [ + $.make('h2', { className: 'NB-modal-title' }, [ + $.make('div', { className: 'NB-icon' }), + 'Goodies & Extras', + $.make('div', { className: 'NB-icon-dropdown' }) + ]), + + $.make('fieldset', [ + $.make('legend', 'Bookmarklet') + ]), + $.make('div', { className: 'NB-goodies-group' }, [ + NEWSBLUR.generate_bookmarklet(), + $.make('div', { className: 'NB-goodies-title' }, 'Add Site & Share Story Bookmarklet') + ]), + + $.make('fieldset', [ + $.make('legend', 'Dark theme') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'https://userstyles.org/styles/124890/newsblur-dark-theme-by-splike' + }, 'Download an alternate Dark Theme'), + $.make('div', { className: 'NB-goodies-title' }, 'Dark theme for the web'), + $.make('div', { className: 'NB-goodies-subtitle' }, [ + 'Use the Stylus browser extension to install a user-contributed dark theme. Note that you should use the Stylus extension and not the Stylish extension due to privacy concerns. ', + $.make('a', { href: 'https://www.ghacks.net/2017/05/16/stylus-is-a-stylish-fork-without-analytics/' }, 'You can install Stylus for Firefox, Opera, and Chrome.') + ]) + ]), + + $.make('fieldset', [ + $.make('legend', 'Mobile Apps for NewsBlur') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: '/ios/' + }, 'See the iOS App'), + $.make('div', { className: 'NB-goodies-ios' }), + $.make('div', { className: 'NB-goodies-title' }, 'Official NewsBlur iPhone/iPad App') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: '/android/' + }, 'See the Android App'), + $.make('div', { className: 'NB-goodies-android' }), + $.make('div', { className: 'NB-goodies-title' }, 'Official NewsBlur Android App') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://reederapp.com/ios' + }, 'Download for iPhone and iPad'), + $.make('div', { className: 'NB-goodies-reeder-ios' }), + $.make('div', { className: 'NB-goodies-title' }, 'Reeder for iOS') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://supertop.co/unread/' + }, 'Download for iPhone and iPad'), + $.make('div', { className: 'NB-goodies-unread-ios' }), + $.make('div', { className: 'NB-goodies-title' }, 'Unread for iOS') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://addmyfeed.cubesoft.fr' + }, 'Download for iPhone'), + $.make('div', { className: 'NB-goodies-ios' }), + $.make('div', { className: 'NB-goodies-title' }, 'Add My Feed for iOS') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'https://play.google.com/store/apps/details?id=com.grazeten' + }, 'View in Play Store'), + $.make('div', { className: 'NB-goodies-android' }), + $.make('div', { className: 'NB-goodies-title' }, 'GrazeTEN') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'https://www.microsoft.com/store/productId/9N85PV1RJD6V' + }, 'View in Microsoft Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'RSS Tracker for Windows 10') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'https://www.microsoft.com/en-us/store/apps/hypersonic/9nblggh5wnb6' + }, 'View in Windows Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'Hypersonic for Windows 10 & Phone') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://windowsphone.com/s?appid=900e67fd-9934-e011-854c-00237de2db9e' + }, 'View in Windows Phone Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'Feed Me') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://windowsphone.com/s?appid=2585d348-0894-41b6-8c26-77aeb257f9d8' + }, 'View in Windows Phone Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'Metroblur') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://www.windowsphone.com/s?appid=f001b025-94d7-4769-a33d-7dd34778141c' + }, 'View in Windows Phone Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'NewsSpot') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://www.windowsphone.com/s?appid=5bef74a6-9ccc-df11-9eae-00237de2db9e' + }, 'View in Windows Phone Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'Feed Reader') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://www.windowsphone.com/en-us/store/app/swift-reader/e1e672a1-dd3a-483d-8457-81d3ca4a13ef' + }, 'View in Windows Phone Store'), + $.make('div', { className: 'NB-goodies-windows' }), + $.make('div', { className: 'NB-goodies-title' }, 'Swift Reader') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://projects.developer.nokia.com/feed_reader' + }, 'View in Nokia Store'), + $.make('div', { className: 'NB-goodies-nokia' }), + $.make('div', { className: 'NB-goodies-title' }, 'Web Feeds') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'https://github.com/jrutila/harbour-newsblur' + }, 'View in Sailfish OS'), + $.make('div', { className: 'NB-goodies-sailfish' }), + $.make('div', { className: 'NB-goodies-title' }, 'Sailblur') + ]), + $.make('fieldset', [ + $.make('legend', 'Native Apps for NewsBlur') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://readkitapp.com' + }, 'Download ReadKit for Mac'), + $.make('div', { className: 'NB-goodies-readkit' }), + $.make('div', { className: 'NB-goodies-title' }, 'ReadKit') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://reederapp.com/mac' + }, 'Download Reeder for Mac'), + $.make('div', { className: 'NB-goodies-reeder-mac' }), + $.make('div', { className: 'NB-goodies-title' }, 'Reeder for Mac') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CB8QFjAAahUKEwio9f6u_bDHAhXLOIgKHdRdAuY&url=https%3A%2F%2Fitunes.apple.com%2Fus%2Fapp%2Fleaf-rss-news-reader%2Fid576338668%3Fmt%3D12&ei=IUfSVejgIcvxoATUu4mwDg&usg=AFQjCNGAqtn9qxkLqh5LfjPUZ0QFKr1mLg&sig2=yreuovrI2rRrWvzUkB4ydw&bvm=bv.99804247,d.cGU' + }, 'Download Leaf for Mac'), + $.make('div', { className: 'NB-goodies-leaf' }), + $.make('div', { className: 'NB-goodies-title' }, 'Leaf for Mac') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://www.tafitiapp.com/mx/' + }, 'Download Tafiti for Windows 8'), + $.make('div', { className: 'NB-goodies-tafiti' }), + $.make('div', { className: 'NB-goodies-title' }, 'Tafiti') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-mobile-link NB-modal-submit-button NB-modal-submit-green', + href: 'http://apps.microsoft.com/windows/en-us/app/bluree/35b1d32a-5abb-479a-8fd1-bbed4fa0172e' + }, 'Download Bluree for Windows 8'), + $.make('div', { className: 'NB-goodies-bluree' }), + $.make('div', { className: 'NB-goodies-title' }, 'Bluree') + ]), + $.make('fieldset', [ + $.make('legend', 'Browser Extensions for NewsBlur') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-firefox-link NB-modal-submit-button NB-modal-submit-green', + href: '#' + }, 'Add to Firefox'), + $.make('div', { className: 'NB-goodies-firefox' }), + $.make('div', { className: 'NB-goodies-title' }, 'Firefox: Register NewsBlur as an RSS reader') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: 'https://addons.mozilla.org/en-US/firefox/addon/newsblurcom-notifier/' + }, 'Download'), + $.make('div', { className: 'NB-goodies-firefox' }), + $.make('div', { className: 'NB-goodies-title' }, 'Firefox: NewsBlur Notifier'), + $.make('div', { className: 'NB-goodies-subtitle' }, 'Shows a button with the number of unread articles.') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('div', { className: 'NB-goodies-firefox' }), + $.make('div', { className: 'NB-goodies-title' }, 'Firefox: Open links to background tab'), + $.make('div', { className: 'NB-goodies-subtitle' }, [ + $.make('ul', [ + $.make('li', [ + 'Open a new tab, enter ', + $.make('a', { href: 'about:config', target: '_blank' }, 'about:config') + ]), + $.make('li', [ + 'Search for ', + $.make('b', 'browser.tabs.loadDivertedInBackground') + ]), + $.make('li', 'Double click on \'false\' to set \'Value\' to \'true\''), + $.make('li', 'Go to NewsBlur and open a story with \'o\' and see it load in the background') + ]) + ]) + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: 'https://chrome.google.com/webstore/detail/rss-subscription-extensio/nlbjncdgjeocebhnmkbbbdekmmmcbfjd/details?hl=en' + }, 'Add to Chrome'), + $.make('div', { className: 'NB-goodies-chrome' }), + $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: Register NewsBlur as an RSS reader'), + $.make('div', { className: 'NB-goodies-subtitle' }, [ + 'To use this extension, use the custom add site URL below.' + ]) + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: 'https://chrome.google.com/webstore/detail/rss-subscription-extensio/bmjffnfcokiodbeiamclanljnaheeoke' + }, 'Download'), + $.make('div', { className: 'NB-goodies-chrome' }), + $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: NewsBlur Chrome Web App'), + $.make('div', { className: 'NB-goodies-subtitle' }, 'Adds one-click subscription to your toolbar.') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: 'https://chrome.google.com/webstore/detail/newsblur-notifier-plus/nbmlfepgaaalffdmmjhpkgpjjlnpjjlp' + }, 'Download'), + $.make('div', { className: 'NB-goodies-chrome' }), + $.make('div', { className: 'NB-goodies-title' }, 'Chrome: NewsBlur Notifier Plus'), + $.make('div', { className: 'NB-goodies-subtitle' }, 'Shows the unread count from your NewsBlur account.') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: 'https://chrome.google.com/webstore/detail/ieeimmkgocgaaabphkgjdkophaejfnlk/' + }, 'Download'), + $.make('div', { className: 'NB-goodies-chrome' }), + $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: Open links in background tab'), + $.make('div', { className: 'NB-goodies-subtitle' }, [ + 'This extension allows you to open a link in a background tab by pressing a customizable hotkey (default \'o\' or \'v\'). This feature used to work without an extension, but it broke starting with Chrome 41.' + ]) + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: 'https://chrome.google.com/webstore/detail/unofficial-newsblur-reade/hnegmjknmfninedmmlhndnjlblopjgad?utm_campaign=en&utm_source=en-ha-na-us-bk-webstr&utm_medium=ha' + }, 'Download'), + $.make('div', { className: 'NB-goodies-chrome' }), + $.make('div', { className: 'NB-goodies-title' }, 'Google Chrome: Unofficial browser extension'), + $.make('div', { className: 'NB-goodies-subtitle' }, [ + 'This extension displays all of your unread stories and unread counts.' + ]) + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-modal-submit-button NB-modal-submit-green', + href: NEWSBLUR.Globals.MEDIA_URL + 'extensions/NewsBlur Safari Helper.app.zip' + }, 'Add to Safari'), + $.make('div', { className: 'NB-goodies-safari' }), + $.make('div', { className: 'NB-goodies-title' }, 'Safari: Register NewsBlur as an RSS reader'), + $.make('div', { className: 'NB-goodies-subtitle' }, [ + 'To use this extension, extract and move the NewsBlur Safari Helper.app ', + 'to your Applications folder. Then in ', + $.make('b', 'Safari > Settings > RSS'), + ' choose the new NewsBlur Safari Helper.app. If you don\'t have an RSS chooser, ', + 'you will have to use ', + $.make('a', { href: 'http://www.rubicode.com/Software/RCDefaultApp/', className: 'NB-splash-link' }, 'RCDefaultApp'), + ' to select the NewsBlur Safari Helper as your RSS reader. Then loading an RSS ', + 'feed in Safari will open the feed in NewsBlur. Simple!' + ]) + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-safari-notifier NB-modal-submit-button NB-modal-submit-green', + href: 'https://github.com/anaconda/NewsBlur-Counter' + }, 'Download'), + $.make('div', { className: 'NB-goodies-safari' }), + $.make('div', { className: 'NB-goodies-title' }, 'Safari: NewsBlur unread count notifier'), + $.make('div', { className: 'NB-goodies-subtitle' }, 'Safari extension to show on the toolbar how many unread stories are waiting for you on NewsBlur.') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('a', { + className: 'NB-goodies-safari-notifier NB-modal-submit-button NB-modal-submit-green', + href: 'https://github.com/nriley/NewsBlur-Helper' + }, 'Download'), + $.make('div', { className: 'NB-goodies-safari' }), + $.make('div', { className: 'NB-goodies-title' }, 'Safari: Open links in background tab'), + $.make('div', { className: 'NB-goodies-subtitle' }, 'Safari extension to let you choose if links open in a new active tab or background tab.') + ]), + + $.make('fieldset', [ + $.make('legend', 'Custom URLs') + ]), + $.make('div', { className: 'NB-goodies-group NB-modal-submit' }, [ + $.make('input', { + className: 'NB-goodies-custom-input', + value: 'https://www.newsblur.com/?url=BLOG_URL_GOES_HERE' + }), + $.make('div', { className: 'NB-goodies-custom' }), + $.make('div', { className: 'NB-goodies-title' }, 'Custom Add Site URL') + ]) + ]); + }, + + // =========== + // = Actions = + // =========== + + handle_click: function (elem, e) { + var self = this; + + $.targetIs(e, { tagSelector: '.NB-goodies-bookmarklet-button' }, function ($t, $p) { + e.preventDefault(); + + alert('Drag this button to your bookmark toolbar.'); + }); + + $.targetIs(e, { tagSelector: '.NB-goodies-firefox-link' }, function ($t, $p) { + e.preventDefault(); + var host = [ + document.location.protocol, + '//', + document.location.host, + '/' + ].join(''); + navigator.registerContentHandler("application/vnd.mozilla.maybe.feed", + host + "?url=%s", + "NewsBlur"); + navigator.registerContentHandler("application/atom+xml", + host + "?url=%s", + "NewsBlur"); + navigator.registerContentHandler("application/rss+xml", + host + "?url=%s", + "NewsBlur"); + }); - $.targetIs(e, { tagSelector: '.NB-goodies-firefox-link' }, function($t, $p) { - e.preventDefault(); - var host = [ - document.location.protocol, - '//', - document.location.host, - '/' - ].join(''); - navigator.registerContentHandler("application/vnd.mozilla.maybe.feed", - host + "?url=%s", - "NewsBlur"); - navigator.registerContentHandler("application/atom+xml", - host + "?url=%s", - "NewsBlur"); - navigator.registerContentHandler("application/rss+xml", - host + "?url=%s", - "NewsBlur"); - }); + $.targetIs(e, { tagSelector: '.NB-goodies-custom-input' }, function ($t, $p) { + e.preventDefault(); + $t.select(); + }); + } - $.targetIs(e, { tagSelector: '.NB-goodies-custom-input' }, function($t, $p) { - e.preventDefault(); - $t.select(); - }); - } - }); diff --git a/media/js/newsblur/reader/reader_intro.js b/media/js/newsblur/reader/reader_intro.js index adf3984ebe..5d5426375f 100644 --- a/media/js/newsblur/reader/reader_intro.js +++ b/media/js/newsblur/reader/reader_intro.js @@ -1,12 +1,12 @@ -NEWSBLUR.ReaderIntro = function(options) { +NEWSBLUR.ReaderIntro = function (options) { var defaults = { modal_container_class: "NB-full-container" }; var intro_page = NEWSBLUR.assets.preference('intro_page'); - + _.bindAll(this, 'close', 'post_connect'); this.options = $.extend({ - 'page_number': intro_page && _.isNumber(intro_page) && intro_page <= 4 ? intro_page : 1 + 'page_number': intro_page && _.isNumber(intro_page) && intro_page <= 4 ? intro_page : 1 }, defaults, options); this.services = { 'twitter': {}, @@ -15,7 +15,7 @@ NEWSBLUR.ReaderIntro = function(options) { this.flags = {}; this.autofollow = true; this.chosen_categories = []; - + this.page_number = this.options.page_number; this.slider_value = 0; this.intervals = {}; @@ -27,26 +27,26 @@ NEWSBLUR.ReaderIntro.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderIntro.prototype.constructor = NEWSBLUR.ReaderIntro; _.extend(NEWSBLUR.ReaderIntro.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); this.make_find_friends_and_services(); this.open_modal(); this.page(this.page_number); this.fetch_categories(); this.fetch_friends(); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); this.$modal.bind('change', $.rescope(this.handle_change, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-intro NB-modal' }, [ $.make('div', { className: 'NB-modal-page' }, [ $.make('span', { className: 'NB-modal-page-text' }), - $.make('span', { className: 'NB-modal-loading NB-spinner'}) + $.make('span', { className: 'NB-modal-loading NB-spinner' }) ]), $.make('h2', { className: 'NB-modal-title' }, [ 'Welcome to NewsBlur', @@ -57,7 +57,7 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { $.make('h4', { className: 'NB-page-1-started' }, "So much time and so little to do. Strike that! Reverse it.") ]), $.make('div', { className: 'NB-page NB-page-2' }, [ - $.make('div', { className: 'NB-intro-imports NB-intro-imports-start'}, [ + $.make('div', { className: 'NB-intro-imports NB-intro-imports-start' }, [ $.make('div', { className: 'NB-page-2-started' }, [ $.make('h4', "Let's get some sites to read."), $.make('div', { className: 'NB-intro-import-starred-message' }) @@ -73,7 +73,7 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { $.make('div', { className: 'NB-intro-module-container NB-right' }, [ $.make('h3', { className: 'NB-module-content-header' }, 'Upload'), $.make('div', { className: 'NB-intro-module NB-intro-import-opml' }, [ - $.make('div', { className: 'NB-carousel'}, [ + $.make('div', { className: 'NB-carousel' }, [ $.make('div', { className: 'NB-carousel-inner NB-intro-imports' }, [ $.make('div', { className: 'NB-carousel-item NB-intro-imports-start' }, [ $.make('h3', 'OPML'), @@ -165,40 +165,40 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { ]) ]), $.make('div', { className: 'NB-modal-submit-bottom' }, [ - $.make('div', { className: 'NB-page-next NB-modal-submit-button NB-modal-submit-green NB-modal-submit-save' }, [ - $.make('span', { className: 'NB-tutorial-next-page-text' }, "Let's Get Started "), - $.make('span', { className: 'NB-raquo' }, '»') - ]) + $.make('div', { className: 'NB-page-next NB-modal-submit-button NB-modal-submit-green NB-modal-submit-save' }, [ + $.make('span', { className: 'NB-tutorial-next-page-text' }, "Let's Get Started "), + $.make('span', { className: 'NB-raquo' }, '»') + ]) ]) ]); - + if (this.options.force_import) { // this.$modal.addClass('NB-intro-import-only'); } }, - + // ============== // = Categories = // ============== - - fetch_categories: function(callback) { + + fetch_categories: function (callback) { $('.NB-intro-categories-loader', this.$modal).addClass('NB-active'); - NEWSBLUR.assets.fetch_categories(_.bind(function(data) { + NEWSBLUR.assets.fetch_categories(_.bind(function (data) { this.categories = data.categories; this.category_feeds = data.feeds; this.make_categories(); callback && callback(); - }, this), _.bind(function(data) { + }, this), _.bind(function (data) { console.log(['Categories fetch error', data]); }, this)); }, - - make_categories: function() { + + make_categories: function () { $('.NB-intro-categories-loader', this.$modal).removeClass('NB-active'); var $categories = $(".NB-intro-categories", this.$modal); - var categories = _.map(this.categories, _.bind(function(category) { - var $feeds = _.compact(_.map(category.feed_ids, _.bind(function(feed_id) { + var categories = _.map(this.categories, _.bind(function (category) { + var $feeds = _.compact(_.map(category.feed_ids, _.bind(function (feed_id) { var feed = this.category_feeds[feed_id]; if (!feed) return; feed = new NEWSBLUR.Models.Feed(feed); @@ -217,11 +217,11 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { ]).data('category', category.title); return $category; }, this)); - + $categories.html($.make('div', categories)); }, - - toggle_category: function(category, $category) { + + toggle_category: function (category, $category) { var on = _.contains(this.chosen_categories, category); if (on) { this.chosen_categories = _.without(this.chosen_categories, category); @@ -230,7 +230,7 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } $category.toggleClass('NB-active', !on); $(".NB-category-title", $category).toggleClass('NB-modal-submit-grey', on) - .toggleClass('NB-modal-submit-green', !on); + .toggleClass('NB-modal-submit-green', !on); if (this.chosen_categories.length) { NEWSBLUR.assets.preference('has_setup_feeds', true); @@ -238,43 +238,43 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { $('.NB-tutorial-next-page-text', this.$modal).text('Next step '); } }, - - submit_categories: function() { + + submit_categories: function () { if (this.chosen_categories.length) { - NEWSBLUR.assets.subscribe_to_categories(this.chosen_categories, function() { + NEWSBLUR.assets.subscribe_to_categories(this.chosen_categories, function () { NEWSBLUR.assets.load_feeds(); }); } }, - + // ========== // = Social = // ========== - - fetch_friends: function(callback) { + + fetch_friends: function (callback) { $('.NB-modal-loading', this.$modal).addClass('NB-active'); - NEWSBLUR.assets.fetch_friends(_.bind(function(data) { + NEWSBLUR.assets.fetch_friends(_.bind(function (data) { this.profile = NEWSBLUR.assets.user_profile; this.services = data.services; this.autofollow = data.autofollow; this.make_find_friends_and_services(); callback && callback(); - }, this), _.bind(function(data) { + }, this), _.bind(function (data) { console.log(['Friends fetch error', data]); }, this)); }, - - make_find_friends_and_services: function() { + + make_find_friends_and_services: function () { $('.NB-modal-loading', this.$modal).removeClass('NB-active'); var $services = $('.NB-intro-services', this.$modal).empty(); var service_syncing = false; - - _.each(['twitter', 'facebook'], _.bind(function(service) { + + _.each(['twitter', 'facebook'], _.bind(function (service) { var $service; - if (this.services && this.services[service][service+'_uid'] && !this.services[service].syncing) { - $service = $.make('div', { className: 'NB-intro-module-container NB-friends-service NB-connected NB-friends-service-'+service }, [ + if (this.services && this.services[service][service + '_uid'] && !this.services[service].syncing) { + $service = $.make('div', { className: 'NB-intro-module-container NB-friends-service NB-connected NB-friends-service-' + service }, [ $.make('h3', { className: 'NB-module-content-header' }, _.string.capitalize(service)), - $.make('div', { className: 'NB-intro-module NB-intro-module-'+service }, [ + $.make('div', { className: 'NB-intro-module NB-intro-module-' + service }, [ $.make('h3', [ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/' + service + '_big.png', width: 44, height: 44 }) ]), @@ -286,10 +286,10 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } else { var syncing = this.services && this.services[service] && this.services[service].syncing; if (syncing) service_syncing = true; - - $service = $.make('div', { className: 'NB-intro-module-container NB-friends-service NB-friends-service-'+service + (syncing ? ' NB-friends-service-syncing' : '') }, [ + + $service = $.make('div', { className: 'NB-intro-module-container NB-friends-service NB-friends-service-' + service + (syncing ? ' NB-friends-service-syncing' : '') }, [ $.make('h3', { className: 'NB-module-content-header' }, _.string.capitalize(service)), - $.make('div', { className: 'NB-intro-module NB-intro-module-'+service }, [ + $.make('div', { className: 'NB-intro-module NB-intro-module-' + service }, [ $.make('h3', [ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/' + service + '_big.png', width: 44, height: 44 }) ]), @@ -301,15 +301,15 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } $services.append($service); }, this)); - - var $autofollow = $.make('div', { className: 'NB-friends-autofollow'}, [ + + var $autofollow = $.make('div', { className: 'NB-friends-autofollow' }, [ $.make('input', { type: 'checkbox', className: 'NB-friends-autofollow-checkbox', id: 'NB-friends-autofollow-checkbox', checked: this.autofollow ? 'checked' : null }), $.make('label', { className: 'NB-friends-autofollow-label', 'for': 'NB-friends-autofollow-checkbox' }, 'and auto-follow them') ]); $services.prepend($autofollow); - + if (!this.services.twitter.twitter_uid || !this.services.facebook.facebook_uid) { - var $note = $.make('div', { className: 'NB-note'}, [ + var $note = $.make('div', { className: 'NB-note' }, [ 'Feel comfortable connecting to these services.', $.make('br'), 'Nothing happens without your permission.' @@ -318,10 +318,10 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } if (this.services.twitter.twitter_uid || this.services.facebook.facebook_uid) { var $stats = $.make('div', { className: 'NB-services-stats' }); - _.each(['following', 'follower'], _.bind(function(follow) { + _.each(['following', 'follower'], _.bind(function (follow) { var $stat = $.make('div', { className: 'NB-intro-services-stats-count' }, [ - $.make('div', { className: 'NB-intro-services-stats-count-number' }, this.profile.get(follow+'_count')), - $.make('div', { className: 'NB-intro-services-stats-count-description' }, Inflector.pluralize(follow, this.profile.get(follow+'_count'))) + $.make('div', { className: 'NB-intro-services-stats-count-number' }, this.profile.get(follow + '_count')), + $.make('div', { className: 'NB-intro-services-stats-count-description' }, Inflector.pluralize(follow, this.profile.get(follow + '_count'))) ]); $stats.append($stat); }, this)); @@ -332,22 +332,22 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { if (service_syncing) { clearTimeout(this.sync_interval); this.sync_checks += 1; - this.sync_interval = _.delay(_.bind(function() { + this.sync_interval = _.delay(_.bind(function () { this.fetch_friends(); }, this), this.sync_checks * 1000); } }, - - connect: function(service) { + + connect: function (service) { var options = "location=0,status=0,width=800,height=500"; var url = "/oauth/" + service + "_connect"; this.sync_checks = 0; this.connect_window = window.open(url, '_blank', options); - this.connect_window_timer = setInterval(_.bind(function() { + this.connect_window_timer = setInterval(_.bind(function () { console.log(["post connect window?", this.connect_window, this.connect_window.closed, this.connect_window.location]); try { - if (!this.connect_window || - !this.connect_window.location || + if (!this.connect_window || + !this.connect_window.location || this.connect_window.closed) { this.post_connect({}); } @@ -356,24 +356,24 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } }, this), 1000); // _gaq.push(['_trackEvent', 'reader_intro', 'Connect to ' + service.name + ' attempt']); - + NEWSBLUR.assets.preference('has_found_friends', true); NEWSBLUR.reader.check_hide_getting_started(); }, - - disconnect: function(service) { - var $service = $('.NB-friends-service-'+service, this.$modal); + + disconnect: function (service) { + var $service = $('.NB-friends-service-' + service, this.$modal); $('.NB-friends-service-connect', $service).text('Disconnecting...'); // _gaq.push(['_trackEvent', 'reader_intro', 'Disconnect from ' + service.name]); - NEWSBLUR.assets.disconnect_social_service(service, _.bind(function(data) { + NEWSBLUR.assets.disconnect_social_service(service, _.bind(function (data) { this.services = data.services; this.make_find_friends_and_services(); this.make_profile_section(); this.make_profile_tab(); }, this)); }, - - post_connect: function(data) { + + post_connect: function (data) { console.log(["Intro post_connect", data]); clearInterval(this.connect_window_timer); $('.NB-error', this.$modal).remove(); @@ -382,9 +382,9 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { var $error = $.make('div', { className: 'NB-error' }, [ $.make('span', { className: 'NB-raquo' }, '» '), data.error - ]).css({'opacity': 0}); + ]).css({ 'opacity': 0 }); $('.NB-intro-services', this.$modal).append($error); - $error.animate({'opacity': 1}, {'duration': 1000}); + $error.animate({ 'opacity': 1 }, { 'duration': 1000 }); this.resize(); // _gaq.push(['_trackEvent', 'reader_intro', 'Connect to service error']); } else { @@ -392,73 +392,73 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { // _gaq.push(['_trackEvent', 'reader_intro', 'Connect to service success']); } }, - + // ========== // = Paging = // ========== - - next_page: function() { - return this.page(this.page_number+1, this.page_number); + + next_page: function () { + return this.page(this.page_number + 1, this.page_number); }, - - previous_page: function() { - return this.page(this.page_number-1, this.page_number); + + previous_page: function () { + return this.page(this.page_number - 1, this.page_number); }, - - page: function(page_number, from_page) { - if (page_number == null) { - return this.page_number; - } - var page_count = $('.NB-page', this.$modal).length; - this.page_number = page_number; - - if (page_number == page_count) { - $('.NB-tutorial-next-page-text', this.$modal).text('All Done '); - } else if (page_number > page_count) { - NEWSBLUR.reader.check_hide_getting_started(); - NEWSBLUR.assets.preference('has_setup_feeds', true); - this.close(_.bind(function() { - NEWSBLUR.reader.open_dialog_after_feeds_loaded({ - delayed_import: this.flags.delayed_import, - finished_intro: true - }); - }, this)); - return; - } else if (page_number == 1) { - $('.NB-tutorial-next-page-text', this.$modal).text("Let's Get Started "); - } else { - $('.NB-tutorial-next-page-text', this.$modal).text('Skip this step '); - } - $('.NB-page', this.$modal).css({'display': 'none'}); - $('.NB-page-'+this.page_number, this.$modal).css({'display': 'block'}); - $('.NB-modal-page-text', this.$modal).html($.make('div', [ - 'Step ', - $.make('b', this.page_number), - ' of ', - $.make('b', page_count) - ])); - if (page_number > 1) { - $('.NB-intro-spinning-logo', this.$modal).css({'top': 12, 'left': 12, 'width': 48, 'height': 48}); - // $('.NB-modal-title', this.$modal).css({'paddingLeft': 42}); - } - - if (page_number == 2) { - this.advance_import_carousel(); - } - if (page_number == 3) { - this.submit_categories(); - this.make_find_friends_and_services(); - } - if (page_number == 4) { - this.show_twitter_follow_buttons(); - } - - clearTimeout(this.sync_interval); - NEWSBLUR.assets.preference('intro_page', page_number); - // _gaq.push(['_trackEvent', 'reader_intro', 'Page ' + this.page_number]); + + page: function (page_number, from_page) { + if (page_number == null) { + return this.page_number; + } + var page_count = $('.NB-page', this.$modal).length; + this.page_number = page_number; + + if (page_number == page_count) { + $('.NB-tutorial-next-page-text', this.$modal).text('All Done '); + } else if (page_number > page_count) { + NEWSBLUR.reader.check_hide_getting_started(); + NEWSBLUR.assets.preference('has_setup_feeds', true); + this.close(_.bind(function () { + NEWSBLUR.reader.open_dialog_after_feeds_loaded({ + delayed_import: this.flags.delayed_import, + finished_intro: true + }); + }, this)); + return; + } else if (page_number == 1) { + $('.NB-tutorial-next-page-text', this.$modal).text("Let's Get Started "); + } else { + $('.NB-tutorial-next-page-text', this.$modal).text('Skip this step '); + } + $('.NB-page', this.$modal).css({ 'display': 'none' }); + $('.NB-page-' + this.page_number, this.$modal).css({ 'display': 'block' }); + $('.NB-modal-page-text', this.$modal).html($.make('div', [ + 'Step ', + $.make('b', this.page_number), + ' of ', + $.make('b', page_count) + ])); + if (page_number > 1) { + $('.NB-intro-spinning-logo', this.$modal).css({ 'top': 12, 'left': 12, 'width': 48, 'height': 48 }); + // $('.NB-modal-title', this.$modal).css({'paddingLeft': 42}); + } + + if (page_number == 2) { + this.advance_import_carousel(); + } + if (page_number == 3) { + this.submit_categories(); + this.make_find_friends_and_services(); + } + if (page_number == 4) { + this.show_twitter_follow_buttons(); + } + + clearTimeout(this.sync_interval); + NEWSBLUR.assets.preference('intro_page', page_number); + // _gaq.push(['_trackEvent', 'reader_intro', 'Page ' + this.page_number]); }, - - advance_import_carousel: function(page, options) { + + advance_import_carousel: function (page, options) { options = options || {}; var $carousel = $('.NB-carousel-inner', this.$modal); @@ -467,20 +467,20 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { NEWSBLUR.reader.check_hide_getting_started(); $('.NB-tutorial-next-page-text', this.$modal).text('Next step '); } - - $carousel.animate({'left': (-1 * page * 100) + '%'}, { + + $carousel.animate({ 'left': (-1 * page * 100) + '%' }, { 'queue': false, 'easing': 'easeInOutQuint', 'duration': 1000 }); this.count_feeds(options); }, - - count_feeds: function(options) { + + count_feeds: function (options) { options = options || {}; var feed_count = options.fake_count || NEWSBLUR.assets.feeds.size(); var starred_count = options.starred_count || NEWSBLUR.assets.starred_count; - + if (feed_count) { $(".NB-page-2-started h4", this.$modal).text([ 'You are subscribed to ', @@ -499,65 +499,65 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { ].join("")).show(); } }, - - fade_out_logo: function() { + + fade_out_logo: function () { var self = this; var $logo = $('.NB-intro-spinning-logo', this.$modal); var $page1 = $('.NB-page-1', this.$modal); var $page2 = $('.NB-page-2', this.$modal); var $submit = $('.NB-modal-submit', this.$modal); var $title = $('.NB-modal-title', this.$modal); - - $submit.animate({'opacity': 0}, {'duration': 800, 'easing': 'easeInOutQuad'}); - $page1.animate({'opacity': 0}, { - 'duration': 800, - 'easing': 'easeInOutQuint', - 'complete': function() { - $logo.animate({'top': 12, 'left': 12, 'width': 48, 'height': 48}, { + + $submit.animate({ 'opacity': 0 }, { 'duration': 800, 'easing': 'easeInOutQuad' }); + $page1.animate({ 'opacity': 0 }, { + 'duration': 800, + 'easing': 'easeInOutQuint', + 'complete': function () { + $logo.animate({ 'top': 12, 'left': 12, 'width': 48, 'height': 48 }, { 'duration': 1160, 'easing': 'easeInOutCubic', - 'complete': function() { - $page2.css({'opacity': 0}); + 'complete': function () { + $page2.css({ 'opacity': 0 }); self.page(2); - $page2.animate({'opacity': 1}, {'duration': 1000, 'easing': 'easeInOutQuad'}); - $submit.animate({'opacity': 1}, {'duration': 1000, 'easing': 'easeInOutQuad'}); + $page2.animate({ 'opacity': 1 }, { 'duration': 1000, 'easing': 'easeInOutQuad' }); + $submit.animate({ 'opacity': 1 }, { 'duration': 1000, 'easing': 'easeInOutQuad' }); } }); // $title.animate({'paddingLeft': 42}, {'duration': 1100, 'easing': 'easeInOutQuart'}); } }); }, - - close_and_load_newsblur_blog: function() { - this.close(); - NEWSBLUR.reader.load_feed_in_tryfeed_view(this.newsblur_feed.id, {'feed': this.newsblur_feed}); + + close_and_load_newsblur_blog: function () { + this.close(); + NEWSBLUR.reader.load_feed_in_tryfeed_view(this.newsblur_feed.id, { 'feed': this.newsblur_feed }); }, - + // ========== // = Import = // ========== - - handle_opml_upload: function() { + + handle_opml_upload: function () { var self = this; var $loading = $('.NB-intro-imports-progress .NB-loading', this.$modal); var $file = $('.NB-intro-upload-opml-button', this.$modal); $loading.addClass('NB-active'); this.advance_import_carousel(1); - + // NEWSBLUR.log(['Uploading']); var params = { url: NEWSBLUR.URLs['opml-upload'], type: 'POST', dataType: 'json', success: function (data, status) { - NEWSBLUR.assets.load_feeds(function() { + NEWSBLUR.assets.load_feeds(function () { console.log(["opml upload", data, status]); $loading.removeClass('NB-active'); self.advance_import_carousel(2); if (data.payload.delayed) { NEWSBLUR.reader.flags.delayed_import = true; - self.count_feeds({fake_count: data.payload.feed_count}); + self.count_feeds({ fake_count: data.payload.feed_count }); $('.NB-intro-import-delayed', self.$modal).show(); $('.NB-intro-import-restart', self.$modal).hide(); $('.NB-intro-import-message', self.$modal).hide(); @@ -586,40 +586,40 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { if (window.FormData) { var formData = new FormData($file.closest('form')[0]); params['data'] = formData; - + $.ajax(params); } else { // IE9 has no FormData params['secureuri'] = false; params['fileElementId'] = 'NB-intro-upload-opml-button'; params['dataType'] = 'json'; - + $.ajaxFileUpload(params); } - + $file.replaceWith($file.clone()); - + return false; }, - + // =================== // = Stay Up To Date = // =================== - - show_twitter_follow_buttons: function() { + + show_twitter_follow_buttons: function () { $('.NB-intro-uptodate-follow', this.$modal).toggleClass('NB-intro-uptodate-twitter-inactive', !this.services.twitter.twitter_uid); }, - - follow_twitter_account: function(username) { - var $input = $('#NB-intro-uptodate-follow-'+username, this.$modal); + + follow_twitter_account: function (username) { + var $input = $('#NB-intro-uptodate-follow-' + username, this.$modal); var $button = $input.closest('.NB-intro-uptodate-follow'); - + if ($input.is(':checked')) { $button.addClass('NB-active'); if (this.services.twitter.twitter_uid) { NEWSBLUR.assets.follow_twitter_account(username); } else { - window.open('http://twitter.com/'+username, '_blank'); + window.open('http://twitter.com/' + username, '_blank'); } } else { $button.removeClass('NB-active'); @@ -628,66 +628,66 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { } } }, - - subscribe_to_feed: function(feed) { - var $button = $('.NB-intro-uptodate-follow-'+feed); + + subscribe_to_feed: function (feed) { + var $button = $('.NB-intro-uptodate-follow-' + feed); var $parent = $button.closest(".NB-intro-uptodate-follow"); var blog_url = 'http://blog.newsblur.com/rss'; var popular_username = 'social:popular'; console.log(["subscribe_to_feed", feed, $button, $parent]); $parent.addClass('NB-active'); if (feed == 'blog') { - NEWSBLUR.assets.save_add_url(blog_url, "", function() { + NEWSBLUR.assets.save_add_url(blog_url, "", function () { NEWSBLUR.assets.load_feeds(); - }, {auto_active: false, skip_fetch: true}); - + }, { auto_active: false, skip_fetch: true }); + } else if (feed == 'popular') { - NEWSBLUR.assets.follow_user(popular_username, function() { + NEWSBLUR.assets.follow_user(popular_username, function () { NEWSBLUR.app.feed_list.make_social_feeds(); }); } }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-page-next' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-page-next' }, function ($t, $p) { e.preventDefault(); - + if (self.page_number == 1) { self.fade_out_logo(); } else { self.next_page(); } }); - $.targetIs(e, { tagSelector: '.NB-tutorial-finish-newsblur-blog' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-tutorial-finish-newsblur-blog' }, function ($t, $p) { e.preventDefault(); - + self.close_and_load_newsblur_blog(); }); - - $.targetIs(e, { tagSelector: '.NB-starredimport-button' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-starredimport-button' }, function ($t, $p) { e.preventDefault(); // self.google_reader_connect({'starred_only': true}); }); - $.targetIs(e, { tagSelector: '.NB-intro-import-restart' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-intro-import-restart' }, function ($t, $p) { e.preventDefault(); self.advance_import_carousel(0); }); - $.targetIs(e, { tagSelector: '.NB-intro-upload-opml' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-intro-upload-opml' }, function ($t, $p) { // e.preventDefault(); // return false; }); - $.targetIs(e, { tagSelector: '.NB-goodies-bookmarklet-button' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-goodies-bookmarklet-button' }, function ($t, $p) { e.preventDefault(); - + alert('Drag this button to your bookmark toolbar.'); }); - $.targetIs(e, { tagSelector: '.NB-friends-service-connect' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-friends-service-connect' }, function ($t, $p) { e.preventDefault(); var service; var $service = $t.closest('.NB-friends-service'); @@ -702,36 +702,36 @@ _.extend(NEWSBLUR.ReaderIntro.prototype, { self.connect(service); } }); - - $.targetIs(e, { tagSelector: '.NB-intro-uptodate-follow-blog' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-intro-uptodate-follow-blog' }, function ($t, $p) { self.subscribe_to_feed('blog'); }); - $.targetIs(e, { tagSelector: '.NB-intro-uptodate-follow-popular' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-intro-uptodate-follow-popular' }, function ($t, $p) { self.subscribe_to_feed('popular'); }); - $.targetIs(e, { tagSelector: '.NB-category' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-category' }, function ($t, $p) { var category = $t.data('category'); self.toggle_category(category, $t); }); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-intro-upload-opml-button' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-intro-upload-opml-button' }, function ($t, $p) { e.preventDefault(); - + self.handle_opml_upload(); }); - $.targetIs(e, { tagSelector: '.NB-friends-autofollow-checkbox' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-friends-autofollow-checkbox' }, function ($t, $p) { NEWSBLUR.assets.preference('autofollow_friends', $t.is(':checked')); }); - $.targetIs(e, { tagSelector: '#NB-intro-uptodate-follow-newsblur' }, function($t, $p) { + $.targetIs(e, { tagSelector: '#NB-intro-uptodate-follow-newsblur' }, function ($t, $p) { self.follow_twitter_account('newsblur'); }); - $.targetIs(e, { tagSelector: '#NB-intro-uptodate-follow-samuelclay' }, function($t, $p) { + $.targetIs(e, { tagSelector: '#NB-intro-uptodate-follow-samuelclay' }, function ($t, $p) { self.follow_twitter_account('samuelclay'); }); } - + }); diff --git a/media/js/newsblur/reader/reader_keyboard.js b/media/js/newsblur/reader/reader_keyboard.js index 5fbd942a66..fda656544b 100644 --- a/media/js/newsblur/reader/reader_keyboard.js +++ b/media/js/newsblur/reader/reader_keyboard.js @@ -1,417 +1,417 @@ -NEWSBLUR.ReaderKeyboard = function(options) { - var defaults = { - width: 700 - }; - - this.options = $.extend({}, defaults, options); - this.runner(); +NEWSBLUR.ReaderKeyboard = function (options) { + var defaults = { + width: 700 + }; + + this.options = $.extend({}, defaults, options); + this.runner(); }; NEWSBLUR.ReaderKeyboard.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderKeyboard.prototype.constructor = NEWSBLUR.ReaderKeyboard; _.extend(NEWSBLUR.ReaderKeyboard.prototype, { - - runner: function() { - this.make_modal(); - this.handle_cancel(); - this.open_modal(); - - this.$modal.bind('click', $.rescope(this.handle_click, this)); - }, - - make_modal: function() { - var self = this; - - this.$modal = $.make('div', { className: 'NB-modal-keyboard NB-modal' }, [ - $.make('div', { className: 'NB-modal-tabs' }, [ - $.make('div', { className: 'NB-modal-tab NB-active NB-modal-tab-general' }, 'General'), - $.make('div', { className: 'NB-modal-tab NB-modal-tab-feeds' }, 'Feeds'), - $.make('div', { className: 'NB-modal-tab NB-modal-tab-stories' }, 'Stories') + + runner: function () { + this.make_modal(); + this.handle_cancel(); + this.open_modal(); + + this.$modal.bind('click', $.rescope(this.handle_click, this)); + }, + + make_modal: function () { + var self = this; + + this.$modal = $.make('div', { className: 'NB-modal-keyboard NB-modal' }, [ + $.make('div', { className: 'NB-modal-tabs' }, [ + $.make('div', { className: 'NB-modal-tab NB-active NB-modal-tab-general' }, 'General'), + $.make('div', { className: 'NB-modal-tab NB-modal-tab-feeds' }, 'Feeds'), + $.make('div', { className: 'NB-modal-tab NB-modal-tab-stories' }, 'Stories') + ]), + $.make('h2', { className: 'NB-modal-title' }, [ + $.make('div', { className: 'NB-icon' }), + 'Keyboard shortcuts', + $.make('div', { className: 'NB-icon-dropdown' }) + ]), + + // General + + $.make('div', { className: 'NB-tab NB-tab-general NB-active' }, [ + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Switch views'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '←' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '→' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Quick search for a site'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'g' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Dashboard'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'esc' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'd' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open Everything'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'e' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Hide sites'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'u' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Full screen'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'f' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Switch focus/unread'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '+' ]), - $.make('h2', { className: 'NB-modal-title' }, [ - $.make('div', { className: 'NB-icon' }), - 'Keyboard shortcuts', - $.make('div', { className: 'NB-icon-dropdown' }) + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '-' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'View keyboard shortcuts'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '?' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Add site/folder'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'a' + ]) + ]) + ]) + ]), + + // Feeds + + $.make('div', { className: 'NB-tab NB-tab-feeds' }, [ + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next site'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + '↓' ]), - - // General - - $.make('div', { className: 'NB-tab NB-tab-general NB-active' }, [ - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Switch views'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '←' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '→' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Quick search for a site'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'g' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Dashboard'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'esc' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'd' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open Everything'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'e' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Hide sites'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'u' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Full screen'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'f' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Switch focus/unread'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '+' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '-' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'View keyboard shortcuts'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '?' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Add site/folder'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'a' - ]) - ]) - ]) + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'j' + ]) + // TODO: Mention "shift + n" here? It will be too wide. + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Prev. site'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + '↑' ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'k' + ]) + // TODO: Mention "shift + p" here? It will be too wide. + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open site/feed trainer'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 't' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open story trainer'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 't' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Mark all as read'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'a' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Oldest unread story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'm' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Reload feed/folder'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'r' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Search feed'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '/' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Toggle unread/all'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'L' + ]) + ]) + ]) + ]), - // Feeds + // Stories - $.make('div', { className: 'NB-tab NB-tab-feeds' }, [ - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next site'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - '↓' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'j' - ]) - // TODO: Mention "shift + n" here? It will be too wide. - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Prev. site'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - '↑' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'k' - ]) - // TODO: Mention "shift + p" here? It will be too wide. - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open site/feed trainer'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 't' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open story trainer'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 't' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Mark all as read'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'a' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Oldest unread story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'm' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Reload feed/folder'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'r' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Search feed'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '/' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Toggle unread/all'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'L' - ]) - ]) - ]) + $.make('div', { className: 'NB-tab NB-tab-stories' }, [ + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '↓' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'j' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Previous story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '↑' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'k' + ]) + ]) + ]), + + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in Story view'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'enter' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in Text view'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'enter' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page down'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'space' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page up'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'space' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next Unread Story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'n' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Toggle read/unread'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'u' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'm' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Mark below stories read'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'b' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Mark above stories read'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'y' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Save/Unsave story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 's' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Email story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'e' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in background tab'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'o' ]), - - // Stories - - $.make('div', { className: 'NB-tab NB-tab-stories' }, [ - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '↓' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'j' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Previous story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '↑' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'k' - ]) - ]) - ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'v' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in new window'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'v' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Expand story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'x' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Collapse story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'x' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Share this story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 's' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Save comments'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'ctrl', + $.make('span', '+'), + 'enter' + ]) + ]) + ]), + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Scroll to comments'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'c' + ]) + ]) + ]) + ]) + ]); + }, + + handle_cancel: function () { + var $cancel = $('.NB-modal-cancel', this.$modal); + + $cancel.click(function (e) { + e.preventDefault(); + $.modal.close(); + }); + }, + + // =========== + // = Actions = + // =========== + + handle_click: function (elem, e) { + var self = this; - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in Story view'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'enter' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in Text view'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'enter' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page down'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'space' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page up'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'space' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next Unread Story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'n' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Toggle read/unread'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'u' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'm' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Mark below stories read'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'b' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Mark above stories read'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'y' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Save/Unsave story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 's' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Email story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'e' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in background tab'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'o' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'v' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open in new window'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'v' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Expand story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span','+'), - 'x' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Collapse story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'x' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Share this story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 's' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Save comments'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'ctrl', - $.make('span', '+'), - 'enter' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Scroll to comments'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'c' - ]) - ]) - ]) - ]) - ]); - }, - - handle_cancel: function() { - var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { - e.preventDefault(); - $.modal.close(); - }); - }, - - // =========== - // = Actions = - // =========== + $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function ($t, $p) { + e.preventDefault(); + var newtab; + if ($t.hasClass('NB-modal-tab-general')) { + newtab = 'general'; + } else if ($t.hasClass('NB-modal-tab-feeds')) { + newtab = 'feeds'; + } else if ($t.hasClass('NB-modal-tab-stories')) { + newtab = 'stories'; + } + self.switch_tab(newtab); + }); + } - handle_click: function(elem, e) { - var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function($t, $p) { - e.preventDefault(); - var newtab; - if ($t.hasClass('NB-modal-tab-general')) { - newtab = 'general'; - } else if ($t.hasClass('NB-modal-tab-feeds')) { - newtab = 'feeds'; - } else if ($t.hasClass('NB-modal-tab-stories')) { - newtab = 'stories'; - } - self.switch_tab(newtab); - }); - } - }); diff --git a/media/js/newsblur/reader/reader_mark_read.js b/media/js/newsblur/reader/reader_mark_read.js index 4c0feb293a..5c1c001b8b 100644 --- a/media/js/newsblur/reader/reader_mark_read.js +++ b/media/js/newsblur/reader/reader_mark_read.js @@ -1,9 +1,9 @@ -NEWSBLUR.ReaderMarkRead = function(options) { +NEWSBLUR.ReaderMarkRead = function (options) { var defaults = { days: 1, modal_container_class: "NB-full-container" }; - + this.flags = {}; this.values = { 0: 0, @@ -37,23 +37,23 @@ NEWSBLUR.ReaderMarkRead.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderMarkRead.prototype.constructor = NEWSBLUR.ReaderMarkRead; _.extend(NEWSBLUR.ReaderMarkRead.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); this.load_slider(); this.generate_explanation(this.options['days']); this.handle_cancel(); this.open_modal(); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); $(document).bind('keydown.mark_read', 'return', _.bind(this.save_mark_read, this)); $(document).bind('keydown.mark_read', 'ctrl+return', _.bind(this.save_mark_read, this)); $(document).bind('keydown.mark_read', 'meta+return', _.bind(this.save_mark_read, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-markread NB-modal' }, [ $.make('h2', { className: 'NB-modal-title' }, [ $.make('div', { className: 'NB-icon' }), @@ -61,72 +61,72 @@ _.extend(NEWSBLUR.ReaderMarkRead.prototype, { $.make('div', { className: 'NB-icon-dropdown' }) ]), $.make('form', { className: 'NB-markread-form' }, [ - $.make('div', { className: 'NB-markread-slider'}), - $.make('div', { className: 'NB-markread-explanation'}), + $.make('div', { className: 'NB-markread-slider' }), + $.make('div', { className: 'NB-markread-explanation' }), $.make('div', { className: 'NB-modal-submit' }, [ $.make('input', { type: 'submit', className: 'NB-modal-submit-button NB-modal-submit-green', value: 'Do it' }) ]) - ]).bind('submit', function(e) { + ]).bind('submit', function (e) { e.preventDefault(); self.save_mark_read(); return false; }) ]); }, - - load_slider: function() { + + load_slider: function () { var self = this; var $slider = $('.NB-markread-slider', this.$modal); - + $slider.slider({ range: 'min', min: 0, - max: Object.keys(this.values).length-1, + max: Object.keys(this.values).length - 1, step: 1, value: _.indexOf(_.values(this.values), this.options['days']), - slide: function(e, ui) { + slide: function (e, ui) { var value = self.values[ui.value]; self.update_dayofweek(value); self.generate_explanation(value); }, - stop: function(e, ui) { - + stop: function (e, ui) { + } }); }, - - update_dayofweek: function(value) { - + + update_dayofweek: function (value) { + }, - - generate_explanation: function(value) { + + generate_explanation: function (value) { var $button = $('.NB-modal-submit-button', this.$modal); var explanation; - + if (value == 0) { explanation = "Mark every story as read"; } else if (value >= 1) { - explanation = "Mark all stories older than " + value + " day" + (value==1?'':'s') + " old as read"; + explanation = "Mark all stories older than " + value + " day" + (value == 1 ? '' : 's') + " old as read"; } - + $button.val(explanation); }, - - save_mark_read: function() { + + save_mark_read: function () { if (this.flags.saving) return; - + var $save = $('.NB-modal input[type=submit]'); var $slider = $('.NB-markread-slider', this.$modal); var days = this.values[$slider.slider('option', 'value')]; - + this.flags.saving = true; $save.attr('value', 'Marking as read...').addClass('NB-disabled').attr('disabled', true); if (NEWSBLUR.Globals.is_authenticated) { - this.model.save_mark_read(days, _.bind(function() { + this.model.save_mark_read(days, _.bind(function () { NEWSBLUR.reader.start_count_unreads_after_import(); $.modal.close(); - NEWSBLUR.reader.force_feeds_refresh(function() { + NEWSBLUR.reader.force_feeds_refresh(function () { NEWSBLUR.reader.finish_count_unreads_after_import(); }, true); this.flags.saving = false; @@ -136,26 +136,26 @@ _.extend(NEWSBLUR.ReaderMarkRead.prototype, { $.modal.close(); } }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-add-url-submit' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-add-url-submit' }, function ($t, $p) { e.preventDefault(); }); }, - - handle_cancel: function() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); } - + }); diff --git a/media/js/newsblur/reader/reader_newsletters.js b/media/js/newsblur/reader/reader_newsletters.js index 416b229e52..5077c1bbdc 100644 --- a/media/js/newsblur/reader/reader_newsletters.js +++ b/media/js/newsblur/reader/reader_newsletters.js @@ -1,8 +1,8 @@ -NEWSBLUR.ReaderNewsletters = function(options) { +NEWSBLUR.ReaderNewsletters = function (options) { var defaults = { 'width': 800 }; - + this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.assets; this.runner(); @@ -12,34 +12,34 @@ NEWSBLUR.ReaderNewsletters.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderNewsletters.prototype.constructor = NEWSBLUR.ReaderNewsletters; _.extend(NEWSBLUR.ReaderNewsletters.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); - this.open_modal(_.bind(function() { + this.open_modal(_.bind(function () { $('.NB-newsletters-email').click(); }, this)); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; var email = NEWSBLUR.Globals.username + "-" + NEWSBLUR.Globals.secret_token + "@newsletters.newsblur.com"; - + this.$modal = $.make('div', { className: 'NB-modal-newsletters NB-modal' }, [ $.make('h2', { className: 'NB-modal-title' }, [ $.make('div', { className: 'NB-icon' }), 'Email Newsletters', $.make('div', { className: 'NB-icon-dropdown' }) ]), - + $.make('fieldset', [ $.make('legend', 'Forwarding email address') ]), $.make('div', { className: 'NB-newsletters-group' }, [ $.make('input', { type: 'text', value: email, className: 'NB-newsletters-email' }) ]), - + $.make('fieldset', [ $.make('legend', 'Setup instructions') ]), @@ -58,18 +58,18 @@ _.extend(NEWSBLUR.ReaderNewsletters.prototype, { ]) ]); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - $.targetIs(e, { tagSelector: '.NB-newsletters-email' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-newsletters-email' }, function ($t, $p) { e.preventDefault(); $t.select(); }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/reader/reader_notifications.js b/media/js/newsblur/reader/reader_notifications.js index 71c88cc0de..9f6166376d 100644 --- a/media/js/newsblur/reader/reader_notifications.js +++ b/media/js/newsblur/reader/reader_notifications.js @@ -1,15 +1,15 @@ -NEWSBLUR.ReaderNotifications = function(feed_id, options) { +NEWSBLUR.ReaderNotifications = function (feed_id, options) { var defaults = { - 'onOpen': function() { + 'onOpen': function () { $(window).trigger('resize.simplemodal'); } }; - + this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; + this.model = NEWSBLUR.assets; this.feed_id = feed_id; - this.feed = this.model.get_feed(feed_id); - + this.feed = this.model.get_feed(feed_id); + this.runner(); }; @@ -17,8 +17,8 @@ NEWSBLUR.ReaderNotifications.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderNotifications.prototype.constructor = NEWSBLUR.ReaderNotifications; _.extend(NEWSBLUR.ReaderNotifications.prototype, { - - runner: function() { + + runner: function () { console.log(['Reader notifications', this.feed, this.feed_id]); this.make_modal(); this.handle_cancel(); @@ -27,56 +27,56 @@ _.extend(NEWSBLUR.ReaderNotifications.prototype, { this.initialize_feed(this.feed_id); } - $('input[name=notification_title_only]', this.$modal).each(function() { + $('input[name=notification_title_only]', this.$modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.notification_title_only) { $(this).attr('checked', true); return false; } }); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); this.$modal.bind('change', $.rescope(this.handle_change, this)); }, - initialize_feed: function(feed_id) { + initialize_feed: function (feed_id) { var frequency = this.feed.get('notification_frequency'); var notifications = this.feed.get('notifications'); NEWSBLUR.Modal.prototype.initialize_feed.call(this, feed_id); - + var $site = $(".NB-modal-section-site", this.$modal); $site.html(this.make_feed_notification(this.feed)); var $all = $(".NB-modal-section-all", this.$modal); $all.html(this.make_feed_notifications()); - + this.resize(); }, - - get_feed_settings: function() { + + 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; + this.model.get_feed_settings; settings_fn.call(this.model, this.feed_id, _.bind(this.populate_settings, this)); }, - - populate_settings: function(data) { + + populate_settings: function (data) { var $submit = $('.NB-modal-submit-save', this.$modal); var $loading = $('.NB-modal-loading', this.$modal); - + $loading.removeClass('NB-active'); this.resize(); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-notifications NB-modal' }, [ - (this.feed && $.make('div', { className: 'NB-modal-feed-chooser-container'}, [ + (this.feed && $.make('div', { className: 'NB-modal-feed-chooser-container' }, [ this.make_feed_chooser() ])), $.make('div', { className: 'NB-modal-loading' }), @@ -89,7 +89,7 @@ _.extend(NEWSBLUR.ReaderNotifications.prototype, { $.make('div', { className: 'NB-fieldset NB-modal-submit' }, [ $.make('fieldset', [ $.make('legend', 'Notification Preferences'), - $.make('div', { className: 'NB-modal-section NB-modal-section-preferences'}, [ + $.make('div', { className: 'NB-modal-section NB-modal-section-preferences' }, [ $.make('div', { className: 'NB-preference NB-preference-notification-title-only' }, [ $.make('div', { className: 'NB-preference-options' }, [ $.make('div', [ @@ -116,7 +116,7 @@ _.extend(NEWSBLUR.ReaderNotifications.prototype, { (this.feed && $.make('div', { className: 'NB-fieldset NB-modal-submit' }, [ $.make('fieldset', [ $.make('legend', 'Site Notifications'), - $.make('div', { className: 'NB-modal-section NB-modal-section-site'}, [ + $.make('div', { className: 'NB-modal-section NB-modal-section-site' }, [ this.make_feed_notification(this.feed) ]) ]) @@ -124,99 +124,99 @@ _.extend(NEWSBLUR.ReaderNotifications.prototype, { $.make('div', { className: 'NB-fieldset NB-modal-submit' }, [ $.make('fieldset', [ $.make('legend', 'All Notifications'), - $.make('div', { className: 'NB-modal-section NB-modal-section-all'}, [ + $.make('div', { className: 'NB-modal-section NB-modal-section-all' }, [ this.make_feed_notifications() ]) ]) ]) ]); }, - - handle_cancel: function() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); }, - - make_feed_notification: function(feed) { - var $feed = new NEWSBLUR.Views.FeedNotificationView({model: feed}); - + + make_feed_notification: function (feed) { + var $feed = new NEWSBLUR.Views.FeedNotificationView({ model: feed }); + return $feed.render().$el; }, - - make_feed_notifications: function() { + + make_feed_notifications: function () { var site_feed_id = this.feed && this.feed.id; - var notifications = this.model.get_feeds().select(function(feed) { + var notifications = this.model.get_feeds().select(function (feed) { return feed.get('notification_types') && feed.id != site_feed_id; }); var $feeds = []; - - notifications.sort(function(a, b) { return a.get('feed_title') < b.get('feed_title'); }); + + notifications.sort(function (a, b) { return a.get('feed_title') < b.get('feed_title'); }); for (var feed in notifications) { $feeds.push(this.make_feed_notification(notifications[feed])); } - + return $feeds; }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-submit-retry' }, function($t, $p) { + + $.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) { + $.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) { + $.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) { + $.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){ + $.targetIs(e, { tagSelector: '.NB-premium-only-link' }, function ($t, $p) { e.preventDefault(); - - self.close(function() { - NEWSBLUR.reader.open_feedchooser_modal({premium_only: true}); + + self.close(function () { + NEWSBLUR.reader.open_feedchooser_modal({ premium_only: true }); }); }); }, - - animate_saved: function() { + + 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}); + 'complete': function () { + _.delay(function () { + $status.animate({ 'opacity': 0 }, { 'queue': false, 'duration': 1000 }); }, 300); } }); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function ($t, $p) { var feed_id = $t.val(); self.first_load = false; self.initialize_feed(feed_id); @@ -228,5 +228,5 @@ _.extend(NEWSBLUR.ReaderNotifications.prototype, { NEWSBLUR.assets.preference('notification_title_only', notification_title_only); }); } - + }); diff --git a/media/js/newsblur/reader/reader_organizer.js b/media/js/newsblur/reader/reader_organizer.js index 18afc3ec7d..41e9cfc7e4 100644 --- a/media/js/newsblur/reader/reader_organizer.js +++ b/media/js/newsblur/reader/reader_organizer.js @@ -1,24 +1,24 @@ -NEWSBLUR.ReaderOrganizer = function(user_id, options) { +NEWSBLUR.ReaderOrganizer = function (user_id, options) { var defaults = { width: 800, sorting: 'alphabetical', - onOpen: _.bind(function() { + onOpen: _.bind(function () { this.resize_modal(); }, this), hierarchy: "nested", inverse_sorting: false }; - + this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; + this.model = NEWSBLUR.assets; this.init(); }; NEWSBLUR.ReaderOrganizer.prototype = new NEWSBLUR.Modal; _.extend(NEWSBLUR.ReaderOrganizer.prototype, { - - init: function() { + + init: function () { this.reset_feeds(); this.make_modal(); this.open_modal(); @@ -26,14 +26,14 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { this.$modal.bind('click', $.rescope(this.handle_click, this)); this.$modal.bind('change', $.rescope(this.handle_change, this)); }, - - reset_feeds: function() { - NEWSBLUR.assets.feeds.each(function(feed) { - feed.highlight_in_all_folders(false, true, {silent: true}); + + reset_feeds: function () { + NEWSBLUR.assets.feeds.each(function (feed) { + feed.highlight_in_all_folders(false, true, { silent: true }); }); }, - - make_modal: function() { + + make_modal: function () { var self = this; this.$modal = $.make('div', { className: 'NB-modal NB-modal-organizer' }, [ @@ -43,14 +43,14 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { 'Organize sites', $.make('div', { className: 'NB-icon-dropdown' }) ]), - $.make('div', { className: 'NB-organizer-sidebar'}, [ + $.make('div', { className: 'NB-organizer-sidebar' }, [ $.make('div', { className: 'NB-organizer-sidebar-hierarchy' }, [ $.make('div', { className: 'NB-organizer-sidebar-title' }, 'Show Folders'), $.make('div', { className: 'NB-organizer-sidebar-container' }, [ $.make('ul', { className: 'segmented-control' }, [ $.make('li', { className: 'NB-organizer-hierarchy NB-organizer-hierarchy-nested NB-active' }, 'Nested'), $.make('li', { className: 'NB-organizer-hierarchy NB-organizer-hierarchy-flat' }, 'Flat') - ]) + ]) ]) ]), $.make('div', { className: 'NB-organizer-sidebar-move' }, [ @@ -100,8 +100,8 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { this.make_feeds() ]); }, - - resize_modal: function(previous_height) { + + resize_modal: function (previous_height) { var resize_height = 0; var $feedlist = $('.NB-feedchooser', this.$modal); var content_height = $feedlist.height() + 90; @@ -110,19 +110,19 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { var chooser_height = $feedlist.height(); var diff = Math.max(4, content_height - container_height); resize_height = chooser_height - diff; - $feedlist.css({'max-height': resize_height}); - _.defer(_.bind(function() { this.resize_modal(content_height); }, this), 1); + $feedlist.css({ 'max-height': resize_height }); + _.defer(_.bind(function () { this.resize_modal(content_height); }, this), 1); } if (resize_height) { this.options.resize = resize_height; } }, - + // ============= // = Feed list = // ============= - - make_feeds: function(options) { + + make_feeds: function (options) { var feeds = this.model.feeds; if (this.options.hierarchy == "flat") { this.options.folders = new NEWSBLUR.Collections.Folders(NEWSBLUR.assets.feeds.pluck('id')); @@ -133,68 +133,68 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { NEWSBLUR.Collections.Folders.organizer_sortorder = this.options.sorting; NEWSBLUR.Collections.Folders.organizer_inversesort = this.options.inverse_sorting; this.options.folders.sort(); - + this.feedlist = new NEWSBLUR.Views.FeedList({ feed_chooser: true, organizer: true, hierarchy: this.options.hierarchy, sorting: this.options.sorting, inverse_sorting: this.options.inverse_sorting - }).make_feeds({folders: this.options.folders}); + }).make_feeds({ folders: this.options.folders }); var $feeds = this.feedlist.$el; if (this.options.resize) { - $feeds.css({'max-height': this.options.resize}); + $feeds.css({ 'max-height': this.options.resize }); } if ($feeds.data('sortable')) $feeds.data('sortable').disable(); - + // Expand collapsed folders $('.NB-folder-collapsed', $feeds).css({ 'display': 'block', 'opacity': 1 }).removeClass('NB-folder-collapsed'); - + // Pretend unfetched feeds are fine $('.NB-feed-unfetched', $feeds).removeClass('NB-feed-unfetched'); // Make sure all folders are visible $('.NB-folder.NB-hidden', $feeds).removeClass('NB-hidden'); - + $('.NB-organizer-sorts', this.$modal).toggleClass('NB-sort-inverse', this.options.inverse_sorting); - + NEWSBLUR.Collections.Folders.organizer_sortorder = null; this.options.folders.sort(); NEWSBLUR.assets.feeds.off('change:highlighted') - .on('change:highlighted', _.bind(this.change_selection, this)); - + .on('change:highlighted', _.bind(this.change_selection, this)); + return $feeds; }, - - make_folders: function() { + + make_folders: function () { var $folders = $.make('div', { className: 'NB-organizer-sidebar-folderjump' }, [ NEWSBLUR.utils.make_folders() ]); - + return $folders; }, - - replace_folders: function() { + + replace_folders: function () { $(".NB-folders", this.$modal).replaceWith(NEWSBLUR.utils.make_folders()); }, - + // ============= // = Selecting = // ============= - - change_select: function(select) { + + change_select: function (select) { if (select == "all") { - this.feedlist.folder_view.highlight_feeds({force_highlight: true}); + this.feedlist.folder_view.highlight_feeds({ force_highlight: true }); } else if (select == "none") { - this.feedlist.folder_view.highlight_feeds({force_deselect: true}); + this.feedlist.folder_view.highlight_feeds({ force_deselect: true }); } }, - - change_selection: function() { + + change_selection: function () { var $title = $(".NB-organizer-selects .NB-organizer-action-title", this.$modal); var $move = $(".NB-action-move", this.$modal); var $error = $(".NB-error-move", this.$modal); @@ -203,7 +203,7 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { // console.log(['change_selection', count]); $title.text(count ? count + " selected" : "Select"); $error.text(''); - + if (!count) { $delete.text('Delete').addClass('NB-disabled'); $move.text('Move').addClass('NB-disabled'); @@ -211,16 +211,16 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { $delete.text('Delete ' + Inflector.pluralize('site', count, true)).removeClass('NB-disabled'); $move.text('Move ' + Inflector.pluralize('site', count, true)).removeClass('NB-disabled'); } - + NEWSBLUR.assets.feeds.off('change:highlighted') - .on('change:highlighted', _.bind(this.change_selection, this)); + .on('change:highlighted', _.bind(this.change_selection, this)); }, - + // =========== // = Sorting = // =========== - - change_sort: function(sorting) { + + change_sort: function (sorting) { var inverse = this.options.inverse_sorting; if (this.options.sorting == sorting) { this.options.inverse_sorting = !inverse; @@ -230,39 +230,39 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { this.options.sorting = sorting; var $feedlist = $('.NB-feedchooser', this.$modal); var old_position = $feedlist.scrollTop(); - - $(".NB-action-"+sorting, this.$modal).addClass('NB-active').siblings().removeClass('NB-active'); - $(".NB-feedlist", this.$modal).replaceWith(this.make_feeds()); + $(".NB-action-" + sorting, this.$modal).addClass('NB-active').siblings().removeClass('NB-active'); + + $(".NB-feedlist", this.$modal).replaceWith(this.make_feeds()); var $feedlist = $('.NB-feedchooser', this.$modal); $feedlist.scrollTop(old_position); }, - + // ============= // = Hierarchy = // ============= - - toggle_hierarchy: function(hierarchy) { + + toggle_hierarchy: function (hierarchy) { this.options.hierarchy = hierarchy; $(".NB-organizer-hierarchy", this.$modal).removeClass('NB-active'); - $(".NB-organizer-hierarchy-"+hierarchy, this.$modal).addClass('NB-active'); - + $(".NB-organizer-hierarchy-" + hierarchy, this.$modal).addClass('NB-active'); + $(".NB-feedlist", this.$modal).replaceWith(this.make_feeds()); }, - + // ========== // = Server = // ========== - - serialize: function() { + + serialize: function () { var highlighted_feeds = this.feedlist.folder_view.highlighted_feeds({ collection: this.options.folders }); - + return highlighted_feeds; }, - - move_feeds: function() { + + move_feeds: function () { var self = this; var highlighted_feeds = this.serialize(); var $move = $('.NB-action-move', this.$modal); @@ -275,12 +275,12 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { $loading.addClass('NB-active'); $move.addClass('NB-disabled').text('Moving...'); NEWSBLUR.reader.flags['reloading_feeds'] = true; - + if (!this._open_folder) new_folder = null; - + console.log(["Moving feeds by folder", highlighted_feeds, to_folder, new_folder]); - NEWSBLUR.assets.move_feeds_by_folder(highlighted_feeds, to_folder, new_folder, function() { + NEWSBLUR.assets.move_feeds_by_folder(highlighted_feeds, to_folder, new_folder, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; $loading.removeClass('NB-active'); NEWSBLUR.assets.feeds.trigger('reset'); @@ -291,15 +291,15 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { if (self._open_folder) self.toggle_folder_add(); self.replace_folders(); $new_folder.val(''); - }, function(error) { + }, function (error) { NEWSBLUR.reader.flags['reloading_feeds'] = false; $loading.removeClass('NB-active'); self.change_selection(); $error.show().text("Sorry, there was a problem moving feeds."); }); }, - - delete_feeds: function() { + + delete_feeds: function () { var self = this; var highlighted_feeds = this.serialize(); var $loading = $('.NB-modal-loading', this.$modal); @@ -311,15 +311,15 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { NEWSBLUR.reader.flags['reloading_feeds'] = true; console.log(["Deleting feeds by folder", highlighted_feeds]); - + if (this.options.hierarchy == 'flat') { // Ignore folder when flat, which will delte feed out of all folders - highlighted_feeds = _.map(highlighted_feeds, function(feed_folder) { + highlighted_feeds = _.map(highlighted_feeds, function (feed_folder) { return [feed_folder[0], null]; }); } - - NEWSBLUR.assets.delete_feeds_by_folder(highlighted_feeds, function() { + + NEWSBLUR.assets.delete_feeds_by_folder(highlighted_feeds, function () { NEWSBLUR.reader.flags['reloading_feeds'] = false; $loading.removeClass('NB-active'); self.reset_feeds(); @@ -328,79 +328,79 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { NEWSBLUR.assets.feeds.trigger('reset'); $delete.text('Deleted!'); if (self._open_folder) self.toggle_folder_add(); - }, function(error) { + }, function (error) { NEWSBLUR.reader.flags['reloading_feeds'] = false; $loading.removeClass('NB-active'); self.change_selection(); $error.show().text("Sorry, there was a problem deleting feeds."); }); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; $.targetIs(e, { tagSelector: '.NB-organizer-action', childOf: '.NB-organizer-sorts' }, - _.bind(function($t, $p) { - e.preventDefault(); - - var sort = $t.attr('class').match(/\bNB-action-(\w+)\b/)[1]; - this.change_sort(sort); - }, this)); - + _.bind(function ($t, $p) { + e.preventDefault(); + + var sort = $t.attr('class').match(/\bNB-action-(\w+)\b/)[1]; + this.change_sort(sort); + }, this)); + $.targetIs(e, { tagSelector: '.NB-organizer-action', childOf: '.NB-organizer-selects' }, - _.bind(function($t, $p) { - e.preventDefault(); - - var select = $t.attr('class').match(/\bNB-action-select-(\w+)\b/)[1]; - this.change_select(select); - }, this)); + _.bind(function ($t, $p) { + e.preventDefault(); + + var select = $t.attr('class').match(/\bNB-action-select-(\w+)\b/)[1]; + this.change_select(select); + }, this)); $.targetIs(e, { tagSelector: '.NB-organizer-hierarchy' }, - _.bind(function($t, $p) { - e.preventDefault(); + _.bind(function ($t, $p) { + e.preventDefault(); - var hierarchy = $t.hasClass('NB-organizer-hierarchy-nested') ? 'nested' : 'flat'; - if (this.options.hierarchy == hierarchy) return; + var hierarchy = $t.hasClass('NB-organizer-hierarchy-nested') ? 'nested' : 'flat'; + if (this.options.hierarchy == hierarchy) return; - this.toggle_hierarchy(hierarchy); - }, this)); + this.toggle_hierarchy(hierarchy); + }, this)); $.targetIs(e, { tagSelector: '.NB-icon-add' }, - _.bind(function($t, $p) { - e.preventDefault(); - - this.toggle_folder_add(); - }, this)); - + _.bind(function ($t, $p) { + e.preventDefault(); + + this.toggle_folder_add(); + }, this)); + $.targetIs(e, { tagSelector: '.NB-action-move' }, - _.bind(function($t, $p) { - e.preventDefault(); - - if ($t.is('.NB-disabled')) return; - this.move_feeds(); - }, this)); - + _.bind(function ($t, $p) { + e.preventDefault(); + + if ($t.is('.NB-disabled')) return; + this.move_feeds(); + }, this)); + $.targetIs(e, { tagSelector: '.NB-action-delete' }, - _.bind(function($t, $p) { - e.preventDefault(); - - if ($t.is('.NB-disabled')) return; - this.delete_feeds(); - }, this)); + _.bind(function ($t, $p) { + e.preventDefault(); + + if ($t.is('.NB-disabled')) return; + this.delete_feeds(); + }, this)); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { $.targetIs(e, { tagSelector: '.NB-folders', childOf: '.NB-organizer-sidebar-folderjump' }, - _.bind(function($t, $p) { - this.jump_to_folder($t.val()); - }, this)); + _.bind(function ($t, $p) { + this.jump_to_folder($t.val()); + }, this)); }, - toggle_folder_add: function() { + toggle_folder_add: function () { var $folder = $(".NB-add-folder", this.$modal); var $icon = $(".NB-icon-add", this.$modal); @@ -414,21 +414,21 @@ _.extend(NEWSBLUR.ReaderOrganizer.prototype, { $folder.slideDown(300); } }, - - jump_to_folder: function(folder_title) { + + jump_to_folder: function (folder_title) { if (this.options.hierarchy != 'nested') this.toggle_hierarchy('nested'); var $feedlist = $('.NB-feedchooser', this.$modal); - var $folder_title = $(".folder_title_text", $feedlist).filter(function(i) { + var $folder_title = $(".folder_title_text", $feedlist).filter(function (i) { console.log(["folder", this, _.string.trim($(this).text()), folder_title]); return _.string.trim($(this).text()) == folder_title; }); - if ($folder_title.length) $feedlist.scrollTo($folder_title, { + if ($folder_title.length) $feedlist.scrollTo($folder_title, { duration: 850, - axis: 'y', - easing: 'easeInOutCubic', - offset: -4, + axis: 'y', + easing: 'easeInOutCubic', + offset: -4, queue: false }); } - + }); diff --git a/media/js/newsblur/reader/reader_popover.js b/media/js/newsblur/reader/reader_popover.js index 58be4e2b39..9b77adad74 100644 --- a/media/js/newsblur/reader/reader_popover.js +++ b/media/js/newsblur/reader/reader_popover.js @@ -1,12 +1,12 @@ NEWSBLUR.ReaderPopover = Backbone.View.extend({ - + _open: false, - + events: { "click .NB-modal-cancel": "close" }, - - initialize: function(options) { + + initialize: function (options) { _.bindAll(this, 'handle_esc'); this.options = _.extend({}, { width: 236, @@ -18,8 +18,8 @@ NEWSBLUR.ReaderPopover = Backbone.View.extend({ }, this.options, options); $(document).bind('keydown', 'esc', this.handle_esc); }, - - render: function($content) { + + render: function ($content) { var self = this; this._open = true; @@ -29,14 +29,14 @@ NEWSBLUR.ReaderPopover = Backbone.View.extend({ $.make('div', { className: "popover-content" }, $content || this.$el) ]) ]); - + this.$overlay = $.make('div', { className: 'NB-overlay fade ' + (this.options.overlay_top && "NB-top") }); $('body').append(this.$overlay); - + this.$popover.width(this.options.width); - + $('body').append(this.$popover); - + this.$popover.addClass(this.options.placement.replace('-', '').replace(' ', '-')); this.$popover.addClass(this.options.popover_class); this.$popover.align(this.anchor(), this.options.placement, this.options.offset); @@ -44,7 +44,7 @@ NEWSBLUR.ReaderPopover = Backbone.View.extend({ clickable: true, onHide: _.bind(this.close, this) }); - + if (this.options.animate) { this.$popover.addClass("in"); this.$overlay.addClass("in"); @@ -58,11 +58,11 @@ NEWSBLUR.ReaderPopover = Backbone.View.extend({ this.$popover.width(this.options.width + 18); } }, this)); - + return this; }, - - close: function(e, hide_callback) { + + close: function (e, hide_callback) { var $el = window.a = this.$popover; var self = this; if (_.isFunction(e)) hide_callback = e; @@ -106,48 +106,48 @@ NEWSBLUR.ReaderPopover = Backbone.View.extend({ this.remove(); hide_callback(); } - + return false; }, - - anchor: function() { + + anchor: function () { if (_.isFunction(this.options.anchor)) { return this.options.anchor(); } else { return $(this.options.anchor); } }, - - handle_esc: function(e) { + + handle_esc: function (e) { if (this._open) { e.preventDefault(); e.stopPropagation(); - + this.close(); - + return false; } } - + }, { - - create: function(options) { + + create: function (options) { if (NEWSBLUR.ReaderPopover._popover && NEWSBLUR.ReaderPopover._popover._open) { NEWSBLUR.ReaderPopover._popover.close(); } - + NEWSBLUR.ReaderPopover._popover = new this(options); - + }, - - close: function() { + + close: function () { if (NEWSBLUR.ReaderPopover._popover && NEWSBLUR.ReaderPopover._popover._open) { NEWSBLUR.ReaderPopover._popover.close(); } }, - - is_open: function() { + + is_open: function () { return NEWSBLUR.ReaderPopover._popover && NEWSBLUR.ReaderPopover._popover._open; } - + }); diff --git a/media/js/newsblur/reader/reader_preferences.js b/media/js/newsblur/reader/reader_preferences.js index 85a61d954c..d652bce3aa 100644 --- a/media/js/newsblur/reader/reader_preferences.js +++ b/media/js/newsblur/reader/reader_preferences.js @@ -2,11 +2,11 @@ // - Feed sort order // - New window behavior -NEWSBLUR.ReaderPreferences = function(options) { +NEWSBLUR.ReaderPreferences = function (options) { var defaults = { width: 700 }; - + this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.assets; this.runner(); @@ -17,8 +17,8 @@ NEWSBLUR.ReaderPreferences.prototype.constructor = NEWSBLUR.ReaderPreferences; _.extend(NEWSBLUR.ReaderPreferences.prototype, { - runner: function() { - this.options.onOpen = _.bind(function() { + runner: function () { + this.options.onOpen = _.bind(function () { this.resize_modal(); }, this); this.make_modal(); @@ -26,13 +26,13 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { this.handle_change(); this.open_modal(); this.original_preferences = this.serialize_preferences(); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-preferences NB-modal' }, [ $.make('div', { className: 'NB-modal-tabs' }, [ $.make('div', { className: 'NB-modal-tab NB-active NB-modal-tab-general' }, 'General'), @@ -66,7 +66,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Days of unreads' ]) ]), @@ -181,7 +181,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Timezone' ]) ]), @@ -194,7 +194,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Window title' ]) ]), @@ -213,7 +213,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Special Folders' ]) ]), @@ -232,7 +232,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Default folder' ]) ]), @@ -241,19 +241,19 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('div', [ $.make('input', { id: 'NB-preference-animations-1', type: 'radio', name: 'animations', value: 'true' }), $.make('label', { 'for': 'NB-preference-animations-1' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/arrow_in.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/arrow_in.png' }), 'Show all animations' ]) ]), $.make('div', [ $.make('input', { id: 'NB-preference-animations-2', type: 'radio', name: 'animations', value: 'false' }), $.make('label', { 'for': 'NB-preference-animations-2' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/arrow_right.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/arrow_right.png' }), 'Jump immediately with no animations' ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Animations' ]) ]), @@ -262,19 +262,19 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('div', [ $.make('input', { id: 'NB-preference-feedorder-1', type: 'radio', name: 'feed_order', value: 'ALPHABETICAL' }), $.make('label', { 'for': 'NB-preference-feedorder-1' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/pilcrow.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/pilcrow.png' }), 'Alphabetical' ]) ]), $.make('div', [ $.make('input', { id: 'NB-preference-feedorder-2', type: 'radio', name: 'feed_order', value: 'MOSTUSED' }), $.make('label', { 'for': 'NB-preference-feedorder-2' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/report_user.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/report_user.png' }), 'Most used at top, then alphabetical' ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Site sidebar order' ]) ]), @@ -293,7 +293,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Folder unread counts' ]) ]), @@ -343,12 +343,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { 'Right-clicking', $.make('div', { className: 'NB-preference-sublabel' }, 'Folders, feeds, and story titles') ]) - ]), + ]), $.make('div', { className: 'NB-preference NB-preference-opml' }, [ $.make('div', { className: 'NB-preference-options' }, [ $.make('a', { className: 'NB-splash-link', href: NEWSBLUR.URLs['opml-export'] }, 'Download OPML') ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Backup your sites', $.make('div', { className: 'NB-preference-sublabel' }, 'Download this XML file as a backup') ]) @@ -360,40 +360,40 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.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("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("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("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("img", { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/circular/nav_story_grid_active.png' }), $.make("div", { className: "NB-layout-title" }, "Grid") ]) ]), $.make('div', [ $.make('label', { 'for': 'NB-preference-layout-5' }, [ $.make('input', { id: 'NB-preference-layout-5', type: 'radio', name: 'story_layout', value: 'magazine' }), - $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_magazine_active.png' }), + $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/circular/nav_story_magazine_active.png' }), $.make("div", { className: "NB-layout-title" }, "Magazine") ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Default layout', $.make('div', { className: 'NB-preference-sublabel' }, 'You can override this on a per-site basis.'), $.make('div', { className: 'NB-clear-overrides-layout NB-preference-sublabel-link NB-splash-link' }, "Clear all overrides") @@ -404,33 +404,33 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('div', { className: "NB-view-setting-original" }, [ $.make('label', { 'for': 'NB-preference-view-1' }, [ $.make('input', { id: 'NB-preference-view-1', type: 'radio', name: 'default_view', value: 'page' }), - $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_original_active.png' }), + $.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: 'default_view', value: 'feed' }), - $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_feed_active.png' }), + $.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: 'default_view', value: 'text' }), - $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_text_active.png' }), + $.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: 'default_view', value: 'story' }), - $.make("img", { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/circular/nav_story_story_active.png' }), + $.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'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Default view', $.make('div', { className: 'NB-preference-sublabel' }, 'You can override this on a per-site basis.'), $.make('div', { className: 'NB-clear-overrides-view NB-preference-sublabel-link NB-splash-link' }, "Clear all overrides") @@ -447,7 +447,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('li', { className: 'NB-preference-view-setting-read-filter-unread' }, 'Unread only') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Default story order', $.make('div', { className: 'NB-preference-sublabel' }, 'You can override this on a per-site and per-folder basis.'), $.make('div', { className: 'NB-clear-overrides-order NB-preference-sublabel-link NB-splash-link' }, "Clear all overrides") @@ -468,7 +468,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'When opening a site' ]) ]), @@ -487,7 +487,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Mark stories read on scroll' ]) ]), @@ -506,7 +506,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Spacing between feeds and story titles' ]) ]), @@ -525,7 +525,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Story content preview' ]) ]), @@ -562,7 +562,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Image preview' ]) ]), @@ -587,7 +587,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Double-clicking a site' ]) ]), @@ -606,7 +606,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Double-clicking an unread count' ]) ]), @@ -625,7 +625,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Marking All Site Stories as read' ]) ]), @@ -649,19 +649,23 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('input', { id: 'NB-preference-readstorydelay-0', type: 'radio', name: 'read_story_delay', value: "-1" }), $.make('label', { 'for': 'NB-preference-readstorydelay-0' }, [ 'Manually by hitting ', - $.make('div', { className: 'NB-keyboard-shortcut-key', - style: 'display: inline; float: none;margin: 0 4px' }, [ + $.make('div', { + className: 'NB-keyboard-shortcut-key', + style: 'display: inline; float: none;margin: 0 4px' + }, [ 'u' ]), 'or', - $.make('div', { className: 'NB-keyboard-shortcut-key', - style: 'display: inline; float: none;margin: 0 4px' }, [ + $.make('div', { + className: 'NB-keyboard-shortcut-key', + style: 'display: inline; float: none;margin: 0 4px' + }, [ 'm' ]) ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Mark a story as read', $.make('div', { className: 'NB-preference-sublabel' }, 'Clicking on a story marks it as read immediately.') ]) @@ -681,7 +685,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'After marking feed/folder read' ]) ]) @@ -747,7 +751,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('label', { 'for': 'NB-preference-story-share-delicious' }) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Sharing services' ]) ]), @@ -756,19 +760,19 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('div', [ $.make('input', { id: 'NB-preference-window-1', type: 'radio', name: 'new_window', value: 0 }), $.make('label', { 'for': 'NB-preference-window-1' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/application_view_gallery.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_gallery.png' }), 'In this window' ]) ]), $.make('div', [ $.make('input', { id: 'NB-preference-window-2', type: 'radio', name: 'new_window', value: 1 }), $.make('label', { 'for': 'NB-preference-window-2' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/application_side_expand.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_side_expand.png' }), 'In a new window' ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Open links' ]) ]), @@ -793,7 +797,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Truncate stories' ]) ]), @@ -808,7 +812,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('label', { 'for': 'NB-preference-public-comments-2' }, 'Only show comments from friends') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Show all comments' ]) ]), @@ -823,7 +827,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('label', { 'for': 'NB-preference-story-button-placement-2' }, 'Show buttons on the right (when there is room)') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Story button placement' ]) ]), @@ -838,7 +842,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('label', { 'for': 'NB-preference-highlights-2' }, 'Disable the highlighter') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ 'Enable highlighting' ]) ]) @@ -852,27 +856,27 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('div', { className: 'NB-preference NB-preference-keyboard-horizontalarrows' }, [ $.make('div', { className: 'NB-preference-options' }, [ $.make('div', [ - $.make('input', { - id: 'NB-preference-keyboard-horizontalarrows-1', - type: 'radio', - name: 'keyboard_horizontalarrows', + $.make('input', { + id: 'NB-preference-keyboard-horizontalarrows-1', + type: 'radio', + name: 'keyboard_horizontalarrows', value: 'view', disabled: !NEWSBLUR.Globals.is_premium }), $.make('label', { 'for': 'NB-preference-keyboard-horizontalarrows-1' }, 'Switch between views (original, feed, text, story)') ]), $.make('div', [ - $.make('input', { - id: 'NB-preference-keyboard-horizontalarrows-2', - type: 'radio', - name: 'keyboard_horizontalarrows', + $.make('input', { + id: 'NB-preference-keyboard-horizontalarrows-2', + type: 'radio', + name: 'keyboard_horizontalarrows', value: 'site', disabled: !NEWSBLUR.Globals.is_premium }), $.make('label', { 'for': 'NB-preference-keyboard-horizontalarrows-2' }, 'Open the next site/folder') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ '←' ]), @@ -884,20 +888,20 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('div', { className: 'NB-preference NB-preference-keyboard-verticalarrows' }, [ $.make('div', { className: 'NB-preference-options' }, [ $.make('div', [ - $.make('input', { - id: 'NB-preference-keyboard-verticalarrows-1', - type: 'radio', - name: 'keyboard_verticalarrows', + $.make('input', { + id: 'NB-preference-keyboard-verticalarrows-1', + type: 'radio', + name: 'keyboard_verticalarrows', value: 'story', disabled: !NEWSBLUR.Globals.is_premium }), $.make('label', { 'for': 'NB-preference-keyboard-verticalarrows-1' }, 'Navigate between stories') ]), $.make('div', [ - $.make('input', { - id: 'NB-preference-keyboard-verticalarrows-2', - type: 'radio', - name: 'keyboard_verticalarrows', + $.make('input', { + id: 'NB-preference-keyboard-verticalarrows-2', + type: 'radio', + name: 'keyboard_verticalarrows', value: 'scroll', disabled: !NEWSBLUR.Globals.is_premium }), @@ -910,7 +914,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ '↓' ]), @@ -930,34 +934,34 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $.make('input', { name: 'space_scroll_spacing', value: NEWSBLUR.Preferences.space_scroll_spacing, type: 'hidden' }) ]), $.make('div', { className: 'NB-preference-keyboard-spacebaraction' }, [ - $.make('input', { - id: 'NB-preference-keyboard-spacebaraction-1', - type: 'radio', - name: 'space_bar_action', + $.make('input', { + id: 'NB-preference-keyboard-spacebaraction-1', + type: 'radio', + name: 'space_bar_action', value: 'next_unread' }), $.make('label', { 'for': 'NB-preference-keyboard-spacebaraction-1' }, 'Open next unread story when bottom of story is visible') ]), $.make('div', { className: 'NB-preference-keyboard-spacebaraction' }, [ - $.make('input', { - id: 'NB-preference-keyboard-spacebaraction-2', - type: 'radio', - name: 'space_bar_action', + $.make('input', { + id: 'NB-preference-keyboard-spacebaraction-2', + type: 'radio', + name: 'space_bar_action', value: 'next_unread_50' }), $.make('label', { 'for': 'NB-preference-keyboard-spacebaraction-2' }, 'Open next unread story when story is half-way up') ]), $.make('div', [ - $.make('input', { - id: 'NB-preference-keyboard-spacebaraction-3', - type: 'radio', - name: 'space_bar_action', + $.make('input', { + id: 'NB-preference-keyboard-spacebaraction-3', + type: 'radio', + name: 'space_bar_action', value: 'scroll_only' }), $.make('label', { 'for': 'NB-preference-keyboard-spacebaraction-3' }, 'Only page down in story, do not open next unread story') ]) ]), - $.make('div', { className: 'NB-preference-label'}, [ + $.make('div', { className: 'NB-preference-label' }, [ $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ 'space' ]) @@ -970,18 +974,18 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { ]) ]); }, - - make_autoopen_folders: function() { + + make_autoopen_folders: function () { var autoopen_folder = NEWSBLUR.Preferences.autoopen_folder; var $folders = NEWSBLUR.utils.make_folders(autoopen_folder, "All Site Stories", 'default_folder'); return $folders; }, - - resize_modal: function(old_height) { + + resize_modal: function (old_height) { var $scroll = $('.NB-tab.NB-active', this.$modal); var $modal = this.$modal; var $modal_container = $modal.closest('.simplemodal-container'); - + if ($modal.height() == old_height) { console.log(['Modal resize doing nothing, escaping']); return; @@ -992,212 +996,212 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { this.resize_modal($modal.height()); } }, - - select_preferences: function() { + + select_preferences: function () { var $modal = this.$modal; - + if (NEWSBLUR.Preferences.timezone) { - $('select[name=timezone] option', $modal).each(function() { + $('select[name=timezone] option', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.timezone) { $(this).prop('selected', true); return false; } }); } - - $('select[name=default_folder] option', $modal).each(function() { + + $('select[name=default_folder] option', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.default_folder) { $(this).prop('selected', true); return false; } }); - $('input[name=story_layout]', $modal).each(function() { + $('input[name=story_layout]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.story_layout) { $(this).prop('checked', true); return false; } }); - $('input[name=default_view]', $modal).each(function() { + $('input[name=default_view]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.default_view) { $(this).prop('checked', true); return false; } }); - $('input[name=new_window]', $modal).each(function() { + $('input[name=new_window]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.new_window) { $(this).prop('checked', true); return false; } }); - $('input[name=feed_order]', $modal).each(function() { + $('input[name=feed_order]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.feed_order) { $(this).prop('checked', true); return false; } }); - $('input[name=ssl]', $modal).each(function() { + $('input[name=ssl]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.ssl) { $(this).prop('checked', true); return false; } }); - $('input[name=autoopen_folder]', $modal).each(function() { + $('input[name=autoopen_folder]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.autoopen_folder) { $(this).prop('checked', true); return false; } }); - $('input[name=title_counts]', $modal).each(function() { + $('input[name=title_counts]', $modal).each(function () { if (NEWSBLUR.Preferences.title_counts) { $(this).prop('checked', true); return false; } }); - $('input[name=show_global_shared_stories]', $modal).each(function() { + $('input[name=show_global_shared_stories]', $modal).each(function () { if (NEWSBLUR.Preferences.show_global_shared_stories) { $(this).prop('checked', true); return false; } }); - $('input[name=show_infrequent_site_stories]', $modal).each(function() { + $('input[name=show_infrequent_site_stories]', $modal).each(function () { if (NEWSBLUR.Preferences.show_infrequent_site_stories) { $(this).prop('checked', true); return false; } }); - $('input[name=open_feed_action]', $modal).each(function() { + $('input[name=open_feed_action]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.open_feed_action) { $(this).prop('checked', true); return false; } }); - $('input[name=mark_read_on_scroll_titles]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.mark_read_on_scroll_titles) { + $('input[name=mark_read_on_scroll_titles]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.mark_read_on_scroll_titles) { $(this).prop('checked', true); return false; } }); - $('input[name=density]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.density) { + $('input[name=density]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.density) { $(this).prop('checked', true); return false; } }); - $('input[name=show_content_preview]', $modal).each(function() { + $('input[name=show_content_preview]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.show_content_preview) { $(this).prop('checked', true); return false; } }); - $('input[name=image_preview]', $modal).each(function() { + $('input[name=image_preview]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.image_preview) { $(this).prop('checked', true); return false; } }); - $('input[name=doubleclick_feed]', $modal).each(function() { + $('input[name=doubleclick_feed]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.doubleclick_feed) { $(this).prop('checked', true); return false; } }); - $('input[name=doubleclick_unread]', $modal).each(function() { + $('input[name=doubleclick_unread]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.doubleclick_unread) { $(this).prop('checked', true); return false; } }); - $('input[name=mark_read_river_confirm]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.mark_read_river_confirm) { + $('input[name=mark_read_river_confirm]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.mark_read_river_confirm) { $(this).prop('checked', true); return false; } }); - $('input[name=markread_nextfeed]', $modal).each(function() { + $('input[name=markread_nextfeed]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.markread_nextfeed) { $(this).prop('checked', true); return false; } }); - $('input[name=days_of_unread]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.days_of_unread) { + $('input[name=days_of_unread]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.days_of_unread) { $(this).prop('checked', true); return false; } }); - $('input[name=read_story_delay]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.read_story_delay) { + $('input[name=read_story_delay]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.read_story_delay) { $(this).prop('checked', true); return false; } }); - $('input[name=truncate_story]', $modal).each(function() { + $('input[name=truncate_story]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.truncate_story) { $(this).prop('checked', true); return false; } }); - $('input[name=animations]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.animations) { + $('input[name=animations]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.animations) { $(this).prop('checked', true); return false; } }); - $('input[name=dateformat]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.dateformat) { + $('input[name=dateformat]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.dateformat) { $(this).prop('checked', true); return false; } }); - $('input[name=folder_counts]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.folder_counts) { + $('input[name=folder_counts]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.folder_counts) { $(this).prop('checked', true); return false; } }); - $('input[name=show_tooltips]', $modal).each(function() { + $('input[name=show_tooltips]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.show_tooltips) { $(this).prop('checked', true); return false; } }); - $('input[name=show_contextmenus]', $modal).each(function() { + $('input[name=show_contextmenus]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.show_contextmenus) { $(this).prop('checked', true); return false; } }); - $('input[name=hide_public_comments]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.hide_public_comments) { + $('input[name=hide_public_comments]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.hide_public_comments) { $(this).prop('checked', true); return false; } }); - $('input[name=story_button_placement]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.story_button_placement) { + $('input[name=story_button_placement]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.story_button_placement) { $(this).prop('checked', true); return false; } }); - $('input[name=highlights]', $modal).each(function() { - if ($(this).val() == ""+NEWSBLUR.Preferences.highlights) { + $('input[name=highlights]', $modal).each(function () { + if ($(this).val() == "" + NEWSBLUR.Preferences.highlights) { $(this).prop('checked', true); return false; } }); - $('input[name=keyboard_verticalarrows]', $modal).each(function() { + $('input[name=keyboard_verticalarrows]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.keyboard_verticalarrows) { $(this).prop('checked', true); return false; } }); - $('input[name=keyboard_horizontalarrows]', $modal).each(function() { + $('input[name=keyboard_horizontalarrows]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.keyboard_horizontalarrows) { $(this).prop('checked', true); return false; } }); - $('input[name=space_bar_action]', $modal).each(function() { + $('input[name=space_bar_action]', $modal).each(function () { if ($(this).val() == NEWSBLUR.Preferences.space_bar_action) { $(this).prop('checked', true); return false; @@ -1205,22 +1209,22 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { }); $('input[name=arrow_scroll_spacing]', $modal).val(NEWSBLUR.Preferences.arrow_scroll_spacing); $('input[name=space_scroll_spacing]', $modal).val(NEWSBLUR.Preferences.space_scroll_spacing); - + var order = NEWSBLUR.Preferences['default_order']; var read_filter = NEWSBLUR.Preferences['default_read_filter']; $('.NB-preference-view-setting-order-oldest', $modal).toggleClass('NB-active', order == 'oldest'); $('.NB-preference-view-setting-order-newest', $modal).toggleClass('NB-active', order != 'oldest'); $('.NB-preference-view-setting-read-filter-unread', $modal).toggleClass('NB-active', read_filter == 'unread'); $('.NB-preference-view-setting-read-filter-all', $modal).toggleClass('NB-active', read_filter != 'unread'); - - var share_preferences = _.select(_.keys(NEWSBLUR.Preferences), function(p) { - return p.indexOf('story_share') != -1; + + var share_preferences = _.select(_.keys(NEWSBLUR.Preferences), function (p) { + return p.indexOf('story_share') != -1; }); - _.each(share_preferences, function(share) { + _.each(share_preferences, function (share) { var share_name = share.match(/story_share_(.*)/)[1]; - $('input#NB-preference-story-share-'+share_name, $modal).prop('checked', NEWSBLUR.Preferences[share]); + $('input#NB-preference-story-share-' + share_name, $modal).prop('checked', NEWSBLUR.Preferences[share]); }); - + $(".NB-tangle-daysofunread-control", $modal).slider({ range: 'min', min: 1, @@ -1260,10 +1264,10 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { this.slide_arrow_scroll_spacing_slider(); this.slide_space_scroll_spacing_slider(); }, - - slide_days_of_unread_slider: function(e, ui) { + + slide_days_of_unread_slider: function (e, ui) { var value = (ui && ui.value) || - (NEWSBLUR.Preferences.days_of_unread); + (NEWSBLUR.Preferences.days_of_unread); if (NEWSBLUR.Preferences.days_of_unread <= 365 || ui) { $(".NB-tangle-daysofunread", this.$modal).text(value == 1 ? value + ' day' : value + ' days'); $("input[name=daysofunread_input]", this.$modal).val(value); @@ -1284,7 +1288,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { slide_read_story_delay_slider: function (e, ui) { var value = (ui && ui.value) || - (NEWSBLUR.Preferences.read_story_delay > 0 ? NEWSBLUR.Preferences.read_story_delay : 1); + (NEWSBLUR.Preferences.read_story_delay > 0 ? NEWSBLUR.Preferences.read_story_delay : 1); $(".NB-tangle-seconds", this.$modal).text(value == 1 ? value + ' second.' : value + ' seconds.'); if (NEWSBLUR.Preferences.read_story_delay > 0 || ui) { $("#NB-preference-readstorydelay-2", this.$modal).prop('checked', true).val(value); @@ -1294,7 +1298,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { } }, - slide_arrow_scroll_spacing_slider: function(e, ui) { + slide_arrow_scroll_spacing_slider: function (e, ui) { var value = (ui && ui.value) || NEWSBLUR.Preferences.arrow_scroll_spacing; if (!NEWSBLUR.Globals.is_premium) { value = NEWSBLUR.Preferences.arrow_scroll_spacing; @@ -1309,7 +1313,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { } }, - slide_space_scroll_spacing_slider: function(e, ui) { + slide_space_scroll_spacing_slider: function (e, ui) { var value = (ui && ui.value) || NEWSBLUR.Preferences.space_scroll_spacing; if (!NEWSBLUR.Globals.is_premium) { value = NEWSBLUR.Preferences.space_scroll_spacing; @@ -1321,19 +1325,19 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { } }, - serialize_preferences: function() { + serialize_preferences: function () { var preferences = {}; - $('input[type=radio]:checked, select', this.$modal).each(function() { - var name = $(this).attr('name'); + $('input[type=radio]:checked, select', this.$modal).each(function () { + var name = $(this).attr('name'); var preference = preferences[name] = $(this).val(); - if (preference == 'true') preferences[name] = true; + if (preference == 'true') preferences[name] = true; else if (preference == 'false') preferences[name] = false; }); - $('input[type=checkbox]', this.$modal).each(function() { + $('input[type=checkbox]', this.$modal).each(function () { preferences[$(this).attr('name')] = $(this).is(':checked'); }); - $('input[type=hidden]', this.$modal).each(function() { + $('input[type=hidden]', this.$modal).each(function () { preferences[$(this).attr('name')] = $(this).val(); }); preferences['default_order'] = $('.NB-preference-view-setting-order li.NB-active', this.$modal).hasClass('NB-preference-view-setting-order-oldest') ? 'oldest' : 'newest'; @@ -1341,14 +1345,14 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { return preferences; }, - - save_preferences: function() { + + save_preferences: function () { var self = this; var form = this.serialize_preferences(); $('.NB-preference-error', this.$modal).text(''); $('.NB-modal-submit-button', this.$modal).text('Saving...').attr('disabled', true).addClass('NB-disabled'); - - this.model.save_preferences(form, function(data) { + + this.model.save_preferences(form, function (data) { NEWSBLUR.reader.switch_feed_view_unread_view(); NEWSBLUR.reader.apply_story_styling(true); NEWSBLUR.reader.apply_tipsy_titles(); @@ -1358,12 +1362,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { NEWSBLUR.app.sidebar_header.count(); if (self.original_preferences['feed_order'] != form['feed_order'] || self.original_preferences['folder_counts'] != form['folder_counts']) { - NEWSBLUR.app.feed_list.make_feeds(); - NEWSBLUR.app.feed_list.make_social_feeds(); + NEWSBLUR.app.feed_list.make_feeds(); + NEWSBLUR.app.feed_list.make_social_feeds(); } if (self.original_preferences['show_global_shared_stories'] != form['show_global_shared_stories'] || self.original_preferences['show_infrequent_site_stories'] != form['show_infrequent_site_stories']) { - NEWSBLUR.app.feed_list.toggle_filter_feeds(); + NEWSBLUR.app.feed_list.toggle_filter_feeds(); } if (self.original_preferences['ssl'] != form['ssl']) { NEWSBLUR.reader.check_and_load_ssl(); @@ -1374,20 +1378,20 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { self.close(); }); }, - - close_and_load_account: function() { - this.close(function() { - NEWSBLUR.reader.open_account_modal(); - }); + + close_and_load_account: function () { + this.close(function () { + NEWSBLUR.reader.open_account_modal(); + }); }, - - close_and_load_feedchooser: function() { - this.close(function() { + + close_and_load_feedchooser: function () { + this.close(function () { NEWSBLUR.reader.open_feedchooser_modal(); }); }, - - change_view_setting: function(view, setting) { + + change_view_setting: function (view, setting) { if (view == 'order') { $('.NB-preference-view-setting-order-oldest').toggleClass('NB-active', setting == 'oldest'); $('.NB-preference-view-setting-order-newest').toggleClass('NB-active', setting != 'oldest'); @@ -1395,26 +1399,26 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { $('.NB-preference-view-setting-read-filter-unread').toggleClass('NB-active', setting == 'unread'); $('.NB-preference-view-setting-read-filter-all').toggleClass('NB-active', setting != 'unread'); } - + this.enable_save(); }, - - clear_overrides: function(type) { + + clear_overrides: function (type) { var $sublabel = $('.NB-clear-overrides-' + type, this.$modal); $sublabel.text('Resetting...').removeClass('NB-splash-link'); - NEWSBLUR.assets.clear_view_settings(type, _.bind(function(data) { + NEWSBLUR.assets.clear_view_settings(type, _.bind(function (data) { $sublabel.text('Cleared ' + Inflector.pluralize('override', data.removed, true) + '.'); }, this)); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function ($t, $p) { e.preventDefault(); var newtab; if ($t.hasClass('NB-modal-tab-general')) { @@ -1428,67 +1432,67 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, { } self.resize_modal(); self.switch_tab(newtab); - }); - $.targetIs(e, { tagSelector: '.NB-modal-submit-button' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-modal-submit-button' }, function ($t, $p) { e.preventDefault(); - + self.save_preferences(); }); - $.targetIs(e, { tagSelector: '.NB-add-url-submit' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-add-url-submit' }, function ($t, $p) { e.preventDefault(); - + self.save_preferences(); }); - $.targetIs(e, { tagSelector: '.NB-link-account-preferences' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-link-account-preferences' }, function ($t, $p) { e.preventDefault(); - + self.close_and_load_account(); }); - $.targetIs(e, { tagSelector: '.NB-modal-cancel' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-modal-cancel' }, function ($t, $p) { e.preventDefault(); - + self.close(); }); - $.targetIs(e, { tagSelector: '.NB-premium-link' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-premium-link' }, function ($t, $p) { e.preventDefault(); self.close_and_load_feedchooser(); }); - $.targetIs(e, { tagSelector: '.segmented-control.NB-preference-view-setting-order li' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.segmented-control.NB-preference-view-setting-order li' }, function ($t, $p) { e.preventDefault(); var order = $t.hasClass('NB-preference-view-setting-order-oldest') ? 'oldest' : 'newest'; self.change_view_setting('order', order); }); - $.targetIs(e, { tagSelector: '.segmented-control.NB-preference-view-setting-read-filter li' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.segmented-control.NB-preference-view-setting-read-filter li' }, function ($t, $p) { e.preventDefault(); var read_filter = $t.hasClass('NB-preference-view-setting-read-filter-unread') ? 'unread' : 'all'; self.change_view_setting('read_filter', read_filter); }); - $.targetIs(e, { tagSelector: '.NB-clear-overrides-view' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-clear-overrides-view' }, function ($t, $p) { e.preventDefault(); self.clear_overrides('view'); }); - $.targetIs(e, { tagSelector: '.NB-clear-overrides-order' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-clear-overrides-order' }, function ($t, $p) { e.preventDefault(); self.clear_overrides('order'); }); - $.targetIs(e, { tagSelector: '.NB-clear-overrides-layout' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-clear-overrides-layout' }, function ($t, $p) { e.preventDefault(); self.clear_overrides('layout'); }); }, - - handle_change: function() { - + + handle_change: function () { + $('input[type=radio],input[type=checkbox],select', this.$modal).bind('change', _.bind(this.enable_save, this)); }, - - enable_save: function() { + + enable_save: function () { $('.NB-modal-submit-button', this.$modal).removeAttr('disabled').removeClass('NB-disabled').text('Save Preferences'); }, - - disable_save: function() { + + disable_save: function () { $('.NB-modal-submit-button', this.$modal).attr('disabled', true).addClass('NB-disabled').text('Make changes above...'); } - + }); diff --git a/media/js/newsblur/reader/reader_profile_editor.js b/media/js/newsblur/reader/reader_profile_editor.js index 8d31c77f23..217811dcdd 100644 --- a/media/js/newsblur/reader/reader_profile_editor.js +++ b/media/js/newsblur/reader/reader_profile_editor.js @@ -1,21 +1,21 @@ -NEWSBLUR.ReaderProfileEditor = function(options) { +NEWSBLUR.ReaderProfileEditor = function (options) { var defaults = { width: 800 }; - + this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; + this.model = NEWSBLUR.assets; this.profile = this.model.user_profile; - + this.runner(); }; NEWSBLUR.ReaderProfileEditor.prototype = new NEWSBLUR.Modal; _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { - - runner: function() { - this.options.onOpen = _.bind(function() { + + runner: function () { + this.options.onOpen = _.bind(function () { this.resize_modal(); }, this); @@ -28,10 +28,10 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { this.handle_profile_counts(); this.delegate_change(); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal NB-modal-profile-editor' }, [ $.make('div', { className: 'NB-modal-tabs' }, [ $.make('div', { className: 'NB-modal-loading' }), @@ -50,11 +50,11 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { ]), $.make('fieldset', [ $.make('legend', 'Profile picture'), - $.make('div', { className: 'NB-modal-section NB-friends-profilephoto'}) + $.make('div', { className: 'NB-modal-section NB-friends-profilephoto' }) ]), $.make('fieldset', [ $.make('legend', 'Profile Details'), - $.make('div', { className: 'NB-modal-section NB-friends-profile'}, [ + $.make('div', { className: 'NB-modal-section NB-friends-profile' }, [ $.make('form', [ $.make('label', 'Username'), $.make('div', { className: 'NB-profile-username' }, [ @@ -80,13 +80,13 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { ]), $.make('div', { className: 'NB-profile-privacy-options' }, [ $.make('div', { className: 'NB-profile-privacy-option' }, [ - $.make('input', { - id: 'NB-profile-privacy-public', - name: 'protected', - type: 'radio', - value: 'public', + $.make('input', { + id: 'NB-profile-privacy-public', + name: 'protected', + type: 'radio', + value: 'public', checked: !this.profile.get('protected') && - !this.profile.get('private'), + !this.profile.get('private'), disabled: !NEWSBLUR.Globals.is_premium }), $.make('label', { 'for': 'NB-profile-privacy-public', className: 'NB-profile-protected-label' }, [ @@ -95,13 +95,13 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { ]) ]), $.make('div', { className: 'NB-profile-privacy-option' }, [ - $.make('input', { - id: 'NB-profile-privacy-protected', - name: 'protected', - type: 'radio', - value: 'protected', + $.make('input', { + id: 'NB-profile-privacy-protected', + name: 'protected', + type: 'radio', + value: 'protected', checked: this.profile.get('protected') && - !this.profile.get('private'), + !this.profile.get('private'), disabled: !NEWSBLUR.Globals.is_premium }), $.make('label', { 'for': 'NB-profile-privacy-protected', className: 'NB-profile-protected-label' }, [ @@ -111,13 +111,13 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { ]) ]), $.make('div', { className: 'NB-profile-privacy-option' }, [ - $.make('input', { - id: 'NB-profile-privacy-private', - name: 'protected', - type: 'radio', - value: 'private', + $.make('input', { + id: 'NB-profile-privacy-private', + name: 'protected', + type: 'radio', + value: 'private', checked: this.profile.get('protected') && - this.profile.get('private'), + this.profile.get('private'), disabled: !NEWSBLUR.Globals.is_premium }), $.make('label', { 'for': 'NB-profile-privacy-private', className: 'NB-profile-protected-label' }, [ @@ -146,15 +146,15 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { ]), $.make('fieldset', [ $.make('legend', 'Custom CSS for your Blurblog'), - $.make('div', { className: 'NB-modal-section NB-profile-editor-blurblog-custom-css'}, [ + $.make('div', { className: 'NB-modal-section NB-profile-editor-blurblog-custom-css' }, [ $.make('textarea', { 'className': 'NB-profile-blurblog-css', name: 'custom_css' }, this.profile.get('custom_css')) ]) ]), $.make('fieldset', [ $.make('legend', 'Blurblog Options'), - $.make('div', { className: 'NB-modal-section'}, [ + $.make('div', { className: 'NB-modal-section' }, [ $.make('div', { className: 'NB-preference NB-preference-permalinkdirect' }, [ - $.make('label', { className: 'NB-preference-label'}, [ + $.make('label', { className: 'NB-preference-label' }, [ 'Blurblog permalinks' ]), $.make('div', { className: 'NB-preference-options' }, [ @@ -180,93 +180,94 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { $.make('div', { className: 'NB-tab NB-tab-followers' }) ]); }, - - make_color_palette: function() { + + make_color_palette: function () { var user_profile = this.user_profile; var colors = [ // ["rgb(0, 0, 0)", "rgb(67, 67, 67)", "rgb(102, 102, 102)", "rgb(153, 153, 153)","rgb(183, 183, 183)", // "rgb(204, 204, 204)", "rgb(217, 217, 217)", "rgb(239, 239, 239)", "rgb(243, 243, 243)", "rgb(255, 255, 255)"], // ["rgb(152, 0, 0)", "rgb(255, 0, 0)", "rgb(255, 153, 0)", "rgb(255, 255, 0)", "rgb(0, 255, 0)", // "rgb(0, 255, 255)", "rgb(74, 134, 232)", "rgb(0, 0, 255)", "rgb(153, 0, 255)", "rgb(255, 0, 255)"], - ["rgb(230, 184, 175)", "rgb(244, 204, 204)", "rgb(252, 229, 205)", "rgb(255, 242, 204)", "rgb(217, 234, 211)", - "rgb(208, 224, 227)", "rgb(201, 218, 248)", "rgb(207, 226, 243)", "rgb(217, 210, 233)", "rgb(234, 209, 220)", - "rgb(221, 126, 107)", "rgb(234, 153, 153)", "rgb(249, 203, 156)", "rgb(255, 229, 153)", "rgb(182, 215, 168)", - "rgb(162, 196, 201)", "rgb(164, 194, 244)", "rgb(159, 197, 232)", "rgb(180, 167, 214)", "rgb(213, 166, 189)", - "rgb(204, 65, 37)", "rgb(224, 102, 102)", "rgb(246, 178, 107)", "rgb(255, 217, 102)", "rgb(147, 196, 125)", - "rgb(118, 165, 175)", "rgb(109, 158, 235)", "rgb(111, 168, 220)", "rgb(142, 124, 195)", "rgb(194, 123, 160)", - "rgb(166, 28, 0)", "rgb(204, 0, 0)", "rgb(230, 145, 56)", "rgb(241, 194, 50)", "rgb(106, 168, 79)", - "rgb(69, 129, 142)", "rgb(60, 120, 216)", "rgb(61, 133, 198)", "rgb(103, 78, 167)", "rgb(166, 77, 121)", - "rgb(133, 32, 12)", "rgb(153, 0, 0)", "rgb(180, 95, 6)", "rgb(191, 144, 0)", "rgb(56, 118, 29)", - "rgb(19, 79, 92)", "rgb(17, 85, 204)", "rgb(11, 83, 148)", "rgb(53, 28, 117)", "rgb(116, 27, 71)", - "rgb(91, 15, 0)", "rgb(102, 0, 0)", "rgb(120, 63, 4)", "rgb(127, 96, 0)", "rgb(39, 78, 19)", - "rgb(12, 52, 61)", "rgb(28, 69, 135)", "rgb(7, 55, 99)", "rgb(32, 18, 77)", "rgb(76, 17, 48)"] + ["rgb(230, 184, 175)", "rgb(244, 204, 204)", "rgb(252, 229, 205)", "rgb(255, 242, 204)", "rgb(217, 234, 211)", + "rgb(208, 224, 227)", "rgb(201, 218, 248)", "rgb(207, 226, 243)", "rgb(217, 210, 233)", "rgb(234, 209, 220)", + "rgb(221, 126, 107)", "rgb(234, 153, 153)", "rgb(249, 203, 156)", "rgb(255, 229, 153)", "rgb(182, 215, 168)", + "rgb(162, 196, 201)", "rgb(164, 194, 244)", "rgb(159, 197, 232)", "rgb(180, 167, 214)", "rgb(213, 166, 189)", + "rgb(204, 65, 37)", "rgb(224, 102, 102)", "rgb(246, 178, 107)", "rgb(255, 217, 102)", "rgb(147, 196, 125)", + "rgb(118, 165, 175)", "rgb(109, 158, 235)", "rgb(111, 168, 220)", "rgb(142, 124, 195)", "rgb(194, 123, 160)", + "rgb(166, 28, 0)", "rgb(204, 0, 0)", "rgb(230, 145, 56)", "rgb(241, 194, 50)", "rgb(106, 168, 79)", + "rgb(69, 129, 142)", "rgb(60, 120, 216)", "rgb(61, 133, 198)", "rgb(103, 78, 167)", "rgb(166, 77, 121)", + "rgb(133, 32, 12)", "rgb(153, 0, 0)", "rgb(180, 95, 6)", "rgb(191, 144, 0)", "rgb(56, 118, 29)", + "rgb(19, 79, 92)", "rgb(17, 85, 204)", "rgb(11, 83, 148)", "rgb(53, 28, 117)", "rgb(116, 27, 71)", + "rgb(91, 15, 0)", "rgb(102, 0, 0)", "rgb(120, 63, 4)", "rgb(127, 96, 0)", "rgb(39, 78, 19)", + "rgb(12, 52, 61)", "rgb(28, 69, 135)", "rgb(7, 55, 99)", "rgb(32, 18, 77)", "rgb(76, 17, 48)"] ]; - + var $colors = $.make('div', { className: 'NB-profile-blurblog-colors' }); - _.each(colors, function(color_line) { + _.each(colors, function (color_line) { var $color_line = $.make('div', { className: 'NB-profile-blurblog-colorline' }); - _.each(color_line, function(color) { + _.each(color_line, function (color) { var $color = $.make('span', { className: 'NB-profile-blurblog-color', style: 'background-color: ' + color }).data('color', color); $color_line.append($color); }); $colors.append($color_line); }); - + return $colors; }, - - choose_color: function() { + + choose_color: function () { var user_profile = this.profile; var $colors = $('.NB-profile-blurblog-color', this.$modal); - - $colors.each(function() { + + $colors.each(function () { var $color = $(this); var color = $color.data('color'); - + if (user_profile.get('custom_bgcolor') == color) { $color.addClass('NB-active'); return false; } }); }, - - populate_data: function() { + + populate_data: function () { var profile = this.profile; - + $('textarea[name=custom_css]', this.$modal).val(this.profile.get('custom_css')); - $('input[name=bb_permalink_direct]', this.$modal).each(function() { - if ($(this).val() == ""+profile.get('bb_permalink_direct')) { + $('input[name=bb_permalink_direct]', this.$modal).each(function () { + if ($(this).val() == "" + profile.get('bb_permalink_direct')) { $(this).prop('checked', true); } }); }, - - make_profile_section: function() { + + make_profile_section: function () { var $badge = $('.NB-friends-findfriends-profile', this.$modal).empty(); var $profile_badge; var profile = this.profile; - - $profile_badge = new NEWSBLUR.Views.SocialProfileBadge({model: profile}); + + $profile_badge = new NEWSBLUR.Views.SocialProfileBadge({ model: profile }); $badge.append($profile_badge); }, - - make_profile_photo_chooser: function() { + + make_profile_photo_chooser: function () { var $profiles = $('.NB-friends-profilephoto', this.$modal).empty(); - + $profiles.append($.make('div', { className: "NB-photo-upload-error NB-error" })); - - _.each(['nothing', 'upload', 'twitter', 'facebook', 'gravatar'], _.bind(function(service) { - var $profile = $.make('div', { className: 'NB-friends-profile-photo-group NB-friends-photo-'+service }, [ + + _.each(['nothing', 'upload', 'twitter', 'facebook', 'gravatar'], _.bind(function (service) { + var $profile = $.make('div', { className: 'NB-friends-profile-photo-group NB-friends-photo-' + service }, [ $.make('div', { className: 'NB-friends-photo-title' }, [ - $.make('input', { type: 'radio', name: 'profile_photo_service', value: service, id: 'NB-profile-photo-service-'+service }), - $.make('label', { 'for': 'NB-profile-photo-service-'+service }, _.string.capitalize(service)) + $.make('input', { type: 'radio', name: 'profile_photo_service', value: service, id: 'NB-profile-photo-service-' + service }), + $.make('label', { 'for': 'NB-profile-photo-service-' + service }, _.string.capitalize(service)) ]), $.make('div', { className: 'NB-friends-photo-image' }, [ - $.make('label', { 'for': 'NB-profile-photo-service-'+service }, [ + $.make('label', { 'for': 'NB-profile-photo-service-' + service }, [ $.make('div', { className: 'NB-photo-loader' }), - $.make('img', { src: service == 'nothing' || !this.services[service][service+'_picture_url'] ? - NEWSBLUR.Globals.MEDIA_URL + 'img/reader/default_profile_photo.png' : - this.services[service][service+'_picture_url'] + $.make('img', { + src: service == 'nothing' || !this.services[service][service + '_picture_url'] ? + NEWSBLUR.Globals.MEDIA_URL + 'img/reader/default_profile_photo.png' : + this.services[service][service + '_picture_url'] }) ]) ]), @@ -290,10 +291,10 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { $profiles.append($profile); }, this)); }, - - fetch_user_profile: function(callback) { + + fetch_user_profile: function (callback) { $('.NB-modal-loading', this.$modal).addClass('NB-active'); - this.model.load_current_user_profile(_.bind(function(data) { + this.model.load_current_user_profile(_.bind(function (data) { $('.NB-modal-loading', this.$modal).removeClass('NB-active'); this.profile = this.model.user_profile; this.services = data.services; @@ -304,10 +305,10 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { callback && callback(); }, this)); }, - - open_modal: function(callback) { + + open_modal: function (callback) { var self = this; - + this.$modal.modal({ 'minWidth': this.options.width, 'maxWidth': this.options.width, @@ -315,26 +316,26 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { 'onOpen': function (dialog) { dialog.overlay.fadeIn(200, function () { dialog.container.fadeIn(200); - dialog.data.fadeIn(200, function() { + dialog.data.fadeIn(200, function () { if (self.options.onOpen) { self.options.onOpen(); } }); - setTimeout(function() { + setTimeout(function () { $(window).resize(); }); }); }, - 'onShow': function(dialog) { + 'onShow': function (dialog) { $('#simplemodal-container').corner('6px'); if (self.options.onShow) { self.options.onShow(); } }, - 'onClose': function(dialog, callback) { + 'onClose': function (dialog, callback) { dialog.data.hide().empty().remove(); dialog.container.hide().empty().remove(); - dialog.overlay.fadeOut(200, function() { + dialog.overlay.fadeOut(200, function () { dialog.overlay.empty().remove(); $.modal.close(callback); }); @@ -342,73 +343,73 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { } }); }, - - resize_modal: function(count) { + + resize_modal: function (count) { var $tab = $('.NB-tab.NB-active', this.$modal); var $modal = this.$modal; var $modal_container = $modal.closest('.simplemodal-container'); - + if (count > 50) return; - + if ($modal.height() > $modal_container.height() - 24) { $tab.height($tab.height() - 5); - this.resize_modal(count+1); + this.resize_modal(count + 1); } - + }, - - switch_tab: function(newtab) { + + switch_tab: function (newtab) { var $modal_tabs = $('.NB-modal-tab', this.$modal); var $tabs = $('.NB-tab', this.$modal); - + $modal_tabs.removeClass('NB-active'); $tabs.removeClass('NB-active'); - - $modal_tabs.filter('.NB-modal-tab-'+newtab).addClass('NB-active'); - $tabs.filter('.NB-tab-'+newtab).addClass('NB-active'); - + + $modal_tabs.filter('.NB-modal-tab-' + newtab).addClass('NB-active'); + $tabs.filter('.NB-tab-' + newtab).addClass('NB-active'); + this.resize_modal(); }, - close_and_load_account: function() { - this.close(function() { + close_and_load_account: function () { + this.close(function () { NEWSBLUR.reader.open_account_modal(); }); }, - close_and_load_friends: function() { - this.close(function() { + close_and_load_friends: function () { + this.close(function () { NEWSBLUR.reader.open_friends_modal(); }); }, - - close_and_load_feedchooser: function() { - this.close(function() { + + close_and_load_feedchooser: function () { + this.close(function () { NEWSBLUR.reader.open_feedchooser_modal(); }); }, - - serialize_preferences: function($container) { + + serialize_preferences: function ($container) { var preferences = {}; $container = $container || this.$modal; - $('input[type=radio]:checked, select', $container).each(function() { - var name = $(this).attr('name'); + $('input[type=radio]:checked, select', $container).each(function () { + var name = $(this).attr('name'); var preference = preferences[name] = $(this).val(); - if (preference == 'true') preferences[name] = true; + if (preference == 'true') preferences[name] = true; else if (preference == 'false') preferences[name] = false; }); - $('input[type=checkbox]', $container).each(function() { + $('input[type=checkbox]', $container).each(function () { preferences[$(this).attr('name')] = $(this).is(':checked'); }); - $('input[type=hidden],input[type=text],textarea', $container).each(function() { + $('input[type=hidden],input[type=text],textarea', $container).each(function () { preferences[$(this).attr('name')] = $(this).val(); }); return preferences; }, - - save_profile: function() { + + save_profile: function () { var privacy_private = $('input#NB-profile-privacy-private', this.$modal).is(':checked'); var privacy_protected = $('input#NB-profile-privacy-protected', this.$modal).is(':checked') || privacy_private; var data = { @@ -419,7 +420,7 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { 'protected': privacy_protected, 'private': privacy_private }; - this.model.save_user_profile(data, _.bind(function(data) { + this.model.save_user_profile(data, _.bind(function (data) { this.animate_profile_badge(); this.disable_save_profile(); $('input[name=website]', this.$modal).val(this.profile.get('website')); @@ -427,27 +428,27 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { this.disable_save_profile(); $('.NB-profile-save-button', this.$modal).text('Saving...'); }, - - save_blurblog: function() { + + save_blurblog: function () { var data = this.serialize_preferences($(".NB-tab-blurblog")); data['custom_bgcolor'] = $('.NB-profile-blurblog-color.NB-active', this.$modal).data('color'); - this.model.save_blurblog_settings(data, _.bind(function() { + this.model.save_blurblog_settings(data, _.bind(function () { this.disable_save_blurblog(); }, this)); this.disable_save_blurblog(); $('.NB-blurblog-save-button', this.$modal).text('Saving...'); }, - - animate_profile_badge: function($badge) { + + animate_profile_badge: function ($badge) { $badge = $('table', $badge) || $('.NB-friends-findfriends-profile .NB-profile-badge table', this.$modal); - _.delay(_.bind(function() { + _.delay(_.bind(function () { $badge.css('backgroundColor', 'white').animate({ 'backgroundColor': 'gold' }, { 'queue': false, 'duration': 600, 'easing': 'linear', - 'complete': function() { + 'complete': function () { $badge.animate({ 'backgroundColor': 'white' }, { @@ -458,29 +459,29 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { } }); }, this), 800); - $badge.closest('.NB-tab').scrollTo(0, { + $badge.closest('.NB-tab').scrollTo(0, { duration: 1000, - axis: 'y', - easing: 'easeInOutQuint', - offset: 0, + axis: 'y', + easing: 'easeInOutQuint', + offset: 0, queue: false }); }, - - set_active_color: function($color) { + + set_active_color: function ($color) { $('.NB-profile-blurblog-color.NB-active', this.$modal).removeClass('NB-active'); $color.addClass('NB-active'); this.enable_save_blurblog(); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-tab' }, function ($t, $p) { e.preventDefault(); var newtab; if ($t.hasClass('NB-modal-tab-profile')) { @@ -489,57 +490,57 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { newtab = 'blurblog'; } self.switch_tab(newtab); - }); - $.targetIs(e, { tagSelector: '.NB-profile-save-button' }, function($t, $p) { + }); + $.targetIs(e, { tagSelector: '.NB-profile-save-button' }, function ($t, $p) { e.preventDefault(); - + self.save_profile(); }); - $.targetIs(e, { tagSelector: '.NB-blurblog-save-button' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-blurblog-save-button' }, function ($t, $p) { e.preventDefault(); - + self.save_blurblog(); }); - $.targetIs(e, { tagSelector: '.NB-account-link' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-account-link' }, function ($t, $p) { e.preventDefault(); - + self.close_and_load_account(); }); - $.targetIs(e, { tagSelector: '.NB-friends-link' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-friends-link' }, function ($t, $p) { e.preventDefault(); - + self.close_and_load_friends(); }); - $.targetIs(e, { tagSelector: '.NB-profile-blurblog-color' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-profile-blurblog-color' }, function ($t, $p) { e.preventDefault(); self.set_active_color($t); }); - $.targetIs(e, { tagSelector: '.NB-premium-link' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-premium-link' }, function ($t, $p) { e.preventDefault(); self.close_and_load_feedchooser(); }); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - $.targetIs(e, { tagSelector: '.NB-photo-upload-file' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-photo-upload-file' }, function ($t, $p) { e.preventDefault(); - + self.handle_photo_upload(); }); }, - - handle_cancel: function() { + + handle_cancel: function () { var $cancel = $('.NB-modal-cancel', this.$modal); - - $cancel.click(function(e) { + + $cancel.click(function (e) { e.preventDefault(); $.modal.close(); }); }, - - handle_profile_counts: function() { - var focus = function(e) { + + handle_profile_counts: function () { + var focus = function (e) { var $input = $(e.currentTarget); var $count = $input.next('.NB-count').eq(0); var count = parseInt($input.data('max'), 10) - $input.val().length; @@ -551,15 +552,15 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { .delegate('input[type=text]', 'keyup', focus) .delegate('input[type=text]', 'keydown', focus) .delegate('input[type=text]', 'change', focus) - .delegate('input[type=text]', 'blur', function(e) { - var $input = $(e.currentTarget); - var $count = $input.next('.NB-count').eq(0); - $count.hide(); - }); + .delegate('input[type=text]', 'blur', function (e) { + var $input = $(e.currentTarget); + var $count = $input.next('.NB-count').eq(0); + $count.hide(); + }); }, - - - handle_photo_upload: function() { + + + handle_photo_upload: function () { var self = this; var $loading = $('.NB-modal-loading', this.$modal); var $error = $('.NB-photo-upload-error', this.$modal); @@ -572,7 +573,7 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { url: NEWSBLUR.URLs['upload-avatar'], type: 'POST', dataType: 'json', - success: _.bind(function(data, status) { + success: _.bind(function (data, status) { if (data.code < 0) { this.error_uploading_photo(); } else { @@ -592,60 +593,60 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, { if (window.FormData) { var formData = new FormData($file.closest('form')[0]); params['data'] = formData; - + $.ajax(params); } else { // IE9 has no FormData params['secureuri'] = false; params['fileElementId'] = 'NB-photo-upload-file'; params['dataType'] = 'json'; - + $.ajaxFileUpload(params); } - + $file.replaceWith($file.clone()); - + return false; }, - - error_uploading_photo: function() { + + error_uploading_photo: function () { var $loading = $('.NB-modal-loading', this.$modal); var $error = $('.NB-photo-upload-error', this.$modal); - + $loading.removeClass('NB-active'); $error.text("There was a problem uploading your photo."); $error.slideDown(300); }, - - delegate_change: function() { + + delegate_change: function () { $('.NB-tab-profile', this.$modal).delegate('input[type=radio],input[type=checkbox],select', 'change', _.bind(this.enable_save_profile, this)); $('.NB-tab-profile', this.$modal).delegate('input[type=text]', 'keydown', _.bind(this.enable_save_profile, this)); $('.NB-tab-blurblog', this.$modal).delegate('input[type=text],textarea', 'keydown', _.bind(this.enable_save_blurblog, this)); $('.NB-tab-blurblog', this.$modal).delegate('input,textarea', 'change', _.bind(this.enable_save_blurblog, this)); }, - - enable_save_profile: function() { + + enable_save_profile: function () { $('.NB-profile-save-button', this.$modal) .removeClass('NB-disabled') .text('Save My Profile'); }, - - enable_save_blurblog: function() { + + enable_save_blurblog: function () { $('.NB-blurblog-save-button', this.$modal) .removeClass('NB-disabled') .text('Save My Blurblog Settings'); }, - - disable_save_profile: function() { + + disable_save_profile: function () { $('.NB-profile-save-button', this.$modal) .addClass('NB-disabled') .text('Saved!'); }, - - disable_save_blurblog: function() { + + disable_save_blurblog: function () { $('.NB-blurblog-save-button', this.$modal) .addClass('NB-disabled') .text('Saved!'); } - + }); diff --git a/media/js/newsblur/reader/reader_recommend_feed.js b/media/js/newsblur/reader/reader_recommend_feed.js index 0a0b65d38e..8f79796f44 100644 --- a/media/js/newsblur/reader/reader_recommend_feed.js +++ b/media/js/newsblur/reader/reader_recommend_feed.js @@ -1,6 +1,6 @@ -NEWSBLUR.ReaderRecommendFeed = function(feed_id, options) { +NEWSBLUR.ReaderRecommendFeed = function (feed_id, options) { var defaults = {}; - + this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.assets; this.feed_id = feed_id; @@ -14,24 +14,24 @@ NEWSBLUR.ReaderRecommendFeed.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderRecommendFeed.prototype.constructor = NEWSBLUR.ReaderRecommendFeed; _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, { - - runner: function() { + + runner: function () { this.make_modal(); this.open_modal(); - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.get_tagline(); }, this), 50); - + this.$modal.bind('click', $.rescope(this.handle_click, this)); this.$modal.bind('change', $.rescope(this.handle_change, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-recommend NB-modal' }, [ - $.make('div', { className: 'NB-modal-feed-chooser-container'}, [ - this.make_feed_chooser({skip_starred: true, skip_social: true}) + $.make('div', { className: 'NB-modal-feed-chooser-container' }, [ + this.make_feed_chooser({ skip_starred: true, skip_social: true }) ]), $.make('div', { className: 'NB-modal-loading' }), $.make('h2', { className: 'NB-modal-title' }, [ @@ -53,8 +53,8 @@ _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, { $.make('textarea', { className: 'NB-modal-recommend-tagline' }) ]), $.make('div', { className: 'NB-modal-recommend-credit' }, [ - '» Want credit? Enter your Twitter username: ', - $.make('input', { className: 'NB-input NB-modal-recommend-twitter' }) + '» Want credit? Enter your Twitter username: ', + $.make('input', { className: 'NB-input NB-modal-recommend-twitter' }) ]), $.make('form', { className: 'NB-recommend-form' }, [ $.make('div', { className: 'NB-modal-submit' }, [ @@ -65,19 +65,19 @@ _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, { ]) ]); }, - - get_tagline: function() { + + get_tagline: function () { var $loading = $('.NB-modal-loading', this.$modal); $loading.addClass('NB-active'); - + this.model.get_feed_recommendation_info(this.feed_id, _.bind(this.populate_tagline, this)); }, - - populate_tagline: function(data) { + + populate_tagline: function (data) { var $submit = $('.NB-modal-submit-save', this.$modal); var $loading = $('.NB-modal-loading', this.$modal); $loading.removeClass('NB-active'); - + $('.NB-modal-recommend-tagline', this.$modal).val(data.tagline); if (data.previous_recommendation) { @@ -86,10 +86,10 @@ _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, { $submit.removeClass('NB-disabled').val('Recommend Site'); } }, - - open_modal: function() { + + open_modal: function () { var self = this; - + this.$modal.modal({ 'minWidth': 600, 'maxWidth': 600, @@ -98,18 +98,18 @@ _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, { dialog.overlay.fadeIn(200, function () { dialog.container.fadeIn(200); dialog.data.fadeIn(200); - setTimeout(function() { + setTimeout(function () { $(window).resize(); }); }); }, - 'onShow': function(dialog) { + 'onShow': function (dialog) { $('#simplemodal-container').corner('6px'); }, - 'onClose': function(dialog) { + 'onClose': function (dialog) { dialog.data.hide().empty().remove(); dialog.container.hide().empty().remove(); - dialog.overlay.fadeOut(200, function() { + dialog.overlay.fadeOut(200, function () { dialog.overlay.empty().remove(); $.modal.close(); }); @@ -117,49 +117,49 @@ _.extend(NEWSBLUR.ReaderRecommendFeed.prototype, { } }); }, - - save : function() { + + save: function () { var $submit = $('.NB-modal-submit-save', this.$modal); $submit.addClass('NB-disabled').val('Saving...'); - + this.model.save_recommended_site({ - feed_id : this.feed_id, - tagline : $('.NB-modal-recommend-tagline').val(), - twitter : $('.NB-modal-recommend-twitter').val() - }, function() { + feed_id: this.feed_id, + tagline: $('.NB-modal-recommend-tagline').val(), + twitter: $('.NB-modal-recommend-twitter').val() + }, function () { $.modal.close(); }); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-cancel' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-cancel' }, function ($t, $p) { e.preventDefault(); $.modal.close(); }); - - $.targetIs(e, { tagSelector: '.NB-modal-submit-save' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-submit-save' }, function ($t, $p) { e.preventDefault(); if (!$t.hasClass('NB-disabled')) { self.save(); } }); }, - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function($t, $p){ + + $.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_tagline(); }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/reader/reader_send_email.js b/media/js/newsblur/reader/reader_send_email.js index 780c42df01..db3e9d6005 100644 --- a/media/js/newsblur/reader/reader_send_email.js +++ b/media/js/newsblur/reader/reader_send_email.js @@ -1,6 +1,6 @@ -NEWSBLUR.ReaderSendEmail = function(story, options) { +NEWSBLUR.ReaderSendEmail = function (story, options) { var defaults = {}; - + _.bindAll(this, 'close', 'save_callback', 'error'); this.options = $.extend({}, defaults, options); @@ -15,47 +15,49 @@ NEWSBLUR.ReaderSendEmail = function(story, options) { NEWSBLUR.ReaderSendEmail.prototype = new NEWSBLUR.Modal; _.extend(NEWSBLUR.ReaderSendEmail.prototype, { - - runner: function() { + + runner: function () { _.bindAll(this, 'save'); - this.options.onOpen = _.bind(function() { + this.options.onOpen = _.bind(function () { $('input[name=to]', this.$modal).focus(); }, this); this.make_modal(); this.open_modal(); this.existing_emails = $.evalJSON($.cookie('NB:email:addresses')) || []; this.autocomplete_emails(); - + if (!NEWSBLUR.Globals.is_authenticated) { - this.save_callback({'code': -1, 'message': 'You must be logged in to send a story over email.'}); + this.save_callback({ 'code': -1, 'message': 'You must be logged in to send a story over email.' }); } - + this.$modal.bind('click', $.rescope(this.handle_click, this)); $('input, textarea', this.$modal).bind('keydown', 'ctrl+return', this.save); $('input, textarea', this.$modal).bind('keydown', 'meta+return', this.save); }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-email NB-modal' }, [ - $.make('span', { className: 'NB-modal-loading NB-spinner'}), - $.make('div', { className: 'NB-modal-error'}), + $.make('span', { className: 'NB-modal-loading NB-spinner' }), + $.make('div', { className: 'NB-modal-error' }), $.make('h2', { className: 'NB-modal-title' }, 'Send Story by Email'), $.make('h2', { className: 'NB-modal-subtitle' }, [ (this.feed && $.make('div', { className: 'NB-modal-email-feed' }, [ - $.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(this.feed) }), - $.make('div', { className: 'NB-modal-feed-title' }, this.feed.get('feed_title')) + $.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(this.feed) }), + $.make('div', { className: 'NB-modal-feed-title' }, this.feed.get('feed_title')) ])), $.make('div', { className: 'NB-modal-email-story-title' }, this.story.story_title), $.make('div', { className: 'NB-modal-email-story-permalink' }, this.story.story_permalink) ]), $.make('div', { className: 'NB-modal-email-to-container' }, [ - $.make('label', { 'for': 'NB-send-email-to' }, [ - ' Recipient emails: ' - ]), - $.make('input', { className: 'NB-input NB-modal-to', name: 'to', id: 'NB-send-email-to', value: - ($.cookie('NB:email:to') || "") }) + $.make('label', { 'for': 'NB-send-email-to' }, [ + ' Recipient emails: ' + ]), + $.make('input', { + className: 'NB-input NB-modal-to', name: 'to', id: 'NB-send-email-to', value: + ($.cookie('NB:email:to') || "") + }) ]), $.make('div', { className: 'NB-modal-email-explanation' }, [ "Add an optional comment to send with the story. The story will be sent below your comment." @@ -64,29 +66,29 @@ _.extend(NEWSBLUR.ReaderSendEmail.prototype, { $.make('textarea', { className: 'NB-modal-email-comments' }) ]), $.make('div', { className: 'NB-modal-email-from-container' }, [ - $.make('div', [ - $.make('label', { 'for': 'NB-send-email-from-name' }, [ - ' Your name: ' - ]), - $.make('input', { className: 'NB-input NB-modal-email-from', name: 'from_name', id: 'NB-send-email-from-name', value: this.model.preference('full_name') || NEWSBLUR.Globals.username || '' }) - ]), - $.make('div', { style: 'margin-top: 8px' }, [ - $.make('label', { 'for': 'NB-send-email-from-email' }, [ - ' Your email: ' + $.make('div', [ + $.make('label', { 'for': 'NB-send-email-from-name' }, [ + ' Your name: ' + ]), + $.make('input', { className: 'NB-input NB-modal-email-from', name: 'from_name', id: 'NB-send-email-from-name', value: this.model.preference('full_name') || NEWSBLUR.Globals.username || '' }) ]), - $.make('input', { className: 'NB-input NB-modal-email-from', name: 'from_email', id: 'NB-send-email-from-email', value: NEWSBLUR.Globals.email || this.model.preference('email') || '' }) - ]), - $.make('div', { style: 'margin-top: 8px' }, [ - $.make('label', { 'for': 'NB-send-email-cc' }, [ - ' CC me: ' + $.make('div', { style: 'margin-top: 8px' }, [ + $.make('label', { 'for': 'NB-send-email-from-email' }, [ + ' Your email: ' + ]), + $.make('input', { className: 'NB-input NB-modal-email-from', name: 'from_email', id: 'NB-send-email-from-email', value: NEWSBLUR.Globals.email || this.model.preference('email') || '' }) ]), - $.make('div', { className: 'NB-modal-email-cc-wrapper' }, [ - $.make('input', { className: 'NB-modal-email-cc', name: 'email_cc', id: 'NB-send-email-cc', type: "checkbox", checked: this.model.preference('email_cc') }), + $.make('div', { style: 'margin-top: 8px' }, [ $.make('label', { 'for': 'NB-send-email-cc' }, [ - "Yes, send me a copy of this email" + ' CC me: ' + ]), + $.make('div', { className: 'NB-modal-email-cc-wrapper' }, [ + $.make('input', { className: 'NB-modal-email-cc', name: 'email_cc', id: 'NB-send-email-cc', type: "checkbox", checked: this.model.preference('email_cc') }), + $.make('label', { 'for': 'NB-send-email-cc' }, [ + "Yes, send me a copy of this email" + ]) ]) ]) - ]) ]), $.make('form', { className: 'NB-recommend-form' }, [ $.make('div', { className: 'NB-modal-submit' }, [ @@ -98,16 +100,16 @@ _.extend(NEWSBLUR.ReaderSendEmail.prototype, { ]) ]); }, - - save: function(e) { - var from_name = $('input[name=from_name]', this.$modal).val(); + + save: function (e) { + var from_name = $('input[name=from_name]', this.$modal).val(); var from_email = $('input[name=from_email]', this.$modal).val(); - var to = $('input[name=to]', this.$modal).val(); - var email_cc = $('input[name=email_cc]', this.$modal).is(":checked"); - var comments = $('textarea', this.$modal).val(); - var $save = $('input[type=submit]', this.$modal); - var $error = $('.NB-modal-error', this.$modal); - + var to = $('input[name=to]', this.$modal).val(); + var email_cc = $('input[name=email_cc]', this.$modal).is(":checked"); + var comments = $('textarea', this.$modal).val(); + var $save = $('input[type=submit]', this.$modal); + var $error = $('.NB-modal-error', this.$modal); + $error.hide(); $save.addClass('NB-disabled').val('Sending...'); $('.NB-modal-loading', this.$modal).addClass('NB-active'); @@ -115,38 +117,38 @@ _.extend(NEWSBLUR.ReaderSendEmail.prototype, { this.model.preference('email', from_email); this.model.preference('email_cc', email_cc); $('.NB-error', this.$modal).hide(); - + this.model.send_story_email({ - story_id : this.story.id, - feed_id : this.feed_id, - from_name : from_name, - from_email : from_email, - email_cc : email_cc, - to : to, - comments : comments + story_id: this.story.id, + feed_id: this.feed_id, + from_name: from_name, + from_email: from_email, + email_cc: email_cc, + to: to, + comments: comments }, this.save_callback, this.error); }, - - save_callback: function(data) { + + save_callback: function (data) { var $save = $('input[type=submit]', this.$modal); if (!data || data.code < 0) { - $('.NB-error', this.$modal).html(data.message).fadeIn(500); - $('.NB-modal-loading', this.$modal).removeClass('NB-active'); - $save.removeClass('NB-disabled').val('Send this story'); + $('.NB-error', this.$modal).html(data.message).fadeIn(500); + $('.NB-modal-loading', this.$modal).removeClass('NB-active'); + $save.removeClass('NB-disabled').val('Send this story'); } else { - $save.val('Sent!'); - $.cookie('NB:email:to', $('input[name=to]', this.$modal).val()); - var emails = $('input[name=to]', this.$modal).val(); - emails = emails.replace(/[, ]+/g, ' ').split(' '); - emails = _.uniq(this.existing_emails.concat(emails)); - emails = _.map(emails, function(e) { return _.string.trim(e); }); - emails = _.compact(emails); - $.cookie('NB:email:addresses', $.toJSON(emails), {expires: 365*10}); - this.close(); + $save.val('Sent!'); + $.cookie('NB:email:to', $('input[name=to]', this.$modal).val()); + var emails = $('input[name=to]', this.$modal).val(); + emails = emails.replace(/[, ]+/g, ' ').split(' '); + emails = _.uniq(this.existing_emails.concat(emails)); + emails = _.map(emails, function (e) { return _.string.trim(e); }); + emails = _.compact(emails); + $.cookie('NB:email:addresses', $.toJSON(emails), { expires: 365 * 10 }); + this.close(); } }, - - error: function(data) { + + error: function (data) { var $error = $('.NB-error', this.$modal); var $save = $('input[type=submit]', this.$modal); $error.show(); @@ -154,19 +156,19 @@ _.extend(NEWSBLUR.ReaderSendEmail.prototype, { if (!data || !data.message) { $error.text("There was a issue on the backend with sending your email. Sorry about this! It has been noted and will be fixed soon. You should probably send this manually now."); } else { - $error.html(data.message).fadeIn(500); + $error.html(data.message).fadeIn(500); } $save.removeClass('NB-disabled').val('Send this story'); $('.NB-modal-loading', this.$modal).removeClass('NB-active'); - + this.resize(); }, - - open_email_client: function() { - var from_name = $('input[name=from_name]', this.$modal).val(); + + open_email_client: function () { + var from_name = $('input[name=from_name]', this.$modal).val(); var from_email = $('input[name=from_email]', this.$modal).val(); - var to = $('input[name=to]', this.$modal).val(); - var comments = $('textarea', this.$modal).val(); + var to = $('input[name=to]', this.$modal).val(); + var comments = $('textarea', this.$modal).val(); var url = [ 'mailto:', @@ -188,23 +190,23 @@ _.extend(NEWSBLUR.ReaderSendEmail.prototype, { ].join(''); window.open(url); }, - - autocomplete_emails: function() { + + autocomplete_emails: function () { var $to = $('input[name=to]', this.$modal); var existing_emails = this.existing_emails; - - var split = function(val) { - return val.split( /,\s*/ ); + + var split = function (val) { + return val.split(/,\s*/); }; - var extractLast = function(term) { - return split( term ).pop(); + var extractLast = function (term) { + return split(term).pop(); }; $to // don't navigate away from the field on tab when selecting an item - .bind( "keydown", function( event ) { - if ( event.keyCode === $.ui.keyCode.TAB && - $( this ).data( "ui-autocomplete" ).menu.active ) { + .bind("keydown", function (event) { + if (event.keyCode === $.ui.keyCode.TAB && + $(this).data("ui-autocomplete").menu.active) { event.preventDefault(); } }) @@ -212,49 +214,49 @@ _.extend(NEWSBLUR.ReaderSendEmail.prototype, { delay: 0, minLength: 0, appendTo: '.NB-modal-email', - source: function( request, response ) { + source: function (request, response) { // delegate back to autocomplete, but extract the last term console.log(["autocomplete", request, request.term, existing_emails]); - response( $.ui.autocomplete.filter( - existing_emails, extractLast( request.term ) ) ); + response($.ui.autocomplete.filter( + existing_emails, extractLast(request.term))); }, - focus: function() { + focus: function () { // prevent value inserted on focus return false; }, - select: function( event, ui ) { - var terms = split( this.value ); + select: function (event, ui) { + var terms = split(this.value); // remove the current input terms.pop(); // add the selected item - terms.push( ui.item.value ); + terms.push(ui.item.value); // add placeholder to get the comma-and-space at the end - terms.push( "" ); - this.value = terms.join( ", " ); + terms.push(""); + this.value = terms.join(", "); return false; } }); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-submit-button' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-modal-submit-button' }, function ($t, $p) { e.preventDefault(); - + self.save(); return false; }); - $.targetIs(e, { tagSelector: '.NB-modal-emailclient' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-modal-emailclient' }, function ($t, $p) { e.preventDefault(); - + self.open_email_client(); return false; }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/reader/reader_social_profile.js b/media/js/newsblur/reader/reader_social_profile.js index b562c4086b..187347fc92 100644 --- a/media/js/newsblur/reader/reader_social_profile.js +++ b/media/js/newsblur/reader/reader_social_profile.js @@ -1,10 +1,10 @@ -NEWSBLUR.ReaderSocialProfile = function(user_id, options) { +NEWSBLUR.ReaderSocialProfile = function (user_id, options) { var defaults = { width: 800 }; - + this.options = $.extend({}, defaults, options); - this.model = NEWSBLUR.assets; + this.model = NEWSBLUR.assets; this.profiles = new NEWSBLUR.Collections.Users(); user_id = parseInt(_.string.ltrim(user_id, 'social:'), 10); this.runner(user_id); @@ -13,10 +13,10 @@ NEWSBLUR.ReaderSocialProfile = function(user_id, options) { NEWSBLUR.ReaderSocialProfile.prototype = new NEWSBLUR.Modal; _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { - - runner: function(user_id) { + + runner: function (user_id) { if (!this.model.user_profiles.find(user_id)) { - this.model.add_user_profiles([{user_id: user_id}]); + this.model.add_user_profiles([{ user_id: user_id }]); } this.profile = this.model.user_profiles.find(user_id).clone(); this.make_modal(); @@ -26,8 +26,8 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { this.profile.bind('change', _.bind(this.populate_friends, this)); this.$modal.bind('click', $.rescope(this.handle_click, this)); }, - - make_modal: function() { + + make_modal: function () { var self = this; this.$profile = new NEWSBLUR.Views.SocialProfileBadge({ model: this.profile, @@ -85,11 +85,11 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { ]) ]); }, - - fetch_profile: function(user_id, callback) { + + fetch_profile: function (user_id, callback) { $('.NB-modal-loading', this.$modal).addClass('NB-active'); - this.model.fetch_user_profile(user_id, _.bind(function(data) { + this.model.fetch_user_profile(user_id, _.bind(function (data) { $('.NB-modal-loading', this.$modal).removeClass('NB-active'); this.profiles = data.profiles; this.activities = data.activities; @@ -102,12 +102,12 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { callback && callback(); }, this)); }, - - populate_friends: function() { + + populate_friends: function () { // NEWSBLUR.log(["populate_friends", this.profile.get('followers_youknow')]); - _.each(['following_youknow', 'following_everybody', 'followers_youknow', 'followers_everybody'], _.bind(function(f) { + _.each(['following_youknow', 'following_everybody', 'followers_youknow', 'followers_everybody'], _.bind(function (f) { var user_ids = this.profile.get(f); - var $f = $('.NB-profile-'+f.replace('_', '-'), this.$modal); + var $f = $('.NB-profile-' + f.replace('_', '-'), this.$modal); $f.html(this.make_profile_badges(user_ids, this.profiles)); $f.closest('fieldset').toggle(!!user_ids.length); }, this)); @@ -115,11 +115,11 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { $('.NB-profile-following-count', this.$modal).text(this.profile.get('following_count')); _.defer(_.bind(this.resize, this)); }, - - populate_activities: function(activities_html) { + + populate_activities: function (activities_html) { var $activities = $('.NB-profile-activities', this.$modal).empty(); var $section = $(".NB-profile-section-activities", this.$modal); - + if (!this.activities.length) { $section.hide(); } else { @@ -128,11 +128,11 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { $activities.html(activities_html); } }, - - load_images_and_resize: function() { + + load_images_and_resize: function () { var $images = $('img', this.$modal); var image_count = $images.length; - $images.on('load', _.bind(function() { + $images.on('load', _.bind(function () { if (image_count > 1) { image_count -= 1; } else { @@ -140,10 +140,10 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { } }, this)); }, - - make_profile_badges: function(user_ids, profiles) { + + make_profile_badges: function (user_ids, profiles) { $('.tipsy').remove(); - var $badges = $.make('div', { className: 'NB-profile-links' }, _.map(user_ids, function(user_id) { + var $badges = $.make('div', { className: 'NB-profile-links' }, _.map(user_ids, function (user_id) { var user = new NEWSBLUR.Models.User(profiles[user_id]); return $.make('div', { className: 'NB-profile-link', title: user.get('username') }, [ $.make('img', { src: user.get('photo_url') }) @@ -159,10 +159,10 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { // =========== // = Actions = // =========== - - open_modal: function(callback) { + + open_modal: function (callback) { var self = this; - + this.$modal.modal({ 'minWidth': this.options.width, 'maxWidth': this.options.width, @@ -170,7 +170,7 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { 'onOpen': function (dialog) { dialog.overlay.fadeIn(200, function () { dialog.container.fadeIn(200); - dialog.data.fadeIn(200, function() { + dialog.data.fadeIn(200, function () { if (self.options.onOpen) { self.options.onOpen(); } @@ -179,16 +179,16 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { self.resize(); }); }, - 'onShow': function(dialog) { + 'onShow': function (dialog) { $('#simplemodal-container').corner('6px'); if (self.options.onShow) { self.options.onShow(); } }, - 'onClose': function(dialog, callback) { + 'onClose': function (dialog, callback) { dialog.data.hide().empty().remove(); dialog.container.hide().empty().remove(); - dialog.overlay.fadeOut(200, function() { + dialog.overlay.fadeOut(200, function () { dialog.overlay.empty().remove(); $.modal.close(callback); }); @@ -196,35 +196,35 @@ _.extend(NEWSBLUR.ReaderSocialProfile.prototype, { } }); }, - + // =========== // = Actions = // =========== - handle_click: function(elem, e) { + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-account-link' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-account-link' }, function ($t, $p) { e.preventDefault(); - + self.close_and_load_account(); }); - $.targetIs(e, { tagSelector: '.NB-profile-link' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-profile-link' }, function ($t, $p) { e.preventDefault(); - + var user_id = $t.data('user_id'); $t.tipsy('hide').tipsy('disable'); self.fetch_profile(user_id); }); - $.targetIs(e, { tagSelector: '.NB-activity-follow' }, function($t, $p) { + $.targetIs(e, { tagSelector: '.NB-activity-follow' }, function ($t, $p) { e.preventDefault(); e.stopPropagation(); - + var user_id = $t.data('userId'); $t.tipsy('hide').tipsy('disable'); self.fetch_profile(user_id); }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/reader/reader_statistics.js b/media/js/newsblur/reader/reader_statistics.js index 371b5746ab..c969612cdc 100644 --- a/media/js/newsblur/reader/reader_statistics.js +++ b/media/js/newsblur/reader/reader_statistics.js @@ -1,9 +1,9 @@ -NEWSBLUR.ReaderStatistics = function(feed_id, options) { +NEWSBLUR.ReaderStatistics = function (feed_id, options) { var defaults = { embedded: false, width: 700 }; - + this.options = $.extend({}, defaults, options); this.model = NEWSBLUR.assets; if (!feed_id) { @@ -11,10 +11,10 @@ NEWSBLUR.ReaderStatistics = function(feed_id, options) { } this.feed_id = feed_id; if (this.options.embedded) { - this.feed = NEWSBLUR.stats_feed; + this.feed = NEWSBLUR.stats_feed; } else { - this.feed = this.model.get_feed(feed_id); - this.feeds = this.model.get_feeds(); + this.feed = this.model.get_feed(feed_id); + this.feeds = this.model.get_feeds(); } this.first_load = true; this.runner(); @@ -24,31 +24,31 @@ NEWSBLUR.ReaderStatistics.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderStatistics.prototype.constructor = NEWSBLUR.ReaderStatistics; _.extend(NEWSBLUR.ReaderStatistics.prototype, { - - runner: function() { + + runner: function () { var self = this; - + this.initialize_feed(this.feed_id); this.make_modal(); if (this.options.embedded) { - $(".NB-embedded-stats").html(this.$modal); + $(".NB-embedded-stats").html(this.$modal); } else { - this.open_modal(); - setTimeout(function() { - self.get_stats(); - }, 50); - - this.$modal.bind('click', $.rescope(this.handle_click, this)); - this.$modal.bind('change', $.rescope(this.handle_change, this)); + this.open_modal(); + setTimeout(function () { + self.get_stats(); + }, 50); + + this.$modal.bind('click', $.rescope(this.handle_click, this)); + this.$modal.bind('change', $.rescope(this.handle_change, this)); } }, - - make_modal: function() { + + make_modal: function () { var self = this; - + this.$modal = $.make('div', { className: 'NB-modal-statistics NB-modal' }, [ - (!this.options.embedded && $.make('div', { className: 'NB-modal-feed-chooser-container'}, [ - this.make_feed_chooser({skip_starred: true, feed_id: this.feed.id}) + (!this.options.embedded && $.make('div', { className: 'NB-modal-feed-chooser-container' }, [ + this.make_feed_chooser({ skip_starred: true, feed_id: this.feed.id }) ])), $.make('div', { className: 'NB-modal-loading' }), (!this.options.embedded && $.make('h2', { className: 'NB-modal-title' }, [ @@ -65,7 +65,7 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { ]), $.make('div', { className: 'NB-modal-statistics-info' }) ]); - + var $stats = this.make_stats({ 'last_update': '', 'next_update': '', @@ -73,23 +73,23 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { }); $('.NB-modal-statistics-info', this.$modal).replaceWith($stats); }, - - get_stats: function() { + + get_stats: function () { var $loading = $('.NB-modal-loading', this.$modal); $loading.addClass('NB-active'); - + var statistics_fn = this.options.social_feed ? this.model.get_social_statistics : this.model.get_feed_statistics; statistics_fn.call(this.model, this.feed_id, $.rescope(this.populate_stats, this)); }, - - populate_stats: function(s, data) { + + populate_stats: function (s, data) { var self = this; - + NEWSBLUR.log(['Stats', data]); - + var $loading = $('.NB-modal-loading', this.$modal); $loading.removeClass('NB-active'); - + var $stats = this.make_stats(data); $('.NB-modal-statistics-info', this.$modal).replaceWith($stats); $(".NB-modal-feed-subscribers", this.$modal).removeClass('NB-hidden').text(Inflector.pluralize(' subscriber', data.num_subscribers, true)); @@ -102,97 +102,97 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { $expires_label.html(""); $expires.html(""); } - setTimeout(function() { + setTimeout(function () { self.make_chart_count(data); self.make_chart_hours(data); self.make_chart_days(data); }, this.first_load ? 200 : 50); - + if (!this.options.embedded) { - setTimeout(function() { - $.modal.impl.resize(self.$modal); - }, 100); + setTimeout(function () { + $.modal.impl.resize(self.$modal); + }, 100); } }, - - make_stats: function(data) { + + make_stats: function (data) { var update_interval = NEWSBLUR.utils.calculate_update_interval(data['update_interval_minutes']); var premium_update_interval = NEWSBLUR.utils.calculate_update_interval(data['premium_update_interval_minutes']); - + var $stats = $.make('div', { className: 'NB-modal-statistics-info' }, [ - (!this.options.social_feed && $.make('div', { className: 'NB-statistics-stat NB-statistics-updates'}, [ - $.make('div', { className: 'NB-statistics-update'}, [ - $.make('div', { className: 'NB-statistics-label' }, 'Last Update'), - $.make('div', { className: 'NB-statistics-count' }, ' ' + (data['last_update'] && (data['last_update'] + ' ago'))) - ]), - $.make('div', { className: 'NB-statistics-update'}, [ - (data['push'] && $.make('div', { className: 'NB-statistics-realtime' }, [ - $.make('div', { className: 'NB-statistics-label' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/realtime_spinner.gif', className: 'NB-statisics-realtime-spinner' }), - 'Real-time' - ]), - $.make('div', { className: 'NB-statistics-count' }, 'Supplemented by checks every ' + update_interval) - ])), - (!data['push'] && $.make('div', [ - $.make('div', { className: 'NB-statistics-label' }, 'Every'), - $.make('div', { className: 'NB-statistics-count' }, update_interval) - ])) - ]), - $.make('div', { className: 'NB-statistics-update'}, [ - $.make('div', { className: 'NB-statistics-label' }, 'Next Update'), - (data.active && $.make('div', { className: 'NB-statistics-count' }, ' ' + (data['next_update'] && ('in ' + data['next_update'])))), - (!data.active && !data.loading && $.make('div', { className: 'NB-statistics-count' }, "Not active")) - ]), - - $.make('div', { className: 'NB-statistics-update'}, [ - $.make('div', { className: 'NB-statistics-label' }, 'Stories in archive'), - (data['archive_count'] && $.make('div', { className: 'NB-statistics-count', title: Inflector.commas(data['fs_size_bytes']) + " bytes" }, ' ' + ((Inflector.commas(data['archive_count'])) + " " + Inflector.pluralize("story", data['archive_count'])))) - ]), - - ((data.average_stories_per_month == 0 || data.stories_last_month == 0) && - data.update_interval_minutes > 60 && - $.make('div', { className: 'NB-statistics-update-explainer' }, [ - $.make('b', 'Why so infrequently?'), - 'This site has published zero stories in the past month or has averaged less than a single story a month. As soon as it starts publishing at least once a month, it will automatically fetch more frequently.' - ])), - (data.errors_since_good && - $.make('div', { className: 'NB-statistics-update-explainer' }, [ - $.make('b', 'Why is the next update not at the normal rate?'), - 'This site has is throwing exceptions and is not in a healthy state. Look at the bottom of this dialog to see the exact status codes for the feed. The more errors for the feed, the longer time taken between fetches.' - ])), - (!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-statistics-premium-stats' }, [ - $.make('div', { className: 'NB-statistics-update'}, [ - $.make('div', { className: 'NB-statistics-label' }, [ - 'If you went ', - $.make('a', { href: '#', className: 'NB-premium-link NB-splash-link' }, 'premium'), - ', ', - $.make('br'), - 'this site would update every' - ]), - $.make('div', { className: 'NB-statistics-count' }, premium_update_interval), + (!this.options.social_feed && $.make('div', { className: 'NB-statistics-stat NB-statistics-updates' }, [ + $.make('div', { className: 'NB-statistics-update' }, [ + $.make('div', { className: 'NB-statistics-label' }, 'Last Update'), + $.make('div', { className: 'NB-statistics-count' }, ' ' + (data['last_update'] && (data['last_update'] + ' ago'))) + ]), + $.make('div', { className: 'NB-statistics-update' }, [ (data['push'] && $.make('div', { className: 'NB-statistics-realtime' }, [ $.make('div', { className: 'NB-statistics-label' }, [ - 'but it wouldn\'t matter because', - $.make('br'), - 'this site is already in real-time' - ]) + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/realtime_spinner.gif', className: 'NB-statisics-realtime-spinner' }), + 'Real-time' + ]), + $.make('div', { className: 'NB-statistics-count' }, 'Supplemented by checks every ' + update_interval) + ])), + (!data['push'] && $.make('div', [ + $.make('div', { className: 'NB-statistics-label' }, 'Every'), + $.make('div', { className: 'NB-statistics-count' }, update_interval) ])) - ]) - ])) + ]), + $.make('div', { className: 'NB-statistics-update' }, [ + $.make('div', { className: 'NB-statistics-label' }, 'Next Update'), + (data.active && $.make('div', { className: 'NB-statistics-count' }, ' ' + (data['next_update'] && ('in ' + data['next_update'])))), + (!data.active && !data.loading && $.make('div', { className: 'NB-statistics-count' }, "Not active")) + ]), + + $.make('div', { className: 'NB-statistics-update' }, [ + $.make('div', { className: 'NB-statistics-label' }, 'Stories in archive'), + (data['archive_count'] && $.make('div', { className: 'NB-statistics-count', title: Inflector.commas(data['fs_size_bytes']) + " bytes" }, ' ' + ((Inflector.commas(data['archive_count'])) + " " + Inflector.pluralize("story", data['archive_count'])))) + ]), + + ((data.average_stories_per_month == 0 || data.stories_last_month == 0) && + data.update_interval_minutes > 60 && + $.make('div', { className: 'NB-statistics-update-explainer' }, [ + $.make('b', 'Why so infrequently?'), + 'This site has published zero stories in the past month or has averaged less than a single story a month. As soon as it starts publishing at least once a month, it will automatically fetch more frequently.' + ])), + (data.errors_since_good && + $.make('div', { className: 'NB-statistics-update-explainer' }, [ + $.make('b', 'Why is the next update not at the normal rate?'), + 'This site has is throwing exceptions and is not in a healthy state. Look at the bottom of this dialog to see the exact status codes for the feed. The more errors for the feed, the longer time taken between fetches.' + ])), + (!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-statistics-premium-stats' }, [ + $.make('div', { className: 'NB-statistics-update' }, [ + $.make('div', { className: 'NB-statistics-label' }, [ + 'If you went ', + $.make('a', { href: '#', className: 'NB-premium-link NB-splash-link' }, 'premium'), + ', ', + $.make('br'), + 'this site would update every' + ]), + $.make('div', { className: 'NB-statistics-count' }, premium_update_interval), + (data['push'] && $.make('div', { className: 'NB-statistics-realtime' }, [ + $.make('div', { className: 'NB-statistics-label' }, [ + 'but it wouldn\'t matter because', + $.make('br'), + 'this site is already in real-time' + ]) + ])) + ]) + ])) ])), - $.make('div', { className: 'NB-statistics-stat NB-statistics-history'}, [ + $.make('div', { className: 'NB-statistics-stat NB-statistics-history' }, [ $.make('div', { className: 'NB-statistics-history-stat' }, [ $.make('div', { className: 'NB-statistics-label' }, 'Stories per month') ]), $.make('canvas', { id: 'NB-statistics-history-count-chart', className: 'NB-statistics-history-count-chart' }) ]), - $.make('div', { className: 'NB-statistics-stat NB-statistics-history'}, [ + $.make('div', { className: 'NB-statistics-stat NB-statistics-history' }, [ $.make('div', { className: 'NB-statistics-history-stat' }, [ $.make('div', { className: 'NB-statistics-label' }, 'Stories per day') ]), $.make('canvas', { id: 'NB-statistics-history-days-chart', className: 'NB-statistics-history-days-chart' }) ]), - $.make('div', { className: 'NB-statistics-stat NB-statistics-history'}, [ + $.make('div', { className: 'NB-statistics-stat NB-statistics-history' }, [ $.make('div', { className: 'NB-statistics-history-stat' }, [ $.make('div', { className: 'NB-statistics-label' }, 'Daily distribution of stories') ]), @@ -204,16 +204,16 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { this.make_classifier_count('title', data.classifier_counts['title']), this.make_classifier_count('feed', data.classifier_counts['feed']) ])), - (!this.options.social_feed && $.make('div', { className: 'NB-statistics-stat NB-statistics-fetches'}, [ - $.make('div', { className: 'NB-statistics-fetches-half'}, [ + (!this.options.social_feed && $.make('div', { className: 'NB-statistics-stat NB-statistics-fetches' }, [ + $.make('div', { className: 'NB-statistics-fetches-half' }, [ $.make('div', { className: 'NB-statistics-label' }, 'Feed Fetch'), $.make('div', this.make_history(data, 'feed_fetch')) ]), - $.make('div', { className: 'NB-statistics-fetches-half'}, [ + $.make('div', { className: 'NB-statistics-fetches-half' }, [ $.make('div', { className: 'NB-statistics-label' }, 'Page Fetch'), $.make('div', this.make_history(data, 'page_fetch')) ]), - $.make('div', { className: 'NB-statistics-fetches-half'}, [ + $.make('div', { className: 'NB-statistics-fetches-half' }, [ $.make('div', { className: 'NB-statistics-label' }, 'Feed Push'), $.make('div', this.make_history(data, 'feed_push')), $.make('div', { className: 'NB-statistics-label NB-statistics-push-expires-label' }, 'Push Expires'), @@ -221,32 +221,32 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { ]) ])) ]); - + return $stats; }, - - make_classifier_count: function(facet, data) { + + make_classifier_count: function (facet, data) { var self = this; if (!data) return; - + var $facets = $.make('div', { className: 'NB-statistics-facets' }, [ $.make('div', { className: 'NB-statistics-facet-title' }, Inflector.pluralize(facet, data.length)) ]); - + var max = 10; - _.each(data, function(v) { + _.each(data, function (v) { if (v.pos > max || v.neg > max) { max = Math.max(v.pos, v.neg); } }); - + var max_width = 100; var multiplier = max_width / parseFloat(max, 10); - var calculate_width = function(count) { + var calculate_width = function (count) { return Math.max(1, multiplier * count); }; - - _.each(data, function(counts) { + + _.each(data, function (counts) { var pos = counts.pos || 0; var neg = counts.neg || 0; var key = counts[facet]; @@ -254,8 +254,8 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { key = [$.make('div', [ $.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(counts['feed_id']) }), $.make('span', { className: 'NB-modal-feed-title' }, counts['feed_title']) - ])]; - } else if (facet == 'feed') { + ])]; + } else if (facet == 'feed') { key = [$.make('div', [ $.make('img', { className: 'NB-modal-feed-image feed_favicon', src: $.favicon(self.feed) }), $.make('span', { className: 'NB-modal-feed-title' }, self.feed.get('feed_title')) @@ -265,36 +265,36 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { var $facet = $.make('div', { className: 'NB-statistics-facet' }, [ (pos && $.make('div', { className: 'NB-statistics-facet-pos' }, [ $.make('div', { className: 'NB-statistics-facet-bar' }).css('width', calculate_width(pos)), - $.make('div', { className: 'NB-statistics-facet-count' }, Inflector.pluralize(' like', pos, true)).css('margin-left', calculate_width(pos)+5) + $.make('div', { className: 'NB-statistics-facet-count' }, Inflector.pluralize(' like', pos, true)).css('margin-left', calculate_width(pos) + 5) ])), (neg && $.make('div', { className: 'NB-statistics-facet-neg' }, [ $.make('div', { className: 'NB-statistics-facet-bar' }).css('width', calculate_width(neg)), - $.make('div', { className: 'NB-statistics-facet-count' }, Inflector.pluralize(' dislike', neg, true)).css('margin-right', calculate_width(neg)+5) + $.make('div', { className: 'NB-statistics-facet-count' }, Inflector.pluralize(' dislike', neg, true)).css('margin-right', calculate_width(neg) + 5) ])), $.make('div', { className: 'NB-statistics-facet-separator' }), $.make('div', { className: 'NB-statistics-facet-name' }, key) ]); $facets.append($facet); }); - + return $facets; }, - - make_history: function(data, fetch_type) { - var fetches = data[fetch_type+'_history']; + + make_history: function (data, fetch_type) { + var fetches = data[fetch_type + '_history']; var $history; - + if (!fetches || !fetches.length) { $history = $.make('div', { className: 'NB-history-empty' }, "Nothing recorded."); } else { - $history = _.map(fetches, function(fetch) { + $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+')')) + (fetch.status_code && $.make('div', { className: 'NB-history-fetch-code' }, ' (' + fetch.status_code + ')')) ]) ]); }); @@ -302,32 +302,32 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { return $history; }, - - make_chart_count: function(data) { - var labels = _.map(data['story_count_history'], function(date) { + + make_chart_count: function (data) { + var labels = _.map(data['story_count_history'], function (date) { var date_matched = date[0].match(/(\d{4})-(\d{1,2})/); - var date = (new Date(parseInt(date_matched[1], 10), parseInt(date_matched[2],10)-1)); + var date = (new Date(parseInt(date_matched[1], 10), parseInt(date_matched[2], 10) - 1)); return NEWSBLUR.utils.shortMonthNames[date.getMonth()] + " " + date.getUTCFullYear(); }); if (labels.length > 16) { var cut_size = Math.round(labels.length / 16.0); - labels = _.map(labels, function(label, c) { + labels = _.map(labels, function (label, c) { if ((c % cut_size) == 0) return label; return ""; }); } - var values = _.map(data['story_count_history'], function(date) { + var values = _.map(data['story_count_history'], function (date) { return date[1]; }); var points = { labels: labels, datasets: [ { - fillColor : "rgba(151,187,205,0.5)", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", - data : values + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + data: values } ] }; @@ -337,17 +337,17 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { $plot.attr('width', width); $plot.attr('height', height); var myLine = new Chart($plot.get(0).getContext("2d")).Line(points, { - scaleLabel : "<%= Math.round(value) %>", + scaleLabel: "<%= Math.round(value) %>", showTooltips: false, scaleBeginAtZero: true }); }, - - make_chart_hours: function(data) { + + make_chart_hours: function (data) { var max_count = _.max(data.story_hours_history); var $chart = $.make('table', [ $.make('tr', { className: 'NB-statistics-history-chart-hours-row' }, [ - _.map(_.range(24), function(hour) { + _.map(_.range(24), function (hour) { var count = data.story_hours_history[hour] || data.story_hours_history["" + hour] || 0; var opacity = 1 - (count * 1.0 / max_count); var theme = NEWSBLUR.assets.theme(); @@ -360,7 +360,7 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { }) ]), $.make('tr', { className: 'NB-statistics-history-chart-hours-text-row' }, [ - _.compact(_.map(_.range(24), function(hour, count) { + _.compact(_.map(_.range(24), function (hour, count) { var am = hour < 12; if (hour == 0) hour = 12; var hour_name = am ? (hour + "am") : ((hour > 12 ? hour - 12 : hour) + "pm"); @@ -370,24 +370,24 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { })) ]) ]); - + $(".NB-statistics-history-hours-chart", this.$modal).html($chart); }, - - make_chart_days: function(data) { + + make_chart_days: function (data) { var labels = NEWSBLUR.utils.dayNames; - var values = _.map(_.range(7), function(day) { + var values = _.map(_.range(7), function (day) { return data['story_days_history'][day] || 0; }); var points = { labels: labels, datasets: [ { - fillColor : "rgba(151,187,205,0.5)", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", - data : values + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + data: values } ] }; @@ -399,39 +399,39 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, { var myLine = new Chart($plot.get(0).getContext("2d")).Radar(points, { scaleShowLabelBackdrop: false, - showTooltips: false, - scaleFontSize: 16 + showTooltips: false, + scaleFontSize: 16 }); }, - - close_and_load_premium: function() { - this.close(function() { - NEWSBLUR.reader.open_feedchooser_modal(); - }); + + close_and_load_premium: function () { + this.close(function () { + NEWSBLUR.reader.open_feedchooser_modal(); + }); }, - + // =========== // = Actions = // =========== - - handle_change: function(elem, e) { + + handle_change: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function($t, $p){ + + $.targetIs(e, { tagSelector: '.NB-modal-feed-chooser' }, function ($t, $p) { var feed_id = $t.val().replace('feed:', ''); self.first_load = false; self.initialize_feed(feed_id); self.get_stats(); }); }, - - handle_click: function(elem, e) { + + handle_click: function (elem, e) { var self = this; - - $.targetIs(e, { tagSelector: '.NB-premium-link' }, function($t, $p) { + + $.targetIs(e, { tagSelector: '.NB-premium-link' }, function ($t, $p) { e.preventDefault(); self.close_and_load_premium(); }); } - + }); diff --git a/media/js/newsblur/reader/reader_taskbar_info.js b/media/js/newsblur/reader/reader_taskbar_info.js index 542cd2cfa2..94f586e1ec 100644 --- a/media/js/newsblur/reader/reader_taskbar_info.js +++ b/media/js/newsblur/reader/reader_taskbar_info.js @@ -1,80 +1,80 @@ NEWSBLUR.Views.ReaderTaskbarInfo = Backbone.View.extend({ - + className: 'NB-taskbar-info', - - initialize: function() { + + initialize: function () { _.bindAll(this, 'show_stories_error'); }, - - render: function() { + + render: function () { NEWSBLUR.reader.$s.$story_taskbar.append(this.$el); return this; }, - - center: function(force) { + + center: function (force) { var count_width = this.$el.width(); var left_buttons_offset = $('.NB-taskbar-view').outerWidth(true); var right_buttons_offset = $(".NB-taskbar-options-container").position().left; var usable_space = right_buttons_offset - left_buttons_offset; var left = (usable_space / 2) - (count_width / 2) + left_buttons_offset; // console.log(["Taskbar info center", count_width, left, left_buttons_offset, right_buttons_offset, usable_space]); - + if (!force && count_width + 12 > usable_space) { this.$el.hide(); } else { this.$el.show(); } - - this.$el.css({'left': left}); + + this.$el.css({ 'left': left }); }, - + // ================= // = Story loading = // ================= - - show_stories_progress_bar: function(feeds_loading, message) { + + show_stories_progress_bar: function (feeds_loading, message) { message = message || "Fetching stories"; var $progress = $.make('div', { className: 'NB-river-progress' }, [ $.make('div', { className: 'NB-river-progress-text' }), $.make('div', { className: 'NB-river-progress-bar' }) - ]).css({'opacity': 0}); - + ]).css({ 'opacity': 0 }); + this.$el.html($progress); this.center(); - - $progress.animate({'opacity': 1}, {'duration': 500, 'queue': false}); - + + $progress.animate({ 'opacity': 1 }, { 'duration': 500, 'queue': false }); + var $bar = $('.NB-river-progress-bar', $progress); var unreads; if (feeds_loading) unreads = feeds_loading; else unreads = NEWSBLUR.reader.get_total_unread_count(false) / 10; NEWSBLUR.reader.animate_progress_bar($bar, unreads / 10); - + $('.NB-river-progress-text', $progress).text(message); }, - - hide_stories_progress_bar: function(callback) { + + hide_stories_progress_bar: function (callback) { var $progress = this.$('.NB-river-progress'); - $progress.stop().animate({'opacity': 0}, { - 'duration': 250, - 'queue': false, - 'complete': function() { - $progress.remove(); - if (callback) callback(); - } + $progress.stop().animate({ 'opacity': 0 }, { + 'duration': 250, + 'queue': false, + 'complete': function () { + $progress.remove(); + if (callback) callback(); + } }); }, - - show_stories_error: function(data, message) { + + show_stories_error: function (data, message) { NEWSBLUR.log(["show_stories_error", data]); this.hide_stories_progress_bar(); - + NEWSBLUR.app.original_tab_view.iframe_not_busting(); - + if (!message || message == 'error') { message = "Oh no!
There was an error!"; } - + if (data && data.status) { if (data.status == 502) { message = "NewsBlur is down right now.
Try again soon."; @@ -90,30 +90,30 @@ NEWSBLUR.Views.ReaderTaskbarInfo = Backbone.View.extend({ } var type = data.proxied_https ? 'proxy' : 'feed'; - var $error = $.make('div', { className: 'NB-feed-error NB-feed-error-type-'+type }, [ + var $error = $.make('div', { className: 'NB-feed-error NB-feed-error-type-' + type }, [ $.make('div', { className: 'NB-feed-error-icon' }), $.make('div', { className: 'NB-feed-error-text' }, message) - ]).css({'opacity': 0}); - + ]).css({ 'opacity': 0 }); + this.$el.html($error); this.center(true); - - $error.animate({'opacity': 1}, {'duration': 500, 'queue': false}); + + $error.animate({ 'opacity': 1 }, { 'duration': 500, 'queue': false }); }, - - hide_stories_error: function() { + + hide_stories_error: function () { var $error = this.$('.NB-feed-error'); - $error.animate({'opacity': 0}, { - 'duration': 250, - 'queue': false, - 'complete': function() { - $error.remove(); - } + $error.animate({ 'opacity': 0 }, { + 'duration': 250, + 'queue': false, + 'complete': function () { + $error.remove(); + } }); }, - - show_maintenance_page: function() { - NEWSBLUR.reader.switch_taskbar_view('page', {skip_save_type: 'maintenance'}); + + show_maintenance_page: function () { + NEWSBLUR.reader.switch_taskbar_view('page', { skip_save_type: 'maintenance' }); } - + }); diff --git a/media/js/newsblur/reader/reader_tutorial.js b/media/js/newsblur/reader/reader_tutorial.js index 4d6f9a5e23..9acd217722 100644 --- a/media/js/newsblur/reader/reader_tutorial.js +++ b/media/js/newsblur/reader/reader_tutorial.js @@ -1,521 +1,521 @@ -NEWSBLUR.ReaderTutorial = function(options) { - var defaults = {}; - - _.bindAll(this, 'close'); - - this.options = $.extend({ - 'page_number': 1 - }, defaults, options); - this.model = NEWSBLUR.assets; - - this.page_number = this.options.page_number; - this.slider_value = 0; - this.intervals = {}; - this.runner(); +NEWSBLUR.ReaderTutorial = function (options) { + var defaults = {}; + + _.bindAll(this, 'close'); + + this.options = $.extend({ + 'page_number': 1 + }, defaults, options); + this.model = NEWSBLUR.assets; + + this.page_number = this.options.page_number; + this.slider_value = 0; + this.intervals = {}; + this.runner(); }; NEWSBLUR.ReaderTutorial.prototype = new NEWSBLUR.Modal; NEWSBLUR.ReaderTutorial.prototype.constructor = NEWSBLUR.ReaderTutorial; _.extend(NEWSBLUR.ReaderTutorial.prototype, { - - TITLES: [ - 'Learn to use NewsBlur', - 'Three Site Views', - 'Training the Intelligence', - 'Tips and Tricks', - 'Feedback and Open Source' - ], - - runner: function() { - this.make_modal(); - this.open_modal(); - this.page(1); - this.load_newsblur_blog_info(); - this.load_intelligence_slider(); - this.load_tips(); - this.make_story_titles(); - - this.$modal.bind('click', $.rescope(this.handle_click, this)); - }, - - load_newsblur_blog_info: function() { - this.model.load_tutorial({}, _.bind(function(data) { - this.newsblur_feed = data.newsblur_feed; - $('.NB-javascript', this.$modal).removeClass('NB-javascript'); - }, this)); - }, - - make_modal: function() { - var self = this; - - this.$modal = $.make('div', { className: 'NB-modal-tutorial NB-modal' }, [ - $.make('span', { className: 'NB-modal-loading NB-spinner'}), - $.make('div', { className: 'NB-modal-page' }), - $.make('h2', { className: 'NB-modal-title' }, [ - $.make('div', { className: 'NB-icon' }), - $.make('span', 'Tips & Tricks'), - $.make('div', { className: 'NB-icon-dropdown' }) - ]), - $.make('div', { className: 'NB-page NB-page-1' }, [ - $.make('h4', 'NewsBlur is a visual feed reader with intelligence.'), - $.make('div', 'You\'ll figure out much of NewsBlur by playing around and trying things out. This tutorial is here to quickly offer a foundation.'), - $.make('h4', 'This tutorial covers:'), - $.make('ul', [ - $.make('li', [ - $.make('div', { className: 'NB-right' }, 'Page 2'), - 'Using the three views (Original, Feed, and Story)' - ]), - $.make('li', [ - $.make('div', { className: 'NB-right' }, 'Page 3'), - 'Training and filtering stories' - ]), - $.make('li', [ - $.make('div', { className: 'NB-right' }, 'Page 4'), - 'Tips and tricks that may not be obvious' + + TITLES: [ + 'Learn to use NewsBlur', + 'Three Site Views', + 'Training the Intelligence', + 'Tips and Tricks', + 'Feedback and Open Source' + ], + + runner: function () { + this.make_modal(); + this.open_modal(); + this.page(1); + this.load_newsblur_blog_info(); + this.load_intelligence_slider(); + this.load_tips(); + this.make_story_titles(); + + this.$modal.bind('click', $.rescope(this.handle_click, this)); + }, + + load_newsblur_blog_info: function () { + this.model.load_tutorial({}, _.bind(function (data) { + this.newsblur_feed = data.newsblur_feed; + $('.NB-javascript', this.$modal).removeClass('NB-javascript'); + }, this)); + }, + + make_modal: function () { + var self = this; + + this.$modal = $.make('div', { className: 'NB-modal-tutorial NB-modal' }, [ + $.make('span', { className: 'NB-modal-loading NB-spinner' }), + $.make('div', { className: 'NB-modal-page' }), + $.make('h2', { className: 'NB-modal-title' }, [ + $.make('div', { className: 'NB-icon' }), + $.make('span', 'Tips & Tricks'), + $.make('div', { className: 'NB-icon-dropdown' }) + ]), + $.make('div', { className: 'NB-page NB-page-1' }, [ + $.make('h4', 'NewsBlur is a visual feed reader with intelligence.'), + $.make('div', 'You\'ll figure out much of NewsBlur by playing around and trying things out. This tutorial is here to quickly offer a foundation.'), + $.make('h4', 'This tutorial covers:'), + $.make('ul', [ + $.make('li', [ + $.make('div', { className: 'NB-right' }, 'Page 2'), + 'Using the three views (Original, Feed, and Story)' + ]), + $.make('li', [ + $.make('div', { className: 'NB-right' }, 'Page 3'), + 'Training and filtering stories' + ]), + $.make('li', [ + $.make('div', { className: 'NB-right' }, 'Page 4'), + 'Tips and tricks that may not be obvious' + ]), + $.make('li', [ + $.make('div', { className: 'NB-right' }, 'Page 5'), + 'Feedback, open source, the blog, and twitter' + ]) + ]), + $.make('h4', 'Why you should use NewsBlur:'), + $.make('ul', [ + $.make('li', [ + 'This is a free service that is always getting better.' + ]), + $.make('li', [ + 'See the original site and read stories as the author intended.' + ]), + $.make('li', [ + 'Spend less time as NewsBlur filters the stories for you.' + ]) + ]) + ]), + $.make('div', { className: 'NB-page NB-page-2' }, [ + $.make('h4', 'Read your sites with three different views:'), + $.make('div', { className: 'NB-tutorial-view' }, [ + $.make('div', { className: 'NB-tutorial-view-title' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_tile.png' }), + 'Original' + ]), + $.make('img', { className: 'NB-tutorial-view-image', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_view_original.png' }), + $.make('span', 'The site itself.') + ]), + $.make('div', { className: 'NB-tutorial-view' }, [ + $.make('div', { className: 'NB-tutorial-view-title' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_list.png' }), + 'Feed' + ]), + $.make('img', { className: 'NB-tutorial-view-image', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_view_feed.png' }), + $.make('span', 'All feed stories.') + ]), + $.make('div', { className: 'NB-tutorial-view' }, [ + $.make('div', { className: 'NB-tutorial-view-title' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_gallery.png' }), + 'Story' + ]), + $.make('img', { className: 'NB-tutorial-view-image', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_view_story.png' }), + $.make('span', 'Story click-through.') + ]), + $.make('ul', [ + $.make('li', [ + 'The view you choose is saved per-site, so you can mix-and-match.' + ]), + $.make('li', [ + 'Double-click story titles to temporarily open a story in the Story view.' + ]), + $.make('li', [ + 'In the Original view, if a story is not found, it will temporarily open in the Feed view.' + ]), + $.make('li', [ + 'Much about these views can be customized under Preferences.' + ]) + ]) + ]), + $.make('div', { className: 'NB-page NB-page-3' }, [ + $.make('h4', 'NewsBlur works best when you use intelligence classifiers.'), + $.make('ul', [ + $.make('li', { className: 'NB-tutorial-train-1' }, [ + $.make('b', 'First: Train stories and sites.'), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_train_feed.png' }), + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_train_story.png' }) + ]), + $.make('li', [ + $.make('b', 'Second: The intelligence slider filters stories based on training.'), + $.make('div', { className: 'NB-tutorial-stories', id: 'story_titles' }), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-focus.svg' }), + 'Focus stories are stories you like', + $.make('br'), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-unread.svg' }), + 'Unread stories include both focus and unread stories', + $.make('br'), + $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-hidden.svg' }), + 'Hidden stories are filtered out' + ]), + $.make('li', [ + $.make('a', { href: '/faq#intelligence', target: "_blank", className: 'NB-splash-link' }, 'Read more about how Intelligence works in the FAQ') + ]) + ]) + ]), + $.make('div', { className: 'NB-page NB-page-4' }, [ + $.make('h4', 'Here are a few tricks that may enhance your experience:'), + $.make('ul', [ + $.make('li', { className: 'NB-tutorial-tips-sites' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_sites.png' }), + $.make('div', [ + 'Click on the sites count at the top of the sidebar to hide sites with no unread stories.' + ]) + ]), + $.make('li', [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_instafetch.png' }), + 'Instantly refresh a site by right-clicking on it and selecting ', + $.make('b', 'Insta-fetch stories.') + ]), + $.make('li', [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_stories.png' }), + 'Click the arrow next to sites and stories to open up a menu.' + ]), + $.make('li', { className: 'NB-tutorial-tips-train' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_train.png' }), + $.make('div', 'Train sites in the Feed view by clicking directly on the tags and authors. The tags will rotate color between like and dislike.') + ]), + $.make('li', [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_folders.png' }), + 'Folders can be nested inside folders.' + ]), + $.make('li', [ + 'There are more than a dozen keyboard shortcuts you can use:', + $.make('div', { className: 'NB-modal-keyboard' }, [ + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '↓' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'j' + ]) ]), - $.make('li', [ - $.make('div', { className: 'NB-right' }, 'Page 5'), - 'Feedback, open source, the blog, and twitter' + $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Previous story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '↑' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'k' + ]) ]) ]), - $.make('h4', 'Why you should use NewsBlur:'), - $.make('ul', [ - $.make('li', [ - 'This is a free service that is always getting better.' - ]), - $.make('li', [ - 'See the original site and read stories as the author intended.' + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next site'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + '↓' + ]) ]), - $.make('li', [ - 'Spend less time as NewsBlur filters the stories for you.' + $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Prev. site'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + '↑' + ]) ]) - ]) - ]), - $.make('div', { className: 'NB-page NB-page-2' }, [ - $.make('h4', 'Read your sites with three different views:'), - $.make('div', { className: 'NB-tutorial-view' }, [ - $.make('div', { className: 'NB-tutorial-view-title' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_tile.png' }), - 'Original' - ]), - $.make('img', { className: 'NB-tutorial-view-image', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_view_original.png' }), - $.make('span', 'The site itself.') - ]), - $.make('div', { className: 'NB-tutorial-view' }, [ - $.make('div', { className: 'NB-tutorial-view-title' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_list.png' }), - 'Feed' - ]), - $.make('img', { className: 'NB-tutorial-view-image', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_view_feed.png' }), - $.make('span', 'All feed stories.') - ]), - $.make('div', { className: 'NB-tutorial-view' }, [ - $.make('div', { className: 'NB-tutorial-view-title' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/silk/application_view_gallery.png' }), - 'Story' - ]), - $.make('img', { className: 'NB-tutorial-view-image', src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_view_story.png' }), - $.make('span', 'Story click-through.') ]), - $.make('ul', [ - $.make('li', [ - 'The view you choose is saved per-site, so you can mix-and-match.' - ]), - $.make('li', [ - 'Double-click story titles to temporarily open a story in the Story view.' - ]), - $.make('li', [ - 'In the Original view, if a story is not found, it will temporarily open in the Feed view.' - ]), - $.make('li', [ - 'Much about these views can be customized under Preferences.' - ]) - ]) - ]), - $.make('div', { className: 'NB-page NB-page-3' }, [ - $.make('h4', 'NewsBlur works best when you use intelligence classifiers.'), - $.make('ul', [ - $.make('li', { className: 'NB-tutorial-train-1' }, [ - $.make('b', 'First: Train stories and sites.'), - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_train_feed.png' }), - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_train_story.png' }) - ]), - $.make('li', [ - $.make('b', 'Second: The intelligence slider filters stories based on training.'), - $.make('div', { className: 'NB-tutorial-stories', id: 'story_titles' }), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-focus.svg'}), - 'Focus stories are stories you like', - $.make('br'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-unread.svg'}), - 'Unread stories include both focus and unread stories', - $.make('br'), - $.make('img', { className: 'NB-trainer-bullet', src: NEWSBLUR.Globals.MEDIA_URL + '/img/icons/nouns/indicator-hidden.svg'}), - 'Hidden stories are filtered out' - ]), - $.make('li', [ - $.make('a', { href: '/faq#intelligence', target: "_blank", className: 'NB-splash-link' }, 'Read more about how Intelligence works in the FAQ') - ]) - ]) - ]), - $.make('div', { className: 'NB-page NB-page-4' }, [ - $.make('h4', 'Here are a few tricks that may enhance your experience:'), - $.make('ul', [ - $.make('li', { className: 'NB-tutorial-tips-sites' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_sites.png' }), - $.make('div', [ - 'Click on the sites count at the top of the sidebar to hide sites with no unread stories.' + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Switch views'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '←' + ]), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + '→' ]) ]), - $.make('li', [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_instafetch.png' }), - 'Instantly refresh a site by right-clicking on it and selecting ', - $.make('b', 'Insta-fetch stories.') - ]), - $.make('li', [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_stories.png' }), - 'Click the arrow next to sites and stories to open up a menu.' - ]), - $.make('li', { className: 'NB-tutorial-tips-train' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_train.png' }), - $.make('div', 'Train sites in the Feed view by clicking directly on the tags and authors. The tags will rotate color between like and dislike.') - ]), - $.make('li', [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/tutorial_tips_folders.png' }), - 'Folders can be nested inside folders.' - ]), - $.make('li', [ - 'There are more than a dozen keyboard shortcuts you can use:', - $.make('div', { className: 'NB-modal-keyboard' }, [ - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '↓' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'j' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Previous story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '↑' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'k' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next site'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - '↓' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Prev. site'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - '↑' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Switch views'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '←' - ]), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - '→' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open Site'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'enter' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page down'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'space' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page up'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'space' - ]) - ]) - ]), - $.make('div', { className: 'NB-keyboard-group' }, [ - $.make('div', { className: 'NB-keyboard-shortcut' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next Unread Story'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'n' - ]) - ]), - $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ - $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Hide Sidebar'), - $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ - 'shift', - $.make('span', '+'), - 'u' - ]) - ]) - ]) + $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Open Site'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'enter' ]) ]) - ]) - ]), - $.make('div', { className: 'NB-page NB-page-5' }, [ - $.make('h4', 'Stay connected to NewsBlur on Twitter'), - $.make('div', { className: 'NB-tutorial-twitter' }, [ - $.make('a', { className: 'NB-splash-link', href: 'http://twitter.com/samuelclay', target: '_blank' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/static/Samuel%20Clay%20sq.jpg', style: 'border-color: #505050;' }), - $.make('span', '@samuelclay') - ]), - $.make('a', { className: 'NB-splash-link', href: 'http://twitter.com/newsblur', target: '_blank' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/logo_128.png' }), - $.make('span', '@newsblur') - ]) ]), - $.make('h4', { className: 'NB-tutorial-feedback-header' }, 'Community Feedback'), - $.make('ul', [ - $.make('li', [ - $.make('a', { href: 'https://forum.newsblur.com/', className: 'NB-splash-link' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/discourse.png', style: 'vertical-align: middle;margin: -2px 0 0; width: 16px;height: 16px;' }), - ' NewsBlur Support Forum' + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page down'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'space' ]) - ]) - ]), - $.make('h4', { className: 'NB-tutorial-feedback-header' }, [ - 'Open Source Code' - ]), - $.make('ul', [ - $.make('li', [ - $.make('a', { href: 'http://github.com/samuelclay', className: 'NB-splash-link' }, [ - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/howitworks_github.png', style: 'float: right;margin: -68px 12px 0 0' }), - 'NewsBlur on ', - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/github_icon.png', style: 'vertical-align: middle;margin: -2px 0 0' }), - ' GitHub' + ]), + $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Page up'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'space' ]) ]) ]), - $.make('h4', { className: 'NB-tutorial-feedback-header' }, 'The NewsBlur Blog'), - $.make('ul', [ - $.make('li', [ - $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-javascript NB-tutorial-finish-newsblur-blog', style: 'float: right;margin-top: -2px' }, [ - 'Finish Tutorial and Load', - $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/favicon_32.png', style: "margin: -3px 0px 0px 4px; vertical-align: middle;width: 16px;height: 16px;" }), - ' the NewsBlur Blog ', - $.make('span', { className: 'NB-raquo' }, '»') - ]), - 'Monthly updates.' + $.make('div', { className: 'NB-keyboard-group' }, [ + $.make('div', { className: 'NB-keyboard-shortcut' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Next Unread Story'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'n' + ]) + ]), + $.make('div', { className: 'NB-keyboard-shortcut NB-last' }, [ + $.make('div', { className: 'NB-keyboard-shortcut-explanation' }, 'Hide Sidebar'), + $.make('div', { className: 'NB-keyboard-shortcut-key' }, [ + 'shift', + $.make('span', '+'), + 'u' + ]) ]) ]) - ]), - $.make('div', { className: 'NB-modal-submit' }, [ - $.make('div', { className: 'NB-page-next NB-modal-submit-button NB-modal-submit-green NB-modal-submit-save' }, [ - $.make('span', { className: 'NB-tutorial-next-page-text' }, 'Next Page '), - $.make('span', { className: 'NB-raquo' }, '»') - ]), - $.make('div', { className: 'NB-page-previous NB-modal-submit-button NB-modal-submit-grey NB-modal-submit-save' }, [ - $.make('span', { className: 'NB-raquo' }, '«'), - ' Previous Page' - ]) ]) - ]); - }, - - set_title: function() { - $('.NB-modal-title span', this.$modal).text(this.TITLES[this.page_number-1]); - }, - - load_tips: function() { - - }, - - make_story_titles: function() { - var $story_titles = $('.NB-tutorial-stories', this.$modal); - - var stories = [ - ['Story about space travel', '', 'space', 'neutral'], - ['NewsBlur becomes #1 feed reader', '', '', 'positive'], - ['Everyday news', 'Sam Clay', 'news', 'neutral'], - ['Another top 10 list', 'RSC', 'top 10', 'negative'], - ['Boring story about sports', 'Godzilla', '', 'negative'], - ['New Strokes album!', 'P. Smith', 'music', 'positive'] - ]; - - _.each(stories, function(story) { - var $story = $.make('div', { className: 'story NB-story-' + story[3] }, [ - $.make('div', { className: 'NB-storytitles-sentiment'}), - $.make('a', { href: '#', className: 'story_title' }, [ - $.make('span', { className: 'NB-storytitles-title' }, story[0]), - (story[1] && $.make('span', { className: 'NB-storytitles-author' }, story[1])), - (story[2] && $.make('span', { className: 'NB-storytitles-tags'}, [ - $.make('span', { className: 'NB-storytitles-tag'}, story[2]).corner('4px') - ])) + ]) + ]) + ]), + $.make('div', { className: 'NB-page NB-page-5' }, [ + $.make('h4', 'Stay connected to NewsBlur on Twitter'), + $.make('div', { className: 'NB-tutorial-twitter' }, [ + $.make('a', { className: 'NB-splash-link', href: 'http://twitter.com/samuelclay', target: '_blank' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/static/Samuel%20Clay%20sq.jpg', style: 'border-color: #505050;' }), + $.make('span', '@samuelclay') ]), - $.make('div', { className: 'NB-story-manage-icon' }) - ]); - $story_titles.append($story); - }); - }, - - load_intelligence_slider: function() { - var self = this; - var unread_view = this.model.preference('unread_view'); - this.set_slider_value(unread_view); - }, - - rotate_slider: function() { - clearInterval(this.intervals.slider); - this.intervals.slider = setInterval(_.bind(function() { - this.slider_value = ((this.slider_value + 1) % 3); - this.set_slider_value(this.slider_value - 1); - }, this), 2000); - }, - - set_slider_value: function(value) { - this.slider_value = value + 1; - var $slider = $('.NB-intelligence-slider', this.$modal); - - $('.NB-active', $slider).removeClass('NB-active'); - if (value < 0) { - $('.NB-intelligence-slider-red', $slider).addClass('NB-active'); - } else if (value > 0) { - $('.NB-intelligence-slider-green', $slider).addClass('NB-active'); - } else { - $('.NB-intelligence-slider-yellow', $slider).addClass('NB-active'); - } - this.show_story_titles_above_intelligence_level(value); - this.rotate_slider(); - }, - - show_story_titles_above_intelligence_level: function(level) { - level = level || 0; - var $stories_show, $stories_hide; - if (level > 0) { - $stories_show = $('.NB-story-positive', this.$modal); - $stories_hide = $('.NB-story-neutral,.NB-story-negative', this.$modal); - } else if (level == 0) { - $stories_show = $('.NB-story-positive,.NB-story-neutral', this.$modal); - $stories_hide = $('.NB-story-negative', this.$modal); - } else if (level < 0) { - $stories_show = $('.NB-story-positive,.NB-story-neutral,.NB-story-negative', this.$modal); - $stories_hide = $('.NB-story-nothing', this.$modal); - } - - $stories_show.slideDown(500); - $stories_hide.slideUp(500); - }, - - // ========== - // = Paging = - // ========== - - next_page: function() { - return this.page(this.page_number+1); - }, - - previous_page: function() { - return this.page(this.page_number-1); - }, - - page: function(page_number) { - if (page_number == null) { - return this.page_number; - } - var page_count = $('.NB-page', this.$modal).length; - this.page_number = page_number; - - if (page_number == page_count) { - $('.NB-tutorial-next-page-text', this.$modal).text('Finish Tutorial '); - } else if (page_number > page_count) { - return this.close(); - } else { - $('.NB-tutorial-next-page-text', this.$modal).text('Next Page '); - } - $('.NB-page-previous', this.$modal).toggle(page_number != 1); - $('.NB-page', this.$modal).css({'display': 'none'}); - $('.NB-page-'+this.page_number, this.$modal).css({'display': 'block'}); - $('.NB-modal-page', this.$modal).html($.make('div', [ - 'Page ', - $.make('b', this.page_number), - ' of ', - $.make('b', page_count) - ])); - this.set_title(); - this.resize(); - _.defer(_.bind(function() { - this.resize(); - }, this)); - }, - - close: function() { - this.model.load_tutorial({'finished': true}); - NEWSBLUR.Modal.prototype.close.call(this); - }, - - close_and_load_newsblur_blog: function() { - this.close(); - NEWSBLUR.reader.load_feed_in_tryfeed_view(this.newsblur_feed.id, {'feed': this.newsblur_feed}); - }, - - // =========== - // = Actions = - // =========== - - handle_click: function(elem, e) { - var self = this; - - $.targetIs(e, { tagSelector: '.NB-page-next' }, function($t, $p) { - e.preventDefault(); - - self.next_page(); - }); - $.targetIs(e, { tagSelector: '.NB-page-previous' }, function($t, $p) { - e.preventDefault(); - - self.previous_page(); - }); - $.targetIs(e, { tagSelector: '.NB-tutorial-finish-newsblur-blog' }, function($t, $p) { - e.preventDefault(); - - self.close_and_load_newsblur_blog(); - }); - $.targetIs(e, { tagSelector: '.NB-story-manage-icon' }, function($t, $p) { - e.preventDefault(); - e.stopPropagation(); - }); - $.targetIs(e, { tagSelector: '.NB-intelligence-slider-control' }, function($t, $p) { - e.preventDefault(); - e.stopPropagation(); - - var unread_value; - if ($t.hasClass('NB-intelligence-slider-red')) { - unread_value = -1; - } else if ($t.hasClass('NB-intelligence-slider-yellow')) { - unread_value = 0; - } else if ($t.hasClass('NB-intelligence-slider-green')) { - unread_value = 1; - } - - self.set_slider_value(unread_value); - - }); + $.make('a', { className: 'NB-splash-link', href: 'http://twitter.com/newsblur', target: '_blank' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/logo_128.png' }), + $.make('span', '@newsblur') + ]) + ]), + $.make('h4', { className: 'NB-tutorial-feedback-header' }, 'Community Feedback'), + $.make('ul', [ + $.make('li', [ + $.make('a', { href: 'https://forum.newsblur.com/', className: 'NB-splash-link' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/discourse.png', style: 'vertical-align: middle;margin: -2px 0 0; width: 16px;height: 16px;' }), + ' NewsBlur Support Forum' + ]) + ]) + ]), + $.make('h4', { className: 'NB-tutorial-feedback-header' }, [ + 'Open Source Code' + ]), + $.make('ul', [ + $.make('li', [ + $.make('a', { href: 'http://github.com/samuelclay', className: 'NB-splash-link' }, [ + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/howitworks_github.png', style: 'float: right;margin: -68px 12px 0 0' }), + 'NewsBlur on ', + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/github_icon.png', style: 'vertical-align: middle;margin: -2px 0 0' }), + ' GitHub' + ]) + ]) + ]), + $.make('h4', { className: 'NB-tutorial-feedback-header' }, 'The NewsBlur Blog'), + $.make('ul', [ + $.make('li', [ + $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-green NB-javascript NB-tutorial-finish-newsblur-blog', style: 'float: right;margin-top: -2px' }, [ + 'Finish Tutorial and Load', + $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/favicon_32.png', style: "margin: -3px 0px 0px 4px; vertical-align: middle;width: 16px;height: 16px;" }), + ' the NewsBlur Blog ', + $.make('span', { className: 'NB-raquo' }, '»') + ]), + 'Monthly updates.' + ]) + ]) + ]), + $.make('div', { className: 'NB-modal-submit' }, [ + $.make('div', { className: 'NB-page-next NB-modal-submit-button NB-modal-submit-green NB-modal-submit-save' }, [ + $.make('span', { className: 'NB-tutorial-next-page-text' }, 'Next Page '), + $.make('span', { className: 'NB-raquo' }, '»') + ]), + $.make('div', { className: 'NB-page-previous NB-modal-submit-button NB-modal-submit-grey NB-modal-submit-save' }, [ + $.make('span', { className: 'NB-raquo' }, '«'), + ' Previous Page' + ]) + ]) + ]); + }, + + set_title: function () { + $('.NB-modal-title span', this.$modal).text(this.TITLES[this.page_number - 1]); + }, + + load_tips: function () { + + }, + + make_story_titles: function () { + var $story_titles = $('.NB-tutorial-stories', this.$modal); + + var stories = [ + ['Story about space travel', '', 'space', 'neutral'], + ['NewsBlur becomes #1 feed reader', '', '', 'positive'], + ['Everyday news', 'Sam Clay', 'news', 'neutral'], + ['Another top 10 list', 'RSC', 'top 10', 'negative'], + ['Boring story about sports', 'Godzilla', '', 'negative'], + ['New Strokes album!', 'P. Smith', 'music', 'positive'] + ]; + + _.each(stories, function (story) { + var $story = $.make('div', { className: 'story NB-story-' + story[3] }, [ + $.make('div', { className: 'NB-storytitles-sentiment' }), + $.make('a', { href: '#', className: 'story_title' }, [ + $.make('span', { className: 'NB-storytitles-title' }, story[0]), + (story[1] && $.make('span', { className: 'NB-storytitles-author' }, story[1])), + (story[2] && $.make('span', { className: 'NB-storytitles-tags' }, [ + $.make('span', { className: 'NB-storytitles-tag' }, story[2]).corner('4px') + ])) + ]), + $.make('div', { className: 'NB-story-manage-icon' }) + ]); + $story_titles.append($story); + }); + }, + + load_intelligence_slider: function () { + var self = this; + var unread_view = this.model.preference('unread_view'); + this.set_slider_value(unread_view); + }, + + rotate_slider: function () { + clearInterval(this.intervals.slider); + this.intervals.slider = setInterval(_.bind(function () { + this.slider_value = ((this.slider_value + 1) % 3); + this.set_slider_value(this.slider_value - 1); + }, this), 2000); + }, + + set_slider_value: function (value) { + this.slider_value = value + 1; + var $slider = $('.NB-intelligence-slider', this.$modal); + + $('.NB-active', $slider).removeClass('NB-active'); + if (value < 0) { + $('.NB-intelligence-slider-red', $slider).addClass('NB-active'); + } else if (value > 0) { + $('.NB-intelligence-slider-green', $slider).addClass('NB-active'); + } else { + $('.NB-intelligence-slider-yellow', $slider).addClass('NB-active'); + } + this.show_story_titles_above_intelligence_level(value); + this.rotate_slider(); + }, + + show_story_titles_above_intelligence_level: function (level) { + level = level || 0; + var $stories_show, $stories_hide; + if (level > 0) { + $stories_show = $('.NB-story-positive', this.$modal); + $stories_hide = $('.NB-story-neutral,.NB-story-negative', this.$modal); + } else if (level == 0) { + $stories_show = $('.NB-story-positive,.NB-story-neutral', this.$modal); + $stories_hide = $('.NB-story-negative', this.$modal); + } else if (level < 0) { + $stories_show = $('.NB-story-positive,.NB-story-neutral,.NB-story-negative', this.$modal); + $stories_hide = $('.NB-story-nothing', this.$modal); + } + + $stories_show.slideDown(500); + $stories_hide.slideUp(500); + }, + + // ========== + // = Paging = + // ========== + + next_page: function () { + return this.page(this.page_number + 1); + }, + + previous_page: function () { + return this.page(this.page_number - 1); + }, + + page: function (page_number) { + if (page_number == null) { + return this.page_number; + } + var page_count = $('.NB-page', this.$modal).length; + this.page_number = page_number; + + if (page_number == page_count) { + $('.NB-tutorial-next-page-text', this.$modal).text('Finish Tutorial '); + } else if (page_number > page_count) { + return this.close(); + } else { + $('.NB-tutorial-next-page-text', this.$modal).text('Next Page '); } - + $('.NB-page-previous', this.$modal).toggle(page_number != 1); + $('.NB-page', this.$modal).css({ 'display': 'none' }); + $('.NB-page-' + this.page_number, this.$modal).css({ 'display': 'block' }); + $('.NB-modal-page', this.$modal).html($.make('div', [ + 'Page ', + $.make('b', this.page_number), + ' of ', + $.make('b', page_count) + ])); + this.set_title(); + this.resize(); + _.defer(_.bind(function () { + this.resize(); + }, this)); + }, + + close: function () { + this.model.load_tutorial({ 'finished': true }); + NEWSBLUR.Modal.prototype.close.call(this); + }, + + close_and_load_newsblur_blog: function () { + this.close(); + NEWSBLUR.reader.load_feed_in_tryfeed_view(this.newsblur_feed.id, { 'feed': this.newsblur_feed }); + }, + + // =========== + // = Actions = + // =========== + + handle_click: function (elem, e) { + var self = this; + + $.targetIs(e, { tagSelector: '.NB-page-next' }, function ($t, $p) { + e.preventDefault(); + + self.next_page(); + }); + $.targetIs(e, { tagSelector: '.NB-page-previous' }, function ($t, $p) { + e.preventDefault(); + + self.previous_page(); + }); + $.targetIs(e, { tagSelector: '.NB-tutorial-finish-newsblur-blog' }, function ($t, $p) { + e.preventDefault(); + + self.close_and_load_newsblur_blog(); + }); + $.targetIs(e, { tagSelector: '.NB-story-manage-icon' }, function ($t, $p) { + e.preventDefault(); + e.stopPropagation(); + }); + $.targetIs(e, { tagSelector: '.NB-intelligence-slider-control' }, function ($t, $p) { + e.preventDefault(); + e.stopPropagation(); + + var unread_value; + if ($t.hasClass('NB-intelligence-slider-red')) { + unread_value = -1; + } else if ($t.hasClass('NB-intelligence-slider-yellow')) { + unread_value = 0; + } else if ($t.hasClass('NB-intelligence-slider-green')) { + unread_value = 1; + } + + self.set_slider_value(unread_value); + + }); + } + }); diff --git a/media/js/newsblur/reader/reader_utils.js b/media/js/newsblur/reader/reader_utils.js index 048fb67b9d..f9bfd94528 100644 --- a/media/js/newsblur/reader/reader_utils.js +++ b/media/js/newsblur/reader/reader_utils.js @@ -1,40 +1,40 @@ NEWSBLUR.utils = { - - service_name: function(service) { + + service_name: function (service) { switch (service) { case 'twitter': - return 'Twitter'; + return 'Twitter'; case 'facebook': - return 'Facebook'; + return 'Facebook'; } }, - - compute_story_score: function(story) { - var score = 0; - var intelligence = story.get('intelligence'); - if (!intelligence) return score; - var score_max = Math.max(intelligence['title'], - intelligence['author'], - intelligence['tags']); - var score_min = Math.min(intelligence['title'], - intelligence['author'], - intelligence['tags']); - if (score_max > 0) score = score_max; - else if (score_min < 0) score = score_min; - - if (score == 0) score = intelligence['feed']; - - return score; + + compute_story_score: function (story) { + var score = 0; + var intelligence = story.get('intelligence'); + if (!intelligence) return score; + var score_max = Math.max(intelligence['title'], + intelligence['author'], + intelligence['tags']); + var score_min = Math.min(intelligence['title'], + intelligence['author'], + intelligence['tags']); + if (score_max > 0) score = score_max; + else if (score_min < 0) score = score_min; + + if (score == 0) score = intelligence['feed']; + + return score; }, - - generate_shadow: _.memoize(function(feed) { + + generate_shadow: _.memoize(function (feed) { if (!feed) return ''; var color = feed.get('favicon_color'); - + if (!color) { return '0 1px 0 #222'; } - + var r, g, b; if (feed.is_light()) { color = feed.get('favicon_fade'); @@ -50,24 +50,24 @@ NEWSBLUR.utils = { [r, g, b].join(','), ')' ].join(''); - }, function(feed) { + }, function (feed) { return "" + feed.id; }), - - generate_gradient: _.memoize(function(feed, type) { + + generate_gradient: _.memoize(function (feed, type) { if (!feed) return ''; var color = feed.get('favicon_color'); var colorFade = feed.get('favicon_fade'); var colorBorder = feed.get('favicon_border'); if (!color) return ''; - + var r = parseInt(color.substr(0, 2), 16); var g = parseInt(color.substr(2, 2), 16); var b = parseInt(color.substr(4, 2), 16); var rF = parseInt(colorFade.substr(0, 2), 16); var gF = parseInt(colorFade.substr(2, 2), 16); var bF = parseInt(colorFade.substr(4, 2), 16); - + if (type == 'border') { r = parseInt(colorBorder.substr(0, 2), 16); g = parseInt(colorBorder.substr(2, 2), 16); @@ -124,27 +124,27 @@ NEWSBLUR.utils = { ') 100%)' ].join(''); } - }, function(feed, type) { + }, function (feed, type) { return "" + feed.id + '-' + type; }), - - attach_loading_gradient: function($elem, percentage) { - $elem.css('background', '-moz-linear-gradient(left, #b1d2f9 0%, #b1d2f9 '+percentage+'%, #fcfcfc '+percentage+'%, #fcfcfc 100%)'); // FF3.6+ - $elem.css('background', '-webkit-gradient(linear, left top, right top, color-stop(0%,#b1d2f9), color-stop('+percentage+'%,#b1d2f9), color-stop('+percentage+'%,#fcfcfc), color-stop(100%,#fcfcfc))'); // Chrome,Safari4+ - $elem.css('background', '-webkit-linear-gradient(left, #b1d2f9 0%,#b1d2f9 '+percentage+'%,#fcfcfc '+percentage+'%,#fcfcfc 100%)'); // Chrome10+,Safari5.1+ - $elem.css('background', 'linear-gradient(to right, #b1d2f9 0%,#b1d2f9 '+percentage+'%,#fcfcfc '+percentage+'%,#fcfcfc 100%)'); + + attach_loading_gradient: function ($elem, percentage) { + $elem.css('background', '-moz-linear-gradient(left, #b1d2f9 0%, #b1d2f9 ' + percentage + '%, #fcfcfc ' + percentage + '%, #fcfcfc 100%)'); // FF3.6+ + $elem.css('background', '-webkit-gradient(linear, left top, right top, color-stop(0%,#b1d2f9), color-stop(' + percentage + '%,#b1d2f9), color-stop(' + percentage + '%,#fcfcfc), color-stop(100%,#fcfcfc))'); // Chrome,Safari4+ + $elem.css('background', '-webkit-linear-gradient(left, #b1d2f9 0%,#b1d2f9 ' + percentage + '%,#fcfcfc ' + percentage + '%,#fcfcfc 100%)'); // Chrome10+,Safari5.1+ + $elem.css('background', 'linear-gradient(to right, #b1d2f9 0%,#b1d2f9 ' + percentage + '%,#fcfcfc ' + percentage + '%,#fcfcfc 100%)'); $elem.css("filter", "progid:DXImageTransform.Microsoft.gradient( startColorstr='#b1d2f9', endColorstr='#fcfcfc',GradientType=1 )"); }, - - is_feed_social: function(feed_id) { + + is_feed_social: function (feed_id) { return _.string.include(feed_id, 'social:'); }, - - monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], - shortMonthNames: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + + monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + shortMonthNames: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - - format_date: function(date) { + + format_date: function (date) { var dayOfWeek = date.getDay(); var month = date.getMonth(); var year = date.getUTCFullYear(); @@ -164,7 +164,7 @@ NEWSBLUR.utils = { var current_feed_id = options.feed_id; var selected_feed_prefix = ''; - var make_feed_option = function(feed) { + var make_feed_option = function (feed) { if (!feed.get('feed_title')) return; if (!feed.get('active')) return; var prefix = 'feed:'; @@ -173,11 +173,11 @@ NEWSBLUR.utils = { else if (feed.is_search()) prefix = ''; var $option = $.make('option', { value: prefix + feed.id }, feed.get('feed_title')); - $option.appendTo(feed.is_starred() ? $starred_feeds_optgroup : - feed.is_social() ? $social_feeds_optgroup : - feed.is_search() ? $saved_searches_optgroup : - $feeds_optgroup); - + $option.appendTo(feed.is_starred() ? $starred_feeds_optgroup : + feed.is_social() ? $social_feeds_optgroup : + feed.is_search() ? $saved_searches_optgroup : + $feeds_optgroup); + if (feed.id == current_feed_id) { // console.log('Selecting feed id in feed chooser', feed, current_feed_id); $option.attr('selected', true); @@ -185,10 +185,10 @@ NEWSBLUR.utils = { selected_feed_prefix = prefix; } }; - + this.feeds = NEWSBLUR.assets.get_feeds(); this.feeds.each(make_feed_option); - + if (!options.skip_social) { this.social_feeds = NEWSBLUR.assets.get_social_feeds(); this.social_feeds.each(make_feed_option); @@ -198,12 +198,12 @@ NEWSBLUR.utils = { this.search_feeds = NEWSBLUR.assets.get_search_feeds(); this.search_feeds.each(make_feed_option); } - + if (!options.skip_starred) { this.starred_feeds = NEWSBLUR.assets.get_starred_feeds(); this.starred_feeds.each(make_feed_option); } - + if (options.include_folders) { var $folders = NEWSBLUR.utils.make_folders(options.feed_id, options.toplevel, options.name, options.include_special_folders); $('option', $folders).each(function () { @@ -216,7 +216,7 @@ NEWSBLUR.utils = { $('option', $starred_feeds_optgroup).tsort(); $('option', $saved_searches_optgroup).tsort(); // $('option[value^=river]', $folders_optgroup).tsort(); - + if (options.include_folders) { $chooser.append($folders_optgroup); } @@ -237,27 +237,27 @@ NEWSBLUR.utils = { return $chooser; }, - + make_folders: function (selected_folder_title, toplevel, select_name, include_special_folders) { // console.log('make_folders', selected_folder_title); var folders = NEWSBLUR.assets.get_folders(); var $options = $.make('select', { className: 'NB-folders', name: select_name }); - + if (include_special_folders) { var $option = $.make('option', { value: 'river:global' }, "Global Shared Stories"); $options.append($option); if (selected_folder_title == "river:global") { $option.attr('selected', true); } - + var $option = $.make('option', { value: 'river:blurblogs' }, "All Shared Stories"); - $options.append($option); + $options.append($option); if (selected_folder_title == "river:blurblogs") { $option.attr('selected', true); } var $option = $.make('option', { value: 'river:infrequent' }, "Infrequent Site Stories"); - $options.append($option); + $options.append($option); if (selected_folder_title == "river:infrequent") { $option.attr('selected', true); } @@ -270,29 +270,29 @@ NEWSBLUR.utils = { } $options = this.make_folder_options($options, folders, '   ', selected_folder_title); - + return $options; }, - make_folder_options: function($options, items, depth, selected_folder_title) { + make_folder_options: function ($options, items, depth, selected_folder_title) { var self = this; - items.each(function(item) { + items.each(function (item) { if (item.is_folder()) { - var $option = $.make('option', { - value: 'river:'+item.get('folder_title') + var $option = $.make('option', { + value: 'river:' + item.get('folder_title') }, depth + ' ' + item.get('folder_title')); $options.append($option); if (item.get('folder_title') == selected_folder_title) { $option.attr('selected', true); } - $options = self.make_folder_options($options, item.folders, depth+'   ', selected_folder_title); + $options = self.make_folder_options($options, item.folders, depth + '   ', selected_folder_title); } }); - + return $options; }, - - is_url_iframe_buster: function(url) { + + is_url_iframe_buster: function (url) { // Also change in utils/page_importer.py. var BROKEN_URLS = [ 'nytimes.com', @@ -306,14 +306,14 @@ NEWSBLUR.utils = { 'gamespot.com', 'royalroad.com' ]; - return _.any(BROKEN_URLS, function(broken_url) { + return _.any(BROKEN_URLS, function (broken_url) { return _.string.contains(url, broken_url); }); }, - - calculate_update_interval: function(update_interval_minutes) { + + calculate_update_interval: function (update_interval_minutes) { if (!update_interval_minutes) return ' '; - + var interval_start = update_interval_minutes; var interval_end = update_interval_minutes * 1.25; var interval = ''; @@ -326,22 +326,22 @@ NEWSBLUR.utils = { var dec_end = interval_end % 60; interval = interval_start_hours + (dec_start >= 30 ? '.5' : '') + ' to ' + interval_end_hours + (dec_end >= 30 || interval_start_hours == interval_end_hours ? '.5' : '') + ' hours'; } - + return interval; }, - - days_back_to_timestamp: function(days_back) { + + days_back_to_timestamp: function (days_back) { if (days_back > 365) { // It's a timestamp, not the number of days back return days_back; } - + days_back = days_back || 0; var now = Math.round((new Date()).getTime() / 1000); - return now - (days_back * 60*60*24); + return now - (days_back * 60 * 60 * 24); } - - + + }; diff --git a/media/js/newsblur/social_page/social_page.js b/media/js/newsblur/social_page/social_page.js index edd39c7704..4fb5bfc9f6 100644 --- a/media/js/newsblur/social_page/social_page.js +++ b/media/js/newsblur/social_page/social_page.js @@ -1,52 +1,52 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ - + el: 'body', - + page: 1, auto_advance_pages: 0, - + MAX_AUTO_ADVANCED_PAGES: 15, - + events: { - "click .NB-page-controls-next:not(.NB-loaded):not(.NB-loading)" : "next_page", - "click .NB-button-follow" : "follow_user", - "click .NB-button-following" : "unfollow_user" + "click .NB-page-controls-next:not(.NB-loaded):not(.NB-loading)": "next_page", + "click .NB-button-follow": "follow_user", + "click .NB-button-following": "unfollow_user" }, - + stories: {}, - + next_animation_options: { 'duration': 500, 'easing': 'easeInOutQuint', 'queue': false }, - + flags: { loading_page: false }, - - initialize: function() { + + initialize: function () { NEWSBLUR.assets = new NEWSBLUR.SocialPageAssets(); NEWSBLUR.router = new NEWSBLUR.Router; this.cached_page_control_y = 0; - - Backbone.history.start({pushState: true}); + + Backbone.history.start({ pushState: true }); _.bindAll(this, 'detect_scroll'); $(window).scroll(this.detect_scroll); - + this.login_view = new NEWSBLUR.Views.SocialPageLoginSignupView({ el: this.el }); this.initialize_stories(); }, - - initialize_stories: function($stories) { + + initialize_stories: function ($stories) { var self = this; $stories = $stories || this.$el; - $('.NB-shared-story', $stories).each(function() { + $('.NB-shared-story', $stories).each(function () { var $story = $(this); var guid = $story.data('guid'); if (!self.stories[guid]) { @@ -57,22 +57,22 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ self.stories[story_view.story_guid] = story_view; } }); - + this.find_story(); }, - - detect_scroll: function(){ + + detect_scroll: function () { if (this.flags.loading_page) { return; } - + var viewport_y = $(window).height() + $(window).scrollTop(); // this prevents calculating when we are scrolling in previously loaded content if (viewport_y < this.cached_page_control_y) { return; } - + var $controls = this.$('.NB-page-controls'); if ($controls.length) { var page_control_y = $controls.last().offset().top + 25; @@ -83,11 +83,11 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ } } }, - - find_story: function() { + + find_story: function () { var search_story_guid = NEWSBLUR.router.story_guid; if (search_story_guid && this.auto_advance_pages < this.MAX_AUTO_ADVANCED_PAGES) { - var found_story = _.detect(this.stories, function(story) { + var found_story = _.detect(this.stories, function (story) { var hash = story.model.get('story_feed_id') + ":" + story.story_guid; return hash.indexOf(search_story_guid) >= 0; }); @@ -103,10 +103,10 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ } } }, - - scroll_to_story: function(story_view, run) { + + scroll_to_story: function (story_view, run) { var offset = navigator.platform.indexOf("iPhone") != -1 ? 12 : 12 + 48; - + $('html,body').stop().animate({ scrollTop: story_view.$mark.offset().top - offset }, { @@ -115,36 +115,36 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ queue: false }); }, - + // ========== // = Events = // ========== - - next_page: function(e) { + + next_page: function (e) { if ($('.NB-page-controls-end').length) return; - + var $button = e && $(e.currentTarget) || $('.NB-page-controls-next').last(); var $next = $('.NB-page-controls-text-next', $button); var $loading = $('.NB-page-controls-text-loading', $button); var $loaded = $('.NB-page-controls-text-loaded', $button); var height = this.$('.NB-page-controls').height(); var innerheight = $button.height(); - - $loaded.animate({'bottom': height}, this.next_animation_options); - $loading.text('Loading...').css('bottom', height).animate({'bottom': innerheight}, this.next_animation_options); - $next.animate({'bottom': -1 * innerheight}, this.next_animation_options); + + $loaded.animate({ 'bottom': height }, this.next_animation_options); + $loading.text('Loading...').css('bottom', height).animate({ 'bottom': innerheight }, this.next_animation_options); + $next.animate({ 'bottom': -1 * innerheight }, this.next_animation_options); $button.addClass('NB-loading'); - + clearInterval(this.feed_stories_loading); - $button.animate({'backgroundColor': '#5C89C9'}, 650) - .animate({'backgroundColor': '#2B478C'}, 900); - this.feed_stories_loading = setInterval(function() { - $button.animate({'backgroundColor': '#5C89C9'}, {'duration': 650}) - .animate({'backgroundColor': '#2B478C'}, 900); + $button.animate({ 'backgroundColor': '#5C89C9' }, 650) + .animate({ 'backgroundColor': '#2B478C' }, 900); + this.feed_stories_loading = setInterval(function () { + $button.animate({ 'backgroundColor': '#5C89C9' }, { 'duration': 650 }) + .animate({ 'backgroundColor': '#2B478C' }, 900); }, 1550); - + this.page += 1; - + $.ajax({ url: '/', method: 'GET', @@ -157,8 +157,8 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ error: _.bind(this.error_next_page, this) }); }, - - post_next_page: function(data) { + + post_next_page: function (data) { var $controls = this.$('.NB-page-controls').last(); var $button = $('.NB-page-controls-next', $controls); var $loading = $('.NB-page-controls-text-loading', $controls); @@ -166,76 +166,76 @@ NEWSBLUR.Views.SocialPage = Backbone.View.extend({ var height = $controls.height(); var innerheight = $button.height(); this.flags.loading_page = false; - + $button.removeClass('NB-loading').addClass('NB-loaded'); - $button.stop(true).animate({'backgroundColor': '#86B86B'}, {'duration': 750, 'easing': 'easeOutExpo', 'queue': false}); - - $loaded.text('Page ' + this.page).css('bottom', height).animate({'bottom': innerheight}, this.next_animation_options); - $loading.animate({'bottom': -1 * innerheight}, this.next_animation_options); - + $button.stop(true).animate({ 'backgroundColor': '#86B86B' }, { 'duration': 750, 'easing': 'easeOutExpo', 'queue': false }); + + $loaded.text('Page ' + this.page).css('bottom', height).animate({ 'bottom': innerheight }, this.next_animation_options); + $loading.animate({ 'bottom': -1 * innerheight }, this.next_animation_options); + clearInterval(this.feed_stories_loading); - + var $stories = $(data); $controls.after($stories); this.initialize_stories(); }, - - error_next_page: function() { + + error_next_page: function () { var $controls = this.$('.NB-page-controls').last(); var $button = $('.NB-page-controls-next', $controls); var $loading = $('.NB-page-controls-text-loading', $controls); var $next = $('.NB-page-controls-text-next', $controls); var height = $controls.height(); var innerheight = $button.height(); - + $button.removeClass('NB-loading').removeClass('NB-loaded'); - $button.stop(true).animate({'backgroundColor': '#B6686B'}, { - 'duration': 750, - 'easing': 'easeOutExpo', + $button.stop(true).animate({ 'backgroundColor': '#B6686B' }, { + 'duration': 750, + 'easing': 'easeOutExpo', 'queue': false }); - + this.page -= 1; this.flags.loading_page = false; - + $next.text('Whoops! Something went wrong. Try again.') - .animate({'bottom': innerheight}, this.next_animation_options); - $loading.animate({'bottom': height}, this.next_animation_options); - + .animate({ 'bottom': innerheight }, this.next_animation_options); + $loading.animate({ 'bottom': height }, this.next_animation_options); + clearInterval(this.feed_stories_loading); }, - - follow_user: function() { + + follow_user: function () { var $button = this.$(".NB-button-follow"); $button.html('Following...'); - NEWSBLUR.assets.follow_user(NEWSBLUR.Globals.blurblog_user_id, _.bind(function(data) { + NEWSBLUR.assets.follow_user(NEWSBLUR.Globals.blurblog_user_id, _.bind(function (data) { var message = 'You are now following ' + NEWSBLUR.Globals.blurblog_username; if (data.follow_profile.requested_follow) { message = 'Your request to follow ' + NEWSBLUR.Globals.blurblog_username + ' has been sent'; } $button.html('Following').removeClass('NB-button-follow') - .removeClass('NB-blue-button') - .addClass('NB-grey-button') - .addClass('NB-button-following'); + .removeClass('NB-blue-button') + .addClass('NB-grey-button') + .addClass('NB-button-following'); this.$('.NB-stat-followers').html("" + data.follow_profile.follower_count + " " + Inflector.pluralize('follower', data.follow_profile.follower_count)); }, this)); }, - - unfollow_user: function() { + + unfollow_user: function () { var $button = this.$(".NB-button-following"); $button.html('Unfollowing...'); - NEWSBLUR.assets.unfollow_user(NEWSBLUR.Globals.blurblog_user_id, _.bind(function(data) { + NEWSBLUR.assets.unfollow_user(NEWSBLUR.Globals.blurblog_user_id, _.bind(function (data) { $button.html('Follow ' + NEWSBLUR.Globals.blurblog_username).removeClass('NB-button-following') - .removeClass('NB-grey-button') - .addClass('NB-button-follow') - .addClass('NB-blue-button'); + .removeClass('NB-grey-button') + .addClass('NB-button-follow') + .addClass('NB-blue-button'); this.$('.NB-stat-followers').html("" + data.unfollow_profile.follower_count + " " + Inflector.pluralize('follower', data.unfollow_profile.follower_count)); }, this)); } - + }); -$(document).ready(function() { +$(document).ready(function () { NEWSBLUR.app.social_page = new NEWSBLUR.Views.SocialPage(); diff --git a/media/js/newsblur/social_page/social_page_assets.js b/media/js/newsblur/social_page/social_page_assets.js index 6389112706..af9854dcd5 100644 --- a/media/js/newsblur/social_page/social_page_assets.js +++ b/media/js/newsblur/social_page/social_page_assets.js @@ -1,6 +1,6 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ - initialize: function() { + initialize: function () { this.social_services = { twitter: {}, facebook: {} @@ -8,7 +8,7 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ this.user_profile = new Backbone.Model(NEWSBLUR.user_social_profile); }, - make_request: function(url, data, callback, error_callback, options) { + make_request: function (url, data, callback, error_callback, options) { var self = this; var options = $.extend({ 'traditional': true @@ -21,43 +21,43 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ type: request_type, cache: false, cacheResponse: false, - beforeSend: function() { + beforeSend: function () { $.isFunction(options['beforeSend']) && options['beforeSend'](); return true; }, - success: function(o) { + success: function (o) { if (o && o.code < 0 && error_callback) { error_callback(o); } else if ($.isFunction(callback)) { callback(o); } }, - error: function(e, textStatus, errorThrown) { + error: function (e, textStatus, errorThrown) { if (errorThrown == 'abort') { return; } - + if (error_callback) { error_callback(); } else if ($.isFunction(callback)) { var message = "Please create an account. Not much to do without an account."; if (NEWSBLUR.Globals.is_authenticated) { - message = "Sorry, there was an unhandled error."; + message = "Sorry, there was an unhandled error."; } - callback({'message': message}); + callback({ 'message': message }); } } - }, options)); - + }, options)); + }, - - preference: function(preference, value) { + + preference: function (preference, value) { if (typeof value == 'undefined') { var pref = NEWSBLUR.Preferences[preference]; if ((/\d+/).test(pref)) return parseInt(pref, 10); return pref; } - + NEWSBLUR.Preferences[preference] = value; var preferences = {}; preferences[preference] = value; @@ -65,8 +65,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - mark_story_as_shared: function(params, callback, error_callback) { + + mark_story_as_shared: function (params, callback, error_callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/social/share_story', { story_id: params.story_id, @@ -84,7 +84,7 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ } }, - mark_story_as_unshared: function(params, callback, error_callback) { + mark_story_as_unshared: function (params, callback, error_callback) { if (NEWSBLUR.Globals.is_authenticated) { this.make_request('/social/unshare_story', { story_id: params.story_id, @@ -98,8 +98,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ error_callback(); } }, - - save_comment_reply: function(story_id, story_feed_id, comment_user_id, reply_comments, reply_id, callback, error_callback) { + + save_comment_reply: function (story_id, story_feed_id, comment_user_id, reply_comments, reply_id, callback, error_callback) { this.make_request('/social/save_comment_reply', { story_id: story_id, story_feed_id: story_feed_id, @@ -111,8 +111,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - delete_comment_reply: function(story_id, story_feed_id, comment_user_id, reply_id, callback, error_callback) { + + delete_comment_reply: function (story_id, story_feed_id, comment_user_id, reply_id, callback, error_callback) { this.make_request('/social/remove_comment_reply', { story_id: story_id, story_feed_id: story_feed_id, @@ -123,8 +123,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - like_comment: function(story_id, story_feed_id, comment_user_id, callback, error_callback) { + + like_comment: function (story_id, story_feed_id, comment_user_id, callback, error_callback) { this.make_request('/social/like_comment', { story_id: story_id, story_feed_id: story_feed_id, @@ -134,8 +134,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - remove_like_comment: function(story_id, story_feed_id, comment_user_id, callback, error_callback) { + + remove_like_comment: function (story_id, story_feed_id, comment_user_id, callback, error_callback) { this.make_request('/social/remove_like_comment', { story_id: story_id, story_feed_id: story_feed_id, @@ -145,8 +145,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - login: function(username, password, callback, error_callback) { + + login: function (username, password, callback, error_callback) { this.make_request('/api/login', { username: username, password: password @@ -154,14 +154,14 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - logout: function(callback, error_callback) { + + logout: function (callback, error_callback) { this.make_request('/api/logout', {}, callback, error_callback, { request_type: 'POST' }); }, - - signup: function(username, email, password, callback, error_callback) { + + signup: function (username, email, password, callback, error_callback) { this.make_request('/api/signup', { username: username, password: password, @@ -170,8 +170,8 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ request_type: 'POST' }); }, - - request_invite: function(email, callback, error_callback) { + + request_invite: function (email, callback, error_callback) { this.make_request('/social/request_invite', { email: email }, callback, error_callback, { @@ -179,18 +179,18 @@ NEWSBLUR.SocialPageAssets = Backbone.Router.extend({ }); }, - follow_user: function(user_id, callback) { - this.make_request('/social/follow', {'user_id': user_id}, callback, callback, { + follow_user: function (user_id, callback) { + this.make_request('/social/follow', { 'user_id': user_id }, callback, callback, { request_type: 'POST' }); }, - - unfollow_user: function(user_id, callback) { - this.make_request('/social/unfollow', {'user_id': user_id}, callback, callback, { + + unfollow_user: function (user_id, callback) { + this.make_request('/social/unfollow', { 'user_id': user_id }, callback, callback, { request_type: 'POST' }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/social_page/social_page_comment.js b/media/js/newsblur/social_page/social_page_comment.js index faf8c44d32..cad1261dae 100644 --- a/media/js/newsblur/social_page/social_page_comment.js +++ b/media/js/newsblur/social_page/social_page_comment.js @@ -1,5 +1,5 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ - + events: { "click .NB-story-comment-edit-button": "open_edit", "click .NB-story-comment-delete": "delete_comment", @@ -9,37 +9,37 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ "click .NB-story-comment-edit-reply-button": "edit_reply", "click .NB-story-comment-like": "like_comment" }, - - initialize: function(options) { + + initialize: function (options) { this.story_view = options.story_view; this.story_comments_view = options.story_comments_view; }, - + // =========== // = Actions = // =========== - - fetch_comment: function(callback) { + + fetch_comment: function (callback) { this.$('.NB-spinner').addClass('NB-active'); - + this.model.fetch({ - success: _.bind(function() { + success: _.bind(function () { this.$('.NB-spinner').removeClass('NB-active'); callback && callback(); }, this) }); }, - + // ========== // = Events = // ========== - - open_edit: function() { - this.fetch_comment(_.bind(function() { + + open_edit: function () { + this.fetch_comment(_.bind(function () { var $edit = this.options.story_comments_view.$('.NB-story-comment-edit'); var $input = $('.NB-story-comment-input', $edit); var $del = $('.NB-story-comment-delete', $edit); - + this.$el.after($edit); this.$el.addClass('NB-hidden'); $edit.removeClass('NB-hidden'); @@ -47,10 +47,10 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ $input.html(this.model.get('comments')).focus(); }, this)); }, - - edit_reply: function(e) { + + edit_reply: function (e) { var $reply = $(e.currentTarget).closest(".NB-story-comment-reply"); - + this.open_reply({ $reply: $reply, is_editing: true, @@ -58,8 +58,8 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ reply_id: $reply.data("id") }); }, - - open_reply: function(options) { + + open_reply: function (options) { options = options || {}; var current_user = NEWSBLUR.assets.user_profile; @@ -76,7 +76,7 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ (options.is_editing && $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-grey NB-modal-submit-delete' }, 'Delete')) ]); this.remove_social_comment_reply_form(); - + if (options.is_editing && options.$reply) { $form.data('reply_id', options.reply_id); options.$reply.hide().addClass('NB-story-comment-reply-hidden'); @@ -84,35 +84,35 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ } else { this.$el.append($form); } - - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'enter', + + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'enter', _.bind(this.save_social_comment_reply, this)); - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'return', + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'return', _.bind(this.save_social_comment_reply, this)); - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'esc', _.bind(function(e) { + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'esc', _.bind(function (e) { e.preventDefault(); this.remove_social_comment_reply_form(); }, this)); $('input', $form).focus(); - + if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } }, - - remove_social_comment_reply_form: function() { + + remove_social_comment_reply_form: function () { this.$('.NB-story-comment-reply-form').remove(); this.$('.NB-story-comment-reply-hidden').show(); }, - - save_social_comment_reply: function() { + + save_social_comment_reply: function () { var $form = this.$('.NB-story-comment-reply-form'); var $submit = $(".NB-modal-submit-green", $form); var $delete_button = $(".NB-modal-submit-delete", $form); var comment_user_id = this.model.get('user_id'); var comment_reply = $('.NB-story-comment-reply-comments', $form).val(); var reply_id = $form.data('reply_id'); - + if (!comment_reply || comment_reply.length <= 1) { this.remove_social_comment_reply_form(); if (NEWSBLUR.app.story_list) { @@ -120,85 +120,85 @@ NEWSBLUR.Views.SocialPageComment = Backbone.View.extend({ } return; } - + if ($submit.hasClass('NB-disabled')) { return; } - + $delete_button.hide(); $submit.addClass('NB-disabled').text('Posting...'); - NEWSBLUR.assets.save_comment_reply(this.options.story.id, this.options.story.get('story_feed_id'), - comment_user_id, comment_reply, - reply_id, - _.bind(function(data) { - this.options.story_comments_view.replace_comment(this, data); - }, this), _.bind(function(data) { - var message = data && data.message || "Sorry, this reply could not be posted. Probably Adblock."; - if (!NEWSBLUR.Globals.is_authenticated) { - message = "You need to be logged in to reply to a comment."; - } - var $error = $.make('div', { className: 'NB-error' }, message); - $submit.removeClass('NB-disabled').text('Post'); - $form.find('.NB-error').remove(); - $form.append($error); - if (NEWSBLUR.app.story_list) { - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - } - }, this)); + NEWSBLUR.assets.save_comment_reply(this.options.story.id, this.options.story.get('story_feed_id'), + comment_user_id, comment_reply, + reply_id, + _.bind(function (data) { + this.options.story_comments_view.replace_comment(this, data); + }, this), _.bind(function (data) { + var message = data && data.message || "Sorry, this reply could not be posted. Probably Adblock."; + if (!NEWSBLUR.Globals.is_authenticated) { + message = "You need to be logged in to reply to a comment."; + } + var $error = $.make('div', { className: 'NB-error' }, message); + $submit.removeClass('NB-disabled').text('Post'); + $form.find('.NB-error').remove(); + $form.append($error); + if (NEWSBLUR.app.story_list) { + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + } + }, this)); }, - - delete_comment: function() { + + delete_comment: function () { this.story_view.mark_story_as_unshared(); }, - - delete_social_comment_reply: function() { + + delete_social_comment_reply: function () { var $form = this.$('.NB-story-comment-reply-form'); var $submit = $(".NB-modal-submit-green", $form); var $delete_button = $(".NB-modal-submit-delete", $form); var comment_user_id = this.model.get('user_id'); var reply_id = $form.data('reply_id'); - + if ($submit.hasClass('NB-disabled') || $delete_button.hasClass('NB-disabled')) { return; } - + $submit.addClass('NB-disabled'); $delete_button.addClass('NB-disabled').text('Deleting...'); NEWSBLUR.assets.delete_comment_reply(this.options.story.id, - this.options.story.get('story_feed_id'), - comment_user_id, reply_id, - _.bind(function(data) { - this.options.story_comments_view.replace_comment(this, data); - }, this), _.bind(function(data) { - var message = data && data.message || "Sorry, this reply could not be deleted."; - var $error = $.make('div', { className: 'NB-error' }, message); - $submit.removeClass('NB-disabled').text('Post'); - $delete_button.removeClass('NB-disabled').text('Delete'); - $form.find('.NB-error').remove(); - $form.append($error); - if (NEWSBLUR.app.story_list) { - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - } - }, this)); + this.options.story.get('story_feed_id'), + comment_user_id, reply_id, + _.bind(function (data) { + this.options.story_comments_view.replace_comment(this, data); + }, this), _.bind(function (data) { + var message = data && data.message || "Sorry, this reply could not be deleted."; + var $error = $.make('div', { className: 'NB-error' }, message); + $submit.removeClass('NB-disabled').text('Post'); + $delete_button.removeClass('NB-disabled').text('Delete'); + $form.find('.NB-error').remove(); + $form.append($error); + if (NEWSBLUR.app.story_list) { + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + } + }, this)); }, - - like_comment: function() { + + like_comment: function () { var comment_user_id = this.model.get('user_id'); var liked = $(".NB-story-comment-like", this.$el).hasClass('NB-active'); - + if (!liked) { - NEWSBLUR.assets.like_comment(this.options.story.id, - this.options.story.get('story_feed_id'), - comment_user_id, _.bind(function(data) { - this.options.story_comments_view.replace_comment(this, data); - }, this)); + NEWSBLUR.assets.like_comment(this.options.story.id, + this.options.story.get('story_feed_id'), + comment_user_id, _.bind(function (data) { + this.options.story_comments_view.replace_comment(this, data); + }, this)); } else { - NEWSBLUR.assets.remove_like_comment(this.options.story.id, - this.options.story.get('story_feed_id'), - comment_user_id, _.bind(function(data) { - this.options.story_comments_view.replace_comment(this, data); - }, this)); + NEWSBLUR.assets.remove_like_comment(this.options.story.id, + this.options.story.get('story_feed_id'), + comment_user_id, _.bind(function (data) { + this.options.story_comments_view.replace_comment(this, data); + }, this)); } } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/social_page/social_page_comments.js b/media/js/newsblur/social_page/social_page_comments.js index bc5ae81857..67a78ff045 100644 --- a/media/js/newsblur/social_page/social_page_comments.js +++ b/media/js/newsblur/social_page/social_page_comments.js @@ -1,27 +1,27 @@ NEWSBLUR.Views.SocialPageComments = Backbone.View.extend({ - + events: { - "click .NB-story-comment-reply-button" : "check_reply_or_login", - "click .NB-story-comment-input" : "check_comment_or_login", - "click .NB-story-comment-save" : "check_comment_or_login", - "click .NB-story-comment-like" : "check_comment_or_login", + "click .NB-story-comment-reply-button": "check_reply_or_login", + "click .NB-story-comment-input": "check_comment_or_login", + "click .NB-story-comment-save": "check_comment_or_login", + "click .NB-story-comment-like": "check_comment_or_login", "click .NB-story-comments-public-teaser": "load_public_story_comments" }, - - initialize: function() { + + initialize: function () { this.story_view = this.options.story_view; this.page_view = this.options.page_view; - + if (NEWSBLUR.Globals.is_authenticated) { var $comments = this.$('.NB-story-comment'); this.attach_comments($comments); } }, - - attach_comments: function($comments) { + + attach_comments: function ($comments) { var self = this; - - $comments.each(function() { + + $comments.each(function () { var $comment = $(this); var comment = new NEWSBLUR.Models.Comment({ id: $comment.data('id'), @@ -38,12 +38,12 @@ NEWSBLUR.Views.SocialPageComments = Backbone.View.extend({ }); }); }, - + // ========== // = Events = // ========== - - check_reply_or_login: function(e) { + + check_reply_or_login: function (e) { if (!NEWSBLUR.Globals.is_authenticated) { e.preventDefault(); e.stopPropagation(); @@ -51,8 +51,8 @@ NEWSBLUR.Views.SocialPageComments = Backbone.View.extend({ return false; } }, - - check_comment_or_login: function(e) { + + check_comment_or_login: function (e) { if (!NEWSBLUR.Globals.is_authenticated) { e.preventDefault(); e.stopPropagation(); @@ -62,20 +62,20 @@ NEWSBLUR.Views.SocialPageComments = Backbone.View.extend({ return false; } }, - - load_public_story_comments: function() { + + load_public_story_comments: function () { $.get('/social/public_comments', { story_id: this.model.id, feed_id: this.model.get('story_feed_id'), user_id: NEWSBLUR.Globals.blurblog_user_id, format: "html" - }, _.bind(function(template) { + }, _.bind(function (template) { var $template = $($.trim(template)); - var $header = $.make('div', { - className: 'NB-story-comments-public-header-wrapper' + var $header = $.make('div', { + className: 'NB-story-comments-public-header-wrapper' }, [ - $.make('div', { - className: 'NB-story-comments-public-header' + $.make('div', { + className: 'NB-story-comments-public-header' }, Inflector.pluralize(' public comment', $('.NB-story-comment', $template).length, true)) ]); @@ -83,25 +83,25 @@ NEWSBLUR.Views.SocialPageComments = Backbone.View.extend({ $template.before($header); }, this)); }, - - replace_comments: function($new_comments) { + + replace_comments: function ($new_comments) { this.$el.replaceWith($new_comments); this.setElement($new_comments); this.initialize(); }, - - replace_comment: function(comment_view, html) { + + replace_comment: function (comment_view, html) { if (html && html.code < 0) { console.log(["error", html]); return; } var $new_comment = $(html); - + comment_view.$el.replaceWith($new_comment); comment_view.remove(); this.story_view.attach_tooltips(); this.attach_comments($new_comment); } - + }); diff --git a/media/js/newsblur/social_page/social_page_extensions.js b/media/js/newsblur/social_page/social_page_extensions.js index e827cf1e8d..7598d0bb89 100644 --- a/media/js/newsblur/social_page/social_page_extensions.js +++ b/media/js/newsblur/social_page/social_page_extensions.js @@ -5,7 +5,7 @@ $.fn.extend({ // element to be absolutely positioned. Element must be visible. // Position string format is: "top -right". // You can pass an optional offset object with top and left offsets specified. - align : function(target, pos, offset) { + align: function (target, pos, offset) { var el = this; pos = pos || ''; offset = offset || {}; @@ -16,25 +16,25 @@ $.fn.extend({ if (target == window) { var b = { - left : scrollLeft, - top : scrollTop, - width : $(window).width(), - height : $(window).height() + left: scrollLeft, + top: scrollTop, + width: $(window).width(), + height: $(window).height() }; } else { target = $(target); var targOff = target.offset(); var b = { - left : targOff.left, - top : targOff.top, - width : target.innerWidth(), - height : target.innerHeight() + left: targOff.left, + top: targOff.top, + width: target.innerWidth(), + height: target.innerHeight() }; } var elb = { - width : el.innerWidth(), - height : el.innerHeight() + width: el.innerWidth(), + height: el.innerHeight() }; var left, top; @@ -79,7 +79,7 @@ $.fn.extend({ // top -= offParent.offset().top; // } - $(el).css({position : 'absolute', left : left + 'px', top : top + 'px'}); + $(el).css({ position: 'absolute', left: left + 'px', top: top + 'px' }); return el; } diff --git a/media/js/newsblur/social_page/social_page_login_view.js b/media/js/newsblur/social_page/social_page_login_view.js index 467d53af13..88965ef684 100644 --- a/media/js/newsblur/social_page/social_page_login_view.js +++ b/media/js/newsblur/social_page/social_page_login_view.js @@ -1,23 +1,23 @@ NEWSBLUR.Views.SocialPageLoginSignupView = Backbone.View.extend({ - + events: { - "click .NB-user-tab" : "open_user_dropdown", - "tap .NB-user-tab" : "open_user_dropdown", - "click .NB-menu-logout" : "logout", - "click .NB-menu-newsblur" : "open_in_newsblur", - "click .NB-login-button" : "login", - "click .NB-signup-button" : "signup", - "click .NB-switch-login-button" : "switch_login", - "click .NB-switch-signup-button" : "switch_signup", - "keypress .NB-login input" : "maybe_login", - "keypress .NB-signup input" : "maybe_signup" - }, - - initialize: function() { + "click .NB-user-tab": "open_user_dropdown", + "tap .NB-user-tab": "open_user_dropdown", + "click .NB-menu-logout": "logout", + "click .NB-menu-newsblur": "open_in_newsblur", + "click .NB-login-button": "login", + "click .NB-signup-button": "signup", + "click .NB-switch-login-button": "switch_login", + "click .NB-switch-signup-button": "switch_signup", + "keypress .NB-login input": "maybe_login", + "keypress .NB-signup input": "maybe_signup" + }, + + initialize: function () { this.setup_login_popover(); }, - - setup_login_popover: function() { + + setup_login_popover: function () { this.login_popover = this.$(".NB-user-tab").clickover({ html: true, placement: "bottom", @@ -27,36 +27,36 @@ NEWSBLUR.Views.SocialPageLoginSignupView = Backbone.View.extend({ onHidden: _.bind(this.on_hide_popover, this) }); }, - - on_show_popover: function() { + + on_show_popover: function () { this.$('.NB-user-tab').addClass('NB-active'); this.$('.popover input[name=login_username]').focus(); }, - - on_hide_popover: function() { + + on_hide_popover: function () { this.$('.NB-user-tab').removeClass('NB-active'); }, - - toggle_login_dialog: function(options) { + + toggle_login_dialog: function (options) { options = options || {}; - - _.defer(_.bind(function() { + + _.defer(_.bind(function () { this.login_popover.data('clickover').clickery(); }, this)); }, - - open_user_dropdown: function(e) { + + open_user_dropdown: function (e) { e.preventDefault(); - - if (e.currentTarget != e.target && + + if (e.currentTarget != e.target && $(e.target).closest('.NB-tab-inner').parent().get(0) != e.currentTarget) { return; } var $button = this.$(".NB-user-tab"); - + if (!$button.hasClass('open')) { - _.defer(function() { - $('html').one('click', function() { + _.defer(function () { + $('html').one('click', function () { $button.removeClass('open'); }); }); @@ -65,107 +65,107 @@ NEWSBLUR.Views.SocialPageLoginSignupView = Backbone.View.extend({ } else { $button.removeClass('open'); } - + }, - + // ========== // = Events = // ========== - - open_in_newsblur: function(e) { + + open_in_newsblur: function (e) { console.log(["open_in_newsblur", e]); e.preventDefault(); window.location.href = NEWSBLUR.URLs.newsblur_page; }, - - clean: function() { + + clean: function () { this.$('.NB-error').remove(); }, - - maybe_login: function(e) { + + maybe_login: function (e) { if (e.keyCode == 13) { this.login(); } }, - - maybe_signup: function(e) { + + maybe_signup: function (e) { if (e.keyCode == 13) { this.signup(); } }, - - login: function() { + + login: function () { this.clean(); - + var username = this.$('.popover input[name=login_username]').val(); var password = this.$('.popover input[name=login_password]').val(); - + NEWSBLUR.assets.login(username, password, _.bind(this.post_login, this), _.bind(this.login_error, this)); }, - - post_login: function(data) { + + post_login: function (data) { NEWSBLUR.log(["login data", data]); window.location.reload(); }, - - login_error: function(data) { + + login_error: function (data) { this.clean(); - + var error = _.first(_.values(data.errors))[0]; this.error(error); }, - - logout: function(e) { + + logout: function (e) { e.preventDefault(); NEWSBLUR.assets.logout(_.bind(this.post_logout, this), _.bind(this.logout_error, this)); }, - - post_logout: function(data) { + + post_logout: function (data) { window.location.reload(); }, - - logout_error: function(data) { + + logout_error: function (data) { alert('There was an error trying to logout, ouch.'); }, - - signup: function() { - this.clean(); - var username = this.$('.popover input[name=signup_username]').val(); - var email = this.$('.popover input[name=signup_email]').val(); - var password = this.$('.popover input[name=signup_password]').val(); - + + signup: function () { + this.clean(); + var username = this.$('.popover input[name=signup_username]').val(); + var email = this.$('.popover input[name=signup_email]').val(); + var password = this.$('.popover input[name=signup_password]').val(); + NEWSBLUR.assets.signup(username, email, password, _.bind(this.post_signup, this), _.bind(this.signup_error, this)); }, - - post_signup: function(data) { + + post_signup: function (data) { window.location.reload(); }, - - signup_error: function(data) { + + signup_error: function (data) { this.clean(); - + var error = _.first(_.values(data.errors))[0]; this.error(error); }, - - error: function(message) { + + error: function (message) { this.$('.popover .popover-title').append($.make('div', { className: 'NB-error' }, message)); }, - - switch_signup: function() { + + switch_signup: function () { this.clean(); this.$(".popover").removeClass("NB-show-signup") - .removeClass("NB-show-login") - .addClass("NB-show-signup"); + .removeClass("NB-show-login") + .addClass("NB-show-signup"); this.$('.popover input[name=signup_username]').focus(); }, - - switch_login: function() { + + switch_login: function () { this.clean(); this.$(".popover").removeClass("NB-show-signup") - .removeClass("NB-show-login") - .addClass("NB-show-login"); + .removeClass("NB-show-login") + .addClass("NB-show-login"); this.$('.popover input[name=login_username]').focus(); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/social_page/social_page_router.js b/media/js/newsblur/social_page/social_page_router.js index 5ab593500e..1dd1aacc72 100644 --- a/media/js/newsblur/social_page/social_page_router.js +++ b/media/js/newsblur/social_page/social_page_router.js @@ -1,6 +1,6 @@ NEWSBLUR.Router = Backbone.Router.extend({ - - routes : { + + routes: { "": "index", "story/:slug/:guid": "story_slug", "story/:slug/:guid/": "story_slug", @@ -9,21 +9,21 @@ NEWSBLUR.Router = Backbone.Router.extend({ "site/:feed_id": "site", "site/:feed_id/": "site" }, - - index: function() { - + + index: function () { + }, - - story: function(guid) { + + story: function (guid) { this.story_guid = guid.replace(/\?(.*)$/, ''); }, - - story_slug: function(slug, guid) { + + story_slug: function (slug, guid) { this.story_guid = guid.replace(/\?(.*)$/, ''); }, - - site: function(feed_id) { + + site: function (feed_id) { this.feed_id = feed_id; } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/social_page/social_page_shares_view.js b/media/js/newsblur/social_page/social_page_shares_view.js index 8610bf8d1b..206d2b88fd 100644 --- a/media/js/newsblur/social_page/social_page_shares_view.js +++ b/media/js/newsblur/social_page/social_page_shares_view.js @@ -1,15 +1,15 @@ NEWSBLUR.Views.SocialPageSharesView = Backbone.View.extend({ - + events: {}, - - initialize: function() { + + initialize: function () { this.story_view = this.options.story_view; }, - - replace_shares: function($new_shares) { + + replace_shares: function ($new_shares) { this.$el.replaceWith($new_shares); this.setElement($new_shares); this.initialize(); } - + }); diff --git a/media/js/newsblur/social_page/social_page_story.js b/media/js/newsblur/social_page/social_page_story.js index aa2d0c8685..165acfb8ee 100644 --- a/media/js/newsblur/social_page/social_page_story.js +++ b/media/js/newsblur/social_page/social_page_story.js @@ -1,26 +1,26 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ - + FUDGE_CONTENT_HEIGHT_OVERAGE: 250, - + STORY_CONTENT_MAX_HEIGHT: 400, // ALSO CHANGE IN social_page.css - + flags: {}, - + events: { - "click .NB-story-content-expander" : "expand_story", - "focus .NB-story-comment-input" : "focus_comment_input", + "click .NB-story-content-expander": "expand_story", + "focus .NB-story-comment-input": "focus_comment_input", // "blur .NB-story-comment-input" : "blur_comment_input" - "keyup .NB-story-comment-input" : "keypress_comment_input", - "click .NB-story-comment-save" : "mark_story_as_shared", - "click .NB-story-comment-crosspost-twitter" : "toggle_twitter", - "click .NB-story-comment-crosspost-facebook" : "toggle_facebook" + "keyup .NB-story-comment-input": "keypress_comment_input", + "click .NB-story-comment-save": "mark_story_as_shared", + "click .NB-story-comment-crosspost-twitter": "toggle_twitter", + "click .NB-story-comment-crosspost-facebook": "toggle_facebook" }, - - initialize: function() { + + initialize: function () { var story_id = this.$el.data("storyId"); var feed_id = this.$el.data("feedId"); // attr because .data munges numeral guids (ex: 002597 vs. a05bd2) - var story_guid = ""+this.$el.attr("data-guid"); + var story_guid = "" + this.$el.attr("data-guid"); var user_comments = this.$el.data("userComments"); var shared = this.$el.hasClass('NB-story-shared'); var $sideoptions = this.$('.NB-feed-story-sideoptions-container'); @@ -30,7 +30,7 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ shared_comments: user_comments, shared: shared }); - + this.story_guid = story_guid; this.comments_view = new NEWSBLUR.Views.SocialPageComments({ @@ -48,9 +48,9 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ }); this.model.social_page_shares = this.shares_view; this.model.social_page_story = this; - + if (NEWSBLUR.Globals.is_authenticated) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.share_view = new NEWSBLUR.Views.StoryShareView({ el: this.el, model: this.model, @@ -63,15 +63,15 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ }))); }, this), 50); } - + this.$mark = this.$el.closest('.NB-mark'); this.attach_tooltips(); this.attach_keyboard(); this.truncate_story_height(); this.watch_images_for_story_height(); }, - - attach_tooltips: function() { + + attach_tooltips: function () { this.$('.NB-user-avatar').tipsy({ delayIn: 50, gravity: 's', @@ -79,25 +79,25 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ offset: 3 }); }, - - attach_keyboard: function() { + + attach_keyboard: function () { var $input = this.$('.NB-story-comment-input'); - + $input.bind('keydown', 'esc', _.bind(this.blur_comment_input, this)); $input.bind('keydown', 'meta+return', _.bind(this.mark_story_as_shared, this)); $input.bind('keydown', 'ctrl+return', _.bind(this.mark_story_as_shared, this)); }, - - truncate_story_height: function() { + + truncate_story_height: function () { var $expander = this.$(".NB-story-content-expander"); var $expander_cutoff = this.$(".NB-story-cutoff"); var $wrapper = this.$(".NB-story-content-wrapper"); var $content = this.$(".NB-story-content"); - + var max_height = parseInt($wrapper.css('maxHeight'), 10) || this.STORY_CONTENT_MAX_HEIGHT; var content_height = $content.outerHeight(true); - - if (content_height > max_height && + + if (content_height > max_height && content_height < max_height + this.FUDGE_CONTENT_HEIGHT_OVERAGE) { // console.log(["Height over but within fudge", content_height, max_height]); $wrapper.addClass('NB-story-content-wrapper-height-fudged'); @@ -107,33 +107,33 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ $wrapper.removeClass('NB-story-content-wrapper-height-fudged'); $wrapper.addClass('NB-story-content-wrapper-height-truncated'); var pages = Math.round(content_height / max_height, true); - var dots = _.map(_.range(pages), function() { return '·'; }).join(' '); - + var dots = _.map(_.range(pages), function () { return '·'; }).join(' '); + // console.log(["Height over, truncating...", content_height, max_height, pages]); this.$(".NB-story-content-expander-pages").html(dots); } else { // console.log(["Height under.", content_height, max_height]); } }, - - watch_images_for_story_height: function() { - this.$('img').on('load', _.bind(function() { + + watch_images_for_story_height: function () { + this.$('img').on('load', _.bind(function () { this.truncate_story_height(); }, this)); }, - - story_url: function() { + + story_url: function () { var guid = this.story_guid.substr(0, 6); var url = window.location.protocol + '//' + window.location.host + '/story/' + guid; return url; }, - + // =========== // = Actions = // =========== - - replace_shares_and_comments: function(html) { + + replace_shares_and_comments: function (html) { var $new_story = $(html); var $new_comments = $('.NB-story-comments', $new_story); var $new_shares = $('.NB-story-shares', $new_story); @@ -143,12 +143,12 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ this.attach_tooltips(); this.attach_keyboard(); }, - - mark_story_as_shared: function(options) { + + mark_story_as_shared: function (options) { options = options || {}; var $input = this.$('.NB-story-comment-input'); var $submit = this.$('.NB-story-comment-save'); - + var comments = _.string.trim($input.val()); var source_user_id = NEWSBLUR.Globals.blurblog_user_id; var relative_user_id = NEWSBLUR.Globals.blurblog_user_id; @@ -156,48 +156,48 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ NEWSBLUR.assets.preference('post_to_twitter') && 'twitter', NEWSBLUR.assets.preference('post_to_facebook') && 'facebook' ]); - + $submit.addClass('NB-saving').addClass('NB-disabled').text('Sharing...'); var data = { - story_id: this.model.id, - story_feed_id: this.model.get('story_feed_id'), - comments: comments, + story_id: this.model.id, + story_feed_id: this.model.get('story_feed_id'), + comments: comments, source_user_id: source_user_id, relative_user_id: relative_user_id, post_to_services: post_to_services }; - NEWSBLUR.assets.mark_story_as_shared(data, _.bind(this.post_share_story, this, true), _.bind(function(data) { + NEWSBLUR.assets.mark_story_as_shared(data, _.bind(this.post_share_story, this, true), _.bind(function (data) { this.post_share_error(data, true); }, this)); - + if (NEWSBLUR.reader) { NEWSBLUR.reader.blur_to_page(); } }, - - post_share_story: function(shared, data) { + + post_share_story: function (shared, data) { this.model.set("shared", shared); - + this.$el.toggleClass('NB-story-shared', this.model.get('shared')); this.replace_shares_and_comments(data); }, - - post_share_error: function(data, shared) { + + post_share_error: function (data, shared) { var $share_button = this.$('.NB-sideoption-share-save'); var $unshare_button = this.$('.NB-sideoption-share-unshare'); var $share_button_menu = $('.NB-menu-manage .NB-menu-manage-story-share-save'); var message = data && data.message || ("Sorry, this story could not be " + (shared ? "" : "un") + "shared. Probably Adblock."); - + if (!NEWSBLUR.Globals.is_authenticated) { message = "You need to be logged in to share a story."; } var $error = $.make('div', { className: 'NB-error' }, message); - + $share_button.removeClass('NB-saving').removeClass('NB-disabled').text('Share'); $unshare_button.removeClass('NB-saving').removeClass('NB-disabled').text('Delete Share'); $share_button.siblings('.NB-error').remove(); $share_button.after($error); - + if ($share_button_menu.length) { $share_button_menu.removeClass('NB-disabled').text('Share'); $share_button_menu.siblings('.NB-error').remove(); @@ -205,28 +205,28 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ } NEWSBLUR.log(["post_share_error", data, shared, message, $share_button, $unshare_button, $share_button_menu, $error]); }, - - mark_story_as_unshared: function(options) { + + mark_story_as_unshared: function (options) { options = options || {}; var $unshare_button = this.$('.NB-story-comment-delete'); $unshare_button.addClass('NB-saving').addClass('NB-disabled').text('Deleting...'); - + var params = { - story_id: this.model.id, + story_id: this.model.id, story_feed_id: this.model.get('story_feed_id'), relative_user_id: NEWSBLUR.Globals.blurblog_user_id }; - NEWSBLUR.assets.mark_story_as_unshared(params, _.bind(this.post_share_story, this, false), _.bind(function(data) { + NEWSBLUR.assets.mark_story_as_unshared(params, _.bind(this.post_share_story, this, false), _.bind(function (data) { this.post_share_error(data, false); }, this)); }, - - check_crosspost_buttons: function() { + + check_crosspost_buttons: function () { var $twitter = this.$('.NB-story-comment-crosspost-twitter'); var $facebook = this.$('.NB-story-comment-crosspost-facebook'); if (!NEWSBLUR.user_social_services) return; - + if (NEWSBLUR.user_social_services.twitter && NEWSBLUR.user_social_services.twitter.twitter_uid) { $twitter.removeClass('NB-hidden'); @@ -235,16 +235,16 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ NEWSBLUR.user_social_services.facebook.facebook_uid) { $facebook.removeClass('NB-hidden'); } - + $twitter.toggleClass('NB-active', !!NEWSBLUR.assets.preference('post_to_twitter')); $facebook.toggleClass('NB-active', !!NEWSBLUR.assets.preference('post_to_facebook')); }, - + // ========== // = Events = // ========== - - expand_story: function(options) { + + expand_story: function (options) { options = options || {}; var $expander = this.$(".NB-story-content-expander"); var $expander_cutoff = this.$(".NB-story-cutoff"); @@ -253,7 +253,7 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ var max_height = parseInt($wrapper.css('maxHeight'), 10) || this.STORY_CONTENT_MAX_HEIGHT; var content_height = $content.outerHeight(true); var height_ratio = content_height / max_height; - + if (content_height < max_height) return; $wrapper.removeClass('NB-story-content-wrapper-height-truncated'); // console.log(["max height", max_height, content_height, content_height / max_height]); @@ -263,22 +263,22 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ duration: options.instant ? 0 : Math.min(2 * 1000, parseInt(200 * height_ratio, 10)), easing: 'easeInOutQuart' }); - + $expander.add($expander_cutoff).animate({ bottom: -1 * $expander.outerHeight() - 48 }, { duration: options.instant ? 0 : Math.min(2 * 1000, parseInt(200 * height_ratio, 10)), easing: 'easeInOutQuart' }); - + }, - - focus_comment_input: function() { + + focus_comment_input: function () { console.log("in focus_comment_input"); var $form = this.$('.NB-story-comment-input-form'); var $input = this.$('.NB-story-comment-input'); var $buttons = this.$('.NB-story-comment-buttons'); - + // $form.toggleClass('NB-active', $input.is(':focus')); $buttons.css('display', 'block'); $form.addClass('NB-active'); @@ -286,8 +286,8 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ this.keypress_comment_input(); this.reset_posting_label(); }, - - blur_comment_input: function() { + + blur_comment_input: function () { var $buttons = this.$('.NB-story-comment-buttons'); var $form = this.$('.NB-story-comment-input-form'); var $input = this.$('.NB-story-comment-input'); @@ -295,71 +295,71 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ $buttons.css('display', 'none'); $form.removeClass('NB-active'); $input.blur(); - + if (this.model.get('shared')) { this.$('.NB-story-comment.NB-hidden').removeClass('NB-hidden'); this.$('.NB-story-comment-edit').addClass('NB-hidden'); } }, - - keypress_comment_input: function() { + + keypress_comment_input: function () { var $input = this.$('.NB-story-comment-input'); var $save = this.$('.NB-story-comment-save'); - + if (!_.string.isBlank($input.val())) { $save.text('Share with comments'); } else { $save.text("Share this story"); } - + var input_width = $input.innerWidth(); // Perform auto-height expansion }, - - toggle_twitter: function() { + + toggle_twitter: function () { var $twitter_button = this.$('.NB-story-comment-crosspost-twitter'); - + if (NEWSBLUR.assets.preference('post_to_twitter')) { NEWSBLUR.assets.preference('post_to_twitter', false); } else { NEWSBLUR.assets.preference('post_to_twitter', true); } - + $twitter_button.toggleClass('NB-active', NEWSBLUR.assets.preference('post_to_twitter')); this.reset_posting_label(); }, - - toggle_facebook: function() { + + toggle_facebook: function () { var $facebook_button = this.$('.NB-story-comment-crosspost-facebook'); - + if (NEWSBLUR.assets.preference('post_to_facebook')) { NEWSBLUR.assets.preference('post_to_facebook', false); } else { NEWSBLUR.assets.preference('post_to_facebook', true); } - + $facebook_button.toggleClass('NB-active', NEWSBLUR.assets.preference('post_to_facebook')); this.reset_posting_label(); }, - - show_twitter_posting_label: function() { + + show_twitter_posting_label: function () { this.show_posting_label(true, false); }, - - show_facebook_posting_label: function() { + + show_facebook_posting_label: function () { this.show_posting_label(false, true); }, - - reset_posting_label: function() { + + reset_posting_label: function () { this.show_posting_label(); }, - - show_posting_label: function(twitter, facebook) { + + show_posting_label: function (twitter, facebook) { var social_services = NEWSBLUR.user_social_services || {}; var $text = this.$('.NB-story-comment-crosspost-text'); twitter = twitter || (social_services.twitter && social_services.twitter.twitter_uid && NEWSBLUR.assets.preference('post_to_twitter')); facebook = facebook || (social_services.facebook && social_services.facebook.facebook_uid && NEWSBLUR.assets.preference('post_to_facebook')); - + if (twitter || facebook) { var message = "Post to "; if (twitter && !facebook) { @@ -369,11 +369,11 @@ NEWSBLUR.Views.SocialPageStory = Backbone.View.extend({ } else { message += "Twitter & FB"; } - + $text.text(message); } else { $text.text(""); } } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/dashboard_river_view.js b/media/js/newsblur/views/dashboard_river_view.js index 5689f9caac..ae08a8d097 100644 --- a/media/js/newsblur/views/dashboard_river_view.js +++ b/media/js/newsblur/views/dashboard_river_view.js @@ -1,20 +1,20 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ - + events: { - "click .NB-module-search-add-url" : "add_url", - "click .NB-feedbar-options" : "open_options_popover", - "click .NB-module-river-favicon" : "reload", + "click .NB-module-search-add-url": "add_url", + "click .NB-feedbar-options": "open_options_popover", + "click .NB-module-river-favicon": "reload", "click .NB-module-river-title": "open_river", "click .NB-dashboard-column-option": "choose_columns" }, - + initialize: function () { var $river_on_dashboard = $(".NB-dashboard-rivers-" + this.model.get('river_side') + " .NB-dashboard-river-order-" + this.model.get('river_order')); // console.log(['Initialize dashboard river', this.model, this.$el, this.el, $river_on_dashboard]) // if ($river_on_dashboard.length) { // this.setElement($river_on_dashboard); // } - + if (this.model.get('river_id') == "river:infrequent") { this.options.infrequent = NEWSBLUR.assets.preference('infrequent_stories_per_month'); } else if (this.model.get('river_id') == "river:global") { @@ -77,9 +77,9 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ this.render_columns(); this.$stories = this.$(".NB-module-item .NB-story-titles"); - + // console.log(['dashboard stories view', this.$stories, this.options, this.$stories.el]); - + this.story_titles = new NEWSBLUR.Views.StoryTitlesView({ el: this.$stories.get(0), collection: this.options.dashboard_stories, @@ -94,13 +94,13 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ this.setup_dashboard_refresh(); this.load_stories(); this.options_template(); - + return this; }, render_columns: function () { var columns = NEWSBLUR.assets.preference('dashboard_columns'); - + this.$(".NB-dashboard-columns-control-1").toggleClass('NB-active', columns == 1); this.$(".NB-dashboard-columns-control-2").toggleClass('NB-active', columns == 2); this.$(".NB-dashboard-columns-control-3").toggleClass('NB-active', columns == 3); @@ -122,10 +122,10 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({
', { feed_id: this.model.get('river_id') })); - + this.$(".NB-module-river-settings").html($options); }, - + feeds: function (include_read) { var river_id = this.model.get('river_id'); @@ -142,7 +142,7 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ if (_.string.startsWith(river_id, 'search:river')) { river_id = river_id.substring("search:".length, river_id.lastIndexOf(":")); } - + var active_folder = NEWSBLUR.assets.get_folder(river_id); if (!active_folder) { active_folder = NEWSBLUR.assets.folders; @@ -163,22 +163,22 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ open_river: function () { this.open_story(); }, - + // =========== // = Refresh = // =========== - - setup_dashboard_refresh: function() { + + setup_dashboard_refresh: function () { if (NEWSBLUR.Globals.debug) return; - + // Reload dashboard graphs every N minutes. - var reload_interval = NEWSBLUR.Globals.is_staff ? 15*60*1000 : 15*60*1000; + var reload_interval = NEWSBLUR.Globals.is_staff ? 15 * 60 * 1000 : 15 * 60 * 1000; // var reload_interval = 60*60*1000; // console.log(['setup_dashboard_refresh', this.refresh_interval]); - + clearTimeout(this.refresh_interval); this.refresh_interval = setTimeout(_.bind(function () { - + if (NEWSBLUR.reader.flags['deactivate_refresh_dashboard']) { console.log(['...NOT refreshing dashboard', this.model.get('river_id')]); return; @@ -194,13 +194,13 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ }, this), reload_interval * (Math.random() * (1.25 - 0.75) + 0.75)); }, - + // ========== // = Events = // ========== - + redraw: function () { - this.story_titles.render({immediate: true}); + this.story_titles.render({ immediate: true }); }, reload: function () { @@ -238,29 +238,29 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ query: this.options.query, }, options || {}); if (options.feed_selector) return; - + var feeds = this.feeds(); if (!feeds.length) return; if (!this.$stories.length) return; if (this.model.get('river_id') == "river:global") feeds = []; - + // console.log(['dashboard river load_stories', this.model.get('river_id'), this.page, feeds, options, this.$stories.length]); this.page = 1; this.story_titles.show_loading(); NEWSBLUR.assets.fetch_dashboard_stories(this.model.get('river_id'), feeds, this.page, this.options.dashboard_stories, options, _.bind(this.post_load_stories, this), NEWSBLUR.app.taskbar_info.show_stories_error); - + this.setup_dashboard_refresh(); }, - + post_load_stories: function (data) { // console.log(['post_load_stories', this.model.get('river_id'), this.options.dashboard_stories.length, data, data.stories.length]) this.story_titles.end_loading(); this.fill_out({ new_stories: data.stories.length }); this.cache.story_hashes = this.options.dashboard_stories.pluck('story_hash'); }, - - fill_out: function(options) { + + fill_out: function (options) { options = _.extend({ global_feed: this.options.global_feed, infrequent: this.options.infrequent, @@ -285,7 +285,7 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ this.complete_fill(); return; } - + var counts = NEWSBLUR.assets.folders.unread_counts(); var unread_view = NEWSBLUR.assets.preference('unread_view'); if (unread_view >= 1) { @@ -308,7 +308,7 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ return; } } - + if (options.new_stories == 0) { this.complete_fill(); return; @@ -318,10 +318,10 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ this.page += 1; this.story_titles.show_loading(); NEWSBLUR.assets.fetch_dashboard_stories(this.model.get('river_id'), feeds, this.page, this.options.dashboard_stories, options, - _.bind(this.post_load_stories, this), NEWSBLUR.app.taskbar_info.show_stories_error); + _.bind(this.post_load_stories, this), NEWSBLUR.app.taskbar_info.show_stories_error); }, - - check_read_stories: function(story, attr) { + + check_read_stories: function (story, attr) { // console.log(['story read', story, story.get('story_hash'), story.get('read_status'), attr]); if (!_.contains(this.cache.story_hashes, story.get('story_hash'))) return; var dashboard_story = this.options.dashboard_stories.get_by_story_hash(story.get('story_hash')); @@ -329,11 +329,11 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ console.log(['Error: missing story on dashboard', story, this.cache.story_hashes]); return; } - + dashboard_story.set('read_status', story.get('read_status')); // dashboard_story.set('selected', false); }, - + open_story: function (story) { var river_id = this.model.get('river_id'); var options = { @@ -354,7 +354,7 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ } else if (river_id == "river:infrequent") { NEWSBLUR.reader.open_river_stories(null, null, _.extend({ infrequent: this.options.infrequent - }, options)); + }, options)); } else if (river_id == "river:global") { NEWSBLUR.reader.open_river_blurblogs_stories(_.extend({ global: true @@ -374,31 +374,31 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ NEWSBLUR.reader.open_feed(river_id, options); } }, - - show_end_line: function() { + + show_end_line: function () { this.story_titles.show_no_more_stories(); this.$(".NB-end-line").addClass("NB-visible"); }, - + complete_fill: function () { // console.log(['complete_fill', this.model.get('river_id')]) var feeds = this.feeds(); NEWSBLUR.assets.complete_river(this.model.get('river_id'), feeds, this.page); }, - - new_story: function(story_hash, timestamp) { + + new_story: function (story_hash, timestamp) { var current_timestamp = Math.floor(Date.now() / 1000); - if (timestamp > (current_timestamp + 60*60)) { - console.log(['New story newer than current time + 1 hour', - (timestamp - current_timestamp)/60 + " minutes newer"]); + if (timestamp > (current_timestamp + 60 * 60)) { + console.log(['New story newer than current time + 1 hour', + (timestamp - current_timestamp) / 60 + " minutes newer"]); return; } - + var oldest_story = this.options.dashboard_stories.last(); if (oldest_story) { var last_timestamp = parseInt(oldest_story.get('story_timestamp'), 10); timestamp = parseInt(timestamp, 10); - + if (NEWSBLUR.assets.view_setting(this.model.get('river_id'), 'order') == 'newest') { if (timestamp < last_timestamp) { // console.log(['New story older than last/oldest dashboard story', timestamp, '<', last_timestamp]); @@ -411,7 +411,7 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ } } } - + var feed_id = story_hash.split(':')[0]; var feed = NEWSBLUR.assets.get_feed(feed_id); if (!feed) { @@ -431,12 +431,12 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ console.log(['New story not in folder', this.model.get('river_id'), feed_id, this.feeds()]); return; } - + var dashboard_count = parseInt(NEWSBLUR.assets.view_setting(this.model.get('river_id'), 'dashboard_count'), 10); var subs = feed.get('num_subscribers'); var delay = subs * 2; // 1,000 subs = 2 seconds // console.log(['Fetching dashboard story', this.model.get('river_id'), story_hash, delay + 'ms delay', dashboard_count]); - + if (NEWSBLUR.reader.flags['deactivate_new_dashboard_story']) { console.log(['...NOT Fetching dashboard story', this.model.get('river_id')]); return; @@ -445,10 +445,10 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ // _.delay(_.bind(function() { NEWSBLUR.assets.add_dashboard_story(story_hash, this.options.dashboard_stories, dashboard_count); // }, this), Math.random() * delay); - + }, - open_options_popover: function(e) { + open_options_popover: function (e) { NEWSBLUR.FeedOptionsPopover.create({ anchor: this.$(".NB-feedbar-options"), feed_id: this.model.get('river_id'), @@ -458,5 +458,5 @@ NEWSBLUR.Views.DashboardRiver = Backbone.View.extend({ show_markscroll: false }); }, - + }); diff --git a/media/js/newsblur/views/dashboard_rivers_view.js b/media/js/newsblur/views/dashboard_rivers_view.js index 93cfd85adf..3a5c59ff0f 100644 --- a/media/js/newsblur/views/dashboard_rivers_view.js +++ b/media/js/newsblur/views/dashboard_rivers_view.js @@ -1,5 +1,5 @@ NEWSBLUR.Views.DashboardRivers = Backbone.View.extend({ - + el: ".NB-dashboard-rivers", options: { @@ -12,12 +12,12 @@ NEWSBLUR.Views.DashboardRivers = Backbone.View.extend({ this.$el.empty(); this.rivers = NEWSBLUR.assets.dashboard_rivers.side(side).map(_.bind(function (river, r) { var river_view = new NEWSBLUR.Views.DashboardRiver({ - dashboard_stories: new NEWSBLUR.Collections.Stories({dashboard_river_id: river.get('river_id')}), + dashboard_stories: new NEWSBLUR.Collections.Stories({ dashboard_river_id: river.get('river_id') }), model: river }); // console.log(['Adding river', side, river.get('river_id'), river_view, river_view.$el, this.$el]) this.$el.append(river_view.$el); - + return river_view; }, this)); diff --git a/media/js/newsblur/views/dashboard_search.js b/media/js/newsblur/views/dashboard_search.js index 5cdb4d49dd..b523f94b1e 100644 --- a/media/js/newsblur/views/dashboard_search.js +++ b/media/js/newsblur/views/dashboard_search.js @@ -1,35 +1,35 @@ NEWSBLUR.Views.DashboardSearch = Backbone.View.extend({ - + el: ".NB-module-search", - + events: { - "keyup .NB-module-search-sites input" : "search_sites", - "keyup .NB-module-search-people input" : "search_people", - "click .NB-module-search-sites .NB-search-close" : "clear_site", - "click .NB-module-search-people .NB-search-close" : "clear_person", - "click .NB-module-search-add-url" : "add_url" + "keyup .NB-module-search-sites input": "search_sites", + "keyup .NB-module-search-people input": "search_people", + "click .NB-module-search-sites .NB-search-close": "clear_site", + "click .NB-module-search-people .NB-search-close": "clear_person", + "click .NB-module-search-add-url": "add_url" }, - - initialize: function() { + + initialize: function () { this.$site = this.$(".NB-module-search-sites"); this.$site_input = this.$(".NB-module-search-sites input"); this.$person = this.$(".NB-module-search-people"); this.$person_input = this.$(".NB-module-search-people input"); this.$results = this.$(".NB-module-search-results"); - + this.cache = {}; }, - + // ========== // = Events = // ========== - - search_sites: function() { + + search_sites: function () { var query = this.$site_input.val(); - + if (this.cache.site_query == query) return; this.cache.site_query = query; - + if (query == "") { this.$site.removeClass("NB-active"); this.$results.empty(); @@ -37,28 +37,28 @@ NEWSBLUR.Views.DashboardSearch = Backbone.View.extend({ } else { this.$site.addClass("NB-active"); } - + this.$site_input.addClass('NB-active'); this.$site.removeClass("NB-active"); - - NEWSBLUR.assets.search_for_feeds(query, _.bind(function(data) { + + NEWSBLUR.assets.search_for_feeds(query, _.bind(function (data) { this.$site_input.removeClass('NB-active'); this.$site.addClass("NB-active"); if (!data || !data.feeds || !data.feeds.length) { - this.$results.html($.make('div', { - className: 'NB-friends-search-badges-empty NB-feed-badge' + this.$results.html($.make('div', { + className: 'NB-friends-search-badges-empty NB-feed-badge' }, [ $.make('div', { className: 'NB-raquo' }, '»'), - 'Sorry, nothing matches "'+query+'".' + 'Sorry, nothing matches "' + query + '".' ])); } else { - this.$results.html($.make('div', _.map(data.feeds, function(feed) { + this.$results.html($.make('div', _.map(data.feeds, function (feed) { var model = new NEWSBLUR.Models.Feed(feed); - return new NEWSBLUR.Views.FeedBadge({model: model}); + return new NEWSBLUR.Views.FeedBadge({ model: model }); }))); } - + if (query.indexOf('.') != -1) { this.$results.append($.make('div', { className: 'NB-feed-badge' }, [ $.make('div', { className: 'NB-module-search-add-url NB-badge-action-add NB-modal-submit-button NB-modal-submit-green' }, 'Subscribe to ' + query) @@ -66,13 +66,13 @@ NEWSBLUR.Views.DashboardSearch = Backbone.View.extend({ } }, this)); }, - - search_people: function() { + + search_people: function () { var query = this.$person_input.val(); - + if (this.cache.person_query == query) return; this.cache.person_query = query; - + if (query == "") { this.$person.removeClass("NB-active"); this.$results.empty(); @@ -80,47 +80,47 @@ NEWSBLUR.Views.DashboardSearch = Backbone.View.extend({ } else { this.$person.addClass("NB-active"); } - + this.$person_input.addClass('NB-active'); this.$person.removeClass("NB-active"); - - NEWSBLUR.assets.search_for_friends(query, _.bind(function(data) { + + NEWSBLUR.assets.search_for_friends(query, _.bind(function (data) { this.$person_input.removeClass('NB-active'); this.$person.addClass("NB-active"); - + if (!data || !data.profiles || !data.profiles.length) { - this.$results.html($.make('div', { - className: 'NB-friends-search-badges-empty' + this.$results.html($.make('div', { + className: 'NB-friends-search-badges-empty' }, [ $.make('div', { className: 'NB-raquo' }, '»'), - 'Sorry, nobody matches "'+query+'".' + 'Sorry, nobody matches "' + query + '".' ])); return; } - - this.$results.html($.make('div', _.map(data.profiles, function(profile) { + + this.$results.html($.make('div', _.map(data.profiles, function (profile) { var user = new NEWSBLUR.Models.User(profile); - return new NEWSBLUR.Views.SocialProfileBadge({model: user}); + return new NEWSBLUR.Views.SocialProfileBadge({ model: user }); }))); }, this)); }, - - clear_site: function() { + + clear_site: function () { this.$site_input.val(''); this.$results.empty(); this.$site.removeClass('NB-active'); }, - - clear_person: function() { + + clear_person: function () { this.$person_input.val(''); this.$results.empty(); this.$person.removeClass('NB-active'); }, - - add_url: function() { + + add_url: function () { var query = this.$site_input.val(); - NEWSBLUR.reader.open_add_feed_modal({url: query}); + NEWSBLUR.reader.open_add_feed_modal({ url: query }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/feed_badge_view.js b/media/js/newsblur/views/feed_badge_view.js index 667f5fa0b8..f1cac6b159 100644 --- a/media/js/newsblur/views/feed_badge_view.js +++ b/media/js/newsblur/views/feed_badge_view.js @@ -1,28 +1,28 @@ NEWSBLUR.Views.FeedBadge = Backbone.View.extend({ - + className: "NB-feed-badge", - + events: { - "click .NB-badge-action-try" : "try_feed", - "click .NB-badge-action-add" : "add_feed", - "click .NB-icon-stats" : "open_stats" + "click .NB-badge-action-try": "try_feed", + "click .NB-badge-action-add": "add_feed", + "click .NB-icon-stats": "open_stats" }, - - constructor : function(options) { + + constructor: function (options) { Backbone.View.call(this, options); this.render(); return this.el; }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'render'); this.model.bind('change', this.render); }, - - render: function() { + + render: function () { var subscribed = NEWSBLUR.assets.get_feed(this.model.id); - + this.$el.html($.make('div', { className: 'NB-feed-badge-inner' }, [ $.make('div', { className: "NB-feed-badge-title" }, [ $.make('img', { src: $.favicon(this.model) }), @@ -40,12 +40,12 @@ NEWSBLUR.Views.FeedBadge = Backbone.View.extend({ ]), (subscribed && $.make('div', { className: 'NB-subscribed' }, "Subscribed")), (!subscribed && $.make('div', [ - $.make('div', { - className: 'NB-badge-action-try NB-modal-submit-button NB-modal-submit-green' + $.make('div', { + className: 'NB-badge-action-try NB-modal-submit-button NB-modal-submit-green' }, [ $.make('span', 'Try') ]), - $.make('div', { + $.make('div', { className: 'NB-badge-action-add NB-modal-submit-button NB-modal-submit-grey ' }, 'Add') ])) @@ -53,20 +53,20 @@ NEWSBLUR.Views.FeedBadge = Backbone.View.extend({ return this; }, - - try_feed: function() { + + try_feed: function () { NEWSBLUR.reader.load_feed_in_tryfeed_view(this.model.id); }, - - add_feed: function() { - NEWSBLUR.reader.open_add_feed_modal({url: this.model.get('feed_address')}); + + add_feed: function () { + NEWSBLUR.reader.open_add_feed_modal({ url: this.model.get('feed_address') }); }, - - open_stats: function() { - NEWSBLUR.assets.load_canonical_feed(this.model.id, _.bind(function() { + + open_stats: function () { + NEWSBLUR.assets.load_canonical_feed(this.model.id, _.bind(function () { NEWSBLUR.reader.open_feed_statistics_modal(this.model.id); }, this)); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/feed_list_view.js b/media/js/newsblur/views/feed_list_view.js index 22259584ea..7245ea436f 100644 --- a/media/js/newsblur/views/feed_list_view.js +++ b/media/js/newsblur/views/feed_list_view.js @@ -1,47 +1,47 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ - + tagName: 'ul', - + className: 'folder NB-feedlist', - + options: { sorting: "alphabetical" }, - - initialize: function() { + + initialize: function () { this.$s = NEWSBLUR.reader.$s; - + if (!this.$el.length) return; if (this.options.feed_chooser) { this.$el.addClass('NB-feedchooser'); this.$el.addClass('unread_view_positive'); - if (this.options.organizer) { + if (this.options.organizer) { this.$el.attr('id', 'NB-organizer-feeds'); } else { this.$el.attr('id', 'NB-feedchooser-feeds'); } return; } - + $('.NB-callout-ftux .NB-callout-text').text('Loading feeds...'); - this.$s.$feed_link_loader.css({'display': 'block'}); - this.$s.$feed_link_error.css({'display': 'none'}); - NEWSBLUR.assets.feeds.bind('reset', _.bind(function(options) { + this.$s.$feed_link_loader.css({ 'display': 'block' }); + this.$s.$feed_link_error.css({ 'display': 'none' }); + NEWSBLUR.assets.feeds.bind('reset', _.bind(function (options) { this.make_feeds(options); - + // TODO: Refactor this to load after both feeds and social feeds load. this.load_router(); this.show_read_stories_header(); this.update_dashboard_count(); this.scroll_to_selected(); }, this)); - NEWSBLUR.assets.social_feeds.bind('reset', _.bind(function() { + NEWSBLUR.assets.social_feeds.bind('reset', _.bind(function () { this.make_social_feeds(); }, this)); - NEWSBLUR.assets.starred_feeds.bind('reset', _.bind(function(models, options) { + NEWSBLUR.assets.starred_feeds.bind('reset', _.bind(function (models, options) { this.make_starred_tags(options); }, this)); - NEWSBLUR.assets.searches_feeds.bind('reset', _.bind(function(models, options) { + NEWSBLUR.assets.searches_feeds.bind('reset', _.bind(function (models, options) { this.make_saved_searches(options); }, this)); NEWSBLUR.assets.social_feeds.bind('change:selected', this.scroll_to_selected, this); @@ -53,7 +53,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ } NEWSBLUR.assets.feeds.bind('add', this.update_dashboard_count, this); NEWSBLUR.assets.feeds.bind('remove', this.update_dashboard_count, this); - + $('.NB-feeds-header-river-global .NB-feeds-header-icon').attr('src', $.favicon('river:global')); $('.NB-feeds-header-river-blurblogs .NB-feeds-header-icon').attr('src', $.favicon('river:blurblogs')); $('.NB-feeds-header-river-infrequent .NB-feeds-header-icon').attr('src', $.favicon('river:infrequent')); @@ -63,50 +63,50 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ $('.NB-feeds-header-read .NB-feeds-header-icon').attr('src', $.favicon('read')); $('.NB-feeds-header-starred .NB-feeds-header-icon').attr('src', $.favicon('starred')); }, - - make_feeds: function(options) { + + make_feeds: function (options) { options = options || {}; var self = this; var folders = options.folders || NEWSBLUR.assets.folders; var feeds = NEWSBLUR.assets.feeds; - + this.$el.empty(); - this.$s.$story_taskbar.css({'display': 'block'}); + this.$s.$story_taskbar.css({ 'display': 'block' }); this.folder_view = new NEWSBLUR.Views.Folder({ - collection: folders, + collection: folders, root: true, hierarchy: this.options.hierarchy, feed_chooser: this.options.feed_chooser, organizer: this.options.organizer }).render(); this.$el.css({ - 'display': 'block', + 'display': 'block', 'opacity': 0 }); this.$el.addClass("NB-sort-" + this.options.sorting); this.$el.html(this.folder_view.el); - this.$el.animate({'opacity': 1}, {'duration': 700}); + this.$el.animate({ 'opacity': 1 }, { 'duration': 700 }); // this.count_collapsed_unread_stories(); - this.$s.$feed_link_error.css({'display': 'none'}); - this.$s.$feed_link_loader.fadeOut(250, _.bind(function() { - this.$s.$feed_link_loader.css({'display': 'none'}); + this.$s.$feed_link_error.css({ 'display': 'none' }); + this.$s.$feed_link_loader.fadeOut(250, _.bind(function () { + this.$s.$feed_link_loader.css({ 'display': 'none' }); }, this)); - + if (!this.options.feed_chooser && !options.feed_selector) { - if (NEWSBLUR.Globals.is_authenticated && + if (NEWSBLUR.Globals.is_authenticated && NEWSBLUR.assets.flags['has_chosen_feeds']) { - _.delay(function() { + _.delay(function () { if (!NEWSBLUR.reader.flags['refresh_inline_feed_delay']) return; NEWSBLUR.reader.start_count_unreads_after_import(); }, 1000); NEWSBLUR.reader.flags['refresh_inline_feed_delay'] = true; - NEWSBLUR.reader.force_feeds_refresh(function() { + NEWSBLUR.reader.force_feeds_refresh(function () { NEWSBLUR.reader.flags['refresh_inline_feed_delay'] = false; NEWSBLUR.reader.finish_count_unreads_after_import(); - }, true, null, function() { + }, true, null, function () { NEWSBLUR.reader.flags['refresh_inline_feed_delay'] = false; - NEWSBLUR.reader.finish_count_unreads_after_import({error: true}); + NEWSBLUR.reader.finish_count_unreads_after_import({ error: true }); }); } @@ -122,18 +122,18 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ $('.NB-feeds-header-river-sites-container').css({ 'display': 'block', 'opacity': 0 - }).animate({'opacity': 1}, {'duration': 700}); + }).animate({ 'opacity': 1 }, { 'duration': 700 }); } - + if (NEWSBLUR.reader.flags['showing_feed_in_tryfeed_view'] || NEWSBLUR.reader.flags['showing_social_feed_in_tryfeed_view']) { NEWSBLUR.reader.hide_tryfeed_view(); NEWSBLUR.reader.force_feed_refresh(); } - + this.toggle_filter_feeds(); - - _.defer(_.bind(function() { + + _.defer(_.bind(function () { NEWSBLUR.reader.open_dialog_after_feeds_loaded(); NEWSBLUR.reader.toggle_focus_in_slider(); this.scroll_to_selected(); @@ -145,32 +145,32 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ } }, this)); } - + return this; }, handle_error: function (model, resp, options) { console.log(['Error loading feeds', model, resp, options]); - + this.$s.$feed_link_error.css({ 'display': 'block' }); - this.$s.$feed_link_loader.css({'display': 'none'}); + this.$s.$feed_link_loader.css({ 'display': 'none' }); }, retry: function () { if (!NEWSBLUR.assets.folders.size()) { - this.$s.$feed_link_loader.css({'display': 'block'}); - this.$s.$feed_link_error.css({'display': 'none'}); - + this.$s.$feed_link_loader.css({ 'display': 'block' }); + this.$s.$feed_link_error.css({ 'display': 'none' }); + NEWSBLUR.assets.load_feeds(null, _.bind(this.handle_error, this)); } }, - - toggle_filter_feeds: function() { + + toggle_filter_feeds: function () { if (NEWSBLUR.assets.preference('show_global_shared_stories')) { $('.NB-feeds-header-river-global-container').css({ 'display': 'block', 'opacity': 0 - }).animate({'opacity': 1}, {'duration': 700}); + }).animate({ 'opacity': 1 }, { 'duration': 700 }); } else { $('.NB-feeds-header-river-global-container').css({ 'display': 'none', @@ -182,7 +182,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ $('.NB-feeds-header-river-infrequent-container').css({ 'display': 'block', 'opacity': 0 - }).animate({'opacity': 1}, {'duration': 700}); + }).animate({ 'opacity': 1 }, { 'duration': 700 }); } else { $('.NB-feeds-header-river-infrequent-container').css({ 'display': 'none', @@ -190,14 +190,14 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ }); } }, - - make_social_feeds: function() { + + make_social_feeds: function () { var $social_feeds = $('.NB-socialfeeds', this.$s.$social_feeds); var profile = NEWSBLUR.assets.user_profile; - var $feeds = NEWSBLUR.assets.social_feeds.map(function(feed) { + var $feeds = NEWSBLUR.assets.social_feeds.map(function (feed) { var feed_view = new NEWSBLUR.Views.FeedTitleView({ - model: feed, - type: 'feed', + model: feed, + type: 'feed', depth: 0 }).render(); feed.views.push(feed_view); @@ -205,37 +205,37 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ }); $social_feeds.empty().css({ - 'display': 'block', + 'display': 'block', 'opacity': 0 - }); + }); $social_feeds.html($feeds); if (NEWSBLUR.assets.social_feeds.length) { $('.NB-feeds-header-river-blurblogs-container').css({ 'display': 'block', 'opacity': 0 - }).animate({'opacity': 1}, {'duration': 700}); + }).animate({ 'opacity': 1 }, { 'duration': 700 }); } - var collapsed = NEWSBLUR.app.sidebar.check_river_blurblog_collapsed({skip_animation: true}); - $social_feeds.animate({'opacity': 1}, {'duration': collapsed ? 0 : 700}); + var collapsed = NEWSBLUR.app.sidebar.check_river_blurblog_collapsed({ skip_animation: true }); + $social_feeds.animate({ 'opacity': 1 }, { 'duration': collapsed ? 0 : 700 }); // if (this.socket) { // this.send_socket_active_feeds(); // } - + $('.NB-module-stats-count-shared-stories .NB-module-stats-count-number').text(profile.get('shared_stories_count')); $('.NB-module-stats-count-followers .NB-module-stats-count-number').text(profile.get('follower_count')); $('.NB-module-stats-count-following .NB-module-stats-count-number').text(profile.get('following_count')); }, - - make_starred_tags: function(options) { + + make_starred_tags: function (options) { options = options || {}; var $starred_feeds = $('.NB-starred-feeds', this.$s.$starred_feeds); - var $feeds = _.compact(NEWSBLUR.assets.starred_feeds.map(function(feed) { + var $feeds = _.compact(NEWSBLUR.assets.starred_feeds.map(function (feed) { if (!feed.get('is_highlights') && (feed.get('tag') == "" || !feed.get('tag'))) return; var feed_view = new NEWSBLUR.Views.FeedTitleView({ - model: feed, - type: 'feed', + model: feed, + type: 'feed', depth: 0, starred_tag: true }).render(); @@ -244,28 +244,28 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ })); $starred_feeds.empty().css({ - 'display': 'block', + 'display': 'block', 'opacity': options.update ? 1 : 0 - }); + }); $starred_feeds.html($feeds); if (NEWSBLUR.assets.starred_feeds.length) { $('.NB-feeds-header-starred-container').css({ 'display': 'block', 'opacity': 0 - }).animate({'opacity': 1}, {'duration': options.update ? 0 : 700}); + }).animate({ 'opacity': 1 }, { 'duration': options.update ? 0 : 700 }); } - var collapsed = NEWSBLUR.app.sidebar.check_starred_collapsed({skip_animation: true}); - $starred_feeds.animate({'opacity': 1}, {'duration': (collapsed || options.update) ? 0 : 700}); + var collapsed = NEWSBLUR.app.sidebar.check_starred_collapsed({ skip_animation: true }); + $starred_feeds.animate({ 'opacity': 1 }, { 'duration': (collapsed || options.update) ? 0 : 700 }); }, - - make_saved_searches: function(options) { + + make_saved_searches: function (options) { options = options || {}; var $searches_feeds = $('.NB-searches-feeds', this.$s.$searches_feeds); - var $feeds = _.compact(NEWSBLUR.assets.searches_feeds.map(function(feed) { + var $feeds = _.compact(NEWSBLUR.assets.searches_feeds.map(function (feed) { var feed_view = new NEWSBLUR.Views.FeedTitleView({ - model: feed, - type: 'feed', + model: feed, + type: 'feed', depth: 0, saved_search: true }).render(); @@ -274,25 +274,25 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ })); $searches_feeds.empty().css({ - 'display': 'block', + 'display': 'block', 'opacity': options.update ? 1 : 0 - }); + }); $searches_feeds.html($feeds); if (NEWSBLUR.assets.searches_feeds.length) { $('.NB-feeds-header-searches-container').css({ 'display': 'block', 'opacity': 0 - }).animate({'opacity': 1}, {'duration': options.update ? 0 : 700}); + }).animate({ 'opacity': 1 }, { 'duration': options.update ? 0 : 700 }); } - var collapsed = NEWSBLUR.app.sidebar.check_searches_collapsed({skip_animation: true}); - $searches_feeds.animate({'opacity': 1}, {'duration': (collapsed || options.update) ? 0 : 700}); + var collapsed = NEWSBLUR.app.sidebar.check_searches_collapsed({ skip_animation: true }); + $searches_feeds.animate({ 'opacity': 1 }, { 'duration': (collapsed || options.update) ? 0 : 700 }); }, - - load_router: function() { + + load_router: function () { if (!NEWSBLUR.router) { NEWSBLUR.router = new NEWSBLUR.Router; - var route_found = Backbone.history.start({pushState: true}); + var route_found = Backbone.history.start({ pushState: true }); var next = this.load_url_next_param(route_found); if (!next && !route_found && NEWSBLUR.assets.preference("autoopen_folder")) { this.load_default_folder(); @@ -300,11 +300,11 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ } }, - load_url_next_param: function(route_found) { + load_url_next_param: function (route_found) { var next = $.getQueryString('next') || $.getQueryString('test'); if (next) console.log(['load_url_next_param', next, route_found]); if (next == 'optout') { - NEWSBLUR.reader.open_account_modal({'animate_email': true}); + NEWSBLUR.reader.open_account_modal({ 'animate_email': true }); } else if (next == 'goodies') { NEWSBLUR.reader.open_goodies_modal(); } else if (next == 'newsletters') { @@ -316,29 +316,29 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ } else if (next == 'account') { NEWSBLUR.reader.open_account_modal(); } else if (next == 'payments') { - NEWSBLUR.reader.open_account_modal({'tab': 'premium'}); + NEWSBLUR.reader.open_account_modal({ 'tab': 'premium' }); } else if (next == 'opml') { - NEWSBLUR.reader.open_intro_modal({page_number: 2}); + NEWSBLUR.reader.open_intro_modal({ page_number: 2 }); } else if (next == 'organizer') { NEWSBLUR.reader.open_organizer_modal(); } else if (next == 'chooser') { NEWSBLUR.reader.open_feedchooser_modal(); } else if (next == 'premium') { - NEWSBLUR.reader.open_feedchooser_modal({'premium_only': true}); + NEWSBLUR.reader.open_feedchooser_modal({ 'premium_only': true }); } else if (next == 'renew') { - NEWSBLUR.reader.open_feedchooser_modal({'premium_only': true}); + NEWSBLUR.reader.open_feedchooser_modal({ 'premium_only': true }); } else if (next == 'password') { - NEWSBLUR.reader.open_account_modal({'change_password': true}); + NEWSBLUR.reader.open_account_modal({ 'change_password': true }); } else if (next == 'notifications') { - _.delay(function() { + _.delay(function () { NEWSBLUR.reader.open_notifications_modal(NEWSBLUR.assets.active_feed && NEWSBLUR.assets.active_feed.id); }, 200); } var url = $.getQueryString('url') || $.getQueryString('add'); if (url) { - NEWSBLUR.reader.open_add_feed_modal({url: url}); - + NEWSBLUR.reader.open_add_feed_modal({ url: url }); + // Only trim the ?add=url if authenticated, otherwise keep it if (!NEWSBLUR.Globals.is_authenticated) { route_found = true; @@ -350,13 +350,13 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ // In case this needs to be found again: window.location.href = BACKBONE window.history.replaceState({}, null, '/'); } - + return next; }, - - load_default_folder: function() { + + load_default_folder: function () { var default_folder = NEWSBLUR.assets.preference('default_folder'); - + if (!default_folder || default_folder == "" || default_folder == "river:") { NEWSBLUR.reader.open_river_stories(); } else { @@ -366,61 +366,61 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ } } }, - - update_dashboard_count: function() { + + update_dashboard_count: function () { var feed_count = _.unique(NEWSBLUR.assets.folders.feed_ids_in_folder()).length; $(".NB-module-stats-count-number-sites").html(Inflector.commas(feed_count)); }, - + // =========== // = Actions = // =========== - - scroll_to_show_selected_feed: function() { + + scroll_to_show_selected_feed: function () { var $feed_lists = this.$s.$feed_lists; - var model = NEWSBLUR.assets.feeds.selected() || - NEWSBLUR.assets.social_feeds.selected() || - NEWSBLUR.assets.starred_feeds.selected() || - NEWSBLUR.assets.searches_feeds.selected(); + var model = NEWSBLUR.assets.feeds.selected() || + NEWSBLUR.assets.social_feeds.selected() || + NEWSBLUR.assets.starred_feeds.selected() || + NEWSBLUR.assets.searches_feeds.selected(); if (!model) return; var feed_view = model.get("selected_title_view"); if (!feed_view) { - feed_view = _.detect(model.views, _.bind(function(view) { + feed_view = _.detect(model.views, _.bind(function (view) { return !!view.$el.closest(this.$s.$feed_lists).length; }, this)); } if (!feed_view) return; - + if (!$feed_lists.isScrollVisible(feed_view.$el)) { var scroll = feed_view.$el.position().top; var container = $feed_lists.scrollTop(); var height = $feed_lists.outerHeight(); - $feed_lists.scrollTop(scroll+container-height/5); + $feed_lists.scrollTop(scroll + container - height / 5); } - + return true; }, - - scroll_to_show_highlighted_feed: function() { + + scroll_to_show_highlighted_feed: function () { var $feed_lists = this.$s.$feed_lists; var $feed = $('.NB-feed-selector-selected'); - + if (!$feed.length) return; - + var is_feed_visible = $feed_lists.isScrollVisible($feed); if (!is_feed_visible) { var scroll = $feed.position().top; var container = $feed_lists.scrollTop(); var height = $feed_lists.outerHeight(); - $feed_lists.scrollTop(scroll+container-height/5); - } + $feed_lists.scrollTop(scroll + container - height / 5); + } }, - - scroll_to_show_selected_folder: function() { + + scroll_to_show_selected_folder: function () { var $feed_lists = this.$s.$feed_lists; var $selected_view; - + var folder = NEWSBLUR.assets.folders.selected(); if (folder) { $selected_view = folder.folder_view.$el; @@ -431,7 +431,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ $selected_view = folder.list_view.$el; } } - + if (!$selected_view && NEWSBLUR.reader.active_feed == 'river:') { $selected_view = NEWSBLUR.reader.$s.$river_sites_header.closest(".NB-feeds-header-container"); } else if (!$selected_view && NEWSBLUR.reader.active_feed == 'river:infrequent') { @@ -442,41 +442,41 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({ $selected_view = NEWSBLUR.reader.$s.$read_header.closest(".NB-feeds-header-container"); } if (!$selected_view) return; - + var is_folder_visible = $feed_lists.isScrollVisible($selected_view); if (!is_folder_visible) { var scroll = $selected_view.position().top; var container = $feed_lists.scrollTop(); var height = $feed_lists.outerHeight(); - $feed_lists.scrollTop(scroll+container-height/5); + $feed_lists.scrollTop(scroll + container - height / 5); } - + return true; }, - - scroll_to_selected: function() { + + scroll_to_selected: function () { var found = this.scroll_to_show_selected_feed(); if (!found) { this.scroll_to_show_selected_folder(); } }, - - start_sorting: function() { + + start_sorting: function () { this.options.sorting = true; }, - - end_sorting: function() { + + end_sorting: function () { this.options.sorting = false; }, - - is_sorting: function() { + + is_sorting: function () { return this.options.sorting; }, - - show_read_stories_header: function() { + + show_read_stories_header: function () { NEWSBLUR.reader.$s.$read_header.closest('.NB-feeds-header-read-container') - .addClass('NB-block'); + .addClass('NB-block'); } - + }); diff --git a/media/js/newsblur/views/feed_notification_view.js b/media/js/newsblur/views/feed_notification_view.js index 2e347824ed..de662ff1ec 100644 --- a/media/js/newsblur/views/feed_notification_view.js +++ b/media/js/newsblur/views/feed_notification_view.js @@ -1,18 +1,18 @@ NEWSBLUR.Views.FeedNotificationView = Backbone.View.extend({ - + events: { "click .NB-feed-notification-filter-unread": "toggle_unread", - "click .NB-feed-notification-filter-focus" : "toggle_focus", - "click .NB-feed-notification-email" : "toggle_email", - "click .NB-feed-notification-ios" : "toggle_ios", - "click .NB-feed-notification-android" : "toggle_android", - "click .NB-feed-notification-web" : "toggle_web" + "click .NB-feed-notification-filter-focus": "toggle_focus", + "click .NB-feed-notification-email": "toggle_email", + "click .NB-feed-notification-ios": "toggle_ios", + "click .NB-feed-notification-android": "toggle_android", + "click .NB-feed-notification-web": "toggle_web" }, - - initialize: function(m) { + + initialize: function (m) { }, - - render: function() { + + render: function () { var feed = this.model; var $feed = $(_.template('
\
\ @@ -41,25 +41,25 @@ NEWSBLUR.Views.FeedNotificationView = Backbone.View.extend({ <% } %>\
\ ', { - feed : feed, - selected : this.options.selected, - popover : this.options.popover, - frequency : feed && this.frequency(feed.get('average_stories_per_month')), - frequency_count : this.frequency_count(), - is_email : _.contains(feed.get('notification_types'), 'email'), - is_ios : _.contains(feed.get('notification_types'), 'ios'), - is_android : _.contains(feed.get('notification_types'), 'android'), - is_web : _.contains(feed.get('notification_types'), 'web'), - is_focus : feed.get('notification_filter') == 'focus' + feed: feed, + selected: this.options.selected, + popover: this.options.popover, + frequency: feed && this.frequency(feed.get('average_stories_per_month')), + frequency_count: this.frequency_count(), + is_email: _.contains(feed.get('notification_types'), 'email'), + is_ios: _.contains(feed.get('notification_types'), 'ios'), + is_android: _.contains(feed.get('notification_types'), 'android'), + is_web: _.contains(feed.get('notification_types'), 'web'), + is_focus: feed.get('notification_filter') == 'focus' })); - + this.$el.replaceWith($feed); this.setElement($feed); - + return this; }, - - frequency: function(count) { + + frequency: function (count) { if (count == 0) { return "No stories published last month"; } else if (count < 30) { @@ -68,11 +68,11 @@ NEWSBLUR.Views.FeedNotificationView = Backbone.View.extend({ return Inflector.pluralize("story", Math.round(count / 30.0), true) + " per day"; } }, - - frequency_count: function() { + + frequency_count: function () { var freq = this.model.get('notification_frequency'); var story_count = this.model.get('stories_per_month') / 30.0; - + if (!freq) freq = 0; if (freq == 0) { return Inflector.pluralize("story", Math.ceil(story_count), true); @@ -82,28 +82,28 @@ NEWSBLUR.Views.FeedNotificationView = Backbone.View.extend({ return Inflector.pluralize("story", story_count, true); } }, - + // ========== // = Events = // ========== - - toggle_email: function() { + + toggle_email: function () { this.toggle_type('email'); }, - - toggle_ios: function() { + + toggle_ios: function () { this.toggle_type('ios'); }, - - toggle_android: function() { + + toggle_android: function () { this.toggle_type('android'); }, - - toggle_web: function() { + + toggle_web: function () { this.toggle_type('web'); }, - - toggle_type: function(type) { + + toggle_type: function (type) { var notification_types = this.model.get('notification_types') || []; var is_type = _.contains(notification_types, type); if (is_type) { @@ -113,34 +113,34 @@ NEWSBLUR.Views.FeedNotificationView = Backbone.View.extend({ } this.model.set('notification_types', notification_types); this.save(); - - _.each(['web', 'ios', 'android', 'email'], _.bind(function(type) { + + _.each(['web', 'ios', 'android', 'email'], _.bind(function (type) { var func = _.contains(notification_types, type) ? "addClass" : "removeClass"; - this.$(".NB-feed-notification-"+type)[func]('NB-active'); + this.$(".NB-feed-notification-" + type)[func]('NB-active'); }, this)); - + }, - - toggle_focus: function() { + + toggle_focus: function () { this.model.set('notification_filter', 'focus'); this.save(); - + this.$(".NB-feed-notification-filter-focus").addClass("NB-active"); this.$(".NB-feed-notification-filter-unread").removeClass("NB-active"); }, - - toggle_unread: function() { - this.model.set('notification_filter', 'unread'); + + toggle_unread: function () { + this.model.set('notification_filter', 'unread'); this.save(); this.$(".NB-feed-notification-filter-focus").removeClass("NB-active"); this.$(".NB-feed-notification-filter-unread").addClass("NB-active"); }, - - save: function() { - NEWSBLUR.assets.set_notifications_for_feed(this.model, function() { + + save: function () { + NEWSBLUR.assets.set_notifications_for_feed(this.model, function () { NEWSBLUR.reader.make_feed_title_in_stories(); }); } - + }); diff --git a/media/js/newsblur/views/feed_options_popover.js b/media/js/newsblur/views/feed_options_popover.js index d11342665a..58ebf1abca 100644 --- a/media/js/newsblur/views/feed_options_popover.js +++ b/media/js/newsblur/views/feed_options_popover.js @@ -1,7 +1,7 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ - + className: "NB-filter-popover", - + options: { 'width': 304, 'anchor': '.NB-feedbar-options', @@ -19,7 +19,7 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ 'show_imagepreview': true, 'show_order': true }, - + events: { "click .NB-view-setting-option": "change_view_setting", "click .NB-filter-popover-filter-icon": "open_site_settings", @@ -30,11 +30,11 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ "click .NB-filter-popover-dashboard-remove-module": "remove_dashboard_module", "change .NB-modal-feed-chooser": "change_feed" }, - - initialize: function(options) { + + initialize: function (options) { this.options = _.extend({}, this.options, options); this.options.offset.left = -1 * $(this.options.anchor).width() - 31; - + if (NEWSBLUR.reader.active_feed == "read") { this.options['show_readfilter'] = false; } @@ -45,15 +45,15 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ this.options.feed_id = "starred"; // Ignore tags this.options['show_readfilter'] = false; } - + // console.log("Opening feed options", this.options, this.options.feed_id); - + NEWSBLUR.ReaderPopover.prototype.initialize.call(this, this.options); this.model = NEWSBLUR.assets; this.render(); this.show_correct_feed_view_options_in_menu(); }, - + close: function () { if (this.options.on_dashboard) { this.options.on_dashboard.$(".NB-feedbar-options").removeClass('NB-active'); @@ -70,9 +70,9 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ feed = NEWSBLUR.assets.get_feed(this.options.feed_id) } var is_feed = feed && feed.is_feed(); - + NEWSBLUR.ReaderPopover.prototype.render.call(this); - + this.$el.html($.make('div', [ (this.options.on_dashboard && $.make('div', { className: 'NB-popover-section' }, [ $.make('div', { className: 'NB-modal-feed-chooser-container' }, [ @@ -242,15 +242,15 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ $.make('div', { className: 'NB-section-icon NB-filter-popover-notifications-icon' }), $.make('div', { className: 'NB-popover-section-title' }, 'Notifications'), $.make('div', { className: 'NB-feedbar-options-notifications' }, [ - new NEWSBLUR.Views.FeedNotificationView({model: feed, popover: true}).render().$el + new NEWSBLUR.Views.FeedNotificationView({ model: feed, popover: true }).render().$el ]) ])) ])); - + return this; }, - - show_correct_feed_view_options_in_menu: function() { + + show_correct_feed_view_options_in_menu: function () { var order = NEWSBLUR.assets.view_setting(this.options.feed_id, 'order'); var read_filter = NEWSBLUR.assets.view_setting(this.options.feed_id, 'read_filter'); var dashboard_count = parseInt(NEWSBLUR.assets.view_setting(this.options.feed_id, 'dashboard_count'), 10); @@ -283,7 +283,7 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ var $image_preview_sr = this.$('.NB-view-setting-imagepreview-small-right'); var $image_preview_ll = this.$('.NB-view-setting-imagepreview-large-left'); var $image_preview_lr = this.$('.NB-view-setting-imagepreview-large-right'); - + $oldest.toggleClass('NB-active', order == 'oldest'); $newest.toggleClass('NB-active', order != 'oldest'); $oldest.text('Oldest' + (order == 'oldest' ? ' first' : '')); @@ -312,18 +312,18 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ $image_preview_ll.toggleClass('NB-active', image_preview == "large-left"); $image_preview_lr.toggleClass('NB-active', image_preview == "1" || image_preview == "large-right"); this.$('.NB-options-feed-size li').removeClass('NB-active'); - this.$('.NB-options-feed-size .NB-options-feed-size-'+feed_size).addClass('NB-active'); + this.$('.NB-options-feed-size .NB-options-feed-size-' + feed_size).addClass('NB-active'); this.$('.NB-options-feed-font .NB-view-setting-option').removeClass('NB-active'); - this.$('.NB-options-feed-font .NB-view-setting-feed-font-'+feed_font).addClass('NB-active'); + this.$('.NB-options-feed-font .NB-view-setting-feed-font-' + feed_font).addClass('NB-active'); var frequencies = [5, 15, 30, 60, 90]; for (var f in frequencies) { var freq = frequencies[f]; var $infrequent = this.$('.NB-view-setting-infrequent-' + freq); $infrequent.toggleClass('NB-active', infrequent == freq); - $infrequent.text(infrequent == freq ? '< '+freq+'/month' : freq); + $infrequent.text(infrequent == freq ? '< ' + freq + '/month' : freq); } - + if (this.options.on_dashboard) { this.options.on_dashboard.$(".NB-feedbar-options").addClass('NB-active'); this.$('option[value="' + this.options.feed_id + '"]').attr('selected', true); @@ -332,32 +332,32 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } }, - + // ========== // = Events = // ========== - - change_view_setting: function(e) { + + change_view_setting: function (e) { var $target = $(e.currentTarget); var options = {}; // console.log(['change_view_setting', $target]); if ($target.hasClass("NB-view-setting-order-newest")) { - options = {order: 'newest'}; + options = { order: 'newest' }; } else if ($target.hasClass("NB-view-setting-order-oldest")) { - options = {order: 'oldest'}; + options = { order: 'oldest' }; } else if ($target.hasClass("NB-view-setting-dashboardcount-5")) { - options = {dashboard_count: 5}; + options = { dashboard_count: 5 }; } else if ($target.hasClass("NB-view-setting-dashboardcount-10")) { - options = {dashboard_count: 10}; + options = { dashboard_count: 10 }; } else if ($target.hasClass("NB-view-setting-dashboardcount-15")) { - options = {dashboard_count: 15}; + options = { dashboard_count: 15 }; } else if ($target.hasClass("NB-view-setting-dashboardcount-20")) { - options = {dashboard_count: 20}; + options = { dashboard_count: 20 }; } else if ($target.hasClass("NB-view-setting-readfilter-all")) { - options = {read_filter: 'all'}; + options = { read_filter: 'all' }; } else if ($target.hasClass("NB-view-setting-readfilter-unread")) { - options = {read_filter: 'unread'}; + options = { read_filter: 'unread' }; } else if ($target.hasClass("NB-view-setting-markscroll-unread")) { NEWSBLUR.assets.preference('mark_read_on_scroll_titles', false); } else if ($target.hasClass("NB-view-setting-markscroll-read")) { @@ -427,28 +427,28 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-view-setting-feed-font-gotham")) { this.update_feed_font('gotham'); } - + if (NEWSBLUR.reader.flags.search) { options.search = NEWSBLUR.reader.flags.search; } this.update_feed(options); this.show_correct_feed_view_options_in_menu(); }, - - update_feed_font_size: function(setting) { + + update_feed_font_size: function (setting) { NEWSBLUR.assets.preference('feed_size', setting); NEWSBLUR.reader.apply_story_styling(); }, - - update_feed_font: function(setting) { + + update_feed_font: function (setting) { NEWSBLUR.assets.preference('feed_font', setting); NEWSBLUR.reader.apply_story_styling(); }, - - update_feed: function(setting) { + + update_feed: function (setting) { var changed = NEWSBLUR.assets.view_setting(this.options.feed_id, setting); if (!changed) return; - + this.reload_feed(); }, @@ -459,22 +459,22 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ NEWSBLUR.reader.reload_feed(); } }, - - open_site_settings: function() { - this.close(function() { + + open_site_settings: function () { + this.close(function () { NEWSBLUR.reader.open_feed_exception_modal(); }); }, - - open_site_statistics: function() { - this.close(function() { + + open_site_statistics: function () { + this.close(function () { console.log(["stats"]); NEWSBLUR.reader.open_feed_statistics_modal(); }); }, - open_notifications: function() { - this.close(_.bind(function() { + open_notifications: function () { + this.close(_.bind(function () { NEWSBLUR.reader.open_notifications_modal(this.options.feed_id); }, this)); }, @@ -511,7 +511,7 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ this.close(); }, this), function (e) { console.log(['Error saving dashboard river', e]); - }); + }); }, change_feed: function () { @@ -521,5 +521,5 @@ NEWSBLUR.FeedOptionsPopover = NEWSBLUR.ReaderPopover.extend({ this.close(); } - + }); diff --git a/media/js/newsblur/views/feed_search_header.js b/media/js/newsblur/views/feed_search_header.js index b1678d9c51..94471e9871 100644 --- a/media/js/newsblur/views/feed_search_header.js +++ b/media/js/newsblur/views/feed_search_header.js @@ -1,22 +1,22 @@ NEWSBLUR.Views.FeedSearchHeader = Backbone.View.extend({ - + el: ".NB-search-header", - + className: "NB-search-header", - + events: { "click .NB-search-header-save": "save_search" }, - - unload: function() { + + unload: function () { this.$el.addClass("NB-hidden"); }, - - render: function() { - this.showing_fake_folder = NEWSBLUR.reader.flags['river_view'] && - NEWSBLUR.reader.active_folder && + + render: function () { + this.showing_fake_folder = NEWSBLUR.reader.flags['river_view'] && + NEWSBLUR.reader.active_folder && (NEWSBLUR.reader.active_folder.get('fake') || !NEWSBLUR.reader.active_folder.get('folder_title')); - + if (NEWSBLUR.reader.flags.search && NEWSBLUR.reader.flags.searching && NEWSBLUR.reader.flags.search.length) { this.$el.removeClass("NB-hidden"); @@ -29,8 +29,8 @@ NEWSBLUR.Views.FeedSearchHeader = Backbone.View.extend({ this.unload(); } }, - - make_title: function() { + + make_title: function () { var feed_title = NEWSBLUR.reader.feed_title(); var $view = $(_.template('
\ @@ -40,39 +40,39 @@ NEWSBLUR.Views.FeedSearchHeader = Backbone.View.extend({ feed_title: feed_title, query: NEWSBLUR.reader.flags.search })); - + return $view; }, - - is_saved: function() { + + is_saved: function () { return !!NEWSBLUR.assets.get_search_feeds(this.saved_feed_id(), NEWSBLUR.reader.flags.search); }, - - saved_feed_id: function() { + + saved_feed_id: function () { var feed_id = NEWSBLUR.reader.active_feed; if (_.isNumber(feed_id)) { feed_id = "feed:" + feed_id; } return feed_id; }, - + // ========== // = Events = // ========== - - save_search: function(e) { + + save_search: function (e) { var feed_id = this.saved_feed_id(); if (this.is_saved()) { - NEWSBLUR.assets.delete_saved_search(feed_id, NEWSBLUR.reader.flags.search, _.bind(function(e) { + NEWSBLUR.assets.delete_saved_search(feed_id, NEWSBLUR.reader.flags.search, _.bind(function (e) { console.log(['Saved searches', e]); this.render(); }, this)); } else { - NEWSBLUR.assets.save_search(feed_id, NEWSBLUR.reader.flags.search, _.bind(function(e) { + NEWSBLUR.assets.save_search(feed_id, NEWSBLUR.reader.flags.search, _.bind(function (e) { console.log(['Saved searches', e]); this.render(); }, this)); } } - + }); diff --git a/media/js/newsblur/views/feed_search_view.js b/media/js/newsblur/views/feed_search_view.js index 1a816c59a5..2cec057988 100644 --- a/media/js/newsblur/views/feed_search_view.js +++ b/media/js/newsblur/views/feed_search_view.js @@ -1,23 +1,23 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ - + className: "NB-story-title-search", - + events: { "focus .NB-story-title-search-input": "focus_search", - "blur .NB-story-title-search-input" : "blur_search", - "keyup input[name=feed_search]" : "keyup", - "keydown input[name=feed_search]" : "keydown", - "click .NB-search-close" : "close_search", - "mouseenter" : "mouseenter", - "mouseleave" : "mouseleave" - }, - - initialize: function(options) { + "blur .NB-story-title-search-input": "blur_search", + "keyup input[name=feed_search]": "keyup", + "keydown input[name=feed_search]": "keydown", + "click .NB-search-close": "close_search", + "mouseenter": "mouseenter", + "mouseleave": "mouseleave" + }, + + initialize: function (options) { this.feedbar_view = options.feedbar_view; this.search_debounced = _.debounce(_.bind(this.perform_search, this), 350); }, - - render: function() { + + render: function () { if (NEWSBLUR.app.active_search) { NEWSBLUR.app.active_search.remove(); } @@ -29,13 +29,13 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ ', { search: NEWSBLUR.reader.flags['search'] })); - + this.$el.html($view); - + return this; }, - - remove: function() { + + remove: function () { var $icon = this.$('.NB-search-icon'); var tipsy = $icon.data('tipsy'); if (tipsy) { @@ -45,12 +45,12 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ NEWSBLUR.reader.$s.$story_titles_header.removeClass("NB-searching"); Backbone.View.prototype.remove.call(this); }, - + // ============ // = Indexing = // ============ - - update_indexing_progress: function(message) { + + update_indexing_progress: function (message) { var $input = this.$('input'); var $icon = this.$('.NB-search-icon'); @@ -59,7 +59,7 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ } else if (message == "done") { $input.attr('style', null); var tipsy = $icon.data('tipsy'); - _.defer(function() { + _.defer(function () { if (!tipsy) return; tipsy.disable(); tipsy.hide(); @@ -67,7 +67,7 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ this.retry(); } else if (_.string.startsWith(message, 'feeds:')) { var feed_ids = message.replace('feeds:', '').split(','); - _.each(feed_ids, function(feed_id) { + _.each(feed_ids, function (feed_id) { var feed = NEWSBLUR.assets.get_feed(parseInt(feed_id, 10)); feed.set('search_indexed', true); }); @@ -78,59 +78,59 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ NEWSBLUR.utils.attach_loading_gradient($input, progress); } }, - - show_indexing_tooltip: function(show) { + + show_indexing_tooltip: function (show) { var $icon = this.$('.NB-search-icon'); var tipsy = $icon.data('tipsy'); - + if (tipsy) return; - + $icon.tipsy({ - title: function() { return "Hang tight, indexing..."; }, + title: function () { return "Hang tight, indexing..."; }, gravity: 'nw', fade: true, trigger: 'manual', offset: 4 }); var tipsy = $icon.data('tipsy'); - _.defer(function() { + _.defer(function () { tipsy.enable(); if (show) tipsy.show(); }); - _.delay(function() { + _.delay(function () { tipsy.hide(); }, 3 * 1000); }, - + // ========== // = Events = // ========== - - focus: function() { + + focus: function () { this.$("input").focus(); }, - - has_focus: function() { + + has_focus: function () { return this.$("input:focus").length; }, - blur: function() { + blur: function () { this.$("input").blur(); }, - - focus_search: function() { + + focus_search: function () { if (!NEWSBLUR.reader.flags.searching || !NEWSBLUR.reader.flags.search) { NEWSBLUR.reader.flags.searching = true; NEWSBLUR.reader.flags.search = ""; } NEWSBLUR.reader.$s.$story_titles_header.addClass("NB-searching"); }, - - blur_search: function() { + + blur_search: function () { var $search = this.$("input[name=feed_search]"); var query = $search.val(); - + if (query.length == 0) { NEWSBLUR.reader.flags.searching = false; NEWSBLUR.reader.$s.$story_titles_header.removeClass("NB-searching"); @@ -139,10 +139,10 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ } } }, - - keyup: function(e) { - var arrow = {left: 37, up: 38, right: 39, down: 40, enter: 13, esc: 27}; - + + keyup: function (e) { + var arrow = { left: 37, up: 38, right: 39, down: 40, enter: 13, esc: 27 }; + if (e.which == arrow.up || e.which == arrow.down) { this.blur(); @@ -152,13 +152,13 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ return false; } - + this.search(); }, - - keydown: function(e) { - var arrow = {left: 37, up: 38, right: 39, down: 40, enter: 13, esc: 27}; - + + keydown: function (e) { + var arrow = { left: 37, up: 38, right: 39, down: 40, enter: 13, esc: 27 }; + if (e.which == arrow.esc) { this.close_search(); e.preventDefault(); @@ -166,26 +166,26 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ return false; } }, - - retry: function() { + + retry: function () { if (!NEWSBLUR.reader.flags.search) return; - + NEWSBLUR.reader.flags.search = null; this.search(); }, - - search: function() { + + search: function () { var $search = this.$("input[name=feed_search]"); var query = _.escape($search.val()); - + if (query != NEWSBLUR.reader.flags.search) { NEWSBLUR.reader.flags.searching = true; NEWSBLUR.reader.flags.search = query; this.search_debounced(query); } }, - - perform_search: function(query) { + + perform_search: function (query) { if (query && query.length) { window.history.pushState({}, "", $.updateQueryString('search', query, window.location.pathname)); } else { @@ -196,32 +196,32 @@ NEWSBLUR.Views.FeedSearchView = Backbone.View.extend({ }); NEWSBLUR.app.story_titles_header.show_hidden_story_titles(); }, - - close_search: function() { + + close_search: function () { var $search = this.$("input[name=feed_search]"); $search.val(''); window.history.pushState({}, "", $.updateQueryString('search', null, window.location.pathname)); NEWSBLUR.reader.flags.searching = false; - + NEWSBLUR.reader.reload_feed(); }, - - mouseenter: function(e) { + + mouseenter: function (e) { var $icon = this.$('.NB-search-icon'); var tipsy = $icon.data('tipsy'); - + if (!tipsy) return; - + tipsy.show(); }, - - mouseleave: function(e) { + + mouseleave: function (e) { var $icon = this.$('.NB-search-icon'); var tipsy = $icon.data('tipsy'); - + if (!tipsy) return; - + tipsy.hide(); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/feed_selector.js b/media/js/newsblur/views/feed_selector.js index 905ef1fdca..95ffca31bd 100644 --- a/media/js/newsblur/views/feed_selector.js +++ b/media/js/newsblur/views/feed_selector.js @@ -1,31 +1,31 @@ NEWSBLUR.Views.FeedSelector = Backbone.View.extend({ - + el: '.NB-feeds-selector', - + flags: {}, - + events: { - "keyup .NB-feeds-selector-input" : "keyup", - "keydown .NB-feeds-selector-input" : "keydown" + "keyup .NB-feeds-selector-input": "keyup", + "keydown .NB-feeds-selector-input": "keydown" }, - + selected_index: 0, - - initialize: function() { + + initialize: function () { this.selected_feeds = new NEWSBLUR.Collections.Feeds(); }, - - toggle: function() { + + toggle: function () { if (this.flags.showing_feed_selector) { this.hide_feed_selector(); } else { this.show_feed_selector(); } }, - - show_feed_selector: function() { + + show_feed_selector: function () { var $input = this.$(".NB-feeds-selector-input"); - var $feed_list = NEWSBLUR.reader.$s.$feed_list; + var $feed_list = NEWSBLUR.reader.$s.$feed_list; var $social_feeds = NEWSBLUR.reader.$s.$social_feeds; var $body = NEWSBLUR.reader.$s.$body; @@ -33,39 +33,39 @@ NEWSBLUR.Views.FeedSelector = Backbone.View.extend({ $input.val(''); console.log(["Focus on feed selector", $input]); NEWSBLUR.app.feed_list.options.feed_chooser = true; - NEWSBLUR.assets.feeds.trigger('reset', {feed_selector: true}); + NEWSBLUR.assets.feeds.trigger('reset', { feed_selector: true }); $feed_list.addClass('NB-selector-active'); $social_feeds.addClass('NB-selector-active'); $body.addClass('NB-selector-active'); - + this.flags.showing_feed_selector = true; NEWSBLUR.reader.layout.leftLayout.sizePane('north'); - + if (NEWSBLUR.reader.flags['sidebar_closed']) { NEWSBLUR.reader.layout.outerLayout.show('west', true); } $input.focus(); }, - - hide_feed_selector: function() { + + hide_feed_selector: function () { if (!this.flags.showing_feed_selector) return; var $input = this.$(".NB-feeds-selector-input"); - var $feed_list = NEWSBLUR.reader.$s.$feed_list; + var $feed_list = NEWSBLUR.reader.$s.$feed_list; var $social_feeds = NEWSBLUR.reader.$s.$social_feeds; var $body = NEWSBLUR.reader.$s.$body; - + $input.blur(); this.$el.hide(); this.$next_feed = null; NEWSBLUR.app.feed_list.options.feed_chooser = false; - NEWSBLUR.assets.feeds.trigger('reset', {feed_selector: true}); + NEWSBLUR.assets.feeds.trigger('reset', { feed_selector: true }); $feed_list.removeClass('NB-selector-active'); $social_feeds.removeClass('NB-selector-active'); $body.removeClass('NB-selector-active'); $('.NB-feed-selector-selected').removeClass('NB-feed-selector-selected'); - + this.flags.showing_feed_selector = false; NEWSBLUR.reader.layout.leftLayout.sizePane('north'); @@ -73,65 +73,65 @@ NEWSBLUR.Views.FeedSelector = Backbone.View.extend({ NEWSBLUR.reader.layout.outerLayout.hide('west'); } }, - - filter_feed_selector: function(e) { + + filter_feed_selector: function (e) { var $input = this.$(".NB-feeds-selector-input"); var input = $input.val().toLowerCase(); if (input == this.last_input && input.length) return; this.last_input = input; - - this.selected_feeds.each(function(feed) { - _.each(feed.views, function(view) { + + this.selected_feeds.each(function (feed) { + _.each(feed.views, function (view) { view.$el.removeClass('NB-feed-selector-active'); }); }); - - var feeds = NEWSBLUR.assets.feeds.filter(function(feed){ + + var feeds = NEWSBLUR.assets.feeds.filter(function (feed) { return _.string.contains(feed.get('feed_title') && feed.get('feed_title').toLowerCase(), input) || feed.id == input; }); - var socialsubs = NEWSBLUR.assets.social_feeds.filter(function(feed){ + var socialsubs = NEWSBLUR.assets.social_feeds.filter(function (feed) { return _.string.contains(feed.get('feed_title').toLowerCase(), input) || - _.string.contains(feed.get('username').toLowerCase(), input); + _.string.contains(feed.get('username').toLowerCase(), input); }); feeds = socialsubs.concat(feeds); - + // Clear out shown feeds on empty input if (input.length == 0) { this.selected_feeds.reset(); } - + if (feeds.length) { this.selected_feeds.reset(feeds); } - - this.selected_feeds.each(function(feed) { - _.each(feed.views, function(view) { + + this.selected_feeds.each(function (feed) { + _.each(feed.views, function (view) { view.$el.addClass('NB-feed-selector-active'); }); }); - + this.select(0); }, - + // ============== // = Navigation = // ============== - - keyup: function(e) { - var arrow = {left: 37, up: 38, right: 39, down: 40, enter: 13}; - + + keyup: function (e) { + var arrow = { left: 37, up: 38, right: 39, down: 40, enter: 13 }; + if (e.which == arrow.up || e.which == arrow.down) { // return this.navigate(e); } else if (e.which == arrow.enter) { // return this.open(e); } - + return this.filter_feed_selector(e); }, - - keydown: function(e) { - var arrow = {left: 37, up: 38, right: 39, down: 40, enter: 13, esc: 27}; - + + keydown: function (e) { + var arrow = { left: 37, up: 38, right: 39, down: 40, enter: 13, esc: 27 }; + if (e.which == arrow.esc) { e.preventDefault(); e.stopPropagation(); @@ -142,46 +142,46 @@ NEWSBLUR.Views.FeedSelector = Backbone.View.extend({ } else if (e.which == arrow.enter) { return this.open(e); } - + // return this.filter_feed_selector(e); }, - - navigate: function(e) { - var arrow = {left: 37, up: 38, right: 39, down: 40, esc: 27}; - + + navigate: function (e) { + var arrow = { left: 37, up: 38, right: 39, down: 40, esc: 27 }; + if (e.which == arrow.down) { this.select(1); } else if (e.which == arrow.up) { this.select(-1); } - + e.preventDefault(); return false; }, - - select: function(direction) { + + select: function (direction) { var off, on; - + var $current_feed = $('.NB-feed-selector-selected.NB-feed-selector-active'); this.$next_feed = NEWSBLUR.reader.get_next_feed(direction, $current_feed); - + $('.NB-feed-selector-selected').removeClass('NB-feed-selector-selected'); this.$next_feed.addClass('NB-feed-selector-selected'); NEWSBLUR.app.feed_list.scroll_to_show_highlighted_feed(); }, - - open: function(e) { + + open: function (e) { var feed_id = this.$next_feed.data('id'); if (_.string.include(feed_id, 'social:')) { NEWSBLUR.reader.open_social_stories(this.$next_feed.data('id'), { $feed: this.$next_feed }); } else { - NEWSBLUR.reader.open_feed(this.$next_feed.data('id'), {$feed: this.$next_feed}); + NEWSBLUR.reader.open_feed(this.$next_feed.data('id'), { $feed: this.$next_feed }); } - + e.preventDefault(); return false; } - + }); diff --git a/media/js/newsblur/views/feed_title_view.js b/media/js/newsblur/views/feed_title_view.js index 4a6979a4f6..21712e2436 100644 --- a/media/js/newsblur/views/feed_title_view.js +++ b/media/js/newsblur/views/feed_title_view.js @@ -1,30 +1,30 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ - + options: { depth: 0, selected: false }, - + flags: {}, - + events: { - "dblclick .feed_counts" : "dblclick_mark_feed_as_read", - "dblclick" : "open_feed_link", - "click .NB-feedbar-mark-feed-read" : "mark_feed_as_read", - "click .NB-feedbar-mark-feed-read-time" : "mark_feed_as_read_days", - "click .NB-feedbar-mark-feed-read-expand" : "expand_mark_read", - "click .NB-feedbar-train-feed" : "open_trainer", - "click .NB-feedbar-statistics" : "open_statistics", - "click .NB-feedlist-manage-icon" : "show_manage_menu", - "click .feed_favicon" : "show_manage_menu", - "click .NB-feedbar-options" : "open_options_popover", - "click" : "open", - "mousedown" : "highlight_event", - "mouseenter" : "add_hover_inverse", - "mouseleave" : "remove_hover_inverse" + "dblclick .feed_counts": "dblclick_mark_feed_as_read", + "dblclick": "open_feed_link", + "click .NB-feedbar-mark-feed-read": "mark_feed_as_read", + "click .NB-feedbar-mark-feed-read-time": "mark_feed_as_read_days", + "click .NB-feedbar-mark-feed-read-expand": "expand_mark_read", + "click .NB-feedbar-train-feed": "open_trainer", + "click .NB-feedbar-statistics": "open_statistics", + "click .NB-feedlist-manage-icon": "show_manage_menu", + "click .feed_favicon": "show_manage_menu", + "click .NB-feedbar-options": "open_options_popover", + "click": "open", + "mousedown": "highlight_event", + "mouseenter": "add_hover_inverse", + "mouseleave": "remove_hover_inverse" }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'render', 'delete_feed', 'changed', 'render_updated_time'); if (!this.options.feed_chooser) { this.listenTo(this.model, 'change', this.changed); @@ -32,31 +32,31 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ } else { this.listenTo(this.model, 'change:highlighted', this.render); } - + if (this.model.is_social() && !this.model.get('feed_title')) { var profile = NEWSBLUR.assets.user_profiles.get(this.model.get('user_id')) || {}; this.model.set('feed_title', profile.feed_title); } }, - - changed: function(model, options) { + + changed: function (model, options) { options = options || {}; var changes = _.keys(this.model.changedAttributes()); var ignore_attributes = ['notification_types', 'notification_filter']; - - var only_ignored = !_.any(changes, function(key) { + + var only_ignored = !_.any(changes, function (key) { return !_.contains(ignore_attributes, key); }); if (only_ignored) { return; } - var counts_changed = _.any(changes, function(key) { + var counts_changed = _.any(changes, function (key) { return _.contains(['ps', 'nt', 'ng'], key); }); - var only_counts_changed = !_.any(changes, function(key) { + var only_counts_changed = !_.any(changes, function (key) { return !_.contains(['ps', 'nt', 'ng'], key); }); - var only_selected_changed = !_.any(changes, function(key) { + var only_selected_changed = !_.any(changes, function (key) { return key != 'selected'; }); @@ -70,8 +70,8 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ if (!options.instant && counts_changed) this.flash_changes(); } }, - - remove: function() { + + remove: function () { if (this.counts_view) { this.counts_view.destroy(); } @@ -82,8 +82,8 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ this.stopListening(this.model); Backbone.View.prototype.remove.call(this); }, - - render: function() { + + render: function () { var feed = this.model; var extra_classes = this.extra_classes(); var $feed = $(_.template('<<%= list_type %> class="feed <% if (selected) { %>selected<% } %> <%= extra_classes %> <% if (highlighted) { %>NB-highlighted<% } %> <% if (toplevel) { %>NB-toplevel<% } %> <% if (disable_hover) { %>NB-no-hover<% } %>" data-id="<%= feed.id %>">\ @@ -140,20 +140,20 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ <% } %>\ >\ ', { - feed : feed, - type : this.options.type, - disable_hover : this.options.disable_hover, - extra_classes : extra_classes, - toplevel : this.options.depth == 0, - list_type : this.options.type == 'feed' ? 'li' : 'div', - selected : this.model.get('selected'), - highlighted : this.options.feed_chooser && - this.model.highlighted_in_folder(this.options.folder_title), - organizer : this.options.organizer, - pluralize : Inflector.pluralize, - has_notifications : this.model.get('notification_types') || [] + feed: feed, + type: this.options.type, + disable_hover: this.options.disable_hover, + extra_classes: extra_classes, + toplevel: this.options.depth == 0, + list_type: this.options.type == 'feed' ? 'li' : 'div', + selected: this.model.get('selected'), + highlighted: this.options.feed_chooser && + this.model.highlighted_in_folder(this.options.folder_title), + organizer: this.options.organizer, + pluralize: Inflector.pluralize, + has_notifications: this.model.get('notification_types') || [] })); - + if (this.options.type == 'story') { this.search_view = new NEWSBLUR.Views.FeedSearchView({ feedbar_view: this @@ -166,19 +166,19 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ this.render_counts(); this.setup_tooltips(); this.render_updated_time(); - + if (NEWSBLUR.reader.flags.search || NEWSBLUR.reader.flags.searching) { var $search = this.$("input[name=feed_search]"); $search.focus(); } - + this.$el.unbind('contextmenu') - .bind('contextmenu', _.bind(this.show_manage_menu_rightclick, this)); - + .bind('contextmenu', _.bind(this.show_manage_menu_rightclick, this)); + return this; }, - - extra_classes: function() { + + extra_classes: function () { var feed = this.model; var extra_classes = ''; var starred_feed = NEWSBLUR.assets.starred_feeds.get_feed(feed.id); @@ -207,7 +207,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ extra_classes += ' NB-feed-inactive'; } } - + if (feed.is_social()) { extra_classes += ' NB-feed-social'; if (feed.get('subscription_user_id') && !feed.get('shared_stories_count')) { @@ -217,11 +217,11 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ extra_classes += ' NB-feed-self-blurblog'; } } - + return extra_classes; }, - - render_counts: function() { + + render_counts: function () { if (this.counts_view) { this.counts_view.destroy(); } @@ -235,8 +235,8 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ this.$('.NB-story-title-indicator-count').html(this.counts_view.$el.clone()); } }, - - setup_tooltips: function() { + + setup_tooltips: function () { if (this.options.type == 'story' && NEWSBLUR.assets.preference('show_tooltips')) { this.$('.NB-feedbar-train-feed, .NB-feedbar-statistics').tipsy({ gravity: 's', @@ -244,26 +244,26 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ }); } }, - - render_updated_time: function() { + + render_updated_time: function () { if (this.options.type == 'story') { - var updated_text = this.model.get('updated') ? - this.model.get('updated') + ' ago' : - 'Loading...'; + var updated_text = this.model.get('updated') ? + this.model.get('updated') + ' ago' : + 'Loading...'; this.$('.NB-feedbar-last-updated-date').text(updated_text); } }, - - select_feed: function(options) { + + select_feed: function (options) { this.$el.toggleClass('selected', this.model.get('selected')); this.$el.toggleClass('NB-selected', this.model.get('selected')); - - _.each(this.folders, function(folder) { + + _.each(this.folders, function (folder) { folder.view.update_hidden(); }); }, - - flash_changes: function() { + + flash_changes: function () { var $highlight = this.$('.NB-feed-highlight'); $highlight.stop(); $highlight.css({ @@ -273,36 +273,36 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ $highlight.animate({ 'opacity': .7 }, { - 'duration': 800, - 'queue': false, - 'complete': function() { - $highlight.animate({'opacity': 0}, { - 'duration': 1000, + 'duration': 800, + 'queue': false, + 'complete': function () { + $highlight.animate({ 'opacity': 0 }, { + 'duration': 1000, 'queue': false, - 'complete': function() { + 'complete': function () { $highlight.css('display', 'none'); } }); } }); }, - - add_extra_classes: function() { + + add_extra_classes: function () { var extra_classes = this.extra_classes(); $(this.el).removeClass("unread_positive unread_neutral unread_negative unread_starred"); $(this.el).addClass(extra_classes); }, - + // ========== // = Events = // ========== - - click: function(e) { + + click: function (e) { this.highlight(); this.open(e); }, - - open: function(e, options) { + + open: function (e, options) { options = options || {}; if (this.options.feed_chooser && !options.ignore_feed_selector) return; if (this.options.type != 'feed') return; @@ -310,9 +310,9 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ if (e.which == 1 && $('.NB-menu-manage-container:visible').length) return; if (!options.ignore_double_click && $(e.target).closest('.feed_counts').length) { - _.delay(_.bind(function() { + _.delay(_.bind(function () { if (!this.flags.double_click) { - this.open(e, {ignore_double_click: true}); + this.open(e, { ignore_double_click: true }); } }, this), 250); return; @@ -325,9 +325,9 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ (this.model.get('has_exception') && this.model.get('exception_type') == 'feed')) { NEWSBLUR.reader.open_feed_exception_modal(this.model.id); } else if (this.model.is_search()) { - NEWSBLUR.reader.open_saved_search({search_model: this.model, $feed: this.$el}); + NEWSBLUR.reader.open_saved_search({ search_model: this.model, $feed: this.$el }); } else if (this.model.is_social()) { - NEWSBLUR.reader.open_social_stories(this.model.id, {force: true, $feed: this.$el}); + NEWSBLUR.reader.open_social_stories(this.model.id, { force: true, $feed: this.$el }); } else if (this.model.is_starred()) { NEWSBLUR.reader.open_starred_stories({ tag: this.model.tag_slug(), @@ -335,33 +335,33 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ $feed: this.$el }); } else { - NEWSBLUR.reader.open_feed(this.model.id, {$feed: this.$el}); + NEWSBLUR.reader.open_feed(this.model.id, { $feed: this.$el }); } }, - - highlight_event: function(e) { + + highlight_event: function (e) { if (this.$el.hasClass('NB-feed-selector-active')) { - return this.open(e, {'ignore_feed_selector': true}); + return this.open(e, { 'ignore_feed_selector': true }); } return this.highlight(); }, - - highlight: function(on, off) { + + highlight: function (on, off) { if (!this.options.feed_chooser) return; var model = this.model; - + if (this.options.organizer && this.options.hierarchy != 'flat') { model.highlight_in_folder(this.options.folder_title, on, off); } else { // Highlight all folders model.highlight_in_all_folders(on, off); } - + // Feed chooser disables binding to changes, so need to manually render. this.render(); }, - - open_feed_link: function(e) { + + open_feed_link: function (e) { e.preventDefault(); e.stopPropagation(); var dblclick_pref = NEWSBLUR.assets.preference('doubleclick_feed'); @@ -370,9 +370,9 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ if (this.options.starred_tag) return; if (this.options.feed_chooser) return; if ($('.NB-modal-feedchooser').is(':visible')) return; - + this.flags.double_click = true; - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.flags.double_click = false; }, this), 500); @@ -384,18 +384,18 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ window.open(this.model.get('feed_link'), '_blank'); window.focus(); } - + return false; }, - - dblclick_mark_feed_as_read: function(e) { + + dblclick_mark_feed_as_read: function (e) { if (this.options.feed_chooser) return; if (NEWSBLUR.assets.preference('doubleclick_unread') == "ignore") return; - + return this.mark_feed_as_read(e); }, - - mark_feed_as_read: function(e, days) { + + mark_feed_as_read: function (e, days) { if (this.options.starred_tag) return; if (e) { e.preventDefault(); @@ -403,7 +403,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ } this.flags.double_click = true; - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.flags.double_click = false; }, this), 500); NEWSBLUR.reader.mark_feed_as_read(this.model.id, days); @@ -412,22 +412,22 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ return false; } }, - - mark_feed_as_read_days: function(e) { + + mark_feed_as_read_days: function (e) { var days = parseInt($(e.target).data('days'), 10); this.mark_feed_as_read(e, days, true); }, - - expand_mark_read: function() { + + expand_mark_read: function () { var $container = this.$(".NB-feedbar-mark-feed-read-container"); var $markread = this.$(".NB-feedbar-mark-feed-read"); var $hidden = this.$(".NB-story-title-indicator"); var $expand = this.$(".NB-feedbar-mark-feed-read-expand"); var $times = this.$(".NB-feedbar-mark-feed-read-time"); var times_count = $times.length; - + $hidden.hide(); - $markread.css('z-index', times_count+1); + $markread.css('z-index', times_count + 1); $container.css('margin-left', $times.eq(0).outerWidth(true) * (times_count - 1) + 12); $expand.animate({ right: 0, @@ -436,8 +436,8 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ queue: false, easing: 'easeInQuint', duration: 180, - complete: function() { - $times.each(function(i) { + complete: function () { + $times.each(function (i) { $(this).css('z-index', times_count - i); $(this).animate({ right: (32 * (i + 1)) + 6 @@ -450,20 +450,20 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ } }); }, - - show_manage_menu_rightclick: function(e) { + + show_manage_menu_rightclick: function (e) { if (!NEWSBLUR.assets.preference('show_contextmenus')) return; - + return this.show_manage_menu(e); }, - - show_manage_menu: function(e) { + + show_manage_menu: function (e) { if (this.options.feed_chooser) return; - - var feed_type = this.model.is_social() ? 'socialfeed' : - this.model.is_starred() ? 'starred' : - this.model.is_search() ? 'search' : - 'feed'; + + var feed_type = this.model.is_social() ? 'socialfeed' : + this.model.is_starred() ? 'starred' : + this.model.is_search() ? 'search' : + 'feed'; e.preventDefault(); e.stopPropagation(); @@ -474,41 +474,41 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({ }); return false; }, - - delete_feed: function() { + + delete_feed: function () { this.$el.slideUp(500); - + if (this.model.get('selected')) { NEWSBLUR.reader.reset_feed(); NEWSBLUR.reader.show_splash_page(); } }, - - add_hover_inverse: function() { + + add_hover_inverse: function () { if (this.$el.offset().top > $(window).height() - 334) { this.$el.addClass('NB-hover-inverse'); - } + } }, - - remove_hover_inverse: function() { + + remove_hover_inverse: function () { this.$el.removeClass('NB-hover-inverse'); }, - - open_trainer: function() { + + open_trainer: function () { if (!$('.NB-task-manage').hasClass('NB-disabled')) { NEWSBLUR.reader.open_feed_intelligence_modal(1, null, !NEWSBLUR.reader.flags.social_view); } }, - - open_statistics: function() { + + open_statistics: function () { NEWSBLUR.reader.open_feed_statistics_modal(); }, - - open_options_popover: function() { + + open_options_popover: function () { NEWSBLUR.FeedOptionsPopover.create({ anchor: this.$(".NB-feedbar-options"), feed_id: this.model.id }); } - + }); diff --git a/media/js/newsblur/views/folder_view.js b/media/js/newsblur/views/folder_view.js index 45a2c63c4e..7cc70174c6 100644 --- a/media/js/newsblur/views/folder_view.js +++ b/media/js/newsblur/views/folder_view.js @@ -1,36 +1,36 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ className: 'folder', - + tagName: 'li', - + options: { depth: 0, collapsed: false, title: '', root: false }, - + events: { - "click .NB-feedlist-manage-icon" : "show_manage_menu", - "click .folder_title" : "open", - "click .NB-feedlist-collapse-icon" : "collapse_folder", - "click .NB-feedbar-mark-feed-read" : "mark_folder_as_read", - "click .NB-feedbar-mark-feed-read-expand" : "expand_mark_read", - "click .NB-feedbar-mark-feed-read-time" : "mark_folder_as_read_days", - "click .NB-feedbar-options" : "open_options_popover", - "click .NB-story-title-indicator" : "show_hidden_story_titles", - "mousedown .folder_title" : "highlight_feeds", - "mouseenter" : "add_hover_inverse", - "mouseleave" : "remove_hover_inverse" + "click .NB-feedlist-manage-icon": "show_manage_menu", + "click .folder_title": "open", + "click .NB-feedlist-collapse-icon": "collapse_folder", + "click .NB-feedbar-mark-feed-read": "mark_folder_as_read", + "click .NB-feedbar-mark-feed-read-expand": "expand_mark_read", + "click .NB-feedbar-mark-feed-read-time": "mark_folder_as_read_days", + "click .NB-feedbar-options": "open_options_popover", + "click .NB-story-title-indicator": "show_hidden_story_titles", + "mousedown .folder_title": "highlight_feeds", + "mouseenter": "add_hover_inverse", + "mouseleave": "remove_hover_inverse" }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'update_title', 'update_selected', 'delete_folder', 'check_collapsed', - 'update_hidden'); + 'update_hidden'); - this.options.folder_title = this.options.folder_title || - (this.model && this.model.get('folder_title')) || ""; + this.options.folder_title = this.options.folder_title || + (this.model && this.model.get('folder_title')) || ""; if (this.model && !this.options.feed_chooser) { // Root folder does not have a model. @@ -47,12 +47,12 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ this.collection.sort(); } }, - - remove: function() { + + remove: function () { this.destroy(); }, - - destroy: function() { + + destroy: function () { if (this.folder_count) { this.folder_count.destroy(); } @@ -67,8 +67,8 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ } this.$el.empty().remove(); }, - - render: function() { + + render: function () { var depth = this.options.depth; var folder_title = this.options.folder_title || ""; var feed_chooser = this.options.feed_chooser; @@ -76,14 +76,14 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ var hierarchy = this.options.hierarchy; var sorting = this.options.sorting; var folder_collection = this.collection; - this.options.collapsed = folder_title && _.contains(NEWSBLUR.Preferences.collapsed_folders, folder_title); + this.options.collapsed = folder_title && _.contains(NEWSBLUR.Preferences.collapsed_folders, folder_title); var $folder = this.render_folder(); - + if (!this.options.only_title) { - var $feeds = _.compact(this.collection.map(function(item) { + var $feeds = _.compact(this.collection.map(function (item) { if (item.is_feed()) { // if (!feed_chooser && !item.feed.get('active')) return; - var feed_title_view = _.detect(item.feed.views, function(view) { + var feed_title_view = _.detect(item.feed.views, function (view) { if (view.options.feed_chooser == feed_chooser && view.options.folder_title == folder_title) { return view; @@ -91,7 +91,7 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ }); if (!feed_title_view) { feed_title_view = new NEWSBLUR.Views.FeedTitleView({ - model: item.feed, + model: item.feed, type: 'feed', depth: depth, folder_title: folder_title, @@ -107,10 +107,10 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ // Reusing feed titles in chooser needs re-rendering to attach events feed_title_view.render(); } - return feed_title_view.el; + return feed_title_view.el; } else if (item.is_folder()) { // Reuse old feed views from previous choosers - var folder_view = _.detect(item.folder_views, function(view) { + var folder_view = _.detect(item.folder_views, function (view) { if (view.options.feed_chooser == feed_chooser) { return view; } @@ -138,18 +138,18 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ $feeds.push($.make('li', { className: 'feed NB-empty' })); this.$('.folder').append($feeds); } - - this.check_collapsed({skip_animation: true}); + + this.check_collapsed({ skip_animation: true }); this.update_hidden(); if (this.options.depth > 0) { // Only attach to visible folders. Top level has no folder, so wrongly attaches to first child. this.$('.folder_title').eq(0).bind('contextmenu', _.bind(this.show_manage_menu_rightclick, this)); } - + return this; }, - - render_folder: function($feeds) { + + render_folder: function ($feeds) { var $folder = _.template('<<%= list_type %> class="folder NB-folder">\ <% if (!root) { %>\
\ @@ -194,17 +194,17 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ <% } %>\ >\ ', { - depth : this.options.depth, - folder_title : this.options.folder_title, - is_collapsed : this.options.collapsed && !this.options.feed_chooser, - root : this.options.root, - feedbar : this.options.feedbar, - list_type : this.options.feedbar ? 'div' : 'li' + depth: this.options.depth, + folder_title: this.options.folder_title, + is_collapsed: this.options.collapsed && !this.options.feed_chooser, + root: this.options.root, + feedbar: this.options.feedbar, + list_type: this.options.feedbar ? 'div' : 'li' }); this.$el.replaceWith($folder); this.setElement($folder); - + if (this.options.feedbar) { this.show_collapsed_folder_count(); this.search_view = new NEWSBLUR.Views.FeedSearchView({ @@ -219,60 +219,60 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ } return $folder; }, - - update_title: function() { + + update_title: function () { this.$('.folder_title_text span').eq(0).html(this.model.get('folder_title')); }, - - update_selected: function() { + + update_selected: function () { this.$el.toggleClass('NB-selected', this.model.get('selected')); }, - - update_hidden: function() { + + update_hidden: function () { if (!this.model) return; - - var has_unreads = this.model.has_unreads({include_selected: true}); + + var has_unreads = this.model.has_unreads({ include_selected: true }); if (!has_unreads && NEWSBLUR.assets.preference('hide_read_feeds')) { this.$el.addClass('NB-hidden'); } else { this.$el.removeClass('NB-hidden'); } }, - + // =========== // = Actions = // =========== - - check_collapsed: function(options) { + + check_collapsed: function (options) { options = options || {}; var self = this; if (!this.options.folder_title || !this.options.folder_title.length) return; - + var show_folder_counts = NEWSBLUR.assets.preference('folder_counts'); var collapsed = _.contains(NEWSBLUR.Preferences.collapsed_folders, this.options.folder_title); if (collapsed || show_folder_counts) { this.show_collapsed_folder_count(options); } }, - - show_collapsed_folder_count: function(options) { + + show_collapsed_folder_count: function (options) { options = options || {}; var $folder_title = this.$('.folder_title').eq(0); var $counts = $('.feed_counts_floater', $folder_title); var $river = $('.NB-feedlist-collapse-icon', $folder_title); - + this.$el.addClass('NB-folder-collapsed'); $counts.remove(); if ($folder_title.hasClass('NB-hover')) { - $river.animate({'opacity': 0}, {'duration': options.skip_animation ? 0 : 100}); + $river.animate({ 'opacity': 0 }, { 'duration': options.skip_animation ? 0 : 100 }); $folder_title.addClass('NB-feedlist-folder-title-recently-collapsed'); - $folder_title.one('mouseover', function() { - $river.css({'opacity': ''}); + $folder_title.one('mouseover', function () { + $river.css({ 'opacity': '' }); $folder_title.removeClass('NB-feedlist-folder-title-recently-collapsed'); }); } - + if (this.folder_count) { this.folder_count.destroy(); } @@ -287,31 +287,31 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ 'opacity': 0 })); } - $counts.animate({'opacity': 1}, {'duration': options.skip_animation ? 0 : 400}); + $counts.animate({ 'opacity': 1 }, { 'duration': options.skip_animation ? 0 : 400 }); }, - - hide_collapsed_folder_count: function() { + + hide_collapsed_folder_count: function () { var $folder_title = this.$('.folder_title').eq(0); var $counts = $('.feed_counts_floater', $folder_title); var $river = $('.NB-feedlist-collapse-icon', $folder_title); - - $counts.animate({'opacity': 0}, { - 'duration': 300 + + $counts.animate({ 'opacity': 0 }, { + 'duration': 300 }); - - $river.animate({'opacity': .6}, {'duration': 400}); + + $river.animate({ 'opacity': .6 }, { 'duration': 400 }); $folder_title.removeClass('NB-feedlist-folder-title-recently-collapsed'); - $folder_title.one('mouseover', function() { - $river.css({'opacity': ''}); + $folder_title.one('mouseover', function () { + $river.css({ 'opacity': '' }); $folder_title.removeClass('NB-feedlist-folder-title-recently-collapsed'); }); }, - + // ========== // = Events = // ========== - - open: function(e) { + + open: function (e) { if (this.options.feed_chooser) return; e.preventDefault(); e.stopPropagation(); @@ -323,14 +323,14 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ NEWSBLUR.reader.open_river_stories(this.$el, this.model); }, - - show_manage_menu_rightclick: function(e) { + + show_manage_menu_rightclick: function (e) { if (!NEWSBLUR.assets.preference('show_contextmenus')) return; - + return this.show_manage_menu(e); }, - - show_manage_menu: function(e) { + + show_manage_menu: function (e) { if (this.options.feed_chooser) return; e.preventDefault(); e.stopPropagation(); @@ -343,28 +343,28 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ return false; }, - - add_hover_inverse: function() { + + add_hover_inverse: function () { if (this.$el.offset().top > $(window).height() - 246) { this.$el.addClass('NB-hover-inverse'); - } + } }, - - remove_hover_inverse: function() { + + remove_hover_inverse: function () { this.$el.removeClass('NB-hover-inverse'); }, - - delete_folder: function() { + + delete_folder: function () { this.$el.slideUp(500); - + var feed_ids_in_folder = this.model.feed_ids_in_folder(); if (_.contains(feed_ids_in_folder, NEWSBLUR.reader.active_feed)) { NEWSBLUR.reader.reset_feed(); NEWSBLUR.reader.show_splash_page(); } }, - - collapse_folder: function(e, options) { + + collapse_folder: function (e, options) { e.preventDefault(); e.stopPropagation(); options = options || {}; @@ -372,20 +372,20 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ var $children = this.$el.children('ul.folder'); var $folder = $(e.currentTarget).closest('li.folder'); if ($folder[0] != this.el) return; - + // Hiding / Collapsing - if (options.force_collapse || - ($children.length && - $children.eq(0).is(':visible') && - !this.collection.collapsed)) { + if (options.force_collapse || + ($children.length && + $children.eq(0).is(':visible') && + !this.collection.collapsed)) { NEWSBLUR.log(["hiding folder", $children, this.collection, this.options.folder_title]); NEWSBLUR.assets.collapsed_folders(this.options.folder_title, true); this.collection.collapsed = true; this.$el.addClass('NB-folder-collapsed'); - $children.animate({'opacity': 0}, { + $children.animate({ 'opacity': 0 }, { 'queue': false, 'duration': options.force_collapse ? 0 : 200, - 'complete': function() { + 'complete': function () { self.show_collapsed_folder_count(); $children.slideUp({ 'duration': 270, @@ -393,10 +393,10 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ }); } }); - } + } // Showing / Expanding - else if ($children.length && - (this.collection.collapsed || !$children.eq(0).is(':visible'))) { + else if ($children.length && + (this.collection.collapsed || !$children.eq(0).is(':visible'))) { NEWSBLUR.log(["showing folder", this.collection, this.options.folder_title]); NEWSBLUR.assets.collapsed_folders(this.options.folder_title, false); this.collection.collapsed = false; @@ -404,79 +404,79 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ if (!NEWSBLUR.assets.preference('folder_counts')) { this.hide_collapsed_folder_count(); } - $children.css({'opacity': 0}).slideDown({ + $children.css({ 'opacity': 0 }).slideDown({ 'duration': 240, 'easing': 'easeInOutCubic', - 'complete': function() { - $children.animate({'opacity': 1}, {'queue': false, 'duration': 200}); + 'complete': function () { + $children.animate({ 'opacity': 1 }, { 'queue': false, 'duration': 200 }); } }); } }, - - all_children_highlighted: function() { + + all_children_highlighted: function () { var folder_title = this.options.folder_title; - var all_children_highlighted = this.collection.all(function(item) { + var all_children_highlighted = this.collection.all(function (item) { if (item.is_feed()) { - var view = _.any(item.feed.views, function(view) { + var view = _.any(item.feed.views, function (view) { return view.options.feed_chooser && - view.options.folder_title == folder_title; + view.options.folder_title == folder_title; }); - + if (!view) return true; return item.feed.highlighted_in_folder(folder_title); } else if (item.is_folder()) { - return _.all(item.folder_views, function(view) { + return _.all(item.folder_views, function (view) { if (!view.options.feed_chooser) return true; - return view.all_children_highlighted(); + return view.all_children_highlighted(); }); } return true; }); - + return all_children_highlighted; }, - - highlighted_count_unique_folders: function() { + + highlighted_count_unique_folders: function () { var folder_title = this.options.folder_title; - var count = this.collection.reduce(function(memo, item) { + var count = this.collection.reduce(function (memo, item) { if (item.is_feed()) { - var view = _.detect(item.feed.views, function(view) { + var view = _.detect(item.feed.views, function (view) { return view.options.feed_chooser && - view.options.folder_title == folder_title; + view.options.folder_title == folder_title; }); - + if (!view) return memo; - + return item.feed.highlighted_in_folder(folder_title) ? memo + 1 : memo; } else { - return memo + _.reduce(item.folder_views, function(m, view) { + return memo + _.reduce(item.folder_views, function (m, view) { if (!view.options.feed_chooser) return m; return m + view.highlighted_count_unique_folders(); }, 0); } }, 0); - + return count; }, - - highlighted_count: function() { - var count = NEWSBLUR.assets.feeds.reduce(function(memo, item) { - var view = _.detect(item.views, function(view) { + + highlighted_count: function () { + var count = NEWSBLUR.assets.feeds.reduce(function (memo, item) { + var view = _.detect(item.views, function (view) { return view.options.feed_chooser; }); - + if (!view) return memo; - + var folders = item.get('highlighted_in_folders'); return (folders && folders.length) ? memo + 1 : memo; }, 0); - + return count; }, - - highlight_feeds: function(options) { + + highlight_feeds: function (options) { options = options || {}; if (!this.options.feed_chooser) return; var $folder = options.currentTarget && $(options.currentTarget).closest('li.folder'); @@ -486,52 +486,52 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ if (options.force_deselect) all_children_highlighted = true; var folder_title = this.options.folder_title; - this.collection.each(function(item) { + this.collection.each(function (item) { if (item.is_feed()) { - var view = _.detect(item.feed.views, function(view) { + var view = _.detect(item.feed.views, function (view) { if (view.options.feed_chooser && view.options.folder_title == folder_title) { return view; } }); - + if (!view) return; - + if (all_children_highlighted) { view.highlight(false, true); } else { view.highlight(true, false); } } else if (item.is_folder()) { - _.each(item.folder_views, function(view) { + _.each(item.folder_views, function (view) { if (!all_children_highlighted) { - view.highlight_feeds({force_highlight: true}); + view.highlight_feeds({ force_highlight: true }); } else { - view.highlight_feeds({force_deselect: options.force_deselect}); + view.highlight_feeds({ force_deselect: options.force_deselect }); } }); } }); }, - - highlighted_feeds: function(options, feeds) { + + highlighted_feeds: function (options, feeds) { if (!this.options.feed_chooser) return feeds; options = options || {}; feeds = feeds || []; - + var folder_title = this.options.folder_title; var collection = options.collection || this.collection; - + // If using overridden collection, only use for root level. Used for organizer. if (options.collection) delete options.collection; - - collection.each(function(item) { + + collection.each(function (item) { if (item.is_feed() && item.feed.get('highlighted')) { if (_.contains(item.feed.get('highlighted_in_folders'), folder_title)) { feeds.push([item.feed.id, folder_title]); } } else if (item.is_folder()) { - _.each(item.folder_views, function(view) { + _.each(item.folder_views, function (view) { feeds = view.highlighted_feeds(options, feeds); }); } @@ -539,30 +539,30 @@ NEWSBLUR.Views.Folder = Backbone.View.extend({ return feeds; }, - - mark_folder_as_read: function(e, days_back) { + + mark_folder_as_read: function (e, days_back) { NEWSBLUR.reader.mark_folder_as_read(this.model, days_back); this.$('.NB-feedbar-mark-feed-read-container').fadeOut(400); }, - mark_folder_as_read_days: function(e) { + mark_folder_as_read_days: function (e) { var days = parseInt($(e.target).data('days'), 10); this.mark_folder_as_read(e, days); }, - - expand_mark_read: function() { + + expand_mark_read: function () { NEWSBLUR.Views.FeedTitleView.prototype.expand_mark_read.call(this); }, - - open_options_popover: function() { + + open_options_popover: function () { NEWSBLUR.FeedOptionsPopover.create({ anchor: this.$(".NB-feedbar-options"), feed_id: "river:" + this.options.folder_title }); }, - - show_hidden_story_titles: function() { + + show_hidden_story_titles: function () { NEWSBLUR.app.story_titles_header.show_hidden_story_titles(); } - + }); diff --git a/media/js/newsblur/views/interactions_popover.js b/media/js/newsblur/views/interactions_popover.js index 1ccbc2c99d..ff8a2cc011 100644 --- a/media/js/newsblur/views/interactions_popover.js +++ b/media/js/newsblur/views/interactions_popover.js @@ -1,7 +1,7 @@ NEWSBLUR.InteractionsPopover = NEWSBLUR.ReaderPopover.extend({ - + className: "NB-interactions-popover", - + options: { 'width': 386, 'anchor': '.NB-feeds-header-user-interactions', @@ -14,44 +14,44 @@ NEWSBLUR.InteractionsPopover = NEWSBLUR.ReaderPopover.extend({ }, 'tab': 'interactions' }, - + page: 0, - + events: { - "click .NB-tab" : "switch_tab" + "click .NB-tab": "switch_tab" }, - + end_of_list: {}, - - initialize: function(options) { + + initialize: function (options) { this.options = _.extend({}, this.options, options); NEWSBLUR.ReaderPopover.prototype.initialize.call(this); - + this.model = NEWSBLUR.assets; this.render(); this.show_loading(); $(".NB-feeds-header-user-notifications").addClass('NB-active'); - + this.fetch_next_page(); - + }, - - close: function() { + + close: function () { $(".NB-feeds-header-user-notifications").removeClass('NB-active'); NEWSBLUR.app.sidebar_header.update_interactions_count(0); this.model.preference('dashboard_date', new Date); NEWSBLUR.ReaderPopover.prototype.close.call(this); }, - render: function() { + render: function () { var self = this; - + if (!this._on_page) { NEWSBLUR.ReaderPopover.prototype.render.call(this); this._on_page = true; } - + var $tab = $.make('div', [ $.make('div', { className: 'NB-interactions-header' }, [ $.make("div", { className: "NB-tab NB-tab-interactions" }, [ @@ -64,55 +64,55 @@ NEWSBLUR.InteractionsPopover = NEWSBLUR.ReaderPopover.extend({ $.make('div', { className: 'NB-interactions-container ' + (this.options.tab == 'interactions' && 'NB-active') }), $.make('div', { className: 'NB-activities-container ' + (this.options.tab == 'activities' && 'NB-active') }) ]); - + this.$el.html($tab); this.$el.removeClass("NB-active-interactions"); this.$el.removeClass("NB-active-activities"); this.$el.addClass("NB-active-" + this.options.tab); - + this.$(".NB-interactions-container,.NB-activities-container").unbind('scroll') .bind('scroll', _.bind(this.fill_out, this)); - + return this; }, - + // =========== // = Actions = // =========== - - show_loading: function() { + + show_loading: function () { this.hide_loading(); - + var $endline = $.make('div', { className: "NB-end-line NB-short" }); - $endline.css({'background': '#FFF'}); - this.$(".NB-"+this.options.tab+"-container").append($endline); + $endline.css({ 'background': '#FFF' }); + this.$(".NB-" + this.options.tab + "-container").append($endline); clearInterval(this.interactions_loading); - - $endline.animate({'backgroundColor': '#E1EBFF'}, {'duration': 550, 'easing': 'easeInQuad'}) - .animate({'backgroundColor': '#5C89C9'}, {'duration': 1550, 'easing': 'easeOutQuad'}) - .animate({'backgroundColor': '#E1EBFF'}, 1050); - _.delay(_.bind(function() { - this.interactions_loading = setInterval(function() { - $endline.animate({'backgroundColor': '#5C89C9'}, {'duration': 650}) - .animate({'backgroundColor': '#E1EBFF'}, 1050); + + $endline.animate({ 'backgroundColor': '#E1EBFF' }, { 'duration': 550, 'easing': 'easeInQuad' }) + .animate({ 'backgroundColor': '#5C89C9' }, { 'duration': 1550, 'easing': 'easeOutQuad' }) + .animate({ 'backgroundColor': '#E1EBFF' }, 1050); + _.delay(_.bind(function () { + this.interactions_loading = setInterval(function () { + $endline.animate({ 'backgroundColor': '#5C89C9' }, { 'duration': 650 }) + .animate({ 'backgroundColor': '#E1EBFF' }, 1050); }, 1700); - }, this), (550+1550+1050) - 1700); - + }, this), (550 + 1550 + 1050) - 1700); + }, - - hide_loading: function() { + + hide_loading: function () { clearInterval(this.interactions_loading); this.$(".NB-end-line").remove(); }, - - fetch_next_page: function() { + + fetch_next_page: function () { if (this.fetching) return; this.page += 1; this.show_loading(); this.fetching = true; - + // load_interactions_page or load_activities_page - this.model['load_'+this.options.tab+'_page'](this.page, _.bind(function(resp, type) { + this.model['load_' + this.options.tab + '_page'](this.page, _.bind(function (resp, type) { // console.log(["type", type, this.options.tab]); if (type != this.options.tab) return; this.fetching = false; @@ -121,24 +121,24 @@ NEWSBLUR.InteractionsPopover = NEWSBLUR.ReaderPopover.extend({ if (!resp || !$(".NB-interaction", $interactions).length) { this.no_more(); } else { - this.$(".NB-"+this.options.tab+"-container").append($interactions); + this.$(".NB-" + this.options.tab + "-container").append($interactions); this.fill_out(); } }, this)); }, - - no_more: function() { + + no_more: function () { this.end_of_list[this.options.tab] = true; var $end = $.make('div', { className: "NB-end-line" }, [ $.make('div', { className: 'NB-fleuron' }) ]); - this.$(".NB-"+this.options.tab+"-container").append($end); + this.$(".NB-" + this.options.tab + "-container").append($end); }, - - fill_out: function() { + + fill_out: function () { if (this.end_of_list[this.options.tab]) return; - - var $container = this.$(".NB-"+this.options.tab+"-container"); + + var $container = this.$(".NB-" + this.options.tab + "-container"); var containerHeight = $container.height(); var scrollTop = $container.scrollTop(); var $bottom = $(".NB-interaction,.NB-activity", $container).last(); @@ -147,25 +147,25 @@ NEWSBLUR.InteractionsPopover = NEWSBLUR.ReaderPopover.extend({ if (bottomOffset < containerHeight) { this.fetch_next_page(); } - + }, - - reset: function(type) { + + reset: function (type) { this.end_of_list = {}; this.options.tab = type; this.page = 0; this.fetching = false; }, - + // ========== // = Events = // ========== - - switch_tab: function(e) { + + switch_tab: function (e) { var $tab = $(e.currentTarget); e.preventDefault(); e.stopPropagation(); - + if ($tab.hasClass("NB-tab-interactions")) { this.reset('interactions'); this.render(); @@ -176,5 +176,5 @@ NEWSBLUR.InteractionsPopover = NEWSBLUR.ReaderPopover.extend({ this.fetch_next_page(); } } - + }); diff --git a/media/js/newsblur/views/module_follow_requests.js b/media/js/newsblur/views/module_follow_requests.js index 86755bb29f..9f491373e2 100644 --- a/media/js/newsblur/views/module_follow_requests.js +++ b/media/js/newsblur/views/module_follow_requests.js @@ -1,18 +1,18 @@ NEWSBLUR.Views.FollowRequestsModule = Backbone.View.extend({ - + POLL_INTERVAL: 10 * 60 * 1000, - + className: 'NB-module NB-module-followrequests', - - initialize: function() { + + initialize: function () { _.bindAll(this, 'start_polling'); NEWSBLUR.assets.user_profile.bind('change:protected', this.start_polling); }, - - start_polling: function() { - if (NEWSBLUR.assets.user_profile.get('protected') && + + start_polling: function () { + if (NEWSBLUR.assets.user_profile.get('protected') && NEWSBLUR.Globals.is_authenticated) { - this.poll = setInterval(_.bind(function() { + this.poll = setInterval(_.bind(function () { this.fetch_follow_requests(); }, this), this.POLL_INTERVAL); this.fetch_follow_requests(); @@ -20,17 +20,17 @@ NEWSBLUR.Views.FollowRequestsModule = Backbone.View.extend({ clearInterval(this.poll); } }, - - fetch_follow_requests: function() { - NEWSBLUR.assets.fetch_follow_requests(_.bind(function(data) { + + fetch_follow_requests: function () { + NEWSBLUR.assets.fetch_follow_requests(_.bind(function (data) { this.request_profiles = data.request_profiles || []; this.make_module(); }, this)); }, - - make_module: function() { + + make_module: function () { this.$el.empty(); - + if (this.request_profiles.length) { var $profiles = this.make_follow_requests(); this.$el.html($.make('h5', 'Requests to Follow You')); @@ -44,11 +44,11 @@ NEWSBLUR.Views.FollowRequestsModule = Backbone.View.extend({ this.$el.hide(); } }, - - make_follow_requests: function() { + + make_follow_requests: function () { var $profiles = $.make('div', { className: '.NB-followrequests-profiles' }); - - _.each(this.request_profiles, function(profile) { + + _.each(this.request_profiles, function (profile) { var profile_model = new NEWSBLUR.Models.User(profile); var $profile_badge = new NEWSBLUR.Views.SocialProfileBadge({ model: profile_model, @@ -56,8 +56,8 @@ NEWSBLUR.Views.FollowRequestsModule = Backbone.View.extend({ }); $profiles.append($profile_badge); }); - + return $profiles; } - + }); diff --git a/media/js/newsblur/views/original_tab_view.js b/media/js/newsblur/views/original_tab_view.js index 29592a0e1d..42feb3b543 100644 --- a/media/js/newsblur/views/original_tab_view.js +++ b/media/js/newsblur/views/original_tab_view.js @@ -1,19 +1,19 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ - - initialize: function() { + + initialize: function () { _.bindAll(this, 'handle_scroll_feed_iframe', 'handle_mousemove_iframe_view', 'setup_events'); this.reset_flags(); this.unload_feed_iframe(); this.setElement(NEWSBLUR.reader.$s.$feed_iframe); - + this.setup_events(); this.collection.bind('change:selected', this.toggle_selected_story, this); this.collection.bind('reset', this.reset_story_positions, this); this.collection.bind('add', this.reset_story_positions, this); }, - - reset_flags: function() { + + reset_flags: function () { this.cache = { iframe: {}, prefetch_iteration: 0 @@ -29,35 +29,35 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ iframe_buster_buster: false }; }, - - setup_events: function() { + + setup_events: function () { var $iframe_contents = this.$el.contents(); $iframe_contents.unbind('scroll').scroll(this.handle_scroll_feed_iframe); $iframe_contents.unbind('mousemove.reader').bind('mousemove.reader', this.handle_mousemove_iframe_view); }, - + // =================== // = Story Locations = // =================== - find_story_in_feed_iframe: function(story) { + find_story_in_feed_iframe: function (story) { if (!story) return $([]); - + var $iframe = this.$el.contents(); var $stories = $([]); - + if (this.flags['iframe_story_locations_fetched'] || story.id in this.cache.iframe_stories) { return this.cache.iframe_stories[story.id]; } - + var title = story.get('story_title', '').replace(/ |[^a-z0-9-,]/gi, ''); - - var search_document = function(node, title) { + + var search_document = function (node, title) { var skip = 0; - + if (node && node.nodeType == 3) { var pos = node.data.replace(/ |[^a-z0-9-,]/gi, '') - .indexOf(title); + .indexOf(title); if (pos >= 0) { $stories.push($(node).parent()); } @@ -69,20 +69,20 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } return skip; }; - + search_document($iframe.find('body')[0], title); - - $stories = $stories.filter(function() { + + $stories = $stories.filter(function () { return $(this).is(':visible'); }); - + if (!$stories.length) { // Not found straight, so check all header tags with styling children removed. - this.cache.iframe['headers'] = this.cache.iframe['headers'] - || $('h1,h2,h3,h4,h5,h6', $iframe).filter(':visible'); - this.cache.iframe['headers'].each(function() { + this.cache.iframe['headers'] = this.cache.iframe['headers'] + || $('h1,h2,h3,h4,h5,h6', $iframe).filter(':visible'); + this.cache.iframe['headers'].each(function () { var pos = $(this).text().replace(/ |[^a-z0-9-,]/gi, '') - .indexOf(title); + .indexOf(title); // NEWSBLUR.log(['Search headers', title, pos, $(this), $(this).text()]); if (pos >= 0) { $stories.push($(this)); @@ -90,15 +90,15 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } }); } - + if (!$stories.length) { // Still not found, so check Tumblr style .post's - this.cache.iframe['posts'] = this.cache.iframe['posts'] - || $('.entry,.post,.postProp,#postContent,.article', - $iframe).filter(':visible'); - this.cache.iframe['posts'].each(function() { + this.cache.iframe['posts'] = this.cache.iframe['posts'] + || $('.entry,.post,.postProp,#postContent,.article', + $iframe).filter(':visible'); + this.cache.iframe['posts'].each(function () { pos = $(this).text().replace(/ |[^a-z0-9-,]/gi, '') - .indexOf(title); + .indexOf(title); // NEWSBLUR.log(['Search .post', title, pos, $(this), $(this).text().replace(/ |[^a-z0-9-,]/gi, '')]); if (pos >= 0) { $stories.push($(this)); @@ -106,11 +106,11 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } }); } - + // Find the story with the biggest font size var max_size = 0; var $same_size_stories = $([]); - $stories.each(function() { + $stories.each(function () { var $this = $(this); var size = parseInt($this.css('font-size'), 10); if (size > max_size) { @@ -121,15 +121,15 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ $same_size_stories.push($this); } }); - + // NEWSBLUR.log(['Found stories', $stories.length, $same_size_stories.length, $same_size_stories, story.get('story_title')]); - + // Multiple stories at the same big font size? Determine story title overlap, // and choose the smallest difference in title length. var $story = $([]); if ($same_size_stories.length > 1) { var story_similarity = 100; - $same_size_stories.each(function() { + $same_size_stories.each(function () { var $this = $(this); var story_text = $this.text(); var overlap = Math.abs(story_text.length - story.get('story_title').length); @@ -143,24 +143,24 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ if (!$story.length) { $story = $same_size_stories[0]; } - + if ($story && $story.length) { // Check for NB-mark above and use that. if ($story.closest('.NB-mark').length) { $story = $story.closest('.NB-mark'); } - + this.cache.iframe_stories[story.id] = $story; var position_original = parseInt($story.offset().top, 10); // var position_offset = parseInt($story.offsetParent().scrollTop(), 10); var position = position_original; // + position_offset; this.cache.iframe_story_positions[position] = story; this.cache.iframe_story_positions_keys.push(position); - - + + if (!this.flags['iframe_view_not_busting']) { var feed_id = NEWSBLUR.reader.active_feed; - _.delay(_.bind(function() { + _.delay(_.bind(function () { if (feed_id == NEWSBLUR.reader.active_feed) { this.flags['iframe_view_not_busting'] = true; } @@ -169,45 +169,45 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } else { this.cache['story_misses'] += 1; } - + // NEWSBLUR.log(['Found story', $story]); return $story; }, - - scroll_to_selected_story: function(story, options) { + + scroll_to_selected_story: function (story, options) { var $iframe = this.$el; var $story = this.find_story_in_feed_iframe(story); options = options || {}; - + if (!story) return; - + if (options.only_if_hidden && this.$el.isScrollVisible($story, true)) { return; } - + if (!NEWSBLUR.assets.preference('animations') || NEWSBLUR.reader.story_view == 'feed' || NEWSBLUR.reader.story_view == 'story' || NEWSBLUR.reader.flags['page_view_showing_feed_view']) options.immediate = true; // NEWSBLUR.log(["Scroll in Original", story.get('story_title'), options]); - + if ($story && $story.length) { if (!options.immediate) { clearTimeout(NEWSBLUR.reader.locks.scrolling); NEWSBLUR.reader.flags['scrolling_by_selecting_story_title'] = true; } - - $iframe.stop().scrollTo($story, { + + $iframe.stop().scrollTo($story, { duration: options.immediate ? 0 : 380, - axis: 'y', - easing: 'easeInOutQuint', - offset: -24, - queue: false, - onAfter: function() { + axis: 'y', + easing: 'easeInOutQuint', + offset: -24, + queue: false, + onAfter: function () { if (options.immediate) return; - - NEWSBLUR.reader.locks.scrolling = setTimeout(function() { + + NEWSBLUR.reader.locks.scrolling = setTimeout(function () { NEWSBLUR.reader.flags['scrolling_by_selecting_story_title'] = false; }, 100); } @@ -221,20 +221,20 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ return false; }, - - prefetch_story_locations_in_story_frame: function() { + + prefetch_story_locations_in_story_frame: function () { var stories = NEWSBLUR.assets.stories; var $iframe = this.$el.contents(); var prefetch_tries_left = 3; - + if (!this.flags['iframe_loaded']) return; - + this.cache['prefetch_iteration'] += 1; NEWSBLUR.log(['Prefetching Original', !this.flags['iframe_fetching_story_locations'], !this.flags['iframe_story_locations_fetched']]); - if (!this.flags['iframe_fetching_story_locations'] + if (!this.flags['iframe_fetching_story_locations'] && !this.flags['iframe_story_locations_fetched']) { this.setup_events(); - + var last_story_index = this.cache.iframe_story_positions_keys.length; var last_story_position = _.last(this.cache.iframe_story_positions_keys); var last_story = this.cache.iframe_story_positions[last_story_position]; @@ -245,7 +245,7 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ // NEWSBLUR.log(['last_story', last_story_index, last_story_position, last_story, $last_story]); var last_story_same_position; if ($last_story && $last_story.length) { - last_story_same_position = parseInt($last_story.offset().top, 10)==last_story_position; + last_story_same_position = parseInt($last_story.offset().top, 10) == last_story_position; if (!last_story_same_position) { $.extend(this.cache, { 'iframe_stories': {}, @@ -254,19 +254,19 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ }); } } - - NEWSBLUR.assets.stories.any(_.bind(function(story, i) { - if (last_story_same_position && i < last_story_index) return true; - + + NEWSBLUR.assets.stories.any(_.bind(function (story, i) { + if (last_story_same_position && i < last_story_index) return true; + var $story = this.find_story_in_feed_iframe(story); // NEWSBLUR.log(['Pre-fetching', i, last_story_index, last_story_same_position, $story, story.get('story_title')]); - if (!$story || - !$story.length || + if (!$story || + !$story.length || this.flags['iframe_fetching_story_locations'] || this.flags['iframe_story_locations_fetched'] || - parseInt($story.offset().top, 10) > this.cache['prefetch_iteration']*2000) { + parseInt($story.offset().top, 10) > this.cache['prefetch_iteration'] * 2000) { if ($story && $story.length) { - NEWSBLUR.log(['Prefetch break on position too far', parseInt($story.offset().top, 10), this.cache['prefetch_iteration']*4000]); + NEWSBLUR.log(['Prefetch break on position too far', parseInt($story.offset().top, 10), this.cache['prefetch_iteration'] * 4000]); return true; } if (!prefetch_tries_left) { @@ -277,10 +277,10 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } }, this)); } - + if (!this.flags['iframe_fetching_story_locations'] && !this.flags['iframe_story_locations_fetched']) { - setTimeout(_.bind(function() { + setTimeout(_.bind(function () { if (!this.flags['iframe_fetching_story_locations'] && !this.flags['iframe_story_locations_fetched']) { this.prefetch_story_locations_in_story_frame(); @@ -290,13 +290,13 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ this.fetch_story_locations_in_story_frame(); } }, - - fetch_story_locations_in_story_frame: function($iframe, options) { + + fetch_story_locations_in_story_frame: function ($iframe, options) { var self = this; options = options || {}; if (!$iframe) $iframe = this.$el.contents(); if (options.reset_timer) this.counts['positions_timer'] = 0; - + this.flags['iframe_fetching_story_locations'] = true; this.flags['iframe_story_locations_fetched'] = false; @@ -306,13 +306,13 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ 'iframe_story_positions': {}, 'iframe_story_positions_keys': [] }); - - NEWSBLUR.assets.stories.any(_.bind(function(story, i) { - if ((story.get('story_feed_id') == NEWSBLUR.reader.active_feed || + + NEWSBLUR.assets.stories.any(_.bind(function (story, i) { + if ((story.get('story_feed_id') == NEWSBLUR.reader.active_feed || "social:" + story.get('social_user_id') == NEWSBLUR.reader.active_feed)) { var $story = this.find_story_in_feed_iframe(story); // NEWSBLUR.log(['Fetching story', i, story.get('story_title'), $story]); - + if (self.cache['story_misses'] > 5) { // NEWSBLUR.log(['iFrame view entirely loaded', self.cache['story_misses'], self.cache.iframe_stories]); self.flags['iframe_story_locations_fetched'] = true; @@ -321,54 +321,54 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ return true; } } else if (story && story.get('story_feed_id') != NEWSBLUR.reader.active_feed && - "social:" + story.get('social_user_id') != NEWSBLUR.reader.active_feed) { + "social:" + story.get('social_user_id') != NEWSBLUR.reader.active_feed) { NEWSBLUR.log(['Switched off iframe early', NEWSBLUR.reader.active_feed, story.get('story_feed_id'), story.get('social_user_id')]); return true; } }, this)); - - NEWSBLUR.log(['Original view entirely loaded', _.keys(self.cache.iframe_stories).length + " stories", this.counts['positions_timer']/1000 + " sec delay"]); - - this.counts['positions_timer'] = Math.min(60*1000, Math.max(this.counts['positions_timer']*2, 1*1000)); + + NEWSBLUR.log(['Original view entirely loaded', _.keys(self.cache.iframe_stories).length + " stories", this.counts['positions_timer'] / 1000 + " sec delay"]); + + this.counts['positions_timer'] = Math.min(60 * 1000, Math.max(this.counts['positions_timer'] * 2, 1 * 1000)); clearTimeout(this.flags['next_fetch']); this.flags['next_fetch'] = _.delay(_.bind(this.fetch_story_locations_in_story_frame, this), - this.counts['positions_timer']); + this.counts['positions_timer']); }, - - reset_story_positions: function(models) { + + reset_story_positions: function (models) { if (!models || !models.length) { models = NEWSBLUR.assets.stories; } if (!models.length) return; if (NEWSBLUR.reader.story_view != 'page') return; - + this.flags['iframe_fetching_story_locations'] = false; this.flags['iframe_story_locations_fetched'] = false; - + if (NEWSBLUR.reader.flags['story_titles_loaded']) { - this.fetch_story_locations_in_story_frame({reset_timer: true}); + this.fetch_story_locations_in_story_frame({ reset_timer: true }); } else { this.prefetch_story_locations_in_story_frame(); } }, - + // =========== // = Actions = // =========== - - iframe_not_busting: function() { + + iframe_not_busting: function () { this.flags['iframe_not_busting'] = true; }, - - unload_feed_iframe: function() { + + unload_feed_iframe: function () { var $taskbar_view_page = $('.NB-taskbar .task_view_page'); $taskbar_view_page.removeClass('NB-task-return'); - + clearInterval(this.flags['iframe_scroll_snapback_check']); - + this.flags['iframe_story_locations_fetched'] = false; NEWSBLUR.reader.flags['iframe_prevented_from_loading'] = false; - + $.extend(this.cache, { 'iframe_stories': {}, 'iframe_story_positions': {}, @@ -377,7 +377,7 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ 'iframe_scroll': 0, 'iframe_feed_id': null }); - + $.extend(this.flags, { 'iframe_loaded': false, 'iframe_scroll_snapback_check': false, @@ -386,55 +386,55 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ 'iframe_story_locations_fetched': false, 'iframe_scroll_snap_back_prepared': false }); - + this.$el.removeAttr('src'); this.$el.empty(); - + clearInterval(this.iframe_link_attacher); }, - - load_feed_iframe: function(feed_id) { + + load_feed_iframe: function (feed_id) { feed_id = feed_id || NEWSBLUR.reader.active_feed; var self = this; this.flags['iframe_loaded'] = true; - - var page_url = '/reader/page/'+feed_id; + + var page_url = '/reader/page/' + feed_id; if (NEWSBLUR.reader.flags['social_view']) { var feed = NEWSBLUR.assets.get_feed(feed_id); page_url = feed.get('page_url'); } - + NEWSBLUR.reader.flags.iframe_scroll_snap_back_prepared = true; this.iframe_link_attacher_num_links = 0; this.setup_feed_page_iframe_load(); - + this.$el.attr('src', page_url); this.enable_iframe_buster_buster(); - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.prefetch_story_locations_in_story_frame(); }, this), 500); this.setup_events(); - this.$el.ready(function() { - + this.$el.ready(function () { + if (feed_id != NEWSBLUR.reader.active_feed) { NEWSBLUR.log(["Switched feed, unloading iframe"]); self.unload_feed_iframe(); return; } - - setTimeout(function() { - self.$el.on('load', function() { + + setTimeout(function () { + self.$el.on('load', function () { self.flags.iframe_scroll_snap_back_prepared = true; self.return_to_snapback_position(true); self.cache.iframe_feed_id = NEWSBLUR.reader.active_feed; }); }, 50); - self.flags['iframe_scroll_snapback_check'] = setInterval(function() { + self.flags['iframe_scroll_snapback_check'] = setInterval(function () { // NEWSBLUR.log(['Checking scroll', self.cache.iframe_scroll, self.flags.iframe_scroll_snap_back_prepared, self.flags['iframe_scroll_snapback_check']]); - if (self.cache.iframe_scroll && + if (self.cache.iframe_scroll && self.flags.iframe_scroll_snap_back_prepared && self.cache.iframe_feed_id == NEWSBLUR.reader.active_feed) { self.return_to_snapback_position(); @@ -442,10 +442,10 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ clearInterval(self.flags['iframe_scroll_snapback_check']); } }, 500); - + var feed_iframe_src = self.$el.attr('src'); - if (feed_iframe_src && feed_iframe_src.indexOf('/reader/page/'+feed_id) != -1) { - var iframe_link_attacher = function() { + if (feed_iframe_src && feed_iframe_src.indexOf('/reader/page/' + feed_id) != -1) { + var iframe_link_attacher = function () { var contents = self.$el.contents(); var num_links = contents.find('a').length; // NEWSBLUR.log(['Finding links', self.iframe_link_attacher_num_links, num_links]); @@ -454,23 +454,23 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ self.iframe_link_attacher_num_links = num_links; contents.find('a') .unbind('click.NB-taskbar') - .bind('click.NB-taskbar', function() { - self.taskbar_show_return_to_page(); - }); + .bind('click.NB-taskbar', function () { + self.taskbar_show_return_to_page(); + }); } }; clearInterval(self.iframe_link_attacher); self.iframe_link_attacher = setInterval(iframe_link_attacher, 1000); iframe_link_attacher(); - self.$el.on('load', function() { + self.$el.on('load', function () { clearInterval(self.iframe_link_attacher); }); } self.setup_events(); }); }, - - return_to_snapback_position: function(iframe_loaded) { + + return_to_snapback_position: function (iframe_loaded) { // console.log(["return_to_snapback_position", iframe_loaded, this.flags.iframe_scroll_snap_back_prepared, this.cache.iframe_scroll, this.cache.iframe_feed_id]); if (this.cache.iframe_scroll && this.$el.contents().scrollTop() == 0 @@ -483,9 +483,9 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } } }, - - setup_feed_page_iframe_load: function() { - this.$el.on('load', _.bind(function() { + + setup_feed_page_iframe_load: function () { + this.$el.on('load', _.bind(function () { this.disable_iframe_buster_buster(); this.setup_events(); if (NEWSBLUR.reader.flags['story_titles_loaded']) { @@ -493,22 +493,22 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ this.fetch_story_locations_in_story_frame(); } // try { - var $iframe_contents = this.$el.contents(); - $iframe_contents.find('a') - .unbind('click.NB-taskbar') - .bind('click.NB-taskbar', _.bind(function(e) { + var $iframe_contents = this.$el.contents(); + $iframe_contents.find('a') + .unbind('click.NB-taskbar') + .bind('click.NB-taskbar', _.bind(function (e) { var href = $(this).attr('href'); if (href && href.indexOf('#') == 0) { e.preventDefault(); - var $footnote = $('a[name='+href.substr(1)+'], [id='+href.substr(1)+']', - $iframe_contents); + var $footnote = $('a[name=' + href.substr(1) + '], [id=' + href.substr(1) + ']', + $iframe_contents); NEWSBLUR.log(['Footnote', $footnote, href, href.substr(1)]); - $iframe_contents.scrollTo($footnote, { + $iframe_contents.scrollTo($footnote, { duration: 600, - axis: 'y', - easing: 'easeInOutQuint', - offset: 0, - queue: false + axis: 'y', + easing: 'easeInOutQuint', + offset: 0, + queue: false }); return false; } @@ -527,60 +527,60 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } }, - - taskbar_show_return_to_page: function() { - _.delay(_.bind(function() { + + taskbar_show_return_to_page: function () { + _.delay(_.bind(function () { var $taskbar_view_page = $('.NB-taskbar .task_view_page'); - + try { NEWSBLUR.log(['return', this.$el.contents().find('body')]); var length = this.$el.contents().find('body').length; if (length) { return false; } - } catch(e) { - $taskbar_view_page.addClass('NB-task-return'); + } catch (e) { + $taskbar_view_page.addClass('NB-task-return'); } finally { - $taskbar_view_page.addClass('NB-task-return'); + $taskbar_view_page.addClass('NB-task-return'); } }, this), 1000); }, - + // ======================== // = iFrame Buster Buster = // ======================== - - enable_iframe_buster_buster: function() { + + enable_iframe_buster_buster: function () { var self = this; var prevent_bust = 0; - window.onbeforeunload = function() { - prevent_bust++; + window.onbeforeunload = function () { + prevent_bust++; }; clearInterval(this.locks.iframe_buster_buster); - this.locks.iframe_buster_buster = setInterval(function() { + this.locks.iframe_buster_buster = setInterval(function () { if (prevent_bust > 0) { prevent_bust -= 2; - if (self.flags['iframe_story_locations_fetched'] && - !self.flags['iframe_view_not_busting'] && - _.contains(['page', 'story'], self.story_view) && + if (self.flags['iframe_story_locations_fetched'] && + !self.flags['iframe_view_not_busting'] && + _.contains(['page', 'story'], self.story_view) && NEWSBLUR.reader.active_feed) { - $('.NB-feed-frame').attr('src', ''); - window.top.location = '/reader/buster'; - NEWSBLUR.reader.switch_taskbar_view('feed'); + $('.NB-feed-frame').attr('src', ''); + window.top.location = '/reader/buster'; + NEWSBLUR.reader.switch_taskbar_view('feed'); } } }, 1); }, - - disable_iframe_buster_buster: function() { + + disable_iframe_buster_buster: function () { clearInterval(this.locks.iframe_buster_buster); }, - + // ========== // = Events = // ========== - - handle_scroll_feed_iframe: function(e) { + + handle_scroll_feed_iframe: function (e) { if (NEWSBLUR.reader.story_view == 'page' && !NEWSBLUR.reader.flags['page_view_showing_feed_view'] && !NEWSBLUR.reader.flags['scrolling_by_selecting_story_title']) { @@ -591,9 +591,9 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ if (!story) return; if (story.score() < NEWSBLUR.reader.get_unread_view_score()) return; // NEWSBLUR.log(['Scroll iframe', from_top, closest, positions[closest], this.cache.iframe_story_positions[positions[closest]]]); - + if (!story.get('selected')) { - story.set('selected', true, {selected_in_original: true, scroll: true, immediate: true}); + story.set('selected', true, { selected_in_original: true, scroll: true, immediate: true }); } if (!this.flags.iframe_scroll_snap_back_prepared) { this.cache.iframe_scroll = from_top - NEWSBLUR.reader.cache.mouse_position_y; @@ -601,9 +601,9 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ this.flags.iframe_scroll_snap_back_prepared = false; } }, - - handle_mousemove_iframe_view: function(e) { - var self = this; + + handle_mousemove_iframe_view: function (e) { + var self = this; NEWSBLUR.reader.show_mouse_indicator(); if (parseInt(NEWSBLUR.assets.preference('lock_mouse_indicator'), 10)) { @@ -614,11 +614,11 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ // NEWSBLUR.log(["mousemove", e, scroll_top, e.pageY]); NEWSBLUR.reader.cache.mouse_position_y = e.pageY - scroll_top; NEWSBLUR.reader.$s.$mouse_indicator.css('top', NEWSBLUR.reader.cache.mouse_position_y - 8); - + // setTimeout(_.bind(function() { // this.flags['mousemove_timeout'] = false; // }, this), 40); - + if (this.flags['mousemove_timeout'] || NEWSBLUR.reader.flags['scrolling_by_selecting_story_title']) { return; @@ -637,18 +637,18 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } return; } - + if (!story.get('selected')) { - story.set('selected', true, {selected_in_original: true, mouse: true, immediate: true}); + story.set('selected', true, { selected_in_original: true, mouse: true, immediate: true }); // this.flags['mousemove_timeout'] = false; } }, - - toggle_selected_story: function(model, selected, options) { + + toggle_selected_story: function (model, selected, options) { options = options || {}; - - if (selected && - NEWSBLUR.reader.story_view == 'page' && + + if (selected && + NEWSBLUR.reader.story_view == 'page' && !options.selected_in_original && !options.selected_by_scrolling) { var found = this.scroll_to_selected_story(model); @@ -660,5 +660,5 @@ NEWSBLUR.Views.OriginalTabView = Backbone.View.extend({ } } } - + }); diff --git a/media/js/newsblur/views/profile_badge_view.js b/media/js/newsblur/views/profile_badge_view.js index 62c2b87928..6b213c5c9a 100644 --- a/media/js/newsblur/views/profile_badge_view.js +++ b/media/js/newsblur/views/profile_badge_view.js @@ -1,7 +1,7 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ - + className: "NB-profile-badge", - + events: { "click .NB-profile-badge-action-follow": "follow_user", "click .NB-profile-badge-action-unfollow": "unfollow_user", @@ -17,26 +17,26 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ "mouseenter .NB-profile-badge-action-follow": "mouseenter_follow", "mouseleave .NB-profile-badge-action-follow": "mouseleave_follow" }, - - constructor : function(options) { + + constructor: function (options) { Backbone.View.call(this, options); this.render(); return this.el; }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'render'); this.model.bind('change', this.render); }, - - render: function() { + + render: function () { var profile = this.model; this.$el.html($.make('table', {}, [ $.make('tr', [ $.make('td', { className: 'NB-profile-badge-photo-wrapper' }, [ $.make('div', { className: 'NB-profile-badge-photo' }, [ - $.make('img', { src: profile.photo_url({'size': this.options.photo_size}) }) + $.make('img', { src: profile.photo_url({ 'size': this.options.photo_size }) }) ]) ]), $.make('td', { className: 'NB-profile-badge-info' }, [ @@ -45,29 +45,29 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ ]), $.make('div', { className: 'NB-profile-badge-username NB-splash-link' }, profile.get('username')), $.make('div', { className: 'NB-profile-badge-location' }, profile.get('location')), - (profile.get('website') && $.make('a', { - href: profile.get('website'), + (profile.get('website') && $.make('a', { + href: profile.get('website'), target: '_blank', rel: 'nofollow', className: 'NB-profile-badge-website NB-splash-link' }, profile.get('website').replace('http://', ''))), $.make('div', { className: 'NB-profile-badge-bio' }, profile.get('bio')), - (_.isNumber(profile.get('shared_stories_count')) && - $.make('div', { className: 'NB-profile-badge-stats' }, [ - $.make('span', { className: 'NB-count' }, Inflector.commas(profile.get('shared_stories_count'))), - 'shared ', - Inflector.pluralize('story', profile.get('shared_stories_count')), - ' · ', - $.make('a', { href: profile.blurblog_url(), target: "_blank", className: "NB-profile-badge-blurblog-link NB-splash-link" }, profile.blurblog_url().replace('http://', '')), - (this.model.get('following_you') && $.make('span', [ + (_.isNumber(profile.get('shared_stories_count')) && + $.make('div', { className: 'NB-profile-badge-stats' }, [ + $.make('span', { className: 'NB-count' }, Inflector.commas(profile.get('shared_stories_count'))), + 'shared ', + Inflector.pluralize('story', profile.get('shared_stories_count')), ' · ', - $.make('div', { className: 'NB-profile-badge-following-you' }, 'Follows you') - ])), - (NEWSBLUR.Globals.is_admin && $.make('span', [ - ' · ', - $.make('span', { className: 'NB-profile-badge-action-admin' }) + $.make('a', { href: profile.blurblog_url(), target: "_blank", className: "NB-profile-badge-blurblog-link NB-splash-link" }, profile.blurblog_url().replace('http://', '')), + (this.model.get('following_you') && $.make('span', [ + ' · ', + $.make('div', { className: 'NB-profile-badge-following-you' }, 'Follows you') + ])), + (NEWSBLUR.Globals.is_admin && $.make('span', [ + ' · ', + $.make('span', { className: 'NB-profile-badge-action-admin' }) + ])) ])) - ])) ]) ]) ])); @@ -75,83 +75,83 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ var $actions; if (this.options.request_approval) { $actions = $.make('div', { className: 'NB-profile-badge-action-buttons' }, [ - $.make('div', { - className: 'NB-profile-badge-action-approve NB-modal-submit-button NB-modal-submit-green' + $.make('div', { + className: 'NB-profile-badge-action-approve NB-modal-submit-button NB-modal-submit-green' }, [ $.make('span', 'Approve') ]), - $.make('div', { + $.make('div', { className: 'NB-profile-badge-action-ignore NB-modal-submit-button NB-modal-submit-grey ' + - (!profile.get('shared_stories_count') ? 'NB-disabled' : '') + (!profile.get('shared_stories_count') ? 'NB-disabled' : '') }, 'Ignore') - ]); + ]); } else if (NEWSBLUR.reader.model.user_profile.get('user_id') == profile.get('user_id')) { $actions = $.make('div', { className: 'NB-profile-badge-action-buttons' }, [ - $.make('div', { - className: 'NB-profile-badge-action-self NB-modal-submit-button' + $.make('div', { + className: 'NB-profile-badge-action-self NB-modal-submit-button' }, 'You'), - (this.options.show_edit_button && $.make('div', { + (this.options.show_edit_button && $.make('div', { className: 'NB-profile-badge-action-edit NB-modal-submit-button NB-modal-submit-grey ' + - (!profile.get('shared_stories_count') ? 'NB-disabled' : '') + (!profile.get('shared_stories_count') ? 'NB-disabled' : '') }, 'Edit Profile')) ]); } else if (profile.get('followed_by_you')) { - $actions = $.make('div', { - className: 'NB-profile-badge-action-unfollow NB-profile-badge-action-buttons NB-modal-submit-button NB-modal-submit-grey' + $actions = $.make('div', { + className: 'NB-profile-badge-action-unfollow NB-profile-badge-action-buttons NB-modal-submit-button NB-modal-submit-grey' }, 'Following'); } else if (profile.get('requested_follow')) { - $actions = $.make('div', { - className: 'NB-profile-badge-action-unfollow NB-profile-badge-action-buttons NB-modal-submit-button NB-modal-submit-grey' + $actions = $.make('div', { + className: 'NB-profile-badge-action-unfollow NB-profile-badge-action-buttons NB-modal-submit-button NB-modal-submit-grey' }, [ $.make('span', 'Requested') ]); } else if (profile.get('protected')) { $actions = $.make('div', { className: 'NB-profile-badge-action-buttons' }, [ - $.make('div', { - className: 'NB-profile-badge-action-follow NB-profile-badge-action-protected-follow NB-modal-submit-button NB-modal-submit-green' + $.make('div', { + className: 'NB-profile-badge-action-follow NB-profile-badge-action-protected-follow NB-modal-submit-button NB-modal-submit-green' }, [ $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + 'img/icons/circular/g_icn_lock.png' }), $.make('span', 'Follow') ]), - (!profile.get('private') && $.make('div', { + (!profile.get('private') && $.make('div', { className: 'NB-profile-badge-action-preview NB-modal-submit-button NB-modal-submit-grey ' + - (!profile.get('shared_stories_count') ? 'NB-disabled' : '') + (!profile.get('shared_stories_count') ? 'NB-disabled' : '') }, 'Preview')), - ($.make('div', { + ($.make('div', { className: 'NB-profile-badge-action-mute NB-modal-submit-button NB-modal-submit-grey' }, $.make('span', (profile.get('muted') ? 'Unmute' : 'Mute')))) - ]); + ]); } else { $actions = $.make('div', { className: 'NB-profile-badge-action-buttons' }, [ - $.make('div', { - className: 'NB-profile-badge-action-follow NB-modal-submit-button NB-modal-submit-green' + $.make('div', { + className: 'NB-profile-badge-action-follow NB-modal-submit-button NB-modal-submit-green' }, [ $.make('span', 'Follow') ]), - $.make('div', { + $.make('div', { className: 'NB-profile-badge-action-preview NB-modal-submit-button NB-modal-submit-grey ' + - (!profile.get('shared_stories_count') ? 'NB-disabled' : '') + (!profile.get('shared_stories_count') ? 'NB-disabled' : '') }, 'Preview'), - $.make('div', { + $.make('div', { className: 'NB-profile-badge-action-mute NB-modal-submit-button NB-modal-submit-grey ' }, $.make('span', (profile.get('muted') ? 'Unmute' : 'Mute'))) ]); } this.$('.NB-profile-badge-actions').append($actions); - + if (this.options.embiggen) { this.$el.addClass("NB-profile-badge-embiggen"); } - + return this; }, - - follow_user: function() { + + follow_user: function () { this.$('.NB-loading').addClass('NB-active'); - NEWSBLUR.assets.follow_user(this.model.get('user_id'), _.bind(function(data) { + NEWSBLUR.assets.follow_user(this.model.get('user_id'), _.bind(function (data) { this.$('.NB-loading').removeClass('NB-active'); this.model.set(data.follow_profile); - + var $button = this.$('.NB-profile-badge-action-follow'); $button.find('span').text(this.model.get('protected') ? 'Requested' : 'Following'); $button.removeClass('NB-modal-submit-green') @@ -159,33 +159,33 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ .addClass('NB-modal-submit-grey'); $button.removeClass('NB-profile-badge-action-follow') .addClass('NB-profile-badge-action-unfollow'); - + NEWSBLUR.app.feed_list.make_social_feeds(); }, this)); }, - - unfollow_user: function() { + + unfollow_user: function () { this.$('.NB-loading').addClass('NB-active'); - NEWSBLUR.reader.model.unfollow_user(this.model.get('user_id'), _.bind(function(data, unfollow_user) { + NEWSBLUR.reader.model.unfollow_user(this.model.get('user_id'), _.bind(function (data, unfollow_user) { this.$('.NB-loading').removeClass('NB-active'); this.model.set(data.unfollow_profile); - + var $button = this.$('.NB-profile-badge-action-follow'); $button.find('span').text(this.model.get('protected') ? 'Canceled Request' : 'Unfollowed'); $button.removeClass('NB-modal-submit-grey') .addClass('NB-modal-submit-red'); $button.removeClass('NB-profile-badge-action-unfollow') .addClass('NB-profile-badge-action-follow'); - + NEWSBLUR.app.feed_list.make_social_feeds(); }, this)); }, - - approve_user: function() { + + approve_user: function () { this.$('.NB-loading').addClass('NB-active'); - NEWSBLUR.assets.approve_follower(this.model.get('user_id'), _.bind(function(data) { + NEWSBLUR.assets.approve_follower(this.model.get('user_id'), _.bind(function (data) { this.$('.NB-loading').removeClass('NB-active'); - + var $button = this.$('.NB-profile-badge-action-approve'); $button.find('span').text('Approved'); $button.removeClass('NB-modal-submit-green'); @@ -196,12 +196,12 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ $button.remove(); }, this)); }, - - ignore_user: function() { + + ignore_user: function () { this.$('.NB-loading').addClass('NB-active'); - NEWSBLUR.assets.ignore_follower(this.model.get('user_id'), _.bind(function(data) { + NEWSBLUR.assets.ignore_follower(this.model.get('user_id'), _.bind(function (data) { this.$('.NB-loading').removeClass('NB-active'); - + var $button = this.$('.NB-profile-badge-action-approve'); $button.find('span').text('Ignored'); $button.removeClass('NB-modal-submit-green'); @@ -212,41 +212,41 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ $button.remove(); }, this)); }, - + mute_user: function () { if (this.model.get('muted')) { return this.unmute_user(); } this.$('.NB-loading').addClass('NB-active'); - NEWSBLUR.assets.mute_user(this.model.get('user_id'), _.bind(function(data) { + NEWSBLUR.assets.mute_user(this.model.get('user_id'), _.bind(function (data) { this.model.set('muted', true); - + this.$('.NB-loading').removeClass('NB-active'); var $button = this.$('.NB-profile-badge-action-mute'); $button.find('span').text('Muted'); }, this)); }, - - unmute_user: function() { + + unmute_user: function () { this.$('.NB-loading').addClass('NB-active'); - NEWSBLUR.assets.unmute_user(this.model.get('user_id'), _.bind(function(data) { + NEWSBLUR.assets.unmute_user(this.model.get('user_id'), _.bind(function (data) { this.model.set('muted', false); - + this.$('.NB-loading').removeClass('NB-active'); var $button = this.$('.NB-profile-badge-action-mute'); $button.find('span').text('Unmuted'); }, this)); }, - - preview_user: function() { + + preview_user: function () { if (this.$('.NB-profile-badge-action-preview').hasClass('NB-disabled')) return; - var open_preview = _.bind(function() { + var open_preview = _.bind(function () { window.ss = this.model; var socialsub = NEWSBLUR.reader.model.add_social_feed(this.model); NEWSBLUR.reader.load_social_feed_in_tryfeed_view(socialsub); }, this); - + if (!_.keys($.modal.impl.d).length) { open_preview(); } else { @@ -254,43 +254,43 @@ NEWSBLUR.Views.SocialProfileBadge = Backbone.View.extend({ } }, - open_profile: function() { + open_profile: function () { var user_id = this.model.get('user_id'); NEWSBLUR.reader.model.add_user_profiles([this.model]); - + if ($('.NB-modal').is(':visible')) { - $.modal.close(function() { + $.modal.close(function () { NEWSBLUR.reader.open_social_profile_modal(user_id); }); } else { NEWSBLUR.reader.open_social_profile_modal(user_id); } }, - - open_edit_profile: function() { - $.modal.close(function() { + + open_edit_profile: function () { + $.modal.close(function () { NEWSBLUR.reader.open_profile_editor_modal(); }); }, - - mouseenter_unfollow: function() { + + mouseenter_unfollow: function () { this.$('.NB-profile-badge-action-unfollow span').text(this.model.get('requested_follow') ? 'Cancel' : 'Unfollow').addClass('NB-active'); }, - - mouseleave_unfollow: function() { + + mouseleave_unfollow: function () { this.$('.NB-profile-badge-action-unfollow span').text(this.model.get('requested_follow') ? 'Requested' : 'Following').removeClass('NB-active'); }, - - mouseenter_follow: function() { + + mouseenter_follow: function () { this.$('.NB-profile-badge-action-follow span').text('Follow').addClass('NB-active'); }, - - mouseleave_follow: function() { + + mouseleave_follow: function () { this.$('.NB-profile-badge-action-follow span').text('Follow').removeClass('NB-active'); }, - - open_user_admin: function() { - NEWSBLUR.reader.open_user_admin_modal({user: this.model}); + + open_user_admin: function () { + NEWSBLUR.reader.open_user_admin_modal({ user: this.model }); } - + }); diff --git a/media/js/newsblur/views/profile_thumb.js b/media/js/newsblur/views/profile_thumb.js index 73b3d6150e..557b0cef21 100644 --- a/media/js/newsblur/views/profile_thumb.js +++ b/media/js/newsblur/views/profile_thumb.js @@ -1,18 +1,18 @@ NEWSBLUR.Views.ProfileThumb = Backbone.View.extend({ - + className: 'NB-story-share-profile', - + events: { "click .NB-user-avatar": "open_social_profile_modal" }, - - initialize: function() { + + initialize: function () { if (this.model) { this.model.profile_thumb_view = this; } }, - - render: function() { + + render: function () { var $profile = $.make('div', { className: 'NB-user-avatar', title: this.model.get('username') }, [ (this.model.get('private') && $.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + 'img/icons/circular/g_icn_lock.png', className: 'NB-user-avatar-private' })), $.make('img', { src: this.model.get('photo_url'), className: 'NB-user-avatar-image' }) @@ -22,27 +22,27 @@ NEWSBLUR.Views.ProfileThumb = Backbone.View.extend({ fade: true, offset: 3 }); - + this.$el.html($profile); return this; }, - - open_social_profile_modal: function() { + + open_social_profile_modal: function () { this.$('.NB-user-avatar').tipsy('hide'); NEWSBLUR.reader.open_social_profile_modal(this.model.id); } - + }, { - - create: function(user_id, options) { + + create: function (user_id, options) { var user = NEWSBLUR.assets.user_profiles.find(user_id); if (!user && user_id == NEWSBLUR.Globals.user_id) { user = NEWSBLUR.assets.user_profile; } if (user) { - return new NEWSBLUR.Views.ProfileThumb(_.extend({}, {model: user}, options)); + return new NEWSBLUR.Views.ProfileThumb(_.extend({}, { model: user }, options)); } } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/sidebar.js b/media/js/newsblur/views/sidebar.js index 3f3ff40666..ea7baa278a 100644 --- a/media/js/newsblur/views/sidebar.js +++ b/media/js/newsblur/views/sidebar.js @@ -1,7 +1,7 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ - + el: '.NB-sidebar', - + events: { "click .NB-feeds-header-starred .NB-feedlist-collapse-icon": "collapse_starred_stories", "click .NB-feeds-header-starred": "open_starred_stories", @@ -13,34 +13,34 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ "click .NB-feeds-header-river-global": "open_river_global_stories", "click .NB-feeds-header-river-dashboard": "show_splash_page" }, - - initialize: function() {}, - + + initialize: function () { }, + // =========== // = Actions = // =========== - - check_starred_collapsed: function(options) { + + check_starred_collapsed: function (options) { options = options || {}; var collapsed = _.contains(NEWSBLUR.Preferences.collapsed_folders, 'starred'); - + if (collapsed) { this.show_collapsed_starred(options); } - + return collapsed; }, - - show_collapsed_starred: function(options) { + + show_collapsed_starred: function (options) { options = options || {}; var $header = NEWSBLUR.reader.$s.$starred_header; var $folder = this.$('.NB-starred-folder'); - + $header.addClass('NB-folder-collapsed'); - + if (!options.skip_animation) { $header.addClass('NB-feedlist-folder-title-recently-collapsed'); - $header.one('mouseover', function() { + $header.one('mouseover', function () { $header.removeClass('NB-feedlist-folder-title-recently-collapsed'); }); } else { @@ -50,28 +50,28 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ }); } }, - - check_searches_collapsed: function(options) { + + check_searches_collapsed: function (options) { options = options || {}; var collapsed = _.contains(NEWSBLUR.Preferences.collapsed_folders, 'searches'); - + if (collapsed) { this.show_collapsed_searches(options); } - + return collapsed; }, - - show_collapsed_searches: function(options) { + + show_collapsed_searches: function (options) { options = options || {}; var $header = NEWSBLUR.reader.$s.$starred_header; var $folder = this.$('.NB-starred-folder'); - + $header.addClass('NB-folder-collapsed'); - + if (!options.skip_animation) { $header.addClass('NB-feedlist-folder-title-recently-collapsed'); - $header.one('mouseover', function() { + $header.one('mouseover', function () { $header.removeClass('NB-feedlist-folder-title-recently-collapsed'); }); } else { @@ -81,8 +81,8 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ }); } }, - - check_river_blurblog_collapsed: function(options) { + + check_river_blurblog_collapsed: function (options) { options = options || {}; var show_folder_counts = NEWSBLUR.assets.preference('folder_counts'); var collapsed = _.contains(NEWSBLUR.Preferences.collapsed_folders, 'river_blurblog'); @@ -92,25 +92,25 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ } else if (show_folder_counts) { this.show_counts(options); } - + return collapsed; }, - - show_collapsed_river_blurblog_count: function(options) { + + show_collapsed_river_blurblog_count: function (options) { options = options || {}; var $header = NEWSBLUR.reader.$s.$river_blurblogs_header; var $counts = $('.feed_counts_floater', $header); var $river = $('.NB-feedlist-collapse-icon', $header); var $folder = this.$('.NB-socialfeeds-folder'); - + $header.addClass('NB-folder-collapsed'); $counts.remove(); - + if (!options.skip_animation) { // $river.animate({'opacity': 0}, {'duration': options.skip_animation ? 0 : 100}); $header.addClass('NB-feedlist-folder-title-recently-collapsed'); - $header.one('mouseover', function() { - $river.css({'opacity': ''}); + $header.one('mouseover', function () { + $river.css({ 'opacity': '' }); $header.removeClass('NB-feedlist-folder-title-recently-collapsed'); }); } else { @@ -119,11 +119,11 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ opacity: 0 }); } - + this.show_counts(options); }, - - show_counts: function(options) { + + show_counts: function (options) { var $header = NEWSBLUR.reader.$s.$river_blurblogs_header; if (this.unread_count) { this.unread_count.destroy(); @@ -132,7 +132,7 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ collection: NEWSBLUR.assets.social_feeds }).render(); var $counts = this.unread_count.$el; - + if (this.options.feedbar) { this.$('.NB-story-title-indicator-count').html($counts.clone()); } else { @@ -140,67 +140,67 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ 'opacity': 0 })); } - $counts.animate({'opacity': 1}, {'duration': options.skip_animation ? 0 : 400}); + $counts.animate({ 'opacity': 1 }, { 'duration': options.skip_animation ? 0 : 400 }); }, - - hide_collapsed_river_blurblog_count: function() { + + hide_collapsed_river_blurblog_count: function () { var $header = NEWSBLUR.reader.$s.$river_blurblogs_header; var $counts = $('.feed_counts_floater', $header); var $river = $('.NB-feedlist-collapse-icon', $header); - - $counts.animate({'opacity': 0}, { - 'duration': 300 + + $counts.animate({ 'opacity': 0 }, { + 'duration': 300 }); - - $river.animate({'opacity': .6}, {'duration': 400}); + + $river.animate({ 'opacity': .6 }, { 'duration': 400 }); $header.removeClass('NB-feedlist-folder-title-recently-collapsed'); - $header.one('mouseover', function() { - $river.css({'opacity': ''}); + $header.one('mouseover', function () { + $river.css({ 'opacity': '' }); $header.removeClass('NB-feedlist-folder-title-recently-collapsed'); }); }, - + // ========== // = Events = // ========== - - show_splash_page: function() { + + show_splash_page: function () { NEWSBLUR.reader.show_splash_page(); }, - - open_starred_stories: function() { + + open_starred_stories: function () { return NEWSBLUR.reader.open_starred_stories(); }, - - open_read_stories: function() { + + open_read_stories: function () { return NEWSBLUR.reader.open_read_stories(); }, - - open_river_stories: function() { + + open_river_stories: function () { return NEWSBLUR.reader.open_river_stories(); }, - - open_river_infrequent_stories: function() { - return NEWSBLUR.reader.open_river_stories(null, null, {'infrequent': NEWSBLUR.assets.preference('infrequent_stories_per_month')}); + + open_river_infrequent_stories: function () { + return NEWSBLUR.reader.open_river_stories(null, null, { 'infrequent': NEWSBLUR.assets.preference('infrequent_stories_per_month') }); }, - - collapse_river_blurblog: function(e, options) { + + collapse_river_blurblog: function (e, options) { e.stopPropagation(); options = options || {}; - + var $header = NEWSBLUR.reader.$s.$river_blurblogs_header; var $folder = this.$('.NB-socialfeeds-folder'); - + // Hiding / Collapsing - if (options.force_collapse || - ($folder.length && - $folder.eq(0).is(':visible'))) { + if (options.force_collapse || + ($folder.length && + $folder.eq(0).is(':visible'))) { NEWSBLUR.assets.collapsed_folders('river_blurblog', true); $header.addClass('NB-folder-collapsed'); - $folder.animate({'opacity': 0}, { + $folder.animate({ 'opacity': 0 }, { 'queue': false, 'duration': options.force_collapse ? 0 : 200, - 'complete': _.bind(function() { + 'complete': _.bind(function () { this.show_collapsed_river_blurblog_count(); $folder.slideUp({ 'duration': 270, @@ -208,44 +208,44 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ }); }, this) }); - } + } // Showing / Expanding - else if ($folder.length && - (!$folder.eq(0).is(':visible'))) { + else if ($folder.length && + (!$folder.eq(0).is(':visible'))) { NEWSBLUR.assets.collapsed_folders('river_blurblog', false); $header.removeClass('NB-folder-collapsed'); if (!NEWSBLUR.assets.preference('folder_counts')) { this.hide_collapsed_river_blurblog_count(); } - $folder.css({'opacity': 0}).slideDown({ + $folder.css({ 'opacity': 0 }).slideDown({ 'duration': 240, 'easing': 'easeInOutCubic', - 'complete': function() { - $folder.animate({'opacity': 1}, {'queue': false, 'duration': 200}); + 'complete': function () { + $folder.animate({ 'opacity': 1 }, { 'queue': false, 'duration': 200 }); } }); } - + return false; }, - - collapse_starred_stories: function(e, options) { + + collapse_starred_stories: function (e, options) { e.stopPropagation(); options = options || {}; - + var $header = NEWSBLUR.reader.$s.$starred_header; var $folder = this.$('.NB-starred-folder'); - + // Hiding / Collapsing - if (options.force_collapse || - ($folder.length && - $folder.eq(0).is(':visible'))) { + if (options.force_collapse || + ($folder.length && + $folder.eq(0).is(':visible'))) { NEWSBLUR.assets.collapsed_folders('starred', true); $header.addClass('NB-folder-collapsed'); - $folder.animate({'opacity': 0}, { + $folder.animate({ 'opacity': 0 }, { 'queue': false, 'duration': options.force_collapse ? 0 : 200, - 'complete': _.bind(function() { + 'complete': _.bind(function () { this.show_collapsed_starred(); $folder.slideUp({ 'duration': 270, @@ -253,30 +253,30 @@ NEWSBLUR.Views.Sidebar = Backbone.View.extend({ }); }, this) }); - } + } // Showing / Expanding - else if ($folder.length && - (!$folder.eq(0).is(':visible'))) { + else if ($folder.length && + (!$folder.eq(0).is(':visible'))) { NEWSBLUR.assets.collapsed_folders('starred', false); $header.removeClass('NB-folder-collapsed'); - $folder.css({'opacity': 0}).slideDown({ + $folder.css({ 'opacity': 0 }).slideDown({ 'duration': 240, 'easing': 'easeInOutCubic', - 'complete': function() { - $folder.animate({'opacity': 1}, {'queue': false, 'duration': 200}); + 'complete': function () { + $folder.animate({ 'opacity': 1 }, { 'queue': false, 'duration': 200 }); } }); } - + return false; }, - - open_river_blurblogs_stories: function() { + + open_river_blurblogs_stories: function () { return NEWSBLUR.reader.open_river_blurblogs_stories(); }, - - open_river_global_stories: function() { - return NEWSBLUR.reader.open_river_blurblogs_stories({'global': true}); + + open_river_global_stories: function () { + return NEWSBLUR.reader.open_river_blurblogs_stories({ 'global': true }); } - + }); diff --git a/media/js/newsblur/views/sidebar_header_view.js b/media/js/newsblur/views/sidebar_header_view.js index 90b3859f46..4fd4231450 100644 --- a/media/js/newsblur/views/sidebar_header_view.js +++ b/media/js/newsblur/views/sidebar_header_view.js @@ -3,17 +3,17 @@ NEWSBLUR.Views.SidebarHeader = Backbone.View.extend({ options: { el: '.left-north' }, - + events: { - 'click .NB-feeds-header-user-interactions' : 'show_interactions_popover', - 'click .NB-feeds-header-collapse-sidebar' : 'collapse_sidebar' + 'click .NB-feeds-header-user-interactions': 'show_interactions_popover', + 'click .NB-feeds-header-collapse-sidebar': 'collapse_sidebar' }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'render', 'defer_render'); this.feed_collection = this.options.feed_collection; this.socialfeed_collection = this.options.socialfeed_collection; - + this.feed_collection.bind('reset', this.defer_render); this.feed_collection.bind('add', this.defer_render); this.feed_collection.bind('remove', this.defer_render); @@ -27,12 +27,12 @@ NEWSBLUR.Views.SidebarHeader = Backbone.View.extend({ this.socialfeed_collection.bind('change:nt', this.defer_render); this.socialfeed_collection.bind('change:ng', this.defer_render); }, - - defer_render: function() { + + defer_render: function () { _.defer(this.render); }, - - render: function() { + + render: function () { this.count(); var hide_read_feeds = NEWSBLUR.assets.preference('hide_read_feeds'); @@ -44,34 +44,34 @@ NEWSBLUR.Views.SidebarHeader = Backbone.View.extend({ <%= positive_count %>\
\ ', { - positive_count : Inflector.commas(this.unread_counts['ps']), - neutral_count : Inflector.commas(this.unread_counts['nt']), - negative_count : Inflector.commas(this.unread_counts['ng']), - hide_read_feeds : !!hide_read_feeds + positive_count: Inflector.commas(this.unread_counts['ps']), + neutral_count: Inflector.commas(this.unread_counts['nt']), + negative_count: Inflector.commas(this.unread_counts['ng']), + hide_read_feeds: !!hide_read_feeds }); - + this.$('.NB-feeds-header-dashboard').html($header); - + this.toggle_hide_read_preference(); NEWSBLUR.reader.toggle_focus_in_slider(); - + return this; }, - - toggle_hide_read_preference: function() { + + toggle_hide_read_preference: function () { var hide_read_feeds = NEWSBLUR.assets.preference('hide_read_feeds'); if (NEWSBLUR.reader.flags['feed_list_showing_starred']) hide_read_feeds = true; this.$('.NB-feeds-header-sites').toggleClass('NB-feedlist-hide-read-feeds', !!hide_read_feeds); $("body").toggleClass("NB-feedlist-hide-read-feeds", !!hide_read_feeds); }, - - count: function() { + + count: function () { this.unread_counts = NEWSBLUR.assets.folders.unread_counts(); this.unread_counts = NEWSBLUR.assets.social_feeds.unread_counts(this.unread_counts); - + if (!NEWSBLUR.Globals.is_authenticated) return; if (!NEWSBLUR.assets.preference('title_counts')) return; - + var counts = []; var unread_view = _.isNumber(this.options.unread_view) && this.options.unread_view || NEWSBLUR.assets.preference('unread_view'); if (unread_view <= -1) { @@ -92,32 +92,32 @@ NEWSBLUR.Views.SidebarHeader = Backbone.View.extend({ } document.title = title; }, - - count_feeds: function() { - return this.feed_collection.select(function(f) { + + count_feeds: function () { + return this.feed_collection.select(function (f) { return f.get('active'); }).length; }, - - update_interactions_count: function(interactions_count) { + + update_interactions_count: function (interactions_count) { var $badge = this.$(".NB-feeds-header-user-interactions-badge"); - + if (!interactions_count) { $badge.addClass('NB-hidden').text(''); } else { $badge.removeClass('NB-hidden').text('' + interactions_count); } }, - + // ========== // = Events = // ========== - - show_interactions_popover: function() { + + show_interactions_popover: function () { NEWSBLUR.InteractionsPopover.create({}); }, - - collapse_sidebar: function() { + + collapse_sidebar: function () { if (!NEWSBLUR.reader.flags['splash_page_frontmost']) { NEWSBLUR.reader.close_sidebar(); } diff --git a/media/js/newsblur/views/story_comment_reply_view.js b/media/js/newsblur/views/story_comment_reply_view.js index bd5d3864fb..91375749ef 100644 --- a/media/js/newsblur/views/story_comment_reply_view.js +++ b/media/js/newsblur/views/story_comment_reply_view.js @@ -1,25 +1,25 @@ NEWSBLUR.Views.StoryCommentReply = Backbone.View.extend({ - + className: "NB-story-comment-reply", - + events: { "click .NB-user-avatar": "open_social_profile_modal", "click .NB-story-comment-username": "open_social_profile_modal", "click .NB-story-comment-reply-edit-button": "edit_reply" }, - - render: function() { + + render: function () { var $reply = $(this.template({ reply: this.model, user: NEWSBLUR.assets.get_user(this.model.get('user_id')), current_user_id: NEWSBLUR.Globals.user_id })); - + this.$el.html($reply); - + return this; }, - + template: _.template('\ " />\
<%= user.get("username") %>
\ @@ -31,19 +31,19 @@ NEWSBLUR.Views.StoryCommentReply = Backbone.View.extend({ <% } %>\
<%= reply.get("comments") %>
\ '), - + // ========== // = Events = // ========== - - open_social_profile_modal: function(e) { + + open_social_profile_modal: function (e) { e.stopPropagation(); NEWSBLUR.reader.open_social_profile_modal(this.model.get('user_id')); }, - - edit_reply: function() { - this.options.comment.open_reply({is_editing: true, reply: this.model, $reply: this.$el}); + + edit_reply: function () { + this.options.comment.open_reply({ is_editing: true, reply: this.model, $reply: this.$el }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/story_comment_view.js b/media/js/newsblur/views/story_comment_view.js index ee0484273f..fdd691714a 100644 --- a/media/js/newsblur/views/story_comment_view.js +++ b/media/js/newsblur/views/story_comment_view.js @@ -1,7 +1,7 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ - + className: 'NB-story-comment', - + events: { "click .NB-user-avatar": "open_social_profile_modal", "click .NB-story-comment-username": "open_social_profile_modal", @@ -11,8 +11,8 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ "click .NB-story-comment-reply .NB-modal-submit-green": "save_social_comment_reply", "click .NB-story-comment-reply .NB-modal-submit-delete": "delete_social_comment_reply" }, - - initialize: function(options) { + + initialize: function (options) { this.story = options.story; if (!this.options.on_social_page) { this.user = NEWSBLUR.assets.user_profiles.find(this.model.get('user_id')); @@ -20,8 +20,8 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ this.model.bind('change:liking_users', this.call_out_like, this); } }, - - render: function() { + + render: function () { var comments = this.model.get('comments').replace(/\n+/g, '

'); var reshare_class = this.model.get('source_user_id') ? 'NB-story-comment-reshare' : ''; var has_likes = _.any(this.model.get('liking_users')); @@ -58,42 +58,42 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ $.make('div', { className: 'NB-story-comment-content' }, comments), this.make_story_share_comment_replies() ]); - + this.$el.html($comment); return this; }, - - make_story_share_comment_replies: function() { + + make_story_share_comment_replies: function () { if (!this.model.replies || !this.model.replies.length) return; - + var user_id = NEWSBLUR.Globals.user_id; - var $replies = this.model.replies.map(_.bind(function(reply) { + var $replies = this.model.replies.map(_.bind(function (reply) { if (!NEWSBLUR.assets.get_user(reply.get('user_id'))) return; - return new NEWSBLUR.Views.StoryCommentReply({model: reply, comment: this}).render().el; + return new NEWSBLUR.Views.StoryCommentReply({ model: reply, comment: this }).render().el; }, this)); $replies = $.make('div', { className: 'NB-story-comment-replies' }, $replies); return $replies; }, - - render_liking_users: function() { + + render_liking_users: function () { var $users = $.make('div', { className: 'NB-story-comment-likes-users' }); - _.each(this.model.get('liking_users'), function(user_id) { + _.each(this.model.get('liking_users'), function (user_id) { if (!NEWSBLUR.assets.get_user(user_id)) return; var $thumb = NEWSBLUR.Views.ProfileThumb.create(user_id).render().el; $users.append($thumb); }); - + return $users; }, - - call_out_like: function() { + + call_out_like: function () { var $like = this.$('.NB-story-comment-like'); var liked = _.contains(this.model.get('liking_users'), NEWSBLUR.Globals.user_id); - - $like.attr({'title': liked ? 'Favorited!' : 'Unfavorited'}); + + $like.attr({ 'title': liked ? 'Favorited!' : 'Unfavorited' }); $like.tipsy({ gravity: 'sw', fade: true, @@ -101,7 +101,7 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ offsetOpposite: -1 }); var tipsy = $like.data('tipsy'); - _.defer(function() { + _.defer(function () { if (!tipsy) return; tipsy.enable(); tipsy.show(); @@ -112,7 +112,7 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ }, { 'duration': 850, 'queue': false, - 'complete': function() { + 'complete': function () { if (tipsy && tipsy.enabled) { tipsy.hide(); tipsy.disable(); @@ -120,27 +120,27 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ } }); }, - + // ========== // = Events = // ========== - - open_social_profile_modal: function() { + + open_social_profile_modal: function () { NEWSBLUR.reader.open_social_profile_modal(this.model.get("user_id")); }, - - toggle_feed_story_save_dialog: function() { + + toggle_feed_story_save_dialog: function () { this.story.story_save_view.toggle_feed_story_save_dialog(); }, - - toggle_feed_story_share_dialog: function() { + + toggle_feed_story_share_dialog: function () { this.story.story_share_view.toggle_feed_story_share_dialog(); }, - - open_reply: function(options) { + + open_reply: function (options) { options = options || {}; var current_user = NEWSBLUR.assets.user_profile; - + if (this.user.get('protected') && this.options.public_comment) { var $error = this.$('.NB-story-comment-error'); $error.text("You must be following " + this.user.get('username') + " to reply"); @@ -156,7 +156,7 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ (options.is_editing && $.make('div', { className: 'NB-modal-submit-button NB-modal-submit-grey NB-modal-submit-delete' }, 'Delete')) ]); this.remove_social_comment_reply_form(); - + if (options.is_editing && options.$reply) { $form.data('reply_id', options.reply.get("reply_id")); options.$reply.hide().addClass('NB-story-comment-reply-hidden'); @@ -164,37 +164,37 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ } else { this.$el.append($form); } - - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'return', + + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'return', _.bind(this.save_social_comment_reply, this)); - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'ctrl+return', + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'ctrl+return', _.bind(this.save_social_comment_reply, this)); - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'meta+return', + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'meta+return', _.bind(this.save_social_comment_reply, this)); - $('.NB-story-comment-reply-comments', $form).bind('keydown', 'esc', _.bind(function(e) { + $('.NB-story-comment-reply-comments', $form).bind('keydown', 'esc', _.bind(function (e) { e.preventDefault(); this.remove_social_comment_reply_form(); }, this)); $('input', $form).focus(); - + if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } }, - - remove_social_comment_reply_form: function() { + + remove_social_comment_reply_form: function () { this.$('.NB-story-comment-reply-form').remove(); this.$('.NB-story-comment-reply-hidden').show(); }, - - save_social_comment_reply: function() { + + save_social_comment_reply: function () { var $form = this.$('.NB-story-comment-reply-form'); var $submit = $(".NB-modal-submit-green", $form); var $delete_button = $(".NB-modal-submit-delete", $form); var comment_user_id = this.model.get('user_id'); var comment_reply = $('.NB-story-comment-reply-comments', $form).val(); var reply_id = $form.data('reply_id'); - + if (!comment_reply || comment_reply.length <= 1) { this.remove_social_comment_reply_form(); if (NEWSBLUR.app.story_list) { @@ -202,92 +202,92 @@ NEWSBLUR.Views.StoryComment = Backbone.View.extend({ } return; } - + if ($submit.hasClass('NB-disabled')) { return; } - + $delete_button.hide(); $submit.addClass('NB-disabled').text('Posting...'); - NEWSBLUR.assets.save_comment_reply(this.options.story.id, this.options.story.get('story_feed_id'), - comment_user_id, comment_reply, - reply_id, - _.bind(function(data) { - if (this.options.on_social_page) { - this.options.story_comments_view.replace_comment(this.model.get('user_id'), data); - } else { - this.model.set(data.comment); - this.render(); - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - } - }, this), _.bind(function(data) { - var message = data && data.message || "Sorry, this reply could not be posted. Probably Adblock."; - if (!NEWSBLUR.Globals.is_authenticated) { - message = "You need to be logged in to reply to a comment."; - } - var $error = $.make('div', { className: 'NB-error' }, message); - $submit.removeClass('NB-disabled').text('Post'); - $form.find('.NB-error').remove(); - $form.append($error); - if (NEWSBLUR.app.story_list) { - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - } - }, this)); + NEWSBLUR.assets.save_comment_reply(this.options.story.id, this.options.story.get('story_feed_id'), + comment_user_id, comment_reply, + reply_id, + _.bind(function (data) { + if (this.options.on_social_page) { + this.options.story_comments_view.replace_comment(this.model.get('user_id'), data); + } else { + this.model.set(data.comment); + this.render(); + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + } + }, this), _.bind(function (data) { + var message = data && data.message || "Sorry, this reply could not be posted. Probably Adblock."; + if (!NEWSBLUR.Globals.is_authenticated) { + message = "You need to be logged in to reply to a comment."; + } + var $error = $.make('div', { className: 'NB-error' }, message); + $submit.removeClass('NB-disabled').text('Post'); + $form.find('.NB-error').remove(); + $form.append($error); + if (NEWSBLUR.app.story_list) { + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + } + }, this)); }, - - delete_social_comment_reply: function() { + + delete_social_comment_reply: function () { var $form = this.$('.NB-story-comment-reply-form'); var $submit = $(".NB-modal-submit-green", $form); var $delete_button = $(".NB-modal-submit-delete", $form); var comment_user_id = this.model.get('user_id'); var reply_id = $form.data('reply_id'); - + if ($submit.hasClass('NB-disabled') || $delete_button.hasClass('NB-disabled')) { return; } - + $submit.addClass('NB-disabled'); $delete_button.addClass('NB-disabled').text('Deleting...'); NEWSBLUR.assets.delete_comment_reply(this.options.story.id, - this.options.story.get('story_feed_id'), - comment_user_id, reply_id, - _.bind(function(data) { - if (this.options.on_social_page) { - this.options.story_comments_view.replace_comment(this.model.get('user_id'), data); - } else { - this.model.set(data.comment); - this.render(); - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - } - }, this), _.bind(function(data) { - var message = data && data.message || "Sorry, this reply could not be deleted."; - var $error = $.make('div', { className: 'NB-error' }, message); - $submit.removeClass('NB-disabled').text('Post'); - $delete_button.removeClass('NB-disabled').text('Delete'); - $form.find('.NB-error').remove(); - $form.append($error); - if (NEWSBLUR.app.story_list) { - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - } - }, this)); + this.options.story.get('story_feed_id'), + comment_user_id, reply_id, + _.bind(function (data) { + if (this.options.on_social_page) { + this.options.story_comments_view.replace_comment(this.model.get('user_id'), data); + } else { + this.model.set(data.comment); + this.render(); + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + } + }, this), _.bind(function (data) { + var message = data && data.message || "Sorry, this reply could not be deleted."; + var $error = $.make('div', { className: 'NB-error' }, message); + $submit.removeClass('NB-disabled').text('Post'); + $delete_button.removeClass('NB-disabled').text('Delete'); + $form.find('.NB-error').remove(); + $form.append($error); + if (NEWSBLUR.app.story_list) { + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + } + }, this)); }, - - like_comment: function() { + + like_comment: function () { var liking_user_ids = this.model.get('liking_users') || []; var comment_user_id = this.model.get('user_id'); var liked = _.contains(liking_user_ids, NEWSBLUR.Globals.user_id); - + if (!liked) { this.model.set('liking_users', _.union(liking_user_ids, NEWSBLUR.Globals.user_id)); - NEWSBLUR.assets.like_comment(this.options.story.id, - this.options.story.get('story_feed_id'), - comment_user_id); + NEWSBLUR.assets.like_comment(this.options.story.id, + this.options.story.get('story_feed_id'), + comment_user_id); } else { this.model.set('liking_users', _.without(liking_user_ids, NEWSBLUR.Globals.user_id)); - NEWSBLUR.assets.remove_like_comment(this.options.story.id, - this.options.story.get('story_feed_id'), - comment_user_id); + NEWSBLUR.assets.remove_like_comment(this.options.story.id, + this.options.story.get('story_feed_id'), + comment_user_id); } } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/story_comments_view.js b/media/js/newsblur/views/story_comments_view.js index 6a1cc78f3e..d607baa015 100644 --- a/media/js/newsblur/views/story_comments_view.js +++ b/media/js/newsblur/views/story_comments_view.js @@ -1,13 +1,13 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ - + className: 'NB-feed-story-comments', - + events: { "click .NB-story-comments-public-teaser": "load_public_story_comments" }, - - render: function() { + + render: function () { var self = this; var $el = this.$el; if (this.model.get('share_count')) { @@ -18,45 +18,45 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ this.render_comments_friends(); this.render_shares_friends(); this.render_comments_public(); - this.$el.toggleClass('NB-hidden', (!this.model.get('comment_count') && - !this.model.get('share_count_friends'))); + this.$el.toggleClass('NB-hidden', (!this.model.get('comment_count') && + !this.model.get('share_count_friends'))); } return this; }, - - destroy: function() { + + destroy: function () { this.remove(); }, - - render_teaser: function() { + + render_teaser: function () { if (!this.model.get('share_count')) return; - + var $comments_friends = this.$('.NB-story-share-profiles-comments-friends'); var $comments_public = this.$('.NB-story-share-profiles-comments-public'); - _.each(this.model.get('commented_by_friends'), function(user_id) { + _.each(this.model.get('commented_by_friends'), function (user_id) { var $thumb = NEWSBLUR.Views.ProfileThumb.create(user_id).render().el; $comments_friends.append($thumb); }); - _.each(this.model.get('commented_by_public'), function(user_id) { + _.each(this.model.get('commented_by_public'), function (user_id) { var $thumb = NEWSBLUR.Views.ProfileThumb.create(user_id).render().el; $comments_public.append($thumb); }); if (!this.model.friend_comments.length && !this.model.public_comments.length && !this.model.friend_shares.length) { this.$el.hide(); } - + var $shares_friends = this.$('.NB-story-share-profiles-shares-friends'); var $shares_public = this.$('.NB-story-share-profiles-shares-public'); var comment_user_ids = this.model.get('comment_user_ids'); - _.each(this.model.get('shared_by_friends'), function(user_id) { + _.each(this.model.get('shared_by_friends'), function (user_id) { if (_.contains(comment_user_ids, user_id)) return; var profile_thumb = NEWSBLUR.Views.ProfileThumb.create(user_id); if (!profile_thumb) return; var $thumb = profile_thumb.render().el; $shares_friends.append($thumb); }); - _.each(this.model.get('shared_by_public'), function(user_id) { + _.each(this.model.get('shared_by_public'), function (user_id) { if (_.contains(comment_user_ids, user_id)) return; var profile_thumb = NEWSBLUR.Views.ProfileThumb.create(user_id); if (!profile_thumb) return; @@ -64,23 +64,23 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ $shares_public.append($thumb); }); }, - - render_comments_friends: function() { + + render_comments_friends: function () { if (!this.model.get('comment_count_friends') || !this.model.get('comment_count')) return; - - var $header = $.make('div', { - className: 'NB-story-comments-public-header-wrapper' - }, $.make('div', { - className: 'NB-story-comments-public-header NB-module-header' + + var $header = $.make('div', { + className: 'NB-story-comments-public-header-wrapper' + }, $.make('div', { + className: 'NB-story-comments-public-header NB-module-header' }, [ Inflector.pluralize(' comment', this.model.get('comment_count_friends'), true) ])); - + this.$el.append($header); - - this.model.friend_comments.each(_.bind(function(comment) { + + this.model.friend_comments.each(_.bind(function (comment) { var $comment = new NEWSBLUR.Views.StoryComment({ - model: comment, + model: comment, story: this.model, friend_comment: true }).render().el; @@ -88,21 +88,21 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ }, this)); }, - render_shares_friends: function() { + render_shares_friends: function () { var shares_without_comments = this.model.get('shared_by_friends'); if (shares_without_comments.length <= 0) return; - - var $header = $.make('div', { - className: 'NB-story-comments-public-header-wrapper' - }, $.make('div', { - className: 'NB-story-comments-public-header NB-module-header' + + var $header = $.make('div', { + className: 'NB-story-comments-public-header-wrapper' + }, $.make('div', { + className: 'NB-story-comments-public-header NB-module-header' }, [ Inflector.pluralize(' share', shares_without_comments.length, true) ])); - + this.$el.append($header); - - this.model.friend_shares.each(_.bind(function(comment) { + + this.model.friend_shares.each(_.bind(function (comment) { var $comment = new NEWSBLUR.Views.StoryComment({ model: comment, story: this.model, @@ -111,10 +111,10 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ this.$el.append($comment); }, this)); }, - - render_comments_public: function() { + + render_comments_public: function () { if (!this.model.get('comment_count_public') || !this.model.get('comment_count')) return; - + if (NEWSBLUR.assets.preference('hide_public_comments')) { var $public_teaser = $.make('div', { className: 'NB-story-comments-public-teaser-wrapper' }, [ $.make('div', { className: 'NB-story-comments-public-teaser NB-module-header' }, [ @@ -129,17 +129,17 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ ]); this.$el.append($public_teaser); } else { - var $header = $.make('div', { - className: 'NB-story-comments-public-header-wrapper' - }, $.make('div', { - className: 'NB-story-comments-public-header NB-module-header' + var $header = $.make('div', { + className: 'NB-story-comments-public-header-wrapper' + }, $.make('div', { + className: 'NB-story-comments-public-header NB-module-header' }, Inflector.pluralize(' public comment', this.model.get('comment_count_public'), true))); - + this.$el.append($header); - - this.model.public_comments.each(_.bind(function(comment) { + + this.model.public_comments.each(_.bind(function (comment) { var $comment = new NEWSBLUR.Views.StoryComment({ - model: comment, + model: comment, story: this.model, public_comment: true }).render().el; @@ -147,7 +147,7 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({ }, this)); } }, - + template: _.template('\
\
\ @@ -180,39 +180,39 @@ NEWSBLUR.Views.StoryCommentsView = Backbone.View.extend({
\
\ '), - + // ========== // = Events = // ========== - - load_public_story_comments: function() { + + load_public_story_comments: function () { var following_user_ids = NEWSBLUR.assets.user_profile.get('following_user_ids'); this.$(".NB-story-comments-expand-icon").addClass("NB-loading"); - - NEWSBLUR.assets.load_public_story_comments(this.model.id, this.model.get('story_feed_id'), _.bind(function(comments) { + + NEWSBLUR.assets.load_public_story_comments(this.model.id, this.model.get('story_feed_id'), _.bind(function (comments) { this.$(".NB-story-comments-expand-icon").addClass("NB-loading"); var $comments = $.make('div', { className: 'NB-story-comments-public' }); - var public_comments = comments.select(_.bind(function(comment) { + var public_comments = comments.select(_.bind(function (comment) { return !_.contains(following_user_ids, comment.get('user_id')); }, this)); - var $header = $.make('div', { - className: 'NB-story-comments-public-header-wrapper' - }, $.make('div', { - className: 'NB-story-comments-public-header NB-module-header' + var $header = $.make('div', { + className: 'NB-story-comments-public-header-wrapper' + }, $.make('div', { + className: 'NB-story-comments-public-header NB-module-header' }, Inflector.pluralize(' public comment', public_comments.length, true))).prependTo($comments); - _.each(public_comments, _.bind(function(comment) { + _.each(public_comments, _.bind(function (comment) { var $comment = new NEWSBLUR.Views.StoryComment({ - model: comment, + model: comment, story: this.model, public_comment: true }).render().el; $comments.append($comment); }, this)); - + this.$('.NB-story-comments-public-teaser-wrapper').replaceWith($comments); NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); }, this)); } - + }); diff --git a/media/js/newsblur/views/story_detail_view.js b/media/js/newsblur/views/story_detail_view.js index 238aba7b35..54fc735a32 100644 --- a/media/js/newsblur/views/story_detail_view.js +++ b/media/js/newsblur/views/story_detail_view.js @@ -1,38 +1,38 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ - + tagName: 'li', - + className: 'NB-feed-story', FUDGE_CONTENT_HEIGHT_OVERAGE: 260, - + STORY_CONTENT_MAX_HEIGHT: 460, // ALSO CHANGE IN reader.css: .NB-story-content-wrapper-height-truncated - + events: { - "click" : "mark_read", - "click .NB-feed-story-content a" : "click_link_in_story", + "click": "mark_read", + "click .NB-feed-story-content a": "click_link_in_story", "click .NB-feed-story-share-container a": "click_link_in_story", - "click .NB-feed-story-comments a" : "click_link_in_story", - "click .NB-feed-story-title" : "click_link_in_story", - "mouseenter .NB-feed-story-manage-icon" : "mouseenter_manage_icon", - "mouseleave .NB-feed-story-manage-icon" : "mouseleave_manage_icon", - "contextmenu .NB-feed-story-header" : "show_manage_menu_rightclick", - "mouseup .NB-story-content-wrapper" : "mouseup_check_selection", - "click .NB-feed-story-manage-icon" : "show_manage_menu", - "click .NB-feed-story-show-changes" : "show_story_changes", - "click .NB-feed-story-header-title" : "open_feed", - "click .NB-feed-story-tag" : "save_classifier", - "click .NB-feed-story-author" : "save_classifier", - "click .NB-feed-story-train" : "open_story_trainer", - "click .NB-feed-story-email" : "open_email", - "click .NB-feed-story-save" : "toggle_starred", - "click .NB-story-comments-label" : "scroll_to_comments", - "click .NB-story-content-expander" : "expand_story", - "click .NB-highlight-selection" : "highlight_selected_text", - "click .NB-unhighlight-selection" : "unhighlight_selected_text" + "click .NB-feed-story-comments a": "click_link_in_story", + "click .NB-feed-story-title": "click_link_in_story", + "mouseenter .NB-feed-story-manage-icon": "mouseenter_manage_icon", + "mouseleave .NB-feed-story-manage-icon": "mouseleave_manage_icon", + "contextmenu .NB-feed-story-header": "show_manage_menu_rightclick", + "mouseup .NB-story-content-wrapper": "mouseup_check_selection", + "click .NB-feed-story-manage-icon": "show_manage_menu", + "click .NB-feed-story-show-changes": "show_story_changes", + "click .NB-feed-story-header-title": "open_feed", + "click .NB-feed-story-tag": "save_classifier", + "click .NB-feed-story-author": "save_classifier", + "click .NB-feed-story-train": "open_story_trainer", + "click .NB-feed-story-email": "open_email", + "click .NB-feed-story-save": "toggle_starred", + "click .NB-story-comments-label": "scroll_to_comments", + "click .NB-story-content-expander": "expand_story", + "click .NB-highlight-selection": "highlight_selected_text", + "click .NB-unhighlight-selection": "unhighlight_selected_text" }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'mouseleave', 'mouseenter', 'mouseup_check_selection', 'highlight_selected_text', 'unhighlight_selected_text'); this.model.bind('change', this.toggle_classes, this); this.model.bind('change:read_status', this.toggle_read_status, this); @@ -46,12 +46,12 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ if (this.collection) { this.collection.bind('render:intelligence', this.render_intelligence, this); } - + // Binding directly instead of using event delegation. Need for speed. // this.$el.bind('mouseenter', this.mouseenter); // this.$el.bind('mouseleave', this.mouseleave); - - if (!this.options.feed_floater && + + if (!this.options.feed_floater && !this.options.text_view && !this.options.inline_story_title) { this.model.story_view = this; @@ -63,21 +63,21 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ this.model.latest_story_detail_view = this; } }, - + // ============= // = Rendering = // ============= - - render: function() { + + render: function () { var params = this.get_render_params(); params['story_header'] = this.story_header_template(params); this.sideoptions_view = new NEWSBLUR.Views.StorySideoptionsView({ - model: this.model, + model: this.model, el: this.el }); this.save_view = this.sideoptions_view.save_view; this.share_view = this.sideoptions_view.share_view; - + params['story_save_view'] = this.sideoptions_view.save_view.render(); params['story_share_view'] = this.sideoptions_view.share_view.template({ story: this.model, @@ -98,28 +98,28 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ this.attach_handlers(); // if (!this.model.get('image_urls') || (this.model.get('image_urls') && this.model.get('image_urls').length == 0)) { // } - + return this; }, - - setElement: function($el) { + + setElement: function ($el) { Backbone.View.prototype.setElement.call(this, $el); if (this.share_view) this.share_view.setElement($el); }, - - render_starred_tags: function() { + + render_starred_tags: function () { if (this.model.get('starred')) { this.save_view.toggle_feed_story_save_dialog(); } }, - - resize_starred_tags: function() { + + resize_starred_tags: function () { if (this.model.get('starred')) { - this.save_view.reset_height({immediate: true}); + this.save_view.reset_height({ immediate: true }); } }, - attach_handlers: function() { + attach_handlers: function () { this.watch_images_for_story_height(); this.attach_syntax_highlighter_handler(); this.attach_fitvid_handler(); @@ -128,14 +128,14 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ this.watch_images_load(); this.attach_custom_handler(); }, - + attach_custom_handler: function () { // Use this to create your own story_content handler. // Add this to your Manage > Account > Custom CSS: // // NEWSBLUR.Views.StoryDetailView.prototype.attach_custom_handler = () => { console.log(['Story selected', NEWSBLUR.reader.active_story.get('story_title'), NEWSBLUR.reader.active_story.get('story_content').length + " bytes"]); } }, - + watch_images_load: function () { var pane_width; if (this.options.inline_story_title) { @@ -150,11 +150,11 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ pane_width = pane_width - (28 + 2); // 28px to compensate for both margins var has_tables = this.$("table").length; - this.$el.imagesLoaded(_.bind(function() { + this.$el.imagesLoaded(_.bind(function () { var largest = 0; var $largest; // console.log(["Images loaded", this.model.get('story_title').substr(0, 30), this.$("img")]); - this.$("img").each(function() { + this.$("img").each(function () { // console.log(["Largest?", this.width, this.naturalWidth, this.height, this.naturalHeight, largest, pane_width, this.src]); if (this.width > 60 && this.width > largest) { largest = this.width; @@ -199,34 +199,34 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ } }, this)); }, - - render_header: function(model, value, options) { + + render_header: function (model, value, options) { var params = this.get_render_params(); this.$('.NB-feed-story-header-feed').remove(); this.$('.NB-feed-story-header').replaceWith($(this.story_header_template(params))); this.generate_gradients(); }, - - get_render_params: function() { + + get_render_params: function () { this.feed = NEWSBLUR.assets.get_feed(this.model.get('story_feed_id')); this.classifiers = NEWSBLUR.assets.classifiers[this.model.get('story_feed_id')]; - var show_feed_title = NEWSBLUR.reader.flags.river_view || - NEWSBLUR.reader.flags.social_view || - this.options.show_feed_title; + var show_feed_title = NEWSBLUR.reader.flags.river_view || + NEWSBLUR.reader.flags.social_view || + this.options.show_feed_title; return { - story : this.model, - feed : show_feed_title && this.feed, - tag : _.first(this.model.get("story_tags")), - title : this.make_story_title(), - authors_score : this.classifiers && - this.classifiers.authors[this.model.get('story_authors')], - tags_score : this.classifiers && this.classifiers.tags, - options : this.options, - truncatable : this.is_truncatable(), + story: this.model, + feed: show_feed_title && this.feed, + tag: _.first(this.model.get("story_tags")), + title: this.make_story_title(), + authors_score: this.classifiers && + this.classifiers.authors[this.model.get('story_authors')], + tags_score: this.classifiers && this.classifiers.tags, + options: this.options, + truncatable: this.is_truncatable(), inline_story_title: this.options.inline_story_title }; }, - + story_header_template: _.template('\
\ <% if (feed) { %>\ @@ -284,7 +284,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
\
\ '), - + template: _.template('\ <%= story_header %>\
\ @@ -330,52 +330,52 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
\ <% } %>\ '), - - generate_gradients: function() { + + generate_gradients: function () { var $header = this.$('.NB-feed-story-header-feed'); if (!this.feed) return; - + var favicon_color = this.feed.get('favicon_color'); if (favicon_color) { - $header.css('backgroundColor', '#' + favicon_color); + $header.css('backgroundColor', '#' + favicon_color); $header.css('background-image', 'none'); } $header.css('background-image', NEWSBLUR.utils.generate_gradient(this.feed, 'webkit')); $header.css('background-image', NEWSBLUR.utils.generate_gradient(this.feed, 'moz')); // $header.css('borderTop', NEWSBLUR.utils.generate_gradient(this.feed, 'border')); // $header.css('borderBottom', NEWSBLUR.utils.generate_gradient(this.feed, 'border')); - $header.css('textShadow', NEWSBLUR.utils.generate_shadow(this.feed)); + $header.css('textShadow', NEWSBLUR.utils.generate_shadow(this.feed)); }, - - is_truncatable: function() { - return NEWSBLUR.assets.preference("truncate_story") == 'all' || - (NEWSBLUR.assets.preference("truncate_story") == 'social' && + + is_truncatable: function () { + return NEWSBLUR.assets.preference("truncate_story") == 'all' || + (NEWSBLUR.assets.preference("truncate_story") == 'social' && NEWSBLUR.reader.flags['social_view']); }, - - make_story_title: function(story) { + + make_story_title: function (story) { story = story || this.model; var title = story.get('story_title'); var classifiers = NEWSBLUR.assets.classifiers[story.get('story_feed_id')]; var feed_titles = classifiers && classifiers.titles || []; - - _.each(feed_titles, function(score, title_classifier) { + + _.each(feed_titles, function (score, title_classifier) { if (!title_classifier || title.toLowerCase().indexOf(title_classifier.toLowerCase()) != -1) { var pos = title.toLowerCase().indexOf(title_classifier.toLowerCase()); - title = title.substr(0, pos) + ''+title.substr(pos, title_classifier.length)+'' + title.substr(pos + title_classifier.length); + title = title.substr(0, pos) + '' + title.substr(pos, title_classifier.length) + '' + title.substr(pos + title_classifier.length); } }); - + return title; }, - - render_comments: function() { + + render_comments: function () { var $original_comments = this.$('.NB-feed-story-comments-container,.NB-feed-story-comments'); var $original_shares = this.$('.NB-feed-story-shares-container,.NB-feed-story-shares'); - + if (this.model.get("comment_count") || this.model.get("share_count")) { - var comments_view = new NEWSBLUR.Views.StoryCommentsView({model: this.model}); + var comments_view = new NEWSBLUR.Views.StoryCommentsView({ model: this.model }); this.comments_view = comments_view.render(); var $comments = this.comments_view.el; $original_comments.html($comments); @@ -386,15 +386,15 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ $original_shares.replaceWith($.make('div', { className: 'NB-feed-story-shares-container' })); } }, - - render_story_content: function() { + + render_story_content: function () { this.$(".NB-feed-story-show-changes-text").text((this.model.get('showing_diff') ? "Hide" : "Show") + " story changes"); this.$(".NB-feed-story-content").html(this.model.story_content()); - + this.attach_handlers(); }, - - destroy: function() { + + destroy: function () { // console.log(["destroy story detail", this.model.get('story_title')]); clearTimeout(this.truncate_delay_function); this.images_to_load = null; @@ -405,12 +405,12 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ delete this.model.inline_story_detail_view; this.remove(); }, - - render_intelligence: function(options) { + + render_intelligence: function (options) { options = options || {}; var score = this.model.score(); var unread_view = NEWSBLUR.reader.get_unread_view_score(); - + if (score >= unread_view) { this.$el.removeClass('NB-hidden'); this.model.set('visible', true); @@ -419,37 +419,37 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ this.model.set('visible', false); } }, - + // ============ // = Bindings = // ============ - - toggle_classes: function() { + + toggle_classes: function () { var changes = this.model.changedAttributes(); - var onlySelected = changes && _.all(_.keys(changes), function(change) { + var onlySelected = changes && _.all(_.keys(changes), function (change) { return _.contains(['selected', 'read', 'intelligence', 'visible'], change); }); - + if (onlySelected) return; - + if (this.model.changedAttributes()) { // NEWSBLUR.log(["Story changed", this.model.changedAttributes(), this.model.previousAttributes()]); } - + this.setup_classes(); }, - - setup_classes: function() { + + setup_classes: function () { var story = this.model; var unread_view = NEWSBLUR.reader.get_unread_view_score(); - + this.$el.toggleClass('NB-river-story', NEWSBLUR.reader.flags.river_view || - NEWSBLUR.reader.flags.social_view); + NEWSBLUR.reader.flags.social_view); this.$el.toggleClass('NB-story-starred', !!story.get('starred')); this.$el.toggleClass('NB-story-shared', !!story.get('shared')); this.toggle_intelligence(); this.render_intelligence(); - + if (NEWSBLUR.assets.preference('show_tooltips')) { this.$('.NB-story-sentiment').tipsy({ delayIn: 375, @@ -460,22 +460,22 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ }); } }, - - toggle_read_status: function() { + + toggle_read_status: function () { this.$el.toggleClass('read', !!this.model.get('read_status')); }, - toggle_intelligence: function() { + toggle_intelligence: function () { var score = this.model.score(); this.$el.removeClass('NB-story-negative NB-story-neutral NB-story-postiive') - .addClass('NB-story-'+this.model.score_name(score)); + .addClass('NB-story-' + this.model.score_name(score)); }, - - toggle_selected: function(model, selected, options) { + + toggle_selected: function (model, selected, options) { options = options || {}; this.$el.toggleClass('NB-selected', !!this.model.get('selected')); NEWSBLUR.app.taskbar_info.hide_stories_error(); - + if (selected && options.scroll_to_comments) { NEWSBLUR.app.story_list.scroll_to_selected_story(model, { scroll_offset: -50, @@ -485,29 +485,29 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ NEWSBLUR.app.story_list.scroll_to_selected_story(model, { 'scroll_to_top': true }); - } else if (selected && + } else if (selected && !options.selected_by_scrolling && (NEWSBLUR.reader.story_view == 'feed' || - (NEWSBLUR.reader.story_view == 'page' && - NEWSBLUR.reader.flags['page_view_showing_feed_view']))) { + (NEWSBLUR.reader.story_view == 'page' && + NEWSBLUR.reader.flags['page_view_showing_feed_view']))) { // NEWSBLUR.app.story_list.show_stories_preference_in_feed_view(); NEWSBLUR.app.story_list.scroll_to_selected_story(model, options); } - + if (NEWSBLUR.reader.flags['feed_view_showing_story_view'] || NEWSBLUR.reader.flags['temporary_story_view']) { NEWSBLUR.reader.switch_to_correct_view(); } }, - + // ============ // = Expander = // ============ - - truncate_story_height: function() { + + truncate_story_height: function () { if (this._truncated) return; if (!this.is_truncatable()) return; - + if (NEWSBLUR.assets.preference('feed_view_single_story')) return; // console.log(["Checking truncate", this.$el, this.images_to_load, this.truncate_delay / 1000 + " sec delay"]); @@ -517,8 +517,8 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ var $content = this.$(".NB-feed-story-content"); var max_height = parseInt($wrapper.css('maxHeight'), 10) || this.STORY_CONTENT_MAX_HEIGHT; var content_height = $content.outerHeight(true); - - if (content_height > max_height && + + if (content_height > max_height && content_height < max_height + this.FUDGE_CONTENT_HEIGHT_OVERAGE) { // console.log(["Height over but within fudge", this.model.get('story_title').substr(0, 30), content_height, max_height]); $wrapper.addClass('NB-story-content-wrapper-height-fudged'); @@ -528,35 +528,35 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ $wrapper.removeClass('NB-story-content-wrapper-height-fudged'); $wrapper.addClass('NB-story-content-wrapper-height-truncated'); var pages = Math.round(content_height / max_height, true); - var dots = _.map(_.range(pages), function() { return '·'; }).join(' '); - + var dots = _.map(_.range(pages), function () { return '·'; }).join(' '); + // console.log(["Height over, truncating...", this.model.get('story_title').substr(0, 30), content_height, max_height, pages]); this.$(".NB-story-content-expander-pages").html(dots); this._truncated = true; } else { // console.log(["Height under.", this.model.get('story_title').substr(0, 30), content_height, max_height]); } - + if (this.images_to_load > 0) { this.truncate_delay *= 1 + Math.random(); clearTimeout(this.truncate_delay_function); this.truncate_delay_function = _.delay(_.bind(this.truncate_story_height, this), this.truncate_delay); } }, - - watch_images_for_story_height: function() { - this.model.on('change:images_loaded', _.bind(function() { + + watch_images_for_story_height: function () { + this.model.on('change:images_loaded', _.bind(function () { this.resize_starred_tags(); }, this)); var is_truncatable = this.is_truncatable(); - + // console.log(['truncatable', is_truncatable, this.images_to_load]); if (!is_truncatable) return; - + this.truncate_delay = 100; this.images_to_load = this.$('img').length; if (is_truncatable) this.truncate_story_height(); - this.$('img').on('load', _.bind(function() { + this.$('img').on('load', _.bind(function () { this.images_to_load -= 1; if (is_truncatable) this.truncate_story_height(); if (this.images_to_load <= 0) { @@ -566,8 +566,8 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ } }, this)); }, - - expand_story: function(options) { + + expand_story: function (options) { options = options || {}; var $expander = this.$(".NB-story-content-expander"); var $expander_cutoff = this.$(".NB-story-cutoff"); @@ -576,46 +576,46 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ var max_height = parseInt($wrapper.css('maxHeight'), 10) || this.STORY_CONTENT_MAX_HEIGHT; var content_height = $content.outerHeight(true); var height_ratio = content_height / max_height; - + if (content_height < max_height) return; // console.log(["max height", max_height, content_height, content_height / max_height]); clearInterval(this._fetch_interval); - this._fetch_interval = setInterval(function() { + this._fetch_interval = setInterval(function () { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); }, 250); - + $wrapper.animate({ maxHeight: content_height }, { duration: options.instant ? 0 : Math.min(2 * 1000, parseInt(200 * height_ratio, 10)), easing: 'easeInOutQuart', - complete: _.bind(function() { + complete: _.bind(function () { clearInterval(this._fetch_interval); NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); $wrapper.removeClass('NB-story-content-wrapper-height-truncated'); }, this) }); - + $expander.add($expander_cutoff).animate({ bottom: -1 * $expander.outerHeight() - 76 }, { duration: options.instant ? 0 : Math.min(2 * 1000, parseInt(200 * height_ratio, 10)), easing: 'easeInOutQuart' }); - + }, - + // =========== // = Actions = // =========== - - mark_read: function() { - this.model.mark_read({force: true}); + + mark_read: function () { + this.model.mark_read({ force: true }); }, - - preserve_classifier_color: function(classifier_type, value, score) { + + preserve_classifier_color: function (classifier_type, value, score) { var $tag; - this.$('.NB-feed-story-'+classifier_type).each(function() { + this.$('.NB-feed-story-' + classifier_type).each(function () { if (_.string.trim($(this).text()) == value) { $tag = $(this); return false; @@ -624,60 +624,60 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ $tag.removeClass('NB-score-now-1') .removeClass('NB-score-now--1') .removeClass('NB-score-now-0') - .addClass('NB-score-now-'+score) - .one('mouseleave', function() { + .addClass('NB-score-now-' + score) + .one('mouseleave', function () { // console.log(["leave", score]); - $tag.removeClass('NB-score-now-'+score); - _.delay(function() { - $tag.one('mouseenter', function() { + $tag.removeClass('NB-score-now-' + score); + _.delay(function () { + $tag.one('mouseenter', function () { // console.log(["enter", score]); - $tag.removeClass('NB-score-now-'+score); + $tag.removeClass('NB-score-now-' + score); }); }, 100); }); }, - render_starred: function() { + render_starred: function () { var story = this.model; var $sideoption_title = this.$('.NB-feed-story-save .NB-sideoption-title'); - + if (story.get('starred')) { $sideoption_title.text('Saved'); } else { $sideoption_title.text('Removed'); - $sideoption_title.one('mouseleave', function() { - _.delay(function() { + $sideoption_title.one('mouseleave', function () { + _.delay(function () { if (!story.get('starred')) { $sideoption_title.text('Save'); } }, 200); - }); + }); } }, - - attach_syntax_highlighter_handler: function() { - _.delay(_.bind(function() { + + attach_syntax_highlighter_handler: function () { + _.delay(_.bind(function () { // hljs.configure({useBR: true}); // Don't use - this.$('pre').each(function(i, e) { + this.$('pre').each(function (i, e) { hljs.highlightBlock(e); }); }, this), 100); }, - - attach_fitvid_handler: function() { + + attach_fitvid_handler: function () { // Thanks to feedbin for the custom selector - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.$el.fitVids({ customSelector: "iframe[src*='youtu.be'],iframe[src*='www.flickr.com'],iframe[src*='view.vzaar.com']" }); }, this), 50); }, - + // ========== // = Events = // ========== - - click_link_in_story: function(e) { + + click_link_in_story: function (e) { if (NEWSBLUR.hotkeys.shift) return; var $target = $(e.currentTarget); @@ -687,12 +687,12 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ if (e.which == 1 && $('.NB-menu-manage-container:visible').length) return; var href = $target.attr('href'); - + // Fix footnotes if (_.string.contains(href, "#")) { try { footnote_href = href.replace(/^.*?\#(.*?)$/, "\#$1") - .replace(':', "\\\:"); + .replace(':', "\\\:"); var $footnote = $(footnote_href); } catch (err) { $footnote = []; @@ -703,60 +703,60 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ var $scroll; if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { $scroll = NEWSBLUR.reader.$s.$story_titles; - } else if (NEWSBLUR.reader.flags['temporary_story_view'] || + } else if (NEWSBLUR.reader.flags['temporary_story_view'] || NEWSBLUR.reader.story_view == 'text') { $scroll = NEWSBLUR.reader.$s.$text_view; } else { $scroll = NEWSBLUR.reader.$s.$feed_scroll; } offset += $scroll.scrollTop(); - $scroll.stop().scrollTo(offset-60, { + $scroll.stop().scrollTo(offset - 60, { duration: 340, - axis: 'y', + axis: 'y', easing: 'easeInOutQuint' }); return; } } - + if (NEWSBLUR.assets.preference('new_window') == 1) { window.open(href, '_blank'); } else { window.open(href); } - - this.model.set('selected', true, {selected_by_scrolling: true}); - + + this.model.set('selected', true, { selected_by_scrolling: true }); + return false; }, - - mouseenter_manage_icon: function() { + + mouseenter_manage_icon: function () { var menu_height = 270; if (this.$el.offset().top > $(window).height() - menu_height) { this.$el.addClass('NB-hover-inverse'); } }, - - mouseleave_manage_icon: function() { + + mouseleave_manage_icon: function () { this.$el.removeClass('NB-hover-inverse'); }, - - mouseenter: function() { + + mouseenter: function () { if (this.model.get('selected')) return; - + if (NEWSBLUR.reader.flags['scrolling_by_selecting_story_title'] || NEWSBLUR.assets.preference('feed_view_single_story')) { return; } - - this.model.set('selected', true, {selected_by_scrolling: true}); + + this.model.set('selected', true, { selected_by_scrolling: true }); }, - - mouseleave: function() { - + + mouseleave: function () { + }, - - mouseup_check_selection: function(e) { + + mouseup_check_selection: function (e) { var $doc = this.$(".NB-feed-story-content"); // console.log(['mouseup_check_selection', e, e.which, $(e.target)]); if (e.which == 3) { @@ -772,7 +772,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ this.$(".NB-starred-story-selection-highlight,[data-tippy]").contents().unwrap(); $doc.attr('id', 'NB-highlighting'); - + var text = ""; var selection; if (window.getSelection) { @@ -784,7 +784,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ } this.serialized_highlight = _.string.trim(text); // console.log(['mouseup_check_selection 1', this.serialized_highlight]); - + if (this.tooltip && this.tooltip.tooltips && this.tooltip.tooltips.length) { this.tooltip.tooltips[0].hide(); } @@ -798,7 +798,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ "className": "NB-starred-story-selection-highlight", "separateWordSearch": false, "acrossElements": true, - "filter": function(node, term, total_counter, counter) { + "filter": function (node, term, total_counter, counter) { if (!selection.containsNode(node)) return false; // Highlighting the second 'baz' will fail, and the entire 'baz quz baz' will be highlighted instead. // foo bar baz quz baz bar foo @@ -806,7 +806,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ // if (node.textContent.indexOf(term) != selection.anchorOffset) return false; return true; }, - "done": _.bind(function() { + "done": _.bind(function () { var $selection = $(".NB-starred-story-selection-highlight", $doc); console.log(['$selection', $selection, $selection.first().get(0), $selection.last().get(0)]); $selection.attr('title', "
Highlight
"); @@ -821,12 +821,12 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ trigger: 'click', interactive: true, performance: true, - onHide: _.bind(function() { + onHide: _.bind(function () { $selection.removeClass("NB-starred-story-selection-highlight"); }, this) }); this.tooltip = $t; - _.defer(function() { + _.defer(function () { if ($t.tooltips && $t.tooltips.length) $t.tooltips[0].show(); }); @@ -842,16 +842,16 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ range.setEndAfter($selection.last().get(0), 0); selection.addRange(range); } - // } else if (doc.body.createTextRange) { - // range = doc.body.createTextRange(); - // range.moveToElementText($selection[0]); - // range.select(); + // } else if (doc.body.createTextRange) { + // range = doc.body.createTextRange(); + // range.moveToElementText($selection[0]); + // range.select(); } }, this) }); }, - - show_unhighlight_tooltip: function($highlight) { + + show_unhighlight_tooltip: function ($highlight) { this.$highlight = $highlight; $highlight.attr('title', "
Unhighlight
"); var $t = tippy($highlight.get(0), { @@ -865,60 +865,60 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ trigger: 'click', interactive: true, performance: true, - onHide: _.bind(function() { + onHide: _.bind(function () { // $highlight.removeClass('NB-starred-story-selection-highlight'); }, this) }); this.tooltip = $t; - _.defer(function() { + _.defer(function () { if ($t.tooltips && $t.tooltips.length) $t.tooltips[0].show(); }); - + }, - - highlight_selected_text: function() { + + highlight_selected_text: function () { var highlights = this.model.get('highlights'); if (!highlights || !$.isArray(highlights)) highlights = []; highlights.push(this.serialized_highlight); - this.model.set('highlights', highlights, {silent: true}); + this.model.set('highlights', highlights, { silent: true }); this.model.trigger('change:highlights'); console.log(['highlight_selected_text', this.serialized_highlight, highlights]); - + if (this.tooltip && this.tooltip.tooltips && this.tooltip.tooltips.length) { this.tooltip.tooltips[0].hide(); } - + this.apply_starred_story_selections(); - + return true; }, - - unhighlight_selected_text: function(el) { + + unhighlight_selected_text: function (el) { var remove_highlight = this.$highlight.text(); var highlights = this.model.get('highlights'); if (!highlights || !$.isArray(highlights)) highlights = []; - highlights = _.filter(highlights, function(value) { return !_.string.contains(value, remove_highlight); }); - - this.model.set('highlights', highlights, {silent: true}); + highlights = _.filter(highlights, function (value) { return !_.string.contains(value, remove_highlight); }); + + this.model.set('highlights', highlights, { silent: true }); this.model.trigger('change:highlights'); console.log(['UNhighlighting', remove_highlight, highlights]); - + if (this.tooltip && this.tooltip.tooltips && this.tooltip.tooltips.length) { this.tooltip.tooltips[0].hide(); } - + this.apply_starred_story_selections(true); - + return true; }, - - apply_starred_story_selections: function(force) { + + apply_starred_story_selections: function (force) { var highlights = this.model.user_highlights(); if (!force) { if (!highlights || !highlights.length) return; } console.log(['Applying highlights', highlights]); - + var $doc = this.$(".NB-feed-story-content"); $doc.unmark(); @@ -930,14 +930,14 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ }); $doc.removeAttr('id'); }, - - show_manage_menu_rightclick: function(e) { + + show_manage_menu_rightclick: function (e) { if (!NEWSBLUR.assets.preference('show_contextmenus')) return; - + return this.show_manage_menu(e); }, - show_manage_menu: function(e) { + show_manage_menu: function (e) { e.preventDefault(); e.stopPropagation(); NEWSBLUR.reader.show_manage_menu('story', this.$el, { @@ -947,22 +947,22 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ }); return false; }, - - show_story_changes: function() { - NEWSBLUR.assets.fetch_story_changes(this.model.get('story_hash'), !this.model.get('showing_diff'), _.bind(function(data) { + + show_story_changes: function () { + NEWSBLUR.assets.fetch_story_changes(this.model.get('story_hash'), !this.model.get('showing_diff'), _.bind(function (data) { this.model.set('showing_diff', !this.model.get('showing_diff')); this.model.set('story_content', data.story['story_content']); - NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); - }, this), function() { + NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); + }, this), function () { console.log(['Failed to fetch story changes']); }); }, - - open_feed: function() { + + open_feed: function () { NEWSBLUR.reader.open_feed(this.model.get('story_feed_id')); }, - - save_classifier: function(e) { + + save_classifier: function (e) { var $tag = $(e.currentTarget); var classifier_type = $tag.hasClass('NB-feed-story-tag') ? 'tag' : 'author'; var value = _.string.trim($tag.text()); @@ -971,26 +971,26 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ var data = { 'feed_id': feed_id }; - + if (score == 0) { - data['remove_like_'+classifier_type] = value; + data['remove_like_' + classifier_type] = value; } else if (score == 1) { - data['like_'+classifier_type] = value; + data['like_' + classifier_type] = value; } else if (score == -1) { - data['dislike_'+classifier_type] = value; + data['dislike_' + classifier_type] = value; } - this.model.set('visible', true, {silent: true}); - NEWSBLUR.assets.classifiers[feed_id][classifier_type+'s'][value] = score; - NEWSBLUR.assets.recalculate_story_scores(feed_id, {story_view: this}); - NEWSBLUR.assets.save_classifier(data, function(resp) { + this.model.set('visible', true, { silent: true }); + NEWSBLUR.assets.classifiers[feed_id][classifier_type + 's'][value] = score; + NEWSBLUR.assets.recalculate_story_scores(feed_id, { story_view: this }); + NEWSBLUR.assets.save_classifier(data, function (resp) { NEWSBLUR.reader.feed_unread_count(feed_id); }); - + this.model.trigger('change:intelligence'); this.preserve_classifier_color(classifier_type, value, score); }, - - open_story_trainer: function() { + + open_story_trainer: function () { var feed_id = this.model.get('story_feed_id'); var options = {}; if (NEWSBLUR.reader.flags['social_view']) { @@ -999,16 +999,16 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ } NEWSBLUR.reader.open_story_trainer(this.model.id, feed_id, options); }, - - open_email: function() { + + open_email: function () { NEWSBLUR.reader.send_story_to_email(this.model); }, - - toggle_starred: function() { + + toggle_starred: function () { this.model.toggle_starred(); }, - - scroll_to_comments: function() { + + scroll_to_comments: function () { if (_.contains(['list', 'grid', 'magazine'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { NEWSBLUR.app.story_titles.scroll_to_selected_story(this.model, { scroll_to_comments: true, @@ -1021,6 +1021,6 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ }); } } - + }); diff --git a/media/js/newsblur/views/story_list_view.js b/media/js/newsblur/views/story_list_view.js index 7ad1b8f6a0..edb378f2b2 100644 --- a/media/js/newsblur/views/story_list_view.js +++ b/media/js/newsblur/views/story_list_view.js @@ -1,18 +1,18 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ - + el: '.NB-feed-stories', - + events: { - "click .NB-feed-story-premium-only a" : function(e) { + "click .NB-feed-story-premium-only a": function (e) { e.preventDefault(); - NEWSBLUR.reader.open_feedchooser_modal({premium_only: true}); + NEWSBLUR.reader.open_feedchooser_modal({ premium_only: true }); } }, - - initialize: function() { - _.bindAll(this, 'check_feed_view_scrolled_to_bottom', - 'check_feed_view_scrolling_from_top', - 'scroll'); + + initialize: function () { + _.bindAll(this, 'check_feed_view_scrolled_to_bottom', + 'check_feed_view_scrolling_from_top', + 'scroll'); this.collection.bind('reset', this.reset_flags, this); this.collection.bind('reset', this.render, this); this.collection.bind('reset', this.reset_story_positions, this); @@ -27,8 +27,8 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ NEWSBLUR.reader.$s.$feed_scroll.scroll(this.scroll); this.reset_flags(); }, - - reset_flags: function() { + + reset_flags: function () { this.clear(); this.cache = { story_pane_position: null, @@ -44,17 +44,17 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ positions_timer: 0 }; }, - + // ========== // = Render = // ========== - - render: function() { + + render: function () { // console.log(["Rendering story list", NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')]); if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; - + var collection = this.collection; - var stories = _.compact(this.collection.map(function(story) { + var stories = _.compact(this.collection.map(function (story) { // if (story.story_view) return story; var view = new NEWSBLUR.Views.StoryDetailView({ model: story, @@ -66,7 +66,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ return view.render(); } })); - + if (NEWSBLUR.assets.preference('feed_view_single_story')) { this.show_correct_explainer(); } else { @@ -77,14 +77,14 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ _.defer(this.check_feed_view_scrolled_to_bottom); this.end_loading(); }, - - add: function(options) { + + add: function (options) { if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; if (options.added) { var collection = this.collection; var added = this.collection.models.slice(-1 * options.added); - var stories = _.compact(_.map(added, function(story) { + var stories = _.compact(_.map(added, function (story) { if (story.story_view) return; var view = new NEWSBLUR.Views.StoryDetailView({ model: story, @@ -100,7 +100,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.$el.append(_.pluck(stories, 'el')); _.invoke(stories, 'attach_handlers'); } - + this.stories = this.stories.concat(stories); _.defer(this.check_feed_view_scrolled_to_bottom); this.show_correct_explainer(); @@ -110,22 +110,22 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.end_loading(); }, - - clear: function() { + + clear: function () { _.invoke(this.stories, 'destroy'); this.$el.empty(); this.collection.page_fill_outs = 0; this.collection.no_more_stories = false; this.clear_explainer(); }, - - clear_explainer: function() { + + clear_explainer: function () { this.$(".NB-story-list-empty").remove(); this.$el.removeClass("NB-empty"); }, - - show_correct_explainer: function() { - if (NEWSBLUR.assets.preference('feed_view_single_story') && + + show_correct_explainer: function () { + if (NEWSBLUR.assets.preference('feed_view_single_story') && NEWSBLUR.assets.stories.visible().length) { this.show_explainer_single_story_mode(); } else if (!NEWSBLUR.assets.stories.visible().length) { @@ -134,12 +134,12 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.clear_explainer(); } }, - - show_explainer_single_story_mode: function() { + + show_explainer_single_story_mode: function () { this.clear_explainer(); - + if (NEWSBLUR.reader.active_story) return; - + var $empty = $.make("div", { className: "NB-story-list-empty" }, [ $.make('div', { className: 'NB-world' }), 'Select a story to read' @@ -147,13 +147,13 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.$el.append($empty); }, - - show_explainer_no_stories: function() { + + show_explainer_no_stories: function () { this.clear_explainer(); - + if (NEWSBLUR.reader.active_story) return; if (!this.collection.no_more_stories) return; - + var counts = NEWSBLUR.reader.get_unread_count(); var unread_view_score = NEWSBLUR.reader.get_unread_view_score(); var hidden_stories = false; @@ -177,15 +177,15 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ ]); this.$el.append($empty); - + this.$el.addClass("NB-empty"); }, - + // =========== // = Actions = // =========== - - scroll_to_selected_story: function(story, options) { + + scroll_to_selected_story: function (story, options) { options = options || {}; if (!story) story = NEWSBLUR.reader.active_story; if (!story || !story.story_view) return; @@ -198,34 +198,34 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ if (options.scroll_to_comments) { $story = $('.NB-feed-story-comments', $story); } - + if (options.only_if_hidden && NEWSBLUR.reader.$s.$feed_scroll.isScrollVisible($story, true)) { return; } - + if (!options.scroll_to_top && !$story.closest(NEWSBLUR.reader.$s.$feed_scroll).length) { console.log(['Story not visible, not scrolling', $story]); return; } - + clearTimeout(NEWSBLUR.reader.locks.scrolling); NEWSBLUR.reader.flags.scrolling_by_selecting_story_title = true; var scroll_to = options.scroll_to_top ? 0 : $story; - NEWSBLUR.reader.$s.$feed_scroll.stop().scrollTo(scroll_to, { + NEWSBLUR.reader.$s.$feed_scroll.stop().scrollTo(scroll_to, { duration: options.immediate ? 0 : 340, - axis: 'y', - easing: 'easeInOutQuint', + axis: 'y', + easing: 'easeInOutQuint', offset: options.scroll_offset || 0, - queue: false, - onAfter: function() { - NEWSBLUR.reader.locks.scrolling = setTimeout(function() { + queue: false, + onAfter: function () { + NEWSBLUR.reader.locks.scrolling = setTimeout(function () { NEWSBLUR.reader.flags.scrolling_by_selecting_story_title = false; }, 100); } }); }, - - show_only_selected_story: function(model) { + + show_only_selected_story: function (model) { if (!model) model = NEWSBLUR.reader.active_story; if (!NEWSBLUR.assets.preference('feed_view_single_story')) return; if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; @@ -234,8 +234,8 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ } this.clear_explainer(); - - this.collection.any(_.bind(function(story) { + + this.collection.any(_.bind(function (story) { if (story && story.get('selected') && story.story_view) { this.$el.html(story.story_view.el); story.story_view.setElement(story.story_view.el); @@ -243,22 +243,22 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ return true; } }, this)); - + this.show_no_more_stories(); }, - - show_no_more_stories: function() { + + show_no_more_stories: function () { if (!this.collection.no_more_stories) return; - + if (!NEWSBLUR.assets.stories.visible().length) { this.show_explainer_no_stories(); // return; } - + var pane_height = NEWSBLUR.reader.$s.$story_pane.height(); var indicator_position = NEWSBLUR.assets.preference('lock_mouse_indicator'); var endbar_height = 20; - if (indicator_position && + if (indicator_position && _.contains(['full', 'split'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { var last_visible_story = _.last(NEWSBLUR.assets.stories.visible()); var last_story_height = last_visible_story && last_visible_story.story_view && last_visible_story.story_view.$el.height() || 100; @@ -270,7 +270,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ if (empty_space > 0) endbar_height += empty_space + 1; // console.log(["endbar height full/split", endbar_height, empty_space, pane_height, last_story_offset, last_story_height]); } - + this.$('.NB-end-line').remove(); if (NEWSBLUR.assets.preference('feed_view_single_story')) { var last_story = NEWSBLUR.assets.stories.last(); @@ -278,63 +278,63 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ } endbar_height /= 2; // Splitting padding between top and bottom - var $end_stories_line = $.make('div', { + var $end_stories_line = $.make('div', { className: 'NB-end-line' }, [ $.make('div', { className: 'NB-fleuron' }) ]).css('paddingBottom', endbar_height).css('paddingTop', endbar_height); - + this.$el.append($end_stories_line); }, - - show_stories_preference_in_feed_view: function(is_creating) { - if (NEWSBLUR.reader.active_story && + + show_stories_preference_in_feed_view: function (is_creating) { + if (NEWSBLUR.reader.active_story && NEWSBLUR.assets.preference('feed_view_single_story')) { this.$el.removeClass('NB-feed-view-feed').addClass('NB-feed-view-story'); NEWSBLUR.reader.$s.$feed_stories.scrollTop('0px'); this.flags['feed_view_positions_calculated'] = false; } else { this.$el.removeClass('NB-feed-view-story').addClass('NB-feed-view-feed'); - NEWSBLUR.reader.show_story_titles_above_intelligence_level({'animate': false}); + NEWSBLUR.reader.show_story_titles_above_intelligence_level({ 'animate': false }); } this.cache.story_pane_position = NEWSBLUR.reader.$s.$story_pane.offset().top;; }, - fill_out: function(options) { - if (this.collection.no_more_stories || + fill_out: function (options) { + if (this.collection.no_more_stories || !NEWSBLUR.reader.flags.story_titles_closed) { this.show_no_more_stories(); return; } - + options = options || {}; - - if (this.collection.page_fill_outs < NEWSBLUR.reader.constants.FILL_OUT_PAGES && + + if (this.collection.page_fill_outs < NEWSBLUR.reader.constants.FILL_OUT_PAGES && !this.collection.no_more_stories) { // var $last = this.$('.NB-feed-story:visible:last'); // var container_height = NEWSBLUR.reader.$s.$story_titles.height(); // NEWSBLUR.log(["fill out", $last.length && $last.position().top, container_height, $last.length, NEWSBLUR.reader.$s.$story_titles.scrollTop()]); this.collection.page_fill_outs += 1; - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.scroll(); }, this), 10); } else { this.show_no_more_stories(); } }, - - show_loading: function(options) { + + show_loading: function (options) { options = options || {}; if (this.collection.no_more_stories) return; - + var $feed_scroll = NEWSBLUR.reader.$s.$feed_scroll; this.$('.NB-end-line').remove(); var $endline = $.make('div', { className: "NB-end-line NB-load-line NB-short" }); - $endline.css({'background': '#FFF'}); + $endline.css({ 'background': '#FFF' }); $feed_scroll.append($endline); }, - - check_premium_river: function() { + + check_premium_river: function () { if (!NEWSBLUR.Globals.is_premium && NEWSBLUR.Globals.is_authenticated && NEWSBLUR.reader.flags['river_view']) { @@ -344,16 +344,16 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.show_no_more_stories(); } }, - - check_premium_search: function() { + + check_premium_search: function () { if (!NEWSBLUR.Globals.is_premium && NEWSBLUR.reader.flags.search) { this.show_no_more_stories(); this.append_search_premium_only_notification(); } }, - - end_loading: function() { + + end_loading: function () { var $endbar = NEWSBLUR.reader.$s.$feed_scroll.find('.NB-end-line'); $endbar.remove(); @@ -361,8 +361,8 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.show_no_more_stories(); } }, - - append_river_premium_only_notification: function() { + + append_river_premium_only_notification: function () { var message = [ 'The full River of News is a ', $.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'), @@ -384,21 +384,21 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ } var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [ - $.make('div', { className: 'NB-feed-story-premium-only-text'}, message) + $.make('div', { className: 'NB-feed-story-premium-only-text' }, message) ]); this.$('.NB-feed-story-premium-only').remove(); - if (_.contains(['full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { - $(".NB-story-list-empty").append($notice); - } else { - this.$(".NB-end-line").append($notice); - } - + if (_.contains(['full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) { + $(".NB-story-list-empty").append($notice); + } else { + this.$(".NB-end-line").append($notice); + } + // console.log(["append_search_premium_only_notification", this.$(".NB-end-line")]); }, - - append_search_premium_only_notification: function() { + + append_search_premium_only_notification: function () { var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [ - $.make('div', { className: 'NB-feed-story-premium-only-text'}, [ + $.make('div', { className: 'NB-feed-story-premium-only-text' }, [ 'Search is a ', $.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'), '.' @@ -407,43 +407,43 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.$('.NB-feed-story-premium-only').remove(); this.$(".NB-end-line").append($notice); }, - + // ============= // = Positions = // ============= - - is_feed_loaded_for_location_fetch: function() { - var images_begun = NEWSBLUR.assets.stories.any(function(s) { - return !_.isUndefined(s.get('images_loaded')); + + is_feed_loaded_for_location_fetch: function () { + var images_begun = NEWSBLUR.assets.stories.any(function (s) { + return !_.isUndefined(s.get('images_loaded')); }); if (images_begun) { - var images_loaded = NEWSBLUR.assets.stories.all(function(s) { - return s.get('images_loaded'); + var images_loaded = NEWSBLUR.assets.stories.all(function (s) { + return s.get('images_loaded'); }); return images_loaded; } return images_begun; }, - - prefetch_story_locations_in_feed_view: function() { + + prefetch_story_locations_in_feed_view: function () { var self = this; var stories = NEWSBLUR.assets.stories; if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; if (NEWSBLUR.assets.preference('feed_view_single_story')) return; - + // NEWSBLUR.log(['Prefetching Feed', this.flags['feed_view_positions_calculated'], this.is_feed_loaded_for_location_fetch()]); if (!NEWSBLUR.assets.stories.size()) return; - + if (!this.flags['feed_view_positions_calculated']) { - + $.extend(this.cache, { 'feed_view_story_positions': {}, 'feed_view_story_positions_keys': [] }); - - NEWSBLUR.assets.stories.any(_.bind(function(story) { + + NEWSBLUR.assets.stories.any(_.bind(function (story) { if (!story.story_view) return; this.determine_feed_view_story_position(story); var $story = story.story_view.$el; @@ -451,50 +451,50 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ return true; } }, this)); - + clearTimeout(this.flags['prefetch']); - this.flags['prefetch'] = setTimeout(_.bind(function() { + this.flags['prefetch'] = setTimeout(_.bind(function () { if (!this.flags['feed_view_positions_calculated']) { this.prefetch_story_locations_in_feed_view(); } }, this), 2000); - } - + } + if (this.is_feed_loaded_for_location_fetch()) { - this.fetch_story_locations_in_feed_view({'reset_timer': true}); + this.fetch_story_locations_in_feed_view({ 'reset_timer': true }); } else { // NEWSBLUR.log(['Still loading feed view...', this.cache.feed_view_story_positions_keys.length]); } }, - - fetch_story_locations_in_feed_view: function(options) { + + fetch_story_locations_in_feed_view: function (options) { options = options || {}; var stories = NEWSBLUR.assets.stories; - + if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; if (NEWSBLUR.assets.preference('feed_view_single_story')) return; if (!stories || !stories.length) return; if (options.reset_timer) this.counts['positions_timer'] = 0; - + $.extend(this.cache, { 'feed_view_story_positions': {}, 'feed_view_story_positions_keys': [] }); - NEWSBLUR.assets.stories.each(_.bind(function(story) { + NEWSBLUR.assets.stories.each(_.bind(function (story) { this.determine_feed_view_story_position(story); }, this)); this.flags['feed_view_positions_calculated'] = true; // NEWSBLUR.log(['Feed view entirely loaded', NEWSBLUR.assets.stories.length + " stories", this.counts['positions_timer']/1000 + " sec delay"]); - - this.counts['positions_timer'] = Math.min(Math.max(this.counts['positions_timer']+1000, 1000), 15*1000); + + this.counts['positions_timer'] = Math.min(Math.max(this.counts['positions_timer'] + 1000, 1000), 15 * 1000); clearTimeout(this.flags['next_fetch']); this.flags['next_fetch'] = _.delay(_.bind(this.fetch_story_locations_in_feed_view, this), - this.counts['positions_timer']); + this.counts['positions_timer']); }, - - determine_feed_view_story_position: function(story) { + + determine_feed_view_story_position: function (story) { if (!story.story_view) return; var $story = story.story_view.$el; if (story && $story.is(':visible')) { @@ -503,12 +503,12 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ var position = position_original + position_offset; this.cache.feed_view_story_positions[position] = story; this.cache.feed_view_story_positions_keys.push(position); - this.cache.feed_view_story_positions_keys.sort(function(a, b) { return a-b; }); + this.cache.feed_view_story_positions_keys.sort(function (a, b) { return a - b; }); // NEWSBLUR.log(['Positioning story', position, story.get('story_title')]); } }, - check_feed_view_scrolled_to_bottom: function(model, selected) { + check_feed_view_scrolled_to_bottom: function (model, selected) { // console.log(['check_feed_view_scrolled_to_bottom', model, selected]); if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; if (NEWSBLUR.assets.preference('feed_view_single_story')) return; @@ -516,37 +516,37 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ if (!NEWSBLUR.assets.stories.size()) return; if (!NEWSBLUR.reader.active_feed) return; if (selected === false) return; - + var last_story = _.last(NEWSBLUR.assets.stories.visible()); if (!last_story || last_story.get('selected')) { - NEWSBLUR.reader.load_page_of_feed_stories({scroll_to_loadbar: false}); + NEWSBLUR.reader.load_page_of_feed_stories({ scroll_to_loadbar: false }); return; } if (NEWSBLUR.assets.preference('feed_view_single_story')) return; - + if (!last_story.story_view) return; - + var $last_story = last_story.story_view.$el; var container_offset = NEWSBLUR.reader.$s.$feed_scroll.position().top; var full_height = ($last_story.length && $last_story.offset().top) + $last_story.height() - container_offset; var visible_height = NEWSBLUR.reader.$s.$feed_scroll.height() * 2; var scroll_y = NEWSBLUR.reader.$s.$feed_scroll.scrollTop(); - + // Fudge factor is simply because it looks better at 64 pixels off. // NEWSBLUR.log(['check_feed_view_scrolled_to_bottom', full_height, container_offset, visible_height, scroll_y, NEWSBLUR.reader.flags['opening_feed']]); if ((visible_height + 2048) >= full_height) { // NEWSBLUR.log(['check_feed_view_scrolled_to_bottom', full_height, container_offset, visible_height, scroll_y, NEWSBLUR.reader.flags['opening_feed']]); - NEWSBLUR.reader.load_page_of_feed_stories({scroll_to_loadbar: false}); + NEWSBLUR.reader.load_page_of_feed_stories({ scroll_to_loadbar: false }); } }, - - check_feed_view_scrolling_from_top: function(scroll_top) { + + check_feed_view_scrolling_from_top: function (scroll_top) { var cursor_position = NEWSBLUR.reader.cache.mouse_position_y + scroll_top; var positions = this.cache.feed_view_story_positions_keys; - _.any(positions, _.bind(function(position) { + _.any(positions, _.bind(function (position) { if (position > cursor_position) return true; if (position <= this.cache.latest_mark_read_scroll_position) return false; - + var story = this.cache.feed_view_story_positions[position]; if (!story.get('read_status')) story.mark_read(); @@ -554,23 +554,23 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ return false; }, this)); }, - - reset_story_positions: function(models) { + + reset_story_positions: function (models) { if (!_.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'))) return; if (NEWSBLUR.assets.preference('feed_view_single_story')) return; - + if (!models || !models.length) { models = NEWSBLUR.assets.stories; } if (!models.length) return; - + this.flags['feed_view_positions_calculated'] = false; - + if (this.cache.story_pane_position == null) { this.cache.story_pane_position = NEWSBLUR.reader.$s.$story_pane.offset().top; } - models.each(_.bind(function(story) { + models.each(_.bind(function (story) { if (!story.story_view) return; var image_count = story.story_view.$('.NB-feed-story-content img').length; if (!image_count) { @@ -581,8 +581,8 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ // loads, the position is calculated and the next story can calculate // its position (after its own images are loaded). story.set('images_loaded', false); - (function(story, image_count) { - story.story_view.$('.NB-feed-story-content img').on('load', function() { + (function (story, image_count) { + story.story_view.$('.NB-feed-story-content img').on('load', function () { // NEWSBLUR.log(['Loaded image', story.get('story_title'), image_count]); if (image_count <= 1) { story.set('images_loaded', true); @@ -594,11 +594,11 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ })(story, image_count); } }, this)); - + this.prefetch_story_locations_in_feed_view(); }, - - switch_story_view: function(story, selected, options) { + + switch_story_view: function (story, selected, options) { // console.log(['switch_story_view list', story, selected, options]); if (selected && !options.selected_by_scrolling) { var story_view = NEWSBLUR.assets.view_setting(story.get('story_feed_id'), 'view'); @@ -610,33 +610,33 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ } } }, - + // ========== // = Events = // ========== - - handle_mousemove_feed_view: function(e) { + + handle_mousemove_feed_view: function (e) { var self = this; - + if (NEWSBLUR.assets.preference('feed_view_single_story')) { return NEWSBLUR.reader.hide_mouse_indicator(); } else { NEWSBLUR.reader.show_mouse_indicator(); } - + if (parseInt(NEWSBLUR.assets.preference('lock_mouse_indicator'), 10)) { return; } - + // console.log(["mousemove", e.pageY, this.cache.story_pane_position, NEWSBLUR.reader.cache.mouse_position_y]); NEWSBLUR.reader.cache.mouse_position_y = e.pageY - this.cache.story_pane_position; NEWSBLUR.reader.$s.$mouse_indicator.css('top', NEWSBLUR.reader.cache.mouse_position_y - 8); - + if (this.flags['mousemove_timeout'] || NEWSBLUR.reader.flags['scrolling_by_selecting_story_title']) { return; } - + var from_top = NEWSBLUR.reader.cache.mouse_position_y + NEWSBLUR.reader.$s.$feed_scroll.scrollTop(); var offset = this.cache.offset || 0; if (NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full' && !this.cache.offset) { @@ -649,20 +649,20 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ // console.log(["mousemove", from_top, offset, position, positions]); if (!story) return; if (!story.get('selected')) { - story.set('selected', true, {selected_by_scrolling: true, mouse: true, immediate: true}); + story.set('selected', true, { selected_by_scrolling: true, mouse: true, immediate: true }); } }, - - scroll: function(elem, e) { + + scroll: function (elem, e) { var self = this; var story_view = NEWSBLUR.reader.story_view; var offset = this.cache.offset || 0; if (NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout') == 'full' && !this.cache.offset) { // offset = this.cache.offset = $(".NB-feed-story-view-header").outerHeight(); } - + if ((story_view == 'feed' || - (story_view == 'page' && NEWSBLUR.reader.flags['page_view_showing_feed_view'])) && + (story_view == 'page' && NEWSBLUR.reader.flags['page_view_showing_feed_view'])) && !NEWSBLUR.reader.flags['scrolling_by_selecting_story_title'] && !NEWSBLUR.assets.preference('feed_view_single_story')) { var scroll_top = NEWSBLUR.reader.$s.$feed_scroll.scrollTop(); @@ -675,7 +675,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ if (!story) return; // NEWSBLUR.log(["Scroll Feed", NEWSBLUR.reader.cache.mouse_position_y, NEWSBLUR.reader.$s.$feed_scroll.scrollTop(), from_top, offset, position, closest, story.get('story_title')]); if (!story.get('selected')) { - story.set('selected', true, {selected_by_scrolling: true, mouse: true, immediate: true}); + story.set('selected', true, { selected_by_scrolling: true, mouse: true, immediate: true }); } this.check_feed_view_scrolled_to_bottom(); @@ -683,7 +683,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ this.check_feed_view_scrolling_from_top(scroll_top); } } - + if ((NEWSBLUR.reader.flags['river_view'] || NEWSBLUR.reader.flags['social_view']) && !NEWSBLUR.assets.preference('feed_view_single_story')) { var story; @@ -697,5 +697,5 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({ } } } - + }); diff --git a/media/js/newsblur/views/story_options_popover.js b/media/js/newsblur/views/story_options_popover.js index 23309a90c3..ca1a3da35a 100644 --- a/media/js/newsblur/views/story_options_popover.js +++ b/media/js/newsblur/views/story_options_popover.js @@ -1,7 +1,7 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ - + className: "NB-style-popover", - + options: { 'width': 274, 'anchor': '.NB-taskbar-options', @@ -15,7 +15,7 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ 'overlay_bottom': true, 'popover_class': 'NB-style-popover-container' }, - + events: { "click .NB-font-family-option": "change_font_family", "click .NB-story-font-size-option": "change_story_font_size", @@ -28,26 +28,26 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ "click .NB-grid-height-option": "change_grid_height", "click .NB-premium-link": "open_premium_modal" }, - - initialize: function(options) { + + initialize: function (options) { this.options = _.extend({}, this.options, options); NEWSBLUR.ReaderPopover.prototype.initialize.call(this, this.options); this.model = NEWSBLUR.assets; this.render(); this.show_correct_options(); }, - - close: function() { + + close: function () { NEWSBLUR.reader.$s.$taskbar_options.removeClass('NB-active'); NEWSBLUR.ReaderPopover.prototype.close.apply(this, arguments); }, - render: function() { + render: function () { var self = this; var feed = NEWSBLUR.assets.active_feed; - + NEWSBLUR.ReaderPopover.prototype.render.call(this); - + this.$el.html($.make('div', [ $.make('div', { className: 'NB-popover-section' }, [ $.make('div', { className: 'NB-popover-section-title' }, 'Story Layout - Split'), @@ -171,11 +171,11 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ ])) ]) ])); - + return this; }, - - show_correct_options: function() { + + show_correct_options: function () { var font_family = NEWSBLUR.assets.preference('story_styling'); var story_font_size = NEWSBLUR.assets.preference('story_size'); var line_spacing = NEWSBLUR.assets.preference('story_line_spacing'); @@ -184,42 +184,42 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ var grid_columns = NEWSBLUR.assets.preference('grid_columns'); var grid_height = NEWSBLUR.assets.preference('grid_height'); var story_position = NEWSBLUR.assets.preference('story_position'); - + this.$('.NB-font-family-option').removeClass('NB-active'); - this.$('.NB-options-font-family-'+font_family).addClass('NB-active'); + this.$('.NB-options-font-family-' + font_family).addClass('NB-active'); this.$('.NB-story-font-size-option').removeClass('NB-active'); - this.$('.NB-options-story-font-size .NB-options-font-size-'+story_font_size).addClass('NB-active'); + this.$('.NB-options-story-font-size .NB-options-font-size-' + story_font_size).addClass('NB-active'); this.$('.NB-line-spacing-option').removeClass('NB-active'); - this.$('.NB-options-line-spacing-'+line_spacing).addClass('NB-active'); + this.$('.NB-options-line-spacing-' + line_spacing).addClass('NB-active'); this.$('.NB-story-position-option').removeClass('NB-active'); - this.$('.NB-options-story-position-'+story_position).addClass('NB-active'); + this.$('.NB-options-story-position-' + story_position).addClass('NB-active'); this.$('.NB-story-titles-pane-option').removeClass('NB-active'); - this.$('.NB-options-story-titles-pane-'+titles_layout_pane).addClass('NB-active'); + this.$('.NB-options-story-titles-pane-' + titles_layout_pane).addClass('NB-active'); this.$('.NB-single-story-option').removeClass('NB-active'); - this.$('.NB-options-single-story-'+(single_story?'on':'off')).addClass('NB-active'); + this.$('.NB-options-single-story-' + (single_story ? 'on' : 'off')).addClass('NB-active'); this.$('.NB-grid-columns-option').removeClass('NB-active'); - this.$('.NB-options-grid-columns-'+grid_columns).addClass('NB-active'); + this.$('.NB-options-grid-columns-' + grid_columns).addClass('NB-active'); this.$('.NB-grid-height-option').removeClass('NB-active'); - this.$('.NB-options-grid-height-'+grid_height).addClass('NB-active'); + this.$('.NB-options-grid-height-' + grid_height).addClass('NB-active'); NEWSBLUR.reader.$s.$taskbar_options.addClass('NB-active'); - + if (!NEWSBLUR.Globals.is_premium) { this.$(".NB-premium-only").addClass('NB-disabled').attr('disabled', 'disabled'); } }, - + // ========== // = Events = // ========== - - change_font_family: function(e) { + + change_font_family: function (e) { var $target = $(e.target); - + if ($target.hasClass("NB-options-font-family-serif")) { this.update_font_family('serif'); } else if ($target.hasClass("NB-options-font-family-sans-serif")) { @@ -235,18 +235,18 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ this.update_font_family('chronicle'); } } - + this.show_correct_options(); }, - - update_font_family: function(setting) { + + update_font_family: function (setting) { NEWSBLUR.assets.preference('story_styling', setting); NEWSBLUR.reader.apply_story_styling(); }, - - change_story_font_size: function(e) { + + change_story_font_size: function (e) { var $target = $(e.target); - + if ($target.hasClass("NB-options-font-size-xs")) { this.update_story_font_size('xs'); } else if ($target.hasClass("NB-options-font-size-s")) { @@ -258,18 +258,18 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-options-font-size-xl")) { this.update_story_font_size('xl'); } - + this.show_correct_options(); }, - - update_story_font_size: function(setting) { + + update_story_font_size: function (setting) { NEWSBLUR.assets.preference('story_size', setting); NEWSBLUR.reader.apply_story_styling(); }, - - change_line_spacing: function(e) { + + change_line_spacing: function (e) { var $target = $(e.currentTarget); - + if ($target.hasClass("NB-options-line-spacing-xs")) { this.update_line_spacing('xs'); } else if ($target.hasClass("NB-options-line-spacing-s")) { @@ -281,18 +281,18 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-options-line-spacing-xl")) { this.update_line_spacing('xl'); } - + this.show_correct_options(); }, - - update_line_spacing: function(setting) { + + update_line_spacing: function (setting) { NEWSBLUR.assets.preference('story_line_spacing', setting); NEWSBLUR.reader.apply_story_styling(); }, - - change_story_titles_pane: function(e) { + + change_story_titles_pane: function (e) { var $target = $(e.currentTarget); - + if ($target.hasClass("NB-options-story-titles-pane-north")) { this.update_story_titles_pane('north'); } else if ($target.hasClass("NB-options-story-titles-pane-west")) { @@ -300,14 +300,14 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-options-story-titles-pane-south")) { this.update_story_titles_pane('south'); } - + this.show_correct_options(); }, - - update_story_titles_pane: function(setting) { + + update_story_titles_pane: function (setting) { var old_anchor = NEWSBLUR.assets.preference('story_pane_anchor'); var pane_size = NEWSBLUR.assets.preference('story_titles_pane_size'); - + if (setting == 'west' && _.contains(['north', 'south'], old_anchor)) { // Moving from top to side pane_size *= 2; @@ -321,9 +321,9 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ NEWSBLUR.app.story_titles.render(); }, - change_story_position: function(e) { + change_story_position: function (e) { var $target = $(e.currentTarget); - + if ($target.hasClass("NB-options-story-position-stretch")) { this.update_story_position('stretch'); } else if ($target.hasClass("NB-options-story-position-left")) { @@ -333,41 +333,41 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-options-story-position-right")) { this.update_story_position('right'); } - + this.show_correct_options(); }, - - update_story_position: function(setting) { + + update_story_position: function (setting) { NEWSBLUR.assets.preference('story_position', setting); NEWSBLUR.reader.add_body_classes(); }, - - change_single_story: function(e) { + + change_single_story: function (e) { var $target = $(e.currentTarget); - + if ($target.hasClass("NB-options-single-story-off")) { this.update_single_story(0); } else if ($target.hasClass("NB-options-single-story-on")) { this.update_single_story(1); } - + this.show_correct_options(); }, - - update_single_story: function(setting) { + + update_single_story: function (setting) { NEWSBLUR.assets.preference('feed_view_single_story', setting); NEWSBLUR.app.story_list.render(); - _.defer(function() { + _.defer(function () { NEWSBLUR.reader.resize_window(); if (NEWSBLUR.reader.active_story) { NEWSBLUR.reader.active_story.set('selected', false).set('selected', true); } }); }, - - change_grid_columns: function(e) { + + change_grid_columns: function (e) { var $target = $(e.currentTarget); - + if ($target.hasClass("NB-options-grid-columns-0")) { this.update_grid_columns(0); } else if ($target.hasClass("NB-options-grid-columns-1")) { @@ -379,22 +379,22 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-options-grid-columns-4")) { this.update_grid_columns(4); } - + this.show_correct_options(); }, - - update_grid_columns: function(setting) { + + update_grid_columns: function (setting) { NEWSBLUR.assets.preference('grid_columns', setting); NEWSBLUR.app.story_list.render(); - _.defer(function() { + _.defer(function () { NEWSBLUR.app.story_titles.override_grid(); NEWSBLUR.reader.resize_window(); }); }, - - change_grid_height: function(e) { + + change_grid_height: function (e) { var $target = $(e.currentTarget); - + if ($target.hasClass("NB-options-grid-height-xs")) { this.update_grid_height('xs'); } else if ($target.hasClass("NB-options-grid-height-s")) { @@ -406,24 +406,24 @@ NEWSBLUR.StoryOptionsPopover = NEWSBLUR.ReaderPopover.extend({ } else if ($target.hasClass("NB-options-grid-height-xl")) { this.update_grid_height('xl'); } - + this.show_correct_options(); }, - - update_grid_height: function(setting) { + + update_grid_height: function (setting) { NEWSBLUR.assets.preference('grid_height', setting); NEWSBLUR.app.story_list.render(); - _.defer(function() { + _.defer(function () { NEWSBLUR.app.story_titles.override_grid(); NEWSBLUR.reader.resize_window(); }); }, - open_premium_modal: function(e) { - this.close(e, function() { - NEWSBLUR.reader.open_feedchooser_modal({'premium_only': true}); - }); + open_premium_modal: function (e) { + this.close(e, function () { + NEWSBLUR.reader.open_feedchooser_modal({ 'premium_only': true }); + }); } - - + + }); diff --git a/media/js/newsblur/views/story_save_view.js b/media/js/newsblur/views/story_save_view.js index 73a0bf43b8..ba3c83f2d7 100644 --- a/media/js/newsblur/views/story_save_view.js +++ b/media/js/newsblur/views/story_save_view.js @@ -1,21 +1,21 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ - + events: { - "click .NB-sideoption-save-populate" : "populate_story_tags", - "keypress .NB-sideoption-save-notes" : "autosize", - "keyup .NB-sideoption-save-notes" : "debounced_save_user_notes", - "change .NB-sideoption-save-notes" : "save_user_notes" + "click .NB-sideoption-save-populate": "populate_story_tags", + "keypress .NB-sideoption-save-notes": "autosize", + "keyup .NB-sideoption-save-notes": "debounced_save_user_notes", + "change .NB-sideoption-save-notes": "save_user_notes" }, - - initialize: function() { + + initialize: function () { this.debounced_save_user_notes = _.debounce(this.save_user_notes, 1000); _.bindAll(this, 'toggle_feed_story_save_dialog', 'save_user_notes', 'autosize', 'debounced_save_user_notes'); this.sideoptions_view = this.options.sideoptions_view; this.model.story_save_view = this; this.model.bind('change:starred', this.toggle_feed_story_save_dialog); }, - - render: function() { + + render: function () { return this.template({ story: this.model, tags: this.model.existing_tags(), @@ -24,7 +24,7 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ profile: NEWSBLUR.assets.user_profile }); }, - + template: _.template('\
NB-active<% } %>">\
\ @@ -48,33 +48,33 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({
\
\ '), - - populate_story_tags: function() { + + populate_story_tags: function () { var $populate = this.$('.NB-sideoption-save-populate'); var $tag_input = this.$('.NB-sideoption-save-tag'); var tags = this.model.get('story_tags'); $populate.fadeOut(500); - _.each(tags, function(tag) { + _.each(tags, function (tag) { $tag_input.tagit('createTag', tag, null, true); }); - - this.toggle_feed_story_save_dialog({resize_open:true}); + + this.toggle_feed_story_save_dialog({ resize_open: true }); this.save_tags(); }, - - toggle_feed_story_save_dialog: function(options) { + + toggle_feed_story_save_dialog: function (options) { options = options || {}; var self = this; var feed_id = this.model.get('story_feed_id'); var $sideoption = this.$('.NB-sideoption.NB-feed-story-save'); var $save_wrapper = this.$('.NB-sideoption-save-wrapper'); var $tag_input = this.$('.NB-sideoption-save-tag'); - + if (options.close || !this.model.get('starred')) { // Close this.is_open = false; - this.resize({close: true}); + this.resize({ close: true }); } else { // Open/resize this.is_open = true; @@ -84,7 +84,7 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ $tag_input.tagit({ fieldName: "tags", availableTags: this.model.all_tags(), - autocomplete: {delay: 0, minLength: 0}, + autocomplete: { delay: 0, minLength: 0 }, showAutocompleteOnFocus: true, createTagOnBlur: false, removeConfirmation: true, @@ -98,17 +98,17 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ singleFieldNode: null, tabIndex: null, - afterTagAdded: function(event, options) { + afterTagAdded: function (event, options) { options = options || {}; if (!options.duringInitialization) { - self.resize({change_tag: true}); + self.resize({ change_tag: true }); self.save_tags(); } }, - afterTagRemoved: function(event, duringInitialization) { + afterTagRemoved: function (event, duringInitialization) { options = options || {}; if (!options.duringInitialization) { - self.resize({change_tag: true}); + self.resize({ change_tag: true }); self.save_tags(); } } @@ -126,19 +126,19 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ queue: false, easing: 'easeInOutQuint', offset: this.model.latest_story_detail_view.$el.height() - - $scroll_container.height() + $scroll_container.height() }); } - + this.resize(options); } }, - autosize: function() { - this.resize({duration: 100, resize_open: true}); + autosize: function () { + this.resize({ duration: 100, resize_open: true }); }, - resize: function(options) { + resize: function (options) { options = options || {}; var $sideoption_container = this.$('.NB-feed-story-sideoptions-container'); var $save_wrapper = this.$('.NB-sideoption-save-wrapper'); @@ -174,7 +174,7 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ 'duration': options.immediate ? 0 : options.duration || 350, 'easing': 'easeInOutQuint', 'queue': false, - 'complete': _.bind(function() { + 'complete': _.bind(function () { if ($tag_input.length == 1) { $tag_input.focus(); } @@ -187,12 +187,12 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ } }, this) }); - - var sideoptions_height = $sideoption_container.height(); - var content_height = $story_content.height(); - var comments_height = $story_comments.height(); - var left_height = content_height + comments_height; - var original_height = $story_content.data('original_height') || content_height; + + var sideoptions_height = $sideoption_container.height(); + var content_height = $story_content.height(); + var comments_height = $story_comments.height(); + var left_height = content_height + comments_height; + var original_height = $story_content.data('original_height') || content_height; if (!NEWSBLUR.reader.flags.narrow_content && !options.close && !options.force && new_sideoptions_height >= original_height) { // Sideoptions too big, embiggen left side @@ -203,7 +203,7 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ 'duration': 350, 'easing': 'easeInOutQuint', 'queue': false, - 'complete': function() { + 'complete': function () { if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } @@ -221,60 +221,60 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({ 'duration': 300, 'easing': 'easeInOutQuint', 'queue': false, - 'complete': function() { + 'complete': function () { if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } } }); } else if (this.sideoptions_view.share_view.is_open && !options.from_share_view) { - this.sideoptions_view.share_view.resize({from_save_view: true}); + this.sideoptions_view.share_view.resize({ from_save_view: true }); } } - + if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } }, - - reset_height: function() { + + reset_height: function () { var $story_content = this.$('.NB-feed-story-content,.NB-story-content'); // Reset story content height to get an accurate height measurement. $story_content.stop(true, true).css('height', 'auto'); $story_content.removeData('original_height'); - this.resize({change_tag: true}); + this.resize({ change_tag: true }); }, - - save_tags: function() { + + save_tags: function () { var $tag_input = this.$('.NB-sideoption-save-tag'); var user_tags = $tag_input.tagit('assignedTags'); this.model.set('user_tags', user_tags); }, - save_user_notes: function(options) { + save_user_notes: function (options) { var $notes = this.$('.NB-sideoption-save-notes'); var $message = this.$('.NB-sideoption-save-message'); var user_notes = $notes.val(); - + if (this.model.get('user_notes') == user_notes) return; console.log('save_user_notes', user_notes); - this.model.set('user_notes', user_notes, {silent: true}); + this.model.set('user_notes', user_notes, { silent: true }); $message.removeClass('NB-active'); if (this.saved_defer) { clearTimeout(this.saved_defer); } - NEWSBLUR.assets.mark_story_as_starred(this.model.id, _.bind(function() { + NEWSBLUR.assets.mark_story_as_starred(this.model.id, _.bind(function () { $message.addClass('NB-active'); if (this.saved_defer) { clearTimeout(this.saved_defer); } - this.saved_defer = _.delay(_.bind(function() { + this.saved_defer = _.delay(_.bind(function () { $message.removeClass('NB-active'); this.saved_defer = null; - }, this), 3000); + }, this), 3000); }, this)); } diff --git a/media/js/newsblur/views/story_share_view.js b/media/js/newsblur/views/story_share_view.js index 1500257257..26ed8146ff 100644 --- a/media/js/newsblur/views/story_share_view.js +++ b/media/js/newsblur/views/story_share_view.js @@ -1,22 +1,22 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ - + events: { - "click .NB-feed-story-share" : "toggle_feed_story_share_dialog", - "click .NB-sideoption-share-save" : "mark_story_as_shared", - "click .NB-sideoption-share-unshare" : "mark_story_as_unshared", - "click .NB-sideoption-share-crosspost-twitter" : "toggle_twitter", - "click .NB-sideoption-share-crosspost-facebook" : "toggle_facebook", - "keypress .NB-sideoption-share-comments" : "autosize", - "keyup .NB-sideoption-share-comments" : "update_share_button_label", - "keydown .NB-sideoption-share-comments" : "maybe_close" + "click .NB-feed-story-share": "toggle_feed_story_share_dialog", + "click .NB-sideoption-share-save": "mark_story_as_shared", + "click .NB-sideoption-share-unshare": "mark_story_as_unshared", + "click .NB-sideoption-share-crosspost-twitter": "toggle_twitter", + "click .NB-sideoption-share-crosspost-facebook": "toggle_facebook", + "keypress .NB-sideoption-share-comments": "autosize", + "keyup .NB-sideoption-share-comments": "update_share_button_label", + "keydown .NB-sideoption-share-comments": "maybe_close" }, - - initialize: function() { + + initialize: function () { this.sideoptions_view = this.options.sideoptions_view; this.model.story_share_view = this; }, - - render: function() { + + render: function () { this.$el.html(this.template({ story: this.model, social_services: NEWSBLUR.assets.social_services, @@ -25,7 +25,7 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ return this; }, - + template: _.template('\
\
\ @@ -48,8 +48,8 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
\
\ '), - - toggle_feed_story_share_dialog: function(options) { + + toggle_feed_story_share_dialog: function (options) { options = options || {}; var feed_id = this.model.get('story_feed_id'); var $sideoption = this.$('.NB-sideoption.NB-feed-story-share'); @@ -62,12 +62,12 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ var $unshare_button = this.$('.NB-sideoption-share-unshare'); var $twitter_button = this.$('.NB-sideoption-share-crosspost-twitter'); var $facebook_button = this.$('.NB-sideoption-share-crosspost-facebook'); - + if (options.close || ($sideoption.hasClass('NB-active') && !options.resize_open)) { // Close this.is_open = false; - this.resize({close: true}); + this.resize({ close: true }); NEWSBLUR.reader.blur_to_page(); } else { // Open/resize @@ -92,30 +92,30 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ queue: false, easing: 'easeInOutQuint', offset: this.model.latest_story_detail_view.$el.height() - - $scroll_container.height() + $scroll_container.height() }); } this.resize(options); - var share = _.bind(function(e) { + var share = _.bind(function (e) { e.preventDefault(); - this.mark_story_as_shared({'source': 'sideoption'}); + this.mark_story_as_shared({ 'source': 'sideoption' }); }, this); var $comments = $('.NB-sideoption-share-comments', $share); $comments.unbind('keydown.story_share').unbind('keypress.story_share') - .bind('keydown.story_share', 'ctrl+return', share) - .bind('keydown.story_share', 'meta+return', share) - .bind('keypress.story_share', 'esc', blur); + .bind('keydown.story_share', 'ctrl+return', share) + .bind('keydown.story_share', 'meta+return', share) + .bind('keypress.story_share', 'esc', blur); $comments.focus(); } }, - - blur: function() { + + blur: function () { NEWSBLUR.reader.blur_to_page(); }, - - resize: function(options) { + + resize: function (options) { options = options || {}; var $sideoption_container = this.$('.NB-feed-story-sideoptions-container'); var $share_wrapper = this.$('.NB-sideoption-share-wrapper'); @@ -134,7 +134,7 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ var sideoption_content_height = $share_clone.height(); $share_clone.remove(); var new_sideoptions_height = $sideoption_container.height() - $share_wrapper.height() + sideoption_content_height; - + if (!options.close) { $share_wrapper.addClass('NB-active'); $sideoption.addClass('NB-active'); @@ -146,7 +146,7 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ 'duration': options.immediate ? 0 : options.duration || 350, 'easing': 'easeInOutQuint', 'queue': false, - 'complete': _.bind(function() { + 'complete': _.bind(function () { if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } @@ -156,13 +156,13 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ } }, this) }); - - var sideoptions_height = $sideoption_container.height(); - var content_height = $story_content.height(); - var comments_height = $story_comments.height(); - var left_height = content_height + comments_height; - var original_height = $story_content.data('original_height') || content_height; - + + var sideoptions_height = $sideoption_container.height(); + var content_height = $story_content.height(); + var comments_height = $story_comments.height(); + var left_height = content_height + comments_height; + var original_height = $story_content.data('original_height') || content_height; + if (!NEWSBLUR.reader.flags.narrow_content && !options.close && new_sideoptions_height >= original_height) { // Sideoptions too big, embiggen left side @@ -172,7 +172,7 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ 'duration': 350, 'easing': 'easeInOutQuint', 'queue': false, - 'complete': function() { + 'complete': function () { if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } @@ -190,26 +190,26 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ 'duration': 300, 'easing': 'easeInOutQuint', 'queue': false, - 'complete': function() { + 'complete': function () { if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } } }); - } else if (this.sideoptions_view && - this.sideoptions_view.save_view && - this.sideoptions_view.save_view.is_open && - !options.from_save_view) { - this.sideoptions_view.save_view.resize({from_share_view: true}); + } else if (this.sideoptions_view && + this.sideoptions_view.save_view && + this.sideoptions_view.save_view.is_open && + !options.from_save_view) { + this.sideoptions_view.save_view.resize({ from_share_view: true }); } } - + if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } }, - - mark_story_as_shared: function(options) { + + mark_story_as_shared: function (options) { options = options || {}; var $share_button = this.$('.NB-sideoption-share-save'); var $share_button_menu = $('.NB-menu-manage .NB-menu-manage-story-share-save'); @@ -232,32 +232,32 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ $twitter_button.hasClass('NB-active') && 'twitter', $facebook_button.hasClass('NB-active') && 'facebook', ]); - + $share_button.addClass('NB-saving').addClass('NB-disabled').text('Sharing...'); $share_button_menu.addClass('NB-saving').addClass('NB-disabled').text('Sharing...'); - + var data = { - story_id: this.model.id, - story_feed_id: this.model.get('story_feed_id'), + story_id: this.model.id, + story_feed_id: this.model.get('story_feed_id'), comments: comments, source_user_id: source_user_id, relative_user_id: NEWSBLUR.Globals.blurblog_user_id, post_to_services: post_to_services }; - NEWSBLUR.assets.mark_story_as_shared(data, _.bind(this.post_share_story, this, true), _.bind(function(data) { + NEWSBLUR.assets.mark_story_as_shared(data, _.bind(this.post_share_story, this, true), _.bind(function (data) { this.post_share_error(data, true); }, this)); - + if (NEWSBLUR.reader) { NEWSBLUR.reader.blur_to_page(); } - + if (_.contains(post_to_services, 'facebook')) { NEWSBLUR.reader.open_facebook_modal(); } }, - - mark_story_as_unshared: function(options) { + + mark_story_as_unshared: function (options) { options = options || {}; var $unshare_button = this.$('.NB-sideoption-share-unshare'); var $unshare_button_menu = $('.NB-menu-manage-story-share-unshare'); @@ -265,45 +265,45 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ $unshare_button.addClass('NB-saving').addClass('NB-disabled').text('Deleting...'); var params = { - story_id: this.model.id, + story_id: this.model.id, story_feed_id: this.model.get('story_feed_id'), relative_user_id: NEWSBLUR.Globals.blurblog_user_id }; - NEWSBLUR.assets.mark_story_as_unshared(params, _.bind(this.post_share_story, this, false), _.bind(function(data) { + NEWSBLUR.assets.mark_story_as_unshared(params, _.bind(this.post_share_story, this, false), _.bind(function (data) { this.post_share_error(data, false); }, this)); - + if (NEWSBLUR.reader) { NEWSBLUR.reader.blur_to_page(); } }, - - post_share_story: function(shared, data) { + + post_share_story: function (shared, data) { this.model.set("shared", shared); this.model.trigger('change:comments', data); - + var $share_star = this.model.story_title_view && this.model.story_title_view.$('.NB-storytitles-share'); var $share_button = this.$('.NB-sideoption-share-save'); var $unshare_button = this.$('.NB-sideoption-share-unshare'); var $share_sideoption = this.$('.NB-feed-story-share .NB-sideoption-title'); var $comments_sideoptions = this.$('.NB-sideoption-share-comments'); var shared_text = this.model.get('shared') ? 'Shared' : 'Unshared'; - - this.toggle_feed_story_share_dialog({'close': true}); + + this.toggle_feed_story_share_dialog({ 'close': true }); $share_button.removeClass('NB-saving').removeClass('NB-disabled').text('Share'); $unshare_button.removeClass('NB-saving').removeClass('NB-disabled').text('Delete Share'); $share_sideoption.text(shared_text).closest('.NB-sideoption'); $comments_sideoptions.val(this.model.get('shared_comments')); - + if (this.options.on_social_page) { this.model.social_page_story.$el.toggleClass('NB-story-shared', this.model.get('shared')); this.model.social_page_story.replace_shares_and_comments(data); } else { NEWSBLUR.reader.hide_confirm_story_share_menu_item(true); } - + if (this.model.get('shared') && $share_star) { - $share_star.attr({'title': shared_text + '!'}); + $share_star.attr({ 'title': shared_text + '!' }); $share_star.tipsy({ gravity: 'sw', fade: true, @@ -314,89 +314,89 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ tipsy.enable(); tipsy.show(); - _.delay(function() { + _.delay(function () { if (tipsy.enabled) { tipsy.hide(); tipsy.disable(); } }, 850); } - + if (NEWSBLUR.app.story_list) { NEWSBLUR.app.story_list.fetch_story_locations_in_feed_view(); } - - + + }, - - post_share_error: function(data, shared) { + + post_share_error: function (data, shared) { var $share_button = this.$('.NB-sideoption-share-save'); var $unshare_button = this.$('.NB-sideoption-share-unshare'); var $share_button_menu = $('.NB-menu-manage .NB-menu-manage-story-share-save'); var message = data && data.message || ("Sorry, this story could not be " + (shared ? "" : "un") + "shared. Probably Adblock."); - + if (!NEWSBLUR.Globals.is_authenticated) { message = "You need to be logged in to share a story."; } var $error = $.make('div', { className: 'NB-error' }, message); - + $share_button.removeClass('NB-saving').removeClass('NB-disabled').text('Share'); $unshare_button.removeClass('NB-saving').removeClass('NB-disabled').text('Delete Share'); $share_button.siblings('.NB-error').remove(); $share_button.after($error); - + if ($share_button_menu.length) { $share_button_menu.removeClass('NB-disabled').text('Share'); $share_button_menu.siblings('.NB-error').remove(); $share_button_menu.after($error.clone()); } - this.toggle_feed_story_share_dialog({'resize_open': true}); + this.toggle_feed_story_share_dialog({ 'resize_open': true }); NEWSBLUR.log(["post_share_error", data, shared, message, $share_button, $unshare_button, $share_button_menu, $error]); }, - - autosize: function() { - this.resize({duration: 100}); + + autosize: function () { + this.resize({ duration: 100 }); }, - - update_share_button_label: function() { + + update_share_button_label: function () { var $share = this.$('.NB-sideoption-share'); var $comment_input = this.$('.NB-sideoption-share-comments'); var $share_button = this.$('.NB-sideoption-share-save,.NB-menu-manage-story-share-save'); - + $share_button.removeClass('NB-saving').removeClass('NB-disabled'); - + if (!_.string.isBlank($comment_input.val())) { $share_button.text('Share with comment'); } else { $share_button.text('Share'); } }, - - count_selected_words_when_sharing_story: function($feed_story) { + + count_selected_words_when_sharing_story: function ($feed_story) { var $wordcount = $('.NB-sideoption-share-wordcount', $feed_story); - + }, - - toggle_twitter: function() { + + toggle_twitter: function () { var $twitter_button = this.$('.NB-sideoption-share-crosspost-twitter'); - + $twitter_button.toggleClass('NB-active', !$twitter_button.hasClass('NB-active')); }, - - toggle_facebook: function() { + + toggle_facebook: function () { var $facebook_button = this.$('.NB-sideoption-share-crosspost-facebook'); - + $facebook_button.toggleClass('NB-active', !$facebook_button.hasClass('NB-active')); }, - - maybe_close: function(e) { + + maybe_close: function (e) { if (e.which == 27) { e.preventDefault(); e.stopPropagation(); - this.toggle_feed_story_share_dialog({close: true}); + this.toggle_feed_story_share_dialog({ close: true }); return false; } } - - -}); \ No newline at end of file + + +}); diff --git a/media/js/newsblur/views/story_sideoptions_view.js b/media/js/newsblur/views/story_sideoptions_view.js index c858677e86..97125858a8 100644 --- a/media/js/newsblur/views/story_sideoptions_view.js +++ b/media/js/newsblur/views/story_sideoptions_view.js @@ -1,16 +1,16 @@ NEWSBLUR.Views.StorySideoptionsView = Backbone.View.extend({ - - initialize: function() { + + initialize: function () { this.save_view = new NEWSBLUR.Views.StorySaveView({ - model: this.model, + model: this.model, el: this.el, sideoptions_view: this }); this.share_view = new NEWSBLUR.Views.StoryShareView({ - model: this.model, + model: this.model, el: this.el, sideoptions_view: this }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/views/story_tab_view.js b/media/js/newsblur/views/story_tab_view.js index ec7c45be93..9fab54686c 100644 --- a/media/js/newsblur/views/story_tab_view.js +++ b/media/js/newsblur/views/story_tab_view.js @@ -1,84 +1,84 @@ NEWSBLUR.Views.StoryTabView = Backbone.View.extend({ - + flags: {}, - - initialize: function() { + + initialize: function () { this.setElement(NEWSBLUR.reader.$s.$story_view); this.$iframe = NEWSBLUR.reader.$s.$story_iframe; this.collection.bind('change:selected', this.select_story, this); - this.$iframe.on('load', _.bind(function() { + this.$iframe.on('load', _.bind(function () { this.ensure_proxied_story(); }, this)); }, - + // =========== // = Actions = // =========== - - prepare_story: function(story, is_temporary) { + + prepare_story: function (story, is_temporary) { if (!story) story = NEWSBLUR.reader.active_story; if (!story) return; var feed = NEWSBLUR.assets.get_feed(story.get('story_feed_id')); - if ((feed && feed.get('disabled_page')) || + if ((feed && feed.get('disabled_page')) || NEWSBLUR.utils.is_url_iframe_buster(story.get('story_permalink'))) { if (!is_temporary) { - NEWSBLUR.reader.switch_taskbar_view('text', {skip_save_type: 'story'}); + NEWSBLUR.reader.switch_taskbar_view('text', { skip_save_type: 'story' }); NEWSBLUR.app.taskbar_info.show_stories_error({}, "Sorry, the original story
could not be proxied."); } } else { - NEWSBLUR.reader.switch_taskbar_view('story', {skip_save_type: is_temporary ? 'story' : false}); + NEWSBLUR.reader.switch_taskbar_view('story', { skip_save_type: is_temporary ? 'story' : false }); } }, - - open_story: function(story) { + + open_story: function (story) { if (!story) story = NEWSBLUR.reader.active_story; if (!story) return; - + var permalink = story.get('story_permalink'); // if (window.location.protocol == 'https:' && !_.string.startsWith(permalink, 'https')) { - this.flags.proxied_https = true; - this.load_original_story_page(story); + this.flags.proxied_https = true; + this.load_original_story_page(story); // } else { // this.flags.proxied_https = false; // this.load_story_iframe(story); // } }, - - load_original_story_page: function(story) { + + load_original_story_page: function (story) { this.$(".NB-story-list-empty").remove(); this.show_loading(); - var url = '/rss_feeds/original_story?story_hash='+story.get('story_hash'); + var url = '/rss_feeds/original_story?story_hash=' + story.get('story_hash'); console.log(['url', url]); if (!_.string.contains(this.$iframe.attr('src'), url)) { this.unload_story_iframe(); - + NEWSBLUR.reader.flags.iframe_scroll_snap_back_prepared = true; - this.$iframe.removeAttr('src').attr({src: url}); + this.$iframe.removeAttr('src').attr({ src: url }); } }, - - load_story_iframe: function(story) { + + load_story_iframe: function (story) { story = story || NEWSBLUR.reader.active_story; if (!story) return; - + this.$(".NB-story-list-empty").remove(); if (this.$iframe.attr('src') != story.get('story_permalink')) { this.unload_story_iframe(); - + NEWSBLUR.reader.flags.iframe_scroll_snap_back_prepared = true; - this.$iframe.removeAttr('src').attr({src: story.get('story_permalink')}); + this.$iframe.removeAttr('src').attr({ src: story.get('story_permalink') }); } }, - - unload_story_iframe: function() { + + unload_story_iframe: function () { NEWSBLUR.app.taskbar_info.hide_stories_error(); - + this.$iframe.empty(); this.$iframe.removeAttr('src');//.attr({src: 'about:blank'}); }, - - show_explainer_single_story_mode: function() { + + show_explainer_single_story_mode: function () { var $empty = $.make("div", { className: "NB-story-list-empty" }, [ $.make('div', { className: 'NB-world' }), 'Select a story to read' @@ -88,12 +88,12 @@ NEWSBLUR.Views.StoryTabView = Backbone.View.extend({ this.$el.append($empty); }, - show_loading: function() { + show_loading: function () { NEWSBLUR.app.taskbar_info.hide_stories_error(); NEWSBLUR.app.taskbar_info.show_stories_progress_bar(10, "Fetching story"); }, - - ensure_proxied_story: function() { + + ensure_proxied_story: function () { NEWSBLUR.app.taskbar_info.hide_stories_progress_bar(); if (this.$iframe.attr('src') == 'about:blank') { console.log(['Blank iframe, ignoring']); @@ -115,12 +115,12 @@ NEWSBLUR.Views.StoryTabView = Backbone.View.extend({ // ========== // = Events = // ========== - - select_story: function(story, selected) { + + select_story: function (story, selected) { if (selected && NEWSBLUR.reader.story_view == 'story') { this.prepare_story(story); this.open_story(story); } } - + }); diff --git a/media/js/newsblur/views/story_title_view.js b/media/js/newsblur/views/story_title_view.js index 24c0078439..a32760f445 100644 --- a/media/js/newsblur/views/story_title_view.js +++ b/media/js/newsblur/views/story_title_view.js @@ -1,19 +1,19 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ - + className: 'NB-story-title-container', - + events: { - "dblclick .NB-story-title" : "open_story_in_story_view", - "click .NB-story-title" : "select_story", - "contextmenu .NB-story-title" : "show_manage_menu_rightclick", - "click .NB-story-manage-icon" : "show_manage_menu", + "dblclick .NB-story-title": "open_story_in_story_view", + "click .NB-story-title": "select_story", + "contextmenu .NB-story-title": "show_manage_menu_rightclick", + "click .NB-story-manage-icon": "show_manage_menu", "click .NB-storytitles-sentiment": "show_manage_menu", - "click .NB-storytitles-shares" : "select_story_shared", - "mouseenter .NB-story-title" : "mouseenter_manage_icon", - "mouseleave .NB-story-title" : "mouseleave_manage_icon" + "click .NB-storytitles-shares": "select_story_shared", + "mouseenter .NB-story-title": "mouseenter_manage_icon", + "mouseleave .NB-story-title": "mouseleave_manage_icon" }, - - initialize: function() { + + initialize: function () { this.model.bind('change', this.toggle_classes, this); this.model.bind('change:read_status', this.toggle_read_status, this); this.model.bind('change:selected', this.switch_story_view, this); @@ -23,12 +23,12 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ this.collection.bind('render:intelligence', this.render_intelligence, this); this.model.story_title_view = this; }, - - render: function() { + + render: function () { var template_name = 'template'; var story_layout = this.options.override_layout || NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); var pane_anchor = this.options.override_layout ? "west" : NEWSBLUR.assets.preference('story_pane_anchor'); - + if (this.options.is_list) template_name = "list_template"; if (story_layout == 'split' && _.contains(['north', 'south'], pane_anchor)) template_name = "list_template";; if (this.options.is_grid) template_name = "grid_template"; @@ -36,16 +36,16 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ if (this.options.is_list || this.options.is_grid || this.options.is_magazine) { if (this.model.get('selected')) template_name = "list_template"; } - + // console.log(['render story title', template_name, this.$el[0], this.options.is_grid, this.show_image_preview(), this.options.override_layout, NEWSBLUR.assets.get_feed(this.model.get('story_feed_id'))]); this.$el.html(this[template_name]({ - story : this.model, - feed : (this.options.override_layout == 'split' || - NEWSBLUR.reader.flags.river_view || - NEWSBLUR.reader.flags.social_view) && - NEWSBLUR.assets.get_feed(this.model.get('story_feed_id')), - options : this.options, - show_content_preview : this.show_content_preview(template_name), + story: this.model, + feed: (this.options.override_layout == 'split' || + NEWSBLUR.reader.flags.river_view || + NEWSBLUR.reader.flags.social_view) && + NEWSBLUR.assets.get_feed(this.model.get('story_feed_id')), + options: this.options, + show_content_preview: this.show_content_preview(template_name), show_image_preview: this.show_image_preview(), show_inline_author: story_layout == "list", pane_anchor: this.options.override_layout ? "west" : NEWSBLUR.assets.preference('story_pane_anchor') @@ -58,10 +58,10 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ if (this.options.is_grid) this.watch_grid_image(); if (_.contains(['list', 'magazine'], story_layout) && this.show_image_preview()) this.watch_grid_image(); if (_.contains(['split'], story_layout) && this.show_image_preview() && NEWSBLUR.assets.preference('feed_view_single_story')) this.watch_grid_image(); - + return this; }, - + template: _.template('\
\
\ @@ -110,7 +110,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
\
\ '), - + list_template: _.template('\
\
\ @@ -153,7 +153,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
\
\ '), - + grid_template: _.template('\
\
\ @@ -196,7 +196,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
\
\ '), - + magazine_template: _.template('\
\
\ @@ -239,8 +239,8 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
\
\ '), - - render_inline_story_detail: function(temporary_text) { + + render_inline_story_detail: function (temporary_text) { // console.log(['render_inline_story_detail', this.model.get('story_title')]); if (NEWSBLUR.reader.story_view == 'text' || temporary_text) { this.text_view = new NEWSBLUR.Views.TextTabView({ @@ -265,12 +265,12 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ this.story_detail.setElement(this.story_detail.$el); } }, - + render_magazine_story_detail: function () { this.render_inline_story_detail(); }, - - destroy: function() { + + destroy: function () { // console.log(["destroy story title", this.model.get('story_title')]); if (this.text_view) { this.text_view.destroy(); @@ -282,8 +282,8 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ this.collection.unbind(null, null, this); this.remove(); }, - - destroy_inline_story_detail: function() { + + destroy_inline_story_detail: function () { if (this.story_detail) { this.story_detail.destroy(); } @@ -292,18 +292,18 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ } // this.$(".NB-story-detail").empty(); }, - - collapse_story: function() { + + collapse_story: function () { this.model.set('selected', false); NEWSBLUR.app.story_titles.fill_out(); }, - - render_intelligence: function(options) { + + render_intelligence: function (options) { options = options || {}; var score = this.model.score(); var unread_view = NEWSBLUR.reader.get_unread_view_score(); // console.log(['render_intelligence', score, unread_view, this.model.get('visible'), this.model.get('story_title')]); - + if (score >= unread_view) { this.$el.removeClass('NB-hidden'); this.$st.removeClass('NB-hidden'); @@ -314,8 +314,8 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ this.model.set('visible', false); } }, - - show_content_preview: function(template_name) { + + show_content_preview: function (template_name) { var preference = NEWSBLUR.assets.preference('show_content_preview'); if (!preference) return preference; var max_length = preference == 'small' ? 300 : preference == 'medium' ? 600 : 1000; @@ -327,43 +327,43 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ } var pruned_description = this.model.content_preview('story_content', max_length) || " "; var pruned_title = this.model.content_preview('story_title'); - + if (pruned_title.substr(0, 30) == pruned_description.substr(0, 30)) return false; if (pruned_description.length < 30) return false; return pruned_description; }, - + show_image_preview: function () { var show_image_preview = NEWSBLUR.assets.preference('image_preview'); if (!show_image_preview || show_image_preview == "none") { return false; } - - var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + + var story_layout = this.options.override_layout || + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); var pane_anchor = this.options.override_layout ? "west" : NEWSBLUR.assets.preference('story_pane_anchor'); if (_.contains(['list', 'grid', 'magazine'], story_layout)) return true; if (story_layout == 'split' && _.contains(['north', 'south'], pane_anchor)) return true; return !!this.model.image_url(); }, - + // ============ // = Bindings = // ============ - - color_feedbar: function() { + + color_feedbar: function () { var $inner = this.$st.find(".NB-storytitles-feed-border-inner"); var $outer = this.$st.find(".NB-storytitles-feed-border-outer"); var feed = NEWSBLUR.assets.get_feed(this.model.get('story_feed_id')); if (!feed) return; - + $inner.css('background-color', '#' + feed.get('favicon_fade')); $outer.css('background-color', '#' + feed.get('favicon_color')); }, - - found_largest_image: function(image_url) { + + found_largest_image: function (image_url) { if (this.load_youtube_embeds()) { return; } @@ -373,8 +373,8 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ 'display': 'block' }); }, - - watch_grid_image: function(index) { + + watch_grid_image: function (index) { if (!index) index = 0; var self = this; if (!index && this.load_youtube_embeds()) { @@ -387,7 +387,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ // console.log(["watch_grid_image", index, this.model.image_url(index), this.model.get('story_title').substr(0, 30)]); // this.model == NEWSBLUR.assets.stories.at(5) && console.log(["Watching images", index, this.model.image_url(index), this.model.get('story_title').substr(0, 30)]); var $img = $(""); - $img.imagesLoaded(function() { + $img.imagesLoaded(function () { // console.log(["Loaded", index, $img[0].width, $img.attr('src'), self.model.get('story_title').substr(0, 30)]); if ($img[0].width > 60 && $img[0].height > 60) { self.$(".NB-storytitles-story-image").css({ @@ -395,15 +395,15 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ 'display': 'block' }); } else { - self.watch_grid_image(index+1); + self.watch_grid_image(index + 1); } - }).attr('src', this.model.image_url(index)).each(function() { + }).attr('src', this.model.image_url(index)).each(function () { // fail-safe for cached images which sometimes don't trigger "load" events if (this.complete) $(this).trigger('load'); }); }, - - select_regex: function(query, url) { + + select_regex: function (query, url) { if (url == null) { return; } @@ -414,17 +414,17 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ return; } }, - - load_youtube_embeds: function() { + + load_youtube_embeds: function () { var text = this.model.get('story_content'); var g = /youtube\.com\/embed\/([A-Za-z0-9\-_]+)/gi; var f = /youtube\.com\/v\/([A-Za-z0-9\-_]+)/gi; var e = /ytimg\.com\/vi\/([A-Za-z0-9\-_]+)/gi; var d = /youtube\.com\/watch\?v=([A-Za-z0-9\-_]+)/gi; - var i = this.select_regex(g, text) || - this.select_regex(f, text) || - this.select_regex(e, text) || - this.select_regex(d, text); + var i = this.select_regex(g, text) || + this.select_regex(f, text) || + this.select_regex(e, text) || + this.select_regex(d, text); if (i) { // this.$(".NB-storytitles-story-image").css({ // 'display': 'block', @@ -433,14 +433,14 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ return true; } }, - - toggle_classes: function() { + + toggle_classes: function () { var changes = this.model.changedAttributes(); - - if (changes && _.all(_.keys(changes), function(change) { + + if (changes && _.all(_.keys(changes), function (change) { return _.contains(['intelligence', 'read_status', 'selected'], change); })) return; - + var story = this.model; var unread_view = NEWSBLUR.reader.get_unread_view_score(); @@ -448,7 +448,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ this.$st.toggleClass('NB-story-shared', !!story.get('shared')); this.toggle_intelligence(); this.render_intelligence(); - + if (NEWSBLUR.assets.preference('show_tooltips')) { this.$('.NB-story-sentiment').tipsy({ delayIn: 375, @@ -456,22 +456,22 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ }); } }, - - toggle_intelligence: function() { + + toggle_intelligence: function () { var score = this.model.score(); this.$st.removeClass('NB-story-negative NB-story-neutral NB-story-postiive') - .addClass('NB-story-'+this.model.score_name(score)); + .addClass('NB-story-' + this.model.score_name(score)); }, - - toggle_read_status: function(model, read_status, options) { + + toggle_read_status: function (model, read_status, options) { options = options || {}; this.$st.toggleClass('read', !!this.model.get('read_status')); - + if (options.error_marking_unread) { var pane_alignment = NEWSBLUR.assets.preference('story_pane_anchor'); var $star = this.$('.NB-storytitles-sentiment'); $star.stop().css('opacity', null); - $star.attr({'title': options.message || 'Failed to mark as unread'}); + $star.attr({ 'title': options.message || 'Failed to mark as unread' }); $star.tipsy({ gravity: pane_alignment == 'north' ? 'nw' : 'sw', fade: true, @@ -479,10 +479,10 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ offsetOpposite: -1 }); var tipsy = $star.data('tipsy'); - _.defer(function() { + _.defer(function () { tipsy.enable(); tipsy.show(); - _.delay(function() { + _.delay(function () { if (tipsy.enabled) { tipsy.hide(); tipsy.disable(); @@ -491,15 +491,15 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ }); } }, - + toggle_selected: function (model, selected, options) { var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); if (this.options.is_grid) this.render(); - + this.$st.toggleClass('NB-selected', !!this.model.get('selected')); this.$el.toggleClass('NB-selected', !!this.model.get('selected')); - + if (!!this.model.get('selected')) { if (_.contains(['list', 'grid'], story_layout)) { this.render_inline_story_detail(); @@ -513,19 +513,19 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ this.destroy_inline_story_detail(); } }, - - toggle_starred: function() { - var story_titles_visible = _.contains(['split', 'full'], this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')); + + toggle_starred: function () { + var story_titles_visible = _.contains(['split', 'full'], this.options.override_layout || + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')); var pane_alignment = NEWSBLUR.assets.preference('story_pane_anchor'); var $star = this.$('.NB-storytitles-star'); - + if (story_titles_visible) { NEWSBLUR.app.story_titles.scroll_to_selected_story(this.model); } - + if (this.model.get('starred')) { - $star.attr({'title': 'Saved!'}); + $star.attr({ 'title': 'Saved!' }); $star.tipsy({ gravity: pane_alignment == 'north' ? 'nw' : 'sw', fade: true, @@ -533,7 +533,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ offsetOpposite: -1 }); var tipsy = $star.data('tipsy'); - _.defer(function() { + _.defer(function () { tipsy.enable(); tipsy.show(); }); @@ -543,7 +543,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ }, { 'duration': 850, 'queue': false, - 'complete': function() { + 'complete': function () { if (tipsy.enabled) { tipsy.hide(); tipsy.disable(); @@ -551,11 +551,11 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ } }); } else { - this.$st.one('mouseout', _.bind(function() { + this.$st.one('mouseout', _.bind(function () { this.$st.removeClass('NB-unstarred'); }, this)); - $star.attr({'title': 'Removed'}); - + $star.attr({ 'title': 'Removed' }); + $star.tipsy({ gravity: pane_alignment == 'north' ? 'nw' : 'sw', fade: true, @@ -566,53 +566,53 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ tipsy.enable(); tipsy.show(); - _.delay(function() { + _.delay(function () { if (tipsy.enabled) { tipsy.hide(); tipsy.disable(); } }, 850); - + } }, - + // ========== // = Events = // ========== - - select_story: function(e) { + + select_story: function (e) { if (NEWSBLUR.hotkeys.shift) return; - + e.preventDefault(); e.stopPropagation(); if (e.which == 1 && $('.NB-menu-manage-container:visible').length) return; - + if (this.options.on_dashboard) { // console.log(['clicked story', this.model]); this.options.on_dashboard.open_story(this.model); return; } - - if (_.contains(['list', 'grid', 'magazine'], this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && + + if (_.contains(['list', 'grid', 'magazine'], this.options.override_layout || + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')) && this.model.get('selected')) { this.collapse_story(); } else { - this.model.set('selected', true, {'click_on_story_title': true}); + this.model.set('selected', true, { 'click_on_story_title': true }); } if (NEWSBLUR.hotkeys.command) { this.model.open_story_in_new_tab(true); } }, - - select_story_shared: function(e) { + + select_story_shared: function (e) { e.preventDefault(); e.stopPropagation(); - - this.model.set('selected', true, {'click_on_story_title': true}); + + this.model.set('selected', true, { 'click_on_story_title': true }); if (NEWSBLUR.reader.story_view == 'page') { - NEWSBLUR.reader.switch_taskbar_view('feed', {skip_save_type: 'page'}); + NEWSBLUR.reader.switch_taskbar_view('feed', { skip_save_type: 'page' }); } NEWSBLUR.app.story_list.scroll_to_selected_story(this.model, { @@ -620,20 +620,20 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ scroll_offset: -50 }); }, - - show_manage_menu_rightclick: function(e) { + + show_manage_menu_rightclick: function (e) { if (!NEWSBLUR.assets.preference('show_contextmenus')) return; - + return this.show_manage_menu(e); }, - - show_manage_menu: function(e) { + + show_manage_menu: function (e) { e.preventDefault(); e.stopPropagation(); if (this.options.on_dashboard) { return this.select_story(e); } - + // NEWSBLUR.log(["showing manage menu", this.model.is_social() ? 'socialfeed' : 'feed', $(this.el), this]); NEWSBLUR.reader.show_manage_menu('story', this.$st, { story_id: this.model.id, @@ -642,20 +642,20 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ }); return false; }, - - mouseenter_manage_icon: function() { + + mouseenter_manage_icon: function () { var menu_height = 270; // console.log(["mouseenter_manage_icon", this.$el.offset().top, $(window).height(), menu_height]); if (this.$el.offset().top > $(window).height() - menu_height) { this.$st.addClass('NB-hover-inverse'); } }, - - mouseleave_manage_icon: function() { + + mouseleave_manage_icon: function () { this.$st.removeClass('NB-hover-inverse'); }, - - open_story_in_story_view: function(e) { + + open_story_in_story_view: function (e) { e.preventDefault(); e.stopPropagation(); if (this.options.on_dashboard) { @@ -666,8 +666,8 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({ NEWSBLUR.app.story_tab_view.open_story(this.model); return false; }, - - switch_story_view: function(story, selected, options) { + + switch_story_view: function (story, selected, options) { // console.log(['switch_story_view title', story, selected, options]); if (selected && !options.selected_by_scrolling) { var story_view = NEWSBLUR.assets.view_setting(story.get('story_feed_id'), 'view'); diff --git a/media/js/newsblur/views/story_titles_header_view.js b/media/js/newsblur/views/story_titles_header_view.js index 1a806e58ea..f3d5a1120d 100644 --- a/media/js/newsblur/views/story_titles_header_view.js +++ b/media/js/newsblur/views/story_titles_header_view.js @@ -1,40 +1,40 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({ - + el: ".NB-story-titles-header", - + options: { 'layout': 'split' }, - + events: { - "click .NB-feedbar-options" : "open_options_popover", - "click .NB-feedbar-mark-feed-read" : "mark_folder_as_read", - "click .NB-feedbar-mark-feed-read-expand" : "expand_mark_read", - "click .NB-feedbar-mark-feed-read-time" : "mark_folder_as_read_days", - "click .NB-story-title-indicator" : "show_hidden_story_titles" + "click .NB-feedbar-options": "open_options_popover", + "click .NB-feedbar-mark-feed-read": "mark_folder_as_read", + "click .NB-feedbar-mark-feed-read-expand": "expand_mark_read", + "click .NB-feedbar-mark-feed-read-time": "mark_folder_as_read_days", + "click .NB-story-title-indicator": "show_hidden_story_titles" }, - - initialize: function() { + + initialize: function () { this.$story_titles_feedbar = $(".NB-story-titles-header"); this.$feed_view_feedbar = $(".NB-feed-story-view-header"); - + // if (this.options.layout == 'split' || this.options.layout == 'list') { - this.$story_titles_feedbar.show(); - this.$feed_view_feedbar.hide(); + this.$story_titles_feedbar.show(); + this.$feed_view_feedbar.hide(); // } else if (this.options.layout == 'full') { // this.$story_titles_feedbar.hide(); // this.$feed_view_feedbar.show(); // this.setElement(this.$feed_view_feedbar); // } }, - - render: function(options) { + + render: function (options) { var $view; this.options = _.extend({}, this.options, options); - this.showing_fake_folder = NEWSBLUR.reader.flags['river_view'] && - NEWSBLUR.reader.active_folder && + this.showing_fake_folder = NEWSBLUR.reader.flags['river_view'] && + NEWSBLUR.reader.active_folder && (NEWSBLUR.reader.active_folder.get('fake') || !NEWSBLUR.reader.active_folder.get('folder_title')); - + if (NEWSBLUR.reader.flags['starred_view']) { $view = $(_.template('\
\ @@ -114,87 +114,87 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({ infrequent_stories: NEWSBLUR.reader.active_feed == "river:infrequent", infrequent_freq: NEWSBLUR.assets.preference('infrequent_stories_per_month'), show_options: !NEWSBLUR.reader.active_folder.get('fake') || - NEWSBLUR.reader.active_folder.get('show_options') + NEWSBLUR.reader.active_folder.get('show_options') })); this.search_view = new NEWSBLUR.Views.FeedSearchView({ feedbar_view: this }).render(); this.search_view.blur_search(); $(".NB-search-container", $view).html(this.search_view.$el); - } else if (NEWSBLUR.reader.flags['river_view'] && - NEWSBLUR.reader.active_folder && - NEWSBLUR.reader.active_folder.get('folder_title')) { + } else if (NEWSBLUR.reader.flags['river_view'] && + NEWSBLUR.reader.active_folder && + NEWSBLUR.reader.active_folder.get('folder_title')) { this.view = new NEWSBLUR.Views.Folder({ model: NEWSBLUR.reader.active_folder, collection: NEWSBLUR.reader.active_folder.folder_view.collection, feedbar: true, only_title: true - }).render(); - $view = this.view.$el; + }).render(); + $view = this.view.$el; this.search_view = this.view.search_view; } else { this.view = new NEWSBLUR.Views.FeedTitleView({ - model: NEWSBLUR.assets.get_feed(this.options.feed_id), + model: NEWSBLUR.assets.get_feed(this.options.feed_id), type: 'story' }).render(); $view = this.view.$el; this.search_view = this.view.search_view; } - + this.$el.html($view); - + if (NEWSBLUR.reader.flags.searching) { this.focus_search(); } - + return this; }, - - remove: function() { + + remove: function () { if (this.view) { this.view.remove(); delete this.view; } // Backbone.View.prototype.remove.call(this); }, - - search_has_focus: function() { + + search_has_focus: function () { return this.search_view && this.search_view.has_focus(); }, - - focus_search: function() { + + focus_search: function () { if (!this.search_view) return; - + this.search_view.focus_search(); }, - - watch_toggled_sidebar: function() { + + watch_toggled_sidebar: function () { if (NEWSBLUR.reader.flags['sidebar_closed']) { $(".NB-feedbar").addClass("NB-sidebar-closed"); } else { $(".NB-feedbar").removeClass("NB-sidebar-closed"); } }, - + // =========== // = Actions = // =========== - - show_feed_hidden_story_title_indicator: function(is_feed_load) { + + show_feed_hidden_story_title_indicator: function (is_feed_load) { if (!is_feed_load) return; if (!NEWSBLUR.reader.active_feed) return; if (NEWSBLUR.reader.flags.search) return; if (NEWSBLUR.reader.flags['feed_list_showing_starred']) return; NEWSBLUR.reader.flags['unread_threshold_temporarily'] = null; - + var unread_view_name = NEWSBLUR.reader.get_unread_view_name(); var $indicator = this.$('.NB-story-title-indicator'); var unread_hidden_stories; if (NEWSBLUR.reader.flags['river_view']) { unread_hidden_stories = NEWSBLUR.reader.active_folder && - NEWSBLUR.reader.active_folder.folders && - NEWSBLUR.reader.active_folder.folders.unread_counts && - NEWSBLUR.reader.active_folder.folders.unread_counts().ng; + NEWSBLUR.reader.active_folder.folders && + NEWSBLUR.reader.active_folder.folders.unread_counts && + NEWSBLUR.reader.active_folder.folders.unread_counts().ng; } else { unread_hidden_stories = NEWSBLUR.assets.active_feed.unread_counts().ng; } @@ -203,38 +203,38 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({ $indicator.hide(); return; } - + if (is_feed_load) { - $indicator.css({'display': 'block', 'opacity': 0}); - _.delay(function() { - $indicator.animate({'opacity': 1}, {'duration': 1000, 'easing': 'easeOutCubic'}); + $indicator.css({ 'display': 'block', 'opacity': 0 }); + _.delay(function () { + $indicator.animate({ 'opacity': 1 }, { 'duration': 1000, 'easing': 'easeOutCubic' }); }, 500); } $indicator.removeClass('unread_threshold_positive') - .removeClass('unread_threshold_neutral') - .removeClass('unread_threshold_negative') - .addClass('unread_threshold_'+unread_view_name); + .removeClass('unread_threshold_neutral') + .removeClass('unread_threshold_negative') + .addClass('unread_threshold_' + unread_view_name); }, - - show_hidden_story_titles: function() { + + show_hidden_story_titles: function () { var $indicator = this.$('.NB-story-title-indicator'); var temp_unread_view_name = NEWSBLUR.reader.get_unread_view_name(); var unread_view_name = NEWSBLUR.reader.get_unread_view_name(null, true); - var hidden_stories_at_threshold = NEWSBLUR.assets.stories.any(function(story) { + var hidden_stories_at_threshold = NEWSBLUR.assets.stories.any(function (story) { var score = story.score(); if (temp_unread_view_name == 'positive') return score == 0; else if (temp_unread_view_name == 'neutral') return score < 0; }); - var hidden_stories_below_threshold = temp_unread_view_name == 'positive' && - NEWSBLUR.assets.stories.any(function(story) { - return story.score() < 0; - }); - + var hidden_stories_below_threshold = temp_unread_view_name == 'positive' && + NEWSBLUR.assets.stories.any(function (story) { + return story.score() < 0; + }); + // NEWSBLUR.log(['show_hidden_story_titles', hidden_stories_at_threshold, hidden_stories_below_threshold, unread_view_name, temp_unread_view_name, NEWSBLUR.reader.flags['unread_threshold_temporarily']]); - + // First click, open neutral. Second click, open negative. - if (temp_unread_view_name == 'positive' && - hidden_stories_at_threshold && + if (temp_unread_view_name == 'positive' && + hidden_stories_at_threshold && hidden_stories_below_threshold) { NEWSBLUR.reader.flags['unread_threshold_temporarily'] = 'neutral'; NEWSBLUR.reader.show_story_titles_above_intelligence_level({ @@ -243,7 +243,7 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({ 'follow': true }); $indicator.removeClass('unread_threshold_positive') - .removeClass('unread_threshold_negative'); + .removeClass('unread_threshold_negative'); $indicator.addClass('unread_threshold_neutral'); $(".NB-story-title-indicator-text", $indicator).text("show hidden stories"); } else if (NEWSBLUR.reader.flags['unread_threshold_temporarily'] != 'negative') { @@ -254,7 +254,7 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({ 'follow': true }); $indicator.removeClass('unread_threshold_positive') - .removeClass('unread_threshold_neutral'); + .removeClass('unread_threshold_neutral'); $indicator.addClass('unread_threshold_negative'); // $indicator.animate({'opacity': 0}, {'duration': 500}).css('display', 'none'); $(".NB-story-title-indicator-text", $indicator).text("hide hidden stories"); @@ -266,44 +266,44 @@ NEWSBLUR.Views.StoryTitlesHeader = Backbone.View.extend({ 'follow': true }); $indicator.removeClass('unread_threshold_positive') - .removeClass('unread_threshold_neutral') - .removeClass('unread_threshold_negative'); - $indicator.addClass('unread_threshold_'+unread_view_name); + .removeClass('unread_threshold_neutral') + .removeClass('unread_threshold_negative'); + $indicator.addClass('unread_threshold_' + unread_view_name); $(".NB-story-title-indicator-text", $indicator).text("show hidden stories"); } }, - - open_options_popover: function(e) { + + open_options_popover: function (e) { if (!(this.showing_fake_folder || - NEWSBLUR.reader.active_feed == "read" || - NEWSBLUR.reader.flags['starred_view'])) return; - + NEWSBLUR.reader.active_feed == "read" || + NEWSBLUR.reader.flags['starred_view'])) return; + NEWSBLUR.FeedOptionsPopover.create({ anchor: this.$(".NB-feedbar-options"), feed_id: NEWSBLUR.reader.active_feed }); }, - - mark_folder_as_read: function(e, days_back) { + + mark_folder_as_read: function (e, days_back) { if (!this.showing_fake_folder) return; if (NEWSBLUR.assets.preference('mark_read_river_confirm')) { - NEWSBLUR.reader.open_mark_read_modal({days: days_back || 0}); + NEWSBLUR.reader.open_mark_read_modal({ days: days_back || 0 }); } else { NEWSBLUR.reader.mark_folder_as_read(); } this.$('.NB-feedbar-mark-feed-read-container').fadeOut(400); }, - - mark_folder_as_read_days: function(e) { + + mark_folder_as_read_days: function (e) { if (!this.showing_fake_folder) return; var days = parseInt($(e.target).data('days'), 10); this.mark_folder_as_read(e, days); }, - - expand_mark_read: function() { + + expand_mark_read: function () { if (!this.showing_fake_folder) return; NEWSBLUR.Views.FeedTitleView.prototype.expand_mark_read.call(this); } - + }); diff --git a/media/js/newsblur/views/story_titles_view.js b/media/js/newsblur/views/story_titles_view.js index 19097516bf..737d16f85b 100644 --- a/media/js/newsblur/views/story_titles_view.js +++ b/media/js/newsblur/views/story_titles_view.js @@ -1,15 +1,15 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ - + el: '.NB-story-titles', - + events: { - "click .NB-feed-story-premium-only a" : function(e) { + "click .NB-feed-story-premium-only a": function (e) { e.preventDefault(); - NEWSBLUR.reader.open_feedchooser_modal({premium_only: true}); + NEWSBLUR.reader.open_feedchooser_modal({ premium_only: true }); } }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'scroll'); // console.log(['initialize story titles view', this.collection]); this.collection.bind('reset', this.render, this); @@ -21,21 +21,21 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.$story_titles.scroll(this.scroll); this.stories = []; }, - + // ========== // = Render = // ========== - - render: function(options) { + + render: function (options) { // console.log(['render story_titles', this.options.override_layout, this.collection.length, this.$story_titles[0]]); this.clear(); this.$story_titles.scrollTop(0); var collection = this.collection; var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); var on_dashboard = this.options.on_dashboard; var override_layout = this.options.override_layout; - var stories = this.collection.map(function(story) { + var stories = this.collection.map(function (story) { return new NEWSBLUR.Views.StoryTitleView({ model: story, collection: collection, @@ -47,7 +47,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ }).render(); }); this.stories = stories; - var $stories = _.map(stories, function(story) { + var $stories = _.map(stories, function (story) { return story.el; }); this.$el.html($stories); @@ -55,19 +55,19 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.end_loading(); this.fill_out(); this.override_grid(); - + this.scroll_to_selected_story(null, options); }, - - add: function(options) { + + add: function (options) { // console.log(['add story_titles', options]); var collection = this.collection; if (options.added) { var on_dashboard = this.options.on_dashboard; var override_layout = this.options.override_layout; var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); - var stories = _.compact(_.map(this.collection.models.slice(-1 * options.added), function(story) { + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + var stories = _.compact(_.map(this.collection.models.slice(-1 * options.added), function (story) { if (story.story_title_view) return; return new NEWSBLUR.Views.StoryTitleView({ model: story, @@ -80,7 +80,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ }).render(); })); this.stories = this.stories.concat(stories); - var $stories = _.map(stories, function(story) { + var $stories = _.map(stories, function (story) { return story.el; }); this.$el.append($stories); @@ -93,42 +93,42 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.fill_out(); }, - clear: function() { + clear: function () { // console.log(['clear story titles', this.stories.length, this.$el]); _.invoke(this.stories, 'destroy'); this.cache = {}; this.collection.page_fill_outs = 0; this.collection.no_more_stories = false; }, - - override_grid: function() { + + override_grid: function () { if (!NEWSBLUR.reader.active_feed) return; - + var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); if (story_layout != 'grid') return; - + var columns = NEWSBLUR.assets.preference('grid_columns'); var height = NEWSBLUR.assets.preference('grid_height'); var $layout = this.$story_titles; $layout.removeClass('NB-grid-columns-1') - .removeClass('NB-grid-columns-2') - .removeClass('NB-grid-columns-3') - .removeClass('NB-grid-columns-4'); + .removeClass('NB-grid-columns-2') + .removeClass('NB-grid-columns-3') + .removeClass('NB-grid-columns-4'); $layout.removeClass('NB-grid-height-xs') - .removeClass('NB-grid-height-s') - .removeClass('NB-grid-height-m') - .removeClass('NB-grid-height-l') - .removeClass('NB-grid-height-xl'); + .removeClass('NB-grid-height-s') + .removeClass('NB-grid-height-m') + .removeClass('NB-grid-height-l') + .removeClass('NB-grid-height-xl'); if (columns > 0) { $layout.addClass('NB-grid-columns-' + columns); } $layout.addClass('NB-grid-height-' + height); }, - - append_river_premium_only_notification: function() { + + append_river_premium_only_notification: function () { var message = [ 'The full River of News is a ', $.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'), @@ -149,15 +149,15 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ ]; } var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [ - $.make('div', { className: 'NB-feed-story-premium-only-text'}, message) + $.make('div', { className: 'NB-feed-story-premium-only-text' }, message) ]); this.$('.NB-feed-story-premium-only').remove(); this.$(".NB-end-line").append($notice); }, - - append_search_premium_only_notification: function() { + + append_search_premium_only_notification: function () { var $notice = $.make('div', { className: 'NB-feed-story-premium-only' }, [ - $.make('div', { className: 'NB-feed-story-premium-only-text'}, [ + $.make('div', { className: 'NB-feed-story-premium-only-text' }, [ 'Search is a ', $.make('a', { href: '#', className: 'NB-splash-link' }, 'premium feature'), '.' @@ -166,56 +166,56 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.$('.NB-feed-story-premium-only').remove(); this.$(".NB-end-line").append($notice); }, - + // =========== // = Actions = // =========== - + fill_out: function (options) { this.snap_back_scroll_position(); - if (this.collection.no_more_stories || + if (this.collection.no_more_stories || !this.collection.length || NEWSBLUR.reader.flags.story_titles_closed) { return; } - + options = options || {}; // console.log(['fill out story titles', this.options.on_dashboard ? "dashboard" : "stories", options, NEWSBLUR.assets.flags['no_more_stories'], NEWSBLUR.assets.stories.length, NEWSBLUR.reader.flags.story_titles_closed]); - - if (this.collection.page_fill_outs < NEWSBLUR.reader.constants.FILL_OUT_PAGES && + + if (this.collection.page_fill_outs < NEWSBLUR.reader.constants.FILL_OUT_PAGES && !this.collection.no_more_stories) { var $last = this.$('.NB-story-title:visible:last'); var container_height = this.$story_titles.height(); // NEWSBLUR.log(["fill out", $last.length && $last.position().top, container_height, $last.length, this.$story_titles.scrollTop()]); this.collection.page_fill_outs += 1; - _.delay(_.bind(function() { + _.delay(_.bind(function () { this.scroll(); }, this), 10); } else { this.show_no_more_stories(); } }, - - show_loading: function(options) { + + show_loading: function (options) { options = options || {}; if (this.collection.no_more_stories) return; var $story_titles = this.$story_titles; this.$('.NB-end-line').remove(); var $endline = $.make('div', { className: "NB-end-line NB-load-line NB-short" }); - $endline.css({'background': '#FFF'}); + $endline.css({ 'background': '#FFF' }); this.$el.append($endline); - + if (options.scroll_to_loadbar) { this.pre_load_page_scroll_position = $('#story_titles').scrollTop(); if (this.pre_load_page_scroll_position > 0) { this.pre_load_page_scroll_position += $endline.outerHeight(); } - $story_titles.stop().scrollTo($endline, { + $story_titles.stop().scrollTo($endline, { duration: 0, - axis: 'y', - easing: 'easeInOutQuint', - offset: 0, + axis: 'y', + easing: 'easeInOutQuint', + offset: 0, queue: false }); this.post_load_page_scroll_position = $('#story_titles').scrollTop(); @@ -224,8 +224,8 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.post_load_page_scroll_position = null; } }, - - check_premium_river: function() { + + check_premium_river: function () { if (!NEWSBLUR.Globals.is_premium && NEWSBLUR.Globals.is_authenticated && (this.options.on_dashboard || NEWSBLUR.reader.flags['river_view'])) { @@ -235,16 +235,16 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.show_no_more_stories(); } }, - - check_premium_search: function() { + + check_premium_search: function () { if (!NEWSBLUR.Globals.is_premium && NEWSBLUR.reader.flags.search) { this.show_no_more_stories(); this.append_search_premium_only_notification(); } }, - - end_loading: function() { + + end_loading: function () { var $endbar = this.$story_titles.find('.NB-end-line'); $endbar.remove(); @@ -252,14 +252,14 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.show_no_more_stories(); } }, - - show_no_more_stories: function() { + + show_no_more_stories: function () { this.$('.NB-end-line').remove(); var $end_stories_line = $.make('div', { className: "NB-end-line" }, [ $.make('div', { className: 'NB-fleuron' }) ]); var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); if (_.contains(['list', 'grid', 'magazine'], story_layout) || NEWSBLUR.assets.preference('mark_read_on_scroll_titles')) { var pane_height = this.$story_titles.height(); var endbar_height = 20; @@ -269,7 +269,7 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ var empty_space = pane_height - last_story_height - endbar_height; if (empty_space > 0) endbar_height += empty_space + 1; - + // endbar_height /= 2; // Splitting padding between top and bottom $end_stories_line.css('paddingBottom', endbar_height); // $end_stories_line.css('paddingTop', endbar_height); @@ -278,40 +278,40 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ this.$el.append($end_stories_line); }, - - snap_back_scroll_position: function() { + + snap_back_scroll_position: function () { var $story_titles = this.$story_titles; if (this.post_load_page_scroll_position == $story_titles.scrollTop() && this.pre_load_page_scroll_position != null && !NEWSBLUR.reader.flags['select_story_in_feed']) { - $story_titles.stop().scrollTo(this.pre_load_page_scroll_position, { + $story_titles.stop().scrollTo(this.pre_load_page_scroll_position, { duration: 0, - axis: 'y', - offset: 0, + axis: 'y', + offset: 0, queue: false }); } }, - + // ============ // = Bindings = // ============ - - scroll_to_selected_story: function(story, options) { + + scroll_to_selected_story: function (story, options) { options = options || {}; var story_title_view = (story && story.story_title_view) || - (this.collection.active_story && this.collection.active_story.story_title_view); + (this.collection.active_story && this.collection.active_story.story_title_view); var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); if (!story_title_view) return; - if (story && - !story.get('selected') && - !options.force && + if (story && + !story.get('selected') && + !options.force && story_layout != 'grid') return; - + // console.log(["scroll_to_selected_story 1", story, options]); var story_title_visisble = this.$story_titles.isScrollVisible(story_title_view.$el); - if (!story_title_visisble || options.force || + if (!story_title_visisble || options.force || _.contains(['list', 'grid', 'magazine'], story_layout)) { var container_offset = this.$story_titles.position().top; var scroll = story_title_view.$el.find('.NB-story-title').position().top; @@ -320,15 +320,15 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ } var container = this.$story_titles.scrollTop(); var height = this.$story_titles.outerHeight(); - var position = scroll+container-height/5; + var position = scroll + container - height / 5; // console.log(["scroll_to_selected_story 2", container_offset, scroll, container, height, position]); if (_.contains(['list', 'grid', 'magazine'], story_layout)) { - position = scroll+container; + position = scroll + container; } if (story_layout == 'grid') { // position -= 21; } - + // console.log(["scroll_to_selected_story 3", position]); NEWSBLUR.reader.flags['opening_story'] = true; this.$story_titles.stop().scrollTo(position, { @@ -338,17 +338,17 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ NEWSBLUR.reader.flags['opening_story'] = false; } }); - } + } }, // ========== // = Events = // ========== - - scroll: function() { + + scroll: function () { var $story_titles = this.$story_titles; var scroll_y = $story_titles.scrollTop(); - + if (!this.options.on_dashboard) { if (NEWSBLUR.reader.flags['opening_feed']) return; // if (NEWSBLUR.reader.flags['opening_story']) return; @@ -357,35 +357,35 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ } if (this.collection.no_more_stories) return; } - + var position = $story_titles.position(); if (!position) return; - + var container_offset = position.top; var visible_height = $story_titles.height() * 2; var total_height = this.$el.outerHeight() + NEWSBLUR.reader.$s.$feedbar.innerHeight(); - + // console.log(["scroll titles", this.options.on_dashboard ? "dashboard" : "stories", visible_height, scroll_y, ">", total_height, this.$el, container_offset]); if (visible_height + scroll_y >= total_height) { - NEWSBLUR.reader.load_page_of_feed_stories({scroll_to_loadbar: false}); + NEWSBLUR.reader.load_page_of_feed_stories({ scroll_to_loadbar: false }); } }, - - mark_read_stories_above_scroll: function(scroll_y) { + + mark_read_stories_above_scroll: function (scroll_y) { var $story_titles = this.$story_titles; var score = NEWSBLUR.reader.get_unread_view_score(); var unread_stories = []; var story_layout = this.options.override_layout || - NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); + NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout'); var grid = story_layout == 'grid'; var point = this.$story_titles.offset(); - var offset = grid ? {top: 100, left: 100} : {top: 30, left: 30}; - var $story_title = $(document.elementFromPoint(point.left + offset.left, - point.top + offset.top - )).closest('.'+NEWSBLUR.Views.StoryTitleView.prototype.className); - var reached_bottom = this.collection.no_more_stories && - this.$el.height() - $story_titles.height() - scroll_y <= 0; - var topstory = _.detect(this.stories, function(view) { + var offset = grid ? { top: 100, left: 100 } : { top: 30, left: 30 }; + var $story_title = $(document.elementFromPoint(point.left + offset.left, + point.top + offset.top + )).closest('.' + NEWSBLUR.Views.StoryTitleView.prototype.className); + var reached_bottom = this.collection.no_more_stories && + this.$el.height() - $story_titles.height() - scroll_y <= 0; + var topstory = _.detect(this.stories, function (view) { if (!reached_bottom && view.el == $story_title[0]) return true; if (view.model.get('read_status') == 0 && view.model.score() >= score) { unread_stories.push(view.model); @@ -402,5 +402,5 @@ NEWSBLUR.Views.StoryTitlesView = Backbone.View.extend({ } _.invoke(unread_stories, 'mark_read'); } - + }); diff --git a/media/js/newsblur/views/text_tab_view.js b/media/js/newsblur/views/text_tab_view.js index 2d2ddb0e18..3b74c8df65 100644 --- a/media/js/newsblur/views/text_tab_view.js +++ b/media/js/newsblur/views/text_tab_view.js @@ -1,29 +1,29 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({ - + events: { - "click .NB-text-view-premium-only a" : function(e) { + "click .NB-text-view-premium-only a": function (e) { e.preventDefault(); - NEWSBLUR.reader.open_feedchooser_modal({'premium_only': true}); + NEWSBLUR.reader.open_feedchooser_modal({ 'premium_only': true }); } }, - - initialize: function() { + + initialize: function () { _.bindAll(this, 'render', 'error'); - + if (this.collection) { this.collection.bind('change:selected', this.select_story, this); } }, - - destroy: function() { + + destroy: function () { this.remove(); }, - + // =========== // = Actions = // =========== - - fetch_and_render: function(story, is_temporary) { + + fetch_and_render: function (story, is_temporary) { if (!story) story = NEWSBLUR.reader.active_story; if (!story && is_temporary) { NEWSBLUR.reader.show_next_story(1); @@ -38,7 +38,7 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({ } if (this.story == story) return; - + this.story = story; this.story_detail = new NEWSBLUR.Views.StoryDetailView({ model: this.story, @@ -54,18 +54,18 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({ this.story_detail.attach_handlers(); this.show_loading(); NEWSBLUR.assets.fetch_original_text(story.get('story_hash'), this.render, this.error); - + return this; }, - - render: function(data) { + + render: function (data) { if (!this.story) return; - - if (data && (data.story_id != this.story.get('id') || - data.feed_id != this.story.get('story_feed_id'))) { + + if (data && (data.story_id != this.story.get('id') || + data.feed_id != this.story.get('story_feed_id'))) { return; } - + this.hide_loading(); var $content = this.$('.NB-feed-story-content'); @@ -84,40 +84,40 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({ duration: 250, queue: false }); - + if (!NEWSBLUR.Globals.is_premium) { this.append_premium_only_notification(); } }, - - unload: function() { + + unload: function () { this.story = null; this.$el.empty(); }, - - show_loading: function() { + + show_loading: function () { NEWSBLUR.app.taskbar_info.hide_stories_error(); NEWSBLUR.app.taskbar_info.show_stories_progress_bar(10, "Fetching text"); }, - - hide_loading: function() { + + hide_loading: function () { NEWSBLUR.app.taskbar_info.hide_stories_progress_bar(); }, - - error: function() { + + error: function () { this.hide_loading(); NEWSBLUR.app.taskbar_info.show_stories_error({}, "Sorry, the story\'s text
could not be extracted."); - + var $content = this.$('.NB-feed-story-content'); $content.html(this.story.story_content()); this.story_detail.attach_handlers(); }, - - append_premium_only_notification: function() { + + append_premium_only_notification: function () { var $content = this.$('.NB-feed-story-content'); var $notice = $.make('div', { className: 'NB-text-view-premium-only' }, [ - $.make('div', { className: 'NB-feed-story-premium-only-divider'}), - $.make('div', { className: 'NB-feed-story-premium-only-text'}, [ + $.make('div', { className: 'NB-feed-story-premium-only-divider' }), + $.make('div', { className: 'NB-feed-story-premium-only-text' }, [ 'The full ', $.make('img', { src: NEWSBLUR.Globals['MEDIA_URL'] + 'img/icons/circular/nav_story_text_active.png' }), ' Text view is a ', @@ -125,50 +125,50 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({ '.' ]) ]); - + $notice.hide(); this.$('.NB-feed-story-premium-only').remove(); $content.after($notice); this.$el.addClass('NB-premium-only'); - + $notice.css('opacity', 0); $notice.show(); - $notice.animate({'opacity': 1}, {'duration': 250, 'queue': false}); + $notice.animate({ 'opacity': 1 }, { 'duration': 250, 'queue': false }); }, - - show_explainer_single_story_mode: function() { + + show_explainer_single_story_mode: function () { var $empty = $.make("div", { className: "NB-story-list-empty" }, [ $.make('div', { className: 'NB-world' }), 'Select a story to read' ]); - + this.$(".NB-story-list-empty").remove(); this.$el.append($empty); }, - - resize_starred_tags: function() { + + resize_starred_tags: function () { if (this.story.get('starred')) { - this.story_detail.save_view.reset_height({immediate: true}); + this.story_detail.save_view.reset_height({ immediate: true }); } }, - + // ========== // = Events = // ========== - - select_story: function(story, selected) { + + select_story: function (story, selected) { if (!selected) return; // this.hide_loading(); // Not sure why this is here? - + if ((NEWSBLUR.reader.story_view == 'text' && - _.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')))) { + _.contains(['split', 'full'], NEWSBLUR.assets.view_setting(NEWSBLUR.reader.active_feed, 'layout')))) { if (NEWSBLUR.reader.flags['temporary_story_view']) { NEWSBLUR.reader.switch_to_correct_view(); } this.fetch_and_render(story); } } - + }); diff --git a/media/js/newsblur/views/unread_count_view.js b/media/js/newsblur/views/unread_count_view.js index c2297009c2..aa5d6eb41d 100644 --- a/media/js/newsblur/views/unread_count_view.js +++ b/media/js/newsblur/views/unread_count_view.js @@ -1,8 +1,8 @@ NEWSBLUR.Views.UnreadCount = Backbone.View.extend({ - + className: 'feed_counts_floater', - - initialize: function() { + + initialize: function () { _.bindAll(this, 'render'); if (!this.options.stale && !this.options.feed_chooser) { if (this.model) { @@ -19,12 +19,12 @@ NEWSBLUR.Views.UnreadCount = Backbone.View.extend({ } } }, - + // ========== // = Render = // ========== - - render: function() { + + render: function () { var unread_class = ""; var counts; var muted = false; @@ -50,20 +50,20 @@ NEWSBLUR.Views.UnreadCount = Backbone.View.extend({ if (muted) { unread_class += ' NB-muted-count'; } - + this.$el.html(this.template({ - ps : this.options.feed_chooser ? "On" : counts['ps'], - nt : counts['nt'], - ng : this.options.feed_chooser ? "Off" : counts['ng'], - muted : muted, - st : this.options.include_starred && counts['st'], - unread_class : unread_class + ps: this.options.feed_chooser ? "On" : counts['ps'], + nt: counts['nt'], + ng: this.options.feed_chooser ? "Off" : counts['ng'], + muted: muted, + st: this.options.include_starred && counts['st'], + unread_class: unread_class })); - + return this; }, - - destroy: function() { + + destroy: function () { if (this.model) { this.model.unbind(null, null, this); } else if (this.collection) { @@ -71,7 +71,7 @@ NEWSBLUR.Views.UnreadCount = Backbone.View.extend({ } this.remove(); }, - + template: _.template('\
\ \ @@ -93,38 +93,38 @@ NEWSBLUR.Views.UnreadCount = Backbone.View.extend({ <% } %>\
\ '), - + // =========== // = Actions = // =========== - - center: function() { + + center: function () { var count_width = this.$el.width(); var left_buttons_offset = $('.NB-taskbar-view').outerWidth(true); var right_buttons_offset = $(".NB-taskbar-options-container").position().left; var usable_space = right_buttons_offset - left_buttons_offset; var left = (usable_space / 2) - (count_width / 2) + left_buttons_offset; - + // console.log(["Unread count offset", count_width, left, left_buttons_offset, right_buttons_offset]); - + if (count_width + 12 > usable_space) { this.$el.hide(); } - - this.$el.css({'left': left}); + + this.$el.css({ 'left': left }); }, - - flash: function() { + + flash: function () { var $floater = this.$el; - + if (!NEWSBLUR.assets.preference('animations')) return; - - _.defer(function() { - $floater.animate({'opacity': 1}, {'duration': 250, 'queue': false}); - _.delay(function() { - $floater.animate({'opacity': .2}, {'duration': 250, 'queue': false}); + + _.defer(function () { + $floater.animate({ 'opacity': 1 }, { 'duration': 250, 'queue': false }); + _.delay(function () { + $floater.animate({ 'opacity': .2 }, { 'duration': 250, 'queue': false }); }, 400); - }); + }); } - -}); \ No newline at end of file + +}); diff --git a/media/js/newsblur/welcome/welcome.js b/media/js/newsblur/welcome/welcome.js index aab481fb96..0bedcd935c 100644 --- a/media/js/newsblur/welcome/welcome.js +++ b/media/js/newsblur/welcome/welcome.js @@ -1,53 +1,53 @@ NEWSBLUR.Welcome = Backbone.View.extend({ - + el: '.NB-body-inner', flags: {}, rotation: 0, - + events: { - "click .NB-button-login" : "show_signin_form", - "click .NB-button-tryout" : "show_tryout", - "click .NB-welcome-header-caption" : "click_header_caption", - "focus input" : "stop_rotation", - "mouseenter .NB-welcome-header-caption" : "enter_header_caption", - "mouseleave .NB-welcome-header-caption" : "leave_header_caption" + "click .NB-button-login": "show_signin_form", + "click .NB-button-tryout": "show_tryout", + "click .NB-welcome-header-caption": "click_header_caption", + "focus input": "stop_rotation", + "mouseenter .NB-welcome-header-caption": "enter_header_caption", + "mouseleave .NB-welcome-header-caption": "leave_header_caption" }, - - initialize: function() { + + initialize: function () { this.start_rotation(); - _.delay(_.bind(function() { + _.delay(_.bind(function () { // this.debug_password_autocomplete(); }, this), 500); NEWSBLUR.reader.$s.$layout.hide(); }, - - debug_password_autocomplete: function() { + + debug_password_autocomplete: function () { console.log(['Triggering focus']); this.$("input[name=login-username]").trigger('focus'); }, - + // ========== // = Header = // ========== - - fix_misalignment: function(e) { + + fix_misalignment: function (e) { console.log(['Fixing misalignment', e]); - + this.flags.on_signin = true; this.show_signin_form(); }, - - click_header_caption: function(e) { + + click_header_caption: function (e) { this.flags.on_signin = false; this.enter_header_caption(e); }, - - enter_header_caption: function(e) { + + enter_header_caption: function (e) { this.flags.on_header_caption = true; var $caption = $(e.currentTarget); - + if (this.flags.on_signin) return; - + if ($caption.hasClass('NB-welcome-header-caption-signin')) { this.flags.on_signin = true; this.show_signin_form(); @@ -56,39 +56,39 @@ NEWSBLUR.Welcome = Backbone.View.extend({ this.rotate_screenshots(r); } }, - - leave_header_caption: function(e) { + + leave_header_caption: function (e) { var $caption = $(e.currentTarget); if ($caption.hasClass('NB-welcome-header-caption-signin')) { - + } else { this.flags.on_header_caption = false; } }, - - start_rotation: function() { + + start_rotation: function () { if (this.$('.NB-welcome-header-account').hasClass('NB-active')) { this.stop_rotation(); } var $first_img = this.$('.NB-welcome-header-image img').eq(0); if ($first_img[0].complete) { - setInterval(_.bind(this.rotate_screenshots, this), 3000); + setInterval(_.bind(this.rotate_screenshots, this), 3000); } else { - $first_img.on('load', _.bind(function() { + $first_img.on('load', _.bind(function () { setInterval(_.bind(this.rotate_screenshots, this), 3000); }, this)); } }, - - rotate_screenshots: function(force, callback) { + + rotate_screenshots: function (force, callback) { if (this.flags.on_header_caption && _.isUndefined(force)) { return; } if (this.flags.on_signin && _.isUndefined(force)) { return; } - + var NUM_CAPTIONS = 3; var r = force ? force - 1 : (this.rotation + 1) % NUM_CAPTIONS; if (!force) { @@ -120,29 +120,29 @@ NEWSBLUR.Welcome = Backbone.View.extend({ this.$('input').blur(); } }, - - stop_rotation: function() { + + stop_rotation: function () { console.log(['stop_rotation']); this.flags.on_signin = true; }, - - show_signin_form: function() { + + show_signin_form: function () { var open = !NEWSBLUR.reader.flags['sidebar_closed']; this.hide_tryout(); - + this.flags.on_header_caption = true; this.flags.on_signin = true; - + var add_url = $.getQueryString('add') || $.getQueryString('url'); if (add_url) { this.$("input[name=next]").val("/?add=" + add_url); } - - this.$el.scrollTo(0, 500, {queue: false, easing: 'easeInOutQuint'}); - - _.delay(_.bind(function() { - this.rotate_screenshots(4, _.bind(function() { - _.delay(_.bind(function() { + + this.$el.scrollTo(0, 500, { queue: false, easing: 'easeInOutQuint' }); + + _.delay(_.bind(function () { + this.rotate_screenshots(4, _.bind(function () { + _.delay(_.bind(function () { if (this.$("input:focus").length) { console.log(['Already focused']); return; @@ -153,10 +153,10 @@ NEWSBLUR.Welcome = Backbone.View.extend({ }, this), open ? 560 : 0); }, - - show_tryout: function() { + + show_tryout: function () { if (!NEWSBLUR.reader) return; - + if (!this.flags.loaded) { NEWSBLUR.reader.$s.$layout.layout().hide('west', true); NEWSBLUR.reader.$s.$layout.show(); @@ -171,15 +171,15 @@ NEWSBLUR.Welcome = Backbone.View.extend({ easing: 'easeInOutQuint', duration: 560 }); - + this.$('.NB-welcome-container')[open ? 'addClass' : 'removeClass']('NB-welcome-tryout'); }, - - hide_tryout: function() { + + hide_tryout: function () { if (!NEWSBLUR.reader) return; - + NEWSBLUR.reader.close_sidebar(); - + this.$('.NB-inner,.NB-inner-account').animate({ paddingLeft: 0 }, { @@ -187,8 +187,8 @@ NEWSBLUR.Welcome = Backbone.View.extend({ easing: 'easeInOutQuint', duration: 560 }); - + this.$('.NB-welcome-container').removeClass('NB-welcome-tryout'); } -}); \ No newline at end of file +}); diff --git a/newsblur_web/__init__.py b/newsblur_web/__init__.py index a711f1df8d..3990cb0a88 100644 --- a/newsblur_web/__init__.py +++ b/newsblur_web/__init__.py @@ -4,4 +4,4 @@ # Django starts so that shared_task will use this app. from .celeryapp import app as celery_app -__all__ = ['celery_app'] +__all__ = ["celery_app"] diff --git a/newsblur_web/celeryapp.py b/newsblur_web/celeryapp.py index 146be96a66..0f22dac8d5 100644 --- a/newsblur_web/celeryapp.py +++ b/newsblur_web/celeryapp.py @@ -1,17 +1,20 @@ from __future__ import absolute_import, unicode_literals + import os + from celery import Celery from django.apps import apps + # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'newsblur_web.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsblur_web.settings") -app = Celery('newsblur_web') +app = Celery("newsblur_web") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace="CELERY") +app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django app configs. app.autodiscover_tasks(lambda: [n.name for n in apps.get_app_configs()]) diff --git a/newsblur_web/docker_local_settings.py b/newsblur_web/docker_local_settings.py index 31e353f543..fca2669808 100644 --- a/newsblur_web/docker_local_settings.py +++ b/newsblur_web/docker_local_settings.py @@ -5,15 +5,13 @@ # = Server Settings = # =================== -ADMINS = ( - ('Samuel Clay', 'samuel@newsblur.com'), -) +ADMINS = (("Samuel Clay", "samuel@newsblur.com"),) -SERVER_EMAIL = 'server@newsblur.com' -HELLO_EMAIL = 'hello@newsblur.com' -NEWSBLUR_URL = 'https://localhost' -PUSH_DOMAIN = 'localhost' -SESSION_COOKIE_DOMAIN = 'localhost' +SERVER_EMAIL = "server@newsblur.com" +HELLO_EMAIL = "hello@newsblur.com" +NEWSBLUR_URL = "https://localhost" +PUSH_DOMAIN = "localhost" +SESSION_COOKIE_DOMAIN = "localhost" # =================== # = Global Settings = @@ -23,24 +21,24 @@ DEBUG = False # DEBUG = True -# DEBUG_ASSETS controls JS/CSS asset packaging. Turning this off requires you to run +# DEBUG_ASSETS controls JS/CSS asset packaging. Turning this off requires you to run # `./manage.py collectstatic` first. Turn this on for development so you can see -# changes in your JS/CSS. +# changes in your JS/CSS. DEBUG_ASSETS = False # Make sure to run `./manage.py collectstatic` first DEBUG_ASSETS = True # DEBUG_QUERIES controls the output of the database query logs. Can be rather verbose -# but is useful to catch slow running queries. A summary is also useful in cutting +# but is useful to catch slow running queries. A summary is also useful in cutting # down verbosity. DEBUG_QUERIES = DEBUG DEBUG_QUERIES_SUMMARY_ONLY = True # DEBUG_QUERIES_SUMMARY_ONLY = False -MEDIA_URL = '/media/' -IMAGES_URL = '/imageproxy' +MEDIA_URL = "/media/" +IMAGES_URL = "/imageproxy" # Uncomment below to debug iOS/Android widget # IMAGES_URL = 'https://haproxy/imageproxy' -SECRET_KEY = 'YOUR SECRET KEY' +SECRET_KEY = "YOUR SECRET KEY" AUTO_PREMIUM_NEW_USERS = True AUTO_PREMIUM_ARCHIVE_NEW_USERS = True AUTO_PREMIUM_PRO_NEW_USERS = True @@ -57,27 +55,27 @@ PRO_MINUTES_BETWEEN_FETCHES = 15 CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://db_redis:6579/6', + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://db_redis:6579/6", }, } -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Set this to the username that is shown on the homepage to unauthenticated users. -HOMEPAGE_USERNAME = 'popular' +HOMEPAGE_USERNAME = "popular" # Google Reader OAuth API Keys -OAUTH_KEY = 'www.example.com' -OAUTH_SECRET = 'SECRET_KEY_FROM_GOOGLE' +OAUTH_KEY = "www.example.com" +OAUTH_SECRET = "SECRET_KEY_FROM_GOOGLE" -S3_ACCESS_KEY = 'XXX' -S3_SECRET = 'SECRET' -S3_BACKUP_BUCKET = 'newsblur-backups' -S3_PAGES_BUCKET_NAME = 'pages-XXX.newsblur.com' -S3_ICONS_BUCKET_NAME = 'icons-XXX.newsblur.com' -S3_AVATARS_BUCKET_NAME = 'avatars-XXX.newsblur.com' +S3_ACCESS_KEY = "XXX" +S3_SECRET = "SECRET" +S3_BACKUP_BUCKET = "newsblur-backups" +S3_PAGES_BUCKET_NAME = "pages-XXX.newsblur.com" +S3_ICONS_BUCKET_NAME = "icons-XXX.newsblur.com" +S3_AVATARS_BUCKET_NAME = "avatars-XXX.newsblur.com" STRIPE_SECRET = "YOUR-SECRET-API-KEY" STRIPE_PUBLISHABLE = "YOUR-PUBLISHABLE-API-KEY" @@ -86,10 +84,10 @@ # = Social APIs = # =============== -FACEBOOK_APP_ID = '111111111111111' -FACEBOOK_SECRET = '99999999999999999999999999999999' -TWITTER_CONSUMER_KEY = 'ooooooooooooooooooooo' -TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' +FACEBOOK_APP_ID = "111111111111111" +FACEBOOK_SECRET = "99999999999999999999999999999999" +TWITTER_CONSUMER_KEY = "ooooooooooooooooooooo" +TWITTER_CONSUMER_SECRET = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" YOUTUBE_API_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # ============= @@ -97,51 +95,34 @@ # ============= DATABASES = { - 'default': { - 'NAME': 'newsblur', - 'ENGINE': 'django_prometheus.db.backends.postgresql', + "default": { + "NAME": "newsblur", + "ENGINE": "django_prometheus.db.backends.postgresql", #'ENGINE': 'django.db.backends.mysql', - 'USER': 'newsblur', - 'PASSWORD': 'newsblur', - 'HOST': 'db_postgres', - 'PORT': 5432 + "USER": "newsblur", + "PASSWORD": "newsblur", + "HOST": "db_postgres", + "PORT": 5432, }, } -MONGO_DB = { - 'name': 'newsblur', - 'host': 'db_mongo:29019' -} +MONGO_DB = {"name": "newsblur", "host": "db_mongo:29019"} MONGO_ANALYTICS_DB = { - 'name': 'nbanalytics', - 'host': 'db_mongo:29019', + "name": "nbanalytics", + "host": "db_mongo:29019", } -MONGODB_SLAVE = { - 'host': 'db_mongo' -} +MONGODB_SLAVE = {"host": "db_mongo"} # Celery RabbitMQ/Redis Broker BROKER_URL = "redis://db_redis:6579/0" CELERY_RESULT_BACKEND = BROKER_URL CELERY_WORKER_CONCURRENCY = 1 -REDIS_USER = { - 'host': 'db_redis', - 'port': 6579 -} -REDIS_PUBSUB = { - 'host': 'db_redis', - 'port': 6579 -} -REDIS_STORY = { - 'host': 'db_redis', - 'port': 6579 -} -REDIS_SESSIONS = { - 'host': 'db_redis', - 'port': 6579 -} +REDIS_USER = {"host": "db_redis", "port": 6579} +REDIS_PUBSUB = {"host": "db_redis", "port": 6579} +REDIS_STORY = {"host": "db_redis", "port": 6579} +REDIS_SESSIONS = {"host": "db_redis", "port": 6579} CELERY_REDIS_DB_NUM = 4 SESSION_REDIS_DB = 5 @@ -153,9 +134,9 @@ ELASTICSEARCH_STORY_HOST = "http://db_elasticsearch:9200" BACKED_BY_AWS = { - 'pages_on_node': False, - 'pages_on_s3': False, - 'icons_on_s3': False, + "pages_on_node": False, + "pages_on_s3": False, + "icons_on_s3": False, } @@ -167,25 +148,27 @@ LOG_TO_STREAM = True if len(logging._handlerList) < 1: - LOG_FILE = '~/newsblur/logs/development.log' - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)-12s: %(message)s', - datefmt='%b %d %H:%M:%S', - handler=logging.StreamHandler) + LOG_FILE = "~/newsblur/logs/development.log" + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)-12s: %(message)s", + datefmt="%b %d %H:%M:%S", + handler=logging.StreamHandler, + ) -MAILGUN_ACCESS_KEY = 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' -MAILGUN_SERVER_NAME = 'newsblur.com' +MAILGUN_ACCESS_KEY = "key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +MAILGUN_SERVER_NAME = "newsblur.com" -DO_TOKEN_LOG = '0000000000000000000000000000000000000000000000000000000000000000' -DO_TOKEN_FABRIC = '0000000000000000000000000000000000000000000000000000000000000000' +DO_TOKEN_LOG = "0000000000000000000000000000000000000000000000000000000000000000" +DO_TOKEN_FABRIC = "0000000000000000000000000000000000000000000000000000000000000000" SERVER_NAME = "nblocalhost" NEWSBLUR_URL = os.getenv("NEWSBLUR_URL", "https://localhost") -if NEWSBLUR_URL == 'https://localhost': +if NEWSBLUR_URL == "https://localhost": SESSION_COOKIE_DOMAIN = "localhost" -SESSION_ENGINE = 'redis_sessions.session' +SESSION_ENGINE = "redis_sessions.session" # CORS_ORIGIN_REGEX_WHITELIST = ('^(https?://)?(\w+\.)?nb.local\.com$', ) diff --git a/newsblur_web/settings.py b/newsblur_web/settings.py index 046b597820..35bae73683 100644 --- a/newsblur_web/settings.py +++ b/newsblur_web/settings.py @@ -7,23 +7,23 @@ # = Directory Declaractions = # =========================== -SETTINGS_DIR = os.path.dirname(__file__) -NEWSBLUR_DIR = os.path.join(SETTINGS_DIR, "../") -MEDIA_ROOT = os.path.join(NEWSBLUR_DIR, 'media') -STATIC_ROOT = os.path.join(NEWSBLUR_DIR, 'static') -UTILS_ROOT = os.path.join(NEWSBLUR_DIR, 'utils') -VENDOR_ROOT = os.path.join(NEWSBLUR_DIR, 'vendor') -LOG_FILE = os.path.join(NEWSBLUR_DIR, 'logs/newsblur.log') -IMAGE_MASK = os.path.join(NEWSBLUR_DIR, 'media/img/mask.png') +SETTINGS_DIR = os.path.dirname(__file__) +NEWSBLUR_DIR = os.path.join(SETTINGS_DIR, "../") +MEDIA_ROOT = os.path.join(NEWSBLUR_DIR, "media") +STATIC_ROOT = os.path.join(NEWSBLUR_DIR, "static") +UTILS_ROOT = os.path.join(NEWSBLUR_DIR, "utils") +VENDOR_ROOT = os.path.join(NEWSBLUR_DIR, "vendor") +LOG_FILE = os.path.join(NEWSBLUR_DIR, "logs/newsblur.log") +IMAGE_MASK = os.path.join(NEWSBLUR_DIR, "media/img/mask.png") # ============== # = PYTHONPATH = # ============== -if '/utils' not in ' '.join(sys.path): +if "/utils" not in " ".join(sys.path): sys.path.append(UTILS_ROOT) -if '/vendor' not in ' '.join(sys.path): +if "/vendor" not in " ".join(sys.path): sys.path.append(VENDOR_ROOT) import datetime @@ -47,17 +47,15 @@ # = Server Settings = # =================== -ADMINS = ( - ('Samuel Clay', 'samuel@newsblur.com'), -) +ADMINS = (("Samuel Clay", "samuel@newsblur.com"),) -SERVER_NAME = 'newsblur' -SERVER_EMAIL = 'server@newsblur.com' -HELLO_EMAIL = 'hello@newsblur.com' -NEWSBLUR_URL = 'https://www.newsblur.com' -IMAGES_URL = 'https://imageproxy.newsblur.com' -PUSH_DOMAIN = 'push.newsblur.com' -SECRET_KEY = 'YOUR_SECRET_KEY' +SERVER_NAME = "newsblur" +SERVER_EMAIL = "server@newsblur.com" +HELLO_EMAIL = "hello@newsblur.com" +NEWSBLUR_URL = "https://www.newsblur.com" +IMAGES_URL = "https://imageproxy.newsblur.com" +PUSH_DOMAIN = "push.newsblur.com" +SECRET_KEY = "YOUR_SECRET_KEY" IMAGES_SECRET_KEY = "YOUR_SECRET_IMAGE_KEY" DNSIMPLE_TOKEN = "YOUR_DNSIMPLE_TOKEN" RECAPTCHA_SECRET_KEY = "YOUR_RECAPTCHA_KEY" @@ -71,40 +69,40 @@ # = Global Settings = # =================== -DEBUG = True -TEST_DEBUG = False +DEBUG = True +TEST_DEBUG = False SEND_BROKEN_LINK_EMAILS = False -DEBUG_QUERIES = False -MANAGERS = ADMINS -PAYPAL_RECEIVER_EMAIL = 'samuel@ofbrooklyn.com' -TIME_ZONE = 'GMT' -LANGUAGE_CODE = 'en-us' -SITE_ID = 1 -USE_I18N = False -LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/account/login' -MEDIA_URL = '/media/' +DEBUG_QUERIES = False +MANAGERS = ADMINS +PAYPAL_RECEIVER_EMAIL = "samuel@ofbrooklyn.com" +TIME_ZONE = "GMT" +LANGUAGE_CODE = "en-us" +SITE_ID = 1 +USE_I18N = False +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/account/login" +MEDIA_URL = "/media/" # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -CIPHER_USERNAMES = False -DEBUG_ASSETS = True -HOMEPAGE_USERNAME = 'popular' -ALLOWED_HOSTS = ['*'] +CIPHER_USERNAMES = False +DEBUG_ASSETS = True +HOMEPAGE_USERNAME = "popular" +ALLOWED_HOSTS = ["*"] AUTO_PREMIUM_NEW_USERS = True AUTO_ENABLE_NEW_USERS = True ENFORCE_SIGNUP_CAPTCHA = False -ENABLE_PUSH = True -PAYPAL_TEST = False -DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5 MB -FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5 MB +ENABLE_PUSH = True +PAYPAL_TEST = False +DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5 MB +FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5 MB PROMETHEUS_EXPORT_MIGRATIONS = False -MAX_SECONDS_COMPLETE_ARCHIVE_FETCH = 60 * 60 * 1 # 1 hour -MAX_SECONDS_ARCHIVE_FETCH_SINGLE_FEED = 60 * 15 # 15 minutes -MAX_EMAILS_SENT_PER_DAY_PER_USER = 20 # Most are story notifications +MAX_SECONDS_COMPLETE_ARCHIVE_FETCH = 60 * 60 * 1 # 1 hour +MAX_SECONDS_ARCHIVE_FETCH_SINGLE_FEED = 60 * 15 # 15 minutes +MAX_EMAILS_SENT_PER_DAY_PER_USER = 20 # Most are story notifications -# Uncomment below to force all feeds to store this many stories. Default is to cut +# Uncomment below to force all feeds to store this many stories. Default is to cut # off at 25 stories for single subscriber non-premium feeds and 500 for popular feeds. # OVERRIDE_STORY_COUNT_MAX = 1000 @@ -114,31 +112,31 @@ MIDDLEWARE = ( - 'django_prometheus.middleware.PrometheusBeforeMiddleware', - 'django.middleware.gzip.GZipMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'subdomains.middleware.SubdomainMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'apps.profile.middleware.TimingMiddleware', - 'apps.profile.middleware.LastSeenMiddleware', - 'apps.profile.middleware.UserAgentBanMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'apps.profile.middleware.SimpsonsMiddleware', - 'apps.profile.middleware.ServerHostnameMiddleware', - 'oauth2_provider.middleware.OAuth2TokenMiddleware', + "django_prometheus.middleware.PrometheusBeforeMiddleware", + "django.middleware.gzip.GZipMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "subdomains.middleware.SubdomainMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "apps.profile.middleware.TimingMiddleware", + "apps.profile.middleware.LastSeenMiddleware", + "apps.profile.middleware.UserAgentBanMiddleware", + "corsheaders.middleware.CorsMiddleware", + "apps.profile.middleware.SimpsonsMiddleware", + "apps.profile.middleware.ServerHostnameMiddleware", + "oauth2_provider.middleware.OAuth2TokenMiddleware", # 'debug_toolbar.middleware.DebugToolbarMiddleware', - 'utils.request_introspection_middleware.DumpRequestMiddleware', - 'apps.profile.middleware.DBProfilerMiddleware', - 'apps.profile.middleware.SQLLogToConsoleMiddleware', - 'utils.redis_raw_log_middleware.RedisDumpMiddleware', - 'django_prometheus.middleware.PrometheusAfterMiddleware', + "utils.request_introspection_middleware.DumpRequestMiddleware", + "apps.profile.middleware.DBProfilerMiddleware", + "apps.profile.middleware.SQLLogToConsoleMiddleware", + "utils.redis_raw_log_middleware.RedisDumpMiddleware", + "django_prometheus.middleware.PrometheusAfterMiddleware", ) AUTHENTICATION_BACKENDS = ( - 'oauth2_provider.backends.OAuth2Backend', - 'django.contrib.auth.backends.ModelBackend', + "oauth2_provider.backends.OAuth2Backend", + "django.contrib.auth.backends.ModelBackend", ) CORS_ORIGIN_ALLOW_ALL = True @@ -146,14 +144,14 @@ CORS_ALLOW_CREDENTIALS = True OAUTH2_PROVIDER = { - 'SCOPES': { - 'read': 'View new unread stories, saved stories, and shared stories.', - 'write': 'Create new saved stories, shared stories, and subscriptions.', - 'ifttt': 'Pair your NewsBlur account with other services.', + "SCOPES": { + "read": "View new unread stories, saved stories, and shared stories.", + "write": "Create new saved stories, shared stories, and subscriptions.", + "ifttt": "Pair your NewsBlur account with other services.", }, - 'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator', - 'ACCESS_TOKEN_EXPIRE_SECONDS': 60*60*24*365*10, # 10 years - 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60*60, # 1 hour + "CLIENT_ID_GENERATOR_CLASS": "oauth2_provider.generators.ClientIdGenerator", + "ACCESS_TOKEN_EXPIRE_SECONDS": 60 * 60 * 24 * 365 * 10, # 10 years + "AUTHORIZATION_CODE_EXPIRE_SECONDS": 60 * 60, # 1 hour } # =========== @@ -161,104 +159,87 @@ # =========== LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '[%(asctime)-12s] %(message)s', - 'datefmt': '%b %d %H:%M:%S' - }, - 'simple': { - 'format': '%(message)s' - }, + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": {"format": "[%(asctime)-12s] %(message)s", "datefmt": "%b %d %H:%M:%S"}, + "simple": {"format": "%(message)s"}, }, - 'handlers': { - 'null': { - 'level':'DEBUG', - 'class':'logging.NullHandler', - }, - 'console':{ - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' + "handlers": { + "null": { + "level": "DEBUG", + "class": "logging.NullHandler", }, - 'vendor.apns':{ - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' + "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}, + "vendor.apns": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}, + "log_file": { + "level": "DEBUG", + "class": "logging.handlers.RotatingFileHandler", + "filename": LOG_FILE, + "maxBytes": 16777216, # 16megabytes + "formatter": "verbose", }, - 'log_file':{ - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': LOG_FILE, - 'maxBytes': 16777216, # 16megabytes - 'formatter': 'verbose' - }, - 'mail_admins': { - 'level': 'CRITICAL', - 'class': 'django.utils.log.AdminEmailHandler', + "mail_admins": { + "level": "CRITICAL", + "class": "django.utils.log.AdminEmailHandler", # 'filters': ['require_debug_false'], - 'include_html': True, + "include_html": True, }, }, - 'loggers': { - 'django': { - 'handlers': ['console', 'log_file', 'mail_admins'], - 'level': 'ERROR', - 'propagate': False, + "loggers": { + "django": { + "handlers": ["console", "log_file", "mail_admins"], + "level": "ERROR", + "propagate": False, }, - 'django.db.backends': { - 'handlers': ['console'], - 'level': 'INFO', - 'propagate': False, + "django.db.backends": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, }, - 'django.security.DisallowedHost': { - 'handlers': ['null'], - 'propagate': False, + "django.security.DisallowedHost": { + "handlers": ["null"], + "propagate": False, }, - 'elasticsearch': { - 'handlers': ['console', 'log_file'], - 'level': 'ERROR', + "elasticsearch": { + "handlers": ["console", "log_file"], + "level": "ERROR", # 'level': 'DEBUG', - 'propagate': False, + "propagate": False, }, - 'elasticsearch.trace': { - 'handlers': ['console', 'log_file'], - 'level': 'ERROR', + "elasticsearch.trace": { + "handlers": ["console", "log_file"], + "level": "ERROR", # 'level': 'DEBUG', - 'propagate': False, + "propagate": False, }, - 'zebra': { - 'handlers': ['console', 'log_file'], + "zebra": { + "handlers": ["console", "log_file"], # 'level': 'ERROR', - 'level': 'DEBUG', - 'propagate': False, + "level": "DEBUG", + "propagate": False, }, - 'newsblur': { - 'handlers': ['console', 'log_file'], - 'level': 'DEBUG', - 'propagate': False, + "newsblur": { + "handlers": ["console", "log_file"], + "level": "DEBUG", + "propagate": False, }, - 'readability': { - 'handlers': ['console', 'log_file'], - 'level': 'WARNING', - 'propagate': False, + "readability": { + "handlers": ["console", "log_file"], + "level": "WARNING", + "propagate": False, }, - 'apps': { - 'handlers': ['log_file'], - 'level': 'DEBUG', - 'propagate': True, + "apps": { + "handlers": ["log_file"], + "level": "DEBUG", + "propagate": True, + }, + "subdomains.middleware": { + "level": "ERROR", + "propagate": False, }, - 'subdomains.middleware': { - 'level': 'ERROR', - 'propagate': False, - } - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } }, + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, } logging.getLogger("requests").setLevel(logging.WARNING) @@ -268,48 +249,48 @@ # = Miscellaneous Settings = # ========================== -DAYS_OF_UNREAD = 30 -DAYS_OF_UNREAD_FREE = 14 -DAYS_OF_UNREAD_ARCHIVE = 9999 +DAYS_OF_UNREAD = 30 +DAYS_OF_UNREAD_FREE = 14 +DAYS_OF_UNREAD_ARCHIVE = 9999 # DoSH can be more, since you can up this value by N, and after N days, -# you can then up the DAYS_OF_UNREAD value with no impact. +# you can then up the DAYS_OF_UNREAD value with no impact. # The max is for archive subscribers. -DAYS_OF_STORY_HASHES = DAYS_OF_UNREAD +DAYS_OF_STORY_HASHES = DAYS_OF_UNREAD DAYS_OF_STORY_HASHES_ARCHIVE = DAYS_OF_UNREAD_ARCHIVE # SUBSCRIBER_EXPIRE sets the number of days after which a user who hasn't logged in # is no longer considered an active subscriber -SUBSCRIBER_EXPIRE = 7 +SUBSCRIBER_EXPIRE = 7 -# PRO_MINUTES_BETWEEN_FETCHES sets the number of minutes to fetch feeds for +# PRO_MINUTES_BETWEEN_FETCHES sets the number of minutes to fetch feeds for # Premium Pro accounts. Defaults to every 5 minutes, but that's for NewsBlur # servers. On your local, you should probably set this to 10-15 minutes PRO_MINUTES_BETWEEN_FETCHES = 5 -ROOT_URLCONF = 'newsblur_web.urls' -INTERNAL_IPS = ('127.0.0.1',) -LOGGING_LOG_SQL = True -APPEND_SLASH = False -SESSION_ENGINE = 'redis_sessions.session' -TEST_RUNNER = "utils.testrunner.TestRunner" -SESSION_COOKIE_NAME = 'newsblur_sessionid' -SESSION_COOKIE_AGE = 60*60*24*365*10 # 10 years -SESSION_COOKIE_DOMAIN = '.newsblur.com' +ROOT_URLCONF = "newsblur_web.urls" +INTERNAL_IPS = ("127.0.0.1",) +LOGGING_LOG_SQL = True +APPEND_SLASH = False +SESSION_ENGINE = "redis_sessions.session" +TEST_RUNNER = "utils.testrunner.TestRunner" +SESSION_COOKIE_NAME = "newsblur_sessionid" +SESSION_COOKIE_AGE = 60 * 60 * 24 * 365 * 10 # 10 years +SESSION_COOKIE_DOMAIN = ".newsblur.com" SESSION_COOKIE_HTTPONLY = False -SESSION_COOKIE_SECURE = True -SENTRY_DSN = 'https://XXXNEWSBLURXXX@app.getsentry.com/99999999' -SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' -DATA_UPLOAD_MAX_NUMBER_FIELDS = None # Handle long /reader/complete_river calls -EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' +SESSION_COOKIE_SECURE = True +SENTRY_DSN = "https://XXXNEWSBLURXXX@app.getsentry.com/99999999" +SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" +DATA_UPLOAD_MAX_NUMBER_FIELDS = None # Handle long /reader/complete_river calls +EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # ============== # = Subdomains = # ============== SUBDOMAIN_URLCONFS = { - None: 'newsblur_web.urls', - 'www': 'newsblur_web.urls', - 'nb': 'newsblur_web.urls', + None: "newsblur_web.urls", + "www": "newsblur_web.urls", + "nb": "newsblur_web.urls", } REMOVE_WWW_FROM_DOMAIN = True @@ -324,42 +305,42 @@ # = Django Apps = # =============== -OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application' +OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application" INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.admin', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_extensions', - 'django_prometheus', - 'paypal.standard.ipn', - 'apps.rss_feeds', - 'apps.reader', - 'apps.analyzer', - 'apps.feed_import', - 'apps.profile', - 'apps.recommendations', - 'apps.statistics', - 'apps.notifications', - 'apps.static', - 'apps.mobile', - 'apps.push', - 'apps.social', - 'apps.oauth', - 'apps.search', - 'apps.categories', - 'utils', # missing models so no migrations - 'vendor', - 'typogrify', - 'vendor.zebra', - 'anymail', - 'oauth2_provider', - 'corsheaders', - 'pipeline', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.admin", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_extensions", + "django_prometheus", + "paypal.standard.ipn", + "apps.rss_feeds", + "apps.reader", + "apps.analyzer", + "apps.feed_import", + "apps.profile", + "apps.recommendations", + "apps.statistics", + "apps.notifications", + "apps.static", + "apps.mobile", + "apps.push", + "apps.social", + "apps.oauth", + "apps.search", + "apps.categories", + "utils", # missing models so no migrations + "vendor", + "typogrify", + "vendor.zebra", + "anymail", + "oauth2_provider", + "corsheaders", + "pipeline", ) # =================== @@ -378,30 +359,12 @@ # ========== CELERY_TASK_ROUTES = { - "work-queue": { - "queue": "work_queue", - "binding_key": "work_queue" - }, - "new-feeds": { - "queue": "new_feeds", - "binding_key": "new_feeds" - }, - "push-feeds": { - "queue": "push_feeds", - "binding_key": "push_feeds" - }, - "update-feeds": { - "queue": "update_feeds", - "binding_key": "update_feeds" - }, - "beat-tasks": { - "queue": "cron_queue", - "binding_key": "cron_queue" - }, - "search-indexer": { - "queue": "search_indexer", - "binding_key": "search_indexer" - }, + "work-queue": {"queue": "work_queue", "binding_key": "work_queue"}, + "new-feeds": {"queue": "new_feeds", "binding_key": "new_feeds"}, + "push-feeds": {"queue": "push_feeds", "binding_key": "push_feeds"}, + "update-feeds": {"queue": "update_feeds", "binding_key": "update_feeds"}, + "beat-tasks": {"queue": "cron_queue", "binding_key": "cron_queue"}, + "search-indexer": {"queue": "search_indexer", "binding_key": "search_indexer"}, } CELERY_TASK_QUEUES = { "work_queue": { @@ -409,114 +372,100 @@ "exchange_type": "direct", "binding_key": "work_queue", }, - "new_feeds": { - "exchange": "new_feeds", - "exchange_type": "direct", - "binding_key": "new_feeds" - }, - "push_feeds": { - "exchange": "push_feeds", - "exchange_type": "direct", - "binding_key": "push_feeds" - }, - "update_feeds": { - "exchange": "update_feeds", - "exchange_type": "direct", - "binding_key": "update_feeds" - }, - "cron_queue": { - "exchange": "cron_queue", - "exchange_type": "direct", - "binding_key": "cron_queue" - }, + "new_feeds": {"exchange": "new_feeds", "exchange_type": "direct", "binding_key": "new_feeds"}, + "push_feeds": {"exchange": "push_feeds", "exchange_type": "direct", "binding_key": "push_feeds"}, + "update_feeds": {"exchange": "update_feeds", "exchange_type": "direct", "binding_key": "update_feeds"}, + "cron_queue": {"exchange": "cron_queue", "exchange_type": "direct", "binding_key": "cron_queue"}, "beat_feeds_task": { "exchange": "beat_feeds_task", "exchange_type": "direct", - "binding_key": "beat_feeds_task" + "binding_key": "beat_feeds_task", }, "search_indexer": { "exchange": "search_indexer", "exchange_type": "direct", - "binding_key": "search_indexer" + "binding_key": "search_indexer", }, } CELERY_TASK_DEFAULT_QUEUE = "work_queue" CELERY_WORKER_PREFETCH_MULTIPLIER = 1 -CELERY_IMPORTS = ("apps.rss_feeds.tasks", - "apps.social.tasks", - "apps.reader.tasks", - "apps.profile.tasks", - "apps.feed_import.tasks", - "apps.search.tasks", - "apps.statistics.tasks",) -CELERY_TASK_IGNORE_RESULT = True -CELERY_TASK_ACKS_LATE = True # Retry if task fails +CELERY_IMPORTS = ( + "apps.rss_feeds.tasks", + "apps.social.tasks", + "apps.reader.tasks", + "apps.profile.tasks", + "apps.feed_import.tasks", + "apps.search.tasks", + "apps.statistics.tasks", +) +CELERY_TASK_IGNORE_RESULT = True +CELERY_TASK_ACKS_LATE = True # Retry if task fails CELERY_WORKER_MAX_TASKS_PER_CHILD = 10 -CELERY_TASK_TIME_LIMIT = 12 * 30 -CELERY_WORKER_DISABLE_RATE_LIMITS = True +CELERY_TASK_TIME_LIMIT = 12 * 30 +CELERY_WORKER_DISABLE_RATE_LIMITS = True SECONDS_TO_DELAY_CELERY_EMAILS = 60 CELERY_BEAT_SCHEDULE = { - 'task-feeds': { - 'task': 'task-feeds', - 'schedule': datetime.timedelta(minutes=1), - 'options': {'queue': 'beat_feeds_task'}, + "task-feeds": { + "task": "task-feeds", + "schedule": datetime.timedelta(minutes=1), + "options": {"queue": "beat_feeds_task"}, }, - 'task-broken-feeds': { - 'task': 'task-broken-feeds', - 'schedule': datetime.timedelta(hours=6), - 'options': {'queue': 'beat_feeds_task'}, + "task-broken-feeds": { + "task": "task-broken-feeds", + "schedule": datetime.timedelta(hours=6), + "options": {"queue": "beat_feeds_task"}, }, - 'freshen-homepage': { - 'task': 'freshen-homepage', - 'schedule': datetime.timedelta(hours=1), - 'options': {'queue': 'cron_queue'}, + "freshen-homepage": { + "task": "freshen-homepage", + "schedule": datetime.timedelta(hours=1), + "options": {"queue": "cron_queue"}, }, - 'collect-stats': { - 'task': 'collect-stats', - 'schedule': datetime.timedelta(minutes=1), - 'options': {'queue': 'cron_queue'}, + "collect-stats": { + "task": "collect-stats", + "schedule": datetime.timedelta(minutes=1), + "options": {"queue": "cron_queue"}, }, - 'collect-feedback': { - 'task': 'collect-feedback', - 'schedule': datetime.timedelta(minutes=1), - 'options': {'queue': 'cron_queue'}, + "collect-feedback": { + "task": "collect-feedback", + "schedule": datetime.timedelta(minutes=1), + "options": {"queue": "cron_queue"}, }, - 'share-popular-stories': { - 'task': 'share-popular-stories', - 'schedule': datetime.timedelta(minutes=10), - 'options': {'queue': 'cron_queue'}, + "share-popular-stories": { + "task": "share-popular-stories", + "schedule": datetime.timedelta(minutes=10), + "options": {"queue": "cron_queue"}, }, - 'clean-analytics': { - 'task': 'clean-analytics', - 'schedule': datetime.timedelta(hours=12), - 'options': {'queue': 'cron_queue', 'timeout': 720*10}, + "clean-analytics": { + "task": "clean-analytics", + "schedule": datetime.timedelta(hours=12), + "options": {"queue": "cron_queue", "timeout": 720 * 10}, }, - 'reimport-stripe-history': { - 'task': 'reimport-stripe-history', - 'schedule': datetime.timedelta(hours=6), - 'options': {'queue': 'cron_queue'}, + "reimport-stripe-history": { + "task": "reimport-stripe-history", + "schedule": datetime.timedelta(hours=6), + "options": {"queue": "cron_queue"}, }, # 'clean-spam': { # 'task': 'clean-spam', # 'schedule': datetime.timedelta(hours=1), # 'options': {'queue': 'cron_queue'}, # }, - 'clean-social-spam': { - 'task': 'clean-social-spam', - 'schedule': datetime.timedelta(hours=6), - 'options': {'queue': 'cron_queue'}, + "clean-social-spam": { + "task": "clean-social-spam", + "schedule": datetime.timedelta(hours=6), + "options": {"queue": "cron_queue"}, }, - 'premium-expire': { - 'task': 'premium-expire', - 'schedule': datetime.timedelta(hours=24), - 'options': {'queue': 'cron_queue'}, + "premium-expire": { + "task": "premium-expire", + "schedule": datetime.timedelta(hours=24), + "options": {"queue": "cron_queue"}, }, - 'activate-next-new-user': { - 'task': 'activate-next-new-user', - 'schedule': datetime.timedelta(minutes=5), - 'options': {'queue': 'cron_queue'}, + "activate-next-new-user": { + "task": "activate-next-new-user", + "schedule": datetime.timedelta(minutes=5), + "options": {"queue": "cron_queue"}, }, } @@ -528,32 +477,33 @@ else: MONGO_PORT = 27017 MONGO_DB = { - 'host': f'db_mongo:{MONGO_PORT}', - 'name': 'newsblur', + "host": f"db_mongo:{MONGO_PORT}", + "name": "newsblur", } MONGO_ANALYTICS_DB = { - 'host': f'db_mongo_analytics:{MONGO_PORT}', - 'name': 'nbanalytics', + "host": f"db_mongo_analytics:{MONGO_PORT}", + "name": "nbanalytics", } # ==================== # = Database Routers = # ==================== + class MasterSlaveRouter(object): """A router that sets up a simple master/slave configuration""" def db_for_read(self, model, **hints): "Point all read operations to a random slave" - return 'slave' + return "slave" def db_for_write(self, model, **hints): "Point all write operations to the master" - return 'default' + return "default" def allow_relation(self, obj1, obj2, **hints): "Allow any relation between two objects in the db pool" - db_list = ('slave','default') + db_list = ("slave", "default") if obj1._state.db in db_list and obj2._state.db in db_list: return True return None @@ -567,11 +517,11 @@ def allow_migrate(self, db, model): # = Social APIs = # =============== -FACEBOOK_APP_ID = '111111111111111' -FACEBOOK_SECRET = '99999999999999999999999999999999' -FACEBOOK_NAMESPACE = 'newsblur' -TWITTER_CONSUMER_KEY = 'ooooooooooooooooooooo' -TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' +FACEBOOK_APP_ID = "111111111111111" +FACEBOOK_SECRET = "99999999999999999999999999999999" +FACEBOOK_NAMESPACE = "newsblur" +TWITTER_CONSUMER_KEY = "ooooooooooooooooooooo" +TWITTER_CONSUMER_SECRET = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" YOUTUBE_API_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # =============== @@ -580,15 +530,15 @@ def allow_migrate(self, db, model): BACKED_BY_AWS = { - 'pages_on_s3': False, - 'icons_on_s3': False, + "pages_on_s3": False, + "icons_on_s3": False, } PROXY_S3_PAGES = True -S3_BACKUP_BUCKET = 'newsblur-backups' -S3_PAGES_BUCKET_NAME = 'pages.newsblur.com' -S3_ICONS_BUCKET_NAME = 'icons.newsblur.com' -S3_AVATARS_BUCKET_NAME = 'avatars.newsblur.com' +S3_BACKUP_BUCKET = "newsblur-backups" +S3_PAGES_BUCKET_NAME = "pages.newsblur.com" +S3_ICONS_BUCKET_NAME = "icons.newsblur.com" +S3_AVATARS_BUCKET_NAME = "avatars.newsblur.com" # ================== # = Configurations = @@ -605,12 +555,14 @@ def allow_migrate(self, db, model): started_task_or_app = False try: from newsblur_web.task_env import * + print(" ---> Starting NewsBlur task server...") started_task_or_app = True except ModuleNotFoundError: pass try: from newsblur_web.app_env import * + print(" ---> Starting NewsBlur app server...") started_task_or_app = True except ModuleNotFoundError: @@ -619,34 +571,29 @@ def allow_migrate(self, db, model): print(" ---> Starting NewsBlur development server...") if DOCKERBUILD: - CELERY_WORKER_CONCURRENCY = 2 + CELERY_WORKER_CONCURRENCY = 2 elif "task-work" in SERVER_NAME or SERVER_NAME.startswith("task-"): - CELERY_WORKER_CONCURRENCY = 4 + CELERY_WORKER_CONCURRENCY = 4 else: - CELERY_WORKER_CONCURRENCY = 24 - -if not DEBUG: - INSTALLED_APPS += ( - 'django_ses', + CELERY_WORKER_CONCURRENCY = 24 - ) +if not DEBUG: + INSTALLED_APPS += ("django_ses",) sentry_sdk.init( dsn=SENTRY_DSN, integrations=[DjangoIntegration(), RedisIntegration(), CeleryIntegration()], server_name=SERVER_NAME, - # Set traces_sample_rate to 1.0 to capture 100% # of transactions for performance monitoring. # We recommend adjusting this value in production, traces_sample_rate=0.01, - # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. - send_default_pii=True + send_default_pii=True, ) sentry_sdk.utils.MAX_STRING_LENGTH = 8192 - + COMPRESS = not DEBUG ACCOUNT_ACTIVATION_DAYS = 30 AWS_ACCESS_KEY_ID = S3_ACCESS_KEY @@ -655,10 +602,11 @@ def allow_migrate(self, db, model): os.environ["AWS_ACCESS_KEY_ID"] = AWS_ACCESS_KEY_ID os.environ["AWS_SECRET_ACCESS_KEY"] = AWS_SECRET_ACCESS_KEY + def clear_prometheus_aggregation_stats(): - prom_folder = '/srv/newsblur/.prom_cache' + prom_folder = "/srv/newsblur/.prom_cache" os.makedirs(prom_folder, exist_ok=True) - os.environ['PROMETHEUS_MULTIPROC_DIR'] = prom_folder + os.environ["PROMETHEUS_MULTIPROC_DIR"] = prom_folder for filename in os.listdir(prom_folder): file_path = os.path.join(prom_folder, filename) try: @@ -667,24 +615,27 @@ def clear_prometheus_aggregation_stats(): elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: - if 'No such file' in str(e): + if "No such file" in str(e): return - print('Failed to delete %s. Reason: %s' % (file_path, e)) + print("Failed to delete %s. Reason: %s" % (file_path, e)) clear_prometheus_aggregation_stats() if DEBUG: template_loaders = [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", ] else: template_loaders = [ - ('django.template.loaders.cached.Loader', ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - )), + ( + "django.template.loaders.cached.Loader", + ( + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ), + ), ] @@ -692,19 +643,21 @@ def clear_prometheus_aggregation_stats(): TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(NEWSBLUR_DIR, 'templates'), - os.path.join(NEWSBLUR_DIR, 'vendor/zebra/templates')], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(NEWSBLUR_DIR, "templates"), + os.path.join(NEWSBLUR_DIR, "vendor/zebra/templates"), + ], # 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ + "OPTIONS": { + "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.media", - 'django.template.context_processors.request', - 'django.contrib.messages.context_processors.messages', + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", ], - 'loaders': template_loaders, + "loaders": template_loaders, }, } ] @@ -726,14 +679,14 @@ def clear_prometheus_aggregation_stats(): monitoring.register(MONGO_COMMAND_LOGGER) MONGO_DB_DEFAULTS = { - 'name': 'newsblur', - 'host': f'db_mongo:{MONGO_PORT}', - 'alias': 'default', - 'unicode_decode_error_handler': 'ignore', - 'connect': False, + "name": "newsblur", + "host": f"db_mongo:{MONGO_PORT}", + "alias": "default", + "unicode_decode_error_handler": "ignore", + "connect": False, } MONGO_DB = dict(MONGO_DB_DEFAULTS, **MONGO_DB) -MONGO_DB_NAME = MONGO_DB.pop('name') +MONGO_DB_NAME = MONGO_DB.pop("name") # MONGO_URI = 'mongodb://%s' % (MONGO_DB.pop('host'),) # if MONGO_DB.get('read_preference', pymongo.ReadPreference.PRIMARY) != pymongo.ReadPreference.PRIMARY: @@ -747,18 +700,24 @@ def clear_prometheus_aggregation_stats(): # MONGODB = connect(host="mongodb://localhost:27017/newsblur", connect=False) MONGO_ANALYTICS_DB_DEFAULTS = { - 'name': 'nbanalytics', - 'host': f'db_mongo_analytics:{MONGO_PORT}', - 'alias': 'nbanalytics', + "name": "nbanalytics", + "host": f"db_mongo_analytics:{MONGO_PORT}", + "alias": "nbanalytics", } MONGO_ANALYTICS_DB = dict(MONGO_ANALYTICS_DB_DEFAULTS, **MONGO_ANALYTICS_DB) # MONGO_ANALYTICS_DB_NAME = MONGO_ANALYTICS_DB.pop('name') # MONGOANALYTICSDB = connect(MONGO_ANALYTICS_DB_NAME, **MONGO_ANALYTICS_DB) -if 'username' in MONGO_ANALYTICS_DB: - MONGOANALYTICSDB = connect(db=MONGO_ANALYTICS_DB['name'], host=f"mongodb://{MONGO_ANALYTICS_DB['username']}:{MONGO_ANALYTICS_DB['password']}@{MONGO_ANALYTICS_DB['host']}/?authSource=admin", alias="nbanalytics") +if "username" in MONGO_ANALYTICS_DB: + MONGOANALYTICSDB = connect( + db=MONGO_ANALYTICS_DB["name"], + host=f"mongodb://{MONGO_ANALYTICS_DB['username']}:{MONGO_ANALYTICS_DB['password']}@{MONGO_ANALYTICS_DB['host']}/?authSource=admin", + alias="nbanalytics", + ) else: - MONGOANALYTICSDB = connect(db=MONGO_ANALYTICS_DB['name'], host=f"mongodb://{MONGO_ANALYTICS_DB['host']}/", alias="nbanalytics") + MONGOANALYTICSDB = connect( + db=MONGO_ANALYTICS_DB["name"], host=f"mongodb://{MONGO_ANALYTICS_DB['host']}/", alias="nbanalytics" + ) # ========= @@ -777,149 +736,164 @@ def clear_prometheus_aggregation_stats(): REDIS_PUBSUB_PORT = 6383 if REDIS_USER is None: - # REDIS has been renamed to REDIS_USER. + # REDIS has been renamed to REDIS_USER. REDIS_USER = REDIS CELERY_REDIS_DB_NUM = 4 SESSION_REDIS_DB = 5 -CELERY_BROKER_URL = "redis://%s:%s/%s" % (REDIS_USER['host'], REDIS_USER_PORT,CELERY_REDIS_DB_NUM) +CELERY_BROKER_URL = "redis://%s:%s/%s" % (REDIS_USER["host"], REDIS_USER_PORT, CELERY_REDIS_DB_NUM) CELERY_RESULT_BACKEND = CELERY_BROKER_URL -BROKER_TRANSPORT_OPTIONS = { - "max_retries": 3, - "interval_start": 0, - "interval_step": 0.2, - "interval_max": 0.5 -} +BROKER_TRANSPORT_OPTIONS = {"max_retries": 3, "interval_start": 0, "interval_step": 0.2, "interval_max": 0.5} SESSION_REDIS = { - 'host': REDIS_SESSIONS['host'], - 'port': REDIS_SESSION_PORT, - 'db': SESSION_REDIS_DB, + "host": REDIS_SESSIONS["host"], + "port": REDIS_SESSION_PORT, + "db": SESSION_REDIS_DB, # 'password': 'password', - 'prefix': '', - 'socket_timeout': 10, - 'retry_on_timeout': True + "prefix": "", + "socket_timeout": 10, + "retry_on_timeout": True, } CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://%s:%s/6' % (REDIS_USER['host'], REDIS_USER_PORT), + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://%s:%s/6" % (REDIS_USER["host"], REDIS_USER_PORT), }, } -REDIS_POOL = redis.ConnectionPool(host=REDIS_USER['host'], port=REDIS_USER_PORT, db=0, decode_responses=True) -REDIS_ANALYTICS_POOL = redis.ConnectionPool(host=REDIS_USER['host'], port=REDIS_USER_PORT, db=2, decode_responses=True) -REDIS_STATISTICS_POOL = redis.ConnectionPool(host=REDIS_USER['host'], port=REDIS_USER_PORT, db=3, decode_responses=True) -REDIS_FEED_UPDATE_POOL = redis.ConnectionPool(host=REDIS_USER['host'], port=REDIS_USER_PORT, db=4, decode_responses=True) -REDIS_STORY_HASH_TEMP_POOL = redis.ConnectionPool(host=REDIS_USER['host'], port=REDIS_USER_PORT, db=10, decode_responses=True) +REDIS_POOL = redis.ConnectionPool(host=REDIS_USER["host"], port=REDIS_USER_PORT, db=0, decode_responses=True) +REDIS_ANALYTICS_POOL = redis.ConnectionPool( + host=REDIS_USER["host"], port=REDIS_USER_PORT, db=2, decode_responses=True +) +REDIS_STATISTICS_POOL = redis.ConnectionPool( + host=REDIS_USER["host"], port=REDIS_USER_PORT, db=3, decode_responses=True +) +REDIS_FEED_UPDATE_POOL = redis.ConnectionPool( + host=REDIS_USER["host"], port=REDIS_USER_PORT, db=4, decode_responses=True +) +REDIS_STORY_HASH_TEMP_POOL = redis.ConnectionPool( + host=REDIS_USER["host"], port=REDIS_USER_PORT, db=10, decode_responses=True +) # REDIS_CACHE_POOL = redis.ConnectionPool(host=REDIS_USER['host'], port=REDIS_USER_PORT, db=6) # Duped in CACHES -REDIS_STORY_HASH_POOL = redis.ConnectionPool(host=REDIS_STORY['host'], port=REDIS_STORY_PORT, db=1, decode_responses=True) -REDIS_FEED_READ_POOL = redis.ConnectionPool(host=REDIS_SESSIONS['host'], port=REDIS_SESSION_PORT, db=1, decode_responses=True) -REDIS_FEED_SUB_POOL = redis.ConnectionPool(host=REDIS_SESSIONS['host'], port=REDIS_SESSION_PORT, db=2, decode_responses=True) -REDIS_SESSION_POOL = redis.ConnectionPool(host=REDIS_SESSIONS['host'], port=REDIS_SESSION_PORT, db=5, decode_responses=True) -REDIS_PUBSUB_POOL = redis.ConnectionPool(host=REDIS_PUBSUB['host'], port=REDIS_PUBSUB_PORT, db=0, decode_responses=True) +REDIS_STORY_HASH_POOL = redis.ConnectionPool( + host=REDIS_STORY["host"], port=REDIS_STORY_PORT, db=1, decode_responses=True +) +REDIS_FEED_READ_POOL = redis.ConnectionPool( + host=REDIS_SESSIONS["host"], port=REDIS_SESSION_PORT, db=1, decode_responses=True +) +REDIS_FEED_SUB_POOL = redis.ConnectionPool( + host=REDIS_SESSIONS["host"], port=REDIS_SESSION_PORT, db=2, decode_responses=True +) +REDIS_SESSION_POOL = redis.ConnectionPool( + host=REDIS_SESSIONS["host"], port=REDIS_SESSION_PORT, db=5, decode_responses=True +) +REDIS_PUBSUB_POOL = redis.ConnectionPool( + host=REDIS_PUBSUB["host"], port=REDIS_PUBSUB_PORT, db=0, decode_responses=True +) # ========== # = Celery = # ========== # celeryapp.autodiscover_tasks(INSTALLED_APPS) -accept_content = ['pickle', 'json', 'msgpack', 'yaml'] +accept_content = ["pickle", "json", "msgpack", "yaml"] # ========== # = Assets = # ========== -STATIC_URL = '/static/' +STATIC_URL = "/static/" # STATICFILES_STORAGE = 'pipeline.storage.PipelineManifestStorage' -STATICFILES_STORAGE = 'utils.pipeline_utils.PipelineStorage' +STATICFILES_STORAGE = "utils.pipeline_utils.PipelineStorage" # STATICFILES_STORAGE = 'utils.pipeline_utils.GzipPipelineStorage' STATICFILES_FINDERS = ( # 'pipeline.finders.FileSystemFinder', # 'django.contrib.staticfiles.finders.FileSystemFinder', # 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 'pipeline.finders.AppDirectoriesFinder', - 'utils.pipeline_utils.AppDirectoriesFinder', - 'utils.pipeline_utils.FileSystemFinder', + "utils.pipeline_utils.AppDirectoriesFinder", + "utils.pipeline_utils.FileSystemFinder", # 'pipeline.finders.PipelineFinder', ) STATICFILES_DIRS = [ # '/usr/local/lib/python3.9/site-packages/django/contrib/admin/static/', MEDIA_ROOT, ] -with open(os.path.join(SETTINGS_DIR, 'assets.yml')) as stream: +with open(os.path.join(SETTINGS_DIR, "assets.yml")) as stream: assets = yaml.safe_load(stream) PIPELINE = { - 'PIPELINE_ENABLED': not DEBUG_ASSETS, - 'PIPELINE_COLLECTOR_ENABLED': not DEBUG_ASSETS, - 'SHOW_ERRORS_INLINE': DEBUG_ASSETS, - 'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor', - 'JS_COMPRESSOR': 'pipeline.compressors.closure.ClosureCompressor', + "PIPELINE_ENABLED": not DEBUG_ASSETS, + "PIPELINE_COLLECTOR_ENABLED": not DEBUG_ASSETS, + "SHOW_ERRORS_INLINE": DEBUG_ASSETS, + "CSS_COMPRESSOR": "pipeline.compressors.yuglify.YuglifyCompressor", + "JS_COMPRESSOR": "pipeline.compressors.closure.ClosureCompressor", # 'CSS_COMPRESSOR': 'pipeline.compressors.NoopCompressor', # 'JS_COMPRESSOR': 'pipeline.compressors.NoopCompressor', - 'CLOSURE_BINARY': '/usr/bin/java -jar /usr/local/bin/compiler.jar', - 'CLOSURE_ARGUMENTS': '--language_in ECMASCRIPT_2016 --language_out ECMASCRIPT_2016 --warning_level DEFAULT', - 'JAVASCRIPT': { - 'common': { - 'source_filenames': assets['javascripts']['common'], - 'output_filename': 'js/common.js', + "CLOSURE_BINARY": "/usr/bin/java -jar /usr/local/bin/compiler.jar", + "CLOSURE_ARGUMENTS": "--language_in ECMASCRIPT_2016 --language_out ECMASCRIPT_2016 --warning_level DEFAULT", + "JAVASCRIPT": { + "common": { + "source_filenames": assets["javascripts"]["common"], + "output_filename": "js/common.js", }, - 'statistics': { - 'source_filenames': assets['javascripts']['statistics'], - 'output_filename': 'js/statistics.js', + "statistics": { + "source_filenames": assets["javascripts"]["statistics"], + "output_filename": "js/statistics.js", }, - 'payments': { - 'source_filenames': assets['javascripts']['payments'], - 'output_filename': 'js/payments.js', + "payments": { + "source_filenames": assets["javascripts"]["payments"], + "output_filename": "js/payments.js", }, - 'bookmarklet': { - 'source_filenames': assets['javascripts']['bookmarklet'], - 'output_filename': 'js/bookmarklet.js', + "bookmarklet": { + "source_filenames": assets["javascripts"]["bookmarklet"], + "output_filename": "js/bookmarklet.js", }, - 'blurblog': { - 'source_filenames': assets['javascripts']['blurblog'], - 'output_filename': 'js/blurblog.js', + "blurblog": { + "source_filenames": assets["javascripts"]["blurblog"], + "output_filename": "js/blurblog.js", }, }, - 'STYLESHEETS': { - 'common': { - 'source_filenames': assets['stylesheets']['common'], - 'output_filename': 'css/common.css', + "STYLESHEETS": { + "common": { + "source_filenames": assets["stylesheets"]["common"], + "output_filename": "css/common.css", # 'variant': 'datauri', }, - 'bookmarklet': { - 'source_filenames': assets['stylesheets']['bookmarklet'], - 'output_filename': 'css/bookmarklet.css', + "bookmarklet": { + "source_filenames": assets["stylesheets"]["bookmarklet"], + "output_filename": "css/bookmarklet.css", # 'variant': 'datauri', }, - 'blurblog': { - 'source_filenames': assets['stylesheets']['blurblog'], - 'output_filename': 'css/blurblog.css', + "blurblog": { + "source_filenames": assets["stylesheets"]["blurblog"], + "output_filename": "css/blurblog.css", # 'variant': 'datauri', }, - } + }, } -paypalrestsdk.configure({ - "mode": "sandbox" if DEBUG else "live", - "client_id": PAYPAL_API_CLIENTID, - "client_secret": PAYPAL_API_SECRET -}) +paypalrestsdk.configure( + { + "mode": "sandbox" if DEBUG else "live", + "client_id": PAYPAL_API_CLIENTID, + "client_secret": PAYPAL_API_SECRET, + } +) # ======= # = AWS = # ======= S3_CONN = None -if BACKED_BY_AWS.get('pages_on_s3') or BACKED_BY_AWS.get('icons_on_s3'): +if BACKED_BY_AWS.get("pages_on_s3") or BACKED_BY_AWS.get("icons_on_s3"): boto_session = boto3.Session( aws_access_key_id=S3_ACCESS_KEY, aws_secret_access_key=S3_SECRET, ) - S3_CONN = boto_session.resource('s3') + S3_CONN = boto_session.resource("s3") django.http.request.host_validation_re = re.compile(r"^([a-z0-9.-_\-]+|\[[a-f0-9]*:[a-f0-9:]+\])(:\d+)?$") @@ -940,6 +914,7 @@ def monkey_patched_get_user(request): and when this monkey patch is removed. """ from django.contrib.auth.models import AnonymousUser + user = None try: user_id = auth._get_user_session_key(request) @@ -951,7 +926,11 @@ def monkey_patched_get_user(request): backend = auth.load_backend(backend_path) user = backend.get_user(user_id) session_hash = request.session.get(auth.HASH_SESSION_KEY) - logging.debug(request, " ---> Ignoring session hash: %s vs %s" % (user.get_session_auth_hash() if user else "[no user]", session_hash)) + logging.debug( + request, + " ---> Ignoring session hash: %s vs %s" + % (user.get_session_auth_hash() if user else "[no user]", session_hash), + ) # # Verify the session # if hasattr(user, 'get_session_auth_hash'): # session_hash = request.session.get(HASH_SESSION_KEY) @@ -965,4 +944,5 @@ def monkey_patched_get_user(request): return user or AnonymousUser() + auth.get_user = monkey_patched_get_user diff --git a/newsblur_web/sitecustomize.py b/newsblur_web/sitecustomize.py index 80ae27febd..0fb429ec57 100644 --- a/newsblur_web/sitecustomize.py +++ b/newsblur_web/sitecustomize.py @@ -1,7 +1,8 @@ import sys -sys.setdefaultencoding('utf-8') + +sys.setdefaultencoding("utf-8") import os -os.putenv('LANG', 'en_US.UTF-8') -os.putenv('LC_ALL', 'en_US.UTF-8') +os.putenv("LANG", "en_US.UTF-8") +os.putenv("LC_ALL", "en_US.UTF-8") diff --git a/newsblur_web/test_settings.py b/newsblur_web/test_settings.py index bdc58a4490..a4480bda0b 100644 --- a/newsblur_web/test_settings.py +++ b/newsblur_web/test_settings.py @@ -1,13 +1,15 @@ import os + DOCKERBUILD = os.getenv("DOCKERBUILD") from newsblur_web.settings import * -DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3' -DATABASES['default']['OPTIONS'] = {} -DATABASES['default']['NAME'] = 'nb.db' -DATABASES['default']['TEST_NAME'] = os.path.join(BASE_DIR, 'db.sqlite3.test') + +DATABASES["default"]["ENGINE"] = "django.db.backends.sqlite3" +DATABASES["default"]["OPTIONS"] = {} +DATABASES["default"]["NAME"] = "nb.db" +DATABASES["default"]["TEST_NAME"] = os.path.join(BASE_DIR, "db.sqlite3.test") -#DATABASES['default'] = { +# DATABASES['default'] = { # 'NAME': 'newslur', # 'ENGINE': 'django.db.backends.postgresql_psycopg2', # 'USER': 'newsblur', @@ -29,19 +31,19 @@ if DOCKERBUILD: MONGO_PORT = 29019 MONGO_DB = { - 'name': 'newsblur_test', - 'host': 'db_mongo:29019', + "name": "newsblur_test", + "host": "db_mongo:29019", } else: MONGO_PORT = 27017 MONGO_DB = { - 'name': 'newsblur_test', - 'host': '127.0.0.1:27017', + "name": "newsblur_test", + "host": "127.0.0.1:27017", } SERVER_NAME -MONGO_DATABASE_NAME = 'test_newsblur' +MONGO_DATABASE_NAME = "test_newsblur" SOUTH_TESTS_MIGRATE = False DAYS_OF_UNREAD = 9999 @@ -50,5 +52,5 @@ DEBUG = True SITE_ID = 2 SENTRY_DSN = None -HOMEPAGE_USERNAME = 'conesus' -SERVER_NAME = 'test_newsblur' +HOMEPAGE_USERNAME = "conesus" +SERVER_NAME = "test_newsblur" diff --git a/newsblur_web/urls.py b/newsblur_web/urls.py index 3520243b8e..309ad76fea 100644 --- a/newsblur_web/urls.py +++ b/newsblur_web/urls.py @@ -1,83 +1,90 @@ -from django.conf.urls import include, url from django.conf import settings -from apps.reader import views as reader_views -from apps.social import views as social_views -from apps.static import views as static_views -from apps.profile import views as profile_views +from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth.views import LogoutView +from apps.profile import views as profile_views +from apps.reader import views as reader_views +from apps.social import views as social_views +from apps.static import views as static_views + admin.autodiscover() urlpatterns = [ - url(r'^$', reader_views.index, name='index'), - url(r'^reader/', include('apps.reader.urls')), - url(r'^add/?', reader_views.index), - url(r'^try/?', reader_views.index), - url(r'^site/(?P\d+)?', reader_views.index), - url(r'^folder/(?P\d+)?', reader_views.index, name='folder'), - url(r'^saved/(?P\d+)?', reader_views.index, name='saved-stories-tag'), - url(r'^saved/?', reader_views.index), - url(r'^read/?', reader_views.index), - url(r'^social/\d+/.*?', reader_views.index), - url(r'^user/.*?', reader_views.index), - url(r'^null/.*?', reader_views.index), - url(r'^story/.*?', reader_views.index), - url(r'^feed/?', social_views.shared_stories_rss_feed_noid), - url(r'^rss_feeds/', include('apps.rss_feeds.urls')), - url(r'^analyzer/', include('apps.analyzer.urls')), - url(r'^classifier/', include('apps.analyzer.urls')), - url(r'^folder_rss/', include('apps.profile.urls')), - url(r'^profile/', include('apps.profile.urls')), - url(r'^import/', include('apps.feed_import.urls')), - url(r'^api/', include('apps.api.urls')), - url(r'^recommendations/', include('apps.recommendations.urls')), - url(r'^notifications/?', include('apps.notifications.urls')), - url(r'^statistics/', include('apps.statistics.urls')), - url(r'^social/', include('apps.social.urls')), - url(r'^search/', include('apps.search.urls')), - url(r'^oauth/', include('apps.oauth.urls')), - url(r'^mobile/', include('apps.mobile.urls')), - url(r'^m/', include('apps.mobile.urls')), - url(r'^push/', include('apps.push.urls')), - url(r'^newsletters/', include('apps.newsletters.urls')), - url(r'^categories/', include('apps.categories.urls')), - url(r'^_haproxychk', static_views.haproxy_check), - url(r'^_dbcheck/postgres', static_views.postgres_check), - url(r'^_dbcheck/mongo', static_views.mongo_check), - url(r'^_dbcheck/redis', static_views.redis_check), - url(r'^_dbcheck/elasticsearch', static_views.elasticsearch_check), - url(r'^admin/', admin.site.urls), - url(r'^about/?', static_views.about, name='about'), - url(r'^faq/?', static_views.faq, name='faq'), - url(r'^api/?$', static_views.api, name='api'), - url(r'^press/?', static_views.press, name='press'), - url(r'^feedback/?', static_views.feedback, name='feedback'), - url(r'^privacy/?', static_views.privacy, name='privacy'), - url(r'^tos/?', static_views.tos, name='tos'), - url(r'^manifest.webmanifest', static_views.webmanifest, name='webmanifest'), - url(r'^.well-known/apple-app-site-association', static_views.apple_app_site_assoc, name='apple-app-site-assoc'), - url(r'^.well-known/apple-developer-merchantid-domain-association', static_views.apple_developer_merchantid, name='apple-developer-merchantid'), - url(r'^ios/download/?', static_views.ios_download, name='ios-download'), - url(r'^ios/NewsBlur.plist', static_views.ios_plist, name='ios-download-plist'), - url(r'^ios/NewsBlur.ipa', static_views.ios_ipa, name='ios-download-ipa'), - url(r'^ios/?', static_views.ios, name='ios-static'), - url(r'^iphone/?', static_views.ios), - url(r'^ipad/?', static_views.ios), - url(r'^android/?', static_views.android, name='android-static'), - url(r'^firefox/?', static_views.firefox, name='firefox'), - url(r'zebra/', include('zebra.urls', namespace="zebra")), - url(r'^account/redeem_code/?$', profile_views.redeem_code, name='redeem-code'), - url(r'^account/login/?$', profile_views.login, name='login'), - url(r'^account/signup/?$', profile_views.signup, name='signup'), - url(r'^account/logout/?$', - LogoutView, - {'next_page': '/'}, name='logout'), - url(r'^account/ifttt/v1/', include('apps.oauth.urls')), - url(r'^account/', include('oauth2_provider.urls', namespace='oauth2_provider')), - url(r'^monitor/', include('apps.monitor.urls'), name="monitor"), - url('', include('django_prometheus.urls')), + url(r"^$", reader_views.index, name="index"), + url(r"^reader/", include("apps.reader.urls")), + url(r"^add/?", reader_views.index), + url(r"^try/?", reader_views.index), + url(r"^site/(?P\d+)?", reader_views.index), + url(r"^folder/(?P\d+)?", reader_views.index, name="folder"), + url(r"^saved/(?P\d+)?", reader_views.index, name="saved-stories-tag"), + url(r"^saved/?", reader_views.index), + url(r"^read/?", reader_views.index), + url(r"^social/\d+/.*?", reader_views.index), + url(r"^user/.*?", reader_views.index), + url(r"^null/.*?", reader_views.index), + url(r"^story/.*?", reader_views.index), + url(r"^feed/?", social_views.shared_stories_rss_feed_noid), + url(r"^rss_feeds/", include("apps.rss_feeds.urls")), + url(r"^analyzer/", include("apps.analyzer.urls")), + url(r"^classifier/", include("apps.analyzer.urls")), + url(r"^folder_rss/", include("apps.profile.urls")), + url(r"^profile/", include("apps.profile.urls")), + url(r"^import/", include("apps.feed_import.urls")), + url(r"^api/", include("apps.api.urls")), + url(r"^recommendations/", include("apps.recommendations.urls")), + url(r"^notifications/?", include("apps.notifications.urls")), + url(r"^statistics/", include("apps.statistics.urls")), + url(r"^social/", include("apps.social.urls")), + url(r"^search/", include("apps.search.urls")), + url(r"^oauth/", include("apps.oauth.urls")), + url(r"^mobile/", include("apps.mobile.urls")), + url(r"^m/", include("apps.mobile.urls")), + url(r"^push/", include("apps.push.urls")), + url(r"^newsletters/", include("apps.newsletters.urls")), + url(r"^categories/", include("apps.categories.urls")), + url(r"^_haproxychk", static_views.haproxy_check), + url(r"^_dbcheck/postgres", static_views.postgres_check), + url(r"^_dbcheck/mongo", static_views.mongo_check), + url(r"^_dbcheck/redis", static_views.redis_check), + url(r"^_dbcheck/elasticsearch", static_views.elasticsearch_check), + url(r"^admin/", admin.site.urls), + url(r"^about/?", static_views.about, name="about"), + url(r"^faq/?", static_views.faq, name="faq"), + url(r"^api/?$", static_views.api, name="api"), + url(r"^press/?", static_views.press, name="press"), + url(r"^feedback/?", static_views.feedback, name="feedback"), + url(r"^privacy/?", static_views.privacy, name="privacy"), + url(r"^tos/?", static_views.tos, name="tos"), + url(r"^manifest.webmanifest", static_views.webmanifest, name="webmanifest"), + url( + r"^.well-known/apple-app-site-association", + static_views.apple_app_site_assoc, + name="apple-app-site-assoc", + ), + url( + r"^.well-known/apple-developer-merchantid-domain-association", + static_views.apple_developer_merchantid, + name="apple-developer-merchantid", + ), + url(r"^ios/download/?", static_views.ios_download, name="ios-download"), + url(r"^ios/NewsBlur.plist", static_views.ios_plist, name="ios-download-plist"), + url(r"^ios/NewsBlur.ipa", static_views.ios_ipa, name="ios-download-ipa"), + url(r"^ios/?", static_views.ios, name="ios-static"), + url(r"^iphone/?", static_views.ios), + url(r"^ipad/?", static_views.ios), + url(r"^android/?", static_views.android, name="android-static"), + url(r"^firefox/?", static_views.firefox, name="firefox"), + url(r"zebra/", include("zebra.urls", namespace="zebra")), + url(r"^account/redeem_code/?$", profile_views.redeem_code, name="redeem-code"), + url(r"^account/login/?$", profile_views.login, name="login"), + url(r"^account/signup/?$", profile_views.signup, name="signup"), + url(r"^account/logout/?$", LogoutView, {"next_page": "/"}, name="logout"), + url(r"^account/ifttt/v1/", include("apps.oauth.urls")), + url(r"^account/", include("oauth2_provider.urls", namespace="oauth2_provider")), + url(r"^monitor/", include("apps.monitor.urls"), name="monitor"), + url("", include("django_prometheus.urls")), ] if settings.DEBUG: diff --git a/newsblur_web/wsgi.py b/newsblur_web/wsgi.py index cfbfbc22fe..2f071b032b 100644 --- a/newsblur_web/wsgi.py +++ b/newsblur_web/wsgi.py @@ -6,7 +6,9 @@ """ import os + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsblur_web.settings") from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() \ No newline at end of file + +application = get_wsgi_application() diff --git a/perf/locust.py b/perf/locust.py index 38587d8efa..b9bd6a6991 100644 --- a/perf/locust.py +++ b/perf/locust.py @@ -1,7 +1,9 @@ -import time -from locust import HttpUser, task, between import os +import time + import requests +from locust import HttpUser, between, task + class NB_PerfTest(HttpUser): wait_time = between(1, 2.5) diff --git a/utils/PyRSS2Gen.py b/utils/PyRSS2Gen.py index 8a4ff827e6..19bb11cd5b 100644 --- a/utils/PyRSS2Gen.py +++ b/utils/PyRSS2Gen.py @@ -8,16 +8,18 @@ import datetime + # Could make this the base class; will need to add 'publish' class WriteXmlMixin: - def write_xml(self, outfile, encoding = "iso-8859-1"): + def write_xml(self, outfile, encoding="iso-8859-1"): from xml.sax import saxutils + handler = saxutils.XMLGenerator(outfile, encoding) handler.startDocument() self.publish(handler) handler.endDocument() - def to_xml(self, encoding = "iso-8859-1"): + def to_xml(self, encoding="iso-8859-1"): try: import io as StringIO except ImportError: @@ -27,7 +29,7 @@ def to_xml(self, encoding = "iso-8859-1"): return f.getvalue() -def _element(handler, name, obj, d = {}): +def _element(handler, name, obj, d={}): if isinstance(obj, str) or obj is None: # special-case handling to make the API easier # to use for the common case. @@ -39,6 +41,7 @@ def _element(handler, name, obj, d = {}): # It better know how to emit the correct XML. obj.publish(handler) + def _opt_element(handler, name, obj): if obj is None: return @@ -58,13 +61,16 @@ def _format_date(dt): # rfc822 and email.Utils modules assume a timestamp. The # following is based on the rfc822 module. return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( - ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()], - dt.day, - ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month-1], - dt.year, dt.hour, dt.minute, dt.second) + ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()], + dt.day, + ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month - 1], + dt.year, + dt.hour, + dt.minute, + dt.second, + ) + - ## # A couple simple wrapper objects for the fields which # take a simple value other than a string. @@ -72,19 +78,23 @@ class IntElement: """implements the 'publish' API for integers Takes the tag name and the integer value to publish. - + (Could be used for anything which uses str() to be published to text for XML.) """ + element_attrs = {} + def __init__(self, name, val): self.name = name self.val = val + def publish(self, handler): handler.startElement(self.name, self.element_attrs) handler.characters(str(self.val)) handler.endElement(self.name) + class DateElement: """implements the 'publish' API for a datetime.datetime @@ -92,53 +102,70 @@ class DateElement: Converts the datetime to RFC 2822 timestamp (4-digit year). """ + def __init__(self, name, dt): self.name = name self.dt = dt + def publish(self, handler): _element(handler, self.name, _format_date(self.dt)) + + #### + class Category: """Publish a category element""" - def __init__(self, category, domain = None): + + def __init__(self, category, domain=None): self.category = category self.domain = domain + def publish(self, handler): d = {} if self.domain is not None: d["domain"] = self.domain _element(handler, "category", self.category, d) + class Cloud: """Publish a cloud""" - def __init__(self, domain, port, path, - registerProcedure, protocol): + + def __init__(self, domain, port, path, registerProcedure, protocol): self.domain = domain self.port = port self.path = path self.registerProcedure = registerProcedure self.protocol = protocol + def publish(self, handler): - _element(handler, "cloud", None, { - "domain": self.domain, - "port": str(self.port), - "path": self.path, - "registerProcedure": self.registerProcedure, - "protocol": self.protocol}) + _element( + handler, + "cloud", + None, + { + "domain": self.domain, + "port": str(self.port), + "path": self.path, + "registerProcedure": self.registerProcedure, + "protocol": self.protocol, + }, + ) + class Image: """Publish a channel Image""" + element_attrs = {} - def __init__(self, url, title, link, - width = None, height = None, description = None): + + def __init__(self, url, title, link, width=None, height=None, description=None): self.url = url self.title = title self.link = link self.width = width self.height = height self.description = description - + def publish(self, handler): handler.startElement("image", self.element_attrs) @@ -150,7 +177,7 @@ def publish(self, handler): if isinstance(width, int): width = IntElement("width", width) _opt_element(handler, "width", width) - + height = self.height if isinstance(height, int): height = IntElement("height", height) @@ -160,15 +187,18 @@ def publish(self, handler): handler.endElement("image") + class Guid: """Publish a guid Defaults to being a permalink, which is the assumption if it's omitted. Hence strings are always permalinks. """ - def __init__(self, guid, isPermaLink = 1): + + def __init__(self, guid, isPermaLink=1): self.guid = guid self.isPermaLink = isPermaLink + def publish(self, handler): d = {} if self.isPermaLink: @@ -177,12 +207,15 @@ def publish(self, handler): d["isPermaLink"] = "false" _element(handler, "guid", self.guid, d) + class TextInput: """Publish a textInput Apparently this is rarely used. """ + element_attrs = {} + def __init__(self, title, description, name, link): self.title = title self.description = description @@ -196,37 +229,51 @@ def publish(self, handler): _element(handler, "name", self.name) _element(handler, "link", self.link) handler.endElement("textInput") - + class Enclosure: """Publish an enclosure""" + def __init__(self, url, length, type): self.url = url self.length = length self.type = type + def publish(self, handler): - _element(handler, "enclosure", None, - {"url": self.url, - "length": str(self.length), - "type": self.type, - }) + _element( + handler, + "enclosure", + None, + { + "url": self.url, + "length": str(self.length), + "type": self.type, + }, + ) + class Source: """Publish the item's original source, used by aggregators""" + def __init__(self, name, url): self.name = name self.url = url + def publish(self, handler): _element(handler, "source", self.name, {"url": self.url}) + class SkipHours: """Publish the skipHours This takes a list of hours, as integers. """ + element_attrs = {} + def __init__(self, hours): self.hours = hours + def publish(self, handler): if self.hours: handler.startElement("skipHours", self.element_attrs) @@ -234,14 +281,18 @@ def publish(self, handler): _element(handler, "hour", str(hour)) handler.endElement("skipHours") + class SkipDays: """Publish the skipDays This takes a list of days as strings. """ + element_attrs = {} + def __init__(self, days): self.days = days + def publish(self, handler): if self.days: handler.startElement("skipDays", self.element_attrs) @@ -249,41 +300,40 @@ def publish(self, handler): _element(handler, "day", day) handler.endElement("skipDays") + class RSS2(WriteXmlMixin): """The main RSS class. Stores the channel attributes, with the "category" elements under ".categories" and the RSS items under ".items". """ - + rss_attrs = {"version": "2.0"} element_attrs = {} - def __init__(self, - title, - link, - description, - - language = None, - copyright = None, - managingEditor = None, - webMaster = None, - pubDate = None, # a datetime, *in* *GMT* - lastBuildDate = None, # a datetime - - categories = None, # list of strings or Category - generator = _generator_name, - docs = "http://blogs.law.harvard.edu/tech/rss", - cloud = None, # a Cloud - ttl = None, # integer number of minutes - - image = None, # an Image - rating = None, # a string; I don't know how it's used - textInput = None, # a TextInput - skipHours = None, # a SkipHours with a list of integers - skipDays = None, # a SkipDays with a list of strings - - items = None, # list of RSSItems - ): + + def __init__( + self, + title, + link, + description, + language=None, + copyright=None, + managingEditor=None, + webMaster=None, + pubDate=None, # a datetime, *in* *GMT* + lastBuildDate=None, # a datetime + categories=None, # list of strings or Category + generator=_generator_name, + docs="http://blogs.law.harvard.edu/tech/rss", + cloud=None, # a Cloud + ttl=None, # integer number of minutes + image=None, # an Image + rating=None, # a string; I don't know how it's used + textInput=None, # a TextInput + skipHours=None, # a SkipHours with a list of integers + skipDays=None, # a SkipDays with a list of strings + items=None, # list of RSSItems + ): self.title = title self.link = link self.description = description @@ -294,7 +344,7 @@ def __init__(self, self.webMaster = webMaster self.pubDate = pubDate self.lastBuildDate = lastBuildDate - + if categories is None: categories = [] self.categories = categories @@ -320,7 +370,7 @@ def publish(self, handler): _element(handler, "description", self.description) self.publish_extensions(handler) - + _opt_element(handler, "language", self.language) _opt_element(handler, "copyright", self.copyright) _opt_element(handler, "managingEditor", self.managingEditor) @@ -374,27 +424,27 @@ def publish_extensions(self, handler): # output after the three required fields. pass - - + class RSSItem(WriteXmlMixin): """Publish an RSS Item""" + element_attrs = {} - def __init__(self, - title = None, # string - link = None, # url as string - description = None, # string - author = None, # email address as string - categories = None, # list of string or Category - comments = None, # url as string - enclosure = None, # an Enclosure - guid = None, # a unique string - pubDate = None, # a datetime - source = None, # a Source - ): - + + def __init__( + self, + title=None, # string + link=None, # url as string + description=None, # string + author=None, # email address as string + categories=None, # list of string or Category + comments=None, # url as string + enclosure=None, # an Enclosure + guid=None, # a unique string + pubDate=None, # a datetime + source=None, # a Source + ): if title is None and description is None: - raise TypeError( - "must define at least one of 'title' or 'description'") + raise TypeError("must define at least one of 'title' or 'description'") self.title = title self.link = link self.description = description @@ -421,7 +471,7 @@ def publish(self, handler): if isinstance(category, str): category = Category(category) category.publish(handler) - + _opt_element(handler, "comments", self.comments) if self.enclosure is not None: self.enclosure.publish(handler) @@ -434,7 +484,7 @@ def publish(self, handler): if self.source is not None: self.source.publish(handler) - + handler.endElement("item") def publish_extensions(self, handler): diff --git a/utils/S3.py b/utils/S3.py index 5e219d06ca..489d4dd8a1 100644 --- a/utils/S3.py +++ b/utils/S3.py @@ -13,40 +13,43 @@ import hmac import http.client import re -import sha import sys import time -import urllib.request, urllib.parse, urllib.error +import urllib.error import urllib.parse +import urllib.request import xml.sax -DEFAULT_HOST = 's3.amazonaws.com' -PORTS_BY_SECURITY = { True: 443, False: 80 } -METADATA_PREFIX = 'x-amz-meta-' -AMAZON_HEADER_PREFIX = 'x-amz-' +import sha + +DEFAULT_HOST = "s3.amazonaws.com" +PORTS_BY_SECURITY = {True: 443, False: 80} +METADATA_PREFIX = "x-amz-meta-" +AMAZON_HEADER_PREFIX = "x-amz-" + # generates the aws canonical string for the given parameters def canonical_string(method, bucket="", key="", query_args={}, headers={}, expires=None): interesting_headers = {} for header_key in headers: lk = header_key.lower() - if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX): + if lk in ["content-md5", "content-type", "date"] or lk.startswith(AMAZON_HEADER_PREFIX): interesting_headers[lk] = headers[header_key].strip() # these keys get empty strings if they don't exist - if 'content-type' not in interesting_headers: - interesting_headers['content-type'] = '' - if 'content-md5' not in interesting_headers: - interesting_headers['content-md5'] = '' + if "content-type" not in interesting_headers: + interesting_headers["content-type"] = "" + if "content-md5" not in interesting_headers: + interesting_headers["content-md5"] = "" # just in case someone used this. it's not necessary in this lib. - if 'x-amz-date' in interesting_headers: - interesting_headers['date'] = '' + if "x-amz-date" in interesting_headers: + interesting_headers["date"] = "" # if you're using expires for query string auth, then it trumps date # (and x-amz-date) if expires: - interesting_headers['date'] = str(expires) + interesting_headers["date"] = str(expires) sorted_header_keys = list(interesting_headers.keys()) sorted_header_keys.sort() @@ -78,6 +81,7 @@ def canonical_string(method, bucket="", key="", query_args={}, headers={}, expir return buf + # computes the base64'ed hmac-sha hash of the canonical string and the secret # access key, optionally urlencoding the result def encode(aws_secret_access_key, str, urlencode=False): @@ -87,6 +91,7 @@ def encode(aws_secret_access_key, str, urlencode=False): else: return b64_hmac + def merge_meta(headers, metadata): final_headers = headers.copy() for k in list(metadata.keys()): @@ -94,6 +99,7 @@ def merge_meta(headers, metadata): return final_headers + # builds the query arg string def query_args_hash_to_string(query_args): query_string = "" @@ -104,7 +110,7 @@ def query_args_hash_to_string(query_args): piece += "=%s" % urllib.parse.quote_plus(str(v)) pairs.append(piece) - return '&'.join(pairs) + return "&".join(pairs) class CallingFormat: @@ -113,9 +119,9 @@ class CallingFormat: VANITY = 3 def build_url_base(protocol, server, port, bucket, calling_format): - url_base = '%s://' % protocol + url_base = "%s://" % protocol - if bucket == '': + if bucket == "": url_base += server elif calling_format == CallingFormat.SUBDOMAIN: url_base += "%s.%s" % (bucket, server) @@ -126,7 +132,7 @@ def build_url_base(protocol, server, port, bucket, calling_format): url_base += ":%s" % port - if (bucket != '') and (calling_format == CallingFormat.PATH): + if (bucket != "") and (calling_format == CallingFormat.PATH): url_base += "/%s" % bucket return url_base @@ -134,17 +140,21 @@ def build_url_base(protocol, server, port, bucket, calling_format): build_url_base = staticmethod(build_url_base) - class Location: DEFAULT = None - EU = 'EU' - + EU = "EU" class AWSAuthConnection: - def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True, - server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN): - + def __init__( + self, + aws_access_key_id, + aws_secret_access_key, + is_secure=True, + server=DEFAULT_HOST, + port=None, + calling_format=CallingFormat.SUBDOMAIN, + ): if not port: port = PORTS_BY_SECURITY[is_secure] @@ -156,86 +166,69 @@ def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True, self.calling_format = calling_format def create_bucket(self, bucket, headers={}): - return Response(self._make_request('PUT', bucket, '', {}, headers)) + return Response(self._make_request("PUT", bucket, "", {}, headers)) def create_located_bucket(self, bucket, location=Location.DEFAULT, headers={}): if location == Location.DEFAULT: body = "" else: - body = "" + \ - location + \ - "" - return Response(self._make_request('PUT', bucket, '', {}, headers, body)) + body = ( + "" + + location + + "" + ) + return Response(self._make_request("PUT", bucket, "", {}, headers, body)) def check_bucket_exists(self, bucket): - return self._make_request('HEAD', bucket, '', {}, {}) + return self._make_request("HEAD", bucket, "", {}, {}) def list_bucket(self, bucket, options={}, headers={}): - return ListBucketResponse(self._make_request('GET', bucket, '', options, headers)) + return ListBucketResponse(self._make_request("GET", bucket, "", options, headers)) def delete_bucket(self, bucket, headers={}): - return Response(self._make_request('DELETE', bucket, '', {}, headers)) + return Response(self._make_request("DELETE", bucket, "", {}, headers)) def put(self, bucket, key, object, headers={}): if not isinstance(object, S3Object): object = S3Object(object) - return Response( - self._make_request( - 'PUT', - bucket, - key, - {}, - headers, - object.data, - object.metadata)) + return Response(self._make_request("PUT", bucket, key, {}, headers, object.data, object.metadata)) def get(self, bucket, key, headers={}): - return GetResponse( - self._make_request('GET', bucket, key, {}, headers)) + return GetResponse(self._make_request("GET", bucket, key, {}, headers)) def delete(self, bucket, key, headers={}): - return Response( - self._make_request('DELETE', bucket, key, {}, headers)) + return Response(self._make_request("DELETE", bucket, key, {}, headers)) def get_bucket_logging(self, bucket, headers={}): - return GetResponse(self._make_request('GET', bucket, '', { 'logging': None }, headers)) + return GetResponse(self._make_request("GET", bucket, "", {"logging": None}, headers)) def put_bucket_logging(self, bucket, logging_xml_doc, headers={}): - return Response(self._make_request('PUT', bucket, '', { 'logging': None }, headers, logging_xml_doc)) + return Response(self._make_request("PUT", bucket, "", {"logging": None}, headers, logging_xml_doc)) def get_bucket_acl(self, bucket, headers={}): - return self.get_acl(bucket, '', headers) + return self.get_acl(bucket, "", headers) def get_acl(self, bucket, key, headers={}): - return GetResponse( - self._make_request('GET', bucket, key, { 'acl': None }, headers)) + return GetResponse(self._make_request("GET", bucket, key, {"acl": None}, headers)) def put_bucket_acl(self, bucket, acl_xml_document, headers={}): - return self.put_acl(bucket, '', acl_xml_document, headers) + return self.put_acl(bucket, "", acl_xml_document, headers) def put_acl(self, bucket, key, acl_xml_document, headers={}): - return Response( - self._make_request( - 'PUT', - bucket, - key, - { 'acl': None }, - headers, - acl_xml_document)) + return Response(self._make_request("PUT", bucket, key, {"acl": None}, headers, acl_xml_document)) def list_all_my_buckets(self, headers={}): - return ListAllMyBucketsResponse(self._make_request('GET', '', '', {}, headers)) + return ListAllMyBucketsResponse(self._make_request("GET", "", "", {}, headers)) def get_bucket_location(self, bucket): - return LocationResponse(self._make_request('GET', bucket, '', {'location' : None})) + return LocationResponse(self._make_request("GET", bucket, "", {"location": None})) # end public methods - def _make_request(self, method, bucket='', key='', query_args={}, headers={}, data='', metadata={}): - - server = '' - if bucket == '': + def _make_request(self, method, bucket="", key="", query_args={}, headers={}, data="", metadata={}): + server = "" + if bucket == "": server = self.server elif self.calling_format == CallingFormat.SUBDOMAIN: server = "%s.%s" % (bucket, self.server) @@ -244,18 +237,17 @@ def _make_request(self, method, bucket='', key='', query_args={}, headers={}, da else: server = self.server - path = '' + path = "" - if (bucket != '') and (self.calling_format == CallingFormat.PATH): + if (bucket != "") and (self.calling_format == CallingFormat.PATH): path += "/%s" % bucket # add the slash after the bucket regardless # the key will be appended if it is non-empty path += "/%s" % urllib.parse.quote_plus(key) - # build the path_argument string - # add the ? in all cases since + # add the ? in all cases since # signature and credentials follow path args if len(query_args): path += "?" + query_args_hash_to_string(query_args) @@ -263,12 +255,12 @@ def _make_request(self, method, bucket='', key='', query_args={}, headers={}, da is_secure = self.is_secure host = "%s:%d" % (server, self.port) while True: - if (is_secure): + if is_secure: connection = http.client.HTTPSConnection(host) else: connection = http.client.HTTPConnection(host) - final_headers = merge_meta(headers, metadata); + final_headers = merge_meta(headers, metadata) # add auth header self._add_aws_auth_header(final_headers, method, bucket, key, query_args) @@ -277,44 +269,55 @@ def _make_request(self, method, bucket='', key='', query_args={}, headers={}, da if resp.status < 300 or resp.status >= 400: return resp # handle redirect - location = resp.getheader('location') + location = resp.getheader("location") if not location: return resp # (close connection) resp.read() - scheme, host, path, params, query, fragment \ - = urllib.parse.urlparse(location) - if scheme == "http": is_secure = True - elif scheme == "https": is_secure = False - else: raise invalidURL("Not http/https: " + location) - if query: path += "?" + query + scheme, host, path, params, query, fragment = urllib.parse.urlparse(location) + if scheme == "http": + is_secure = True + elif scheme == "https": + is_secure = False + else: + raise invalidURL("Not http/https: " + location) + if query: + path += "?" + query # retry with redirect def _add_aws_auth_header(self, headers, method, bucket, key, query_args): - if 'Date' not in headers: - headers['Date'] = time.strftime("%a, %d %b %Y %X GMT", time.gmtime()) + if "Date" not in headers: + headers["Date"] = time.strftime("%a, %d %b %Y %X GMT", time.gmtime()) c_string = canonical_string(method, bucket, key, query_args, headers) - headers['Authorization'] = \ - "AWS %s:%s" % (self.aws_access_key_id, encode(self.aws_secret_access_key, c_string)) + headers["Authorization"] = "AWS %s:%s" % ( + self.aws_access_key_id, + encode(self.aws_secret_access_key, c_string), + ) class QueryStringAuthGenerator: # by default, expire in 1 minute DEFAULT_EXPIRES_IN = 60 - def __init__(self, aws_access_key_id, aws_secret_access_key, is_secure=True, - server=DEFAULT_HOST, port=None, calling_format=CallingFormat.SUBDOMAIN): - + def __init__( + self, + aws_access_key_id, + aws_secret_access_key, + is_secure=True, + server=DEFAULT_HOST, + port=None, + calling_format=CallingFormat.SUBDOMAIN, + ): if not port: port = PORTS_BY_SECURITY[is_secure] self.aws_access_key_id = aws_access_key_id self.aws_secret_access_key = aws_secret_access_key - if (is_secure): - self.protocol = 'https' + if is_secure: + self.protocol = "https" else: - self.protocol = 'http' + self.protocol = "http" self.is_secure = is_secure self.server = server @@ -335,58 +338,53 @@ def set_expires(self, expires): self.__expires_in = None def create_bucket(self, bucket, headers={}): - return self.generate_url('PUT', bucket, '', {}, headers) + return self.generate_url("PUT", bucket, "", {}, headers) def list_bucket(self, bucket, options={}, headers={}): - return self.generate_url('GET', bucket, '', options, headers) + return self.generate_url("GET", bucket, "", options, headers) def delete_bucket(self, bucket, headers={}): - return self.generate_url('DELETE', bucket, '', {}, headers) + return self.generate_url("DELETE", bucket, "", {}, headers) def put(self, bucket, key, object, headers={}): if not isinstance(object, S3Object): object = S3Object(object) - return self.generate_url( - 'PUT', - bucket, - key, - {}, - merge_meta(headers, object.metadata)) + return self.generate_url("PUT", bucket, key, {}, merge_meta(headers, object.metadata)) def get(self, bucket, key, headers={}): - return self.generate_url('GET', bucket, key, {}, headers) + return self.generate_url("GET", bucket, key, {}, headers) def delete(self, bucket, key, headers={}): - return self.generate_url('DELETE', bucket, key, {}, headers) + return self.generate_url("DELETE", bucket, key, {}, headers) def get_bucket_logging(self, bucket, headers={}): - return self.generate_url('GET', bucket, '', { 'logging': None }, headers) + return self.generate_url("GET", bucket, "", {"logging": None}, headers) def put_bucket_logging(self, bucket, logging_xml_doc, headers={}): - return self.generate_url('PUT', bucket, '', { 'logging': None }, headers) + return self.generate_url("PUT", bucket, "", {"logging": None}, headers) def get_bucket_acl(self, bucket, headers={}): - return self.get_acl(bucket, '', headers) + return self.get_acl(bucket, "", headers) - def get_acl(self, bucket, key='', headers={}): - return self.generate_url('GET', bucket, key, { 'acl': None }, headers) + def get_acl(self, bucket, key="", headers={}): + return self.generate_url("GET", bucket, key, {"acl": None}, headers) def put_bucket_acl(self, bucket, acl_xml_document, headers={}): - return self.put_acl(bucket, '', acl_xml_document, headers) + return self.put_acl(bucket, "", acl_xml_document, headers) # don't really care what the doc is here. def put_acl(self, bucket, key, acl_xml_document, headers={}): - return self.generate_url('PUT', bucket, key, { 'acl': None }, headers) + return self.generate_url("PUT", bucket, key, {"acl": None}, headers) def list_all_my_buckets(self, headers={}): - return self.generate_url('GET', '', '', {}, headers) + return self.generate_url("GET", "", "", {}, headers) - def make_bare_url(self, bucket, key=''): + def make_bare_url(self, bucket, key=""): full_url = self.generate_url(self, bucket, key) - return full_url[:full_url.index('?')] + return full_url[: full_url.index("?")] - def generate_url(self, method, bucket='', key='', query_args={}, headers={}): + def generate_url(self, method, bucket="", key="", query_args={}, headers={}): expires = 0 if self.__expires_in != None: expires = int(time.time() + self.__expires_in) @@ -402,9 +400,9 @@ def generate_url(self, method, bucket='', key='', query_args={}, headers={}): url += "/%s" % urllib.parse.quote_plus(key) - query_args['Signature'] = encoded_canonical - query_args['Expires'] = expires - query_args['AWSAccessKeyId'] = self.aws_access_key_id + query_args["Signature"] = encoded_canonical + query_args["Expires"] = expires + query_args["AWSAccessKeyId"] = self.aws_access_key_id url += "?%s" % query_args_hash_to_string(query_args) @@ -416,13 +414,15 @@ def __init__(self, data, metadata={}): self.data = data self.metadata = metadata + class Owner: - def __init__(self, id='', display_name=''): + def __init__(self, id="", display_name=""): self.id = id self.display_name = display_name + class ListEntry: - def __init__(self, key='', last_modified=None, etag='', size=0, storage_class='', owner=None): + def __init__(self, key="", last_modified=None, etag="", size=0, storage_class="", owner=None): self.key = key self.last_modified = last_modified self.etag = etag @@ -430,15 +430,18 @@ def __init__(self, key='', last_modified=None, etag='', size=0, storage_class='' self.storage_class = storage_class self.owner = owner + class CommonPrefixEntry: - def __init(self, prefix=''): + def __init(self, prefix=""): self.prefix = prefix + class Bucket: - def __init__(self, name='', creation_date=''): + def __init__(self, name="", creation_date=""): self.name = name self.creation_date = creation_date + class Response: def __init__(self, http_response): self.http_response = http_response @@ -451,7 +454,6 @@ def __init__(self, http_response): self.message = "%03d %s" % (http_response.status, http_response.reason) - class ListBucketResponse(Response): def __init__(self, http_response): Response.__init__(self, http_response) @@ -470,20 +472,22 @@ def __init__(self, http_response): else: self.entries = [] + class ListAllMyBucketsResponse(Response): def __init__(self, http_response): Response.__init__(self, http_response) - if http_response.status < 300: + if http_response.status < 300: handler = ListAllMyBucketsHandler() xml.sax.parseString(self.body, handler) self.entries = handler.entries else: self.entries = [] + class GetResponse(Response): def __init__(self, http_response): Response.__init__(self, http_response) - response_headers = http_response.msg # older pythons don't have getheaders + response_headers = http_response.msg # older pythons don't have getheaders metadata = self.get_aws_metadata(response_headers) self.object = S3Object(self.body, metadata) @@ -491,82 +495,83 @@ def get_aws_metadata(self, headers): metadata = {} for hkey in list(headers.keys()): if hkey.lower().startswith(METADATA_PREFIX): - metadata[hkey[len(METADATA_PREFIX):]] = headers[hkey] + metadata[hkey[len(METADATA_PREFIX) :]] = headers[hkey] del headers[hkey] return metadata + class LocationResponse(Response): def __init__(self, http_response): Response.__init__(self, http_response) - if http_response.status < 300: + if http_response.status < 300: handler = LocationHandler() xml.sax.parseString(self.body, handler) self.location = handler.location + class ListBucketHandler(xml.sax.ContentHandler): def __init__(self): self.entries = [] self.curr_entry = None - self.curr_text = '' + self.curr_text = "" self.common_prefixes = [] self.curr_common_prefix = None - self.name = '' - self.marker = '' - self.prefix = '' + self.name = "" + self.marker = "" + self.prefix = "" self.is_truncated = False - self.delimiter = '' + self.delimiter = "" self.max_keys = 0 - self.next_marker = '' + self.next_marker = "" self.is_echoed_prefix_set = False def startElement(self, name, attrs): - if name == 'Contents': + if name == "Contents": self.curr_entry = ListEntry() - elif name == 'Owner': + elif name == "Owner": self.curr_entry.owner = Owner() - elif name == 'CommonPrefixes': + elif name == "CommonPrefixes": self.curr_common_prefix = CommonPrefixEntry() - def endElement(self, name): - if name == 'Contents': + if name == "Contents": self.entries.append(self.curr_entry) - elif name == 'CommonPrefixes': + elif name == "CommonPrefixes": self.common_prefixes.append(self.curr_common_prefix) - elif name == 'Key': + elif name == "Key": self.curr_entry.key = self.curr_text - elif name == 'LastModified': + elif name == "LastModified": self.curr_entry.last_modified = self.curr_text - elif name == 'ETag': + elif name == "ETag": self.curr_entry.etag = self.curr_text - elif name == 'Size': + elif name == "Size": self.curr_entry.size = int(self.curr_text) - elif name == 'ID': + elif name == "ID": self.curr_entry.owner.id = self.curr_text - elif name == 'DisplayName': + elif name == "DisplayName": self.curr_entry.owner.display_name = self.curr_text - elif name == 'StorageClass': + elif name == "StorageClass": self.curr_entry.storage_class = self.curr_text - elif name == 'Name': + elif name == "Name": self.name = self.curr_text - elif name == 'Prefix' and self.is_echoed_prefix_set: + elif name == "Prefix" and self.is_echoed_prefix_set: self.curr_common_prefix.prefix = self.curr_text - elif name == 'Prefix': + elif name == "Prefix": self.prefix = self.curr_text self.is_echoed_prefix_set = True - elif name == 'Marker': + elif name == "Marker": self.marker = self.curr_text - elif name == 'IsTruncated': - self.is_truncated = self.curr_text == 'true' - elif name == 'Delimiter': + elif name == "IsTruncated": + self.is_truncated = self.curr_text == "true" + elif name == "Delimiter": self.delimiter = self.curr_text - elif name == 'MaxKeys': + elif name == "MaxKeys": self.max_keys = int(self.curr_text) - elif name == 'NextMarker': + elif name == "NextMarker": self.next_marker = self.curr_text - self.curr_text = '' + self.curr_text = "" def characters(self, content): self.curr_text += content @@ -576,18 +581,18 @@ class ListAllMyBucketsHandler(xml.sax.ContentHandler): def __init__(self): self.entries = [] self.curr_entry = None - self.curr_text = '' + self.curr_text = "" def startElement(self, name, attrs): - if name == 'Bucket': + if name == "Bucket": self.curr_entry = Bucket() def endElement(self, name): - if name == 'Name': + if name == "Name": self.curr_entry.name = self.curr_text - elif name == 'CreationDate': + elif name == "CreationDate": self.curr_entry.creation_date = self.curr_text - elif name == 'Bucket': + elif name == "Bucket": self.entries.append(self.curr_entry) def characters(self, content): @@ -597,21 +602,24 @@ def characters(self, content): class LocationHandler(xml.sax.ContentHandler): def __init__(self): self.location = None - self.state = 'init' + self.state = "init" def startElement(self, name, attrs): - if self.state == 'init': - if name == 'LocationConstraint': - self.state = 'tag_location' - self.location = '' - else: self.state = 'bad' - else: self.state = 'bad' + if self.state == "init": + if name == "LocationConstraint": + self.state = "tag_location" + self.location = "" + else: + self.state = "bad" + else: + self.state = "bad" def endElement(self, name): - if self.state == 'tag_location' and name == 'LocationConstraint': - self.state = 'done' - else: self.state = 'bad' + if self.state == "tag_location" and name == "LocationConstraint": + self.state = "done" + else: + self.state = "bad" def characters(self, content): - if self.state == 'tag_location': + if self.state == "tag_location": self.location += content diff --git a/utils/archive/Image Color Algorithm.py b/utils/archive/Image Color Algorithm.py index f02c6fdbd7..01c1192275 100644 --- a/utils/archive/Image Color Algorithm.py +++ b/utils/archive/Image Color Algorithm.py @@ -1,9 +1,10 @@ -from PIL import Image +from pprint import pprint + import scipy import scipy.cluster -from pprint import pprint +from PIL import Image -image = Image.open('logo.png') +image = Image.open("logo.png") NUM_CLUSTERS = 5 # Convert image into array of values for each point. @@ -20,11 +21,20 @@ # Pare centroids, removing blacks and whites and shades of really dark and really light. original_codes = codes for low, hi in [(60, 200), (35, 230), (10, 250)]: - codes = scipy.array([code for code in codes - if not ((code[0] < low and code[1] < low and code[2] < low) or - (code[0] > hi and code[1] > hi and code[2] > hi))]) - if not len(codes): codes = original_codes - else: break + codes = scipy.array( + [ + code + for code in codes + if not ( + (code[0] < low and code[1] < low and code[2] < low) + or (code[0] > hi and code[1] > hi and code[2] > hi) + ) + ] + ) + if not len(codes): + codes = original_codes + else: + break # Assign codes (vector quantization). Each vector is compared to the centroids # and assigned the nearest one. @@ -34,12 +44,12 @@ counts, bins = scipy.histogram(vecs, len(codes)) # Show colors for each code in its hex value. -colors = [''.join(chr(c) for c in code).encode('hex') for code in codes] +colors = ["".join(chr(c) for c in code).encode("hex") for code in codes] total = scipy.sum(counts) -color_dist = dict(list(zip(colors, [count/float(total) for count in counts]))) +color_dist = dict(list(zip(colors, [count / float(total) for count in counts]))) pprint(color_dist) # Find the most frequent color, based on the counts. index_max = scipy.argmax(counts) peak = codes[index_max] -color = ''.join(chr(c) for c in peak).encode('hex') +color = "".join(chr(c) for c in peak).encode("hex") diff --git a/utils/archive/bootstrap_intel.py b/utils/archive/bootstrap_intel.py index fd2d7fe3f5..e5b350c086 100644 --- a/utils/archive/bootstrap_intel.py +++ b/utils/archive/bootstrap_intel.py @@ -1,13 +1,16 @@ import sys -from mongoengine.queryset import OperationError + from mongoengine.errors import ValidationError -from apps.analyzer.models import MClassifierFeed -from apps.analyzer.models import MClassifierAuthor -from apps.analyzer.models import MClassifierTag -from apps.analyzer.models import MClassifierTitle +from mongoengine.queryset import OperationError -for classifier_cls in [MClassifierFeed, MClassifierAuthor, - MClassifierTag, MClassifierTitle]: +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, +) + +for classifier_cls in [MClassifierFeed, MClassifierAuthor, MClassifierTag, MClassifierTitle]: print(" ================================================================= ") print((" Now on %s " % classifier_cls.__name__)) print(" ================================================================= ") @@ -28,4 +31,3 @@ except ValidationError as e: print((" ***> ValidationError error on: %s" % e)) print((" ***> Original classifier: %s" % classifier.__dict__)) - diff --git a/utils/archive/bootstrap_mongo.py b/utils/archive/bootstrap_mongo.py index d7fd747aa2..4162ec0a15 100644 --- a/utils/archive/bootstrap_mongo.py +++ b/utils/archive/bootstrap_mongo.py @@ -1,16 +1,24 @@ +import sys from pprint import pprint + +import mongoengine +import pymongo from django.conf import settings -from apps.reader.models import MUserStory -from apps.rss_feeds.models import Feed, MStory, MFeedPage -from apps.rss_feeds.models import MFeedIcon, FeedIcon -from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag -import mongoengine, pymongo -import sys from mongoengine.queryset import OperationError + +from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, +) +from apps.reader.models import MUserStory +from apps.rss_feeds.models import Feed, FeedIcon, MFeedIcon, MFeedPage, MStory from utils import json_functions as json MONGO_DB = settings.MONGO_DB -db = mongoengine.connect(MONGO_DB['NAME'], host=MONGO_DB['HOST'], port=MONGO_DB['PORT']) +db = mongoengine.connect(MONGO_DB["NAME"], host=MONGO_DB["HOST"], port=MONGO_DB["PORT"]) + def bootstrap_stories(): print("Mongo DB stories: %s" % MStory.objects().count()) @@ -20,24 +28,23 @@ def bootstrap_stories(): print("Stories: %s" % Story.objects.all().count()) pprint(db.stories.index_information()) - feeds = Feed.objects.all().order_by('-average_stories_per_month') + feeds = Feed.objects.all().order_by("-average_stories_per_month") feed_count = feeds.count() i = 0 for feed in feeds: i += 1 - print("%s/%s: %s (%s stories)" % (i, feed_count, - feed, Story.objects.filter(story_feed=feed).count())) + print("%s/%s: %s (%s stories)" % (i, feed_count, feed, Story.objects.filter(story_feed=feed).count())) sys.stdout.flush() - + stories = list(Story.objects.filter(story_feed=feed).values()) for story in stories: # story['story_tags'] = [tag.name for tag in Tag.objects.filter(story=story['id'])] try: - story['story_tags'] = json.decode(story['story_tags']) + story["story_tags"] = json.decode(story["story_tags"]) except: continue - del story['id'] - del story['story_author_id'] + del story["id"] + del story["story_author_id"] try: MStory(**story).save() except: @@ -45,6 +52,7 @@ def bootstrap_stories(): print("\nMongo DB stories: %s" % MStory.objects().count()) + def bootstrap_userstories(): print("Mongo DB userstories: %s" % MUserStory.objects().count()) # db.userstories.drop() @@ -56,58 +64,64 @@ def bootstrap_userstories(): userstories = list(UserStory.objects.all().values()) for userstory in userstories: try: - story = Story.objects.get(pk=userstory['story_id']) + story = Story.objects.get(pk=userstory["story_id"]) except Story.DoesNotExist: continue try: - userstory['story'] = MStory.objects(story_feed_id=story.story_feed.pk, story_guid=story.story_guid)[0] + userstory["story"] = MStory.objects( + story_feed_id=story.story_feed.pk, story_guid=story.story_guid + )[0] except: - print('!') + print("!") continue - print('.') - del userstory['id'] - del userstory['opinion'] - del userstory['story_id'] + print(".") + del userstory["id"] + del userstory["opinion"] + del userstory["story_id"] try: MUserStory(**userstory).save() except: - print('\n\n!\n\n') + print("\n\n!\n\n") continue print("\nMongo DB userstories: %s" % MUserStory.objects().count()) + def bootstrap_classifiers(): - for sql_classifier, mongo_classifier in ((ClassifierTitle, MClassifierTitle), - (ClassifierAuthor, MClassifierAuthor), - (ClassifierFeed, MClassifierFeed), - (ClassifierTag, MClassifierTag)): - collection = mongo_classifier.meta['collection'] + for sql_classifier, mongo_classifier in ( + (ClassifierTitle, MClassifierTitle), + (ClassifierAuthor, MClassifierAuthor), + (ClassifierFeed, MClassifierFeed), + (ClassifierTag, MClassifierTag), + ): + collection = mongo_classifier.meta["collection"] print("Mongo DB classifiers: %s - %s" % (collection, mongo_classifier.objects().count())) # db[collection].drop() print("Dropped! Mongo DB classifiers: %s - %s" % (collection, mongo_classifier.objects().count())) print("%s: %s" % (sql_classifier._meta.object_name, sql_classifier.objects.all().count())) pprint(db[collection].index_information()) - + for userclassifier in list(sql_classifier.objects.all().values()): - del userclassifier['id'] - if sql_classifier._meta.object_name == 'ClassifierAuthor': - author = StoryAuthor.objects.get(pk=userclassifier['author_id']) - userclassifier['author'] = author.author_name - del userclassifier['author_id'] - if sql_classifier._meta.object_name == 'ClassifierTag': - tag = Tag.objects.get(pk=userclassifier['tag_id']) - userclassifier['tag'] = tag.name - del userclassifier['tag_id'] - print('.') + del userclassifier["id"] + if sql_classifier._meta.object_name == "ClassifierAuthor": + author = StoryAuthor.objects.get(pk=userclassifier["author_id"]) + userclassifier["author"] = author.author_name + del userclassifier["author_id"] + if sql_classifier._meta.object_name == "ClassifierTag": + tag = Tag.objects.get(pk=userclassifier["tag_id"]) + userclassifier["tag"] = tag.name + del userclassifier["tag_id"] + print(".") try: mongo_classifier(**userclassifier).save() except: - print('\n\n!\n\n') + print("\n\n!\n\n") continue - + print("\nMongo DB classifiers: %s - %s" % (collection, mongo_classifier.objects().count())) - + + def bootstrap_feedpages(): print("Mongo DB feed_pages: %s" % MFeedPage.objects().count()) # db.feed_pages.drop() @@ -116,28 +130,35 @@ def bootstrap_feedpages(): print("FeedPages: %s" % FeedPage.objects.count()) pprint(db.feed_pages.index_information()) - feeds = Feed.objects.all().order_by('-average_stories_per_month') + feeds = Feed.objects.all().order_by("-average_stories_per_month") feed_count = feeds.count() i = 0 for feed in feeds: i += 1 - print("%s/%s: %s" % (i, feed_count, feed,)) + print( + "%s/%s: %s" + % ( + i, + feed_count, + feed, + ) + ) sys.stdout.flush() - + if not MFeedPage.objects(feed_id=feed.pk): feed_page = list(FeedPage.objects.filter(feed=feed).values()) if feed_page: - del feed_page[0]['id'] - feed_page[0]['feed_id'] = feed.pk + del feed_page[0]["id"] + feed_page[0]["feed_id"] = feed.pk try: MFeedPage(**feed_page[0]).save() except: - print('\n\n!\n\n') + print("\n\n!\n\n") continue - print("\nMongo DB feed_pages: %s" % MFeedPage.objects().count()) + def bootstrap_feedicons(): print("Mongo DB feed_icons: %s" % MFeedIcon.objects().count()) db.feed_icons.drop() @@ -146,47 +167,62 @@ def bootstrap_feedicons(): print("FeedIcons: %s" % FeedIcon.objects.count()) pprint(db.feed_icons.index_information()) - feeds = Feed.objects.all().order_by('-average_stories_per_month') + feeds = Feed.objects.all().order_by("-average_stories_per_month") feed_count = feeds.count() i = 0 for feed in feeds: i += 1 - print("%s/%s: %s" % (i, feed_count, feed,)) + print( + "%s/%s: %s" + % ( + i, + feed_count, + feed, + ) + ) sys.stdout.flush() - + if not MFeedIcon.objects(feed_id=feed.pk): feed_icon = list(FeedIcon.objects.filter(feed=feed).values()) if feed_icon: try: MFeedIcon(**feed_icon[0]).save() except: - print('\n\n!\n\n') + print("\n\n!\n\n") continue - print("\nMongo DB feed_icons: %s" % MFeedIcon.objects().count()) + def compress_stories(): count = MStory.objects().count() print("Mongo DB stories: %s" % count) p = 0.0 i = 0 - feeds = Feed.objects.all().order_by('-average_stories_per_month') + feeds = Feed.objects.all().order_by("-average_stories_per_month") feed_count = feeds.count() f = 0 for feed in feeds: f += 1 - print("%s/%s: %s" % (f, feed_count, feed,)) + print( + "%s/%s: %s" + % ( + f, + feed_count, + feed, + ) + ) sys.stdout.flush() - + for story in MStory.objects(story_feed_id=feed.pk): i += 1.0 if round(i / count * 100) != p: p = round(i / count * 100) - print('%s%%' % p) + print("%s%%" % p) story.save() - + + def reindex_stories(): db = pymongo.Connection().newsblur count = MStory.objects().count() @@ -194,18 +230,25 @@ def reindex_stories(): p = 0.0 i = 0 - feeds = Feed.objects.all().order_by('-average_stories_per_month') + feeds = Feed.objects.all().order_by("-average_stories_per_month") feed_count = feeds.count() f = 0 for feed in feeds: f += 1 - print("%s/%s: %s" % (f, feed_count, feed,)) + print( + "%s/%s: %s" + % ( + f, + feed_count, + feed, + ) + ) sys.stdout.flush() for story in MStory.objects(story_feed_id=feed.pk): i += 1.0 if round(i / count * 100) != p: p = round(i / count * 100) - print('%s%%' % p) + print("%s%%" % p) if isinstance(story.id, str): story.story_guid = story.id story.id = pymongo.objectid.ObjectId() @@ -214,14 +257,15 @@ def reindex_stories(): except OperationError as e: print(" ***> OperationError: %s" % e) except e: - print(' ***> Unknown Error: %s' % e) + print(" ***> Unknown Error: %s" % e) db.stories.remove({"_id": story.story_guid}) - -if __name__ == '__main__': + + +if __name__ == "__main__": # bootstrap_stories() # bootstrap_userstories() # bootstrap_classifiers() # bootstrap_feedpages() # compress_stories() # reindex_stories() - bootstrap_feedicons() \ No newline at end of file + bootstrap_feedicons() diff --git a/utils/archive/bootstrap_redis_sessions.py b/utils/archive/bootstrap_redis_sessions.py index fc13bb5753..f718cf0849 100644 --- a/utils/archive/bootstrap_redis_sessions.py +++ b/utils/archive/bootstrap_redis_sessions.py @@ -1,4 +1,5 @@ import math + import redis from django.conf import settings from django.contrib.sessions.models import Session @@ -8,7 +9,7 @@ batch_size = 1000 r = redis.Redis(connection_pool=settings.REDIS_SESSION_POOL) -for batch in range(int(math.ceil(sessions_count / batch_size))+1): +for batch in range(int(math.ceil(sessions_count / batch_size)) + 1): start = batch * batch_size end = (batch + 1) * batch_size print((" ---> Loading sessions #%s - #%s" % (start, end))) @@ -16,4 +17,4 @@ for session in Session.objects.all()[start:end]: _ = pipe.set(session.session_key, session.session_data) _ = pipe.expireat(session.session_key, session.expire_date.strftime("%s")) - _ = pipe.execute() \ No newline at end of file + _ = pipe.execute() diff --git a/utils/archive/bootstrap_story_hash.py b/utils/archive/bootstrap_story_hash.py index efcb31e5fb..266ccb30f2 100644 --- a/utils/archive/bootstrap_story_hash.py +++ b/utils/archive/bootstrap_story_hash.py @@ -1,29 +1,31 @@ import time + import pymongo from django.conf import settings -from apps.rss_feeds.models import MStory, Feed + +from apps.rss_feeds.models import Feed, MStory db = settings.MONGODB batch = 0 start = 0 -for f in range(start, Feed.objects.latest('pk').pk): - if f < batch*100000: continue +for f in range(start, Feed.objects.latest("pk").pk): + if f < batch * 100000: + continue start = time.time() try: cp1 = time.time() - start # if feed.active_premium_subscribers < 1: continue - stories = MStory.objects.filter(story_feed_id=f, story_hash__exists=False)\ - .only('id', 'story_feed_id', 'story_guid')\ - .read_preference(pymongo.ReadPreference.SECONDARY) + stories = ( + MStory.objects.filter(story_feed_id=f, story_hash__exists=False) + .only("id", "story_feed_id", "story_guid") + .read_preference(pymongo.ReadPreference.SECONDARY) + ) cp2 = time.time() - start count = 0 for story in stories: count += 1 - db.newsblur.stories.update({"_id": story.id}, {"$set": { - "story_hash": story.feed_guid_hash - }}) + db.newsblur.stories.update({"_id": story.id}, {"$set": {"story_hash": story.feed_guid_hash}}) cp3 = time.time() - start print(("%s: %3s stories (%s/%s/%s)" % (f, count, round(cp1, 2), round(cp2, 2), round(cp3, 2)))) except Exception as e: print((" ***> (%s) %s" % (f, e))) - diff --git a/utils/archive/check_status.py b/utils/archive/check_status.py index cbad9f317e..5dd6373d3a 100644 --- a/utils/archive/check_status.py +++ b/utils/archive/check_status.py @@ -1,5 +1,7 @@ import time + import requests + url = "http://www.newsblur.com" @@ -8,6 +10,10 @@ req = requests.get(url) content = req.content end = time.time() - print((" ---> [%s] Retrieved %s bytes - %s %s" % (str(end - start)[:4], len(content), req.status_code, req.reason))) + print( + ( + " ---> [%s] Retrieved %s bytes - %s %s" + % (str(end - start)[:4], len(content), req.status_code, req.reason) + ) + ) time.sleep(5) - diff --git a/utils/archive/green.py b/utils/archive/green.py index 46e09359dc..9c83b734af 100644 --- a/utils/archive/green.py +++ b/utils/archive/green.py @@ -1,18 +1,25 @@ from gevent import monkey + monkey.patch_socket() -from newsblur.utils import feedparser +import urllib.error +import urllib.parse +import urllib.request + import gevent from gevent import queue -import urllib.request, urllib.error, urllib.parse + +from newsblur.utils import feedparser + def fetch_title(url): print(("Running %s" % url)) data = urllib.request.urlopen(url).read() print(("Parsing %s" % url)) d = feedparser.parse(data) - print(("Parsed %s" % d.feed.get('title', ''))) - return d.feed.get('title', '') + print(("Parsed %s" % d.feed.get("title", ""))) + return d.feed.get("title", "") + def worker(): while True: @@ -22,15 +29,18 @@ def worker(): finally: q.task_done() -if __name__ == '__main__': + +if __name__ == "__main__": q = queue.JoinableQueue() for i in range(5): - gevent.spawn(worker) + gevent.spawn(worker) - for url in "http://www.43folders.com/rss.xml/nhttp://feeds.feedburner.com/43folders/nhttp://www.43folders.com/rss.xml/nhttp://feeds.feedburner.com/43folders/nhttp://feeds.feedburner.com/AMinuteWithBrendan/nhttp://feeds.feedburner.com/AMinuteWithBrendan/nhttp://www.asianart.org/feeds/Lectures,Classes,Symposia.xml/nhttp://www.asianart.org/feeds/Performances.xml/nhttp://feeds.feedburner.com/ajaxian/nhttp://ajaxian.com/index.xml/nhttp://al3x.net/atom.xml/nhttp://feeds.feedburner.com/AmericanDrink/nhttp://feeds.feedburner.com/eod_full/nhttp://feeds.feedburner.com/typepad/notes/nhttp://feeds.dashes.com/AnilDash/nhttp://rss.sciam.com/assignment-impossible/feed/nhttp://blogs.scientificamerican.com/assignment-impossible//nhttp://feeds.feedburner.com/Beautiful-Pixels/nhttp://feeds.feedburner.com/Beautiful-Pixels/nhttp://www.betabeat.com/feed/".split('/n'): - print(("Spawning: %s" % url)) - q.put(url) + for ( + url + ) in "http://www.43folders.com/rss.xml/nhttp://feeds.feedburner.com/43folders/nhttp://www.43folders.com/rss.xml/nhttp://feeds.feedburner.com/43folders/nhttp://feeds.feedburner.com/AMinuteWithBrendan/nhttp://feeds.feedburner.com/AMinuteWithBrendan/nhttp://www.asianart.org/feeds/Lectures,Classes,Symposia.xml/nhttp://www.asianart.org/feeds/Performances.xml/nhttp://feeds.feedburner.com/ajaxian/nhttp://ajaxian.com/index.xml/nhttp://al3x.net/atom.xml/nhttp://feeds.feedburner.com/AmericanDrink/nhttp://feeds.feedburner.com/eod_full/nhttp://feeds.feedburner.com/typepad/notes/nhttp://feeds.dashes.com/AnilDash/nhttp://rss.sciam.com/assignment-impossible/feed/nhttp://blogs.scientificamerican.com/assignment-impossible//nhttp://feeds.feedburner.com/Beautiful-Pixels/nhttp://feeds.feedburner.com/Beautiful-Pixels/nhttp://www.betabeat.com/feed/".split( + "/n" + ): + print(("Spawning: %s" % url)) + q.put(url) q.join() # block until all tasks are done - - diff --git a/utils/archive/knight.py b/utils/archive/knight.py index d8b4371239..f35db12b64 100644 --- a/utils/archive/knight.py +++ b/utils/archive/knight.py @@ -1,15 +1,15 @@ # Screen scrapes the Knight News Challenge entries (all 64 pages of them) # and counts the number of votes/hearts for each entry. Then displays them # in rank order. -# +# # This script runs in about 20 seconds. import requests from BeautifulSoup import BeautifulSoup # Winners found on http://newschallenge.tumblr.com/post/20962258701/knight-news-challenge-on-networks-moving-to-the-next: -# -# $('.posts .MsoNormal > span').find('a[href^="http://newschallenge.tumblr.com/post"]').map(function() { +# +# $('.posts .MsoNormal > span').find('a[href^="http://newschallenge.tumblr.com/post"]').map(function() { # return $(this).attr('href'); # }); @@ -70,7 +70,9 @@ "http://newschallenge.tumblr.com/post/19493920734/get-to-the-source", "http://newschallenge.tumblr.com/post/19480128205/farm-to-table-school-lunch", "http://newschallenge.tumblr.com/post/19477700441/partisans-org", - "http://newschallenge.tumblr.com/post/19345505702/protecting-journalists-and-engaging-communities"] + "http://newschallenge.tumblr.com/post/19345505702/protecting-journalists-and-engaging-communities", +] + def find_entries(): page = 1 @@ -79,73 +81,85 @@ def find_entries(): while True: print(" ---> Found %s entries so far. Now on page: %s" % (len(entries), page)) - + knight_url = "http://newschallenge.tumblr.com/page/%s" % (page) html = requests.get(knight_url).content soup = BeautifulSoup(html) postboxes = soup.findAll("div", "postbox") - + # Done if only sticky entry is left. if len(postboxes) <= 1: break page += 1 - + # 15 entries per page, plus a sticky throwaway entry for entry in postboxes: - if 'stickyPost' in entry.get('class'): continue - + if "stickyPost" in entry.get("class"): + continue + total_entry_count += 1 likes = entry.find("", "home-likes") if likes and likes.text: likes = int(likes.text) else: likes = 0 - + comments = entry.find("", "home-comments") if comments and comments.text: comments = int(comments.text) else: comments = 0 - + title = entry.find("h2") if title: title = title.text - - url = entry.find('a', "home-view") + + url = entry.find("a", "home-view") if url: - url = url.get('href') - + url = url.get("href") + # Only record active entries if comments or likes: - entries.append({ - 'likes': likes, - 'comments': comments, - 'title': title, - 'url': url, - }) + entries.append( + { + "likes": likes, + "comments": comments, + "title": title, + "url": url, + } + ) # time.sleep(random.randint(0, 2)) - - entries.sort(key=lambda e: e['comments'] + e['likes']) + + entries.sort(key=lambda e: e["comments"] + e["likes"]) entries.reverse() active_entry_count = len(entries) - + found_entries = [] winner_count = 0 for i, entry in enumerate(entries): - is_winner = entry['url'] in winners - if is_winner: winner_count += 1 - print(" * %s#%s: %s likes - [%s](%s)%s" % ( - "**" if is_winner else "", - i + 1, - entry['likes'], entry['title'], - entry['url'], - "**" if is_winner else "")) + is_winner = entry["url"] in winners + if is_winner: + winner_count += 1 + print( + " * %s#%s: %s likes - [%s](%s)%s" + % ( + "**" if is_winner else "", + i + 1, + entry["likes"], + entry["title"], + entry["url"], + "**" if is_winner else "", + ) + ) found_entries.append(entry) - - print(" ***> Found %s active entries among %s total applications with %s/%s winners." % ( - active_entry_count, total_entry_count, winner_count, len(winners))) + + print( + " ***> Found %s active entries among %s total applications with %s/%s winners." + % (active_entry_count, total_entry_count, winner_count, len(winners)) + ) return found_entries -if __name__ == '__main__': - find_entries() \ No newline at end of file + +if __name__ == "__main__": + find_entries() diff --git a/utils/archive/memcached_status.py b/utils/archive/memcached_status.py index e5be7b37a3..3d9dfd17cb 100644 --- a/utils/archive/memcached_status.py +++ b/utils/archive/memcached_status.py @@ -1,47 +1,48 @@ -import memcache import re import sys + +import memcache from settings import CACHE_BACKEND -#gfranxman + +# gfranxman verbose = False -if not CACHE_BACKEND.startswith( 'memcached://' ): +if not CACHE_BACKEND.startswith("memcached://"): print("you are not configured to use memcched as your django cache backend") else: - m = re.search( r'//(.+:\d+)', CACHE_BACKEND ) - cache_host = m.group(1) + m = re.search(r"//(.+:\d+)", CACHE_BACKEND) + cache_host = m.group(1) - h = memcache._Host( cache_host ) + h = memcache._Host(cache_host) h.connect() - h.send_cmd( 'stats' ) + h.send_cmd("stats") stats = {} - pat = re.compile( r'STAT (\w+) (\w+)' ) + pat = re.compile(r"STAT (\w+) (\w+)") - l = '' ; - while l.find( 'END' ) < 0 : + l = "" + while l.find("END") < 0: l = h.readline() if verbose: print(l) - m = pat.match( l ) - if m : - stats[ m.group(1) ] = m.group(2) - + m = pat.match(l) + if m: + stats[m.group(1)] = m.group(2) h.close_socket() if verbose: print(stats) - items = int( stats[ 'curr_items' ] ) - bytes = int( stats[ 'bytes' ] ) - limit_maxbytes = int( stats[ 'limit_maxbytes' ] ) or bytes - current_conns = int( stats[ 'curr_connections' ] ) + items = int(stats["curr_items"]) + bytes = int(stats["bytes"]) + limit_maxbytes = int(stats["limit_maxbytes"]) or bytes + current_conns = int(stats["curr_connections"]) - print("MemCache status for %s" % ( CACHE_BACKEND )) - print("%d items using %d of %d" % ( items, bytes, limit_maxbytes )) - print("%5.2f%% full" % ( 100.0 * bytes / limit_maxbytes )) - print("%d connections being handled" % ( current_conns )) - print() \ No newline at end of file + print("MemCache status for %s" % (CACHE_BACKEND)) + print("%d items using %d of %d" % (items, bytes, limit_maxbytes)) + print("%5.2f%% full" % (100.0 * bytes / limit_maxbytes)) + print("%d connections being handled" % (current_conns)) + print() diff --git a/utils/backups/backup_mongo.py b/utils/backups/backup_mongo.py index f795d516a1..236046b39f 100755 --- a/utils/backups/backup_mongo.py +++ b/utils/backups/backup_mongo.py @@ -1,14 +1,16 @@ #!/usr/bin/python3 -from datetime import datetime, timedelta -import os -import sys -import re import logging import mimetypes -import boto3 -import threading +import os +import re import shutil +import sys +import threading +from datetime import datetime, timedelta + +import boto3 from boto3.s3.transfer import S3Transfer + from newsblur_web import settings logger = logging.getLogger(__name__) diff --git a/utils/backups/backup_psql.py b/utils/backups/backup_psql.py index 6fec0466eb..0d91af2d49 100644 --- a/utils/backups/backup_psql.py +++ b/utils/backups/backup_psql.py @@ -1,7 +1,8 @@ #!/usr/bin/python3 import os -import sys import socket +import sys + CURRENT_DIR = os.path.dirname(__file__) NEWSBLUR_DIR = ''.join([CURRENT_DIR, '/../../']) sys.path.insert(0, NEWSBLUR_DIR) @@ -9,6 +10,7 @@ import threading + class ProgressPercentage(object): def __init__(self, filename): @@ -29,6 +31,7 @@ def __call__(self, bytes_amount): sys.stdout.flush() import time + import boto3 from django.conf import settings diff --git a/utils/backups/backup_redis.py b/utils/backups/backup_redis.py index bf8954a9b1..b416b0c07d 100644 --- a/utils/backups/backup_redis.py +++ b/utils/backups/backup_redis.py @@ -1,7 +1,8 @@ #!/usr/bin/python3 import os -import sys import socket +import sys + CURRENT_DIR = os.path.dirname(__file__) NEWSBLUR_DIR = ''.join([CURRENT_DIR, '/../../']) sys.path.insert(0, NEWSBLUR_DIR) @@ -9,6 +10,7 @@ import threading + class ProgressPercentage(object): def __init__(self, filename): @@ -29,6 +31,7 @@ def __call__(self, bytes_amount): sys.stdout.flush() import time + import boto3 from django.conf import settings diff --git a/utils/backups/copy_mongo_serialized.py b/utils/backups/copy_mongo_serialized.py index 2e081a5d59..ec773fbc7f 100644 --- a/utils/backups/copy_mongo_serialized.py +++ b/utils/backups/copy_mongo_serialized.py @@ -2,9 +2,11 @@ # to another using only pymongo. This circumvents the mongod --repair # option, which can fail. +import datetime import sys + import pymongo -import datetime + from apps.rss_feeds.models import Feed collections = [ diff --git a/utils/db_functions.py b/utils/db_functions.py index 9fdabf8ac4..7a2a7cff77 100644 --- a/utils/db_functions.py +++ b/utils/db_functions.py @@ -3,23 +3,24 @@ PRIMARY_STATE = 1 SECONDARY_STATE = 2 + def mongo_max_replication_lag(connection): try: - status = connection.admin.command('replSetGetStatus') + status = connection.admin.command("replSetGetStatus") except pymongo.errors.OperationFailure: return 0 - - members = status['members'] + + members = status["members"] primary_optime = None oldest_secondary_optime = None for member in members: - member_state = member['state'] - optime = member['optime'] + member_state = member["state"] + optime = member["optime"] if member_state == PRIMARY_STATE: - primary_optime = optime['ts'].time + primary_optime = optime["ts"].time elif member_state == SECONDARY_STATE: - if not oldest_secondary_optime or optime['ts'].time < oldest_secondary_optime: - oldest_secondary_optime = optime['ts'].time + if not oldest_secondary_optime or optime["ts"].time < oldest_secondary_optime: + oldest_secondary_optime = optime["ts"].time if not primary_optime or not oldest_secondary_optime: return 0 diff --git a/utils/exception_middleware.py b/utils/exception_middleware.py index de282de65b..715b618ad9 100644 --- a/utils/exception_middleware.py +++ b/utils/exception_middleware.py @@ -1,25 +1,25 @@ -import traceback -import sys import inspect +import sys +import traceback from pprint import pprint + class ConsoleExceptionMiddleware: def process_exception(self, request, exception): exc_info = sys.exc_info() print("######################## Exception #############################") - print(('\n'.join(traceback.format_exception(*(exc_info or sys.exc_info()))))) + print(("\n".join(traceback.format_exception(*(exc_info or sys.exc_info()))))) print("----------------------------------------------------------------") # pprint(inspect.trace()[-1][0].f_locals) print("################################################################") - - #pprint(request) - #print "################################################################" + + # pprint(request) + # print "################################################################" def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): - response = self.get_response(request) return response diff --git a/utils/facebook_fetcher.py b/utils/facebook_fetcher.py index 356169e85b..80ed75fc91 100644 --- a/utils/facebook_fetcher.py +++ b/utils/facebook_fetcher.py @@ -1,224 +1,237 @@ -import re import datetime +import re + import dateutil.parser from django.conf import settings from django.utils import feedgenerator from django.utils.html import linebreaks -from apps.social.models import MSocialServices + from apps.reader.models import UserSubscription +from apps.social.models import MSocialServices from utils import log as logging from vendor.facebook import GraphAPIError + class FacebookFetcher: - def __init__(self, feed, options=None): self.feed = feed self.options = options or {} - + def fetch(self): page_name = self.extract_page_name() - if not page_name: + if not page_name: return facebook_user = self.facebook_user() if not facebook_user: return - + # If 'video', use video API to get embed: # f.get_object('tastyvegetarian', fields='posts') # f.get_object('1992797300790726', fields='embed_html') - feed = self.fetch_page_feed(facebook_user, page_name, 'name,about,posts,videos,photos') - + feed = self.fetch_page_feed(facebook_user, page_name, "name,about,posts,videos,photos") + data = {} - data['title'] = feed.get('name', "%s on Facebook" % page_name) - data['link'] = feed.get('link', "https://facebook.com/%s" % page_name) - data['description'] = feed.get('about', "%s on Facebook" % page_name) - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur Facebook API Decrapifier - %s' % settings.NEWSBLUR_URL - data['docs'] = None - data['feed_url'] = self.feed.feed_address + data["title"] = feed.get("name", "%s on Facebook" % page_name) + data["link"] = feed.get("link", "https://facebook.com/%s" % page_name) + data["description"] = feed.get("about", "%s on Facebook" % page_name) + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur Facebook API Decrapifier - %s" % settings.NEWSBLUR_URL + data["docs"] = None + data["feed_url"] = self.feed.feed_address rss = feedgenerator.Atom1Feed(**data) merged_data = [] - - posts = feed.get('posts', {}).get('data', None) + + posts = feed.get("posts", {}).get("data", None) if posts: for post in posts: story_data = self.page_posts_story(facebook_user, post) if not story_data: continue merged_data.append(story_data) - - videos = feed.get('videos', {}).get('data', None) + + videos = feed.get("videos", {}).get("data", None) if videos: for video in videos: story_data = self.page_video_story(facebook_user, video) if not story_data: continue for seen_data in merged_data: - if story_data['link'] == seen_data['link']: + if story_data["link"] == seen_data["link"]: # Video wins over posts (and attachments) - seen_data['description'] = story_data['description'] - seen_data['title'] = story_data['title'] + seen_data["description"] = story_data["description"] + seen_data["title"] = story_data["title"] break - + for story_data in merged_data: rss.add_item(**story_data) - - return rss.writeString('utf-8') - + + return rss.writeString("utf-8") + def extract_page_name(self): page = None try: - page_groups = re.search('facebook.com/(\w+)/?', self.feed.feed_address) + page_groups = re.search("facebook.com/(\w+)/?", self.feed.feed_address) if not page_groups: return page = page_groups.group(1) except IndexError: return - + return page - + def facebook_user(self): facebook_api = None social_services = None - - if self.options.get('requesting_user_id', None): - social_services = MSocialServices.get_user(self.options.get('requesting_user_id')) + + if self.options.get("requesting_user_id", None): + social_services = MSocialServices.get_user(self.options.get("requesting_user_id")) facebook_api = social_services.facebook_api() if not facebook_api: - logging.debug(' ***> [%-30s] ~FRFacebook fetch failed: %s: No facebook API for %s' % - (self.feed.log_title[:30], self.feed.feed_address, self.options)) + logging.debug( + " ***> [%-30s] ~FRFacebook fetch failed: %s: No facebook API for %s" + % (self.feed.log_title[:30], self.feed.feed_address, self.options) + ) return else: usersubs = UserSubscription.objects.filter(feed=self.feed) if not usersubs: - logging.debug(' ***> [%-30s] ~FRFacebook fetch failed: %s: No subscriptions' % - (self.feed.log_title[:30], self.feed.feed_address)) + logging.debug( + " ***> [%-30s] ~FRFacebook fetch failed: %s: No subscriptions" + % (self.feed.log_title[:30], self.feed.feed_address) + ) return for sub in usersubs: social_services = MSocialServices.get_user(sub.user_id) - if not social_services.facebook_uid: + if not social_services.facebook_uid: continue facebook_api = social_services.facebook_api() - if not facebook_api: + if not facebook_api: continue else: break - + if not facebook_api: - logging.debug(' ***> [%-30s] ~FRFacebook fetch failed: %s: No facebook API for %s' % - (self.feed.log_title[:30], self.feed.feed_address, usersubs[0].user.username)) + logging.debug( + " ***> [%-30s] ~FRFacebook fetch failed: %s: No facebook API for %s" + % (self.feed.log_title[:30], self.feed.feed_address, usersubs[0].user.username) + ) return - + return facebook_api - + def fetch_page_feed(self, facebook_user, page, fields): try: stories = facebook_user.get_object(page, fields=fields) except GraphAPIError as e: message = str(e).lower() - if 'session has expired' in message: - logging.debug(' ***> [%-30s] ~FRFacebook page failed/expired, disconnecting facebook: %s: %s' % - (self.feed.log_title[:30], self.feed.feed_address, e)) + if "session has expired" in message: + logging.debug( + " ***> [%-30s] ~FRFacebook page failed/expired, disconnecting facebook: %s: %s" + % (self.feed.log_title[:30], self.feed.feed_address, e) + ) self.feed.save_feed_history(560, "Facebook Error: Expired token") return {} - + if not stories: return {} return stories - + def page_posts_story(self, facebook_user, page_story): categories = set() - if 'message' not in page_story: + if "message" not in page_story: # Probably a story shared on the page's timeline, not a published story return - message = linebreaks(page_story['message']) - created_date = page_story['created_time'] + message = linebreaks(page_story["message"]) + created_date = page_story["created_time"] if isinstance(created_date, str): created_date = dateutil.parser.parse(created_date) - fields = facebook_user.get_object(page_story['id'], fields='permalink_url,link,attachments') - permalink = fields.get('link', fields['permalink_url']) + fields = facebook_user.get_object(page_story["id"], fields="permalink_url,link,attachments") + permalink = fields.get("link", fields["permalink_url"]) attachments_html = "" - if fields.get('attachments', None) and fields['attachments']['data']: - for attachment in fields['attachments']['data']: - if 'media' in attachment: - attachments_html += "" % attachment['media']['image']['src'] - if attachment.get('subattachments', None): - for subattachment in attachment['subattachments']['data']: - attachments_html += "" % subattachment['media']['image']['src'] - + if fields.get("attachments", None) and fields["attachments"]["data"]: + for attachment in fields["attachments"]["data"]: + if "media" in attachment: + attachments_html += '' % attachment["media"]["image"]["src"] + if attachment.get("subattachments", None): + for subattachment in attachment["subattachments"]["data"]: + attachments_html += '' % subattachment["media"]["image"]["src"] + content = """
%s
%s
""" % ( message, - attachments_html + attachments_html, ) - + story = { - 'title': message, - 'link': permalink, - 'description': content, - 'categories': list(categories), - 'unique_id': "fb_post:%s" % page_story['id'], - 'pubdate': created_date, + "title": message, + "link": permalink, + "description": content, + "categories": list(categories), + "unique_id": "fb_post:%s" % page_story["id"], + "pubdate": created_date, } - + return story - + def page_video_story(self, facebook_user, page_story): categories = set() - if 'description' not in page_story: + if "description" not in page_story: return - message = linebreaks(page_story['description']) - created_date = page_story['updated_time'] + message = linebreaks(page_story["description"]) + created_date = page_story["updated_time"] if isinstance(created_date, str): created_date = dateutil.parser.parse(created_date) - permalink = facebook_user.get_object(page_story['id'], fields='permalink_url')['permalink_url'] - embed_html = facebook_user.get_object(page_story['id'], fields='embed_html') - - if permalink.startswith('/'): + permalink = facebook_user.get_object(page_story["id"], fields="permalink_url")["permalink_url"] + embed_html = facebook_user.get_object(page_story["id"], fields="embed_html") + + if permalink.startswith("/"): permalink = "https://www.facebook.com%s" % permalink - + content = """
%s
%s
""" % ( message, - embed_html.get('embed_html', '') + embed_html.get("embed_html", ""), ) - + story = { - 'title': page_story.get('story', message), - 'link': permalink, - 'description': content, - 'categories': list(categories), - 'unique_id': "fb_post:%s" % page_story['id'], - 'pubdate': created_date, + "title": page_story.get("story", message), + "link": permalink, + "description": content, + "categories": list(categories), + "unique_id": "fb_post:%s" % page_story["id"], + "pubdate": created_date, } - + return story - + def favicon_url(self): page_name = self.extract_page_name() facebook_user = self.facebook_user() if not facebook_user: - logging.debug(' ***> [%-30s] ~FRFacebook icon failed, disconnecting facebook: %s' % - (self.feed.log_title[:30], self.feed.feed_address)) + logging.debug( + " ***> [%-30s] ~FRFacebook icon failed, disconnecting facebook: %s" + % (self.feed.log_title[:30], self.feed.feed_address) + ) return - + try: - picture_data = facebook_user.get_object(page_name, fields='picture') + picture_data = facebook_user.get_object(page_name, fields="picture") except GraphAPIError as e: message = str(e).lower() - if 'session has expired' in message: - logging.debug(' ***> [%-30s] ~FRFacebook icon failed/expired, disconnecting facebook: %s: %s' % - (self.feed.log_title[:30], self.feed.feed_address, e)) + if "session has expired" in message: + logging.debug( + " ***> [%-30s] ~FRFacebook icon failed/expired, disconnecting facebook: %s: %s" + % (self.feed.log_title[:30], self.feed.feed_address, e) + ) return - if 'picture' in picture_data: - return picture_data['picture']['data']['url'] - \ No newline at end of file + if "picture" in picture_data: + return picture_data["picture"]["data"]["url"] diff --git a/utils/feed_fetcher.py b/utils/feed_fetcher.py index c6b804418e..0a7527f8a5 100644 --- a/utils/feed_fetcher.py +++ b/utils/feed_fetcher.py @@ -37,8 +37,8 @@ from apps.rss_feeds.page_importer import PageImporter from apps.statistics.models import MAnalyticsFetcher, MStatistics -feedparser.sanitizer._HTMLSanitizer.acceptable_elements.update(['iframe']) -feedparser.sanitizer._HTMLSanitizer.acceptable_elements.update(['text']) +feedparser.sanitizer._HTMLSanitizer.acceptable_elements.update(["iframe"]) +feedparser.sanitizer._HTMLSanitizer.acceptable_elements.update(["text"]) from bs4 import BeautifulSoup from celery.exceptions import SoftTimeLimitExceeded @@ -52,7 +52,7 @@ from utils import json_functions as json from utils import log as logging from utils.facebook_fetcher import FacebookFetcher -from utils.feed_functions import TimeoutError, timelimit +from utils.feed_functions import TimeoutError, strip_underscore_from_feed_address, timelimit from utils.json_fetcher import JSONFetcher from utils.story_functions import linkify, pre_process_story, strip_tags from utils.twitter_fetcher import TwitterFetcher @@ -66,6 +66,8 @@ FEED_OK, FEED_SAME, FEED_ERRPARSE, FEED_ERRHTTP, FEED_ERREXC = list(range(5)) +NO_UNDERSCORE_ADDRESSES = ["jwz"] + class FetchFeed: def __init__(self, feed_id, options): @@ -81,15 +83,15 @@ def fetch(self): """ start = time.time() identity = self.get_identity() - if self.options.get('archive_page', None): - log_msg = '%2s ---> [%-30s] ~FYFetching feed (~FB%d~FY) ~BG~FMarchive page~ST~FY: ~SB%s' % ( + if self.options.get("archive_page", None): + log_msg = "%2s ---> [%-30s] ~FYFetching feed (~FB%d~FY) ~BG~FMarchive page~ST~FY: ~SB%s" % ( identity, self.feed.log_title[:30], self.feed.id, - self.options['archive_page'], + self.options["archive_page"], ) else: - log_msg = '%2s ---> [%-30s] ~FYFetching feed (~FB%d~FY), last update: %s' % ( + log_msg = "%2s ---> [%-30s] ~FYFetching feed (~FB%d~FY), last update: %s" % ( identity, self.feed.log_title[:30], self.feed.id, @@ -101,85 +103,88 @@ def fetch(self): modified = self.feed.last_modified.utctimetuple()[:7] if self.feed.last_modified else None address = self.feed.feed_address - if self.options.get('force') or self.options.get('archive_page', None) or random.random() <= 0.01: - self.options['force'] = True + if self.options.get("force") or self.options.get("archive_page", None) or random.random() <= 0.01: + self.options["force"] = True modified = None etag = None - if self.options.get('archive_page', None) == "rfc5005" and self.options.get('archive_page_link', None): - address = self.options['archive_page_link'] - elif self.options.get('archive_page', None): - address = qurl(address, add={self.options['archive_page_key']: self.options['archive_page']}) - elif address.startswith('http'): - address = qurl(address, add={"_": random.randint(0, 10000)}) - logging.debug(' ---> [%-30s] ~FBForcing fetch: %s' % (self.feed.log_title[:30], address)) + if self.options.get("archive_page", None) == "rfc5005" and self.options.get( + "archive_page_link", None + ): + address = self.options["archive_page_link"] + elif self.options.get("archive_page", None): + address = qurl(address, add={self.options["archive_page_key"]: self.options["archive_page"]}) + # Don't use the underscore cache buster: https://forum.newsblur.com/t/jwz-feed-broken-hes-mad-about-url-parameters/10742/15 + # elif address.startswith("http") and not any(item in address for item in NO_UNDERSCORE_ADDRESSES): + # address = qurl(address, add={"_": random.randint(0, 10000)}) + logging.debug(" ---> [%-30s] ~FBForcing fetch: %s" % (self.feed.log_title[:30], address)) elif not self.feed.fetched_once or not self.feed.known_good: modified = None etag = None - if self.options.get('feed_xml'): + if self.options.get("feed_xml"): logging.debug( - ' ---> [%-30s] ~FM~BKFeed has been fat pinged. Ignoring fat: %s' - % (self.feed.log_title[:30], len(self.options.get('feed_xml'))) + " ---> [%-30s] ~FM~BKFeed has been fat pinged. Ignoring fat: %s" + % (self.feed.log_title[:30], len(self.options.get("feed_xml"))) ) - if self.options.get('fpf'): - self.fpf = self.options.get('fpf') + if self.options.get("fpf"): + self.fpf = self.options.get("fpf") logging.debug( - ' ---> [%-30s] ~FM~BKFeed fetched in real-time with fat ping.' % (self.feed.log_title[:30]) + " ---> [%-30s] ~FM~BKFeed fetched in real-time with fat ping." % (self.feed.log_title[:30]) ) return FEED_OK, self.fpf - if 'youtube.com' in address: + if "youtube.com" in address: youtube_feed = self.fetch_youtube() if not youtube_feed: logging.debug( - ' ***> [%-30s] ~FRYouTube fetch failed: %s.' % (self.feed.log_title[:30], address) + " ***> [%-30s] ~FRYouTube fetch failed: %s." % (self.feed.log_title[:30], address) ) return FEED_ERRHTTP, None self.fpf = feedparser.parse(youtube_feed, sanitize_html=False) - elif re.match(r'(https?)?://twitter.com/\w+/?', qurl(address, remove=['_'])): + elif re.match(r"(https?)?://twitter.com/\w+/?", qurl(address, remove=["_"])): twitter_feed = self.fetch_twitter(address) if not twitter_feed: logging.debug( - ' ***> [%-30s] ~FRTwitter fetch failed: %s' % (self.feed.log_title[:30], address) + " ***> [%-30s] ~FRTwitter fetch failed: %s" % (self.feed.log_title[:30], address) ) return FEED_ERRHTTP, None self.fpf = feedparser.parse(twitter_feed) - elif re.match(r'(.*?)facebook.com/\w+/?$', qurl(address, remove=['_'])): + elif re.match(r"(.*?)facebook.com/\w+/?$", qurl(address, remove=["_"])): facebook_feed = self.fetch_facebook() if not facebook_feed: logging.debug( - ' ***> [%-30s] ~FRFacebook fetch failed: %s' % (self.feed.log_title[:30], address) + " ***> [%-30s] ~FRFacebook fetch failed: %s" % (self.feed.log_title[:30], address) ) return FEED_ERRHTTP, None self.fpf = feedparser.parse(facebook_feed) - if not self.fpf and 'json' in address: + if not self.fpf and "json" in address: try: headers = self.feed.fetch_headers() if etag: - headers['If-None-Match'] = etag + headers["If-None-Match"] = etag if modified: # format into an RFC 1123-compliant timestamp. We can't use # time.strftime() since the %a and %b directives can be affected # by the current locale, but RFC 2616 states that dates must be # in English. - short_weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + short_weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", ] - modified_header = '%s, %02d %s %04d %02d:%02d:%02d GMT' % ( + modified_header = "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( short_weekdays[modified[6]], modified[2], months[modified[1] - 1], @@ -188,9 +193,9 @@ def fetch(self): modified[4], modified[5], ) - headers['If-Modified-Since'] = modified_header + headers["If-Modified-Since"] = modified_header if etag or modified: - headers['A-IM'] = 'feed' + headers["A-IM"] = "feed" try: raw_feed = requests.get(address, headers=headers, timeout=15) except (requests.adapters.ConnectionError, TimeoutError): @@ -202,7 +207,10 @@ def fetch(self): % (self.feed.log_title[:30], raw_feed.status_code, raw_feed.headers) ) else: - logging.debug(" ***> [%-30s] ~FRJson feed fetch timed out, trying fake headers: %s" % (self.feed.log_title[:30], address)) + logging.debug( + " ***> [%-30s] ~FRJson feed fetch timed out, trying fake headers: %s" + % (self.feed.log_title[:30], address) + ) raw_feed = requests.get( self.feed.feed_address, headers=self.feed.fetch_headers(fake=True), @@ -210,24 +218,24 @@ def fetch(self): ) json_feed_content_type = any( - json_feed in raw_feed.headers.get('Content-Type', "") - for json_feed in ['application/feed+json', 'application/json'] + json_feed in raw_feed.headers.get("Content-Type", "") + for json_feed in ["application/feed+json", "application/json"] ) if raw_feed.content and json_feed_content_type: # JSON Feed json_feed = self.fetch_json_feed(address, raw_feed) if not json_feed: logging.debug( - ' ***> [%-30s] ~FRJSON fetch failed: %s' % (self.feed.log_title[:30], address) + " ***> [%-30s] ~FRJSON fetch failed: %s" % (self.feed.log_title[:30], address) ) return FEED_ERRHTTP, None self.fpf = feedparser.parse(json_feed) elif raw_feed.content and raw_feed.status_code < 400: response_headers = raw_feed.headers - response_headers['Content-Location'] = raw_feed.url + response_headers["Content-Location"] = raw_feed.url self.raw_feed = smart_str(raw_feed.content) self.fpf = feedparser.parse(self.raw_feed, response_headers=response_headers) - if self.options['verbose']: + if self.options["verbose"]: logging.debug( " ---> [%-30s] ~FBFeed fetch status %s: %s length / %s" % ( @@ -244,7 +252,7 @@ def fetch(self): ) # raise e - if not self.fpf or self.options.get('force_fp', False): + if not self.fpf or self.options.get("force_fp", False): try: self.fpf = feedparser.parse(address, agent=self.feed.user_agent, etag=etag, modified=modified) except ( @@ -260,12 +268,14 @@ def fetch(self): ConnectionResetError, TimeoutError, ) as e: - logging.debug(' ***> [%-30s] ~FRFeed fetch error: %s' % (self.feed.log_title[:30], e)) + logging.debug(" ***> [%-30s] ~FRFeed fetch error: %s" % (self.feed.log_title[:30], e)) pass if not self.fpf: try: - logging.debug(' ***> [%-30s] ~FRTurning off headers: %s' % (self.feed.log_title[:30], address)) + logging.debug( + " ***> [%-30s] ~FRTurning off headers: %s" % (self.feed.log_title[:30], address) + ) self.fpf = feedparser.parse(address, agent=self.feed.user_agent) except ( TypeError, @@ -279,11 +289,11 @@ def fetch(self): http.client.IncompleteRead, ConnectionResetError, ) as e: - logging.debug(' ***> [%-30s] ~FRFetch failed: %s.' % (self.feed.log_title[:30], e)) + logging.debug(" ***> [%-30s] ~FRFetch failed: %s." % (self.feed.log_title[:30], e)) return FEED_ERRHTTP, None logging.debug( - ' ---> [%-30s] ~FYFeed fetch in ~FM%.4ss' % (self.feed.log_title[:30], time.time() - start) + " ---> [%-30s] ~FYFeed fetch in ~FM%.4ss" % (self.feed.log_title[:30], time.time() - start) ) return FEED_OK, self.fpf @@ -333,21 +343,21 @@ def process(self): start = time.time() self.refresh_feed() - if not self.options.get('archive_page', None): + if not self.options.get("archive_page", None): feed_status, ret_values = self.verify_feed_integrity() if feed_status and ret_values: return feed_status, ret_values - + self.fpf.entries = self.fpf.entries[:100] - if not self.options.get('archive_page', None): + if not self.options.get("archive_page", None): self.compare_feed_attribute_changes() # Determine if stories aren't valid and replace broken guids guids_seen = set() permalinks_seen = set() for entry in self.fpf.entries: - guids_seen.add(entry.get('guid')) + guids_seen.add(entry.get("guid")) permalinks_seen.add(Feed.get_permalink(entry)) guid_difference = len(guids_seen) != len(self.fpf.entries) single_guid = len(guids_seen) == 1 @@ -363,45 +373,45 @@ def process(self): stories = [] for entry in self.fpf.entries: story = pre_process_story(entry, self.fpf.encoding) - if not story['title'] and not story['story_content']: + if not story["title"] and not story["story_content"]: continue - if self.options.get('archive_page', None) and story.get('published') > day_ago: + if self.options.get("archive_page", None) and story.get("published") > day_ago: # Archive only: Arbitrary but necessary to prevent feeds from creating an unlimited number of stories # because they don't have a guid so it gets auto-generated based on the date, and if the story # is missing a date, then the latest date gets used. So reject anything newer than 24 hours old # when filling out the archive. # logging.debug(f" ---> [%-30s] ~FBTossing story because it's too new for the archive: ~SB{story}") continue - if story.get('published') < start_date: - start_date = story.get('published') + 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']: + 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) + " ---> [%-30s] ~FBReplacing guid (%s) with timestamp: %s" + % (self.feed.log_title[:30], story.get("guid"), new_story_guid) ) - story['guid'] = new_story_guid + story["guid"] = new_story_guid else: new_story_guid = Feed.get_permalink(story) - if self.options['verbose']: + if self.options["verbose"]: logging.debug( - ' ---> [%-30s] ~FBReplacing guid (%s) with permalink: %s' - % (self.feed.log_title[:30], story.get('guid'), new_story_guid) + " ---> [%-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')) + 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')) + 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: + if self.options["verbose"] or settings.DEBUG: logging.debug( - ' ---> [%-30s] ~FBFound ~SB%s~SN guids, adding ~SB%s~SN/%s guids from db' + " ---> [%-30s] ~FBFound ~SB%s~SN guids, adding ~SB%s~SN/%s guids from db" % ( self.feed.log_title[:30], original_story_hash_count, @@ -427,53 +437,53 @@ def process(self): ret_values = self.feed.add_update_stories( stories, existing_stories, - verbose=self.options['verbose'], - updates_off=self.options['updates_off'], + verbose=self.options["verbose"], + updates_off=self.options["updates_off"], ) # PubSubHubbub - if not self.options.get('archive_page', None): + if not self.options.get("archive_page", None): self.check_feed_for_push() # Push notifications - if ret_values['new'] > 0 and MUserFeedNotification.feed_has_users(self.feed.pk) > 0: - QueueNotifications.delay(self.feed.pk, ret_values['new']) + 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' + " ---> [%-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'], + "~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']) + 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']: - if not getattr(settings, 'TEST_DEBUG', False): + if ret_values["new"]: + if not getattr(settings, "TEST_DEBUG", False): self.feed.trim_feed() self.feed.expire_redis() - if MStatistics.get('raw_feed', None) == self.feed.pk: + if MStatistics.get("raw_feed", None) == self.feed.pk: self.feed.save_raw_feed(self.raw_feed, fetch_date) self.feed.save_feed_history(200, "OK", date=fetch_date) - if self.options['verbose']: + if self.options["verbose"]: logging.debug( - ' ---> [%-30s] ~FBTIME: feed parse in ~FM%.4ss' + " ---> [%-30s] ~FBTIME: feed parse in ~FM%.4ss" % (self.feed.log_title[:30], time.time() - start) ) - if self.options.get('archive_page', None): + if self.options.get("archive_page", None): self.archive_seen_story_hashes.update(story_hashes) - + return FEED_OK, ret_values def verify_feed_integrity(self): @@ -487,12 +497,12 @@ def verify_feed_integrity(self): if not self.feed: return FEED_ERREXC, ret_values - - if hasattr(self.fpf, 'status'): - if self.options['verbose']: + + 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)' + " ---> [%-30s] ~FRBOZO exception: %s ~SB(%s entries)" % (self.feed.log_title[:30], self.fpf.bozo_exception, len(self.fpf.entries)) ) @@ -504,17 +514,17 @@ def verify_feed_integrity(self): # 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'): + if self.fpf.href.endswith("feedburner.com/atom.xml"): return FEED_ERRHTTP, ret_values - redirects, non_redirects = self.feed.count_redirects_in_history('feed') + 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)) ) if len(redirects) >= 10 or len(non_redirects) == 0: address = self.fpf.href - if self.options['force'] and address: - address = qurl(address, remove=['_']) - self.feed.feed_address = address + if self.options["force"] and address: + address = qurl(address, remove=["_"]) + self.feed.feed_address = strip_underscore_from_feed_address(address) if not self.feed.known_good: self.feed.fetched_once = True logging.debug( @@ -559,7 +569,7 @@ def verify_feed_integrity(self): if not self.feed.known_good: fixed_feed, feed = self.feed.check_feed_link_for_feed_address() if not fixed_feed: - self.feed.save_feed_history(552, 'Non-xml feed', self.fpf.bozo_exception) + self.feed.save_feed_history(552, "Non-xml feed", self.fpf.bozo_exception) else: self.feed = feed self.feed = self.feed.save() @@ -573,7 +583,7 @@ def verify_feed_integrity(self): if not self.feed.known_good: fixed_feed, feed = self.feed.check_feed_link_for_feed_address() if not fixed_feed: - self.feed.save_feed_history(553, 'Not an RSS feed', self.fpf.bozo_exception) + self.feed.save_feed_history(553, "Not an RSS feed", self.fpf.bozo_exception) else: self.feed = feed self.feed = self.feed.save() @@ -588,69 +598,69 @@ def compare_feed_attribute_changes(self): if not self.feed: logging.debug(f"Missing feed: {self.feed}") return - + original_etag = self.feed.etag - self.feed.etag = self.fpf.get('etag') + self.feed.etag = self.fpf.get("etag") if self.feed.etag: self.feed.etag = self.feed.etag[:255] # some times this is None (it never should) *sigh* if self.feed.etag is None: - self.feed.etag = '' + self.feed.etag = "" if self.feed.etag != original_etag: - self.feed.save(update_fields=['etag']) + self.feed.save(update_fields=["etag"]) original_last_modified = self.feed.last_modified - if hasattr(self.fpf, 'modified') and self.fpf.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.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.feed.save(update_fields=["last_modified"]) 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.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']) + self.feed.save(update_fields=["feed_title"]) - tagline = self.fpf.feed.get('tagline', self.feed.data.feed_tagline) + tagline = self.fpf.feed.get("tagline", self.feed.data.feed_tagline) if tagline: original_tagline = self.feed.data.feed_tagline self.feed.data.feed_tagline = smart_str(tagline) if self.feed.data.feed_tagline != original_tagline: - self.feed.data.save(update_fields=['feed_tagline']) + self.feed.data.save(update_fields=["feed_tagline"]) if not self.feed.feed_link_locked: - new_feed_link = self.fpf.feed.get('link') or self.fpf.feed.get('id') or self.feed.feed_link - if self.options['force'] and new_feed_link: - new_feed_link = qurl(new_feed_link, remove=['_']) + new_feed_link = self.fpf.feed.get("link") or self.fpf.feed.get("id") or self.feed.feed_link + 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) ) - redirects, non_redirects = self.feed.count_redirects_in_history('page') + redirects, non_redirects = self.feed.count_redirects_in_history("page") 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']) + self.feed.save(update_fields=["feed_link"]) def check_feed_for_push(self): - if not (hasattr(self.fpf, 'feed') and hasattr(self.fpf.feed, 'links') and self.fpf.feed.links): + if not (hasattr(self.fpf, "feed") and hasattr(self.fpf.feed, "links") and self.fpf.feed.links): return - + hub_url = None self_url = self.feed.feed_address for link in self.fpf.feed.links: - if link['rel'] == 'hub' and not hub_url: - hub_url = link['href'] - elif link['rel'] == 'self': - self_url = link['href'] + if link["rel"] == "hub" and not hub_url: + hub_url = link["href"] + elif link["rel"] == "self": + self_url = link["href"] push_expired = False if self.feed.is_push: try: @@ -662,10 +672,10 @@ def check_feed_for_push(self): 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')) + and (push_expired or not self.feed.is_push or self.options.get("force")) ): logging.debug( - ' ---> [%-30s] ~BB~FW%sSubscribing to PuSH hub: %s' + " ---> [%-30s] ~BB~FW%sSubscribing to PuSH hub: %s" % (self.feed.log_title[:30], "~SKRe-~SN" if push_expired else "", hub_url) ) try: @@ -673,13 +683,11 @@ def check_feed_for_push(self): 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' + " ---> [%-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~FWTurning off PuSH, no hub found" % (self.feed.log_title[:30])) self.feed.is_push = False self.feed = self.feed.save() @@ -695,11 +703,11 @@ def __init__(self, options): 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() @@ -713,15 +721,15 @@ def reset_database_connections(self): connection._connection_settings = {} connection._dbs = {} settings.MONGODB = connect(settings.MONGO_DB_NAME, **settings.MONGO_DB) - if 'username' in settings.MONGO_ANALYTICS_DB: + if "username" in settings.MONGO_ANALYTICS_DB: settings.MONGOANALYTICSDB = connect( - db=settings.MONGO_ANALYTICS_DB['name'], + 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'], + db=settings.MONGO_ANALYTICS_DB["name"], host=f"mongodb://{settings.MONGO_ANALYTICS_DB['host']}/", alias="nbanalytics", ) @@ -738,15 +746,15 @@ def process_feed_wrapper(self, feed_queue): identity = current_process._identity[0] # If fetching archive pages, come back once the archive scaffolding is built - if self.options.get('archive_page', None): + if self.options.get("archive_page", None): for feed_id in feed_queue: feed = self.refresh_feed(feed_id) try: self.fetch_and_process_archive_pages(feed_id) except SoftTimeLimitExceeded: logging.debug( - ' ---> [%-30s] ~FRTime limit reached while fetching ~FGarchive pages~FR. Made it to ~SB%s' - % (feed.log_title[:30], self.options['archive_page']) + " ---> [%-30s] ~FRTime limit reached while fetching ~FGarchive pages~FR. Made it to ~SB%s" + % (feed.log_title[:30], self.options["archive_page"]) ) pass if len(feed_queue) == 1: @@ -771,21 +779,21 @@ def process_feed_wrapper(self, feed_queue): set_user({"id": feed_id, "username": feed.feed_title}) skip = False - if self.options.get('fake'): + if self.options.get("fake"): skip = True weight = "-" quick = "-" rand = "-" elif ( - self.options.get('quick') - and not self.options['force'] + 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)) + quick = float(self.options.get("quick", 0)) rand = random.random() if random_weight < 1000 and rand < quick: skip = True @@ -796,7 +804,7 @@ def process_feed_wrapper(self, feed_queue): rand = "-" if skip: logging.debug( - ' ---> [%-30s] ~BGFaking fetch, skipping (%s/month, %s subs, %s < %s)...' + " ---> [%-30s] ~BGFaking fetch, skipping (%s/month, %s subs, %s < %s)..." % (feed.log_title[:30], weight, feed.num_subscribers, rand, quick) ) continue @@ -807,74 +815,74 @@ def process_feed_wrapper(self, feed_queue): 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']: + if (ret_entries and ret_entries["new"]) or self.options["force"]: start = time.time() if not feed.known_good or not feed.fetched_once: feed.known_good = True feed.fetched_once = True feed = feed.save() - if self.options['force'] or random.random() <= 0.02: + if self.options["force"] or random.random() <= 0.02: logging.debug( - ' ---> [%-30s] ~FBPerforming feed cleanup...' % (feed.log_title[:30],) + " ---> [%-30s] ~FBPerforming feed cleanup..." % (feed.log_title[:30],) ) start_cleanup = time.time() feed.count_fs_size_bytes() logging.debug( - ' ---> [%-30s] ~FBDone with feed cleanup. Took ~SB%.4s~SN sec.' + " ---> [%-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],) + " ---> [%-30s] Unread count took too long..." % (feed.log_title[:30],) ) - if self.options['verbose']: + if self.options["verbose"]: logging.debug( - ' ---> [%-30s] ~FBTIME: unread count in ~FM%.4ss' + " ---> [%-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) + " ---> [%-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: - logging.debug(' ---> [%-30s] ~FRFeed is now gone...' % (str(feed_id)[:30])) + logging.debug(" ---> [%-30s] ~FRFeed is now gone..." % (str(feed_id)[:30])) continue except SoftTimeLimitExceeded as e: logging.debug(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed) ret_feed = FEED_ERREXC fetched_feed = None feed_code = 559 - feed.save_feed_history(feed_code, 'Timeout', e) + feed.save_feed_history(feed_code, "Timeout", e) except TimeoutError as e: - logging.debug(' ---> [%-30s] ~FRFeed fetch timed out...' % (feed.log_title[:30])) + logging.debug(" ---> [%-30s] ~FRFeed fetch timed out..." % (feed.log_title[:30])) feed_code = 505 - feed.save_feed_history(feed_code, 'Timeout', e) + feed.save_feed_history(feed_code, "Timeout", e) fetched_feed = None except Exception as e: - logging.debug('[%d] ! -------------------------' % (feed_id,)) + logging.debug("[%d] ! -------------------------" % (feed_id,)) tb = traceback.format_exc() logging.error(tb) - logging.debug('[%d] ! -------------------------' % (feed_id,)) + logging.debug("[%d] ! -------------------------" % (feed_id,)) ret_feed = FEED_ERREXC - feed = Feed.get_by_id(getattr(feed, 'pk', feed_id)) + feed = Feed.get_by_id(getattr(feed, "pk", feed_id)) 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() @@ -897,7 +905,7 @@ def process_feed_wrapper(self, feed_queue): continue if ( - (self.options['force']) + (self.options["force"]) or (random.random() > 0.9) or ( fetched_feed @@ -906,8 +914,7 @@ def process_feed_wrapper(self, feed_queue): 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)) + logging.debug(" ---> [%-30s] ~FYFetching page: %s" % (feed.log_title[:30], feed.feed_link)) page_importer = PageImporter(feed) try: page_data = page_importer.fetch_page() @@ -917,27 +924,27 @@ def process_feed_wrapper(self, feed_queue): " ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed ) page_data = None - feed.save_feed_history(557, 'Timeout', e) + feed.save_feed_history(557, "Timeout", e) except TimeoutError: - logging.debug(' ---> [%-30s] ~FRPage fetch timed out...' % (feed.log_title[:30])) + logging.debug(" ---> [%-30s] ~FRPage fetch timed out..." % (feed.log_title[:30])) page_data = None - feed.save_page_history(555, 'Timeout', '') + feed.save_page_history(555, "Timeout", "") except Exception as e: - logging.debug('[%d] ! -------------------------' % (feed_id,)) + logging.debug("[%d] ! -------------------------" % (feed_id,)) tb = traceback.format_exc() logging.error(tb) - logging.debug('[%d] ! -------------------------' % (feed_id,)) + logging.debug("[%d] ! -------------------------" % (feed_id,)) feed.save_page_history(550, "Page Error", tb) 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'] + logging.debug(" ---> [%-30s] ~FYFetching icon: %s" % (feed.log_title[:30], feed.feed_link)) + force = self.options["force"] if random.random() > 0.99: force = True icon_importer = IconImporter(feed, page_data=page_data, force=force) @@ -948,28 +955,28 @@ def process_feed_wrapper(self, feed_queue): logging.debug( " ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed ) - feed.save_feed_history(558, 'Timeout', e) + feed.save_feed_history(558, "Timeout", e) except TimeoutError: - logging.debug(' ---> [%-30s] ~FRIcon fetch timed out...' % (feed.log_title[:30])) - feed.save_page_history(556, 'Timeout', '') + logging.debug(" ---> [%-30s] ~FRIcon fetch timed out..." % (feed.log_title[:30])) + feed.save_page_history(556, "Timeout", "") except Exception as e: - logging.debug('[%d] ! -------------------------' % (feed_id,)) + logging.debug("[%d] ! -------------------------" % (feed_id,)) tb = traceback.format_exc() logging.error(tb) - logging.debug('[%d] ! -------------------------' % (feed_id,)) + 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' + " ---> [%-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]', + "" if feed.has_page else " [HAS NO PAGE]", ) ) @@ -979,7 +986,7 @@ def process_feed_wrapper(self, feed_queue): feed.last_load_time = round(delta) feed.fetched_once = True try: - feed = feed.save(update_fields=['last_load_time', 'fetched_once']) + feed = feed.save(update_fields=["last_load_time", "fetched_once"]) except IntegrityError: logging.debug( " ***> [%-30s] ~FRIntegrityError on feed: %s" @@ -989,10 +996,10 @@ def process_feed_wrapper(self, feed_queue): ) ) - if ret_entries and ret_entries['new']: - self.publish_to_subscribers(feed, ret_entries['new']) + 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]' % ( + done_msg = "%2s ---> [%-30s] ~FYProcessed in ~FM~SB%.4ss~FY~SN (~FB%s~FY) [%s]" % ( identity, feed.log_title[:30], delta, @@ -1021,31 +1028,38 @@ def process_feed_wrapper(self, feed_queue): def fetch_and_process_archive_pages(self, feed_id): feed = Feed.get_by_id(feed_id) first_seen_feed = None - original_starting_page = self.options['archive_page'] - + original_starting_page = self.options["archive_page"] + for archive_page_key in ["page", "paged", "rfc5005"]: seen_story_hashes = set() failed_pages = 0 - self.options['archive_page_key'] = archive_page_key + self.options["archive_page_key"] = archive_page_key if archive_page_key == "rfc5005": - self.options['archive_page'] = "rfc5005" + self.options["archive_page"] = "rfc5005" link_prev_archive = None if first_seen_feed: - for link in getattr(first_seen_feed.feed, 'links', []): - if link['rel'] == 'prev-archive' or link['rel'] == 'next': - link_prev_archive = link['href'] - logging.debug(' ---> [%-30s] ~FGFeed has ~SBRFC5005~SN links, filling out archive: %s' % (feed.log_title[:30], link_prev_archive)) + for link in getattr(first_seen_feed.feed, "links", []): + if link["rel"] == "prev-archive" or link["rel"] == "next": + link_prev_archive = link["href"] + logging.debug( + " ---> [%-30s] ~FGFeed has ~SBRFC5005~SN links, filling out archive: %s" + % (feed.log_title[:30], link_prev_archive) + ) break else: - logging.debug(' ---> [%-30s] ~FBFeed has no RFC5005 links...' % (feed.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FBFeed has no RFC5005 links..." % (feed.log_title[:30]) + ) else: - self.options['archive_page_link'] = link_prev_archive + self.options["archive_page_link"] = link_prev_archive ffeed = FetchFeed(feed_id, self.options) try: ret_feed, fetched_feed = ffeed.fetch() except TimeoutError: - logging.debug(' ---> [%-30s] ~FRArchive feed fetch timed out...' % (feed.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FRArchive feed fetch timed out..." % (feed.log_title[:30]) + ) # Timeout means don't bother to keep checking... continue @@ -1055,9 +1069,9 @@ def fetch_and_process_archive_pages(self, feed_id): pfeed = ProcessFeed(feed_id, fetched_feed, self.options, raw_feed=raw_feed) if not pfeed.fpf or not pfeed.fpf.entries: continue - for link in getattr(pfeed.fpf.feed, 'links', []): - if link['rel'] == 'prev-archive' or link['rel'] == 'next': - link_prev_archive = link['href'] + for link in getattr(pfeed.fpf.feed, "links", []): + if link["rel"] == "prev-archive" or link["rel"] == "next": + link_prev_archive = link["href"] if not link_prev_archive: continue @@ -1065,16 +1079,21 @@ def fetch_and_process_archive_pages(self, feed_id): while True: if not link_prev_archive: break - if link_prev_archive == self.options.get('archive_page_link', None): - logging.debug(' ---> [%-30s] ~FRNo change in archive page link: %s' % (feed.log_title[:30], link_prev_archive)) - break - self.options['archive_page_link'] = link_prev_archive + if link_prev_archive == self.options.get("archive_page_link", None): + logging.debug( + " ---> [%-30s] ~FRNo change in archive page link: %s" + % (feed.log_title[:30], link_prev_archive) + ) + break + self.options["archive_page_link"] = link_prev_archive link_prev_archive = None ffeed = FetchFeed(feed_id, self.options) try: ret_feed, fetched_feed = ffeed.fetch() except TimeoutError as e: - logging.debug(' ---> [%-30s] ~FRArchive feed fetch timed out...' % (feed.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FRArchive feed fetch timed out..." % (feed.log_title[:30]) + ) # Timeout means don't bother to keep checking... break @@ -1083,15 +1102,22 @@ def fetch_and_process_archive_pages(self, feed_id): if fetched_feed and ret_feed == FEED_OK: pfeed = ProcessFeed(feed_id, fetched_feed, self.options, raw_feed=raw_feed) if not pfeed.fpf or not pfeed.fpf.entries: - logging.debug(' ---> [%-30s] ~FRFeed parse failed, no entries' % (feed.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FRFeed parse failed, no entries" % (feed.log_title[:30]) + ) continue - for link in getattr(pfeed.fpf.feed, 'links', []): - if link['rel'] == 'prev-archive' or link['rel'] == 'next': - link_prev_archive = link['href'] - logging.debug(' ---> [%-30s] ~FGFeed still has ~SBRFC5005~SN links, continuing filling out archive: %s' % (feed.log_title[:30], link_prev_archive)) + for link in getattr(pfeed.fpf.feed, "links", []): + if link["rel"] == "prev-archive" or link["rel"] == "next": + link_prev_archive = link["href"] + logging.debug( + " ---> [%-30s] ~FGFeed still has ~SBRFC5005~SN links, continuing filling out archive: %s" + % (feed.log_title[:30], link_prev_archive) + ) break else: - logging.debug(' ---> [%-30s] ~FBFeed has no more RFC5005 links...' % (feed.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FBFeed has no more RFC5005 links..." % (feed.log_title[:30]) + ) break before_story_hashes = len(seen_story_hashes) @@ -1100,23 +1126,30 @@ def fetch_and_process_archive_pages(self, feed_id): after_story_hashes = len(seen_story_hashes) if before_story_hashes == after_story_hashes: - logging.debug(' ---> [%-30s] ~FRNo change in story hashes, but has archive link: %s' % (feed.log_title[:30], link_prev_archive)) - + logging.debug( + " ---> [%-30s] ~FRNo change in story hashes, but has archive link: %s" + % (feed.log_title[:30], link_prev_archive) + ) + failed_color = "~FR" if not link_prev_archive else "" - logging.debug(f" ---> [{feed.log_title[:30]:<30}] ~FGStory hashes found, archive RFC5005 ~SB{link_prev_archive}~SN: ~SB~FG{failed_color}{len(seen_story_hashes):,} stories~SN~FB") + logging.debug( + f" ---> [{feed.log_title[:30]:<30}] ~FGStory hashes found, archive RFC5005 ~SB{link_prev_archive}~SN: ~SB~FG{failed_color}{len(seen_story_hashes):,} stories~SN~FB" + ) else: for page in range(3 if settings.DEBUG and False else 150): if page < original_starting_page: continue - if failed_pages >= 1: + if failed_pages >= 1: break - self.options['archive_page'] = page+1 + self.options["archive_page"] = page + 1 ffeed = FetchFeed(feed_id, self.options) try: ret_feed, fetched_feed = ffeed.fetch() except TimeoutError as e: - logging.debug(' ---> [%-30s] ~FRArchive feed fetch timed out...' % (feed.log_title[:30])) + logging.debug( + " ---> [%-30s] ~FRArchive feed fetch timed out..." % (feed.log_title[:30]) + ) # Timeout means don't bother to keep checking... break @@ -1140,12 +1173,14 @@ def fetch_and_process_archive_pages(self, feed_id): else: failed_pages += 1 failed_color = "~FR" if failed_pages > 0 else "" - logging.debug(f" ---> [{feed.log_title[:30]:<30}] ~FGStory hashes found, archive page ~SB{page+1}~SN: ~SB~FG{len(seen_story_hashes):,} stories~SN~FB, {failed_color}{failed_pages} failures") + logging.debug( + f" ---> [{feed.log_title[:30]:<30}] ~FGStory hashes found, archive page ~SB{page+1}~SN: ~SB~FG{len(seen_story_hashes):,} stories~SN~FB, {failed_color}{failed_pages} failures" + ) 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) + 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) @@ -1158,7 +1193,7 @@ def count_unreads_for_subscribers(self, feed): user_subs = UserSubscription.objects.filter( feed=feed, active=True, user__profile__last_seen_on__gte=subscriber_expire - ).order_by('-last_read_date') + ).order_by("-last_read_date") if not user_subs.count(): return @@ -1168,16 +1203,16 @@ def count_unreads_for_subscribers(self, feed): sub.needs_unread_recalc = True sub.save() - if self.options['compute_scores']: + 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 = Feed.format_stories(stories, feed.pk) story_hashes = r.zrangebyscore( - 'zF:%s' % feed.pk, - int(feed.unread_cutoff.strftime('%s')), + "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]) + 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 @@ -1185,7 +1220,7 @@ def count_unreads_for_subscribers(self, feed): 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' + " ---> [%-30s] ~FYFound ~SB~FC%s(of %s)/%s~FY~SN un-secondaried stories while computing scores" % ( feed.log_title[:30], len(missing_stories), @@ -1195,7 +1230,7 @@ def count_unreads_for_subscribers(self, feed): ) 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)' + " ---> [%-30s] ~FYComputing scores: ~SB%s stories~SN with ~SB%s subscribers ~SN(%s/%s/%s)" % ( feed.log_title[:30], len(stories), @@ -1206,16 +1241,16 @@ def count_unreads_for_subscribers(self, feed): ) ) self.calculate_feed_scores_with_stories(user_subs, stories) - elif self.options.get('mongodb_replication_lag'): + 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')) + " ---> [%-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 + silent = False if getattr(self.options, "verbose", 0) >= 2 else True sub.calculate_feed_scores(silent=silent, stories=stories) @@ -1231,7 +1266,7 @@ def add_jobs(self, feeds_queue, feeds_count=1): self.feeds_count = feeds_count def run_jobs(self): - if self.options['single_threaded'] or self.num_threads == 1: + 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): diff --git a/utils/feed_functions.py b/utils/feed_functions.py index 104fff0107..ad55e99509 100644 --- a/utils/feed_functions.py +++ b/utils/feed_functions.py @@ -1,20 +1,28 @@ import datetime -import threading +import pprint +import random import sys +import threading import traceback -import pprint -import urllib.request, urllib.parse, urllib.error +import urllib.error import urllib.parse -import random +import urllib.request import warnings -from django.utils.translation import ungettext + +from qurl import qurl from django.utils.encoding import smart_str +from django.utils.translation import ungettext + from utils import log as logging -class TimeoutError(Exception): pass +class TimeoutError(Exception): + pass + + def timelimit(timeout): """borrowed from web.py""" + def _1(function): def _2(*args, **kw): class Dispatch(threading.Thread): @@ -23,7 +31,7 @@ def __init__(self): self.result = None self.error = None self.exc_info = None - + self.setDaemon(True) self.start() @@ -33,28 +41,31 @@ def run(self): except BaseException as e: self.error = e self.exc_info = sys.exc_info() + c = Dispatch() dispatch = c c.join(timeout) if c.is_alive(): - raise TimeoutError('took too long') + raise TimeoutError("took too long") if c.error: - tb = ''.join(traceback.format_exception(c.exc_info[0], c.exc_info[1], c.exc_info[2])) + tb = "".join(traceback.format_exception(c.exc_info[0], c.exc_info[1], c.exc_info[2])) logging.debug(f" ***> Traceback timeout error: {tb}") # mail_admins('Error in timeout: %s' % c.exc_info[0], tb) raise c.error return c.result + return _2 + return _1 - + def utf8encode(tstr): - """ Encodes a unicode string in utf-8 - """ + """Encodes a unicode string in utf-8""" msg = "utf8encode is deprecated. Use django.utils.encoding.smart_str instead." warnings.warn(msg, DeprecationWarning) return smart_str(tstr) + # From: http://www.poromenos.org/node/87 def levenshtein_distance(first, second): """Find the Levenshtein distance between two strings.""" @@ -70,24 +81,25 @@ def levenshtein_distance(first, second): second_length = len(second) + 1 distance_matrix = [[0] * second_length for x in range(first_length)] for i in range(first_length): - distance_matrix[i][0] = i + distance_matrix[i][0] = i for j in range(second_length): - distance_matrix[0][j]=j + distance_matrix[0][j] = j for i in range(1, first_length): for j in range(1, second_length): - deletion = distance_matrix[i-1][j] + 1 - insertion = distance_matrix[i][j-1] + 1 - substitution = distance_matrix[i-1][j-1] - if first[i-1] != second[j-1]: + deletion = distance_matrix[i - 1][j] + 1 + insertion = distance_matrix[i][j - 1] + 1 + substitution = distance_matrix[i - 1][j - 1] + if first[i - 1] != second[j - 1]: substitution += 1 distance_matrix[i][j] = min(insertion, deletion, substitution) - return distance_matrix[first_length-1][second_length-1] - + return distance_matrix[first_length - 1][second_length - 1] + + def _do_timesince(d, chunks, now=None): """ Started as a copy of django.util.timesince.timesince, but modified to only output one time unit, and use months as the maximum unit of measure. - + Takes two datetime objects and returns the time between d and now as a nicely formatted string, e.g. "10 minutes". If d occurs after now, then "0 minutes" is returned. @@ -110,83 +122,86 @@ def _do_timesince(d, chunks, now=None): count = since // seconds if count != 0: break - s = '%(number)d %(type)s' % {'number': count, 'type': name(count)} + s = "%(number)d %(type)s" % {"number": count, "type": name(count)} else: - s = 'just a second' + s = "just a second" return s + def relative_timesince(value): if not value: - return '' + return "" chunks = ( - (60 * 60 * 24, lambda n: ungettext('day', 'days', n)), - (60 * 60, lambda n: ungettext('hour', 'hours', n)), - (60, lambda n: ungettext('minute', 'minutes', n)), - (1, lambda n: ungettext('second', 'seconds', n)), - (0, lambda n: 'just now'), + (60 * 60 * 24, lambda n: ungettext("day", "days", n)), + (60 * 60, lambda n: ungettext("hour", "hours", n)), + (60, lambda n: ungettext("minute", "minutes", n)), + (1, lambda n: ungettext("second", "seconds", n)), + (0, lambda n: "just now"), ) return _do_timesince(value, chunks) - + + def relative_timeuntil(value): if not value: - return '' + return "" chunks = ( - (60 * 60, lambda n: ungettext('hour', 'hours', n)), - (60, lambda n: ungettext('minute', 'minutes', n)) + (60 * 60, lambda n: ungettext("hour", "hours", n)), + (60, lambda n: ungettext("minute", "minutes", n)), ) - + now = datetime.datetime.utcnow() - + return _do_timesince(now, chunks, value) + def seconds_timesince(value): if not value: return 0 now = datetime.datetime.utcnow() delta = now - value - + return delta.days * 24 * 60 * 60 + delta.seconds - + + def format_relative_date(date, future=False): if not date or date < datetime.datetime(2010, 1, 1): return "Soon" - + now = datetime.datetime.utcnow() diff = abs(now - date) if diff < datetime.timedelta(minutes=60): minutes = diff.seconds / 60 - return "%s minute%s %s" % (minutes, - '' if minutes == 1 else 's', - '' if future else 'ago') + return "%s minute%s %s" % (minutes, "" if minutes == 1 else "s", "" if future else "ago") elif datetime.timedelta(minutes=60) <= diff < datetime.timedelta(minutes=90): - return "1 hour %s" % ('' if future else 'ago') + return "1 hour %s" % ("" if future else "ago") elif diff < datetime.timedelta(hours=24): dec = (diff.seconds / 60 + 15) % 60 if dec >= 30: - return "%s.5 hours %s" % ((((diff.seconds / 60) + 15) / 60), - '' if future else 'ago') + return "%s.5 hours %s" % ((((diff.seconds / 60) + 15) / 60), "" if future else "ago") else: - return "%s hours %s" % ((((diff.seconds / 60) + 15) / 60), - '' if future else 'ago') + return "%s hours %s" % ((((diff.seconds / 60) + 15) / 60), "" if future else "ago") else: - days = ((diff.seconds / 60) / 60 / 24) - return "%s day%s %s" % (days, '' if days == 1 else 's', '' if future else 'ago') - -def add_object_to_folder(obj, in_folder, folders, parent='', added=False): - if parent.startswith('river:'): - parent = parent.replace('river:', '') - if in_folder.startswith('river:'): - in_folder = in_folder.replace('river:', '') + days = (diff.seconds / 60) / 60 / 24 + return "%s day%s %s" % (days, "" if days == 1 else "s", "" if future else "ago") + + +def add_object_to_folder(obj, in_folder, folders, parent="", added=False): + if parent.startswith("river:"): + parent = parent.replace("river:", "") + if in_folder.startswith("river:"): + in_folder = in_folder.replace("river:", "") obj_identifier = obj if isinstance(obj, dict): obj_identifier = list(obj.keys())[0] - if ((not in_folder or in_folder == " ") and - not parent and - not isinstance(obj, dict) and - obj_identifier not in folders): + if ( + (not in_folder or in_folder == " ") + and not parent + and not isinstance(obj, dict) + and obj_identifier not in folders + ): folders.append(obj) return folders @@ -198,7 +213,7 @@ def add_object_to_folder(obj, in_folder, folders, parent='', added=False): if obj_identifier not in child_folder_names: folders.append(obj) return folders - + for k, v in enumerate(folders): if isinstance(v, dict): for f_k, f_v in list(v.items()): @@ -206,39 +221,43 @@ def add_object_to_folder(obj, in_folder, folders, parent='', added=False): f_v.append(obj) added = True folders[k][f_k] = add_object_to_folder(obj, in_folder, f_v, f_k, added) - - return folders + + return folders + def mail_feed_error_to_admin(feed, e, local_vars=None, subject=None): # Mail the admins with the error if not subject: subject = "Feed update error" exc_info = sys.exc_info() - subject = '%s: %s' % (subject, repr(e)) - message = 'Traceback:\n%s\n\Feed:\n%s\nLocals:\n%s' % ( - '\n'.join(traceback.format_exception(*exc_info)), + subject = "%s: %s" % (subject, repr(e)) + message = "Traceback:\n%s\n\Feed:\n%s\nLocals:\n%s" % ( + "\n".join(traceback.format_exception(*exc_info)), pprint.pformat(feed.__dict__), - pprint.pformat(local_vars) - ) + pprint.pformat(local_vars), + ) logging.debug(f" ***> Feed error, {subject}: {message}") - -## {{{ http://code.activestate.com/recipes/576611/ (r11) -from operator import itemgetter + + from heapq import nlargest from itertools import repeat +## {{{ http://code.activestate.com/recipes/576611/ (r11) +from operator import itemgetter + + class Counter(dict): - '''Dict subclass for counting hashable objects. Sometimes called a bag + """Dict subclass for counting hashable objects. Sometimes called a bag or multiset. Elements are stored as dictionary keys and their counts are stored as dictionary values. >>> Counter('zyzygy') Counter({'y': 3, 'z': 2, 'g': 1}) - ''' + """ def __init__(self, iterable=None, **kwds): - '''Create a new, empty Counter object. And if given, count elements + """Create a new, empty Counter object. And if given, count elements from an input iterable. Or, initialize the count from another mapping of elements to their counts. @@ -247,26 +266,26 @@ def __init__(self, iterable=None, **kwds): >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping >>> c = Counter(a=4, b=2) # a new counter from keyword args - ''' + """ self.update(iterable, **kwds) def __missing__(self, key): return 0 def most_common(self, n=None): - '''List the n most common elements and their counts from the most + """List the n most common elements and their counts from the most common to the least. If n is None, then list all element counts. >>> Counter('abracadabra').most_common(3) [('a', 5), ('r', 2), ('b', 2)] - ''' + """ if n is None: return sorted(iter(list(self.items())), key=itemgetter(1), reverse=True) return nlargest(n, iter(list(self.items())), key=itemgetter(1)) def elements(self): - '''Iterator over elements repeating each as many times as its count. + """Iterator over elements repeating each as many times as its count. >>> c = Counter('ABCABC') >>> sorted(c.elements()) @@ -275,7 +294,7 @@ def elements(self): If an element's count has been set to zero or is a negative number, elements() will ignore it. - ''' + """ for elem, count in list(self.items()): for _ in repeat(None, count): yield elem @@ -284,11 +303,10 @@ def elements(self): @classmethod def fromkeys(cls, iterable, v=None): - raise NotImplementedError( - 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') + raise NotImplementedError("Counter.fromkeys() is undefined. Use Counter(iterable) instead.") def update(self, iterable=None, **kwds): - '''Like dict.update() but add counts instead of replacing them. + """Like dict.update() but add counts instead of replacing them. Source can be an iterable, a dictionary, or another Counter instance. @@ -299,15 +317,15 @@ def update(self, iterable=None, **kwds): >>> c['h'] # four 'h' in which, witch, and watch 4 - ''' + """ if iterable is not None: - if hasattr(iterable, 'iteritems'): + if hasattr(iterable, "iteritems"): if self: self_get = self.get for elem, count in list(iterable.items()): self[elem] = self_get(elem, 0) + count else: - dict.update(self, iterable) # fast path when counter is empty + dict.update(self, iterable) # fast path when counter is empty else: self_get = self.get for elem in iterable: @@ -316,19 +334,19 @@ def update(self, iterable=None, **kwds): self.update(kwds) def copy(self): - 'Like dict.copy() but returns a Counter instance instead of a dict.' + "Like dict.copy() but returns a Counter instance instead of a dict." return Counter(self) def __delitem__(self, elem): - 'Like dict.__delitem__() but does not raise KeyError for missing values.' + "Like dict.__delitem__() but does not raise KeyError for missing values." if elem in self: dict.__delitem__(self, elem) def __repr__(self): if not self: - return '%s()' % self.__class__.__name__ - items = ', '.join(map('%r: %r'.__mod__, self.most_common())) - return '%s({%s})' % (self.__class__.__name__, items) + return "%s()" % self.__class__.__name__ + items = ", ".join(map("%r: %r".__mod__, self.most_common())) + return "%s({%s})" % (self.__class__.__name__, items) # Multiset-style mathematical operations discussed in: # Knuth TAOCP Volume II section 4.6.3 exercise 19 @@ -340,13 +358,13 @@ def __repr__(self): # c += Counter() def __add__(self, other): - '''Add counts from two counters. + """Add counts from two counters. >>> Counter('abbb') + Counter('bcc') Counter({'b': 4, 'c': 2, 'a': 1}) - ''' + """ if not isinstance(other, Counter): return NotImplemented result = Counter() @@ -357,12 +375,12 @@ def __add__(self, other): return result def __sub__(self, other): - ''' Subtract count, but keep only results with positive counts. + """Subtract count, but keep only results with positive counts. >>> Counter('abbbc') - Counter('bccd') Counter({'b': 2, 'a': 1}) - ''' + """ if not isinstance(other, Counter): return NotImplemented result = Counter() @@ -373,12 +391,12 @@ def __sub__(self, other): return result def __or__(self, other): - '''Union is the maximum of value in either of the input counters. + """Union is the maximum of value in either of the input counters. >>> Counter('abbb') | Counter('bcc') Counter({'b': 3, 'c': 2, 'a': 1}) - ''' + """ if not isinstance(other, Counter): return NotImplemented _max = max @@ -390,12 +408,12 @@ def __or__(self, other): return result def __and__(self, other): - ''' Intersection is the minimum of corresponding counts. + """Intersection is the minimum of corresponding counts. >>> Counter('abbb') & Counter('bcc') Counter({'b': 1}) - ''' + """ if not isinstance(other, Counter): return NotImplemented _min = min @@ -409,11 +427,19 @@ def __and__(self, other): return result -if __name__ == '__main__': +if __name__ == "__main__": import doctest + print((doctest.testmod())) ## end of http://code.activestate.com/recipes/576611/ }}} + def chunks(l, n): for i in range(0, len(l), n): - yield l[i:i+n] + yield l[i : i + n] + + +def strip_underscore_from_feed_address(feed_address): + # Strip _=#### from feed_address + parsed_url = qurl(feed_address, remove="_") + return parsed_url diff --git a/utils/feedfinder_forman.py b/utils/feedfinder_forman.py index d543b2ce65..9ee7b34ee9 100755 --- a/utils/feedfinder_forman.py +++ b/utils/feedfinder_forman.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- - __version__ = "0.0.3" try: @@ -14,6 +13,7 @@ __all__ = ["find_feeds"] import logging + import requests from bs4 import BeautifulSoup from six.moves.urllib import parse as urlparse @@ -30,7 +30,6 @@ def coerce_url(url): class FeedFinder(object): - def __init__(self, user_agent=None): if user_agent is None: user_agent = "NewsBlur Feed Finder" @@ -38,7 +37,9 @@ def __init__(self, user_agent=None): def get_feed(self, url, skip_user_agent=False): try: - r = requests.get(url, headers={"User-Agent": self.user_agent if not skip_user_agent else None}, timeout=15) + r = requests.get( + url, headers={"User-Agent": self.user_agent if not skip_user_agent else None}, timeout=15 + ) except Exception as e: logging.warn("Error while getting '{0}'".format(url)) logging.warn("{0}".format(e)) @@ -51,7 +52,7 @@ def is_feed_data(self, text): data = text.lower() if data and data[:100].count("').replace(''', "'").replace('"', '"').replace('&', '&') + v = ( + v.replace("<", "<") + .replace(">", ">") + .replace("'", "'") + .replace(""", '"') + .replace("&", "&") + ) return v + attrs = [(k.lower(), cleanattr(v)) for k, v in attrs if cleanattr(v)] - attrs = [(k, k in ('rel','type') and v.lower() or v) for k, v in attrs if cleanattr(v)] + attrs = [(k, k in ("rel", "type") and v.lower() or v) for k, v in attrs if cleanattr(v)] return attrs - + def do_base(self, attrs): attrsD = dict(self.normalize_attrs(attrs)) - if 'href' not in attrsD: return - self.baseuri = attrsD['href'] - - def error(self, *a, **kw): pass # we're not picky - + if "href" not in attrsD: + return + self.baseuri = attrsD["href"] + + def error(self, *a, **kw): + pass # we're not picky + + class LinkParser(BaseParser): - FEED_TYPES = ('application/rss+xml', - 'text/xml', - 'application/atom+xml', - 'application/x.atom+xml', - 'application/x-atom+xml') + FEED_TYPES = ( + "application/rss+xml", + "text/xml", + "application/atom+xml", + "application/x.atom+xml", + "application/x-atom+xml", + ) + def do_link(self, attrs): attrsD = dict(self.normalize_attrs(attrs)) - if 'rel' not in attrsD: return - rels = attrsD['rel'].split() - if 'alternate' not in rels: return - if attrsD.get('type') not in self.FEED_TYPES: return - if 'href' not in attrsD: return - self.links.append(urllib.parse.urljoin(self.baseuri, attrsD['href'])) + if "rel" not in attrsD: + return + rels = attrsD["rel"].split() + if "alternate" not in rels: + return + if attrsD.get("type") not in self.FEED_TYPES: + return + if "href" not in attrsD: + return + self.links.append(urllib.parse.urljoin(self.baseuri, attrsD["href"])) + class ALinkParser(BaseParser): def start_a(self, attrs): attrsD = dict(self.normalize_attrs(attrs)) - if 'href' not in attrsD: return - self.links.append(urllib.parse.urljoin(self.baseuri, attrsD['href'])) + if "href" not in attrsD: + return + self.links.append(urllib.parse.urljoin(self.baseuri, attrsD["href"])) + def makeFullURI(uri): - if not uri: return + if not uri: + return uri = uri.strip() - if uri.startswith('feed://'): - uri = 'http://' + uri.split('feed://', 1).pop() - for x in ['http', 'https']: - if uri.startswith('%s://' % x): + if uri.startswith("feed://"): + uri = "http://" + uri.split("feed://", 1).pop() + for x in ["http", "https"]: + if uri.startswith("%s://" % x): return uri - return 'http://%s' % uri + return "http://%s" % uri + def getLinks(data, baseuri): p = LinkParser(baseuri) p.feed(data) return p.links + def getLinksLXML(data, baseuri): parser = etree.HTMLParser(recover=True) tree = etree.parse(StringIO(data), parser) links = [] - for link in tree.findall('.//link'): - if link.attrib.get('type') in LinkParser.FEED_TYPES: - href = link.attrib['href'] - if href: links.append(href) + for link in tree.findall(".//link"): + if link.attrib.get("type") in LinkParser.FEED_TYPES: + href = link.attrib["href"] + if href: + links.append(href) return links + def getALinks(data, baseuri): p = ALinkParser(baseuri) p.feed(data) return p.links + def getLocalLinks(links, baseuri): found_links = [] - if not baseuri: return found_links + if not baseuri: + return found_links baseuri = baseuri.lower() for l in links: try: @@ -198,28 +240,38 @@ def getLocalLinks(links, baseuri): pass return found_links + def isFeedLink(link): - return link[-4:].lower() in ('.rss', '.rdf', '.xml', '.atom') + return link[-4:].lower() in (".rss", ".rdf", ".xml", ".atom") + def isXMLRelatedLink(link): link = link.lower() - return link.count('rss') + link.count('rdf') + link.count('xml') + link.count('atom') + return link.count("rss") + link.count("rdf") + link.count("xml") + link.count("atom") + + +r_brokenRedirect = re.compile("]*>(.*?)", re.S) + -r_brokenRedirect = re.compile(']*>(.*?)', re.S) def tryBrokenRedirect(data): - if ' b) - (a < b) + return (a > b) - (a < b) + def sortFeeds(feed1Info, feed2Info): - return cmp_(feed2Info['headlines_rank'], feed1Info['headlines_rank']) + return cmp_(feed2Info["headlines_rank"], feed1Info["headlines_rank"]) + def getFeedsFromSyndic8(uri): feeds = [] try: - server = xmlrpc.client.Server('http://www.syndic8.com/xmlrpc.php') + server = xmlrpc.client.Server("http://www.syndic8.com/xmlrpc.php") feedids = server.syndic8.FindFeeds(uri) - infolist = server.syndic8.GetFeedInfo(feedids, ['headlines_rank','status','dataurl']) + infolist = server.syndic8.GetFeedInfo(feedids, ["headlines_rank", "status", "dataurl"]) infolist.sort(sortFeeds) - feeds = [f['dataurl'] for f in infolist if f['status']=='Syndicated'] - _debuglog('found %s feeds through Syndic8' % len(feeds)) + feeds = [f["dataurl"] for f in infolist if f["status"] == "Syndicated"] + _debuglog("found %s feeds through Syndic8" % len(feeds)) except: pass return feeds - + + def feeds(uri, all=False, querySyndic8=False, _recurs=None): - if _recurs is None: _recurs = [uri] + if _recurs is None: + _recurs = [uri] fulluri = makeFullURI(uri) try: data = _gatekeeper.get(fulluri, check=False) @@ -261,27 +318,27 @@ def feeds(uri, all=False, querySyndic8=False, _recurs=None): _recurs.append(newuri) return feeds(newuri, all=all, querySyndic8=querySyndic8, _recurs=_recurs) # nope, it's a page, try LINK tags first - _debuglog('looking for LINK tags') + _debuglog("looking for LINK tags") try: outfeeds = getLinks(data, fulluri) except: outfeeds = [] if not outfeeds: - _debuglog('using lxml to look for LINK tags') + _debuglog("using lxml to look for LINK tags") try: outfeeds = getLinksLXML(data, fulluri) except: outfeeds = [] - _debuglog('found %s feeds through LINK tags' % len(outfeeds)) + _debuglog("found %s feeds through LINK tags" % len(outfeeds)) outfeeds = list(filter(isFeed, outfeeds)) if all or not outfeeds: # no LINK tags, look for regular links that point to feeds - _debuglog('no LINK tags, looking at A tags') + _debuglog("no LINK tags, looking at A tags") try: links = getALinks(data, fulluri) except: links = [] - _debuglog('no LINK tags, looking at local links') + _debuglog("no LINK tags, looking at local links") locallinks = getLocalLinks(links, fulluri) # look for obvious feed links on the same server outfeeds.extend(list(filter(isFeed, list(filter(isFeedLink, locallinks))))) @@ -295,82 +352,89 @@ def feeds(uri, all=False, querySyndic8=False, _recurs=None): # look harder for feed links on another server outfeeds.extend(list(filter(isFeed, list(filter(isXMLRelatedLink, links))))) if all or not outfeeds: - _debuglog('no A tags, guessing') - suffixes = [ # filenames used by popular software: - 'feed/', # obvious - 'atom.xml', # blogger, TypePad - 'index.atom', # MT, apparently - 'index.rdf', # MT - 'rss.xml', # Dave Winer/Manila - 'index.xml', # MT - 'index.rss' # Slash + _debuglog("no A tags, guessing") + suffixes = [ # filenames used by popular software: + "feed/", # obvious + "atom.xml", # blogger, TypePad + "index.atom", # MT, apparently + "index.rdf", # MT + "rss.xml", # Dave Winer/Manila + "index.xml", # MT + "index.rss", # Slash ] outfeeds.extend(list(filter(isFeed, [urllib.parse.urljoin(fulluri, x) for x in suffixes]))) if (all or not outfeeds) and querySyndic8: # still no luck, search Syndic8 for feeds (requires xmlrpclib) - _debuglog('still no luck, searching Syndic8') + _debuglog("still no luck, searching Syndic8") outfeeds.extend(getFeedsFromSyndic8(uri)) - if hasattr(__builtins__, 'set') or 'set' in __builtins__: + if hasattr(__builtins__, "set") or "set" in __builtins__: outfeeds = list(set(outfeeds)) return outfeeds -getFeeds = feeds # backwards-compatibility + +getFeeds = feeds # backwards-compatibility + def feed(uri): - #todo: give preference to certain feed formats + # todo: give preference to certain feed formats feedlist = feeds(uri) if feedlist: - feeds_no_comments = [f for f in feedlist if 'comments' not in f.lower()] + feeds_no_comments = [f for f in feedlist if "comments" not in f.lower()] if feeds_no_comments: return feeds_no_comments[0] return feedlist[0] else: return None + ##### test harness ###### + def test(): - uri = 'http://diveintomark.org/tests/client/autodiscovery/html4-001.html' + uri = "http://diveintomark.org/tests/client/autodiscovery/html4-001.html" failed = [] count = 0 while 1: data = _gatekeeper.get(uri) - if data.find('Atom autodiscovery test') == -1: break - sys.stdout.write('.') + if data.find("Atom autodiscovery test") == -1: + break + sys.stdout.write(".") sys.stdout.flush() count += 1 links = getLinks(data, uri) if not links: - print(('\n*** FAILED ***', uri, 'could not find link')) + print(("\n*** FAILED ***", uri, "could not find link")) failed.append(uri) elif len(links) > 1: - print(('\n*** FAILED ***', uri, 'found too many links')) + print(("\n*** FAILED ***", uri, "found too many links")) failed.append(uri) else: atomdata = urllib.request.urlopen(links[0]).read() if atomdata.find(' 2 else "1" droplet_index = int(second_arg) if str(second_arg).isnumeric() else 1 droplet_name = sys.argv[1] # Use correct Digital Ocean team based on "old" - commands = ['ansible-inventory', '--list'] + commands = ["ansible-inventory", "--list"] env = None if second_arg == "old": env = dict(os.environ, ANSIBLE_CONFIG="ansible.old.cfg") @@ -26,7 +28,7 @@ print(" ***> Could not load ansible-inventory!") hosts = json.loads(hosts) - for host, ip_host in hosts['_meta']['hostvars'].items(): + for host, ip_host in hosts["_meta"]["hostvars"].items(): if host.startswith(droplet_name): - print(ip_host['ansible_host']) + print(ip_host["ansible_host"]) break diff --git a/utils/image_functions.py b/utils/image_functions.py index 0b1f5a4f27..91d222518f 100644 --- a/utils/image_functions.py +++ b/utils/image_functions.py @@ -1,67 +1,65 @@ """Operations for images through the PIL.""" import urllib.request -from PIL import Image -from PIL import ImageFile +from io import BytesIO + +from PIL import Image, ImageFile from PIL import ImageOps as PILOps from PIL.ExifTags import TAGS -from io import BytesIO -PROFILE_PICTURE_SIZES = { - 'fullsize': (256, 256), - 'thumbnail': (64, 64) -} +PROFILE_PICTURE_SIZES = {"fullsize": (256, 256), "thumbnail": (64, 64)} + class ImageOps: - """Module that holds all image operations. Since there's no state, + """Module that holds all image operations. Since there's no state, everything is a classmethod.""" - + @classmethod def resize_image(cls, image_body, size, fit_to_size=False): """Takes a raw image (in image_body) and resizes it to fit given - dimensions. Returns a file-like object in the form of a StringIO. - This must happen in this function because PIL is transforming the + dimensions. Returns a file-like object in the form of a StringIO. + This must happen in this function because PIL is transforming the original as it works.""" - + image_file = BytesIO(image_body) try: image = Image.open(image_file) except IOError: # Invalid image file return False - + # Get the image format early, as we lose it after perform a `thumbnail` or `fit`. format = image.format - + # Check for rotation image = cls.adjust_image_orientation(image) - + if not fit_to_size: image.thumbnail(PROFILE_PICTURE_SIZES[size], Image.ANTIALIAS) else: - image = PILOps.fit(image, PROFILE_PICTURE_SIZES[size], - method=Image.ANTIALIAS, - centering=(0.5, 0.5)) - + image = PILOps.fit( + image, PROFILE_PICTURE_SIZES[size], method=Image.ANTIALIAS, centering=(0.5, 0.5) + ) + output = BytesIO() - if format.lower() == 'jpg': - format = 'jpeg' + if format.lower() == "jpg": + format = "jpeg" image.save(output, format=format, quality=95) - + return output - + @classmethod def adjust_image_orientation(cls, image): """Since the iPhone will store an image on its side but with EXIF data stating that it should be rotated, we need to find that EXIF data and correctly rotate the image before storage.""" - - if hasattr(image, '_getexif'): + + if hasattr(image, "_getexif"): exif = image._getexif() if exif: for tag, value in list(exif.items()): decoded = TAGS.get(tag, tag) - if decoded == 'Orientation': + if decoded == "Orientation": if value == 6: image = image.rotate(-90) if value == 8: @@ -70,14 +68,15 @@ def adjust_image_orientation(cls, image): image = image.rotate(180) break return image - + @classmethod def image_size(cls, url, headers=None): - if not headers: headers = {} + if not headers: + headers = {} req = urllib.request.Request(url, data=None, headers=headers) file = urllib.request.urlopen(req) size = file.headers.get("content-length") - if size: + if size: size = int(size) p = ImageFile.Parser() while True: diff --git a/utils/jennyholzer.py b/utils/jennyholzer.py index f03f8160dc..ce845a6cf2 100644 --- a/utils/jennyholzer.py +++ b/utils/jennyholzer.py @@ -3,7 +3,7 @@ # it is not because they are wrong, just that they may be considered # controversial. I'd rather err on the side of safety, which is contrary # to the trusim: "playing it safe can cause a lot of damage in the long run". -# +# # We'll see where this goes. This is an experiment. - Sam, July 6th, 2012 @@ -261,4 +261,4 @@ "you should study as much as possible", # "your actions are pointless if no one notices", # "your oldest fears are the worst ones", -] \ No newline at end of file +] diff --git a/utils/json_fetcher.py b/utils/json_fetcher.py index 08a1befdba..09c6c0c555 100644 --- a/utils/json_fetcher.py +++ b/utils/json_fetcher.py @@ -1,61 +1,62 @@ import datetime + import dateutil.parser from django.conf import settings from django.utils import feedgenerator + from utils import log as logging from utils.json_functions import decode + class JSONFetcher: - def __init__(self, feed, options=None): self.feed = feed self.options = options or {} - + def fetch(self, address, raw_feed): if not address: address = self.feed.feed_address - + json_feed = decode(raw_feed.content) if not json_feed: - logging.debug(' ***> [%-30s] ~FRJSON fetch failed: %s' % - (self.feed.log_title[:30], address)) + logging.debug(" ***> [%-30s] ~FRJSON fetch failed: %s" % (self.feed.log_title[:30], address)) return data = {} - data['title'] = json_feed.get('title', '[Untitled]') - data['link'] = json_feed.get('home_page_url', "") - data['description'] = json_feed.get('title', "") - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur JSON Feed - %s' % settings.NEWSBLUR_URL - data['docs'] = None - data['feed_url'] = json_feed.get('feed_url') - + data["title"] = json_feed.get("title", "[Untitled]") + data["link"] = json_feed.get("home_page_url", "") + data["description"] = json_feed.get("title", "") + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur JSON Feed - %s" % settings.NEWSBLUR_URL + data["docs"] = None + data["feed_url"] = json_feed.get("feed_url") + rss = feedgenerator.Atom1Feed(**data) - for item in json_feed.get('items', []): + for item in json_feed.get("items", []): story_data = self.json_feed_story(item) rss.add_item(**story_data) - - return rss.writeString('utf-8') - + + return rss.writeString("utf-8") + def json_feed_story(self, item): date_published = datetime.datetime.now() - pubdate = item.get('date_published', None) + pubdate = item.get("date_published", None) if pubdate: date_published = dateutil.parser.parse(pubdate) - authors = item.get('authors', item.get('author', {})) + authors = item.get("authors", item.get("author", {})) if isinstance(authors, list): - author_name = ', '.join([author.get('name', "") for author in authors]) + author_name = ", ".join([author.get("name", "") for author in authors]) else: - author_name = authors.get('name', "") + author_name = authors.get("name", "") story = { - 'title': item.get('title', ""), - 'link': item.get('external_url', item.get('url', "")), - 'description': item.get('content_html', item.get('content_text', "")), - 'author_name': author_name, - 'categories': item.get('tags', []), - 'unique_id': str(item.get('id', item.get('url', ""))), - 'pubdate': date_published, + "title": item.get("title", ""), + "link": item.get("external_url", item.get("url", "")), + "description": item.get("content_html", item.get("content_text", "")), + "author_name": author_name, + "categories": item.get("tags", []), + "unique_id": str(item.get("id", item.get("url", ""))), + "pubdate": date_published, } - + return story diff --git a/utils/json_functions.py b/utils/json_functions.py index 1b04ed0abf..feb349bb66 100644 --- a/utils/json_functions.py +++ b/utils/json_functions.py @@ -1,19 +1,22 @@ -#-*- coding: utf-8 -*- -from django.db import models -from django.utils.functional import Promise -from django.utils.encoding import force_text, smart_str +# -*- coding: utf-8 -*- +import datetime import json +import sys from decimal import Decimal -from django.core import serializers + +from bson.objectid import ObjectId from django.conf import settings -from django.http import HttpResponse, HttpResponseForbidden, Http404 +from django.core import serializers +from django.db import models from django.db.models.query import QuerySet +from django.http import Http404, HttpResponse, HttpResponseForbidden +from django.utils.encoding import force_text, smart_str +from django.utils.functional import Promise + # from django.utils.deprecation import CallableBool from mongoengine.queryset.queryset import QuerySet as MongoQuerySet -from bson.objectid import ObjectId + from utils import log as logging -import sys -import datetime def decode(data): @@ -42,7 +45,7 @@ def _any(data): # Opps, we used to check if it is of type list, but that fails # i.e. in the case of django.newforms.utils.ErrorList, which extends # the type "list". Oh man, that was a dumb mistake! - if hasattr(data, 'canonical'): + if hasattr(data, "canonical"): ret = _any(data.canonical()) elif isinstance(data, list): ret = _list(data) @@ -66,7 +69,7 @@ def _any(data): ret = _model(data) # here we need to encode the string as unicode (otherwise we get utf-16 in the json-response) elif isinstance(data, bytes): - ret = data.decode('utf-8', 'ignore') + ret = data.decode("utf-8", "ignore") elif isinstance(data, str): ret = smart_str(data) elif isinstance(data, Exception): @@ -76,7 +79,7 @@ def _any(data): ret = force_text(data) elif isinstance(data, datetime.datetime) or isinstance(data, datetime.date): ret = str(data) - elif hasattr(data, 'to_json'): + elif hasattr(data, "to_json"): ret = data.to_json() else: ret = data @@ -106,7 +109,7 @@ def _dict(data): ret[str(k)] = _any(v) return ret - if hasattr(data, 'to_json'): + if hasattr(data, "to_json"): data = data.to_json() ret = _any(data) return json.dumps(ret) @@ -132,12 +135,12 @@ def json_response(request, response=None): try: if isinstance(response, dict): response = dict(response) - if 'result' not in response: - response['result'] = 'ok' + if "result" not in response: + response["result"] = "ok" authenticated = request.user.is_authenticated - response['authenticated'] = authenticated + response["authenticated"] = authenticated if authenticated: - response['user_id'] = request.user.pk + response["user_id"] = request.user.pk except KeyboardInterrupt: # Allow keyboard interrupts through for debugging. raise @@ -146,28 +149,28 @@ def json_response(request, response=None): except Exception as e: # Mail the admins with the error exc_info = sys.exc_info() - subject = 'JSON view error: %s' % request.path + subject = "JSON view error: %s" % request.path try: request_repr = repr(request) except: - request_repr = 'Request repr() unavailable' + request_repr = "Request repr() unavailable" import traceback - message = 'Traceback:\n%s\n\nRequest:\n%s' % ( - '\n'.join(traceback.format_exception(*exc_info)), + + message = "Traceback:\n%s\n\nRequest:\n%s" % ( + "\n".join(traceback.format_exception(*exc_info)), request_repr, - ) + ) - response = {'result': 'error', - 'text': str(e)} + response = {"result": "error", "text": str(e)} code = 500 if not settings.DEBUG: logging.debug(f" ***> JSON exception {subject}: {message}") - logging.debug('\n'.join(traceback.format_exception(*exc_info))) + logging.debug("\n".join(traceback.format_exception(*exc_info))) else: - print('\n'.join(traceback.format_exception(*exc_info))) + print("\n".join(traceback.format_exception(*exc_info))) json = json_encode(response) - return HttpResponse(json, content_type='application/json; charset=utf-8', status=code) + return HttpResponse(json, content_type="application/json; charset=utf-8", status=code) def main(): @@ -182,5 +185,5 @@ def main(): print(test, json_test) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/utils/log.py b/utils/log.py index 762b1d1acf..987ebd4b39 100644 --- a/utils/log.py +++ b/utils/log.py @@ -3,9 +3,9 @@ import string import time -from django.core.handlers.wsgi import WSGIRequest from django.conf import settings -from django.utils.encoding import smart_str, smart_str +from django.core.handlers.wsgi import WSGIRequest +from django.utils.encoding import smart_str from .user_functions import extract_user_agent @@ -16,7 +16,7 @@ def emit(self, record): def getlogger(): - logger = logging.getLogger('newsblur') + logger = logging.getLogger("newsblur") return logger @@ -25,7 +25,7 @@ def user(u, msg, request=None, warn_color=True): if not u: return debug(msg) - platform = '------' + platform = "------" time_elapsed = "" if isinstance(u, WSGIRequest) or request: if not request: @@ -33,24 +33,24 @@ def user(u, msg, request=None, warn_color=True): u = request.user platform = extract_user_agent(request) - if hasattr(request, 'start_time'): + if hasattr(request, "start_time"): seconds = time.time() - request.start_time - color = '~FB' + color = "~FB" if warn_color: if seconds >= 5: - color = '~FR' + color = "~FR" elif seconds > 1: - color = '~SB~FK' + color = "~SB~FK" time_elapsed = "[%s%.4ss~SB] " % ( color, seconds, ) is_premium = u.is_authenticated and u.profile.is_premium - premium = '*' if is_premium else '' + premium = "*" if is_premium else "" if is_premium and u.profile.is_archive: premium = "^" username = cipher(str(u)) if settings.CIPHER_USERNAMES else str(u) - info(' ---> [~FB~SN%-6s~SB] %s[%s%s] %s' % (platform, time_elapsed, username, premium, msg)) + info(" ---> [~FB~SN%-6s~SB] %s[%s%s] %s" % (platform, time_elapsed, username, premium, msg)) def cipher(msg): @@ -82,91 +82,97 @@ def error(msg): def colorize(msg): params = { - r'\-\-\->' : '~FB~SB--->~FW', - r'\*\*\*>' : '~FB~SB~BB--->~BT~FW', - r'\[' : '~SB~FB[~SN~FM', - r'AnonymousUser' : '~FBAnonymousUser', - r'\*\]' : r'~SN~FR*~FB~SB]', - r'\^\]' : r'~SN~FR^~FB~SB]', - r'\]' : '~FB~SB]~FW~SN', + r"\-\-\->": "~FB~SB--->~FW", + r"\*\*\*>": "~FB~SB~BB--->~BT~FW", + r"\[": "~SB~FB[~SN~FM", + r"AnonymousUser": "~FBAnonymousUser", + r"\*\]": r"~SN~FR*~FB~SB]", + r"\^\]": r"~SN~FR^~FB~SB]", + r"\]": "~FB~SB]~FW~SN", } colors = { - '~SB' : Style.BRIGHT, - '~SN' : Style.NORMAL, - '~SK' : Style.BLINK, - '~SU' : Style.UNDERLINE, - '~ST' : Style.RESET_ALL, - '~FK': Fore.BLACK, - '~FR': Fore.RED, - '~FG': Fore.GREEN, - '~FY': Fore.YELLOW, - '~FB': Fore.BLUE, - '~FM': Fore.MAGENTA, - '~FC': Fore.CYAN, - '~FW': Fore.WHITE, - '~FT': Fore.RESET, - '~BK': Back.BLACK, - '~BR': Back.RED, - '~BG': Back.GREEN, - '~BY': Back.YELLOW, - '~BB': Back.BLUE, - '~BM': Back.MAGENTA, - '~BC': Back.CYAN, - '~BW': Back.WHITE, - '~BT': Back.RESET, + "~SB": Style.BRIGHT, + "~SN": Style.NORMAL, + "~SK": Style.BLINK, + "~SU": Style.UNDERLINE, + "~ST": Style.RESET_ALL, + "~FK": Fore.BLACK, + "~FR": Fore.RED, + "~FG": Fore.GREEN, + "~FY": Fore.YELLOW, + "~FB": Fore.BLUE, + "~FM": Fore.MAGENTA, + "~FC": Fore.CYAN, + "~FW": Fore.WHITE, + "~FT": Fore.RESET, + "~BK": Back.BLACK, + "~BR": Back.RED, + "~BG": Back.GREEN, + "~BY": Back.YELLOW, + "~BB": Back.BLUE, + "~BM": Back.MAGENTA, + "~BC": Back.CYAN, + "~BW": Back.WHITE, + "~BT": Back.RESET, } for k, v in list(params.items()): msg = re.sub(k, v, msg) - msg = msg + '~ST~FW~BT' + msg = msg + "~ST~FW~BT" # msg = re.sub(r'(~[A-Z]{2})', r'%(\1)s', msg) for k, v in list(colors.items()): msg = msg.replace(k, v) return msg - -''' + + +""" This module generates ANSI character codes to printing colors to terminals. See: http://en.wikipedia.org/wiki/ANSI_escape_code -''' +""" + +COLOR_ESC = "\033[" -COLOR_ESC = '\033[' class AnsiCodes(object): def __init__(self, codes): for name in dir(codes): - if not name.startswith('_'): + if not name.startswith("_"): value = getattr(codes, name) - setattr(self, name, COLOR_ESC + str(value) + 'm') + setattr(self, name, COLOR_ESC + str(value) + "m") + class AnsiFore: - BLACK = 30 - RED = 31 - GREEN = 32 - YELLOW = 33 - BLUE = 34 + BLACK = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 MAGENTA = 35 - CYAN = 36 - WHITE = 37 - RESET = 39 + CYAN = 36 + WHITE = 37 + RESET = 39 + class AnsiBack: - BLACK = 40 - RED = 41 - GREEN = 42 - YELLOW = 43 - BLUE = 44 + BLACK = 40 + RED = 41 + GREEN = 42 + YELLOW = 43 + BLUE = 44 MAGENTA = 45 - CYAN = 46 - WHITE = 47 - RESET = 49 + CYAN = 46 + WHITE = 47 + RESET = 49 + class AnsiStyle: - BRIGHT = 1 - DIM = 2 + BRIGHT = 1 + DIM = 2 UNDERLINE = 4 - BLINK = 5 - NORMAL = 22 + BLINK = 5 + NORMAL = 22 RESET_ALL = 0 + Fore = AnsiCodes(AnsiFore) Back = AnsiCodes(AnsiBack) Style = AnsiCodes(AnsiStyle) diff --git a/utils/management_functions.py b/utils/management_functions.py index 914f04e63d..dceec56ef9 100644 --- a/utils/management_functions.py +++ b/utils/management_functions.py @@ -1,5 +1,6 @@ -import os import errno +import os + def daemonize(): """ @@ -7,11 +8,11 @@ def daemonize(): """ # swiped from twisted/scripts/twistd.py # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16 - if os.fork(): # launch child and... - os._exit(0) # kill off parent + if os.fork(): # launch child and... + os._exit(0) # kill off parent os.setsid() - if os.fork(): # launch child and... - os._exit(0) # kill off parent again. + if os.fork(): # launch child and... + os._exit(0) # kill off parent again. os.umask(0o77) null = os.open("/dev/null", os.O_RDWR) for i in range(3): @@ -20,4 +21,4 @@ def daemonize(): except OSError as e: if e.errno != errno.EBADF: raise - os.close(null) \ No newline at end of file + os.close(null) diff --git a/utils/mongo_command_monitor.py b/utils/mongo_command_monitor.py index 84b677a77a..2ff21cd917 100644 --- a/utils/mongo_command_monitor.py +++ b/utils/mongo_command_monitor.py @@ -1,10 +1,11 @@ -from pymongo import monitoring import logging + from django.conf import settings from django.db import connection +from pymongo import monitoring -class MongoCommandLogger(monitoring.CommandListener): +class MongoCommandLogger(monitoring.CommandListener): def __init__(self): self.seen_request_ids = dict() @@ -24,13 +25,13 @@ def succeeded(self, event): op = event.command_name collection = command_dict[op] - command_filter = command_dict.get('filter', None) - command_documents = command_dict.get('documents', None) - command_indexes = command_dict.get('indexes', None) - command_insert = command_dict.get('updates', None) - command_update = command_dict.get('updates', None) - command_sort = command_dict.get('sort', None) - command_get_more = command_dict.get('getMore', None) + command_filter = command_dict.get("filter", None) + command_documents = command_dict.get("documents", None) + command_indexes = command_dict.get("indexes", None) + command_insert = command_dict.get("updates", None) + command_update = command_dict.get("updates", None) + command_sort = command_dict.get("sort", None) + command_get_more = command_dict.get("getMore", None) if command_sort: command_sort = dict(command_sort) @@ -55,19 +56,17 @@ def succeeded(self, event): if op == "insert" or op == "update": op = f"~SB{op}" - - message = { - "op": op, - "query": query, - "collection": collection - } - - if not getattr(connection, 'queriesx', False): + + message = {"op": op, "query": query, "collection": collection} + + if not getattr(connection, "queriesx", False): connection.queriesx = [] - connection.queriesx.append({ - 'mongo': message, - 'time': '%.6f' % (int(event.duration_micros) / 1000000), - }) + connection.queriesx.append( + { + "mongo": message, + "time": "%.6f" % (int(event.duration_micros) / 1000000), + } + ) # logging.info("Command {0.command_name} with request id " # "{0.request_id} on server {0.connection_id} " @@ -75,18 +74,21 @@ def succeeded(self, event): # "microseconds".format(event)) def failed(self, event): - logging.info("Command {0.command_name} with request id " - "{0.request_id} on server {0.connection_id} " - "failed in {0.duration_micros} " - "microseconds".format(event)) + logging.info( + "Command {0.command_name} with request id " + "{0.request_id} on server {0.connection_id} " + "failed in {0.duration_micros} " + "microseconds".format(event) + ) 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_celery(self, profiler): - if not self.activated(profiler): return + if not self.activated(profiler): + return connection.queriesx = [] diff --git a/utils/mongo_raw_log_middleware.py b/utils/mongo_raw_log_middleware.py index 780e5b39bd..e8c5536ed1 100644 --- a/utils/mongo_raw_log_middleware.py +++ b/utils/mongo_raw_log_middleware.py @@ -1,50 +1,58 @@ -from django.core.exceptions import MiddlewareNotUsed +import struct +from time import time + +import bson +import pymongo +from bson.errors import InvalidBSON from django.conf import settings +from django.core.exceptions import MiddlewareNotUsed from django.db import connection from pymongo.mongo_client import MongoClient from pymongo.mongo_replica_set_client import MongoReplicaSetClient -from time import time + from utils import log as logging -import struct -import bson -import pymongo -from bson.errors import InvalidBSON -class MongoDumpMiddleware(object): +class MongoDumpMiddleware(object): 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_view(self, request, callback, callback_args, callback_kwargs): - if not self.activated(request): return + if not self.activated(request): + return self._used_msg_ids = [] - if not getattr(MongoClient, '_logging', False): + if not getattr(MongoClient, "_logging", False): # save old methods - setattr(MongoClient, '_logging', True) - if hasattr(MongoClient, '_send_message_with_response'): + setattr(MongoClient, "_logging", True) + if hasattr(MongoClient, "_send_message_with_response"): connection.queriesx = [] - MongoClient._send_message_with_response = \ - self._instrument(MongoClient._send_message_with_response) - MongoReplicaSetClient._send_message_with_response = \ - self._instrument(MongoReplicaSetClient._send_message_with_response) + MongoClient._send_message_with_response = self._instrument( + MongoClient._send_message_with_response + ) + MongoReplicaSetClient._send_message_with_response = self._instrument( + MongoReplicaSetClient._send_message_with_response + ) return None def process_celery(self, profiler): - if not self.activated(profiler): return + if not self.activated(profiler): + return self._used_msg_ids = [] - if not getattr(MongoClient, '_logging', False): + if not getattr(MongoClient, "_logging", False): # save old methods - setattr(MongoClient, '_logging', True) - if hasattr(MongoClient, '_send_message_with_response'): - MongoClient._send_message_with_response = \ - self._instrument(MongoClient._send_message_with_response) - MongoReplicaSetClient._send_message_with_response = \ - self._instrument(MongoReplicaSetClient._send_message_with_response) + setattr(MongoClient, "_logging", True) + if hasattr(MongoClient, "_send_message_with_response"): + MongoClient._send_message_with_response = self._instrument( + MongoClient._send_message_with_response + ) + MongoReplicaSetClient._send_message_with_response = self._instrument( + MongoReplicaSetClient._send_message_with_response + ) return None def process_response(self, request, response): @@ -56,20 +64,23 @@ def instrumented_method(*args, **kwargs): query = args[1].get_message(False, sock_info, False) message = _mongodb_decode_wire_protocol(query[1]) # message = _mongodb_decode_wire_protocol(args[1][1]) - if not message or message['msg_id'] in self._used_msg_ids: + if not message or message["msg_id"] in self._used_msg_ids: return original_method(*args, **kwargs) - self._used_msg_ids.append(message['msg_id']) + self._used_msg_ids.append(message["msg_id"]) start = time() result = original_method(*args, **kwargs) stop = time() duration = stop - start - if not getattr(connection, 'queriesx', False): + if not getattr(connection, "queriesx", False): connection.queriesx = [] - connection.queriesx.append({ - 'mongo': message, - 'time': '%.6f' % duration, - }) + connection.queriesx.append( + { + "mongo": message, + "time": "%.6f" % duration, + } + ) return result + return instrumented_method def __call__(self, request): @@ -78,34 +89,40 @@ def __call__(self, request): return response + def _mongodb_decode_wire_protocol(message): - """ http://www.mongodb.org/display/DOCS/Mongo+Wire+Protocol """ + """http://www.mongodb.org/display/DOCS/Mongo+Wire+Protocol""" MONGO_OPS = { - 1000: 'msg', - 2001: 'update', - 2002: 'insert', - 2003: 'reserved', - 2004: 'query', - 2005: 'get_more', - 2006: 'delete', - 2007: 'kill_cursors', + 1000: "msg", + 2001: "update", + 2002: "insert", + 2003: "reserved", + 2004: "query", + 2005: "get_more", + 2006: "delete", + 2007: "kill_cursors", } - _, msg_id, _, opcode, _ = struct.unpack(' 90: requests.post( - "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, - auth=("api", settings.MAILGUN_ACCESS_KEY), - data={"from": "NewsBlur Disk Monitor: %s " % (hostname, hostname), - "to": [admin_email], - "subject": "%s hit %s%% disk usage!" % (hostname, percent), - "text": "Usage on %s: %s" % (hostname, disk_usage_output)}) + "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, + auth=("api", settings.MAILGUN_ACCESS_KEY), + data={ + "from": "NewsBlur Disk Monitor: %s " % (hostname, hostname), + "to": [admin_email], + "subject": "%s hit %s%% disk usage!" % (hostname, percent), + "text": "Usage on %s: %s" % (hostname, disk_usage_output), + }, + ) print(" ---> Disk usage is NOT fine: %s / %s%% used" % (hostname, percent)) else: print(" ---> Disk usage is fine: %s / %s%% used" % (hostname, percent)) - -if __name__ == '__main__': + + +if __name__ == "__main__": main() diff --git a/utils/monitor_newsletter_delivery.py b/utils/monitor_newsletter_delivery.py index 35f30a72fa..7d6e036c5f 100755 --- a/utils/monitor_newsletter_delivery.py +++ b/utils/monitor_newsletter_delivery.py @@ -1,35 +1,46 @@ #!/usr/local/bin/python3 import sys -sys.path.append('/srv/newsblur') + +sys.path.append("/srv/newsblur") + +import socket import requests + from newsblur_web import settings -import socket + def main(): hostname = socket.gethostname() admin_email = settings.ADMINS[0][1] - r = requests.get("https://api.mailgun.net/v3/newsletters.newsblur.com/stats/total", - auth=("api", settings.MAILGUN_ACCESS_KEY), - params={"event": ["accepted", "delivered", "failed"], - "duration": "2h"}) - stats = r.json()['stats'][0] - delivered = stats['delivered']['total'] - accepted = stats['delivered']['total'] - bounced = stats['failed']['permanent']['total'] + stats['failed']['temporary']['total'] - if bounced / float(delivered) > 0.5: + r = requests.get( + "https://api.mailgun.net/v3/newsletters.newsblur.com/stats/total", + auth=("api", settings.MAILGUN_ACCESS_KEY), + params={"event": ["accepted", "delivered", "failed"], "duration": "2h"}, + ) + stats = r.json()["stats"][0] + delivered = stats["delivered"]["total"] + accepted = stats["delivered"]["total"] + bounced = stats["failed"]["permanent"]["total"] + stats["failed"]["temporary"]["total"] + if bounced / float(delivered) > 0.5 and bounced > 100: requests.post( - "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, - auth=("api", settings.MAILGUN_ACCESS_KEY), - data={"from": "NewsBlur Newsletter Monitor: %s " % (hostname, hostname), - "to": [admin_email], - "subject": "%s newsletters bounced (2h): %s/%s accepted/delivered -> %s bounced" % (hostname, accepted, delivered, bounced), - "text": "Newsletters are not being delivered! %s delivered, %s bounced" % (delivered, bounced)}) + "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, + auth=("api", settings.MAILGUN_ACCESS_KEY), + data={ + "from": "NewsBlur Newsletter Monitor: %s " % (hostname, hostname), + "to": [admin_email], + "subject": "%s newsletters bounced (2h): %s/%s accepted/delivered -> %s bounced" + % (hostname, accepted, delivered, bounced), + "text": "Newsletters are not being delivered! %s delivered, %s bounced" + % (delivered, bounced), + }, + ) print(" ---> %s newsletters bounced: %s > %s > %s" % (hostname, accepted, delivered, bounced)) else: print(" ---> %s newsletters OK: %s > %s > %s" % (hostname, accepted, delivered, bounced)) - -if __name__ == '__main__': + + +if __name__ == "__main__": main() diff --git a/utils/monitor_redis_bgsave.py b/utils/monitor_redis_bgsave.py index c70893f3ad..fda9df2309 100755 --- a/utils/monitor_redis_bgsave.py +++ b/utils/monitor_redis_bgsave.py @@ -1,33 +1,41 @@ #!/usr/local/bin/python3 import sys -sys.path.append('/srv/newsblur') -import os +sys.path.append("/srv/newsblur") + import datetime +import os +import socket + import requests + from newsblur_web import settings -import socket + def main(): redis_log_path = sys.argv[1] - t = os.popen('stat -c%Y /srv/newsblur/docker/volumes/redis/') - timestamp = t.read().split('\n')[0] + t = os.popen("stat -c%Y /srv/newsblur/docker/volumes/redis/") + timestamp = t.read().split("\n")[0] modified = datetime.datetime.fromtimestamp(int(timestamp)) hostname = socket.gethostname() modified_minutes = datetime.datetime.now() - modified log_tail = os.popen(f"tail -n 100 {redis_log_path}").read() if True: - #if modified < ten_min_ago: + # if modified < ten_min_ago: requests.post( - "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, - auth=("api", settings.MAILGUN_ACCESS_KEY), - data={"from": "NewsBlur Redis Monitor: %s " % (hostname, hostname), - "to": [settings.ADMINS[0][1]], - "subject": "%s hasn't bgsave'd redis in %s!" % (hostname, modified_minutes), - "text": "Last modified %s: %s ago\n\n----\n\n%s" % (hostname, modified_minutes, log_tail)}) + "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, + auth=("api", settings.MAILGUN_ACCESS_KEY), + data={ + "from": "NewsBlur Redis Monitor: %s " % (hostname, hostname), + "to": [settings.ADMINS[0][1]], + "subject": "%s hasn't bgsave'd redis in %s!" % (hostname, modified_minutes), + "text": "Last modified %s: %s ago\n\n----\n\n%s" % (hostname, modified_minutes, log_tail), + }, + ) else: print(" ---> Redis bgsave fine: %s / %s ago" % (hostname, modified_minutes)) - -if __name__ == '__main__': + + +if __name__ == "__main__": main() diff --git a/utils/monitor_task_fetches.py b/utils/monitor_task_fetches.py index 7e0447fd8c..4e4ae98c07 100755 --- a/utils/monitor_task_fetches.py +++ b/utils/monitor_task_fetches.py @@ -1,13 +1,17 @@ #!/usr/local/bin/python3 import sys -sys.path.append('/srv/newsblur') -import requests -from newsblur_web import settings +sys.path.append("/srv/newsblur") + import socket -import redis + import pymongo +import redis +import requests + +from newsblur_web import settings + def main(): hostname = socket.gethostname() @@ -20,33 +24,41 @@ def main(): r = redis.Redis(connection_pool=settings.REDIS_ANALYTICS_POOL) try: - client = pymongo.MongoClient(f"mongodb://{settings.MONGO_DB['username']}:{settings.MONGO_DB['password']}@{settings.MONGO_DB['host']}/?authSource=admin") - feeds_fetched = client.newsblur.statistics.find_one({"key": "feeds_fetched"})['value'] + client = pymongo.MongoClient( + f"mongodb://{settings.MONGO_DB['username']}:{settings.MONGO_DB['password']}@{settings.MONGO_DB['host']}/?authSource=admin" + ) + feeds_fetched = client.newsblur.statistics.find_one({"key": "feeds_fetched"})["value"] redis_task_fetches = int(r.get(monitor_key) or 0) except Exception as e: failed = e - + if feeds_fetched < 5000000 and not failed: if redis_task_fetches > 0 and feeds_fetched < (redis_task_fetches - FETCHES_DROP_AMOUNT): failed = True - # Ignore 0's below, as they simply imply low number, not falling + # Ignore 0's below, as they simply imply low number, not falling # elif redis_task_fetches <= 0: # failed = True if failed: requests.post( - "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, - auth=("api", settings.MAILGUN_ACCESS_KEY), - data={"from": "NewsBlur Task Monitor: %s " % (hostname, hostname), - "to": [admin_email], - "subject": "%s feeds fetched falling: %s (from %s)" % (hostname, feeds_fetched, redis_task_fetches), - "text": "Feed fetches are falling: %s (from %s) %s" % (feeds_fetched, redis_task_fetches, failed)}) + "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, + auth=("api", settings.MAILGUN_ACCESS_KEY), + data={ + "from": "NewsBlur Task Monitor: %s " % (hostname, hostname), + "to": [admin_email], + "subject": "%s feeds fetched falling: %s (from %s)" + % (hostname, feeds_fetched, redis_task_fetches), + "text": "Feed fetches are falling: %s (from %s) %s" + % (feeds_fetched, redis_task_fetches, failed), + }, + ) r.set(monitor_key, feeds_fetched) - r.expire(monitor_key, 60*60*12) # 3 hours + r.expire(monitor_key, 60 * 60 * 12) # 3 hours print(" ---> Feeds fetched falling! %s %s" % (feeds_fetched, failed)) else: print(" ---> Feeds fetched OK: %s" % (feeds_fetched)) - -if __name__ == '__main__': + + +if __name__ == "__main__": main() diff --git a/utils/monitor_work_queue.py b/utils/monitor_work_queue.py index 1c4ba22382..6fc4b4673f 100755 --- a/utils/monitor_work_queue.py +++ b/utils/monitor_work_queue.py @@ -1,13 +1,17 @@ #!/usr/local/bin/python3 import sys -sys.path.append('/srv/newsblur') -import requests -from newsblur_web import settings +sys.path.append("/srv/newsblur") + import socket -import redis + import pymongo +import redis +import requests + +from newsblur_web import settings + def main(): hostname = socket.gethostname() @@ -25,25 +29,30 @@ def main(): redis_work_queue = int(r_monitor.get(monitor_key) or 0) except Exception as e: failed = e - + if work_queue_size > 300 and work_queue_size > (redis_work_queue + QUEUE_DROP_AMOUNT): failed = True if failed: requests.post( - "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, - auth=("api", settings.MAILGUN_ACCESS_KEY), - data={"from": "NewsBlur Queue Monitor: %s " % (hostname, hostname), - "to": [admin_email], - "subject": "%s work queue rising: %s (from %s)" % (hostname, work_queue_size, redis_work_queue), - "text": "Work queue is rising: %s (from %s) %s" % (work_queue_size, redis_work_queue, failed)}) + "https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME, + auth=("api", settings.MAILGUN_ACCESS_KEY), + data={ + "from": "NewsBlur Queue Monitor: %s " % (hostname, hostname), + "to": [admin_email], + "subject": "%s work queue rising: %s (from %s)" + % (hostname, work_queue_size, redis_work_queue), + "text": "Work queue is rising: %s (from %s) %s" % (work_queue_size, redis_work_queue, failed), + }, + ) r_monitor.set(monitor_key, work_queue_size) - r_monitor.expire(monitor_key, 60*60*3) # 3 hours + r_monitor.expire(monitor_key, 60 * 60 * 3) # 3 hours print(" ---> Work queue rising! %s %s" % (work_queue_size, failed)) else: print(" ---> Work queue OK: %s" % (work_queue_size)) - -if __name__ == '__main__': + + +if __name__ == "__main__": main() diff --git a/utils/munin/base.py b/utils/munin/base.py index 203e24b314..1f663c7058 100644 --- a/utils/munin/base.py +++ b/utils/munin/base.py @@ -1,22 +1,21 @@ import sys -class MuninGraph(object): +class MuninGraph(object): def run(self): cmd_name = None if len(sys.argv) > 1: cmd_name = sys.argv[1] - if cmd_name == 'config': + if cmd_name == "config": self.print_config() - else: + else: metrics = self.calculate_metrics() self.print_metrics(metrics) - + def print_config(self): - for key,value in self.graph_config.items(): - print('%s %s' % (key, value)) + for key, value in self.graph_config.items(): + print("%s %s" % (key, value)) def print_metrics(self, metrics): for key, value in metrics.items(): - print('%s.value %s' % (key, value)) - \ No newline at end of file + print("%s.value %s" % (key, value)) diff --git a/utils/munin/newsblur_app_servers.py b/utils/munin/newsblur_app_servers.py index 96be3dd796..04e05a02f9 100755 --- a/utils/munin/newsblur_app_servers.py +++ b/utils/munin/newsblur_app_servers.py @@ -1,71 +1,83 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import datetime import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" from django.conf import settings class NBMuninGraph(MuninGraph): - @property def graph_config(self): graph = { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur App Server Page Loads', - 'graph_vlabel' : '# of page loads / server', - 'graph_args' : '-l 0', - 'total.label' : 'total', - 'total.draw' : 'LINE1', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur App Server Page Loads", + "graph_vlabel": "# of page loads / server", + "graph_args": "-l 0", + "total.label": "total", + "total.draw": "LINE1", } stats = self.stats - graph.update(dict((("%s.label" % s['_id'].replace('-', ''), s['_id']) for s in stats))) - graph.update(dict((("%s.draw" % s['_id'].replace('-', ''), "AREASTACK") for s in stats))) - graph['graph_order'] = ' '.join(sorted(s['_id'].replace('-', '') for s in stats)) + graph.update(dict((("%s.label" % s["_id"].replace("-", ""), s["_id"]) for s in stats))) + graph.update(dict((("%s.draw" % s["_id"].replace("-", ""), "AREASTACK") for s in stats))) + graph["graph_order"] = " ".join(sorted(s["_id"].replace("-", "") for s in stats)) return graph def calculate_metrics(self): - servers = dict((("%s" % s['_id'].replace('-', ''), s['feeds']) for s in self.stats)) - servers['total'] = self.total[0]['feeds'] + servers = dict((("%s" % s["_id"].replace("-", ""), s["feeds"]) for s in self.stats)) + servers["total"] = self.total[0]["feeds"] return servers - + @property def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate([{ - "$match": { - "date": { - "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate( + [ + { + "$match": { + "date": { + "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, + }, + { + "$group": { + "_id": "$server", + "feeds": {"$sum": 1}, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "feeds" : {"$sum": 1}, - }, - }]) - + ] + ) + return list(stats) - + @property def total(self): import datetime + from django.conf import settings - - stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + + stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : 1, - "feeds" : {"$sum": 1}, - }, - }]) - + { + "$group": { + "_id": 1, + "feeds": {"$sum": 1}, + }, + }, + ] + ) + return list(stats) - -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_app_times.py b/utils/munin/newsblur_app_times.py index 228c9963a7..868014e5cd 100755 --- a/utils/munin/newsblur_app_times.py +++ b/utils/munin/newsblur_app_times.py @@ -1,51 +1,57 @@ #!/srv/newsblur/venv/newsblur3/bin/python from utils.munin.base import MuninGraph -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): graph = { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur App Server Times', - 'graph_vlabel' : 'Page load time / server', - 'graph_args' : '-l 0', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur App Server Times", + "graph_vlabel": "Page load time / server", + "graph_args": "-l 0", } stats = self.stats - graph['graph_order'] = ' '.join(sorted(s['_id'] for s in stats)) - graph.update(dict((("%s.label" % s['_id'], s['_id']) for s in stats))) - graph.update(dict((("%s.draw" % s['_id'], 'LINE1') for s in stats))) + graph["graph_order"] = " ".join(sorted(s["_id"] for s in stats)) + graph.update(dict((("%s.label" % s["_id"], s["_id"]) for s in stats))) + graph.update(dict((("%s.draw" % s["_id"], "LINE1") for s in stats))) return graph def calculate_metrics(self): - servers = dict((("%s" % s['_id'], s['page_load']) for s in self.stats)) + servers = dict((("%s" % s["_id"], s["page_load"]) for s in self.stats)) return servers - + @property def stats(self): import datetime import os + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" from django.conf import settings - - stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + + stats = settings.MONGOANALYTICSDB.nbanalytics.page_loads.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, + }, + { + "$group": { + "_id": "$server", + "page_load": {"$avg": "$page_load"}, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "page_load" : {"$avg": "$page_load"}, - }, - }]) - + ] + ) + return list(stats) - -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_classifiers.py b/utils/munin/newsblur_classifiers.py index e06515eb7a..3e28bf8e92 100755 --- a/utils/munin/newsblur_classifiers.py +++ b/utils/munin/newsblur_classifiers.py @@ -1,35 +1,44 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Classifiers', - 'graph_vlabel' : '# of classifiers', - 'graph_args' : '-l 0', - 'feeds.label': 'feeds', - 'authors.label': 'authors', - 'tags.label': 'tags', - 'titles.label': 'titles', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Classifiers", + "graph_vlabel": "# of classifiers", + "graph_args": "-l 0", + "feeds.label": "feeds", + "authors.label": "authors", + "tags.label": "tags", + "titles.label": "titles", } def calculate_metrics(self): - from apps.analyzer.models import MClassifierFeed, MClassifierAuthor, MClassifierTag, MClassifierTitle + from apps.analyzer.models import ( + MClassifierAuthor, + MClassifierFeed, + MClassifierTag, + MClassifierTitle, + ) return { - 'feeds': MClassifierFeed.objects.count(), - 'authors': MClassifierAuthor.objects.count(), - 'tags': MClassifierTag.objects.count(), - 'titles': MClassifierTitle.objects.count(), + "feeds": MClassifierFeed.objects.count(), + "authors": MClassifierAuthor.objects.count(), + "tags": MClassifierTag.objects.count(), + "titles": MClassifierTitle.objects.count(), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_dbtimes.py b/utils/munin/newsblur_dbtimes.py index 46a7668d2f..740faeb26b 100755 --- a/utils/munin/newsblur_dbtimes.py +++ b/utils/munin/newsblur_dbtimes.py @@ -1,44 +1,48 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur DB Times', - 'graph_vlabel' : 'Database times (seconds)', - 'graph_args' : '-l 0', - 'sql_avg.label' : 'SQL avg times (5m)', - 'sql_avg.draw' : 'LINE1', - 'mongo_avg.label' : 'Mongo avg times (5m)', - 'mongo_avg.draw' : 'LINE1', - 'redis_avg.label' :'Redis avg times (5m)', - 'redis_avg.draw' : 'LINE1', - 'task_sql_avg.label' : 'Task SQL avg times (5m)', - 'task_sql_avg.draw' : 'LINE1', - 'task_mongo_avg.label' : 'Task Mongo avg times (5m)', - 'task_mongo_avg.draw' : 'LINE1', - 'task_redis_avg.label' :'Task Redis avg times (5m)', - 'task_redis_avg.draw' : 'LINE1', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur DB Times", + "graph_vlabel": "Database times (seconds)", + "graph_args": "-l 0", + "sql_avg.label": "SQL avg times (5m)", + "sql_avg.draw": "LINE1", + "mongo_avg.label": "Mongo avg times (5m)", + "mongo_avg.draw": "LINE1", + "redis_avg.label": "Redis avg times (5m)", + "redis_avg.draw": "LINE1", + "task_sql_avg.label": "Task SQL avg times (5m)", + "task_sql_avg.draw": "LINE1", + "task_mongo_avg.label": "Task Mongo avg times (5m)", + "task_mongo_avg.draw": "LINE1", + "task_redis_avg.label": "Task Redis avg times (5m)", + "task_redis_avg.draw": "LINE1", } def calculate_metrics(self): from apps.statistics.models import MStatistics - + return { - 'sql_avg': MStatistics.get('latest_sql_avg'), - 'mongo_avg': MStatistics.get('latest_mongo_avg'), - 'redis_avg': MStatistics.get('latest_redis_avg'), - 'task_sql_avg': MStatistics.get('latest_task_sql_avg'), - 'task_mongo_avg': MStatistics.get('latest_task_mongo_avg'), - 'task_redis_avg': MStatistics.get('latest_task_redis_avg'), + "sql_avg": MStatistics.get("latest_sql_avg"), + "mongo_avg": MStatistics.get("latest_mongo_avg"), + "redis_avg": MStatistics.get("latest_redis_avg"), + "task_sql_avg": MStatistics.get("latest_task_sql_avg"), + "task_mongo_avg": MStatistics.get("latest_task_mongo_avg"), + "task_redis_avg": MStatistics.get("latest_task_redis_avg"), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_errors.py b/utils/munin/newsblur_errors.py index 0e1f0d83da..9c040d356d 100755 --- a/utils/munin/newsblur_errors.py +++ b/utils/munin/newsblur_errors.py @@ -1,33 +1,38 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Fetching History', - 'graph_vlabel' : 'errors', - 'graph_args' : '-l 0', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Fetching History", + "graph_vlabel": "errors", + "graph_args": "-l 0", # 'feed_errors.label': 'Feed Errors', - 'feed_success.label': 'Feed Success', + "feed_success.label": "Feed Success", # 'page_errors.label': 'Page Errors', # 'page_success.label': 'Page Success', } def calculate_metrics(self): from apps.statistics.models import MStatistics + statistics = MStatistics.all() - + return { - 'feed_success': statistics['feeds_fetched'], + "feed_success": statistics["feeds_fetched"], } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_feed_counts.py b/utils/munin/newsblur_feed_counts.py index 1ed4ff518c..3c8e139386 100755 --- a/utils/munin/newsblur_feed_counts.py +++ b/utils/munin/newsblur_feed_counts.py @@ -1,69 +1,75 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph -import redis import os + +import redis + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Feed Counts', - 'graph_vlabel' : 'Feeds Feed Counts', - 'graph_args' : '-l 0', - 'scheduled_feeds.label': 'scheduled_feeds', - 'exception_feeds.label': 'exception_feeds', - 'exception_pages.label': 'exception_pages', - 'duplicate_feeds.label': 'duplicate_feeds', - 'active_feeds.label': 'active_feeds', - 'push_feeds.label': 'push_feeds', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Feed Counts", + "graph_vlabel": "Feeds Feed Counts", + "graph_args": "-l 0", + "scheduled_feeds.label": "scheduled_feeds", + "exception_feeds.label": "exception_feeds", + "exception_pages.label": "exception_pages", + "duplicate_feeds.label": "duplicate_feeds", + "active_feeds.label": "active_feeds", + "push_feeds.label": "push_feeds", } def calculate_metrics(self): - from apps.rss_feeds.models import Feed, DuplicateFeed - from apps.push.models import PushSubscription from django.conf import settings + + from apps.push.models import PushSubscription + from apps.rss_feeds.models import DuplicateFeed, Feed from apps.statistics.models import MStatistics - - exception_feeds = MStatistics.get('munin:exception_feeds') + + exception_feeds = MStatistics.get("munin:exception_feeds") if not exception_feeds: exception_feeds = Feed.objects.filter(has_feed_exception=True).count() - MStatistics.set('munin:exception_feeds', exception_feeds, 60*60*12) + MStatistics.set("munin:exception_feeds", exception_feeds, 60 * 60 * 12) - exception_pages = MStatistics.get('munin:exception_pages') + exception_pages = MStatistics.get("munin:exception_pages") if not exception_pages: exception_pages = Feed.objects.filter(has_page_exception=True).count() - MStatistics.set('munin:exception_pages', exception_pages, 60*60*12) + MStatistics.set("munin:exception_pages", exception_pages, 60 * 60 * 12) - duplicate_feeds = MStatistics.get('munin:duplicate_feeds') + duplicate_feeds = MStatistics.get("munin:duplicate_feeds") if not duplicate_feeds: duplicate_feeds = DuplicateFeed.objects.count() - MStatistics.set('munin:duplicate_feeds', duplicate_feeds, 60*60*12) + MStatistics.set("munin:duplicate_feeds", duplicate_feeds, 60 * 60 * 12) - active_feeds = MStatistics.get('munin:active_feeds') + active_feeds = MStatistics.get("munin:active_feeds") if not active_feeds: active_feeds = Feed.objects.filter(active_subscribers__gt=0).count() - MStatistics.set('munin:active_feeds', active_feeds, 60*60*12) + MStatistics.set("munin:active_feeds", active_feeds, 60 * 60 * 12) - push_feeds = MStatistics.get('munin:push_feeds') + push_feeds = MStatistics.get("munin:push_feeds") if not push_feeds: push_feeds = PushSubscription.objects.filter(verified=True).count() - MStatistics.set('munin:push_feeds', push_feeds, 60*60*12) + MStatistics.set("munin:push_feeds", push_feeds, 60 * 60 * 12) r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) - + return { - 'scheduled_feeds': r.zcard('scheduled_updates'), - 'exception_feeds': exception_feeds, - 'exception_pages': exception_pages, - 'duplicate_feeds': duplicate_feeds, - 'active_feeds': active_feeds, - 'push_feeds': push_feeds, + "scheduled_feeds": r.zcard("scheduled_updates"), + "exception_feeds": exception_feeds, + "exception_pages": exception_pages, + "duplicate_feeds": duplicate_feeds, + "active_feeds": active_feeds, + "push_feeds": push_feeds, } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_feeds.py b/utils/munin/newsblur_feeds.py index 61857dbc85..fd14df2552 100755 --- a/utils/munin/newsblur_feeds.py +++ b/utils/munin/newsblur_feeds.py @@ -1,47 +1,51 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Feeds & Subscriptions', - 'graph_vlabel' : 'Feeds & Subscribers', - 'graph_args' : '-l 0', - 'feeds.label': 'feeds', - 'subscriptions.label': 'subscriptions', - 'profiles.label': 'profiles', - 'social_subscriptions.label': 'social_subscriptions', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Feeds & Subscriptions", + "graph_vlabel": "Feeds & Subscribers", + "graph_args": "-l 0", + "feeds.label": "feeds", + "subscriptions.label": "subscriptions", + "profiles.label": "profiles", + "social_subscriptions.label": "social_subscriptions", } def calculate_metrics(self): - from apps.rss_feeds.models import Feed from apps.reader.models import UserSubscription + from apps.rss_feeds.models import Feed from apps.social.models import MSocialProfile, MSocialSubscription from apps.statistics.models import MStatistics - feeds_count = MStatistics.get('munin:feeds_count') + feeds_count = MStatistics.get("munin:feeds_count") if not feeds_count: feeds_count = Feed.objects.all().count() - MStatistics.set('munin:feeds_count', feeds_count, 60*60*12) + MStatistics.set("munin:feeds_count", feeds_count, 60 * 60 * 12) - subscriptions_count = MStatistics.get('munin:subscriptions_count') + subscriptions_count = MStatistics.get("munin:subscriptions_count") if not subscriptions_count: subscriptions_count = UserSubscription.objects.all().count() - MStatistics.set('munin:subscriptions_count', subscriptions_count, 60*60*12) + MStatistics.set("munin:subscriptions_count", subscriptions_count, 60 * 60 * 12) return { - 'feeds': feeds_count, - 'subscriptions': subscriptions_count, - 'profiles': MSocialProfile.objects.count(), - 'social_subscriptions': MSocialSubscription.objects.count(), + "feeds": feeds_count, + "subscriptions": subscriptions_count, + "profiles": MSocialProfile.objects.count(), + "social_subscriptions": MSocialSubscription.objects.count(), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_loadtimes.py b/utils/munin/newsblur_loadtimes.py index 05a76eb32b..4f17a32d1c 100755 --- a/utils/munin/newsblur_loadtimes.py +++ b/utils/munin/newsblur_loadtimes.py @@ -1,30 +1,34 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Loadtimes', - 'graph_vlabel' : 'Loadtimes (seconds)', - 'graph_args' : '-l 0', - 'feed_loadtimes_avg_hour.label': 'Feed Loadtimes Avg (Hour)', - 'feeds_loaded_hour.label': 'Feeds Loaded (Hour)', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Loadtimes", + "graph_vlabel": "Loadtimes (seconds)", + "graph_args": "-l 0", + "feed_loadtimes_avg_hour.label": "Feed Loadtimes Avg (Hour)", + "feeds_loaded_hour.label": "Feeds Loaded (Hour)", } def calculate_metrics(self): from apps.statistics.models import MStatistics - + return { - 'feed_loadtimes_avg_hour': MStatistics.get('latest_avg_time_taken'), - 'feeds_loaded_hour': MStatistics.get('latest_sites_loaded'), + "feed_loadtimes_avg_hour": MStatistics.get("latest_avg_time_taken"), + "feeds_loaded_hour": MStatistics.get("latest_sites_loaded"), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_stories.py b/utils/munin/newsblur_stories.py index 9b71664a0b..c035ce77f6 100755 --- a/utils/munin/newsblur_stories.py +++ b/utils/munin/newsblur_stories.py @@ -1,31 +1,34 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() class NBMuninGraph(MuninGraph): - @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Stories', - 'graph_vlabel' : 'Stories', - 'graph_args' : '-l 0', - 'stories.label': 'Stories', - 'starred_stories.label': 'Starred stories', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Stories", + "graph_vlabel": "Stories", + "graph_args": "-l 0", + "stories.label": "Stories", + "starred_stories.label": "Starred stories", } def calculate_metrics(self): - from apps.rss_feeds.models import MStory, MStarredStory + from apps.rss_feeds.models import MStarredStory, MStory return { - 'stories': MStory.objects.count(), - 'starred_stories': MStarredStory.objects.count(), + "stories": MStory.objects.count(), + "starred_stories": MStarredStory.objects.count(), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_tasks_codes.py b/utils/munin/newsblur_tasks_codes.py index 94106899d9..a79d53f8fe 100755 --- a/utils/munin/newsblur_tasks_codes.py +++ b/utils/munin/newsblur_tasks_codes.py @@ -1,49 +1,57 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): graph = { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Task Codes', - 'graph_vlabel' : 'Status codes on feed fetch', - 'graph_args' : '-l 0', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Task Codes", + "graph_vlabel": "Status codes on feed fetch", + "graph_args": "-l 0", } stats = self.stats - graph.update(dict((("_%s.label" % s['_id'], s['_id']) for s in stats))) - graph['graph_order'] = ' '.join(sorted(("_%s" % s['_id']) for s in stats)) + graph.update(dict((("_%s.label" % s["_id"], s["_id"]) for s in stats))) + graph["graph_order"] = " ".join(sorted(("_%s" % s["_id"]) for s in stats)) return graph def calculate_metrics(self): - servers = dict((("_%s" % s['_id'], s['feeds']) for s in self.stats)) - + servers = dict((("_%s" % s["_id"], s["feeds"]) for s in self.stats)) + return servers - + @property def stats(self): import datetime + from django.conf import settings - - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : "$feed_code", - "feeds" : {"$sum": 1}, - }, - }]) - + { + "$group": { + "_id": "$feed_code", + "feeds": {"$sum": 1}, + }, + }, + ] + ) + return list(stats) - -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_tasks_pipeline.py b/utils/munin/newsblur_tasks_pipeline.py index 430918f739..36c071faf1 100755 --- a/utils/munin/newsblur_tasks_pipeline.py +++ b/utils/munin/newsblur_tasks_pipeline.py @@ -1,54 +1,63 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): graph = { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Task Pipeline', - 'graph_vlabel' : 'Feed fetch pipeline times', - 'graph_args' : '-l 0', - 'feed_fetch.label': 'feed_fetch', - 'feed_process.label': 'feed_process', - 'page.label': 'page', - 'icon.label': 'icon', - 'total.label': 'total', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Task Pipeline", + "graph_vlabel": "Feed fetch pipeline times", + "graph_args": "-l 0", + "feed_fetch.label": "feed_fetch", + "feed_process.label": "feed_process", + "page.label": "page", + "icon.label": "icon", + "total.label": "total", } return graph def calculate_metrics(self): return self.stats - + @property def stats(self): import datetime + from django.conf import settings - - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id": 1, - "feed_fetch": {"$avg": "$feed_fetch"}, - "feed_process": {"$avg": "$feed_process"}, - "page": {"$avg": "$page"}, - "icon": {"$avg": "$icon"}, - "total": {"$avg": "$total"}, - }, - }]) - + { + "$group": { + "_id": 1, + "feed_fetch": {"$avg": "$feed_fetch"}, + "feed_process": {"$avg": "$feed_process"}, + "page": {"$avg": "$page"}, + "icon": {"$avg": "$icon"}, + "total": {"$avg": "$total"}, + }, + }, + ] + ) + return list(stats)[0] - -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_tasks_servers.py b/utils/munin/newsblur_tasks_servers.py index 3b1286a02b..b3cc597ca8 100755 --- a/utils/munin/newsblur_tasks_servers.py +++ b/utils/munin/newsblur_tasks_servers.py @@ -1,71 +1,83 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import datetime import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" from django.conf import settings class NBMuninGraph(MuninGraph): - @property def graph_config(self): graph = { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Task Server Fetches', - 'graph_vlabel' : '# of fetches / server', - 'graph_args' : '-l 0', - 'total.label' : 'total', - 'total.draw' : 'LINE1', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Task Server Fetches", + "graph_vlabel": "# of fetches / server", + "graph_args": "-l 0", + "total.label": "total", + "total.draw": "LINE1", } stats = self.stats - graph.update(dict((("%s.label" % s['_id'].replace('-', ''), s['_id']) for s in stats))) - graph.update(dict((("%s.draw" % s['_id'].replace('-', ''), "AREASTACK") for s in stats))) - graph['graph_order'] = ' '.join(sorted(s['_id'].replace('-', '') for s in stats)) + graph.update(dict((("%s.label" % s["_id"].replace("-", ""), s["_id"]) for s in stats))) + graph.update(dict((("%s.draw" % s["_id"].replace("-", ""), "AREASTACK") for s in stats))) + graph["graph_order"] = " ".join(sorted(s["_id"].replace("-", "") for s in stats)) return graph def calculate_metrics(self): - servers = dict((("%s" % s['_id'].replace('-', ''), s['feeds']) for s in self.stats)) - servers['total'] = self.total[0]['feeds'] + servers = dict((("%s" % s["_id"].replace("-", ""), s["feeds"]) for s in self.stats)) + servers["total"] = self.total[0]["feeds"] return servers - + @property def stats(self): - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gte": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, + }, + { + "$group": { + "_id": "$server", + "feeds": {"$sum": 1}, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "feeds" : {"$sum": 1}, - }, - }]) - + ] + ) + return list(stats) - + @property def total(self): import datetime + from django.conf import settings - - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : 1, - "feeds" : {"$sum": 1}, - }, - }]) - + { + "$group": { + "_id": 1, + "feeds": {"$sum": 1}, + }, + }, + ] + ) + return list(stats) - -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_tasks_times.py b/utils/munin/newsblur_tasks_times.py index 4a650a4674..44bcc11766 100755 --- a/utils/munin/newsblur_tasks_times.py +++ b/utils/munin/newsblur_tasks_times.py @@ -1,53 +1,62 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): graph = { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Task Server Times', - 'graph_vlabel' : 'Feed fetch time / server', - 'graph_args' : '-l 0', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Task Server Times", + "graph_vlabel": "Feed fetch time / server", + "graph_args": "-l 0", } stats = self.stats - graph.update(dict((("%s.label" % s['_id'].replace('-', ''), s['_id']) for s in stats))) - graph.update(dict((("%s.draw" % s['_id'].replace('-', ''), 'LINE1') for s in stats))) - graph['graph_order'] = ' '.join(sorted(s['_id'].replace('-', '') for s in stats)) + graph.update(dict((("%s.label" % s["_id"].replace("-", ""), s["_id"]) for s in stats))) + graph.update(dict((("%s.draw" % s["_id"].replace("-", ""), "LINE1") for s in stats))) + graph["graph_order"] = " ".join(sorted(s["_id"].replace("-", "") for s in stats)) return graph def calculate_metrics(self): - servers = dict((("%s" % s['_id'].replace('-', ''), s['total']) for s in self.stats)) + servers = dict((("%s" % s["_id"].replace("-", ""), s["total"]) for s in self.stats)) return servers - + @property def stats(self): import datetime + from django.conf import settings - - stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate([{ - "$match": { - "date": { - "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + + stats = settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.aggregate( + [ + { + "$match": { + "date": { + "$gt": datetime.datetime.now() - datetime.timedelta(minutes=5), + }, + }, }, - }, - }, { - "$group": { - "_id" : "$server", - "total" : {"$avg": "$total"}, - }, - }]) - + { + "$group": { + "_id": "$server", + "total": {"$avg": "$total"}, + }, + }, + ] + ) + return list(stats) - -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_updates.py b/utils/munin/newsblur_updates.py index 31e0b86e96..5d57fd9721 100755 --- a/utils/munin/newsblur_updates.py +++ b/utils/munin/newsblur_updates.py @@ -1,48 +1,52 @@ #!/srv/newsblur/venv/newsblur3/bin/python +import os + import redis + from utils.munin.base import MuninGraph -import os + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Updates', - 'graph_vlabel' : '# of updates', - 'graph_args' : '-l 0', - 'update_queue.label': 'Queued Feeds', - 'feeds_fetched.label': 'Fetched feeds last hour', - 'tasked_feeds.label': 'Tasked Feeds', - 'error_feeds.label': 'Error Feeds', - 'celery_update_feeds.label': 'Celery - Update Feeds', - 'celery_new_feeds.label': 'Celery - New Feeds', - 'celery_push_feeds.label': 'Celery - Push Feeds', - 'celery_work_queue.label': 'Celery - Work Queue', - 'celery_search_queue.label': 'Celery - Search Queue', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Updates", + "graph_vlabel": "# of updates", + "graph_args": "-l 0", + "update_queue.label": "Queued Feeds", + "feeds_fetched.label": "Fetched feeds last hour", + "tasked_feeds.label": "Tasked Feeds", + "error_feeds.label": "Error Feeds", + "celery_update_feeds.label": "Celery - Update Feeds", + "celery_new_feeds.label": "Celery - New Feeds", + "celery_push_feeds.label": "Celery - Push Feeds", + "celery_work_queue.label": "Celery - Work Queue", + "celery_search_queue.label": "Celery - Search Queue", } - def calculate_metrics(self): from django.conf import settings - + r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL) return { - 'update_queue': r.scard("queued_feeds"), - 'feeds_fetched': r.zcard("fetched_feeds_last_hour"), - 'tasked_feeds': r.zcard("tasked_feeds"), - 'error_feeds': r.zcard("error_feeds"), - 'celery_update_feeds': r.llen("update_feeds"), - 'celery_new_feeds': r.llen("new_feeds"), - 'celery_push_feeds': r.llen("push_feeds"), - 'celery_work_queue': r.llen("work_queue"), - 'celery_search_queue': r.llen("search_indexer"), + "update_queue": r.scard("queued_feeds"), + "feeds_fetched": r.zcard("fetched_feeds_last_hour"), + "tasked_feeds": r.zcard("tasked_feeds"), + "error_feeds": r.zcard("error_feeds"), + "celery_update_feeds": r.llen("update_feeds"), + "celery_new_feeds": r.llen("new_feeds"), + "celery_push_feeds": r.llen("push_feeds"), + "celery_work_queue": r.llen("work_queue"), + "celery_search_queue": r.llen("search_indexer"), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/munin/newsblur_users.py b/utils/munin/newsblur_users.py index a2083ea953..2a0048e7f1 100755 --- a/utils/munin/newsblur_users.py +++ b/utils/munin/newsblur_users.py @@ -1,41 +1,47 @@ #!/srv/newsblur/venv/newsblur3/bin/python -from utils.munin.base import MuninGraph import os + +from utils.munin.base import MuninGraph + os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" import django + django.setup() -class NBMuninGraph(MuninGraph): +class NBMuninGraph(MuninGraph): @property def graph_config(self): return { - 'graph_category' : 'NewsBlur', - 'graph_title' : 'NewsBlur Users', - 'graph_vlabel' : 'users', - 'graph_args' : '-l 0', - 'all.label': 'all', - 'monthly.label': 'monthly', - 'daily.label': 'daily', - 'premium.label': 'premium', - 'queued.label': 'queued', + "graph_category": "NewsBlur", + "graph_title": "NewsBlur Users", + "graph_vlabel": "users", + "graph_args": "-l 0", + "all.label": "all", + "monthly.label": "monthly", + "daily.label": "daily", + "premium.label": "premium", + "queued.label": "queued", } def calculate_metrics(self): import datetime + from django.contrib.auth.models import User + from apps.profile.models import Profile, RNewUserQueue last_month = datetime.datetime.utcnow() - datetime.timedelta(days=30) - last_day = datetime.datetime.utcnow() - datetime.timedelta(minutes=60*24) + last_day = datetime.datetime.utcnow() - datetime.timedelta(minutes=60 * 24) return { - 'all': User.objects.count(), - 'monthly': Profile.objects.filter(last_seen_on__gte=last_month).count(), - 'daily': Profile.objects.filter(last_seen_on__gte=last_day).count(), - 'premium': Profile.objects.filter(is_premium=True).count(), - 'queued': RNewUserQueue.user_count(), + "all": User.objects.count(), + "monthly": Profile.objects.filter(last_seen_on__gte=last_month).count(), + "daily": Profile.objects.filter(last_seen_on__gte=last_day).count(), + "premium": Profile.objects.filter(is_premium=True).count(), + "queued": RNewUserQueue.user_count(), } -if __name__ == '__main__': + +if __name__ == "__main__": NBMuninGraph().run() diff --git a/utils/pipeline_utils.py b/utils/pipeline_utils.py index 51cbf85ff9..c400e3b926 100644 --- a/utils/pipeline_utils.py +++ b/utils/pipeline_utils.py @@ -1,26 +1,29 @@ import re + from django.conf import settings -from pipeline.finders import FileSystemFinder as PipelineFileSystemFinder from pipeline.finders import AppDirectoriesFinder as PipelineAppDirectoriesFinder -from pipeline.storage import GZIPMixin -from pipeline.storage import PipelineManifestStorage +from pipeline.finders import FileSystemFinder as PipelineFileSystemFinder +from pipeline.storage import GZIPMixin, PipelineManifestStorage + class PipelineStorage(PipelineManifestStorage): def url(self, *args, **kwargs): if settings.DEBUG_ASSETS: # print(f"Pre-Pipeline storage: {args} {kwargs}") - kwargs['name'] = re.sub(r'\.[a-f0-9]{12}\.(css|js)$', r'.\1', args[0]) + kwargs["name"] = re.sub(r"\.[a-f0-9]{12}\.(css|js)$", r".\1", args[0]) args = args[1:] url = super().url(*args, **kwargs) if settings.DEBUG_ASSETS: url = url.replace(settings.STATIC_URL, settings.MEDIA_URL) - url = re.sub(r'\.[a-f0-9]{12}\.(css|js)$', r'.\1', url) + url = re.sub(r"\.[a-f0-9]{12}\.(css|js)$", r".\1", url) # print(f"Pipeline storage: {args} {kwargs} {url}") return url + class GzipPipelineStorage(GZIPMixin, PipelineManifestStorage): pass + class AppDirectoriesFinder(PipelineAppDirectoriesFinder): """ Like AppDirectoriesFinder, but doesn't return any additional ignored patterns @@ -28,36 +31,38 @@ class AppDirectoriesFinder(PipelineAppDirectoriesFinder): This allows us to concentrate/compress our components without dragging the raw versions in too. """ + ignore_patterns = [ # '*.js', # '*.css', - '*.less', - '*.scss', - '*.styl', - '*.sh', - '*.html', - '*.ttf', - '*.md', - '*.markdown', - '*.php', - '*.txt', + "*.less", + "*.scss", + "*.styl", + "*.sh", + "*.html", + "*.ttf", + "*.md", + "*.markdown", + "*.php", + "*.txt", # '*.gif', # due to django_extensions/css/jquery.autocomplete.css: django_extensions/img/indicator.gif - '*.png', - '*.jpg', + "*.png", + "*.jpg", # '*.svg', # due to admin/css/base.css: admin/img/sorting-icons.svg - '*.ico', - '*.icns', - '*.psd', - '*.ai', - '*.sketch', - '*.emf', - '*.eps', - '*.pdf', - '*.xml', - '*LICENSE*', - '*README*', + "*.ico", + "*.icns", + "*.psd", + "*.ai", + "*.sketch", + "*.emf", + "*.eps", + "*.pdf", + "*.xml", + "*LICENSE*", + "*README*", ] - + + class FileSystemFinder(PipelineFileSystemFinder): """ Like FileSystemFinder, but doesn't return any additional ignored patterns @@ -65,48 +70,48 @@ class FileSystemFinder(PipelineFileSystemFinder): This allows us to concentrate/compress our components without dragging the raw versions in too. """ + ignore_patterns = [ # '*.js', # '*.css', # '*.less', # '*.scss', # '*.styl', - '*.sh', - '*.html', - '*.ttf', - '*.md', - '*.markdown', - '*.php', - '*.txt', - '*.gif', - '*.png', - '*.jpg', - '*media/**/*.svg', - '*.ico', - '*.icns', - '*.psd', - '*.ai', - '*.sketch', - '*.emf', - '*.eps', - '*.pdf', - '*.xml', - '*embed*', - 'blog*', + "*.sh", + "*.html", + "*.ttf", + "*.md", + "*.markdown", + "*.php", + "*.txt", + "*.gif", + "*.png", + "*.jpg", + "*media/**/*.svg", + "*.ico", + "*.icns", + "*.psd", + "*.ai", + "*.sketch", + "*.emf", + "*.eps", + "*.pdf", + "*.xml", + "*embed*", + "blog*", # # '*bookmarklet*', # # '*circular*', # # '*embed*', - '*css/mobile*', - '*extensions*', - 'fonts/*/*.css', - '*flash*', + "*css/mobile*", + "*extensions*", + "fonts/*/*.css", + "*flash*", # '*jquery-ui*', # 'mobile*', - '*safari*', + "*safari*", # # '*social*', # # '*vendor*', # 'Makefile*', # 'Gemfile*', - 'node_modules', + "node_modules", ] - \ No newline at end of file diff --git a/utils/ratelimit.py b/utils/ratelimit.py index 04e0aeaf96..fe5824e03b 100644 --- a/utils/ratelimit.py +++ b/utils/ratelimit.py @@ -1,46 +1,48 @@ -from django.http import HttpResponse -from django.core.cache import cache -from datetime import datetime, timedelta import functools import hashlib +from datetime import datetime, timedelta + +from django.core.cache import cache +from django.http import HttpResponse class ratelimit(object): "Instances of this class can be used as decorators" # This class is designed to be sub-classed - minutes = 1 # The time period - requests = 4 # Number of allowed requests in that time period - - prefix = 'rl-' # Prefix for memcache key - + minutes = 1 # The time period + requests = 4 # Number of allowed requests in that time period + + prefix = "rl-" # Prefix for memcache key + def __init__(self, **options): for key, value in options.items(): setattr(self, key, value) - + def __call__(self, fn): def wrapper(request, *args, **kwargs): return self.view_wrapper(request, fn, *args, **kwargs) + functools.update_wrapper(wrapper, fn) return wrapper - + def view_wrapper(self, request, fn, *args, **kwargs): if not self.should_ratelimit(request): return fn(request, *args, **kwargs) - + counts = list(self.get_counters(request).values()) - + # Increment rate limiting counter self.cache_incr(self.current_key(request)) - + # Have they failed? if sum(counts) >= self.requests: return self.disallowed(request) - + return fn(request, *args, **kwargs) - + def cache_get_many(self, keys): return cache.get_many(keys) - + def cache_incr(self, key): # memcache is only backend that can increment atomically try: @@ -49,59 +51,53 @@ def cache_incr(self, key): cache.incr(key) except (AttributeError, ValueError): cache.set(key, cache.get(key, 0) + 1, self.expire_after()) - + def should_ratelimit(self, request): return True - + def get_counters(self, request): return self.cache_get_many(self.keys_to_check(request)) - + def keys_to_check(self, request): extra = self.key_extra(request) now = datetime.now() return [ - '%s%s-%s' % ( - self.prefix, - extra, - (now - timedelta(minutes = minute)).strftime('%Y%m%d%H%M') - ) for minute in range(self.minutes + 1) + "%s%s-%s" % (self.prefix, extra, (now - timedelta(minutes=minute)).strftime("%Y%m%d%H%M")) + for minute in range(self.minutes + 1) ] - + def current_key(self, request): - return '%s%s-%s' % ( - self.prefix, - self.key_extra(request), - datetime.now().strftime('%Y%m%d%H%M') - ) - + return "%s%s-%s" % (self.prefix, self.key_extra(request), datetime.now().strftime("%Y%m%d%H%M")) + def key_extra(self, request): - key = getattr(request.session, 'session_key', '') + key = getattr(request.session, "session_key", "") if not key: - key = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0] + key = request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0] if not key: - key = request.COOKIES.get('newsblur_sessionid', '') + key = request.COOKIES.get("newsblur_sessionid", "") if not key: - key = request.META.get('HTTP_USER_AGENT', '') + key = request.META.get("HTTP_USER_AGENT", "") return key - + def disallowed(self, request): - return HttpResponse('Rate limit exceeded', status=429) - + return HttpResponse("Rate limit exceeded", status=429) + def expire_after(self): "Used for setting the memcached cache expiry" return (self.minutes + 1) * 60 + class ratelimit_post(ratelimit): "Rate limit POSTs - can be used to protect a login form" - key_field = None # If provided, this POST var will affect the rate limit - + key_field = None # If provided, this POST var will affect the rate limit + def should_ratelimit(self, request): - return request.method == 'POST' - + return request.method == "POST" + def key_extra(self, request): # IP address and key_field (if it is set) extra = super(ratelimit_post, self).key_extra(request) if self.key_field: - value = hashlib.sha1((request.POST.get(self.key_field, '')).encode('utf-8')).hexdigest() - extra += '-' + value + value = hashlib.sha1((request.POST.get(self.key_field, "")).encode("utf-8")).hexdigest() + extra += "-" + value return extra diff --git a/utils/redis_raw_log_middleware.py b/utils/redis_raw_log_middleware.py index c1049c7ae8..e309bc8e15 100644 --- a/utils/redis_raw_log_middleware.py +++ b/utils/redis_raw_log_middleware.py @@ -1,41 +1,40 @@ -from django.core.exceptions import MiddlewareNotUsed +from pprint import pprint +from time import time + from django.conf import settings +from django.core.exceptions import MiddlewareNotUsed from django.db import connection +from redis.client import Pipeline, Redis from redis.connection import Connection -from redis.client import Redis, Pipeline -from time import time -from pprint import pprint -class RedisDumpMiddleware(object): +class RedisDumpMiddleware(object): 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_view(self, request, callback, callback_args, callback_kwargs): - if not self.activated(request): return - if not getattr(Connection, '_logging', False): + if not self.activated(request): + return + if not getattr(Connection, "_logging", False): # save old methods - setattr(Connection, '_logging', True) + setattr(Connection, "_logging", True) connection.queriesx = [] - Redis.execute_command = \ - self._instrument(Redis.execute_command) - Pipeline._execute_transaction = \ - self._instrument_pipeline(Pipeline._execute_transaction) + Redis.execute_command = self._instrument(Redis.execute_command) + Pipeline._execute_transaction = self._instrument_pipeline(Pipeline._execute_transaction) def process_celery(self, profiler): - if not self.activated(profiler): return - if not getattr(Connection, '_logging', False): + if not self.activated(profiler): + return + if not getattr(Connection, "_logging", False): # save old methods - setattr(Connection, '_logging', True) - Redis.execute_command = \ - self._instrument(Redis.execute_command) - Pipeline._execute_transaction = \ - self._instrument_pipeline(Pipeline._execute_transaction) + setattr(Connection, "_logging", True) + Redis.execute_command = self._instrument(Redis.execute_command) + Pipeline._execute_transaction = self._instrument_pipeline(Pipeline._execute_transaction) def process_response(self, request, response): # if settings.DEBUG and hasattr(self, 'orig_pack_command'): @@ -54,13 +53,16 @@ def instrumented_method(*args, **kwargs): result = original_method(*args, **kwargs) stop = time() duration = stop - start - if not getattr(connection, 'queriesx', False): + if not getattr(connection, "queriesx", False): connection.queriesx = [] - connection.queriesx.append({ - message['redis_server_name']: message, - 'time': '%.6f' % duration, - }) + connection.queriesx.append( + { + message["redis_server_name"]: message, + "time": "%.6f" % duration, + } + ) return result + return instrumented_method def _instrument_pipeline(self, original_method): @@ -72,38 +74,41 @@ def instrumented_method(*args, **kwargs): result = original_method(*args, **kwargs) stop = time() duration = stop - start - if not getattr(connection, 'queriesx', False): + if not getattr(connection, "queriesx", False): connection.queriesx = [] - connection.queriesx.append({ - message['redis_server_name']: message, - 'time': '%.6f' % duration, - }) + connection.queriesx.append( + { + message["redis_server_name"]: message, + "time": "%.6f" % duration, + } + ) return result + return instrumented_method - + def process_message(self, *args, **kwargs): query = [] redis_server_name = None for a, arg in enumerate(args): if isinstance(arg, Redis): redis_connection = arg - redis_server_name = redis_connection.connection_pool.connection_kwargs['host'] - if 'db-redis-user' in redis_server_name: - redis_server_name = 'redis_user' - elif 'db-redis-session' in redis_server_name: - redis_server_name = 'redis_session' - elif 'db-redis-story' in redis_server_name: - redis_server_name = 'redis_story' - elif 'db-redis-pubsub' in redis_server_name: - redis_server_name = 'redis_pubsub' - elif 'db_redis' in redis_server_name: - redis_server_name = 'redis_user' + redis_server_name = redis_connection.connection_pool.connection_kwargs["host"] + if "db-redis-user" in redis_server_name: + redis_server_name = "redis_user" + elif "db-redis-session" in redis_server_name: + redis_server_name = "redis_session" + elif "db-redis-story" in redis_server_name: + redis_server_name = "redis_story" + elif "db-redis-pubsub" in redis_server_name: + redis_server_name = "redis_pubsub" + elif "db_redis" in redis_server_name: + redis_server_name = "redis_user" continue if len(str(arg)) > 100: arg = "[%s bytes]" % len(str(arg)) - query.append(str(arg).replace('\n', '')) - return { 'query': f"{redis_server_name}: {' '.join(query)}", 'redis_server_name': redis_server_name } - + query.append(str(arg).replace("\n", "")) + return {"query": f"{redis_server_name}: {' '.join(query)}", "redis_server_name": redis_server_name} + def process_pipeline(self, *args, **kwargs): queries = [] redis_server_name = None @@ -112,17 +117,17 @@ def process_pipeline(self, *args, **kwargs): continue if isinstance(arg, Pipeline): redis_connection = arg - redis_server_name = redis_connection.connection_pool.connection_kwargs['host'] - if 'db-redis-user' in redis_server_name: - redis_server_name = 'redis_user' - elif 'db-redis-session' in redis_server_name: - redis_server_name = 'redis_session' - elif 'db-redis-story' in redis_server_name: - redis_server_name = 'redis_story' - elif 'db-redis-pubsub' in redis_server_name: - redis_server_name = 'redis_pubsub' - elif 'db_redis' in redis_server_name: - redis_server_name = 'redis_user' + redis_server_name = redis_connection.connection_pool.connection_kwargs["host"] + if "db-redis-user" in redis_server_name: + redis_server_name = "redis_user" + elif "db-redis-session" in redis_server_name: + redis_server_name = "redis_session" + elif "db-redis-story" in redis_server_name: + redis_server_name = "redis_story" + elif "db-redis-pubsub" in redis_server_name: + redis_server_name = "redis_pubsub" + elif "db_redis" in redis_server_name: + redis_server_name = "redis_user" continue if not isinstance(arg, list): continue @@ -132,16 +137,16 @@ def process_pipeline(self, *args, **kwargs): if len(str(arg)) > 10000: arg = "[%s bytes]" % len(str(arg)) # query.append(str(arg).replace('\n', '')) - queries_str = '\n\t\t\t\t\t\t~FC'.join(queries) - return { 'query': f"{redis_server_name}: {queries_str}", 'redis_server_name': redis_server_name } + queries_str = "\n\t\t\t\t\t\t~FC".join(queries) + return {"query": f"{redis_server_name}: {queries_str}", "redis_server_name": redis_server_name} def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response diff --git a/utils/request_introspection_middleware.py b/utils/request_introspection_middleware.py index 7ec0bdf032..7fa53f4f8b 100644 --- a/utils/request_introspection_middleware.py +++ b/utils/request_introspection_middleware.py @@ -1,10 +1,12 @@ -from django.conf import settings -from utils import log as logging -from apps.statistics.rstats import round_time -import pickle import base64 +import pickle import time + import redis +from django.conf import settings + +from apps.statistics.rstats import round_time +from utils import log as logging IGNORE_PATHS = [ "/_haproxychk", @@ -12,6 +14,7 @@ RECORD_SLOW_REQUESTS_ABOVE_SECONDS = 10 + class DumpRequestMiddleware: def process_request(self, request): if settings.DEBUG and request.path not in IGNORE_PATHS: @@ -20,33 +23,49 @@ def process_request(self, request): if request_items: request_items_str = f"{dict(request_items)}" if len(request_items_str) > 500: - request_items_str = request_items_str[:100] + "...[" + str(len(request_items_str)-200) + " bytes]..." + request_items_str[-100:] - logging.debug(" ---> ~FC%s ~SN~FK~BC%s~BT~ST ~FC%s~BK~FC" % (request.method, request.path, request_items_str)) + request_items_str = ( + request_items_str[:100] + + "...[" + + str(len(request_items_str) - 200) + + " bytes]..." + + request_items_str[-100:] + ) + logging.debug( + " ---> ~FC%s ~SN~FK~BC%s~BT~ST ~FC%s~BK~FC" + % (request.method, request.path, request_items_str) + ) else: logging.debug(" ---> ~FC%s ~SN~FK~BC%s~BT~ST" % (request.method, request.path)) def process_response(self, request, response): - if hasattr(request, 'sql_times_elapsed'): - redis_log = "~FCuser:%s%.6f~SNs ~FCstory:%s%.6f~SNs ~FCsession:%s%.6f~SNs ~FCpubsub:%s%.6f~SNs" % ( - self.color_db(request.sql_times_elapsed['redis_user'], '~FC'), - request.sql_times_elapsed['redis_user'], - self.color_db(request.sql_times_elapsed['redis_story'], '~FC'), - request.sql_times_elapsed['redis_story'], - self.color_db(request.sql_times_elapsed['redis_session'], '~FC'), - request.sql_times_elapsed['redis_session'], - self.color_db(request.sql_times_elapsed['redis_pubsub'], '~FC'), - request.sql_times_elapsed['redis_pubsub'], + if hasattr(request, "sql_times_elapsed"): + redis_log = ( + "~FCuser:%s%.6f~SNs ~FCstory:%s%.6f~SNs ~FCsession:%s%.6f~SNs ~FCpubsub:%s%.6f~SNs" + % ( + self.color_db(request.sql_times_elapsed["redis_user"], "~FC"), + request.sql_times_elapsed["redis_user"], + self.color_db(request.sql_times_elapsed["redis_story"], "~FC"), + request.sql_times_elapsed["redis_story"], + self.color_db(request.sql_times_elapsed["redis_session"], "~FC"), + request.sql_times_elapsed["redis_session"], + self.color_db(request.sql_times_elapsed["redis_pubsub"], "~FC"), + request.sql_times_elapsed["redis_pubsub"], + ) + ) + logging.user( + request, + "~SN~FCDB times ~SB~FK%s~SN~FC: ~FYsql: %s%.4f~SNs ~SN~FMmongo: %s%.5f~SNs ~SN~FCredis: %s" + % ( + request.path, + self.color_db(request.sql_times_elapsed["sql"], "~FY"), + request.sql_times_elapsed["sql"], + self.color_db(request.sql_times_elapsed["mongo"], "~FM"), + request.sql_times_elapsed["mongo"], + redis_log, + ), ) - logging.user(request, "~SN~FCDB times ~SB~FK%s~SN~FC: ~FYsql: %s%.4f~SNs ~SN~FMmongo: %s%.5f~SNs ~SN~FCredis: %s" % ( - request.path, - self.color_db(request.sql_times_elapsed['sql'], '~FY'), - request.sql_times_elapsed['sql'], - self.color_db(request.sql_times_elapsed['mongo'], '~FM'), - request.sql_times_elapsed['mongo'], - redis_log - )) - if hasattr(request, 'start_time'): + if hasattr(request, "start_time"): seconds = time.time() - request.start_time if seconds > RECORD_SLOW_REQUESTS_ABOVE_SECONDS: r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL) @@ -56,9 +75,9 @@ def process_response(self, request, response): user_id = request.user.pk if request.user.is_authenticated else "0" data_string = None if request.method == "GET": - data_string = ' '.join([f"{key}={value}" for key, value in request.GET.items()]) + data_string = " ".join([f"{key}={value}" for key, value in request.GET.items()]) elif request.method == "GET": - data_string = ' '.join([f"{key}={value}" for key, value in request.POST.items()]) + data_string = " ".join([f"{key}={value}" for key, value in request.POST.items()]) data = { "user_id": user_id, "time": round(seconds, 2), @@ -66,18 +85,18 @@ def process_response(self, request, response): "method": request.method, "data": data_string, } - pipe.lpush(name, base64.b64encode(pickle.dumps(data)).decode('utf-8')) - pipe.expire(name, 60*60*12) # 12 hours + pipe.lpush(name, base64.b64encode(pickle.dumps(data)).decode("utf-8")) + pipe.expire(name, 60 * 60 * 12) # 12 hours pipe.execute() - + return response - + def color_db(self, seconds, default): color = default - if seconds >= .25: - color = '~SB~FR' - elif seconds > .1: - color = '~FW' + if seconds >= 0.25: + color = "~SB~FR" + elif seconds > 0.1: + color = "~FW" # elif seconds == 0: # color = '~FK~SB' return color @@ -87,11 +106,11 @@ def __init__(self, get_response=None): def __call__(self, request): response = None - if hasattr(self, 'process_request'): + if hasattr(self, "process_request"): response = self.process_request(request) if not response: response = self.get_response(request) - if hasattr(self, 'process_response'): + if hasattr(self, "process_response"): response = self.process_response(request, response) return response diff --git a/utils/rtail.py b/utils/rtail.py index c2f5854b1a..07906ce2d8 100755 --- a/utils/rtail.py +++ b/utils/rtail.py @@ -24,7 +24,9 @@ def main(): # this is a remote location hostname, path = arg.split(":", 1) if options.identity: - s = subprocess.Popen(["ssh", "-i", options.identity, hostname, "tail -f " + path], stdout=subprocess.PIPE) + s = subprocess.Popen( + ["ssh", "-i", options.identity, hostname, "tail -f " + path], stdout=subprocess.PIPE + ) else: s = subprocess.Popen(["ssh", hostname, "tail -f " + path], stdout=subprocess.PIPE) s.name = arg @@ -36,8 +38,7 @@ def main(): try: while True: - r, _, _ = select.select( - [stream.stdout.fileno() for stream in streams], [], []) + r, _, _ = select.select([stream.stdout.fileno() for stream in streams], [], []) for fileno in r: for stream in streams: if stream.stdout.fileno() != fileno: @@ -46,12 +47,13 @@ def main(): if not data: streams.remove(stream) break - host = re.match(r'^(.*?)\.', stream.name) + host = re.match(r"^(.*?)\.", stream.name) combination_message = "[%-6s] %s" % (host.group(1)[:6], data) sys.stdout.write(combination_message) break except KeyboardInterrupt: print(" --- End of Logging ---") + if __name__ == "__main__": main() diff --git a/utils/s3_utils.py b/utils/s3_utils.py index d045a37210..43a9d7bd0e 100644 --- a/utils/s3_utils.py +++ b/utils/s3_utils.py @@ -1,22 +1,22 @@ +import mimetypes import os import sys import time -import mimetypes + from utils.image_functions import ImageOps -if '/srv/newsblur' not in ' '.join(sys.path): +if "/srv/newsblur" not in " ".join(sys.path): sys.path.append("/srv/newsblur") -os.environ['DJANGO_SETTINGS_MODULE'] = 'newsblur_web.settings' +os.environ["DJANGO_SETTINGS_MODULE"] = "newsblur_web.settings" from django.conf import settings -ACCESS_KEY = settings.S3_ACCESS_KEY -SECRET = settings.S3_SECRET +ACCESS_KEY = settings.S3_ACCESS_KEY +SECRET = settings.S3_SECRET BUCKET_NAME = settings.S3_BACKUP_BUCKET # Note that you need to create this bucket first class S3Store: - def __init__(self, bucket_name=settings.S3_AVATARS_BUCKET_NAME): # if settings.DEBUG: # import ssl @@ -31,51 +31,47 @@ def __init__(self, bucket_name=settings.S3_AVATARS_BUCKET_NAME): # ssl._create_default_https_context = _create_unverified_https_context self.bucket_name = bucket_name self.s3 = settings.S3_CONN - + def create_bucket(self, bucket_name): return self.s3.create_bucket(Bucket=bucket_name) - + def save_profile_picture(self, user_id, filename, image_body): content_type, extension = self._extract_content_type(filename) if not content_type or not extension: return - - image_name = 'profile_%s.%s' % (int(time.time()), extension) - - image = ImageOps.resize_image(image_body, 'fullsize', fit_to_size=False) + + image_name = "profile_%s.%s" % (int(time.time()), extension) + + image = ImageOps.resize_image(image_body, "fullsize", fit_to_size=False) if image: - key = 'avatars/%s/large_%s' % (user_id, image_name) + key = "avatars/%s/large_%s" % (user_id, image_name) self._save_object(key, image, content_type=content_type) - image = ImageOps.resize_image(image_body, 'thumbnail', fit_to_size=True) + image = ImageOps.resize_image(image_body, "thumbnail", fit_to_size=True) if image: - key = 'avatars/%s/thumbnail_%s' % (user_id, image_name) + key = "avatars/%s/thumbnail_%s" % (user_id, image_name) self._save_object(key, image, content_type=content_type) - + return image and image_name def _extract_content_type(self, filename): content_type = mimetypes.guess_type(filename)[0] extension = None - - if content_type == 'image/jpeg': - extension = 'jpg' - elif content_type == 'image/png': - extension = 'png' - elif content_type == 'image/gif': - extension = 'gif' - + + if content_type == "image/jpeg": + extension = "jpg" + elif content_type == "image/png": + extension = "png" + elif content_type == "image/gif": + extension = "gif" + return content_type, extension - + def _save_object(self, key, file_object, content_type=None): file_object.seek(0) s3_object = self.s3.Object(bucket_name=self.bucket_name, key=key) if content_type: - s3_object.put(Body=file_object, - ContentType=content_type, - ACL='public-read' - ) + s3_object.put(Body=file_object, ContentType=content_type, ACL="public-read") else: s3_object.put(Body=file_object) - diff --git a/utils/scrubber/__init__.py b/utils/scrubber/__init__.py index 7d38d82a12..d60dbc2f27 100755 --- a/utils/scrubber/__init__.py +++ b/utils/scrubber/__init__.py @@ -9,13 +9,16 @@ __author__ = "Samuel Stauffer " __version__ = "1.6.1" __license__ = "BSD" -__all__ = ['Scrubber', 'SelectiveScriptScrubber', 'ScrubberWarning', 'UnapprovedJavascript', 'urlize'] +__all__ = ["Scrubber", "SelectiveScriptScrubber", "ScrubberWarning", "UnapprovedJavascript", "urlize"] -import re, string -from urllib.parse import urljoin +import re +import string from itertools import chain +from urllib.parse import urljoin + from bs4 import BeautifulSoup, Comment + def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): """Converts any URLs in text into clickable links. @@ -30,42 +33,59 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): *Modified from Django* """ from urllib.parse import quote as urlquote - - LEADING_PUNCTUATION = ['(', '<', '<'] - TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>'] - - word_split_re = re.compile(r'([\s\xa0]+| )') # a0 == NBSP - punctuation_re = re.compile('^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$' % \ - ('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]), - '|'.join([re.escape(x) for x in TRAILING_PUNCTUATION]))) - simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') + + LEADING_PUNCTUATION = ["(", "<", "<"] + TRAILING_PUNCTUATION = [".", ",", ")", ">", "\n", ">"] + + word_split_re = re.compile(r"([\s\xa0]+| )") # a0 == NBSP + punctuation_re = re.compile( + "^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$" + % ( + "|".join([re.escape(x) for x in LEADING_PUNCTUATION]), + "|".join([re.escape(x) for x in TRAILING_PUNCTUATION]), + ) + ) + simple_email_re = re.compile(r"^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$") # del x # Temporary variable def escape(html): - return html.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') - - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + return ( + html.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + .replace("'", "'") + ) + + trim_url = ( + lambda x, limit=trim_url_limit: limit is not None + and (len(x) > limit and ("%s..." % x[: max(0, limit - 3)])) + or x + ) words = word_split_re.split(text) - nofollow_attr = nofollow and ' rel="nofollow"' or '' + nofollow_attr = nofollow and ' rel="nofollow"' or "" for i, word in enumerate(words): match = None - if '.' in word or '@' in word or ':' in word: - match = punctuation_re.match(word.replace('\u2019', "'")) + if "." in word or "@" in word or ":" in word: + match = punctuation_re.match(word.replace("\u2019", "'")) if match: lead, middle, trail = match.groups() - middle = middle.encode('utf-8') - middle = middle.decode('utf-8') # Bytes to str + middle = middle.encode("utf-8") + middle = middle.decode("utf-8") # Bytes to str # Make URL we want to point to. url = None - if middle.startswith('http://') or middle.startswith('https://'): - url = urlquote(middle, safe='%/&=:;#?+*') - elif middle.startswith('www.') or ('@' not in middle and \ - middle and middle[0] in string.ascii_letters + string.digits and \ - (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))): - url = urlquote('http://%s' % middle, safe='%/&=:;#?+*') - elif '@' in middle and not ':' in middle and simple_email_re.match(middle): - url = 'mailto:%s' % middle - nofollow_attr = '' + if middle.startswith("http://") or middle.startswith("https://"): + url = urlquote(middle, safe="%/&=:;#?+*") + elif middle.startswith("www.") or ( + "@" not in middle + and middle + and middle[0] in string.ascii_letters + string.digits + and (middle.endswith(".org") or middle.endswith(".net") or middle.endswith(".com")) + ): + url = urlquote("http://%s" % middle, safe="%/&=:;#?+*") + elif "@" in middle and not ":" in middle and simple_email_re.match(middle): + url = "mailto:%s" % middle + nofollow_attr = "" # Make link. if url: trimmed = trim_url(middle) @@ -73,40 +93,117 @@ def escape(html): lead, trail = escape(lead), escape(trail) url, trimmed = escape(url), escape(trimmed) middle = '%s' % (url, nofollow_attr, trimmed) - words[i] = '%s%s%s' % (lead, middle, trail) + words[i] = "%s%s%s" % (lead, middle, trail) elif autoescape: words[i] = escape(word) elif autoescape: words[i] = escape(word) return "".join(words) - + + class ScrubberWarning(object): pass + class Scrubber(object): - allowed_tags = set(( - 'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'blockquote', 'br', - 'center', 'cite', 'code', - 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'font', - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', - 'kbd', 'li', 'object', 'ol', 'param', 'pre', 'p', 'q', - 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', - 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'tt', 'ul', 'u', - 'var', 'wbr', - )) - disallowed_tags_save_content = set(( - 'blink', 'body', 'html', - )) - allowed_attributes = set(( - 'align', 'alt', 'border', 'cite', 'class', 'dir', - 'height', 'href', 'src', 'style', 'title', 'type', 'width', - 'face', 'size', # font tags - 'flashvars', # Not sure about flashvars - if any harm can come from it - 'classid', # FF needs the classid on object tags for flash - 'name', 'value', 'quality', 'data', 'scale', # for flash embed param tags, could limit to just param if this is harmful - 'salign', 'align', 'wmode', - )) # Bad attributes: 'allowscriptaccess', 'xmlns', 'target' - normalized_tag_replacements = {'b': 'strong', 'i': 'em'} + allowed_tags = set( + ( + "a", + "abbr", + "acronym", + "b", + "bdo", + "big", + "blockquote", + "br", + "center", + "cite", + "code", + "dd", + "del", + "dfn", + "div", + "dl", + "dt", + "em", + "embed", + "font", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "i", + "img", + "ins", + "kbd", + "li", + "object", + "ol", + "param", + "pre", + "p", + "q", + "s", + "samp", + "small", + "span", + "strike", + "strong", + "sub", + "sup", + "table", + "tbody", + "td", + "th", + "thead", + "tr", + "tt", + "ul", + "u", + "var", + "wbr", + ) + ) + disallowed_tags_save_content = set( + ( + "blink", + "body", + "html", + ) + ) + allowed_attributes = set( + ( + "align", + "alt", + "border", + "cite", + "class", + "dir", + "height", + "href", + "src", + "style", + "title", + "type", + "width", + "face", + "size", # font tags + "flashvars", # Not sure about flashvars - if any harm can come from it + "classid", # FF needs the classid on object tags for flash + "name", + "value", + "quality", + "data", + "scale", # for flash embed param tags, could limit to just param if this is harmful + "salign", + "align", + "wmode", + ) + ) # Bad attributes: 'allowscriptaccess', 'xmlns', 'target' + normalized_tag_replacements = {"b": "strong", "i": "em"} def __init__(self, base_url=None, autolink=True, nofollow=True, remove_comments=True): self.base_url = base_url @@ -122,11 +219,12 @@ def __init__(self, base_url=None, autolink=True, nofollow=True, remove_comments= # Find all _scrub_tab_ methods self.tag_scrubbers = {} for k in chain(*[cls.__dict__ for cls in self.__class__.__mro__]): - if k.startswith('_scrub_tag_'): + if k.startswith("_scrub_tag_"): self.tag_scrubbers[k[11:]] = [getattr(self, k)] def autolink_soup(self, soup): """Autolink urls in text nodes that aren't already linked (inside anchor tags).""" + def _autolink(node): if isinstance(node, str): text = node @@ -139,6 +237,7 @@ def _autolink(node): for child in node.contents: _autolink(child) + _autolink(soup) def strip_disallowed(self, soup): @@ -159,7 +258,7 @@ def strip_disallowed(self, soup): # Remove disallowed attributes attrs = {} - if hasattr(node, 'attrs') and isinstance(node.attrs, dict): + if hasattr(node, "attrs") and isinstance(node.attrs, dict): for k, v in list(node.attrs.items()): if not v: continue @@ -170,7 +269,7 @@ def strip_disallowed(self, soup): # TODO: This probably needs to be more robust if isinstance(v, str): v2 = v.lower() - if any(x in v2 for x in ('javascript:', 'vbscript:', 'expression(')): + if any(x in v2 for x in ("javascript:", "vbscript:", "expression(")): continue attrs[k] = v @@ -190,46 +289,48 @@ def _remove_nodes(self, nodes): for keep_contentes, node in nodes: if keep_contentes and node.contents: idx = node.parent.contents.index(node) - for n in reversed(list(node.contents)): # Copy the contents list to avoid modifying while traversing + for n in reversed( + list(node.contents) + ): # Copy the contents list to avoid modifying while traversing node.parent.insert(idx, n) node.extract() def _clean_path(self, node, attrname): url = node.get(attrname) - if url and '://' not in url and not url.startswith('mailto:'): + if url and "://" not in url and not url.startswith("mailto:"): print(url) - if url[0] not in ('/', '.') and not self.base_url: + if url[0] not in ("/", ".") and not self.base_url: node[attrname] = "http://" + url - elif not url.startswith('http') and self.base_url: + elif not url.startswith("http") and self.base_url: print(self.base_url) node[attrname] = urljoin(self.base_url, url) def _scrub_tag_a(self, a): if self.nofollow: - a['rel'] = ["nofollow"] + a["rel"] = ["nofollow"] - if not a.get('class', None): - a['class'] = ["external"] + if not a.get("class", None): + a["class"] = ["external"] - self._clean_path(a, 'href') + self._clean_path(a, "href") def _scrub_tag_img(self, img): try: - if img['src'].lower().startswith('chrome://'): + if img["src"].lower().startswith("chrome://"): return True except KeyError: return True # Make sure images always have an 'alt' attribute - img['alt'] = img.get('alt', '') + img["alt"] = img.get("alt", "") - self._clean_path(img, 'src') + self._clean_path(img, "src") def _scrub_tag_font(self, node): attrs = {} - if hasattr(node, 'attrs') and isinstance(node.attrs, dict): + if hasattr(node, "attrs") and isinstance(node.attrs, dict): for k, v in list(node.attrs.items()): - if k.lower() == 'size' and v.startswith('+'): + if k.lower() == "size" and v.startswith("+"): # Remove "size=+0" continue attrs[k] = v @@ -277,49 +378,59 @@ def scrub(self, html): html = str(soup) return self._scrub_html_post(html) + class UnapprovedJavascript(ScrubberWarning): def __init__(self, src): self.src = src - self.path = src[:src.rfind('/')] + self.path = src[: src.rfind("/")] + class SelectiveScriptScrubber(Scrubber): - allowed_tags = Scrubber.allowed_tags | set(('script', 'noscript', 'iframe')) - allowed_attributes = Scrubber.allowed_attributes | set(('scrolling', 'frameborder')) + allowed_tags = Scrubber.allowed_tags | set(("script", "noscript", "iframe")) + allowed_attributes = Scrubber.allowed_attributes | set(("scrolling", "frameborder")) def __init__(self, *args, **kwargs): super(SelectiveScriptScrubber, self).__init__(*args, **kwargs) - self.allowed_script_srcs = set(( - 'http://www.statcounter.com/counter/counter_xhtml.js', - # 'http://www.google-analytics.com/urchin.js', - 'http://pub.mybloglog.com/', - 'http://rpc.bloglines.com/blogroll', - 'http://widget.blogrush.com/show.js', - 'http://re.adroll.com/', - 'http://widgetserver.com/', - 'http://pagead2.googlesyndication.com/pagead/show_ads.js', # are there pageadX for all kinds of numbers? - )) - - self.allowed_script_line_res = set(re.compile(text) for text in ( - r"^(var )?sc_project\=\d+;$", - r"^(var )?sc_invisible\=\d;$", - r"^(var )?sc_partition\=\d+;$", - r'^(var )?sc_security\="[A-Za-z0-9]+";$', - # """^_uacct \= "[^"]+";$""", - # """^urchinTracker\(\);$""", - r'^blogrush_feed = "[^"]+";$', - # """^!--$""", - # """^//-->$""", - )) - - self.allowed_iframe_srcs = set(re.compile(text) for text in ( - r'^http://www\.google\.com/calendar/embed\?[\w&;=\%]+$', # Google Calendar - r'^https?://www\.youtube\.com/', # YouTube - r'^http://player\.vimeo\.com/', # Vimeo - )) + self.allowed_script_srcs = set( + ( + "http://www.statcounter.com/counter/counter_xhtml.js", + # 'http://www.google-analytics.com/urchin.js', + "http://pub.mybloglog.com/", + "http://rpc.bloglines.com/blogroll", + "http://widget.blogrush.com/show.js", + "http://re.adroll.com/", + "http://widgetserver.com/", + "http://pagead2.googlesyndication.com/pagead/show_ads.js", # are there pageadX for all kinds of numbers? + ) + ) + + self.allowed_script_line_res = set( + re.compile(text) + for text in ( + r"^(var )?sc_project\=\d+;$", + r"^(var )?sc_invisible\=\d;$", + r"^(var )?sc_partition\=\d+;$", + r'^(var )?sc_security\="[A-Za-z0-9]+";$', + # """^_uacct \= "[^"]+";$""", + # """^urchinTracker\(\);$""", + r'^blogrush_feed = "[^"]+";$', + # """^!--$""", + # """^//-->$""", + ) + ) + + self.allowed_iframe_srcs = set( + re.compile(text) + for text in ( + r"^http://www\.google\.com/calendar/embed\?[\w&;=\%]+$", # Google Calendar + r"^https?://www\.youtube\.com/", # YouTube + r"^http://player\.vimeo\.com/", # Vimeo + ) + ) def _scrub_tag_script(self, script): - src = script.get('src', None) + src = script.get("src", None) if src: for asrc in self.allowed_script_srcs: # TODO: It could be dangerous to only check "start" of string @@ -330,7 +441,7 @@ def _scrub_tag_script(self, script): else: self.warnings.append(UnapprovedJavascript(src)) script.extract() - elif script.get('type', '') != 'text/javascript': + elif script.get("type", "") != "text/javascript": script.extract() else: for line in script.string.splitlines(): @@ -345,6 +456,6 @@ def _scrub_tag_script(self, script): break def _scrub_tag_iframe(self, iframe): - src = iframe.get('src', None) + src = iframe.get("src", None) if not src or not any(asrc.match(src) for asrc in self.allowed_iframe_srcs): iframe.extract() diff --git a/utils/story_functions.py b/utils/story_functions.py index 57172d429d..171a2301a9 100644 --- a/utils/story_functions.py +++ b/utils/story_functions.py @@ -1,54 +1,61 @@ -import re +import base64 import datetime -import struct -import dateutil import hashlib -import base64 +import hmac import html +import re +import struct import sys -from random import randint -from lxml.html.diff import tokenize, fixup_ins_del_tags, htmldiff_tokens -from lxml.etree import ParserError, XMLSyntaxError, SerialisationError -import lxml.html, lxml.etree -from lxml.html.clean import Cleaner +from binascii import hexlify +from hashlib import sha1 from itertools import chain +from random import randint + +import dateutil +import feedparser +import lxml.etree +import lxml.html from django.utils.dateformat import DateFormat from django.utils.html import strip_tags as strip_tags_django +from lxml.etree import ParserError, SerialisationError, XMLSyntaxError +from lxml.html.clean import Cleaner +from lxml.html.diff import fixup_ins_del_tags, htmldiff_tokens, tokenize + from utils.tornado_escape import linkify as linkify_tornado from utils.tornado_escape import xhtml_unescape as xhtml_unescape_tornado -import feedparser - -import hmac -from binascii import hexlify -from hashlib import sha1 # COMMENTS_RE = re.compile('\') -COMMENTS_RE = re.compile('\ Following %s \t[%s]" % (hostname, address)) - if hostname in found: return - s = subprocess.Popen(["ssh", "-l", NEWSBLUR_USERNAME, - "-i", os.path.expanduser("/srv/secrets-newsblur/keys/docker.key"), - address, "%s %s" % (command, path)], stdout=subprocess.PIPE) + if hostname in found: + return + s = subprocess.Popen( + [ + "ssh", + "-l", + NEWSBLUR_USERNAME, + "-i", + os.path.expanduser("/srv/secrets-newsblur/keys/docker.key"), + address, + "%s %s" % (command, path), + ], + stdout=subprocess.PIPE, + ) s.name = hostname streams.append(s) found.add(hostname) + def read_streams(streams): while True: - r, _, _ = select.select( - [stream.stdout.fileno() for stream in streams], [], []) + r, _, _ = select.select([stream.stdout.fileno() for stream in streams], [], []) for fileno in r: for stream in streams: if stream.stdout.fileno() != fileno: @@ -137,11 +152,12 @@ def read_streams(streams): sys.stdout.flush() break + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Tail logs from multiple hosts.') - parser.add_argument('hostnames', help='Comma-separated list of hostnames', nargs='?') - parser.add_argument('roles', help='Comma-separated list of roles', nargs='?') - parser.add_argument('--command', help='Command to run on the remote host') - parser.add_argument('--path', help='Path to the log file') + parser = argparse.ArgumentParser(description="Tail logs from multiple hosts.") + parser.add_argument("hostnames", help="Comma-separated list of hostnames", nargs="?") + parser.add_argument("roles", help="Comma-separated list of roles", nargs="?") + parser.add_argument("--command", help="Command to run on the remote host") + parser.add_argument("--path", help="Path to the log file") args = parser.parse_args() main(args.hostnames, command=args.command, path=args.path) diff --git a/utils/tlnbt.py b/utils/tlnbt.py index 6d9f0f8f21..b494d960a5 100755 --- a/utils/tlnbt.py +++ b/utils/tlnbt.py @@ -1,11 +1,11 @@ #!/usr/bin/env python -import tlnb import sys +import tlnb + if __name__ == "__main__": role = "task" if len(sys.argv) > 1: role = sys.argv[1] tlnb.main(roles=[role]) - \ No newline at end of file diff --git a/utils/tlnbw.py b/utils/tlnbw.py index 3a6b85dec5..9e2f4793a1 100755 --- a/utils/tlnbw.py +++ b/utils/tlnbw.py @@ -1,11 +1,11 @@ #!/usr/bin/env python -import tlnb import sys +import tlnb + if __name__ == "__main__": role = "work" if len(sys.argv) > 1: role = sys.argv[1] tlnb.main(roles=[role]) - \ No newline at end of file diff --git a/utils/tornado_escape.py b/utils/tornado_escape.py index cf627a1cf6..ac64b3b58c 100644 --- a/utils/tornado_escape.py +++ b/utils/tornado_escape.py @@ -21,47 +21,51 @@ """ - import html.entities import re import sys -import urllib.parse - +import urllib.parse from urllib.parse import parse_qs + # json module is in the standard library as of python 2.6; fall back to # simplejson if present for older versions. try: import json + assert hasattr(json, "loads") and hasattr(json, "dumps") _json_decode = json.loads _json_encode = json.dumps except Exception: try: import simplejson + _json_decode = lambda s: simplejson.loads(_unicode(s)) _json_encode = lambda v: simplejson.dumps(v) except ImportError: try: # For Google AppEngine from django.utils import simplejson + _json_decode = lambda s: simplejson.loads(_unicode(s)) _json_encode = lambda v: simplejson.dumps(v) except ImportError: + def _json_decode(s): raise NotImplementedError( "A JSON parser is required, e.g., simplejson at " - "http://pypi.python.org/pypi/simplejson/") + "http://pypi.python.org/pypi/simplejson/" + ) + _json_encode = _json_decode _XHTML_ESCAPE_RE = re.compile('[&<>"]') -_XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"'} +_XHTML_ESCAPE_DICT = {"&": "&", "<": "<", ">": ">", '"': """} def xhtml_escape(value): """Escapes a string so it is valid within XML or XHTML.""" - return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)], - to_basestring(value)) + return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)], to_basestring(value)) def xhtml_unescape(value): @@ -94,11 +98,13 @@ def url_escape(value): """Returns a valid URL-encoded version of the given value.""" return urllib.parse.quote_plus(utf8(value)) + # python 3 changed things around enough that we need two separate # implementations of url_unescape. We also need our own implementation # of parse_qs since python 3's version insists on decoding everything. if sys.version_info[0] < 3: - def url_unescape(value, encoding='utf-8'): + + def url_unescape(value, encoding="utf-8"): """Decodes the given value from a URL. The argument may be either a byte or unicode string. @@ -113,7 +119,8 @@ def url_unescape(value, encoding='utf-8'): parse_qs_bytes = parse_qs else: - def url_unescape(value, encoding='utf-8'): + + def url_unescape(value, encoding="utf-8"): """Decodes the given value from a URL. The argument may be either a byte or unicode string. @@ -136,11 +143,10 @@ def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False): """ # This is gross, but python3 doesn't give us another way. # Latin1 is the universal donor of character encodings. - result = parse_qs(qs, keep_blank_values, strict_parsing, - encoding='latin1', errors='strict') + result = parse_qs(qs, keep_blank_values, strict_parsing, encoding="latin1", errors="strict") encoded = {} for k, v in result.items(): - encoded[k] = [i.encode('latin1') for i in v] + encoded[k] = [i.encode("latin1") for i in v] return encoded @@ -158,6 +164,7 @@ def utf8(value): assert isinstance(value, str) return value.encode("utf-8") + _TO_UNICODE_TYPES = (str, type(None)) @@ -172,6 +179,7 @@ def to_unicode(value): assert isinstance(value, bytes) return value.decode("utf-8") + # to_unicode was previously named _unicode not because it was private, # but to avoid conflicts with the built-in unicode() function/type _unicode = to_unicode @@ -217,16 +225,20 @@ def recursive_unicode(obj): else: return obj + # I originally used the regex from # http://daringfireball.net/2010/07/improved_regex_for_matching_urls # but it gets all exponential on certain patterns (such as too many trailing # dots), causing the regex matcher to never return. # This regex should avoid those problems. -_URL_RE = re.compile(r"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)""") +_URL_RE = re.compile( + r"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)""" +) -def linkify(text, shorten=False, extra_params="", - require_protocol=False, permitted_protocols=["http", "https"]): +def linkify( + text, shorten=False, extra_params="", require_protocol=False, permitted_protocols=["http", "https"] +): """Converts plain text into HTML with links. For example: ``linkify("Hello http://tornadoweb.org!")`` would return @@ -269,7 +281,7 @@ def make_link(m): href = m.group(1) if not proto: - href = "http://" + href # no proto specified, use http + href = "http://" + href # no proto specified, use http if callable(extra_params): params = " " + extra_params(href).strip() @@ -291,14 +303,13 @@ def make_link(m): # The path is usually not that interesting once shortened # (no more slug, etc), so it really just provides a little # extra indication of shortening. - url = url[:proto_len] + parts[0] + "/" + \ - parts[1][:8].split('?')[0].split('.')[0] + url = url[:proto_len] + parts[0] + "/" + parts[1][:8].split("?")[0].split(".")[0] if len(url) > max_len * 1.5: # still too long url = url[:max_len] if url != before_clip: - amp = url.rfind('&') + amp = url.rfind("&") # avoid splitting html char entities if amp > max_len - 5: url = url[:amp] @@ -338,4 +349,5 @@ def _build_unicode_map(): unicode_map[name] = chr(value) return unicode_map + _HTML_UNICODE_MAP = _build_unicode_map() diff --git a/utils/twitter_fetcher.py b/utils/twitter_fetcher.py index 356a3eb998..5f0ba41b59 100644 --- a/utils/twitter_fetcher.py +++ b/utils/twitter_fetcher.py @@ -1,26 +1,27 @@ -import re import datetime +import re +from urllib.parse import parse_qs, urlparse -from jmespath import search -from urllib.parse import urlparse, parse_qs -import tweepy import dateutil.parser -from qurl import qurl +import tweepy from django.conf import settings from django.utils import feedgenerator -from django.utils.html import linebreaks from django.utils.dateformat import DateFormat -from apps.social.models import MSocialServices +from django.utils.html import linebreaks +from jmespath import search +from qurl import qurl + from apps.reader.models import UserSubscription +from apps.social.models import MSocialServices from utils import log as logging + class TwitterFetcher: - def __init__(self, feed, options=None): self.feed = feed self.address = self.feed.feed_address self.options = options or {} - + def fetch(self, address=None): data = {} if not address: @@ -28,133 +29,144 @@ def fetch(self, address=None): self.address = address twitter_user = None - if '/lists/' in address: + if "/lists/" in address: list_id = self.extract_list_id() if not list_id: return - + tweets, list_info = self.fetch_list_timeline(list_id) if not tweets: return - - data['title'] = "%s on Twitter" % list_info.full_name - data['link'] = "https://twitter.com%s" % list_info.uri - data['description'] = "%s on Twitter" % list_info.full_name - elif '/search' in address: + + data["title"] = "%s on Twitter" % list_info.full_name + data["link"] = "https://twitter.com%s" % list_info.uri + data["description"] = "%s on Twitter" % list_info.full_name + elif "/search" in address: search_query = self.extract_search_query() if not search_query: return - + tweets = self.fetch_search_query(search_query) if not tweets: return - - data['title'] = "\"%s\" on Twitter" % search_query - data['link'] = "%s" % address - data['description'] = "Searching \"%s\" on Twitter" % search_query + + data["title"] = '"%s" on Twitter' % search_query + data["link"] = "%s" % address + data["description"] = 'Searching "%s" on Twitter' % search_query else: username = self.extract_username() if not username: - logging.debug(u' ***> [%-30s] ~FRTwitter fetch failed: %s: No active user API access' % - (self.feed.log_title[:30], self.address)) + logging.debug( + " ***> [%-30s] ~FRTwitter fetch failed: %s: No active user API access" + % (self.feed.log_title[:30], self.address) + ) return - + twitter_user = self.fetch_user(username) if not twitter_user: return tweets = self.user_timeline(twitter_user) - - data['title'] = "%s on Twitter" % username - data['link'] = "https://twitter.com/%s" % username - data['description'] = "%s on Twitter" % username - - data['lastBuildDate'] = datetime.datetime.utcnow() - data['generator'] = 'NewsBlur Twitter API Decrapifier - %s' % settings.NEWSBLUR_URL - data['docs'] = None - data['feed_url'] = address + + data["title"] = "%s on Twitter" % username + data["link"] = "https://twitter.com/%s" % username + data["description"] = "%s on Twitter" % username + + data["lastBuildDate"] = datetime.datetime.utcnow() + data["generator"] = "NewsBlur Twitter API Decrapifier - %s" % settings.NEWSBLUR_URL + data["docs"] = None + data["feed_url"] = address rss = feedgenerator.Atom1Feed(**data) - + for tweet in tweets: story_data = self.tweet_story(tweet.__dict__) rss.add_item(**story_data) - - return rss.writeString('utf-8') - + + return rss.writeString("utf-8") + def extract_username(self): username = None try: - address = qurl(self.address, remove=['_']) - username_groups = re.search('twitter.com/(\w+)/?$', address) + address = qurl(self.address, remove=["_"]) + username_groups = re.search("twitter.com/(\w+)/?$", address) if not username_groups: return username = username_groups.group(1) except IndexError: return - + return username def extract_list_id(self): list_id = None try: - list_groups = re.search('twitter.com/i/lists/(\w+)/?', self.address) + list_groups = re.search("twitter.com/i/lists/(\w+)/?", self.address) if not list_groups: return list_id = list_groups.group(1) except IndexError: return - + return list_id def extract_search_query(self): search_query = None - address = qurl(self.address, remove=['_']) + address = qurl(self.address, remove=["_"]) query = urlparse(address).query query_dict = parse_qs(query) - if 'q' in query_dict: - search_query = query_dict['q'][0] - + if "q" in query_dict: + search_query = query_dict["q"][0] + return search_query def twitter_api(self, include_social_services=False): twitter_api = None social_services = None - if self.options.get('requesting_user_id', None): - social_services = MSocialServices.get_user(self.options.get('requesting_user_id')) + if self.options.get("requesting_user_id", None): + social_services = MSocialServices.get_user(self.options.get("requesting_user_id")) try: twitter_api = social_services.twitter_api() except tweepy.error.TweepError as e: - logging.debug(' ***> [%-30s] ~FRTwitter fetch failed: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + logging.debug( + " ***> [%-30s] ~FRTwitter fetch failed: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) return else: usersubs = UserSubscription.objects.filter(feed=self.feed) if not usersubs: - logging.debug(' ***> [%-30s] ~FRTwitter fetch failed: %s: No subscriptions' % - (self.feed.log_title[:30], self.address)) + logging.debug( + " ***> [%-30s] ~FRTwitter fetch failed: %s: No subscriptions" + % (self.feed.log_title[:30], self.address) + ) return for sub in usersubs: social_services = MSocialServices.get_user(sub.user_id) - if not social_services.twitter_uid: continue + if not social_services.twitter_uid: + continue try: twitter_api = social_services.twitter_api() - if not twitter_api: + if not twitter_api: continue else: break except tweepy.error.TweepError as e: - logging.debug(' ***> [%-30s] ~FRTwitter fetch failed: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + logging.debug( + " ***> [%-30s] ~FRTwitter fetch failed: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) continue - + if not twitter_api: - logging.debug(' ***> [%-30s] ~FRTwitter fetch failed: %s: No twitter API for %s' % - (self.feed.log_title[:30], self.address, usersubs[0].user.username)) + logging.debug( + " ***> [%-30s] ~FRTwitter fetch failed: %s: No twitter API for %s" + % (self.feed.log_title[:30], self.address, usersubs[0].user.username) + ) return - + if include_social_services: return twitter_api, social_services return twitter_api - + def disconnect_twitter(self): _, social_services = self.twitter_api(include_social_services=True) social_services.disconnect_twitter() @@ -163,298 +175,364 @@ def fetch_user(self, username): twitter_api = self.twitter_api() if not twitter_api: return - + try: twitter_user = twitter_api.get_user(username) except TypeError as e: - logging.debug(' ***> [%-30s] ~FRTwitter fetch failed, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + logging.debug( + " ***> [%-30s] ~FRTwitter fetch failed, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(560, "Twitter Error: %s" % (e)) return except tweepy.error.TweepError as e: message = str(e).lower() - if 'suspended' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + if "suspended" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(562, "Twitter Error: User suspended") # self.disconnect_twitter() return - elif 'expired token' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "expired token" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(563, "Twitter Error: Expired token") self.disconnect_twitter() return - elif 'not found' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "not found" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(564, "Twitter Error: User not found") return - elif 'not authenticate you' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, (not) disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "not authenticate you" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, (not) disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(565, "Twitter Error: API not authorized") return - elif 'over capacity' in message or 'Max retries' in message: - logging.debug(' ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "over capacity" in message or "Max retries" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(460, "Twitter Error: Over capacity") return - elif '503' in message: - logging.debug(' ***> [%-30s] ~FRTwitter throwing a 503, ignoring... %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "503" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter throwing a 503, ignoring... %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(463, "Twitter Error: Twitter's down") return else: raise e - + return twitter_user - + def user_timeline(self, twitter_user): try: - tweets = twitter_user.timeline(tweet_mode='extended') + tweets = twitter_user.timeline(tweet_mode="extended") except tweepy.error.TweepError as e: message = str(e).lower() - if 'not authorized' in message: - logging.debug(' ***> [%-30s] ~FRTwitter timeline failed, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + if "not authorized" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter timeline failed, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(565, "Twitter Error: Not authorized") return [] - elif 'user not found' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "user not found" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(566, "Twitter Error: User not found") return [] - elif '429' in message: - logging.debug(' ***> [%-30s] ~FRTwitter rate limited: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "429" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter rate limited: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(567, "Twitter Error: Rate limited") return [] - elif 'blocked from viewing' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user blocked, ignoring: %s' % - (self.feed.log_title[:30], e)) + elif "blocked from viewing" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user blocked, ignoring: %s" % (self.feed.log_title[:30], e) + ) self.feed.save_feed_history(568, "Twitter Error: Blocked from viewing") return [] - elif 'over capacity' in message: - logging.debug(u' ***> [%-30s] ~FRTwitter over capacity, ignoring: %s' % - (self.feed.log_title[:30], e)) + elif "over capacity" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter over capacity, ignoring: %s" % (self.feed.log_title[:30], e) + ) self.feed.save_feed_history(569, "Twitter Error: Over capacity") return [] else: raise e - + if not tweets: return [] return tweets - + def fetch_list_timeline(self, list_id): twitter_api = self.twitter_api() if not twitter_api: return None, None - + try: - list_timeline = twitter_api.list_timeline(list_id=list_id, tweet_mode='extended') + list_timeline = twitter_api.list_timeline(list_id=list_id, tweet_mode="extended") except TypeError as e: - logging.debug(' ***> [%-30s] ~FRTwitter list fetch failed, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + logging.debug( + " ***> [%-30s] ~FRTwitter list fetch failed, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(570, "Twitter Error: %s" % (e)) return None, None except tweepy.error.TweepError as e: message = str(e).lower() - if 'suspended' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + if "suspended" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(572, "Twitter Error: User suspended") # self.disconnect_twitter() return None, None - elif 'expired token' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "expired token" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(573, "Twitter Error: Expired token") self.disconnect_twitter() return None, None - elif 'not found' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "not found" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(574, "Twitter Error: User not found") return None, None - elif 'not authenticate you' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, (not) disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "not authenticate you" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, (not) disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(565, "Twitter Error: API not authorized") return None, None - elif 'over capacity' in message or 'Max retries' in message: - logging.debug(' ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "over capacity" in message or "Max retries" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(470, "Twitter Error: Over capacity") return None, None else: raise e - + list_info = twitter_api.get_list(list_id=list_id) - + if not list_timeline: return [], list_info return list_timeline, list_info - + def fetch_search_query(self, search_query): twitter_api = self.twitter_api() if not twitter_api: return None - + try: - list_timeline = twitter_api.search(search_query, tweet_mode='extended') + list_timeline = twitter_api.search(search_query, tweet_mode="extended") except TypeError as e: - logging.debug(' ***> [%-30s] ~FRTwitter list fetch failed, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + logging.debug( + " ***> [%-30s] ~FRTwitter list fetch failed, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(570, "Twitter Error: %s" % (e)) return None except tweepy.error.TweepError as e: message = str(e).lower() - if 'suspended' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + if "suspended" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(572, "Twitter Error: User suspended") # self.disconnect_twitter() return None - elif 'expired token' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "expired token" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(573, "Twitter Error: Expired token") self.disconnect_twitter() return None - elif 'not found' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "not found" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(574, "Twitter Error: User not found") return None - elif 'not authenticate you' in message: - logging.debug(' ***> [%-30s] ~FRTwitter user not found, (not) disconnecting twitter: %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "not authenticate you" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter user not found, (not) disconnecting twitter: %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(565, "Twitter Error: API not authorized") return None - elif 'over capacity' in message or 'Max retries' in message: - logging.debug(' ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s' % - (self.feed.log_title[:30], self.address, e)) + elif "over capacity" in message or "Max retries" in message: + logging.debug( + " ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s" + % (self.feed.log_title[:30], self.address, e) + ) self.feed.save_feed_history(470, "Twitter Error: Over capacity") return None else: raise e - + if not list_timeline: return [] return list_timeline - + def tweet_story(self, user_tweet): categories = set() - - if user_tweet['full_text'].startswith('RT @'): - categories.add('retweet') - elif user_tweet['in_reply_to_status_id'] or user_tweet['full_text'].startswith('@'): - categories.add('reply') + + if user_tweet["full_text"].startswith("RT @"): + categories.add("retweet") + elif user_tweet["in_reply_to_status_id"] or user_tweet["full_text"].startswith("@"): + categories.add("reply") else: - categories.add('tweet') - if user_tweet['full_text'].startswith('RT @'): - categories.add('retweet') - if user_tweet['favorite_count']: - categories.add('liked') - if user_tweet['retweet_count']: - categories.add('retweeted') - if 'http' in user_tweet['full_text']: - categories.add('link') - + categories.add("tweet") + if user_tweet["full_text"].startswith("RT @"): + categories.add("retweet") + if user_tweet["favorite_count"]: + categories.add("liked") + if user_tweet["retweet_count"]: + categories.add("retweeted") + if "http" in user_tweet["full_text"]: + categories.add("link") + story = {} content_tweet = user_tweet entities = "" - author = user_tweet.get('author') or user_tweet.get('user') - if not isinstance(author, dict): author = author.__dict__ - author_screen_name = author['screen_name'] - author_name = author['name'] - author_fullname = "%s (%s)" % (author_name, author_screen_name) if author_screen_name != author_name else author_screen_name + author = user_tweet.get("author") or user_tweet.get("user") + if not isinstance(author, dict): + author = author.__dict__ + author_screen_name = author["screen_name"] + author_name = author["name"] + author_fullname = ( + "%s (%s)" % (author_name, author_screen_name) + if author_screen_name != author_name + else author_screen_name + ) original_author_screen_name = author_screen_name - if user_tweet['in_reply_to_user_id'] == author['id']: - categories.add('reply-to-self') + if user_tweet["in_reply_to_user_id"] == author["id"]: + categories.add("reply-to-self") retweet_author = "" - tweet_link = "https://twitter.com/%s/status/%s" % (original_author_screen_name, user_tweet['id']) - if 'retweeted_status' in user_tweet: + tweet_link = "https://twitter.com/%s/status/%s" % (original_author_screen_name, user_tweet["id"]) + if "retweeted_status" in user_tweet: retweet_author = """Retweeted by %s on %s""" % ( author_screen_name, - author['profile_image_url_https'], + author["profile_image_url_https"], author_screen_name, author_fullname, - DateFormat(user_tweet['created_at']).format('l, F jS, Y g:ia').replace('.',''), - ) - content_tweet = user_tweet['retweeted_status'].__dict__ - author = content_tweet['author'] - if not isinstance(author, dict): author = author.__dict__ - author_screen_name = author['screen_name'] - author_name = author['name'] - author_fullname = "%s (%s)" % (author_name, author_screen_name) if author_screen_name != author_name else author_screen_name - tweet_link = "https://twitter.com/%s/status/%s" % (author_screen_name, user_tweet['retweeted_status'].id) - - tweet_title = user_tweet['full_text'] - tweet_text = linebreaks(content_tweet['full_text']) - + DateFormat(user_tweet["created_at"]).format("l, F jS, Y g:ia").replace(".", ""), + ) + content_tweet = user_tweet["retweeted_status"].__dict__ + author = content_tweet["author"] + if not isinstance(author, dict): + author = author.__dict__ + author_screen_name = author["screen_name"] + author_name = author["name"] + author_fullname = ( + "%s (%s)" % (author_name, author_screen_name) + if author_screen_name != author_name + else author_screen_name + ) + tweet_link = "https://twitter.com/%s/status/%s" % ( + author_screen_name, + user_tweet["retweeted_status"].id, + ) + + tweet_title = user_tweet["full_text"] + tweet_text = linebreaks(content_tweet["full_text"]) + replaced = {} - entities_media = content_tweet['entities'].get('media', []) - if 'extended_entities' in content_tweet: - entities_media = content_tweet['extended_entities'].get('media', []) + entities_media = content_tweet["entities"].get("media", []) + if "extended_entities" in content_tweet: + entities_media = content_tweet["extended_entities"].get("media", []) for media in entities_media: - if 'media_url_https' not in media: continue - if media['type'] == 'photo': - if media.get('url') and media['url'] in tweet_text: - tweet_title = tweet_title.replace(media['url'], media['display_url']) - replacement = "%s" % (media['expanded_url'], media['display_url']) - if not replaced.get(media['url']): - tweet_text = tweet_text.replace(media['url'], replacement) - replaced[media['url']] = True - entities += "
" % media['media_url_https'] - categories.add('photo') - if media['type'] == 'video' or media['type'] == 'animated_gif': - if media.get('url') and media['url'] in tweet_text: - tweet_title = tweet_title.replace(media['url'], media['display_url']) - replacement = "%s" % (media['expanded_url'], media['display_url']) - if not replaced.get(media['url']): - tweet_text = tweet_text.replace(media['url'], replacement) - replaced[media['url']] = True + if "media_url_https" not in media: + continue + if media["type"] == "photo": + if media.get("url") and media["url"] in tweet_text: + tweet_title = tweet_title.replace(media["url"], media["display_url"]) + replacement = '%s' % (media["expanded_url"], media["display_url"]) + if not replaced.get(media["url"]): + tweet_text = tweet_text.replace(media["url"], replacement) + replaced[media["url"]] = True + entities += '
' % media["media_url_https"] + categories.add("photo") + if media["type"] == "video" or media["type"] == "animated_gif": + if media.get("url") and media["url"] in tweet_text: + tweet_title = tweet_title.replace(media["url"], media["display_url"]) + replacement = '%s' % (media["expanded_url"], media["display_url"]) + if not replaced.get(media["url"]): + tweet_text = tweet_text.replace(media["url"], replacement) + replaced[media["url"]] = True bitrate = 0 chosen_variant = None - for variant in media['video_info']['variants']: + for variant in media["video_info"]["variants"]: if not chosen_variant: chosen_variant = variant - if variant.get('bitrate', 0) > bitrate: - bitrate = variant['bitrate'] + if variant.get("bitrate", 0) > bitrate: + bitrate = variant["bitrate"] chosen_variant = variant if chosen_variant: - entities += "