remarkbox 0.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- remarkbox/__init__.py +556 -0
- remarkbox/lib/__init__.py +36 -0
- remarkbox/lib/mail.py +221 -0
- remarkbox/lib/mail_messages.py +117 -0
- remarkbox/lib/notify.py +260 -0
- remarkbox/lib/render.py +60 -0
- remarkbox/lib/sanitize_html.py +227 -0
- remarkbox/lib/svgtar/__init__.py +54 -0
- remarkbox/lib/svgtar/svg.py +46 -0
- remarkbox/models/__init__.py +88 -0
- remarkbox/models/event.py +41 -0
- remarkbox/models/meta.py +156 -0
- remarkbox/models/namespace.py +428 -0
- remarkbox/models/namespace_request.py +214 -0
- remarkbox/models/namespace_user.py +44 -0
- remarkbox/models/node.py +674 -0
- remarkbox/models/node_cache.py +88 -0
- remarkbox/models/notification.py +97 -0
- remarkbox/models/oauth.py +64 -0
- remarkbox/models/pay_what_you_can.py +39 -0
- remarkbox/models/uri.py +139 -0
- remarkbox/models/user.py +441 -0
- remarkbox/models/vote.py +30 -0
- remarkbox/models/watcher.py +80 -0
- remarkbox/routes.py +131 -0
- remarkbox/scripts/__init__.py +7 -0
- remarkbox/scripts/alembic/env.py +73 -0
- remarkbox/scripts/alembic/script.py.mako +29 -0
- remarkbox/scripts/alembic/versions/00211ed642cc_add_ip_address_column_to_rb_node.py +24 -0
- remarkbox/scripts/alembic/versions/0208c10ea67f_create_new_node_cache_table.py +57 -0
- remarkbox/scripts/alembic/versions/04e52337756e_autogenerated_indicies.py +38 -0
- remarkbox/scripts/alembic/versions/051f8b810f26_namespace_ignore_query_string.py +26 -0
- remarkbox/scripts/alembic/versions/1040ee4836af_namespace_description.py +26 -0
- remarkbox/scripts/alembic/versions/14a6a35940c7_new_field_for_namespace_request_cache.py +33 -0
- remarkbox/scripts/alembic/versions/184a349cd589_uri_accessed_timestamp.py +26 -0
- remarkbox/scripts/alembic/versions/256468780798_root_nodes_to_namespace_.py +55 -0
- remarkbox/scripts/alembic/versions/2e00b92495e4_node_disabled_timestamp.py +42 -0
- remarkbox/scripts/alembic/versions/3ee195d142fb_add_subscription_type_to_namespace.py +32 -0
- remarkbox/scripts/alembic/versions/40ded1163f95_add_user_surrogate_columns.py +25 -0
- remarkbox/scripts/alembic/versions/43c1e5a99d4e_new_namespace_column_3.py +27 -0
- remarkbox/scripts/alembic/versions/4aeda8e797ab_new_namespace_column_for_stylesheet_uris.py +32 -0
- remarkbox/scripts/alembic/versions/503a2cef2378_root_stats.py +48 -0
- remarkbox/scripts/alembic/versions/591800942adf_node_data_html_column.py +42 -0
- remarkbox/scripts/alembic/versions/59592cf325fb_stylesheet_timestamp.py +26 -0
- remarkbox/scripts/alembic/versions/5a5d0b68db78_add_root_id_field.py +40 -0
- remarkbox/scripts/alembic/versions/6a440d3921d2_added_email_id_column_to_user_table.py +48 -0
- remarkbox/scripts/alembic/versions/6bbc92ccc112_add_changed_column_to_node_table.py +31 -0
- remarkbox/scripts/alembic/versions/6db39621f46c_namespace_group_conversations.py +24 -0
- remarkbox/scripts/alembic/versions/6fc1d0cf350c_notification_columns_to_rb_user_table.py +52 -0
- remarkbox/scripts/alembic/versions/76deff88cfd6_create_graph_depth_column_on_node_model.py +31 -0
- remarkbox/scripts/alembic/versions/771ef7c868de_added_created_timestamp_column_to_.py +31 -0
- remarkbox/scripts/alembic/versions/7a3a4755b4a8_add_hide_unless_approved_column.py +30 -0
- remarkbox/scripts/alembic/versions/87fbd2b4e06e_namespace_theme_embed_column.py +26 -0
- remarkbox/scripts/alembic/versions/8f76e372a953_password_attempts_field.py +28 -0
- remarkbox/scripts/alembic/versions/953d42406dbb_namespace_public_column.py +24 -0
- remarkbox/scripts/alembic/versions/98a90d2add21_namespace_model.py +36 -0
- remarkbox/scripts/alembic/versions/98ced5f6ff36_stripe_id_for_user.py +26 -0
- remarkbox/scripts/alembic/versions/a26302bcfa25_add_created_timestamp_column_to_oauth_.py +41 -0
- remarkbox/scripts/alembic/versions/a38c12eeff2d_locked_rb_node.py +24 -0
- remarkbox/scripts/alembic/versions/a738d6859f18_new_namespace_columns_2.py +37 -0
- remarkbox/scripts/alembic/versions/aa2dfb4decda_hide_powered_by_column.py +26 -0
- remarkbox/scripts/alembic/versions/b132ec968f0c_pay_what_you_can_table.py +33 -0
- remarkbox/scripts/alembic/versions/ba8e83a3b8fd_namespace_stylesheet_embed_column.py +31 -0
- remarkbox/scripts/alembic/versions/c9793937e756_make_all_verified_namespace_request_.py +38 -0
- remarkbox/scripts/alembic/versions/c97ddfcbc406_approve_column.py +24 -0
- remarkbox/scripts/alembic/versions/cbdf3cd4ccba_avatar_size_in_namespace.py +24 -0
- remarkbox/scripts/alembic/versions/d1a0ab4f617f_add_has_uri_to_node.py +24 -0
- remarkbox/scripts/alembic/versions/f5f88293428a_add_mathjax_column_to_rb_namespace.py +26 -0
- remarkbox/scripts/alembic/versions/f7b42b2024fb_namespace_reverse_node_order.py +24 -0
- remarkbox/scripts/alembic/versions/fa8402aa1a00_add_link_protection_column_to_namespace.py +26 -0
- remarkbox/scripts/init_db.py +35 -0
- remarkbox/scripts/invalidate_node_cache.py +32 -0
- remarkbox/scripts/json_import.py +162 -0
- remarkbox/scripts/merge_dupes.py +49 -0
- remarkbox/scripts/modify_namespace.py +159 -0
- remarkbox/scripts/modify_node.py +176 -0
- remarkbox/scripts/modify_uris.py +55 -0
- remarkbox/scripts/modify_user.py +83 -0
- remarkbox/scripts/recompute_node_depths.py +21 -0
- remarkbox/scripts/safe_approve_all_nodes.py +29 -0
- remarkbox/scripts/send_node_digest_notifications.py +19 -0
- remarkbox/static/css/common.css +784 -0
- remarkbox/static/css/embed.css +38 -0
- remarkbox/static/css/pygments.css +61 -0
- remarkbox/static/favicon.ico +0 -0
- remarkbox/static/img/remarkbox-logo.png +0 -0
- remarkbox/static/img/remarkbox-minified.png +0 -0
- remarkbox/static/js/custom.js +93 -0
- remarkbox/static/js/iframe-resizer/ie8.polyfils.map +1 -0
- remarkbox/static/js/iframe-resizer/ie8.polyfils.min.js +4 -0
- remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.js +1106 -0
- remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.map +1 -0
- remarkbox/static/js/iframe-resizer/iframeResizer.contentWindow.min.js +10 -0
- remarkbox/static/js/iframe-resizer/iframeResizer.js +1040 -0
- remarkbox/static/js/iframe-resizer/iframeResizer.map +1 -0
- remarkbox/static/js/iframe-resizer/iframeResizer.min.js +9 -0
- remarkbox/static/js/iframe-resizer/index.js +2 -0
- remarkbox/static/js/jquery-2.1.3.min.js +4 -0
- remarkbox/templates/base.j2 +85 -0
- remarkbox/templates/billing.j2 +44 -0
- remarkbox/templates/dynamic-remarkbox.css.j2 +29 -0
- remarkbox/templates/edit-node.j2 +29 -0
- remarkbox/templates/embed-iframe.txt.j2 +41 -0
- remarkbox/templates/home.j2 +121 -0
- remarkbox/templates/join-or-log-in.j2 +31 -0
- remarkbox/templates/list-namespace-owners.j2 +27 -0
- remarkbox/templates/list-namespace-requests.j2 +29 -0
- remarkbox/templates/list-nodes.j2 +59 -0
- remarkbox/templates/list-notifications.j2 +45 -0
- remarkbox/templates/mail_digest_html.j2 +32 -0
- remarkbox/templates/mail_digest_text.j2 +15 -0
- remarkbox/templates/mail_immediate_html.j2 +27 -0
- remarkbox/templates/mail_immediate_text.j2 +8 -0
- remarkbox/templates/mail_snippet_html.j2 +40 -0
- remarkbox/templates/mail_snippet_text.j2 +27 -0
- remarkbox/templates/namespace-settings.j2 +269 -0
- remarkbox/templates/new.j2 +14 -0
- remarkbox/templates/rss.xml.j2 +40 -0
- remarkbox/templates/setup-namespace.j2 +165 -0
- remarkbox/templates/show-count.j2 +15 -0
- remarkbox/templates/show-node.j2 +191 -0
- remarkbox/templates/sitemap.xml.j2 +11 -0
- remarkbox/templates/snippets/add-to-slack.j2 +7 -0
- remarkbox/templates/snippets/create.j2 +29 -0
- remarkbox/templates/snippets/csrf.j2 +3 -0
- remarkbox/templates/snippets/email.j2 +31 -0
- remarkbox/templates/snippets/flash-alerts.j2 +11 -0
- remarkbox/templates/snippets/forms.j2 +100 -0
- remarkbox/templates/snippets/google-analytics.j2 +12 -0
- remarkbox/templates/snippets/javascript-includes.j2 +29 -0
- remarkbox/templates/snippets/owner-key-comment.j2 +6 -0
- remarkbox/templates/snippets/phone-menu.j2 +41 -0
- remarkbox/templates/snippets/powered-by.j2 +9 -0
- remarkbox/templates/snippets/search.j2 +7 -0
- remarkbox/templates/snippets/snippets.j2 +272 -0
- remarkbox/templates/snippets/stripe.j2 +133 -0
- remarkbox/templates/snippets/stylesheet-includes.j2 +18 -0
- remarkbox/templates/snippets/submit.j2 +1 -0
- remarkbox/templates/update-card.j2 +23 -0
- remarkbox/templates/user-settings.j2 +98 -0
- remarkbox/templates/user-watching.j2 +75 -0
- remarkbox/templates/verification-challenge.j2 +36 -0
- remarkbox/views/__init__.py +131 -0
- remarkbox/views/authenticated/__init__.py +0 -0
- remarkbox/views/authenticated/authenticated.py +403 -0
- remarkbox/views/authenticated/lock.py +95 -0
- remarkbox/views/authenticated/notifications.py +44 -0
- remarkbox/views/authenticated/oauth.py +125 -0
- remarkbox/views/authenticated/stripe.py +94 -0
- remarkbox/views/authenticated/topsecret.py +80 -0
- remarkbox/views/authenticated/watch.py +94 -0
- remarkbox/views/authentication.py +135 -0
- remarkbox/views/list_nodes.py +206 -0
- remarkbox/views/misc.py +127 -0
- remarkbox/views/modify_node.py +100 -0
- remarkbox/views/new_thread.py +93 -0
- remarkbox/views/reply_node.py +133 -0
- remarkbox-0.0.11.dist-info/METADATA +341 -0
- remarkbox-0.0.11.dist-info/RECORD +162 -0
- remarkbox-0.0.11.dist-info/WHEEL +5 -0
- remarkbox-0.0.11.dist-info/entry_points.txt +16 -0
- remarkbox-0.0.11.dist-info/top_level.txt +1 -0
remarkbox/__init__.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
from pyramid.config import Configurator
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import engine_from_config
|
|
4
|
+
|
|
5
|
+
from .models import (
|
|
6
|
+
get_user_by_id,
|
|
7
|
+
get_or_create_user_by_email,
|
|
8
|
+
get_node_by_id,
|
|
9
|
+
get_or_create_node_by_uri,
|
|
10
|
+
get_nodes_who_share_root,
|
|
11
|
+
get_node_id_map,
|
|
12
|
+
get_graph_from_nodes,
|
|
13
|
+
get_or_create_namespace,
|
|
14
|
+
get_namespace_request_by_id,
|
|
15
|
+
get_conversation_graph_from_nodes,
|
|
16
|
+
flatten_graph,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# reject spammers.
|
|
20
|
+
from pyramid.httpexceptions import HTTPUnauthorized
|
|
21
|
+
|
|
22
|
+
# cookie only session, not encrypted but signed to prevent tampering!
|
|
23
|
+
from pyramid.session import SignedCookieSessionFactory
|
|
24
|
+
|
|
25
|
+
# email validation.
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
# needed to load themes.
|
|
29
|
+
from pkg_resources import iter_entry_points
|
|
30
|
+
|
|
31
|
+
# needed to support expanding ENV vars from ini.
|
|
32
|
+
from os.path import expandvars
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
JINJA2_EXTENSION = ".j2"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_int_or_bool_or_none_or_str(value):
|
|
42
|
+
"""
|
|
43
|
+
Given a string value pulled from a configuration file,
|
|
44
|
+
this function attempts to return the value with the proper type.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
return int(value)
|
|
48
|
+
except ValueError:
|
|
49
|
+
if value.lower() in {"yes", "y", "true", "y"}:
|
|
50
|
+
return True
|
|
51
|
+
elif value.lower() in {"no", "n", "false", "f"}:
|
|
52
|
+
return False
|
|
53
|
+
elif value.lower() == "none":
|
|
54
|
+
return None
|
|
55
|
+
return str(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_children_settings(settings, parent_key):
|
|
59
|
+
"""
|
|
60
|
+
Accept a settings dict and parent key, return dict of children
|
|
61
|
+
|
|
62
|
+
For example:
|
|
63
|
+
|
|
64
|
+
auth_tkt.hashalg = md5
|
|
65
|
+
|
|
66
|
+
Results to:
|
|
67
|
+
|
|
68
|
+
{'auth_tkt.hashalg': 'md5'}
|
|
69
|
+
|
|
70
|
+
This function returns the following:
|
|
71
|
+
|
|
72
|
+
>>> get_children_settings({'auth_tkt.hashalg': 'md5'}, 'auth_tkt')
|
|
73
|
+
{'hashalg': 'md5'}
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
# needed to support expanding ENV vars from ini.
|
|
77
|
+
from os.path import expandvars
|
|
78
|
+
|
|
79
|
+
# the +1 is the . between parent and child settings.
|
|
80
|
+
parent_len = len(parent_key) + 1
|
|
81
|
+
children = {}
|
|
82
|
+
for key, value in settings.items():
|
|
83
|
+
if parent_key in key:
|
|
84
|
+
# expandvars replaces template with ENV vars.
|
|
85
|
+
children[key[parent_len:]] = get_int_or_bool_or_none_or_str(
|
|
86
|
+
expandvars(value)
|
|
87
|
+
)
|
|
88
|
+
return children
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_entry_points(group_name):
|
|
92
|
+
"""Return a dictionary of entry_points related to given group_name"""
|
|
93
|
+
entry_points = {}
|
|
94
|
+
for entry_point in iter_entry_points(group=group_name, name=None):
|
|
95
|
+
entry_points[entry_point.name] = entry_point.load()
|
|
96
|
+
return entry_points
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_jinja2_themes(config):
|
|
100
|
+
"""Automatically load any entry_point registered Remarkbox theme."""
|
|
101
|
+
themes = load_entry_points("remarkbox.themes")
|
|
102
|
+
for theme_name, theme_module in themes.items():
|
|
103
|
+
theme_module_name = theme_module.__name__
|
|
104
|
+
# teach Jinja2 about the template dir in the theme package.
|
|
105
|
+
config.add_jinja2_search_path(
|
|
106
|
+
"{}:templates/".format(theme_module_name), name=JINJA2_EXTENSION
|
|
107
|
+
)
|
|
108
|
+
# teach Pyramid about the static assets in the theme package.
|
|
109
|
+
config.add_static_view(
|
|
110
|
+
"static/theme/{}".format(theme_name),
|
|
111
|
+
"{}:static/theme/{}".format(theme_module_name, theme_name),
|
|
112
|
+
cache_max_age=3600,
|
|
113
|
+
)
|
|
114
|
+
return config
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_ipv4(string):
|
|
118
|
+
try:
|
|
119
|
+
parts = list(map(int, string.split(".")))
|
|
120
|
+
except:
|
|
121
|
+
return False
|
|
122
|
+
if len(parts) != 4:
|
|
123
|
+
return False
|
|
124
|
+
for part in parts:
|
|
125
|
+
if part < 0 or part > 256:
|
|
126
|
+
return False
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def maybe_root_domain(string):
|
|
131
|
+
"""
|
|
132
|
+
Maybe get valid "root" domain from given string, for example -
|
|
133
|
+
|
|
134
|
+
valid:
|
|
135
|
+
|
|
136
|
+
* hi.www.example.com -> example.com
|
|
137
|
+
* www.example.com -> example.com
|
|
138
|
+
* example.com -> example.com
|
|
139
|
+
|
|
140
|
+
invalid:
|
|
141
|
+
|
|
142
|
+
* comments.cryptocoin.com.au -> com.au
|
|
143
|
+
"""
|
|
144
|
+
return ".".join(string.split(".")[-2:])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(global_config, **settings):
|
|
148
|
+
"""This function returns a Pyramid WSGI application."""
|
|
149
|
+
|
|
150
|
+
app_settings = get_children_settings(settings, "app")
|
|
151
|
+
session_settings = get_children_settings(settings, "session")
|
|
152
|
+
|
|
153
|
+
def wild_signed_cookie_session_factory(request):
|
|
154
|
+
"""
|
|
155
|
+
Hack SignedCookieSessionFactory to have `wild_domain`.
|
|
156
|
+
Pyramid devs do not want to support this.
|
|
157
|
+
|
|
158
|
+
In addition the domain is determined per request!
|
|
159
|
+
|
|
160
|
+
This means a single deployment of this app can support managing a
|
|
161
|
+
`wild_domain` cookie for any domain (likely using CNAMES).
|
|
162
|
+
"""
|
|
163
|
+
root_domain = maybe_root_domain(request.domain)
|
|
164
|
+
if is_ipv4(request.domain) or root_domain.startswith("com"):
|
|
165
|
+
session_settings["domain"] = request.domain
|
|
166
|
+
else:
|
|
167
|
+
session_settings["domain"] = root_domain
|
|
168
|
+
|
|
169
|
+
factory = SignedCookieSessionFactory(**session_settings)
|
|
170
|
+
return factory(request)
|
|
171
|
+
|
|
172
|
+
# setup session factory to use unencrypted but signed cookies.
|
|
173
|
+
# session_factory = SignedCookieSessionFactory(**session_settings)
|
|
174
|
+
session_factory = wild_signed_cookie_session_factory
|
|
175
|
+
|
|
176
|
+
# build app config object from ini.
|
|
177
|
+
config = Configurator(settings=settings, session_factory=session_factory)
|
|
178
|
+
|
|
179
|
+
# setup and require automatic CSRF checking.
|
|
180
|
+
config.set_default_csrf_options(require_csrf=True)
|
|
181
|
+
|
|
182
|
+
# Create database engine from connection details in ini.
|
|
183
|
+
# make request.dbsession available for use in Pyramid.
|
|
184
|
+
config.include(".models")
|
|
185
|
+
|
|
186
|
+
# setup jinja2 template support.
|
|
187
|
+
config.include("pyramid_jinja2")
|
|
188
|
+
config.add_jinja2_search_path("remarkbox:templates/", name=JINJA2_EXTENSION)
|
|
189
|
+
config.add_jinja2_renderer(JINJA2_EXTENSION)
|
|
190
|
+
config = load_jinja2_themes(config)
|
|
191
|
+
|
|
192
|
+
# compile the email validator regex outside of the functions.
|
|
193
|
+
_email_regex = re.compile("^[^@]+@[^@]+\.[^.@]+$")
|
|
194
|
+
|
|
195
|
+
def add_debug_mode(request):
|
|
196
|
+
"""Return True if debug toolbar is enabled."""
|
|
197
|
+
return "pyramid_debugtoolbar" in request.registry.settings.get(
|
|
198
|
+
"pyramid.includes", ""
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
'''
|
|
202
|
+
def add_redis(request):
|
|
203
|
+
"""Return Redis Connection"""
|
|
204
|
+
redis_host = request.app.get("redis.host", "localhost")
|
|
205
|
+
redis_port = request.app.get("redis.port", 6379)
|
|
206
|
+
redis_db = request.app.get("redis.db", 0)
|
|
207
|
+
return redis.Redis(host=redis_host, port=redis_port, db=redis_db)
|
|
208
|
+
'''
|
|
209
|
+
|
|
210
|
+
def add_email(request):
|
|
211
|
+
"""Return Email or None. Email must pass regex."""
|
|
212
|
+
email = request.params.get("email", "")
|
|
213
|
+
if _email_regex.match(email) is not None:
|
|
214
|
+
return email
|
|
215
|
+
|
|
216
|
+
def add_user(request):
|
|
217
|
+
"""Return User object or None. User.authenticated may be True or False."""
|
|
218
|
+
user = None
|
|
219
|
+
authenticated_user_id = request.session.get("authenticated_user_id", None)
|
|
220
|
+
|
|
221
|
+
if authenticated_user_id:
|
|
222
|
+
# attach the user object from DB to the request.
|
|
223
|
+
user = get_user_by_id(request.dbsession, authenticated_user_id)
|
|
224
|
+
if user is not None:
|
|
225
|
+
user.authenticated = True
|
|
226
|
+
|
|
227
|
+
elif request.email:
|
|
228
|
+
user = get_or_create_user_by_email(request.dbsession, request.email)
|
|
229
|
+
|
|
230
|
+
return user
|
|
231
|
+
|
|
232
|
+
def add_csrf_token(request):
|
|
233
|
+
if request.user and request.user.authenticated:
|
|
234
|
+
return request.session.get_csrf_token()
|
|
235
|
+
|
|
236
|
+
def add_node(request):
|
|
237
|
+
"""Return Node object or None from matchdict or params."""
|
|
238
|
+
thread_uri = request.params.get("thread_uri")
|
|
239
|
+
thread_title = request.params.get("thread_title")
|
|
240
|
+
node_id = request.matchdict.get("node_id")
|
|
241
|
+
nojs = request.params.get("nojs", False)
|
|
242
|
+
|
|
243
|
+
node = None
|
|
244
|
+
|
|
245
|
+
# TODO: thread_uri must take priority over node_id because our forms
|
|
246
|
+
# are dumb. If we fix our forms to only pass node_id/node_path when not
|
|
247
|
+
# None, the priority / order will not matter.
|
|
248
|
+
if thread_uri:
|
|
249
|
+
# Remove "/reply" from the end of the URL if present
|
|
250
|
+
url = request.url
|
|
251
|
+
if url.endswith("/reply"):
|
|
252
|
+
url = url[:-6] # Remove the last 6 characters ("/reply")
|
|
253
|
+
request.session["back_to_thread"] = url
|
|
254
|
+
|
|
255
|
+
node = get_or_create_node_by_uri(
|
|
256
|
+
request.dbsession, thread_uri, thread_title
|
|
257
|
+
)
|
|
258
|
+
elif node_id:
|
|
259
|
+
node = get_node_by_id(request.dbsession, node_id)
|
|
260
|
+
elif nojs and request.referer:
|
|
261
|
+
# if javascript is disabled, use the referer.
|
|
262
|
+
node = get_or_create_node_by_uri(request.dbsession, request.referer)
|
|
263
|
+
|
|
264
|
+
return node
|
|
265
|
+
|
|
266
|
+
def add_root_node(request):
|
|
267
|
+
return request.node.root
|
|
268
|
+
|
|
269
|
+
def add_nodes(request):
|
|
270
|
+
if request.node:
|
|
271
|
+
return get_nodes_who_share_root(
|
|
272
|
+
request.dbsession,
|
|
273
|
+
request.node.root,
|
|
274
|
+
request.node_order,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def add_node_id_map(request):
|
|
278
|
+
return get_node_id_map(request.nodes)
|
|
279
|
+
|
|
280
|
+
def add_node_graph(request):
|
|
281
|
+
return get_graph_from_nodes(request.nodes)
|
|
282
|
+
|
|
283
|
+
def add_node_flat_graph(request):
|
|
284
|
+
return flatten_graph(request.node.root.id, request.node_graph)
|
|
285
|
+
|
|
286
|
+
def add_conversation_graph(request):
|
|
287
|
+
return get_conversation_graph_from_nodes(
|
|
288
|
+
request.nodes,
|
|
289
|
+
graph=request.node_graph,
|
|
290
|
+
node_id_map=request.node_id_map,
|
|
291
|
+
order=request.node_order,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def add_namespace(request):
|
|
295
|
+
namespace_name = request.matchdict.get(
|
|
296
|
+
"namespace", request.params.get("namespace", None)
|
|
297
|
+
)
|
|
298
|
+
if namespace_name:
|
|
299
|
+
return get_or_create_namespace(request.dbsession, namespace_name)
|
|
300
|
+
elif request.node:
|
|
301
|
+
return request.node.root.namespace
|
|
302
|
+
return get_or_create_namespace(request.dbsession, request.domain)
|
|
303
|
+
|
|
304
|
+
def add_mode(request):
|
|
305
|
+
"""return mode of 'embed' or 'basic'"""
|
|
306
|
+
return "embed" if "embed" in request.matched_route.name else "basic"
|
|
307
|
+
|
|
308
|
+
def add_link_prefix(request):
|
|
309
|
+
if request.mode == "embed":
|
|
310
|
+
return "/embed/ns/{}".format(request.namespace.name)
|
|
311
|
+
return ""
|
|
312
|
+
|
|
313
|
+
def add_spam(request):
|
|
314
|
+
"""Test if request looks spammy HTTP Error or False."""
|
|
315
|
+
if request.params.get("email2", "") != "":
|
|
316
|
+
# spam filter, hidden email2 field tricks bots.
|
|
317
|
+
return HTTPUnauthorized("you smell like a spammer")
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
def add_owner_key(request):
|
|
321
|
+
return request.params.get("rb_owner_key", None)
|
|
322
|
+
|
|
323
|
+
def add_namespace_request(request):
|
|
324
|
+
"""Add namespace_request if pending, else None"""
|
|
325
|
+
# we reduce database lookups by only querying when owner_key is
|
|
326
|
+
# present and request.namespace has a pending owner_request.
|
|
327
|
+
if request.owner_key and request.namespace.owner_request_pending:
|
|
328
|
+
namespace_request = get_namespace_request_by_id(
|
|
329
|
+
request.dbsession, request.owner_key
|
|
330
|
+
)
|
|
331
|
+
# for dubugging purposes only.
|
|
332
|
+
# log.info("owner_key={}, pending={}".format(request.owner_key,request.namespace.owner_request_pending))
|
|
333
|
+
if namespace_request and namespace_request.namespace == request.namespace:
|
|
334
|
+
return namespace_request
|
|
335
|
+
|
|
336
|
+
def add_app(request):
|
|
337
|
+
"""Attach app settings dictionary."""
|
|
338
|
+
return app_settings
|
|
339
|
+
|
|
340
|
+
def add_app_url(request):
|
|
341
|
+
"""
|
|
342
|
+
Use the app_url from config if request.domain ends with the
|
|
343
|
+
configured root_domain. Otherwise just use the request's host_url
|
|
344
|
+
"""
|
|
345
|
+
config_app_url = request.app.get("app_url", request.host_url)
|
|
346
|
+
root_domain = request.app.get("root_domain")
|
|
347
|
+
if root_domain and request.domain.endswith(root_domain):
|
|
348
|
+
return config_app_url
|
|
349
|
+
elif request.domain == "localhost":
|
|
350
|
+
return config_app_url
|
|
351
|
+
return request.host_url
|
|
352
|
+
|
|
353
|
+
def add_app_domain(request):
|
|
354
|
+
return request.app_url.split("://")[-1].split(":")[0]
|
|
355
|
+
|
|
356
|
+
def add_marketing_url(request):
|
|
357
|
+
return request.app.get("marketing_url", request.app_url)
|
|
358
|
+
|
|
359
|
+
def add_marketing_domain(request):
|
|
360
|
+
return request.marketing_url.split("://")[-1].split(":")[0]
|
|
361
|
+
|
|
362
|
+
def add_faq_home(request):
|
|
363
|
+
"""Return True if this request's domain equals namespace name."""
|
|
364
|
+
return request.domain == request.namespace.name
|
|
365
|
+
|
|
366
|
+
def add_saas_home(request):
|
|
367
|
+
"""Only one domain should have this method return True per deployment."""
|
|
368
|
+
root_domain = request.app.get("root_domain")
|
|
369
|
+
return (
|
|
370
|
+
root_domain
|
|
371
|
+
and request.domain.endswith(root_domain)
|
|
372
|
+
and request.app_domain == request.namespace.name
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def add_stripe(request):
|
|
376
|
+
"""Attach a stripe object with creds to request."""
|
|
377
|
+
import stripe
|
|
378
|
+
|
|
379
|
+
stripe.api_key = request.app.get("stripe.secret")
|
|
380
|
+
return stripe
|
|
381
|
+
|
|
382
|
+
def add_stripe_customer(request):
|
|
383
|
+
if request.user:
|
|
384
|
+
if not request.user.stripe_id:
|
|
385
|
+
# create a new stripe customer.
|
|
386
|
+
customer = request.stripe.Customer.create(email=request.user.email)
|
|
387
|
+
request.user.stripe_id = customer.id
|
|
388
|
+
request.dbsession.add(request.user)
|
|
389
|
+
request.dbsession.flush()
|
|
390
|
+
return request.stripe.Customer.retrieve(request.user.stripe_id)
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
def add_stripe_saved_cards(request):
|
|
394
|
+
if request.user and request.user.stripe_id:
|
|
395
|
+
return request.stripe_customer.sources
|
|
396
|
+
return []
|
|
397
|
+
|
|
398
|
+
def add_stripe_active_card(request):
|
|
399
|
+
if request.user and request.user.stripe_id:
|
|
400
|
+
if request.stripe_customer.default_source:
|
|
401
|
+
return request.stripe_customer.sources.retrieve(
|
|
402
|
+
request.stripe_customer.default_source
|
|
403
|
+
)
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
def add_avatar_size(request):
|
|
407
|
+
"""Attach avatar size or default."""
|
|
408
|
+
if request.namespace:
|
|
409
|
+
if request.namespace.avatar_size:
|
|
410
|
+
return request.namespace.avatar_size
|
|
411
|
+
return request.app.get("avatar.size", 30)
|
|
412
|
+
|
|
413
|
+
def add_stand_alone_mode(request):
|
|
414
|
+
if request.app.get("stand_alone_mode", "disabled") == "enabled":
|
|
415
|
+
return True
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
def add_theme(request):
|
|
419
|
+
"""Attach theme name or None."""
|
|
420
|
+
if request.mode == "embed":
|
|
421
|
+
if request.namespace and request.namespace.theme_embed:
|
|
422
|
+
return request.namespace.theme_embed
|
|
423
|
+
return request.app.get("theme_embed", None)
|
|
424
|
+
|
|
425
|
+
if request.namespace and request.namespace.theme:
|
|
426
|
+
return request.namespace.theme
|
|
427
|
+
return request.app.get("theme", None)
|
|
428
|
+
|
|
429
|
+
def add_stylesheet(request):
|
|
430
|
+
"""Attach stylesheet or empty string."""
|
|
431
|
+
if request.mode == "embed":
|
|
432
|
+
return request.stylesheet_embed
|
|
433
|
+
return request.stylesheet_basic
|
|
434
|
+
|
|
435
|
+
def add_stylesheet_basic(request):
|
|
436
|
+
"""Attach stylesheet or empty string."""
|
|
437
|
+
if request.namespace and request.namespace.stylesheet:
|
|
438
|
+
return request.namespace.stylesheet
|
|
439
|
+
return ""
|
|
440
|
+
|
|
441
|
+
def add_stylesheet_embed(request):
|
|
442
|
+
"""Attach stylesheet_embed or empty string."""
|
|
443
|
+
if request.namespace and request.namespace.stylesheet_embed:
|
|
444
|
+
return request.namespace.stylesheet_embed
|
|
445
|
+
return ""
|
|
446
|
+
|
|
447
|
+
def add_stylesheet_uri(request):
|
|
448
|
+
"""Attach stylesheet uri or empty string."""
|
|
449
|
+
if request.mode == "embed":
|
|
450
|
+
return request.stylesheet_embed_uri
|
|
451
|
+
return request.stylesheet_basic_uri
|
|
452
|
+
|
|
453
|
+
def add_stylesheet_basic_uri(request):
|
|
454
|
+
"""Attach stylesheet uri or empty string."""
|
|
455
|
+
if request.namespace and request.namespace.stylesheet_uri:
|
|
456
|
+
return request.namespace.stylesheet_uri
|
|
457
|
+
return ""
|
|
458
|
+
|
|
459
|
+
def add_stylesheet_embed_uri(request):
|
|
460
|
+
"""Attach stylesheet_embed uri or empty string."""
|
|
461
|
+
if request.namespace and request.namespace.stylesheet_embed_uri:
|
|
462
|
+
return request.namespace.stylesheet_embed_uri
|
|
463
|
+
return ""
|
|
464
|
+
|
|
465
|
+
def add_base_template(request):
|
|
466
|
+
"""Attach base_template filename."""
|
|
467
|
+
if request.theme:
|
|
468
|
+
return "{}-base.j2".format(request.theme)
|
|
469
|
+
return "base.j2"
|
|
470
|
+
|
|
471
|
+
def add_base_funnel_template(request):
|
|
472
|
+
"""Attach base_funnel template filename."""
|
|
473
|
+
if request.theme:
|
|
474
|
+
return "{}-base-funnel.j2".format(request.theme)
|
|
475
|
+
return "base.j2"
|
|
476
|
+
|
|
477
|
+
def add_page_number(request):
|
|
478
|
+
"""Attach page_number starting at 0"""
|
|
479
|
+
page_number = int(request.params.get("page", 1))
|
|
480
|
+
return page_number if page_number >= 1 else 1
|
|
481
|
+
|
|
482
|
+
def add_page_size(request):
|
|
483
|
+
# if we want to support custom page sizes, set it here.
|
|
484
|
+
return 100
|
|
485
|
+
|
|
486
|
+
def add_page_offset(request):
|
|
487
|
+
return (request.page_number - 1) * request.page_size
|
|
488
|
+
|
|
489
|
+
def add_node_order(request):
|
|
490
|
+
return request.params.get("order", request.namespace.node_order)
|
|
491
|
+
|
|
492
|
+
def add_mathjax(request):
|
|
493
|
+
return "true" if request.namespace.mathjax else "false"
|
|
494
|
+
|
|
495
|
+
# register functions to app config as request methods.
|
|
496
|
+
# each request instance will run these functions and attach results.
|
|
497
|
+
# cache result with `reify=True` to prevent multiple db lookups.
|
|
498
|
+
# config.add_request_method(add_redis, "redis", reify=True)
|
|
499
|
+
config.add_request_method(add_debug_mode, "debug_mode", reify=True)
|
|
500
|
+
config.add_request_method(add_user, "user", reify=True)
|
|
501
|
+
config.add_request_method(add_csrf_token, "csrf_token", reify=True)
|
|
502
|
+
config.add_request_method(add_email, "email", reify=True)
|
|
503
|
+
config.add_request_method(add_node, "node", reify=True)
|
|
504
|
+
config.add_request_method(add_root_node, "root_node", reify=True)
|
|
505
|
+
config.add_request_method(add_nodes, "nodes", reify=True)
|
|
506
|
+
config.add_request_method(add_node_id_map, "node_id_map", reify=True)
|
|
507
|
+
config.add_request_method(add_node_graph, "node_graph", reify=True)
|
|
508
|
+
config.add_request_method(add_node_flat_graph, "node_flat_graph", reify=True)
|
|
509
|
+
config.add_request_method(add_conversation_graph, "conversation_graph", reify=True)
|
|
510
|
+
config.add_request_method(add_namespace, "namespace", reify=True)
|
|
511
|
+
config.add_request_method(add_mode, "mode", reify=True)
|
|
512
|
+
config.add_request_method(add_link_prefix, "link_prefix", reify=True)
|
|
513
|
+
config.add_request_method(add_spam, "spam", reify=True)
|
|
514
|
+
config.add_request_method(add_owner_key, "owner_key", reify=True)
|
|
515
|
+
config.add_request_method(add_namespace_request, "namespace_request", reify=True)
|
|
516
|
+
config.add_request_method(add_app, "app", reify=True)
|
|
517
|
+
config.add_request_method(add_app_url, "app_url", reify=True)
|
|
518
|
+
config.add_request_method(add_app_domain, "app_domain", reify=True)
|
|
519
|
+
config.add_request_method(add_marketing_url, "marketing_url", reify=True)
|
|
520
|
+
config.add_request_method(add_marketing_domain, "marketing_domain", reify=True)
|
|
521
|
+
config.add_request_method(add_faq_home, "faq_home", reify=True)
|
|
522
|
+
config.add_request_method(add_saas_home, "saas_home", reify=True)
|
|
523
|
+
config.add_request_method(add_stripe, "stripe", reify=True)
|
|
524
|
+
config.add_request_method(add_stripe_customer, "stripe_customer", reify=True)
|
|
525
|
+
config.add_request_method(add_stripe_saved_cards, "stripe_saved_cards", reify=True)
|
|
526
|
+
config.add_request_method(add_stripe_active_card, "stripe_active_card", reify=True)
|
|
527
|
+
config.add_request_method(add_stand_alone_mode, "stand_alone_mode", reify=True)
|
|
528
|
+
config.add_request_method(add_avatar_size, "avatar_size", reify=True)
|
|
529
|
+
config.add_request_method(add_theme, "theme", reify=True)
|
|
530
|
+
config.add_request_method(add_stylesheet_basic, "stylesheet_basic", reify=True)
|
|
531
|
+
config.add_request_method(add_stylesheet_embed, "stylesheet_embed", reify=True)
|
|
532
|
+
config.add_request_method(add_stylesheet, "stylesheet", reify=True)
|
|
533
|
+
config.add_request_method(
|
|
534
|
+
add_stylesheet_basic_uri, "stylesheet_basic_uri", reify=True
|
|
535
|
+
)
|
|
536
|
+
config.add_request_method(
|
|
537
|
+
add_stylesheet_embed_uri, "stylesheet_embed_uri", reify=True
|
|
538
|
+
)
|
|
539
|
+
config.add_request_method(add_stylesheet_uri, "stylesheet_uri", reify=True)
|
|
540
|
+
config.add_request_method(add_base_template, "base_template", reify=True)
|
|
541
|
+
config.add_request_method(
|
|
542
|
+
add_base_funnel_template, "base_funnel_template", reify=True
|
|
543
|
+
)
|
|
544
|
+
config.add_request_method(add_page_number, "page_number", reify=True)
|
|
545
|
+
config.add_request_method(add_page_size, "page_size", reify=True)
|
|
546
|
+
config.add_request_method(add_page_offset, "page_offset", reify=True)
|
|
547
|
+
config.add_request_method(add_node_order, "node_order", reify=True)
|
|
548
|
+
config.add_request_method(add_mathjax, "mathjax", reify=True)
|
|
549
|
+
|
|
550
|
+
# all of the web application routes.
|
|
551
|
+
config.include(".routes")
|
|
552
|
+
|
|
553
|
+
# Scan for views.
|
|
554
|
+
config.scan()
|
|
555
|
+
|
|
556
|
+
return config.make_wsgi_app()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from hashlib import md5
|
|
2
|
+
|
|
3
|
+
from ago import human
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def timestamp_to_datetime(timestamp):
|
|
9
|
+
"""Accepts a timestamp and returns a date string"""
|
|
10
|
+
return datetime.fromtimestamp(timestamp / 1000.0)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def timestamp_to_date_string(timestamp):
|
|
14
|
+
return timestamp_to_datetime(timestamp).strftime("%b %d, %Y %I:%M %P")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def timestamp_to_ago_string(timestamp):
|
|
18
|
+
"""Accepts a timestamp and returns a human readable string"""
|
|
19
|
+
return human(timestamp_to_datetime(timestamp), 2, abbreviate=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def gravatar_client(email, size=48, rating="g", default="retro", force_default=False):
|
|
23
|
+
"""Return a gravatar API URI."""
|
|
24
|
+
query_params = ["s={}".format(size), "r={}".format(rating), "d={}".format(default)]
|
|
25
|
+
if force_default:
|
|
26
|
+
query_params.append("f=y")
|
|
27
|
+
uri = "https://secure.gravatar.com/avatar/{0}?{1}"
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Python 2.
|
|
31
|
+
email_md5 = md5(email).hexdigest()
|
|
32
|
+
except TypeError:
|
|
33
|
+
# Python 3.
|
|
34
|
+
email_md5 = md5(email.encode("utf-8")).hexdigest()
|
|
35
|
+
|
|
36
|
+
return uri.format(email_md5, "&".join(query_params))
|