diff --git a/critiquebrainz/data/dump_manager.py b/critiquebrainz/data/dump_manager.py index 974bbd7d2..6c54412c1 100644 --- a/critiquebrainz/data/dump_manager.py +++ b/critiquebrainz/data/dump_manager.py @@ -9,9 +9,10 @@ from time import gmtime, strftime import click +import orjson import sqlalchemy from flask import current_app, jsonify -from flask.json import JSONEncoder +from flask.json.provider import JSONProvider from psycopg2.sql import SQL, Identifier from critiquebrainz import db @@ -147,7 +148,7 @@ def json(location, rotate=False): """ create_path(location) - current_app.json_encoder = DumpJSONEncoder + current_app.json_encoder = OrJSONProvider print("Creating new archives...") with db.engine.begin() as connection: @@ -541,16 +542,9 @@ def reset_sequence(table_names): connection.close() -class DumpJSONEncoder(JSONEncoder): - """Custom JSON encoder for database dumps.""" +class OrJSONProvider(JSONProvider): + def dumps(self, obj, **kwargs): + return orjson.dumps(obj).decode() - def default(self, o): # pylint: disable=method-hidden - try: - if isinstance(o, datetime): - return o.isoformat() - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) + def loads(self, s, **kwargs): + return orjson.loads(s) diff --git a/critiquebrainz/frontend/babel.py b/critiquebrainz/frontend/babel.py index d1d36604a..79e7b8d9d 100644 --- a/critiquebrainz/frontend/babel.py +++ b/critiquebrainz/frontend/babel.py @@ -3,8 +3,6 @@ def init_app(app, domain='messages'): - babel = Babel(app, default_domain=domain) - app.config['LANGUAGES'] = {} for language in app.config['SUPPORTED_LANGUAGES']: app.config['LANGUAGES'][language] = Locale.parse(language).language_name @@ -21,7 +19,6 @@ def after_this_request(f): g.after_request_callbacks.append(f) return f - @babel.localeselector def get_locale(): # pylint: disable=unused-variable supported_languages = app.config['SUPPORTED_LANGUAGES'] language_arg = request.args.get('l') @@ -38,3 +35,6 @@ def remember_language(response): # pylint: disable=unused-variable return language_cookie return request.accept_languages.best_match(supported_languages) + + babel = Babel() + babel.init_app(app, default_domain=domain, locale_selector=get_locale) diff --git a/critiquebrainz/frontend/external/__init__.py b/critiquebrainz/frontend/external/__init__.py index 5ea2d1124..a46502b42 100644 --- a/critiquebrainz/frontend/external/__init__.py +++ b/critiquebrainz/frontend/external/__init__.py @@ -6,8 +6,7 @@ See documentation in each module for information about usage. """ -from flask import current_app, _app_ctx_stack - +from flask import current_app, g from critiquebrainz.frontend.external.entities import get_entity_by_id, get_multiple_entities @@ -38,19 +37,16 @@ def init_app(self, app): @property def get_entity_by_id(self): - ctx = _app_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, 'get_entity_by_id'): - ctx.get_entity_by_id = self.get_entity_by_id_method - return ctx.get_entity_by_id + if not hasattr(g, 'get_entity_by_id'): + g.get_entity_by_id = self.get_entity_by_id_method + return g.get_entity_by_id @property def get_multiple_entities(self): - ctx = _app_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, 'get_multiple_entities'): - ctx.get_multiple_entities = self.get_multiple_entities_method - return ctx.get_multiple_entities + if not hasattr(g, 'get_multiple_entities'): + g.get_multiple_entities = self.get_multiple_entities_method + return g.get_multiple_entities + mbstore = MBDataAccess() @@ -67,6 +63,7 @@ def development_get_multiple_entities(entities): data[mbid] = get_dummy_item(mbid, entity_type) return data + def development_get_entity_by_id(entity_id, entity_type): """Same as get_entity_by_id, but always returns a dummy item if the requested entity isn't in the MusicBrainz database. Used in development with a sample database.""" diff --git a/critiquebrainz/frontend/forms/review.py b/critiquebrainz/frontend/forms/review.py index d84c621f9..64592ff28 100644 --- a/critiquebrainz/frontend/forms/review.py +++ b/critiquebrainz/frontend/forms/review.py @@ -47,8 +47,8 @@ def __init__(self, default_license_id='CC BY-SA 3.0', default_language='en', **k kwargs.setdefault('language', default_language) FlaskForm.__init__(self, **kwargs) - def validate(self): - if not super(ReviewEditForm, self).validate(): + def validate(self, extra_validators=None): + if not super(ReviewEditForm, self).validate(extra_validators): return False if not self.text.data and not self.rating.data: self.text.errors.append("You must provide some text or a rating to complete this review.") diff --git a/critiquebrainz/frontend/views/profile.py b/critiquebrainz/frontend/views/profile.py index 7b0bcd06b..5437455a1 100644 --- a/critiquebrainz/frontend/views/profile.py +++ b/critiquebrainz/frontend/views/profile.py @@ -20,7 +20,7 @@ def edit(): "license_choice": form.license_choice.data, }) flash.success(gettext("Profile updated.")) - return redirect(url_for('user.reviews', user_ref= current_user.user_ref)) + return redirect(url_for('user.reviews', user_ref=current_user.user_ref)) form.display_name.data = current_user.display_name form.email.data = current_user.email diff --git a/critiquebrainz/frontend/views/test/test_comment.py b/critiquebrainz/frontend/views/test/test_comment.py index fb28b2eca..ead1d469a 100644 --- a/critiquebrainz/frontend/views/test/test_comment.py +++ b/critiquebrainz/frontend/views/test/test_comment.py @@ -96,13 +96,20 @@ def test_create(self): # blocked user should not be allowed to comment db_users.block(self.commenter.id) + # because the g global persists for entire duration of the test, we need to manually logout/login + # the user again for the current_user to be refreshed. in production, this would happen automatically + # as the current_user is loaded at the start of each request/request context. + self.temporary_login(self.commenter) + response = self.client.post( url_for("comment.create"), data=payload, follow_redirects=True, ) self.assertIn("You are not allowed to write new comments", str(response.data)) + db_users.unblock(self.commenter.id) + self.temporary_login(self.commenter) # comment with some text and a valid review_id must be saved response = self.client.post( diff --git a/critiquebrainz/frontend/views/test/test_profile.py b/critiquebrainz/frontend/views/test/test_profile.py index 263902cbf..4cbc16d61 100644 --- a/critiquebrainz/frontend/views/test/test_profile.py +++ b/critiquebrainz/frontend/views/test/test_profile.py @@ -31,6 +31,14 @@ def test_edit(self): response = self.client.post('/profile/edit', data=data, query_string=data, follow_redirects=True) self.assert200(response) + + # because the g global persists for entire duration of the test, we need to manually logout/login + # the user again for the current_user to be refreshed. in production, this would happen automatically + # as the current_user is loaded at the start of each request/request context. + self.temporary_login(self.user) + + response = self.client.get(f'/user/{self.user.id}') + self.assert200(response) self.assertIn(data['display_name'], str(response.data)) def test_delete(self): diff --git a/requirements.txt b/requirements.txt index 622058f3c..252ba8aa6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ brainzutils@git+https://github.com/metabrainz/brainzutils-python.git@v2.7.0 beautifulsoup4==4.8.0 click==8.1.3 -Flask-Babel==2.0.0 -Flask-Login==0.6.0 +Flask-Babel==4.0.0 +Flask-Login==0.6.3 Flask-SQLAlchemy==2.5.1 -Flask-WTF==1.0.1 +Flask-WTF==1.2.1 Markdown==3.3.6 bleach==5.0.1 musicbrainzngs==0.7.1 @@ -20,19 +20,20 @@ transifex-client==0.12.4 WTForms==3.0.1 email-validator==1.1.3 langdetect==1.0.7 -Flask==2.2.5 +Flask==3.0.0 Jinja2==3.1.2 -werkzeug==2.2.3 -Flask-DebugToolbar==0.13.1 +werkzeug==3.0.1 +Flask-DebugToolbar@git+https://github.com/amCap1712/flask-debugtoolbar.git@f42bb238cd3fbc79c51b93c341164c2be820025e Flask-UUID==0.2 -sentry-sdk[flask]==1.14.0 +sentry-sdk[flask]==1.37.1 redis==4.4.4 msgpack==0.5.6 requests==2.31.0 SQLAlchemy==1.4.41 mbdata@git+https://github.com/amCap1712/mbdata.git@fix-sqlalchemy-warnings sqlalchemy-dst==1.0.1 -markupsafe==2.1.1 +markupsafe==2.1.3 itsdangerous==2.1.2 flask-shell-ipython -requests-mock==1.9.3 \ No newline at end of file +requests-mock==1.9.3 +orjson==3.9.10