Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ LOGFIRE_TOKEN=
LOGFIRE_CONSOLE_SHOW_PROJECT_LINK=False

SENTRY_DSN=

# Optional product analytics
POSTHOG_KEY=
POSTHOG_HOST=https://us.i.posthog.com
6 changes: 4 additions & 2 deletions agent_commons/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
)

SENTRY_DSN = env("SENTRY_DSN", default="")
POSTHOG_KEY = env("POSTHOG_KEY", default="")
POSTHOG_HOST = env("POSTHOG_HOST", default="https://us.i.posthog.com")


# Quick-start development settings - unsuitable for production
Expand Down Expand Up @@ -136,6 +138,7 @@


"apps.core.context_processors.available_social_providers",
"apps.core.context_processors.analytics_settings",
"apps.pages.context_processors.referrer_banner",
],
},
Expand Down Expand Up @@ -277,9 +280,8 @@
ACCOUNT_LOGOUT_REDIRECT_URL = "landing"

ACCOUNT_USER_MODEL_USERNAME_FIELD = "username"
ACCOUNT_LOGIN_METHODS = {'username'}
ACCOUNT_LOGIN_METHODS = {"username", "email"}
ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"]
ACCOUNT_LOGIN_METHODS = {"username"}
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_EMAIL_SUBJECT_PREFIX = ""
Expand Down
11 changes: 6 additions & 5 deletions apps/core/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from allauth.socialaccount.models import SocialApp
from django.conf import settings

from apps.core.choices import ProfileStates

from agent_commons.utils import get_agent_commons_logger
from apps.core.choices import ProfileStates

logger = get_agent_commons_logger(__name__)

Expand All @@ -14,9 +13,11 @@ def current_state(request):
return {"current_state": ProfileStates.STRANGER}





def analytics_settings(request):
return {
"posthog_key": getattr(settings, "POSTHOG_KEY", ""),
"posthog_host": getattr(settings, "POSTHOG_HOST", "https://us.i.posthog.com"),
}


def available_social_providers(request):
Expand Down
67 changes: 64 additions & 3 deletions apps/core/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from pathlib import Path

import pytest
from allauth.account.models import EmailAddress
from django.contrib.auth.models import User
from django.test import RequestFactory, override_settings
from django.urls import reverse

from apps.api.models import AgentInstallation
from apps.core.context_processors import analytics_settings


@pytest.mark.django_db
Expand Down Expand Up @@ -76,6 +80,50 @@ def test_rotate_api_key_rotates_key_when_verified(self, auth_client, user):
assert response.status_code == 200


@pytest.mark.django_db
class TestLoginView:
def test_login_allows_username_identifier(self, client, user):
response = client.post(
reverse("account_login"),
{"login": user.username, "password": "password123"},
)

assert response.status_code == 302
assert response.url == reverse("home")
assert client.session.get("_auth_user_id") == str(user.id)

def test_login_allows_email_identifier(self, client, user):
response = client.post(
reverse("account_login"),
{"login": user.email, "password": "password123"},
)

assert response.status_code == 302
assert response.url == reverse("home")
assert client.session.get("_auth_user_id") == str(user.id)


class TestAnalyticsScripts:
@override_settings(POSTHOG_KEY="phc_test_key", POSTHOG_HOST="https://eu.i.posthog.com")
def test_analytics_context_processor_exposes_posthog_settings(self):
request = RequestFactory().get("/")

context = analytics_settings(request)

assert context["posthog_key"] == "phc_test_key"
assert context["posthog_host"] == "https://eu.i.posthog.com"

def test_base_templates_include_posthog_snippet(self):
templates_root = Path(__file__).resolve().parents[3] / "frontend" / "templates"
base_landing_template = (templates_root / "base_landing.html").read_text(encoding="utf-8")
base_app_template = (templates_root / "base_app.html").read_text(encoding="utf-8")

for template in (base_landing_template, base_app_template):
assert "{% if posthog_key %}" in template
assert "posthog.init" in template
assert "{{ posthog_host|escapejs }}" in template


@pytest.mark.django_db
class TestAgentInstallationDashboard:
def test_create_agent_installation_succeeds_when_email_verified(self, auth_client, user):
Expand All @@ -92,7 +140,11 @@ def test_create_agent_installation_succeeds_when_email_verified(self, auth_clien
)

assert response.status_code == 200
installation = AgentInstallation.objects.get(profile=user.profile, agent_name="Forge", platform="openclaw")
installation = AgentInstallation.objects.get(
profile=user.profile,
agent_name="Forge",
platform="openclaw",
)
assert installation.agent_version == "v1"

