From 5147aaf358d4d57ca40a46b228c8dfe02435476a Mon Sep 17 00:00:00 2001 From: renzon Date: Fri, 21 Feb 2020 10:06:05 -0300 Subject: [PATCH 1/2] Sanitized email on register forms Missing existing email sanitizing part of #1694 --- pythonpro/conftest.py | 2 + pythonpro/core/forms.py | 14 ++++++- .../core/tests/test_lead_landing_page.py | 42 ++++++++++++++++++- pythonpro/core/tests/test_view_profile.py | 22 ++++++++++ pythonpro/core/views.py | 4 +- pythonpro/launch/forms.py | 14 +++++++ .../{test_form_lead.py => test_lead_form.py} | 18 +++++++- 7 files changed, 109 insertions(+), 7 deletions(-) rename pythonpro/launch/tests/{test_form_lead.py => test_lead_form.py} (79%) diff --git a/pythonpro/conftest.py b/pythonpro/conftest.py index 228fd09b..30ac7d22 100644 --- a/pythonpro/conftest.py +++ b/pythonpro/conftest.py @@ -20,6 +20,8 @@ def client_with_user(client, django_user_model, logged_user): @pytest.fixture def logged_user(django_user_model): logged_user = mommy.make(django_user_model) + logged_user.email = logged_user.email.lower() + logged_user.save() return logged_user diff --git a/pythonpro/core/forms.py b/pythonpro/core/forms.py index 023561a1..a26fc7ec 100644 --- a/pythonpro/core/forms.py +++ b/pythonpro/core/forms.py @@ -9,7 +9,15 @@ from pythonpro.core.models import User -class UserEmailForm(ModelForm): +class NormalizeEmailMixin: + def _normalize_email(self): + self.data = dict(self.data.items()) + email = self.data.get('email') + if email is not None: + self.data['email'] = email.lower() + + +class UserEmailForm(ModelForm, NormalizeEmailMixin): current_password = CharField(label=_("Password"), strip=False, required=True) class Meta: @@ -19,6 +27,7 @@ class Meta: def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') super().__init__(*args, **kwargs) + self._normalize_email() def clean(self): cleaned_data = super().clean() @@ -27,7 +36,7 @@ def clean(self): return cleaned_data -class UserSignupForm(UserCreationForm): +class UserSignupForm(UserCreationForm, NormalizeEmailMixin): class Meta: model = User fields = ('first_name', 'email', 'source') @@ -44,6 +53,7 @@ def __init__(self, *args, **kwargs): self._set_passwords(dct) args = (ChainMap(query_dict, dct), *args[1:]) super().__init__(*args, **kwargs) + self._normalize_email() def _set_passwords(self, data): if 'password1' not in data and 'password2' not in data: diff --git a/pythonpro/core/tests/test_lead_landing_page.py b/pythonpro/core/tests/test_lead_landing_page.py index 78bdc91f..38e09462 100644 --- a/pythonpro/core/tests/test_lead_landing_page.py +++ b/pythonpro/core/tests/test_lead_landing_page.py @@ -32,18 +32,56 @@ def create_lead_mock(mocker): return mocker.patch('pythonpro.domain.user_facade._email_marketing_facade.create_or_update_lead') +@pytest.fixture() +def email(fake): + return fake.email() + + @pytest.fixture -def resp_lead_creation(client, db, fake: Faker, create_lead_mock): +def resp_lead_creation(client, db, fake: Faker, create_lead_mock, email): return client.post( reverse('core:lead_form') + '?utm_source=facebook', data={ 'first_name': fake.name(), - 'email': fake.email(), + 'email': email, }, secure=True ) +def test_email_error_subscribing_with_email_variation(resp_lead_creation, email: str, fake, client): + email_upercase = email.upper() + resp = client.post( + reverse('core:lead_form') + '?utm_source=facebook', + data={ + 'first_name': fake.name(), + 'email': email_upercase, + }, + secure=True + ) + + assert resp.status_code == 400 + + +@pytest.fixture +def resp_email_upper_case(client, db, fake: Faker, create_lead_mock, email): + email = email.upper() + return client.post( + reverse('core:lead_form') + '?utm_source=facebook', + data={ + 'first_name': fake.name(), + 'email': email, + }, + secure=True + ) + + +def test_email_normalization(resp_email_upper_case, email, django_user_model): + email_lower = email.lower() + user = django_user_model.objects.first() + assert user.email == email_lower + + @pytest.fixture def resp_lead_change_pasword(resp_lead_creation, client): client.post( diff --git a/pythonpro/core/tests/test_view_profile.py b/pythonpro/core/tests/test_view_profile.py index 12e0471f..c556729f 100644 --- a/pythonpro/core/tests/test_view_profile.py +++ b/pythonpro/core/tests/test_view_profile.py @@ -54,3 +54,25 @@ def test_edit_email_link(resp_with_user): def test_edit_password_link(resp_with_user): dj_assert_contains(resp_with_user, reverse('core:profile_password')) + + +@pytest.fixture +def user_with_plain_password(django_user_model): + plain_password = 'senha' + u = mommy.make(django_user_model) + u.set_password(plain_password) + u.plain_password = plain_password + u.save() + return u + + +def test_email_normalization(user_with_plain_password, client, django_user_model): + client.force_login(user_with_plain_password) + email = 'ALUNO@PYTHON.PRO.BR' + client.post( + reverse('core:profile_email'), + {'email': email, 'current_password': user_with_plain_password.plain_password}, + secure=True + ) + saved_user = django_user_model.objects.first() + assert saved_user.email == email.lower() diff --git a/pythonpro/core/views.py b/pythonpro/core/views.py index 623bd5ce..1b11a2c4 100644 --- a/pythonpro/core/views.py +++ b/pythonpro/core/views.py @@ -9,7 +9,7 @@ from django_sitemaps import Sitemap from rolepermissions.checkers import has_role -from pythonpro.core.forms import UserEmailForm, UserSignupForm, LeadForm +from pythonpro.core.forms import LeadForm, UserEmailForm, UserSignupForm from pythonpro.core.models import User from pythonpro.domain import user_facade @@ -158,6 +158,6 @@ def lead_form(request): try: user = user_facade.register_lead(first_name, email, source) except user_facade.UserCreationException as e: - return render(request, 'core/lead_form_errors.html', context={'form': e.form}) + return render(request, 'core/lead_form_errors.html', context={'form': e.form}, status=400) login(request, user) return redirect(reverse('payments:client_landing_page_oto')) diff --git a/pythonpro/launch/forms.py b/pythonpro/launch/forms.py index 7a618a38..b329fdb0 100644 --- a/pythonpro/launch/forms.py +++ b/pythonpro/launch/forms.py @@ -1,5 +1,19 @@ from django import forms +from django.forms.utils import ErrorList class LeadForm(forms.Form): email = forms.EmailField(required=True) + + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, + label_suffix=None, empty_permitted=False, field_order=None, use_required_attribute=None, + renderer=None): + super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, field_order, + use_required_attribute, renderer) + self._normalize_email() + + def _normalize_email(self): + self.data = dict(self.data.items()) + email = self.data.get('email') + if email is not None: + self.data['email'] = email.lower() diff --git a/pythonpro/launch/tests/test_form_lead.py b/pythonpro/launch/tests/test_lead_form.py similarity index 79% rename from pythonpro/launch/tests/test_form_lead.py rename to pythonpro/launch/tests/test_lead_form.py index 564b852d..85a7bd27 100644 --- a/pythonpro/launch/tests/test_form_lead.py +++ b/pythonpro/launch/tests/test_lead_form.py @@ -6,7 +6,12 @@ @pytest.fixture def email(fake): - return fake.email() + return fake.email().lower() + + +@pytest.fixture +def email_upper(email): + return email.upper() @pytest.fixture @@ -19,6 +24,11 @@ def resp(client, email, create_or_update_with_no_role, cohort): return client.post(reverse('launch:lead_form'), {'email': email}, secure=True) +@pytest.fixture +def resp_email_upper(client, email_upper, create_or_update_with_no_role, cohort): + return client.post(reverse('launch:lead_form'), {'email': email_upper}, secure=True) + + @pytest.fixture def invalid_email(email): return f'@{email}' @@ -34,6 +44,12 @@ def test_email_marketing_sucess_integration(resp, email, create_or_update_with_n f'turma-{cohort.slug}-semana-do-programador') +def test_email_normalization(resp_email_upper, email, create_or_update_with_no_role, cohort): + first_name = email.split('@')[0] + create_or_update_with_no_role.assert_called_once_with(first_name, email, + f'turma-{cohort.slug}-semana-do-programador') + + @pytest.fixture def resp_with_error(client, invalid_email, create_or_update_with_no_role): return client.post(reverse('launch:lead_form'), {'email': invalid_email}, secure=True) From 3565ca008de26ce61e09660fb707c86f4b011535 Mon Sep 17 00:00:00 2001 From: Moacir Moda Date: Thu, 27 Feb 2020 09:46:01 -0300 Subject: [PATCH 2/2] creating linktree page to add in instagram bio. Closes #2066 --- pythonpro/core/static/img/renzo-thumb.jpg | Bin 0 -> 6398 bytes pythonpro/core/templates/core/base.html | 2 +- pythonpro/core/templates/core/linktree.html | 133 ++++++++++++++++++++ pythonpro/core/tests/test_linktree.py | 11 ++ pythonpro/core/urls.py | 1 + pythonpro/core/views.py | 4 + 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 pythonpro/core/static/img/renzo-thumb.jpg create mode 100644 pythonpro/core/templates/core/linktree.html create mode 100644 pythonpro/core/tests/test_linktree.py diff --git a/pythonpro/core/static/img/renzo-thumb.jpg b/pythonpro/core/static/img/renzo-thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..522c1093880981d7824c93909181f18f05c3c4af GIT binary patch literal 6398 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 ANB{r; literal 0 HcmV?d00001 diff --git a/pythonpro/core/templates/core/base.html b/pythonpro/core/templates/core/base.html index 16aa3819..19f9bb6f 100644 --- a/pythonpro/core/templates/core/base.html +++ b/pythonpro/core/templates/core/base.html @@ -37,7 +37,7 @@ {% block head %}{% endblock %} - + {# Google Tag Manager (noscript)#} diff --git a/pythonpro/core/templates/core/linktree.html b/pythonpro/core/templates/core/linktree.html new file mode 100644 index 00000000..fc60a744 --- /dev/null +++ b/pythonpro/core/templates/core/linktree.html @@ -0,0 +1,133 @@ +{% extends 'core/base_without_nav.html' %} +{% load static %} +{% block title %}Linktree Python Pro{% endblock %} + +{% block body_class %}bg-primary{% endblock %} + +{% block styles %}{{block.super}} + + + +{% endblock %} + +{% block body %} +
+
+
+ +
+
+ +
+
+

