Repository navigation
Expand file tree
/
Copy pathmain.py
More file actions
205 lines (170 loc) Β· 7.49 KB
/
Copy pathmain.py
File metadata and controls
205 lines (170 loc) Β· 7.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""
main.py
This is the application factory for the Nyota β¨ project.
"""
import os
import json
from datetime import timedelta
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from flask import Flask, g, session, request
import mistune
from flask_compress import Compress
from config import Config
from models.nyota import db, migrate
from routes import main_bp, admin_bp
# --- Jinja2 Custom Filters ---
def format_currency(value, symbol='$'):
if value is None:
return f"{symbol} 0.00"
return f"{symbol} {float(value):,.2f}"
from utils.translator import translate
from utils.i18n import DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, normalize_lang, pick_localized
# --- Language Selection ---
def get_locale():
if 'language' in session:
return session['language']
# Check headers for country code (Cloudflare, App Engine, or Generic)
country = request.headers.get('CF-IPCountry') or \
request.headers.get('X-AppEngine-Country') or \
request.headers.get('X-Country-Code')
# If a country is detected and it is NOT Tanzania, default to English
if country and country.upper() != 'TZ':
return 'en'
# Default to Swahili for Tanzania and all unknown locations
return DEFAULT_LANGUAGE
# --- Application Factory Function ---
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# Trust the reverse proxy's X-Forwarded-Proto header so request.url_root
# uses https:// instead of http:// when behind nginx/Cloudflare.
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
# --- Initialize Flask Extensions ---
db.init_app(app)
migrate.init_app(app, db)
Compress(app)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # 1 year cache for static files
# Static files are cached for a year, so every url_for('static', ...) carries a
# ?v=<mtime> stamp β editing a CSS/JS file changes the URL and busts the cache.
_static_versions = {}
@app.url_defaults
def add_static_version(endpoint, values):
if endpoint != 'static' or 'filename' not in values:
return
filename = values['filename']
version = _static_versions.get(filename)
if version is None or app.debug:
path = os.path.join(app.static_folder, filename)
try:
version = str(int(os.path.getmtime(path)))
except OSError:
version = ''
_static_versions[filename] = version
if version:
values['v'] = version
# --- Register Jinja2 Filters ---
app.jinja_env.filters['format_currency'] = format_currency
def nl2br(value):
from markupsafe import Markup, escape
if not value:
return ""
return Markup(str(escape(value)).replace('\n', '<br>\n'))
app.jinja_env.filters['nl2br'] = nl2br
# This lambda function takes text, processes it with mistune, and returns HTML.
app.jinja_env.filters['markdown'] = lambda text: mistune.html(text)
def local_dt_filter(dt, fmt='%b %d, %Y %I:%M %p'):
"""Convert a naive UTC datetime to the creator's local timezone."""
if dt is None:
return ''
tz_str = None
if hasattr(g, 'creator') and g.creator:
tz_str = g.creator.get_setting('creator_timezone')
if not tz_str:
return dt.strftime(fmt)
try:
local = dt.replace(tzinfo=ZoneInfo('UTC')).astimezone(ZoneInfo(tz_str))
return local.strftime(fmt)
except (ZoneInfoNotFoundError, Exception):
return dt.strftime(fmt)
app.jinja_env.filters['local_dt'] = local_dt_filter
# --- Register Blueprints ---
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp)
# --- Request Hooks ---
@app.before_request
def before_request_tasks():
session.permanent = True
app.permanent_session_lifetime = timedelta(days=30)
# ?lang= lets a link carry its own language: a storefront link shared into
# a Swahili WhatsApp group opens in Swahili even for a first-time visitor
# whose browser says otherwise. It sticks, so the rest of the visit
# follows. get_locale() stays a pure selector and never writes state.
requested = normalize_lang(request.args.get('lang'))
if requested and session.get('language') != requested:
session['language'] = requested
g.language = get_locale()
# --- Context Processors ---
@app.context_processor
def inject_global_vars():
from utils.pricing import asset_pricing, contribution_voice, preset_choices
return dict(
store_name="Nyota β¨",
currency_symbol=get_currency_symbol(),
translate=translate,
# One fallback ladder for the creator-written label pairs, shared
# with utils/pricing.py so a half-filled pair reads the same way on
# a card, on the asset page and in the button that takes the money.
pick_localized=pick_localized,
supported_languages=SUPPORTED_LANGUAGES,
# Every surface that quotes a price resolves it through the same
# helper, so a card, an asset page and the JSON-LD can't disagree.
asset_pricing=asset_pricing,
# ...and every surface that asks for a contribution uses the verb
# the creator chose ("Donate", "Buy me a coffee", "Unlock").
contribution_voice=contribution_voice,
# The pool that picker offers, in both languages at once.
cta_presets=preset_choices(),
)
@app.context_processor
def inject_site_footer():
"""Make the footer available to every storefront page.
Routes pass `creator` inconsistently (the library and recovery pages
don't), so the footer resolves the creator itself rather than depending
on each route to remember. Admin pages don't render it.
"""
if request.blueprint == 'admin':
return {}
from models.nyota import Creator
from utils.site_meta import build_site_footer, build_structured_data
try:
creator = Creator.query.first()
footer = build_site_footer(creator)
return dict(
site_footer=footer,
site_schema=build_structured_data(creator, footer),
)
except Exception as exc:
# Pre-setup (no tables yet) or a bad settings row must not 500 the store.
app.logger.warning(f"Footer context unavailable: {exc}")
return dict(site_footer={'enabled': False}, site_schema=None)
def get_currency_symbol():
from models.nyota import Creator
creator = Creator.query.first()
if creator:
return creator.get_setting('payment_uza_currency', 'TZS')
return 'TZS'
# --- Cache Headers for Static Assets ---
@app.after_request
def add_cache_headers(response):
if request.path.startswith('/static/'):
response.cache_control.max_age = 31536000
response.cache_control.public = True
return response
# Start background worker (scheduled campaigns + subscription reminders).
# Suppressed during flask db migrate / flask db upgrade via env var.
import os
if not os.environ.get('FLASK_SKIP_BACKGROUND_WORKER'):
from services.background_tasks import start_background_worker
start_background_worker(app)
return app