def test_create_agent_installation_blocked_when_email_not_verified(self, auth_client, user):
Expand All @@ -107,7 +159,9 @@ def test_create_agent_installation_blocked_when_email_not_verified(self, auth_cl
assert response.status_code == 200
assert not AgentInstallation.objects.filter(profile=user.profile).exists()

def test_create_agent_installation_rejects_duplicate_name_platform_for_owner(self, auth_client, user):
def test_create_agent_installation_rejects_duplicate_name_platform_for_owner(
self, auth_client, user
):
EmailAddress.objects.create(user=user, email=user.email, verified=True, primary=True)
AgentInstallation.objects.create(
profile=user.profile,
Expand All @@ -122,7 +176,14 @@ def test_create_agent_installation_rejects_duplicate_name_platform_for_owner(sel
)

assert response.status_code == 200
assert AgentInstallation.objects.filter(profile=user.profile, agent_name="Forge", platform="openclaw").count() == 1
assert (
AgentInstallation.objects.filter(
profile=user.profile,
agent_name="Forge",
platform="openclaw",
).count()
== 1
)


@pytest.mark.django_db
Expand Down
10 changes: 10 additions & 0 deletions apps/pages/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,16 @@ def test_landing_page_template_includes_link_to_questions_list(self):
self.assertIn("{% url 'questions_list' %}", landing_template)
self.assertIn("Browse all questions", landing_template)

def test_base_templates_include_questions_link_in_navigation(self):
templates_root = Path(__file__).resolve().parents[2] / "frontend" / "templates"
base_landing_template = (templates_root / "base_landing.html").read_text(encoding="utf-8")
base_app_template = (templates_root / "base_app.html").read_text(encoding="utf-8")

self.assertIn("{% url 'questions_list' %}", base_landing_template)
self.assertIn("Questions", base_landing_template)
self.assertIn("{% url 'questions_list' %}", base_app_template)
self.assertIn("Questions", base_app_template)

def test_skill_markdown_endpoint(self):
response = self.client.get("/skill.md")

Expand Down
4 changes: 2 additions & 2 deletions frontend/templates/account/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ <h2 class="mt-6 text-3xl font-extrabold text-center text-gray-900 dark:text-gray
<div class="-space-y-px rounded-md shadow-sm">
<div>
{{ form.login.errors | safe }}
<label for="username" class="sr-only">Username</label>
{% render_field form.login placeholder="Username" id="username" name="username" type="text" autocomplete="username" required=True class="block relative px-3 py-2 w-full placeholder-gray-500 text-gray-900 bg-white dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-400 rounded-none rounded-t-md border border-gray-300 dark:border-gray-700 appearance-none focus:outline-none focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 dark:focus:border-indigo-400 focus:z-10 sm:text-sm" %}
<label for="login" class="sr-only">Email or username</label>
{% render_field form.login placeholder="Email or username" id="login" type="text" autocomplete="username" required=True class="block relative px-3 py-2 w-full placeholder-gray-500 text-gray-900 bg-white dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-400 rounded-none rounded-t-md border border-gray-300 dark:border-gray-700 appearance-none focus:outline-none focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 dark:focus:border-indigo-400 focus:z-10 sm:text-sm" %}
</div>
<div>
{{ form.password.errors | safe }}
Expand Down
12 changes: 12 additions & 0 deletions frontend/templates/base_app.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@
}
</script>

{% if posthog_key %}
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures onSessionId getSessionId get_distinct_id getGroups setPersonProperties setPersonPropertiesForFlags identify setGroup group identify get_property alias $set $set_once unset increment append remove track pageview capture".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init("{{ posthog_key|escapejs }}", {
api_host: "{{ posthog_host|escapejs }}",
person_profiles: "identified_only",
capture_pageview: true,
capture_pageleave: true
});
</script>
{% endif %}

{% stylesheet_pack 'index' %}
{% javascript_pack 'index' attrs='defer' %}
</head>
Expand Down
12 changes: 12 additions & 0 deletions frontend/templates/base_landing.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@
}
</script>

{% if posthog_key %}
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures onSessionId getSessionId get_distinct_id getGroups setPersonProperties setPersonPropertiesForFlags identify setGroup group identify get_property alias $set $set_once unset increment append remove track pageview capture".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init("{{ posthog_key|escapejs }}", {
api_host: "{{ posthog_host|escapejs }}",
person_profiles: "identified_only",
capture_pageview: true,
capture_pageleave: true
});
</script>
{% endif %}

{% stylesheet_pack 'index' %}
{% javascript_pack 'index' attrs='defer' %}
</head>
Expand Down