@renzoprobr

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+{% endblock body %} + +{% block footer %}{% endblock %} \ No newline at end of file diff --git a/pythonpro/core/tests/test_linktree.py b/pythonpro/core/tests/test_linktree.py new file mode 100644 index 00000000..aa9071c5 --- /dev/null +++ b/pythonpro/core/tests/test_linktree.py @@ -0,0 +1,11 @@ +import pytest +from django.urls import reverse + + +@pytest.fixture +def resp(client): + return client.get(reverse('core:linktree'), secure=True) + + +def test_status_code(resp): + assert 200 == resp.status_code diff --git a/pythonpro/core/urls.py b/pythonpro/core/urls.py index 5b9087f8..3af4edf2 100644 --- a/pythonpro/core/urls.py +++ b/pythonpro/core/urls.py @@ -9,6 +9,7 @@ path('sitemap.xml', views.sitemap, name='sitemap'), path('robots.txt', robots_txt(timeout=86400), name='robots'), path('tech-talks', views.teck_talks, name='tech_talks'), + path('linktree', views.linktree, name='linktree'), path('podcast', views.podcast, name='podcast'), path('curso-de-python-gratis', views.lead_landing, name='lead_landing'), path('curso-de-python-gratis-lite', views.lead_landing_lite, name='lead_landing_lite'), diff --git a/pythonpro/core/views.py b/pythonpro/core/views.py index 1b11a2c4..580e0313 100644 --- a/pythonpro/core/views.py +++ b/pythonpro/core/views.py @@ -161,3 +161,7 @@ def lead_form(request): return render(request, 'core/lead_form_errors.html', context={'form': e.form}, status=400) login(request, user) return redirect(reverse('payments:client_landing_page_oto')) + + +def linktree(request): + return render(request, 'core/linktree.html', {})