diff --git a/hushline/model/organization_setting.py b/hushline/model/organization_setting.py index 2f79a729..46e070ba 100644 --- a/hushline/model/organization_setting.py +++ b/hushline/model/organization_setting.py @@ -19,6 +19,7 @@ class OrganizationSetting(Model): BRAND_LOGO = "brand_logo" BRAND_NAME = "brand_name" BRAND_PRIMARY_COLOR = "brand_primary_color" + BRAND_PROFILE_HEADER_TEMPLATE = "brand_profile_header_template" DIRECTORY_INTRO_TEXT = "directory_intro_text" GUIDANCE_ENABLED = "guidance_enabled" GUIDANCE_EXIT_BUTTON_TEXT = "guidance_exit_button_text" @@ -32,6 +33,7 @@ class OrganizationSetting(Model): _DEFAULT_VALUES: dict[str, Any] = { BRAND_NAME: "🤫 Hush Line", BRAND_PRIMARY_COLOR: "#7d25c1", + BRAND_PROFILE_HEADER_TEMPLATE: "Submit message to {{ display_name_or_username }}", GUIDANCE_ENABLED: False, GUIDANCE_EXIT_BUTTON_TEXT: "Leave", GUIDANCE_EXIT_BUTTON_LINK: "https://en.wikipedia.org/wiki/Main_Page", diff --git a/hushline/model/username.py b/hushline/model/username.py index 5b04a1cf..f5e92979 100644 --- a/hushline/model/username.py +++ b/hushline/model/username.py @@ -38,7 +38,6 @@ class Username(Model): is_verified: Mapped[bool] = mapped_column(default=False) show_in_directory: Mapped[bool] = mapped_column(default=False) bio: Mapped[Optional[str]] = mapped_column(db.Text) - profile_header: Mapped[Optional[str]] = mapped_column(db.Text) # Extra fields extra_field_label1: Mapped[Optional[str]] diff --git a/hushline/routes/profile.py b/hushline/routes/profile.py index 3f4f1eae..120e81a4 100644 --- a/hushline/routes/profile.py +++ b/hushline/routes/profile.py @@ -13,6 +13,7 @@ from hushline.crypto import decrypt_field from hushline.db import db from hushline.model import ( + OrganizationSetting, Username, ) from hushline.routes.forms import MessageForm @@ -55,17 +56,14 @@ def profile(username: str) -> Response | str: math_problem = f"{num1} + {num2} =" session["math_answer"] = str(num1 + num2) # Store the answer in session as a string - if uname.profile_header: - profile_header = safe_render_template( - uname.profile_header, - { - "display_name_or_username": uname.display_name or uname.username, - "display_name": uname.display_name, - "username": uname.username, - }, - ) - else: - profile_header = f"Submit message to {uname.display_name or uname.username}" + profile_header = safe_render_template( + OrganizationSetting.fetch_one(OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE), + { + "display_name_or_username": uname.display_name or uname.username, + "display_name": uname.display_name, + "username": uname.username, + }, + ) return render_template( "profile.html", diff --git a/hushline/settings/aliases.py b/hushline/settings/aliases.py index ddcf9f8e..2eebeb8c 100644 --- a/hushline/settings/aliases.py +++ b/hushline/settings/aliases.py @@ -24,14 +24,12 @@ handle_new_alias_form, handle_update_bio, handle_update_directory_visibility, - handle_update_profile_header, ) from hushline.settings.forms import ( DirectoryVisibilityForm, DisplayNameForm, NewAliasForm, ProfileForm, - UpdateProfileHeaderForm, ) @@ -65,7 +63,7 @@ def aliases() -> Response | Tuple[str, int]: @bp.route("/alias/", methods=["GET", "POST"]) @authentication_required - async def alias(username_id: int) -> Response | str: + async def alias(username_id: int) -> Response | Tuple[str, int]: alias = db.session.scalars( db.select(Username).filter_by( id=username_id, user_id=session["user_id"], is_primary=False @@ -80,8 +78,8 @@ async def alias(username_id: int) -> Response | str: directory_visibility_form = DirectoryVisibilityForm( show_in_directory=alias.show_in_directory ) - update_profile_header_form = UpdateProfileHeaderForm(template=alias.profile_header) + status_code = 200 if request.method == "POST": if "update_bio" in request.form and profile_form.validate_on_submit(): return await handle_update_bio(alias, profile_form) @@ -92,12 +90,8 @@ async def alias(username_id: int) -> Response | str: return handle_update_directory_visibility(alias, directory_visibility_form) elif "update_display_name" in request.form and display_name_form.validate_on_submit(): return handle_display_name_form(alias, display_name_form) - elif ( - update_profile_header_form.submit.name in request.form - and update_profile_header_form.validate() - ): - return handle_update_profile_header(alias, update_profile_header_form) else: + status_code = 400 current_app.logger.error( f"Unable to handle form submission on endpoint {request.endpoint!r}, " f"form fields: {request.form.keys()}" @@ -111,5 +105,4 @@ async def alias(username_id: int) -> Response | str: display_name_form=display_name_form, directory_visibility_form=directory_visibility_form, profile_form=profile_form, - update_profile_header_form=update_profile_header_form, - ) + ), status_code diff --git a/hushline/settings/branding.py b/hushline/settings/branding.py index 88ebb9e5..2b840db8 100644 --- a/hushline/settings/branding.py +++ b/hushline/settings/branding.py @@ -9,6 +9,7 @@ request, session, ) +from werkzeug.wrappers.response import Response from hushline.auth import admin_authentication_required from hushline.db import db @@ -26,14 +27,16 @@ UpdateBrandLogoForm, UpdateBrandPrimaryColorForm, UpdateDirectoryTextForm, + UpdateProfileHeaderForm, ) from hushline.storage import public_store +from hushline.utils import redirect_to_self def register_branding_routes(bp: Blueprint) -> None: @bp.route("/branding", methods=["GET", "POST"]) @admin_authentication_required - def branding() -> Tuple[str, int]: + def branding() -> Response | Tuple[str, int]: user = db.session.scalars(db.select(User).filter_by(id=session["user_id"])).one() update_directory_text_form = UpdateDirectoryTextForm( @@ -46,6 +49,7 @@ def branding() -> Tuple[str, int]: set_homepage_username_form = SetHomepageUsernameForm( username=OrganizationSetting.fetch_one(OrganizationSetting.HOMEPAGE_USER_NAME) ) + update_profile_header_form = UpdateProfileHeaderForm() status_code = 200 if request.method == "POST": @@ -147,6 +151,38 @@ def branding() -> Tuple[str, int]: status_code = 500 db.session.rollback() flash("There was an error and the setting could not reset") + elif ( + update_profile_header_form.submit.name in request.form + and update_profile_header_form.validate() + ): + if data := update_profile_header_form.template.data: + OrganizationSetting.upsert( + OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE, data + ) + db.session.commit() + flash("👍 Profile header template updated successfully") + else: + row_count = db.session.execute( + db.delete(OrganizationSetting).filter_by( + key=OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE + ) + ).rowcount + match row_count: + case 0: + flash("👍 Profile header template reset to default") + case 1: + db.session.commit() + flash("👍 Profile header template reset to default") + case _: + current_app.logger.error( + "Deleting OrganizationSetting " + + OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE + + " would have deleted multiple rows" + ) + status_code = 500 + db.session.rollback() + flash("There was an error and the setting could not reset") + return redirect_to_self() elif ( set_homepage_username_form.submit.name in request.form and set_homepage_username_form.validate() @@ -169,5 +205,6 @@ def branding() -> Tuple[str, int]: delete_brand_logo_form=delete_brand_logo_form, update_brand_primary_color_form=update_brand_primary_color_form, update_brand_app_name_form=update_brand_app_name_form, + update_profile_header_form=update_profile_header_form, set_homepage_username_form=set_homepage_username_form, ), status_code diff --git a/hushline/settings/common.py b/hushline/settings/common.py index 8851a522..8cd456e5 100644 --- a/hushline/settings/common.py +++ b/hushline/settings/common.py @@ -33,7 +33,6 @@ NewAliasForm, PGPKeyForm, ProfileForm, - UpdateProfileHeaderForm, ) from hushline.utils import redirect_to_self @@ -289,18 +288,3 @@ def handle_email_forwarding_form( db.session.commit() flash("👍 SMTP settings updated successfully") return redirect_to_self() - - -def handle_update_profile_header( - username: Username, - form: UpdateProfileHeaderForm, -) -> Response: - if form.template.data: - username.profile_header = form.template.data - msg = "👍 Profile header template updated successfully" - else: - username.profile_header = None - msg = "👍 Profile header template reset" - db.session.commit() - flash(msg) - return redirect_to_self() diff --git a/hushline/settings/profile.py b/hushline/settings/profile.py index 8f02dc78..4fbdf10d 100644 --- a/hushline/settings/profile.py +++ b/hushline/settings/profile.py @@ -19,13 +19,11 @@ handle_display_name_form, handle_update_bio, handle_update_directory_visibility, - handle_update_profile_header, ) from hushline.settings.forms import ( DirectoryVisibilityForm, DisplayNameForm, ProfileForm, - UpdateProfileHeaderForm, ) @@ -54,7 +52,6 @@ async def profile() -> Response | Tuple[str, int]: for i in range(1, 5) }, ) - update_profile_header_form = UpdateProfileHeaderForm(template=username.profile_header) status_code = 200 if request.method == "POST": @@ -67,11 +64,6 @@ async def profile() -> Response | Tuple[str, int]: return handle_update_directory_visibility(username, directory_visibility_form) elif profile_form.submit.name in request.form and profile_form.validate(): return await handle_update_bio(username, profile_form) - elif ( - update_profile_header_form.submit.name in request.form - and update_profile_header_form.validate() - ): - return handle_update_profile_header(username, update_profile_header_form) else: form_error() status_code = 400 @@ -93,5 +85,4 @@ async def profile() -> Response | Tuple[str, int]: directory_visibility_form=directory_visibility_form, profile_form=profile_form, business_tier_display_price=business_tier_display_price, - update_profile_header_form=update_profile_header_form, ), status_code diff --git a/hushline/templates/settings/alias.html b/hushline/templates/settings/alias.html index d4022f1c..5e0d5507 100644 --- a/hushline/templates/settings/alias.html +++ b/hushline/templates/settings/alias.html @@ -54,8 +54,6 @@

Public User Directory

- {% include "settings/update_profile_header.html" %} -

Add Your Bio

Logo {% endif %}
+

Profile Header

+
+ {{ update_profile_header_form.hidden_tag() }} +

Set a custom header for profile pages that makes sense for your community. Here are a few examples of what you can do:

+ + {{ update_profile_header_form.template.label }} + {{ update_profile_header_form.template(autocomplete="off") }} + {% if update_profile_header_form.template.errors %} +
+ {% for error in update_profile_header_form.template.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {{ update_profile_header_form.submit }} +
+

Homepage

Public User Directory {{ directory_visibility_form.submit }}
- {% include "settings/update_profile_header.html" %} -

Add Your Bio

Profile Header - - {{ update_profile_header_form.hidden_tag() }} -

Set a custom header for your profile page that makes sense for your community. Here are a few examples of what you can do:

- - {{ update_profile_header_form.template.label }} - {% with placeholder = "Submit message to {{ display_name_or_username }}" %} - {{ update_profile_header_form.template(placeholder=placeholder, autocomplete="off") }} - {% endwith %} - {% if update_profile_header_form.template.errors %} -
- {% for error in update_profile_header_form.template.errors %} - {{ error }} - {% endfor %} -
- {% endif %} - {{ update_profile_header_form.submit }} -
diff --git a/migrations/versions/0a6cdf5c62f8_add_profile_header_to_usernames.py b/migrations/versions/0a6cdf5c62f8_add_profile_header_to_usernames.py deleted file mode 100644 index f78dab67..00000000 --- a/migrations/versions/0a6cdf5c62f8_add_profile_header_to_usernames.py +++ /dev/null @@ -1,26 +0,0 @@ -"""add profile_header to usernames - -Revision ID: 0a6cdf5c62f8 -Revises: cf2a880aff10 -Create Date: 2025-01-14 15:08:02.855886 - -""" - -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = "0a6cdf5c62f8" -down_revision = "cf2a880aff10" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - with op.batch_alter_table("usernames", schema=None) as batch_op: - batch_op.add_column(sa.Column("profile_header", sa.Text(), nullable=True)) - - -def downgrade() -> None: - with op.batch_alter_table("usernames", schema=None) as batch_op: - batch_op.drop_column("profile_header") diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 3d0fd8b8..d2e8f6ea 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -25,7 +25,6 @@ SKIPPABLE_REVISIONS = [ "5ffe5a5c8e9a", # only renames indices and tables, no data changed "06b343c38386", # only renames indices and tables, no data changed - "0a6cdf5c62f8", # adds/removes a nullable column, no test necessary ] diff --git a/tests/test_profile.py b/tests/test_profile.py index 7f17b788..98486f75 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -6,7 +6,7 @@ from flask.testing import FlaskClient from hushline.db import db -from hushline.model import Message, User, Username +from hushline.model import Message, OrganizationSetting, User, Username def get_captcha_from_session(client: FlaskClient, username: str) -> str: @@ -21,7 +21,14 @@ def get_captcha_from_session(client: FlaskClient, username: str) -> str: def test_profile_header(client: FlaskClient, user: User) -> None: - assert not user.primary_username.profile_header # precondition + assert ( + db.session.scalars( + db.select(OrganizationSetting).filter_by( + key=OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE + ) + ).one_or_none() + is None + ) # precondition resp = client.get(url_for("profile", username=user.primary_username.username)) assert resp.status_code == 200 @@ -33,7 +40,7 @@ def test_profile_header(client: FlaskClient, user: User) -> None: rand = str(uuid4()) template = rand + " {{ display_name_or_username }} {{ username }} {{ display_name }}" - user.primary_username.profile_header = template + OrganizationSetting.upsert(OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE, template) db.session.commit() expected = ( diff --git a/tests/test_settings.py b/tests/test_settings.py index ab01bd73..aed5fc98 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1030,11 +1030,11 @@ def test_homepage_user(client: FlaskClient, user: User, admin: User) -> None: assert resp.headers["Location"] == url_for("directory") -@pytest.mark.usefixtures("_authenticated_user") -def test_update_profile_header(client: FlaskClient, user: User) -> None: +@pytest.mark.usefixtures("_authenticated_admin") +def test_update_profile_header(client: FlaskClient, admin: User) -> None: template_str = "{{ display_name_or_username }} {{ display_name }} {{ username }}" resp = client.post( - url_for("settings.profile"), + url_for("settings.branding"), data={ "template": template_str, UpdateProfileHeaderForm.submit.name: "", @@ -1043,12 +1043,13 @@ def test_update_profile_header(client: FlaskClient, user: User) -> None: ) assert resp.status_code == 200 assert "Profile header template updated successfully" in resp.text - - db.session.refresh(user.primary_username) - assert user.primary_username.profile_header == template_str + assert ( + OrganizationSetting.fetch_one(OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE) + == template_str + ) resp = client.post( - url_for("settings.profile"), + url_for("settings.branding"), data={ "template": "{{ INVALID SYNAX AHHHH !!!! }}", UpdateProfileHeaderForm.submit.name: "", @@ -1057,6 +1058,26 @@ def test_update_profile_header(client: FlaskClient, user: User) -> None: ) assert resp.status_code == 400 assert "Your submitted form could not be processed" in resp.text + assert ( + OrganizationSetting.fetch_one(OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE) + == template_str + ) - db.session.refresh(user.primary_username) - assert user.primary_username.profile_header == template_str + resp = client.post( + url_for("settings.branding"), + data={ + "template": "", + UpdateProfileHeaderForm.submit.name: "", + }, + follow_redirects=True, + ) + assert resp.status_code == 200 + assert "Profile header template reset to default" in resp.text + assert ( + db.session.scalars( + db.select(OrganizationSetting).filter_by( + key=OrganizationSetting.BRAND_PROFILE_HEADER_TEMPLATE + ) + ).one_or_none() + is None + )